Coverage for src/ipyvizzu/animation.py: 100%

159 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-12 08:13 +0000

1"""A module for working with chart animations.""" 

2 

3import abc 

4import json 

5from os import PathLike 

6import sys 

7from typing import List, Optional, Tuple, Type, Union 

8import warnings 

9 

10import jsonschema # type: ignore 

11 

12from ipyvizzu.data.converters.defaults import NAN_DIMENSION, NAN_MEASURE 

13from ipyvizzu.data.converters.df.defaults import MAX_ROWS 

14from ipyvizzu.data.converters.numpy import ColumnDtype, ColumnName, NumpyArrayConverter 

15from ipyvizzu.data.converters.pandas import PandasDataFrameConverter 

16from ipyvizzu.data.converters.spark import SparkDataFrame, SparkDataFrameConverter 

17from ipyvizzu.data.type_alias import ( 

18 DimensionValue, 

19 NestedMeasureValues, 

20 MeasureValue, 

21 Record, 

22 Series, 

23 SeriesValues, 

24) 

25from ipyvizzu.json import RawJavaScript, RawJavaScriptEncoder 

26from ipyvizzu.schema import DATA_SCHEMA 

27 

28 

29class AbstractAnimation: 

30 """ 

31 An abstract class for representing animation objects 

32 that have `dump` and `build` methods. 

33 """ 

34 

35 def dump(self) -> str: 

36 """ 

37 A method for converting the built dictionary into string. 

38 

39 Returns: 

40 An str that has been json dumped with 

41 [RawJavaScriptEncoder][ipyvizzu.json.RawJavaScriptEncoder] from a dictionary. 

42 """ 

43 

44 return json.dumps(self.build(), cls=RawJavaScriptEncoder) 

45 

46 @abc.abstractmethod 

47 def build(self) -> dict: 

48 """ 

49 An abstract method for returning a dictionary with values 

50 that can be converted into json string. 

51 

52 Returns: 

53 A dictionary that stored in the animation object. 

54 """ 

55 

56 

57class PlainAnimation(dict, AbstractAnimation): 

58 """ 

59 A class for representing plain animation. 

60 It can build any dictionary. 

61 """ 

62 

63 def build(self) -> dict: 

64 """ 

65 A method for returning the plain animation dictionary. 

66 

67 Returns: 

68 A dictionary that stored in the plain animation object. 

69 """ 

70 

71 return self 

72 

73 

74class Data(dict, AbstractAnimation): 

75 """ 

76 A class for representing data animation. 

77 It can build data option of the chart. 

78 """ 

79 

80 @classmethod 

81 def filter(cls, filter_expr: Optional[str] = None) -> "Data": 

82 """ 

83 A class method for creating a [Data][ipyvizzu.animation.Data] 

84 class instance with a data filter. 

85 

86 Args: 

87 filter_expr: The JavaScript data filter expression. 

88 

89 Returns: 

90 (Data): A data animation instance that contains a data filter. 

91 

92 Example: 

93 Create a [Data][ipyvizzu.animation.Data] class with a data filter: 

94 

95 filter = Data.filter("record['Genres'] == 'Pop'") 

96 """ 

97 

98 data = cls() 

99 data.set_filter(filter_expr) 

100 return data 

101 

102 def set_filter(self, filter_expr: Optional[str] = None) -> None: 

103 """ 

104 A method for adding a filter to an existing 

105 [Data][ipyvizzu.animation.Data] class instance. 

106 

107 Args: 

108 filter_expr: The JavaScript data filter expression. 

109 

110 Example: 

111 Add a data filter to a [Data][ipyvizzu.animation.Data] class instance: 

112 

113 data = Data() 

114 data.set_filter("record['Genres'] == 'Pop'") 

115 """ 

116 

117 filter_expr_raw_js = ( 

118 RawJavaScript(f"record => {{ return ({' '.join(filter_expr.split())}) }}") 

119 if filter_expr is not None 

120 else filter_expr 

121 ) 

122 self.update({"filter": filter_expr_raw_js}) 

123 

124 @classmethod 

125 def from_json(cls, filename: Union[str, bytes, PathLike]) -> "Data": 

126 """ 

127 A method for returning a [Data][ipyvizzu.animation.Data] 

128 class instance which has been created from a json file. 

129 

130 Args: 

131 filename: The path of the data source json file. 

132 

133 Returns: 

134 (Data): A data animation instance that has been created from a json file. 

135 """ 

