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
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-26 15:29 +0000
1"""A module for working with chart animations."""
3import abc
4import json
5from os import PathLike
6import sys
7from typing import Dict, List, Optional, Tuple, Type, Union
8import warnings
10import jsonschema # type: ignore
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
34class AbstractAnimation:
35 """
36 An abstract class for representing animation objects
37 that have `dump` and `build` methods.
38 """
40 def dump(self) -> str:
41 """
42 A method for converting the built dictionary into string.
44 Returns:
45 An str that has been json dumped with
46 [RawJavaScriptEncoder][ipyvizzu.json.RawJavaScriptEncoder] from a dictionary.
47 """
49 return json.dumps(self.build(), cls=RawJavaScriptEncoder)
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.
57 Returns:
58 A dictionary that stored in the animation object.
59 """
62class PlainAnimation(dict, AbstractAnimation):
63 """
64 A class for representing plain animation.
65 It can build any dictionary.
66 """
68 def build(self) -> dict:
69 """
70 A method for returning the plain animation dictionary.
72 Returns:
73 A dictionary that stored in the plain animation object.
74 """
76 return self
79class Data(dict, AbstractAnimation):
80 """
81 A class for representing data animation.
82 It can build data option of the chart.
83 """
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.
91 Args:
92 filter_expr: The JavaScript data filter expression.
94 Returns:
95 (Data): A data animation instance that contains a data filter.
97 Example:
98 Create a [Data][ipyvizzu.animation.Data] class with a data filter:
100 filter = Data.filter("record['Genres'] == 'Pop'")
101 """
103 data = cls()
104 data.set_filter(filter_expr)
105 return data
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.
112 Args:
113 filter_expr: The JavaScript data filter expression.
115 Example:
116 Add a data filter to a [Data][ipyvizzu.animation.Data] class instance:
118 data = Data()
119 data.set_filter("record['Genres'] == 'Pop'")
120 """
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})
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.
135 Args:
136 filename: The path of the data source json file.
138 Returns:
139 (Data): A data animation instance that has been created from a json file.
140 """
142 with open(filename, "r", encoding="utf8") as file_desc:
143 return cls(json.load(file_desc))
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.
150 Args:
151 record: A list that contains data values.
153 Example:
154 Adding a record to a [Data][ipyvizzu.animation.Data] class instance:
156 data = Data()
157 record = ["Pop", "Hard", 114]
158 data.add_record(record)
159 """
161 self._add_value("records", record)
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.
168 Args:
169 records: A list that contains data records.
171 Example:
172 Adding records to a [Data][ipyvizzu.animation.Data] class instance:
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 """
184 list(map(self.add_record, records))
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.
193 Args:
194 name: The name of the series.
195 values: The data values of the series.
196 **kwargs (Optional):
197 Arbitrary keyword arguments.
199 For example infer type can be set with the `type` keywod argument.
201 Example:
202 Adding a series without values to a [Data][ipyvizzu.animation.Data] class instance:
204 data = Data()
205 data.add_series("Genres")
207 Adding a series without values and with infer type to
208 a [Data][ipyvizzu.animation.Data] class instance:
210 data = Data()
211 data.add_series("Kinds", type="dimension")
213 Adding a series with values to a [Data][ipyvizzu.animation.Data] class instance:
215 data = Data()
216 data.add_series(
217 "Popularity", [114, 96, 127, 83]
218 )
219 """
221 self._add_named_value("series", name, values, **kwargs)
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.
228 Args:
229 series: List of series.
230 """
232 if series:
233 self.setdefault("series", []).extend(series)
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.
242 Args:
243 name: The name of the dimension.
244 values: The data values of the dimension.
245 **kwargs (Optional): Arbitrary keyword arguments.
247 Example:
248 Adding a dimension with values to a [Data][ipyvizzu.animation.Data] class instance:
250 data = Data()
251 data.add_dimension("Genres", ["Pop", "Rock"])
252 """
254 self._add_named_value("dimensions", name, values, **kwargs)
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.
263 Args:
264 name: The name of the measure.
265 values: The data values of the measure.
266 **kwargs (Optional): Arbitrary keyword arguments.
268 Example:
269 Adding a measure with values to a [Data][ipyvizzu.animation.Data] class instance:
271 data = Data()
272 data.add_measure(
273 "Popularity",
274 [
275 [114, 96],
276 [127, 83],
277 ],
278 )
279 """
281 self._add_named_value("measures", name, values, **kwargs)
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.
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`.
320 Example:
321 Adding a data frame to a [Data][ipyvizzu.animation.Data] class instance:
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 """
334 # pylint: disable=too-many-arguments
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"]
356 converter = Converter(**arguments) # type: ignore
357 series_list = converter.get_series_list()
358 self.add_series_list(series_list)
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.
370 Add a `pandas` `DataFrame` or `Series` to an existing
371 [Data][ipyvizzu.animation.Data] class instance.
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 """
384 # pylint: disable=line-too-long
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 )
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.
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.
419 Example:
420 Adding a data frame's index to a
421 [Data][ipyvizzu.animation.Data] class instance:
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 """
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)
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.
448 Add the index of a `pandas` `DataFrame` as a series to an existing
449 [Data][ipyvizzu.animation.Data] class instance.
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 """
458 # pylint: disable=line-too-long
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)
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.
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`.
497 Example:
498 Adding a data frame to a [Data][ipyvizzu.animation.Data] class instance:
500 np_array = np.zeros((3, 4))
501 data = Data()
502 data.add_np_array(np_array)
503 """
505 # pylint: disable=too-many-arguments
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)
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}
533 if values is not None:
534 value["values"] = values
536 self._add_value(dest, value)
538 def _add_value(self, dest: str, value: Union[dict, list]) -> None:
539 self.setdefault(dest, []).append(value)
541 def build(self) -> dict:
542 """
543 A method for validating and returning the data animation dictionary.
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 """
550 jsonschema.validate(self, DATA_SCHEMA)
551 return {"data": self}
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.
560 For information on all available chart presets see the
561 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/modules/types_presets/).
562 """
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
569 def _get_preset(cls, preset):
570 config = Config(RawJavaScript(f"lib.presets.{cls.name}({preset})"))
571 return config
574class Config(AbstractAnimation, metaclass=ConfigAttr):
575 """
576 A class for representing config animation.
577 It can build config option of the chart.
578 """
580 def __init__(self, data: Optional[Union[dict, RawJavaScript]]):
581 """
582 Config constructor.
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
591 self._data = data
593 def build(self) -> dict:
594 """
595 A method for returning the config animation dictionary.
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 """
602 return {"config": self._data}
605class Style(AbstractAnimation):
606 """
607 A class for representing style animation.
608 It can build style option of the chart.
609 """
611 def __init__(self, data: Optional[dict]):
612 """
613 Style constructor.
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
623 self._data = data
625 def build(self) -> dict:
626 """
627 A method for returning the style animation dictionary.
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 """
634 return {"style": self._data}
637class Keyframe(AbstractAnimation):
638 """
639 A class for representing keyframe animation.
640 It can build keyframe of the chart.
641 """
643 def __init__(
644 self,
645 *animations: AbstractAnimation,
646 **options: Optional[Union[str, int, float, dict]],
647 ):
648 """
649 Keyframe constructor.
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/).
659 Raises:
660 ValueError: If `animations` is not set.
661 ValueError: If initialized with a `Keyframe`.
662 """ # pylint: disable=line-too-long
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.")
669 self._keyframe = {}
670 self._keyframe["target"] = AnimationMerger.merge_animations(animations).build()
671 if options:
672 self._keyframe["options"] = options
674 def build(self) -> dict:
675 """
676 A method for returning the keyframe animation dictionary.
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 """
684 return self._keyframe
687class Snapshot(AbstractAnimation):
688 """
689 A class for representing a stored chart state.
690 It can build the snapshot id of the chart.
691 """
693 def __init__(self, snapshot_id: str):
694 """
695 Snapshot constructor.
697 Args:
698 snapshot_id: A snapshot id.
699 """
701 self._snapshot_id = snapshot_id
703 def build(self) -> str: # type: ignore
704 """
705 A method for returning the snapshot id str.
707 Returns:
708 An str snapshot id that stored in the snapshot animation object.
709 """
711 return self._snapshot_id
714class Animation(Snapshot):
715 """
716 A class for representing a stored animation.
717 It can build the snapshot id of the animation.
718 """
721class AnimationMerger(AbstractAnimation):
722 """A class for merging different types of animations."""
724 def __init__(self) -> None:
725 """AnimationMerger constructor."""
727 self._dict: dict = {}
728 self._list: list = []
730 @classmethod
731 def merge_animations(
732 cls, animations: Tuple[AbstractAnimation, ...]
733 ) -> AbstractAnimation:
734 """
735 A class method for merging animations.
737 Args:
738 animations: List of `AbstractAnimation` inherited objects.
740 Returns:
741 An `AnimationMerger` class with the merged animations.
742 """
744 if len(animations) == 1 and not isinstance(animations[0], Keyframe):
745 return animations[0]
747 merger = cls()
748 for animation in animations:
749 merger.merge(animation)
751 return merger
753 def merge(self, animation: AbstractAnimation) -> None:
754 """
755 A method for merging an animation with the previously merged animations.
757 Args:
758 animation: An animation to be merged with with previously merged animations.
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 """
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)
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)
782 if common_keys:
783 raise ValueError(f"{common_keys} is already merged.")
785 return data
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.
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 """
800 if self._dict:
801 return self._dict
802 return self._list