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

159 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-26 15:29 +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 # pylint: disable=too-many-positional-arguments 

299 """ 

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

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

302 

303 Args: 

304 df: 

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

306 default_measure_value: 

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

308 default_dimension_value: 

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

310 max_rows: 

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

312 If the `df` contains more rows, 

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

314 include_index: 

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

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

317 units: 

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

319 

320 Example: 

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

322 

323 df = pd.DataFrame( 

324 { 

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

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

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

328 } 

329 ) 

330 data = Data() 

331 data.add_df(df) 

332 """ 

333 

334 # pylint: disable=too-many-arguments 

335 

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

337 arguments = { 

338 "df": df, 

339 "default_measure_value": default_measure_value, 

340 "default_dimension_value": default_dimension_value, 

341 "max_rows": max_rows, 

342 "include_index": include_index, 

343 "units": units, 

344 } 

345 Converter: Union[ 

346 Type[PandasDataFrameConverter], Type[SparkDataFrameConverter] 

347 ] = PandasDataFrameConverter 

348 if isinstance(df, SparkDataFrame): 

349 Converter = SparkDataFrameConverter 

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

351 raise ValueError( 

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

353 ) 

354 del arguments["include_index"] 

355 

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

357 series_list = converter.get_series_list() 

358 self.add_series_list(series_list) 

359 

360 def add_data_frame( 

361 self, 

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

363 default_measure_value: MeasureValue = NAN_MEASURE, 

364 default_dimension_value: DimensionValue = NAN_DIMENSION, 

365 ) -> None: 

366 """ 

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

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

369 

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

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

372 

373 

374 

375 Args: 

376 data_frame: 

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

378 default_measure_value: 

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

380 default_dimension_value: 

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

382 """ 

383 

384 # pylint: disable=line-too-long 

385 

386 reference = "https://ipyvizzu.vizzuhq.com/0.18/reference/ipyvizzu/animation/#ipyvizzu.animation.Data.add_df" 

387 warnings.warn( 

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

389 DeprecationWarning, 

390 stacklevel=2, 

391 ) 

392 self.add_df( 

393 data_frame, 

394 default_measure_value, 

395 default_dimension_value, 

396 max_rows=sys.maxsize, 

397 ) 

398 

399 def add_df_index( 

400 self, 

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

402 column_name: str = "Index", 

403 max_rows: int = MAX_ROWS, 

404 ) -> None: 

405 """ 

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

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

408 

409 Args: 

410 df: 

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

412 column_name: 

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

414 max_rows: 

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

416 If the `df` contains more rows, 

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

418 

419 Example: 

420 Adding a data frame's index to a 

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

422 

423 df = pd.DataFrame( 

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

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

426 ) 

427 data = Data() 

428 data.add_df_index(df, "DataFrameIndex") 

429 data.add_df(df) 

430 """ 

431 

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

433 converter = PandasDataFrameConverter( 

434 df, max_rows=max_rows, include_index=column_name 

435 ) 

436 series_list = converter.get_series_from_index() 

437 self.add_series_list(series_list) 

438 

439 def add_data_frame_index( 

440 self, 

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

442 name: str, 

443 ) -> None: 

444 """ 

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

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

447 

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

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

450 

451 Args: 

452 data_frame: 

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

454 name: 

455 The name of the index series. 

456 """ 

457 

458 # pylint: disable=line-too-long 

459 

460 reference = "https://ipyvizzu.vizzuhq.com/0.18/reference/ipyvizzu/animation/#ipyvizzu.animation.Data.add_df_index" 

461 warnings.warn( 

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

463 DeprecationWarning, 

464 stacklevel=2, 

465 ) 

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

467 

468 def add_np_array( 

469 self, 

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

471 column_name: Optional[ColumnName] = None, 

472 column_dtype: Optional[ColumnDtype] = None, 

473 column_unit: Optional[ColumnUnit] = None, 

474 default_measure_value: MeasureValue = NAN_MEASURE, 

475 default_dimension_value: DimensionValue = NAN_DIMENSION, 

476 ) -> None: 

477 # pylint: disable=too-many-positional-arguments 

478 """ 

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

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

481 

482 Args: 

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

484 column_name: 

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

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

487 column_dtype: 

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

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

490 default_measure_value: 

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

492 default_dimension_value: 

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

494 column_unit: 

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

496 

497 Example: 

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

499 

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

501 data = Data() 

502 data.add_np_array(np_array) 

503 """ 

504 

505 # pylint: disable=too-many-arguments 

506 

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

508 converter = NumpyArrayConverter( 

509 np_array, 

510 column_name, 

511 column_dtype, 

512 column_unit, 

513 default_measure_value, 

514 default_dimension_value, 

515 ) 

516 series_list = converter.get_series_list() 

517 self.add_series_list(series_list) 

518 

519 def _add_named_value( 

520 self, 

521 dest: str, 

522 name: str, 

523 values: Optional[ 

524 Union[ 

525 SeriesValues, 

526 NestedMeasureValues, 

527 ] 

528 ] = None, 

529 **kwargs, 

530 ) -> None: 

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

532 

533 if values is not None: 

534 value["values"] = values 

535 

536 self._add_value(dest, value) 