136 

137 with open(filename, "r", encoding="utf8") as file_desc: 

138 return cls(json.load(file_desc)) 

139 

140 def add_record(self, record: Record) -> None: 

141 """ 

142 A method for adding a record to an existing 

143 [Data][ipyvizzu.animation.Data] class instance. 

144 

145 Args: 

146 record: A list that contains data values. 

147 

148 Example: 

149 Adding a record to a [Data][ipyvizzu.animation.Data] class instance: 

150 

151 data = Data() 

152 record = ["Pop", "Hard", 114] 

153 data.add_record(record) 

154 """ 

155 

156 self._add_value("records", record) 

157 

158 def add_records(self, records: List[Record]) -> None: 

159 """ 

160 A method for adding records to an existing 

161 [Data][ipyvizzu.animation.Data] class instance. 

162 

163 Args: 

164 records: A list that contains data records. 

165 

166 Example: 

167 Adding records to a [Data][ipyvizzu.animation.Data] class instance: 

168 

169 data = Data() 

170 records = [ 

171 ["Pop", "Hard", 114], 

172 ["Rock", "Hard", 96], 

173 ["Pop", "Experimental", 127], 

174 ["Rock", "Experimental", 83], 

175 ] 

176 data.add_records(records) 

177 """ 

178 

179 list(map(self.add_record, records)) 

180 

181 def add_series( 

182 self, name: str, values: Optional[SeriesValues] = None, **kwargs 

183 ) -> None: 

184 """ 

185 A method for adding a series to an existing 

186 [Data][ipyvizzu.animation.Data] class instance. 

187 

188 Args: 

189 name: The name of the series. 

190 values: The data values of the series. 

191 **kwargs (Optional): 

192 Arbitrary keyword arguments. 

193 

194 For example infer type can be set with the `type` keywod argument. 

195 

196 Example: 

197 Adding a series without values to a [Data][ipyvizzu.animation.Data] class instance: 

198 

199 data = Data() 

200 data.add_series("Genres") 

201 

202 Adding a series without values and with infer type to 

203 a [Data][ipyvizzu.animation.Data] class instance: 

204 

205 data = Data() 

206 data.add_series("Kinds", type="dimension") 

207 

208 Adding a series with values to a [Data][ipyvizzu.animation.Data] class instance: 

209 

210 data = Data() 

211 data.add_series( 

212 "Popularity", [114, 96, 127, 83] 

213 ) 

214 """ 

215 

216 self._add_named_value("series", name, values, **kwargs) 

217 

218 def add_series_list(self, series: List[Series]) -> None: 

219 """ 

220 A method for adding list of series to an existing 

221 [Data][ipyvizzu.animation.Data] class instance. 

222 

223 Args: 

224 series: List of series. 

225 """ 

226 

227 if series: 

228 self.setdefault("series", []).extend(series) 

229 

230 def add_dimension( 

231 self, name: str, values: Optional[List[DimensionValue]] = None, **kwargs 

232 ) -> None: 

233 """ 

234 A method for adding a dimension to an existing 

235 [Data][ipyvizzu.animation.Data] class instance. 

236 

237 Args: 

238 name: The name of the dimension. 

239 values: The data values of the dimension. 

240 **kwargs (Optional): Arbitrary keyword arguments. 

241 

242 Example: 

243 Adding a dimension with values to a [Data][ipyvizzu.animation.Data] class instance: 

244 

245 data = Data() 

246 data.add_dimension("Genres", ["Pop", "Rock"]) 

247 """ 

248 

249 self._add_named_value("dimensions", name, values, **kwargs) 

250 

251 def add_measure( 

252 self, name: str, values: Optional[NestedMeasureValues] = None, **kwargs 

253 ) -> None: 

254 """ 

255 A method for adding a measure to an existing 

256 [Data][ipyvizzu.animation.Data] class instance. 

257 

258 Args: 

259 name: The name of the measure. 

260 values: The data values of the measure. 

261 **kwargs (Optional): Arbitrary keyword arguments. 

262 

263 Example: 

264 Adding a measure with values to a [Data][ipyvizzu.animation.Data] class instance: 

265 

266 data = Data() 

267 data.add_measure( 

268 "Popularity", 

269 [ 

270 [114, 96], 

271 [127, 83], 

272 ], 

273 ) 

274 """ 

275 

