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

159 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-02-26 10:12 +0000

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

2 

3import abc 

4import json 

5from os import PathLike 

6import sys 

7from typing import Dict, 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 ( 

15 ColumnDtype, 

16 ColumnName, 

17 ColumnUnit, 

18 NumpyArrayConverter, 

19) 

20from ipyvizzu.data.converters.pandas import PandasDataFrameConverter 

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

22from ipyvizzu.data.type_alias import ( 

23 DimensionValue, 

24 NestedMeasureValues, 

25 MeasureValue, 

26 Record, 

27 Series, 

28 SeriesValues, 

29) 

30from ipyvizzu.json import RawJavaScript, RawJavaScriptEncoder 

31from ipyvizzu.schema import DATA_SCHEMA 

32 

33 

34class AbstractAnimation: 

35 """ 

36 An abstract class for representing animation objects 

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

38 """ 

39 

40 def dump(self) -> str: 

41 """ 

42 A method for converting the built dictionary into string. 

43 

44 Returns: 

45 An str that has been json dumped with 

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

47 """ 

48 

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

50 

51 @abc.abstractmethod 

52 def build(self) -> dict: 

53 """ 

54 An abstract method for returning a dictionary with values 

55 that can be converted into json string. 

56 

57 Returns: 

58 A dictionary that stored in the animation object. 

59 """ 

60 

61 

62class PlainAnimation(dict, AbstractAnimation): 

63 """ 

64 A class for representing plain animation. 

65 It can build any dictionary. 

66 """ 

67 

68 def build(self) -> dict: 

69 """ 

70 A method for returning the plain animation dictionary. 

71 

72 Returns: 

73 A dictionary that stored in the plain animation object. 

74 """ 

75 

76 return self 

77 

78 

79class Data(dict, AbstractAnimation): 

80 """ 

81 A class for representing data animation. 

82 It can build data option of the chart. 

83 """ 

84 

85 @classmethod 

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

87 """ 

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

89 class instance with a data filter. 

90 

91 Args: 

92 filter_expr: The JavaScript data filter expression. 

93 

94 Returns: 

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

96 

97 Example: 

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

99 

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

101 """ 

102 

103 data = cls() 

104 data.set_filter(filter_expr) 

105 return data 

106 

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

108 """ 

109 A method for adding a filter to an existing 

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

111 

112 Args: 

113 filter_expr: The JavaScript data filter expression. 

114 

115 Example: 

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

117 

118 data = Data() 

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

120 """ 

121 

122 filter_expr_raw_js = ( 

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

124 if filter_expr is not None 

125 else filter_expr 

126 ) 

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

128 

129 @classmethod 

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

131 """ 

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

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

134 

135 Args: 

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

137 

138 Returns: 

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

140 """ 

141 

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

143 return cls(json.load(file_desc)) 

144 

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

146 """ 

147 A method for adding a record to an existing 

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

149 

150 Args: 

151 record: A list that contains data values. 

152 

153 Example: 

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

155 

156 data = Data() 

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

158 data.add_record(record) 

159 """ 

160 

161 self._add_value("records", record) 

162 

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

164 """ 

165 A method for adding records to an existing 

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

167 

168 Args: 

169 records: A list that contains data records. 

170 

171 Example: 

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

173 

174 data = Data() 

175 records = [ 

176 ["Pop", "Hard", 114], 

177 ["Rock", "Hard", 96], 

178 ["Pop", "Experimental", 127], 

179 ["Rock", "Experimental", 83], 

180 ] 

181 data.add_records(records) 

182 """ 

183 

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

185 

186 def add_series( 

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

188 ) -> None: 

189 """ 

190 A method for adding a series to an existing 

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

192 

193 Args: 

194 name: The name of the series. 

195 values: The data values of the series. 

196 **kwargs (Optional): 

197 Arbitrary keyword arguments. 

198 

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

200 

201 Example: 

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

203 

204 data = Data() 

205 data.add_series("Genres") 

206 

207 Adding a series without values and with infer type to 

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

209 

210 data = Data() 

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

212 

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

214 

215 data = Data() 

216 data.add_series( 

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

218 ) 

219 """ 