537 

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

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

540 

541 def build(self) -> dict: 

542 """ 

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

544 

545 Returns: 

546 A dictionary that stored in the data animation object. 

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

548 """ 

549 

550 jsonschema.validate(self, DATA_SCHEMA) 

551 return {"data": self} 

552 

553 

554class ConfigAttr(type): 

555 """ 

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

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

558 if the `__getattr__` method called. 

559 

560 For information on all available chart presets see the 

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

562 """ 

563 

564 @classmethod 

565 def __getattr__(mcs, name): 

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

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

568 

569 def _get_preset(cls, preset): 

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

571 return config 

572 

573 

574class Config(AbstractAnimation, metaclass=ConfigAttr): 

575 """ 

576 A class for representing config animation. 

577 It can build config option of the chart. 

578 """ 

579 

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

581 """ 

582 Config constructor. 

583 

584 Args: 

585 data: 

586 A config animation dictionary. 

587 For information on all available config parameters see the 

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

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

590 

591 self._data = data 

592 

593 def build(self) -> dict: 

594 """ 

595 A method for returning the config animation dictionary. 

596 

597 Returns: 

598 A dictionary that stored in the config animation object. 

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

600 """ 

601 

602 return {"config": self._data} 

603 

604 

605class Style(AbstractAnimation): 

606 """ 

607 A class for representing style animation. 

608 It can build style option of the chart. 

609 """ 

610 

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

612 """ 

613 Style constructor. 

614 

615 Args: 

616 data: 

617 A style animation dictionary. 

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

619 chapter or the 

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

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

622 

623 self._data = data 

624 

625 def build(self) -> dict: 

626 """ 

627 A method for returning the style animation dictionary. 

628 

629 Returns: 

630 A dictionary that stored in the style animation object. 

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

632 """ 

633 

634 return {"style": self._data} 

635 

636 

637class Keyframe(AbstractAnimation): 

638 """ 

639 A class for representing keyframe animation. 

640 It can build keyframe of the chart. 

641 """ 

642 

643 def __init__( 

644 self, 

645 *animations: AbstractAnimation, 

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

647 ): 

648 """ 

649 Keyframe constructor. 

650 

651 Args: 

652 *animations: 

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

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

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

656 For information on all available animation options see the 

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

658 

659 Raises: 

660 ValueError: If `animations` is not set. 

661 ValueError: If initialized with a `Keyframe`. 

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

663 

664 if not animations: 

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

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

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

668 

669 self._keyframe = {} 

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

671 if options: 

672 self._keyframe["options"] = options 

673 

674 def build(self) -> dict: 

675 """ 

676 A method for returning the keyframe animation dictionary. 

677 

678 Returns: 

679 A dictionary that stored in the keyframe animation object. 

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

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

682 """ 

683 

684 return self._keyframe 

685 

686 

687class Snapshot(AbstractAnimation): 

688 """ 

689 A class for representing a stored chart state. 

690 It can build the snapshot id of the chart. 

691 """ 

692 

693 def __init__(self, snapshot_id: str): 

694 """ 

695 Snapshot constructor. 

696 

697 Args: 

698 snapshot_id: A snapshot id. 

699 """ 

700 

701 self._snapshot_id = snapshot_id 

702 

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

704 """ 

705 A method for returning the snapshot id str. 

706 

707 Returns: 

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

709 """ 

710 

711 return self._snapshot_id 

712 

713 

714class Animation(Snapshot): 

715 """ 

716 A class for representing a stored animation. 

717 It can build the snapshot id of the animation. 

718 """ 

719 

720 

721class AnimationMerger(AbstractAnimation): 

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

723 

724 def __init__(self) -> None: 

725 """AnimationMerger constructor.""" 

726 

727 self._dict: dict = {} 

728 self._list: list = [] 

729 

730 @classmethod 

731 def merge_animations( 

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

733 ) -> AbstractAnimation: 

734 """ 

735 A class method for merging animations. 

736 

737 Args: 

738 animations: List of `AbstractAnimation` inherited objects. 

739 

740 Returns: 

741 An `AnimationMerger` class with the merged animations. 

742 """ 

743 

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

745 return animations[0] 

746 

747 merger = cls() 

748 for animation in animations: 

749 merger.merge(animation) 

750 

751 return merger 

752 

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

754 """ 

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

756 

757 Args: 

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

759 

760 Raises: 

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

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

763 """ 

764 

765 if isinstance(animation, Keyframe): 

766 if self._dict: 

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

768 data = animation.build() 

769 self._list.append(data) 

770 else: 

771 if self._list: 

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

773 data = self._validate(animation) 

774 self._dict.update(data) 

775 

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

777 if isinstance(animation, Snapshot): 

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

779 data = animation.build() 

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

781 

782 if common_keys: 

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

784 

785 return data 

786 

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

788 """ 

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

790 or a merged dictionary from different types of animations. 

791 

792 Returns: 

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

794 a merged dictionary from 

795 [Data][ipyvizzu.animation.Data], 

796 [Config][ipyvizzu.animation.Config] and 

797 [Style][ipyvizzu.animation.Style] animations. 

798 """ 

799 

800 if self._dict: 

801 return self._dict 

802 return self._list