276 self._add_named_value("measures", name, values, **kwargs) 

277 

278 def add_df( 

279 self, 

280 df: Optional[ # type: ignore 

281 Union[ 

282 "pandas.DataFrame", 

283 "pandas.Series", 

284 "pyspark.sql.DataFrame", 

285 ] 

286 ], 

287 default_measure_value: MeasureValue = NAN_MEASURE, 

288 default_dimension_value: DimensionValue = NAN_DIMENSION, 

289 max_rows: int = MAX_ROWS, 

290 include_index: Optional[str] = None, 

291 ) -> None: 

292 """ 

293 Add a `pandas` `DataFrame`, `Series` or a `pyspark` `DataFrame` 

294 to an existing [Data][ipyvizzu.animation.Data] class instance. 

295 

296 Args: 

297 df: 

298 The `pandas` `DataFrame`, `Series` or the `pyspark` `DataFrame`to add. 

299 default_measure_value: 

300 The default measure value to fill empty values. Defaults to 0. 

301 default_dimension_value: 

302 The default dimension value to fill empty values. Defaults to an empty string. 

303 max_rows: 

304 The maximum number of rows to include in the converted series list. 

305 If the `df` contains more rows, 

306 a random sample of the given number of rows (approximately) will be taken. 

307 include_index: 

308 Add the data frame's index as a column with the given name. Defaults to `None`. 

309 (Cannot be used with `pyspark` `DataFrame`.) 

310 

311 Example: 

312 Adding a data frame to a [Data][ipyvizzu.animation.Data] class instance: 

313 

314 df = pd.DataFrame( 

315 { 

316 "Genres": ["Pop", "Rock", "Pop", "Rock"], 

317 "Kinds": ["Hard", "Hard", "Experimental", "Experimental"], 

318 "Popularity": [114, 96, 127, 83], 

319 } 

320 ) 

321 data = Data() 

322 data.add_df(df) 

323 """ 

324 

325 # pylint: disable=too-many-arguments 

326 

327 if not isinstance(df, type(None)): 

328 arguments = { 

329 "df": df, 

330 "default_measure_value": default_measure_value, 

331 "default_dimension_value": default_dimension_value, 

332 "max_rows": max_rows, 

333 "include_index": include_index, 

334 } 

335 Converter: Union[ 

336 Type[PandasDataFrameConverter], Type[SparkDataFrameConverter] 

337 ] = PandasDataFrameConverter 

338 if isinstance(df, SparkDataFrame): 

339 Converter = SparkDataFrameConverter 

340 if arguments["include_index"] is not None: 

341 raise ValueError( 

342 "`include_index` cannot be used with `pyspark` `DataFrame`" 

343 ) 

344 del arguments["include_index"] 

345 

346 converter = Converter(**arguments) # type: ignore 

347 series_list = converter.get_series_list() 

348 self.add_series_list(series_list) 

349 

350 def add_data_frame( 

351 self, 

352 data_frame: Optional[Union["pandas.DataFrame", "pandas.Series"]], # type: ignore 

353 default_measure_value: MeasureValue = NAN_MEASURE, 

354 default_dimension_value: DimensionValue = NAN_DIMENSION, 

355 ) -> None: 

356 """ 

357 [Deprecated] This function is deprecated and will be removed in future versions. 

358 Use [add_df][ipyvizzu.animation.Data.add_df] function instead. 

359 

360 Add a `pandas` `DataFrame` or `Series` to an existing 

361 [Data][ipyvizzu.animation.Data] class instance. 

362 

363 

364 

365 Args: 

366 data_frame: 

367 The `pandas` `DataFrame` or `Series` to add. 

368 default_measure_value: 

369 The default measure value to fill empty values. Defaults to 0. 

370 default_dimension_value: 

371 The default dimension value to fill empty values. Defaults to an empty string. 

372 """ 

373 

374 # pylint: disable=line-too-long 

375 

376 reference = "https://ipyvizzu.vizzuhq.com/0.16/reference/ipyvizzu/animation/#ipyvizzu.animation.Data.add_df" 

377 warnings.warn( 

378 f"'add_data_frame' is deprecated and will be removed in future versions. Use 'add_df' instead - see {reference}", 

379 DeprecationWarning, 

380 stacklevel=2, 

381 ) 

382 self.add_df( 

383 data_frame, 

384 default_measure_value, 

385 default_dimension_value, 

386 max_rows=sys.maxsize, 

387 ) 