220 

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

222 

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

224 """ 

225 A method for adding list of series to an existing 

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

227 

228 Args: 

229 series: List of series. 

230 """ 

231 

232 if series: 

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

234 

235 def add_dimension( 

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

237 ) -> None: 

238 """ 

239 A method for adding a dimension to an existing 

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

241 

242 Args: 

243 name: The name of the dimension. 

244 values: The data values of the dimension. 

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

246 

247 Example: 

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

249 

250 data = Data() 

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

252 """ 

253 

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

255 

256 def add_measure( 

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

258 ) -> None: 

259 """ 

260 A method for adding a measure to an existing 

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

262 

263 Args: 

264 name: The name of the measure. 

265 values: The data values of the measure. 

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

267 

268 Example: 

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

270 

271 data = Data() 

272 data.add_measure( 

273 "Popularity", 

274 [ 

275 [114, 96], 

276 [127, 83], 

277 ], 

278 ) 

279 """ 

280 

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

282 

283 def add_df( 

284 self, 

285 df: Optional[ # type: ignore 

286 Union[ 

287 "pandas.DataFrame", 

288 "pandas.Series", 

289 "pyspark.sql.DataFrame", 

290 ] 

291 ], 

292 default_measure_value: MeasureValue = NAN_MEASURE, 

293 default_dimension_value: DimensionValue = NAN_DIMENSION, 

294 max_rows: int = MAX_ROWS, 

295 include_index: Optional[str] = None, 

296 units: Optional[Dict[str, str]] = None, 

297 ) -> None: 

298 """ 

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

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

301 

302 Args: 

303 df: 

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

305 default_measure_value: 

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

307 default_dimension_value: 

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

309 max_rows: 

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

311 If the `df` contains more rows, 

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

313 include_index: 

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

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

316 units: 

317 A dictionary of column names and units. Defaults to `None`. 

318 

319 Example: 

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

321 

322 df = pd.DataFrame( 

323 { 

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

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

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

327 } 

328 ) 

329 data = Data() 

330 data.add_df(df) 

331 """ 

332 

333 # pylint: disable=too-many-arguments 

334 

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

336 arguments = { 

337 "df": df, 

338 "default_measure_value": default_measure_value, 

339 "default_dimension_value": default_dimension_value, 

340 "max_rows": max_rows, 

341 "include_index": include_index, 

342 "units": units, 

343 } 

344 Converter: Union[ 

345 Type[PandasDataFrameConverter], Type[SparkDataFrameConverter] 

346 ] = PandasDataFrameConverter 

347 if isinstance(df, SparkDataFrame): 

348 Converter = SparkDataFrameConverter 

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

350 raise ValueError( 

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

352 ) 

353 del arguments["include_index"] 

354 

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

356 series_list = converter.get_series_list() 

357 self.add_series_list(series_list) 

358 

359 def add_data_frame( 

360 self, 

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

362 default_measure_value: MeasureValue = NAN_MEASURE, 

363 default_dimension_value: DimensionValue = NAN_DIMENSION, 

364 ) -> None: 

365 """ 

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

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

368 

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

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

371 

372 

373 

374 Args: 

375 data_frame: 

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

377 default_measure_value: 

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

379 default_dimension_value: 

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

381 """ 

382 

383 # pylint: disable=line-too-long 

384 

385 reference = "https://ipyvizzu.vizzuhq.com/0.17/reference/ipyvizzu/animation/#ipyvizzu.animation.Data.add_df" 

386 warnings.warn( 

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

388 DeprecationWarning, 

389 stacklevel=2, 

390 ) 

391 self.add_df( 

392 data_frame, 

393 default_measure_value, 

394 default_dimension_value, 

395 max_rows=sys.maxsize, 

396 ) 

397 

398 def add_df_index( 

399 self, 

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

401 column_name: str = "Index", 

402 max_rows: int = MAX_ROWS, 

403 ) -> None: 