388 

389 def add_df_index( 

390 self, 

391 df: Optional[Union["pandas.DataFrame", "pandas.Series"]], # type: ignore 

392 column_name: str = "Index", 

393 max_rows: int = MAX_ROWS, 

394 ) -> None: 

395 """ 

396 Add the index of a `pandas` `DataFrame` as a series to an existing 

397 [Data][ipyvizzu.animation.Data] class instance. 

398 

399 Args: 

400 df: 

401 The `pandas` `DataFrame` or `Series` from which to extract the index. 

402 column_name: 

403 Name for the index column to add as a series. 

404 max_rows: 

405 The maximum number of rows to include in the converted series list. 

406 If the `df` contains more rows, 

407 a random sample of the given number of rows (approximately) will be taken. 

408 

409 Example: 

410 Adding a data frame's index to a 

411 [Data][ipyvizzu.animation.Data] class instance: 

412 

413 df = pd.DataFrame( 

414 {"Popularity": [114, 96]}, 

415 index=["x", "y"] 

416 ) 

417 data = Data() 

418 data.add_df_index(df, "DataFrameIndex") 

419 data.add_df(df) 

420 """ 

421 

422 if not isinstance(df, type(None)): 

423 converter = PandasDataFrameConverter( 

424 df, max_rows=max_rows, include_index=column_name 

425 ) 

426 series_list = converter.get_series_from_index() 

427 self.add_series_list(series_list) 

428 

429 def add_data_frame_index( 

430 self, 

431 data_frame: Optional[Union["pandas.DataFrame", "pandas.Series"]], # type: ignore 

432 name: str, 

433 ) -> None: 

434 """ 

435 [Deprecated] This function is deprecated and will be removed in future versions. 

436 Use [add_df_index][ipyvizzu.animation.Data.add_df_index] function instead. 

437 

438 Add the index of a `pandas` `DataFrame` as a series to an existing 

439 [Data][ipyvizzu.animation.Data] class instance. 

440 

441 Args: 

442 data_frame: 

443 The `pandas` `DataFrame` or `Series` from which to extract the index. 

444 name: 

445 The name of the index series. 

446 """ 

447 

448 # pylint: disable=line-too-long 

449 

450 reference = "https://ipyvizzu.vizzuhq.com/0.16/reference/ipyvizzu/animation/#ipyvizzu.animation.Data.add_df_index" 

451 warnings.warn( 

452 f"'add_data_frame_index' is deprecated and will be removed in future versions. Use 'add_df_index' instead - see {reference}", 

453 DeprecationWarning, 

454 stacklevel=2, 

455 ) 

456 self.add_df_index(data_frame, column_name=name, max_rows=sys.maxsize) 

457 

458 def add_np_array( 

459 self, 

460 np_array: Optional["numpy.array"], # type: ignore 

461 column_name: Optional[ColumnName] = None, 

462 column_dtype: Optional[ColumnDtype] = None, 

463 default_measure_value: MeasureValue = NAN_MEASURE, 

464 default_dimension_value: DimensionValue = NAN_DIMENSION, 

465 ) -> None: 

466 """ 

467 Add a `numpy` `array` to an existing 

468 [Data][ipyvizzu.animation.Data] class instance. 

469 

470 Args: 

471 np_array: The `numpy` `array` to add. 

472 column_name: 

473 The name of a column. By default, uses column indices. Can be set with an 

474 Index:Name pair or, for single-dimensional arrays, with just the Name. 

475 column_dtype: 

476 The dtype of a column. By default, uses the np_array's dtype. Can be set 

477 with an Index:DType pair or, for single-dimensional arrays, with just the DType. 

478 default_measure_value: 

479 Default value to use for missing measure values. Defaults to 0. 

480 default_dimension_value: 

481 Default value to use for missing dimension values. Defaults to an empty string. 

482 

483 Example: 

484 Adding a data frame to a [Data][ipyvizzu.animation.Data] class instance: 

485 

486 np_array = np.zeros((3, 4)) 

487 data = Data() 

488 data.add_np_array(np_array) 

489 """ 

490 

491 # pylint: disable=too-many-arguments 

492 

493 if not isinstance(np_array, type(None)): 

494 converter = NumpyArrayConverter( 

495 np_array, 

496 column_name, 

497 column_dtype, 

498 default_measure_value, 

499 default_dimension_value, 

500 ) 

501 series_list = converter.get_series_list() 

502 self.add_series_list(series_list) 

503 

504 def _add_named_value( 

505 self, 

506 dest: str, 

507 name: str, 

508 values: Optional[ 

509 Union[ 

510 SeriesValues, 

511 NestedMeasureValues, 

512 ] 

513 ] = None, 

514 **kwargs, 

515 ) -> None: 

516 value = {"name": name, **kwargs} 

517 

518 if values is not None: 

519 value["values"] = values 

520 

521 self._add_value(dest, value) 

522 

523 def _add_value(self, dest: str, value: Union[dict, list]) -> None: 

524 self.setdefault(dest, []).append(value) 

525 

526 def build(self) -> dict: 

527 """ 

528 A method for validating and returning the data animation dictionary. 

529 

530 Returns: 

531 A dictionary that stored in the data animation object. 

532 It contains a `data` key whose value is the stored animation. 

533 """ 

534 

535 jsonschema.validate(self, DATA_SCHEMA) 

536 return {"data": self} 

537 

538 

539class ConfigAttr(type): 

540 """ 

541 A metaclass class for the [Config][ipyvizzu.animation.Config] class. 

542 Returns a [Config][ipyvizzu.animation.Config] class with a chart preset 

543 if the `__getattr__` method called. 

544 

545 For information on all available chart presets see the 

546 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/modules/Presets/#interfaces). 

547 """ 

548 

549 @classmethod 

550 def __getattr__(mcs, name): 

551 config_attr = mcs("ConfigAttr", (object,), {"name": name}) 

552 return config_attr._get_preset # pylint: disable=no-member 

553 

554 def _get_preset(cls, preset): 

555 config = Config(RawJavaScript(f"lib.presets.{cls.name}({preset})")) 

556 return config 

557 

558 

559class Config(AbstractAnimation, metaclass=ConfigAttr): 

560 """ 

561 A class for representing config animation. 

562 It can build config option of the chart. 

563 """ 

564 

565 def __init__(self, data: Optional[Union[dict, RawJavaScript]]): 

566 """ 

567 Config constructor. 

568 

569 Args: 

570 data: 

571 A config animation dictionary. 

572 For information on all available config parameters see the 

573 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/interfaces/Config.Chart/#properties). 

574 """ # pylint: disable=line-too-long 

575 

576 self._data = data 

577 

578 def build(self) -> dict: 

579 """ 

580 A method for returning the config animation dictionary. 

581 

582 Returns: 

583 A dictionary that stored in the config animation object. 

584 It contains a `config` key whose value is the stored animation. 

585 """ 

586 

587 return {"config": self._data} 

588 

589 

590class Style(AbstractAnimation): 

591 """ 

592 A class for representing style animation. 

593 It can build style option of the chart. 

594 """ 

595 

596 def __init__(self, data: Optional[dict]): 

597 """ 

598 Style constructor. 

599 

600 Args: 

601 data: 

602 A style animation dictionary. 

603 For information on all available style parameters see the [Style][styling-properties] 

604 chapter or the 

605 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/interfaces/Styles.Chart/#properties). 

606 """ # pylint: disable=line-too-long 

607 

608 self._data = data 

609 

610 def build(self) -> dict: 

611 """ 

612 A method for returning the style animation dictionary. 

613 

614 Returns: 

615 A dictionary that stored in the style animation object. 

616 It contains a `style` key whose value is the stored animation. 

617 """ 

618 

619 return {"style": self._data} 

620 

621 

622class Keyframe(AbstractAnimation): 

623 """ 

624 A class for representing keyframe animation. 

625 It can build keyframe of the chart. 

626 """ 

627 

628 def __init__( 

629 self, 

630 *animations: AbstractAnimation, 

631 **options: Optional[Union[str, int, float, dict]], 

632 ): 

633 """ 

634 Keyframe constructor. 

635 

636 Args: 

637 *animations: 

638 List of AbstractAnimation inherited objects such as [Data][ipyvizzu.animation.Data], 

639 [Config][ipyvizzu.animation.Config] and [Style][ipyvizzu.animation.Style]. 

640 **options: Dictionary of animation options for example `duration=1`. 

641 For information on all available animation options see the 

642 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/interfaces/Anim.Options/#properties). 

643 

644 Raises: 

645 ValueError: If `animations` is not set. 

646 ValueError: If initialized with a `Keyframe`. 

647 """ # pylint: disable=line-too-long 