404 """ 

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

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

407 

408 Args: 

409 df: 

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

411 column_name: 

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

413 max_rows: 

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

415 If the `df` contains more rows, 

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

417 

418 Example: 

419 Adding a data frame's index to a 

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

421 

422 df = pd.DataFrame( 

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

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

425 ) 

426 data = Data() 

427 data.add_df_index(df, "DataFrameIndex") 

428 data.add_df(df) 

429 """ 

430 

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

432 converter = PandasDataFrameConverter( 

433 df, max_rows=max_rows, include_index=column_name 

434 ) 

435 series_list = converter.get_series_from_index() 

436 self.add_series_list(series_list) 

437 

438 def add_data_frame_index( 

439 self, 

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

441 name: str, 

442 ) -> None: 

443 """ 

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

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

446 

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

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

449 

450 Args: 

451 data_frame: 

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

453 name: 

454 The name of the index series. 

455 """ 

456 

457 # pylint: disable=line-too-long 

458 

459 reference = "https://ipyvizzu.vizzuhq.com/0.17/reference/ipyvizzu/animation/#ipyvizzu.animation.Data.add_df_index" 

460 warnings.warn( 

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

462 DeprecationWarning, 

463 stacklevel=2, 

464 ) 

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

466 

467 def add_np_array( 

468 self, 

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

470 column_name: Optional[ColumnName] = None, 

471 column_dtype: Optional[ColumnDtype] = None, 

472 column_unit: Optional[ColumnUnit] = None, 

473 default_measure_value: MeasureValue = NAN_MEASURE, 

474 default_dimension_value: DimensionValue = NAN_DIMENSION, 

475 ) -> None: 

476 """ 

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

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

479 

480 Args: 

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

482 column_name: 

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

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

485 column_dtype: 

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

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

488 default_measure_value: 

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

490 default_dimension_value: 

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

492 column_unit: 

493 The unit of a column. Defaults to `None`. 

494 

495 Example: 

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

497 

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

499 data = Data() 

500 data.add_np_array(np_array) 

501 """ 

502 

503 # pylint: disable=too-many-arguments 

504 

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

506 converter = NumpyArrayConverter( 

507 np_array, 

508 column_name, 

509 column_dtype, 

510 column_unit, 

511 default_measure_value, 

512 default_dimension_value, 

513 ) 

514 series_list = converter.get_series_list() 

515 self.add_series_list(series_list) 

516 

517 def _add_named_value( 

518 self, 

519 dest: str, 

520 name: str, 

521 values: Optional[ 

522 Union[ 

523 SeriesValues, 

524 NestedMeasureValues, 

525 ] 

526 ] = None, 

527 **kwargs, 

528 ) -> None: 

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

530 

531 if values is not None: 

532 value["values"] = values 

533 

534 self._add_value(dest, value) 

535 

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

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

538 

539 def build(self) -> dict: 

540 """ 

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

542 

543 Returns: 

544 A dictionary that stored in the data animation object. 

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

546 """ 

547 

548 jsonschema.validate(self, DATA_SCHEMA) 

549 return {"data": self} 

550 

551 

552class ConfigAttr(type): 

553 """ 

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

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

556 if the `__getattr__` method called. 

557 

558 For information on all available chart presets see the 

559 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/modules/types_presets/). 

560 """ 

561 

562 @classmethod 

563 def __getattr__(mcs, name): 

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

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

566 

567 def _get_preset(cls, preset): 

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

569 return config 

570 

571 

572class Config(AbstractAnimation, metaclass=ConfigAttr): 

573 """ 

574 A class for representing config animation. 

575 It can build config option of the chart. 

576 """ 

577 

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

579 """ 

580 Config constructor. 

581 

582 Args: 

583 data: 

584 A config animation dictionary. 

585 For information on all available config parameters see the 

586 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/interfaces/types_config.Chart/). 

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

588 

589 self._data = data 

590 

591 def build(self) -> dict: 

592 """ 

593 A method for returning the config animation dictionary. 

594 

595 Returns: 

596 A dictionary that stored in the config animation object. 

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