648 

649 if not animations: 

650 raise ValueError("No animation was set.") 

651 if [animation for animation in animations if isinstance(animation, Keyframe)]: 

652 raise ValueError("A Keyframe cannot contain a Keyframe.") 

653 

654 self._keyframe = {} 

655 self._keyframe["target"] = AnimationMerger.merge_animations(animations).build() 

656 if options: 

657 self._keyframe["options"] = options 

658 

659 def build(self) -> dict: 

660 """ 

661 A method for returning the keyframe animation dictionary. 

662 

663 Returns: 

664 A dictionary that stored in the keyframe animation object. 

665 It contains a `target` key whose value is the stored animation 

666 and an optional `options` key whose value is the stored animation options. 

667 """ 

668 

669 return self._keyframe 

670 

671 

672class Snapshot(AbstractAnimation): 

673 """ 

674 A class for representing a stored chart state. 

675 It can build the snapshot id of the chart. 

676 """ 

677 

678 def __init__(self, snapshot_id: str): 

679 """ 

680 Snapshot constructor. 

681 

682 Args: 

683 snapshot_id: A snapshot id. 

684 """ 

685 

686 self._snapshot_id = snapshot_id 

687 

688 def build(self) -> str: # type: ignore 

689 """ 

690 A method for returning the snapshot id str. 

691 

692 Returns: 

693 An str snapshot id that stored in the snapshot animation object. 

694 """ 

695 

696 return self._snapshot_id 

697 

698 

699class Animation(Snapshot): 

700 """ 

701 A class for representing a stored animation. 

702 It can build the snapshot id of the animation. 

703 """ 

704 

705 

706class AnimationMerger(AbstractAnimation): 

707 """A class for merging different types of animations.""" 

708 

709 def __init__(self) -> None: 

710 """AnimationMerger constructor.""" 

711 

712 self._dict: dict = {} 

713 self._list: list = [] 

714 

715 @classmethod 

716 def merge_animations( 

717 cls, animations: Tuple[AbstractAnimation, ...] 

718 ) -> AbstractAnimation: 

719 """ 

720 A class method for merging animations. 

721 

722 Args: 

723 animations: List of `AbstractAnimation` inherited objects. 

724 

725 Returns: 

726 An `AnimationMerger` class with the merged animations. 

727 """ 

728 

729 if len(animations) == 1 and not isinstance(animations[0], Keyframe): 

730 return animations[0] 

731 

732 merger = cls() 

733 for animation in animations: 

734 merger.merge(animation) 

735 

736 return merger 

737 

738 def merge(self, animation: AbstractAnimation) -> None: 

739 """ 

740 A method for merging an animation with the previously merged animations. 

741 

742 Args: 

743 animation: An animation to be merged with with previously merged animations. 

744 

745 Raises: 

746 ValueError: If the type of an animation is already merged. 

747 ValueError: If `Keyframe` is merged with different type of animation. 

748 """ 

749 

750 if isinstance(animation, Keyframe): 

751 if self._dict: 

752 raise ValueError("Keyframe cannot be merged with other animations.") 

753 data = animation.build() 

754 self._list.append(data) 

755 else: 

756 if self._list: 

757 raise ValueError("Keyframe cannot be merged with other animations.") 

758 data = self._validate(animation) 

759 self._dict.update(data) 

760 

761 def _validate(self, animation: AbstractAnimation) -> dict: 

762 if isinstance(animation, Snapshot): 

763 raise ValueError("Snapshot cannot be merged with other animations.") 

764 data = animation.build() 

765 common_keys = set(data).intersection(self._dict) 

766 

767 if common_keys: 

768 raise ValueError(f"{common_keys} is already merged.") 

769 

770 return data 

771 

772 def build(self) -> Union[dict, list]: # type: ignore 

773 """ 

774 A method for returning a merged list of `Keyframes` 

775 or a merged dictionary from different types of animations. 

776 

777 Returns: 

778 A merged list of [Keyframes][ipyvizzu.animation.Keyframe] or 

779 a merged dictionary from 

780 [Data][ipyvizzu.animation.Data], 

781 [Config][ipyvizzu.animation.Config] and 

782 [Style][ipyvizzu.animation.Style] animations. 

783 """ 

784 

785 if self._dict: 

786 return self._dict 

787 return self._list