598 """ 

599 

600 return {"config": self._data} 

601 

602 

603class Style(AbstractAnimation): 

604 """ 

605 A class for representing style animation. 

606 It can build style option of the chart. 

607 """ 

608 

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

610 """ 

611 Style constructor. 

612 

613 Args: 

614 data: 

615 A style animation dictionary. 

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

617 chapter or the 

618 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/interfaces/types_styles.Chart/). 

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

620 

621 self._data = data 

622 

623 def build(self) -> dict: 

624 """ 

625 A method for returning the style animation dictionary. 

626 

627 Returns: 

628 A dictionary that stored in the style animation object. 

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

630 """ 

631 

632 return {"style": self._data} 

633 

634 

635class Keyframe(AbstractAnimation): 

636 """ 

637 A class for representing keyframe animation. 

638 It can build keyframe of the chart. 

639 """ 

640 

641 def __init__( 

642 self, 

643 *animations: AbstractAnimation, 

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

645 ): 

646 """ 

647 Keyframe constructor. 

648 

649 Args: 

650 *animations: 

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

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

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

654 For information on all available animation options see the 

655 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/interfaces/types_anim.Options/). 

656 

657 Raises: 

658 ValueError: If `animations` is not set. 

659 ValueError: If initialized with a `Keyframe`. 

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

661 

662 if not animations: 

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

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

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

666 

667 self._keyframe = {} 

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

669 if options: 

670 self._keyframe["options"] = options 

671 

672 def build(self) -> dict: 

673 """ 

674 A method for returning the keyframe animation dictionary. 

675 

676 Returns: 

677 A dictionary that stored in the keyframe animation object. 

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

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

680 """ 

681 

682 return self._keyframe 

683 

684 

685class Snapshot(AbstractAnimation): 

686 """ 

687 A class for representing a stored chart state. 

688 It can build the snapshot id of the chart. 

689 """ 

690 

691 def __init__(self, snapshot_id: str): 

692 """ 

693 Snapshot constructor. 

694 

695 Args: 

696 snapshot_id: A snapshot id. 

697 """ 

698 

699 self._snapshot_id = snapshot_id 

700 

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

702 """ 

703 A method for returning the snapshot id str. 

704 

705 Returns: 

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

707 """ 

708 

709 return self._snapshot_id 

710 

711 

712class Animation(Snapshot): 

713 """ 

714 A class for representing a stored animation. 

715 It can build the snapshot id of the animation. 

716 """ 

717 

718 

719class AnimationMerger(AbstractAnimation): 

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

721 

722 def __init__(self) -> None: 

723 """AnimationMerger constructor.""" 

724 

725 self._dict: dict = {} 

726 self._list: list = [] 

727 

728 @classmethod 

729 def merge_animations( 

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

731 ) -> AbstractAnimation: 

732 """ 

733 A class method for merging animations. 

734 

735 Args: 

736 animations: List of `AbstractAnimation` inherited objects. 

737 

738 Returns: 

739 An `AnimationMerger` class with the merged animations. 

740 """ 

741 

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

743 return animations[0] 

744 

745 merger = cls() 

746 for animation in animations: 

747 merger.merge(animation) 

748 

749 return merger 

750 

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

752 """ 

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

754 

755 Args: 

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

757 

758 Raises: 

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

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

761 """ 

762 

763 if isinstance(animation, Keyframe): 

764 if self._dict: 

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

766 data = animation.build() 

767 self._list.append(data) 

768 else: 

769 if self._list: 

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

771 data = self._validate(animation) 

772 self._dict.update(data) 

773 

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

775 if isinstance(animation, Snapshot): 

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

777 data = animation.build() 

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

779 

780 if common_keys: 

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

782 

783 return data 

784 

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

786 """ 

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

788 or a merged dictionary from different types of animations. 

789 

790 Returns: 

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

792 a merged dictionary from 

793 [Data][ipyvizzu.animation.Data], 

794 [Config][ipyvizzu.animation.Config] and 

795 [Style][ipyvizzu.animation.Style] animations. 

796 """ 

797 

798 if self._dict: 

799 return self._dict 

800 return self._list