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
« prev ^ index » next coverage.py v7.3.2, created at 2023-10-12 08:13 +0000
1"""A module for working with chart animations."""
3import abc
4import json
5from os import PathLike
6import sys
7from typing import 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 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
29class AbstractAnimation:
30 """
31 An abstract class for representing animation objects
32 that have `dump` and `build` methods.
33 """
35 def dump(self) -> str:
36 """
37 A method for converting the built dictionary into string.
39 Returns:
40 An str that has been json dumped with
41 [RawJavaScriptEncoder][ipyvizzu.json.RawJavaScriptEncoder] from a dictionary.
42 """
44 return json.dumps(self.build(), cls=RawJavaScriptEncoder)
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.
52 Returns:
53 A dictionary that stored in the animation object.
54 """
57class PlainAnimation(dict, AbstractAnimation):
58 """
59 A class for representing plain animation.
60 It can build any dictionary.
61 """
63 def build(self) -> dict:
64 """
65 A method for returning the plain animation dictionary.
67 Returns:
68 A dictionary that stored in the plain animation object.
69 """
71 return self
74class Data(dict, AbstractAnimation):
75 """
76 A class for representing data animation.
77 It can build data option of the chart.
78 """
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.
86 Args:
87 filter_expr: The JavaScript data filter expression.
89 Returns:
90 (Data): A data animation instance that contains a data filter.
92 Example:
93 Create a [Data][ipyvizzu.animation.Data] class with a data filter:
95 filter = Data.filter("record['Genres'] == 'Pop'")
96 """
98 data = cls()
99 data.set_filter(filter_expr)
100 return data
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.
107 Args:
108 filter_expr: The JavaScript data filter expression.
110 Example:
111 Add a data filter to a [Data][ipyvizzu.animation.Data] class instance:
113 data = Data()
114 data.set_filter("record['Genres'] == 'Pop'")
115 """
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})
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.
130 Args:
131 filename: The path of the data source json file.
133 Returns:
134 (Data): A data animation instance that has been created from a json file.
135 """
137 with open(filename, "r", encoding="utf8") as file_desc:
138 return cls(json.load(file_desc))
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.
145 Args:
146 record: A list that contains data values.
148 Example:
149 Adding a record to a [Data][ipyvizzu.animation.Data] class instance:
151 data = Data()
152 record = ["Pop", "Hard", 114]
153 data.add_record(record)
154 """
156 self._add_value("records", record)
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.
163 Args:
164 records: A list that contains data records.
166 Example:
167 Adding records to a [Data][ipyvizzu.animation.Data] class instance:
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 """
179 list(map(self.add_record, records))
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.
188 Args:
189 name: The name of the series.
190 values: The data values of the series.
191 **kwargs (Optional):
192 Arbitrary keyword arguments.
194 For example infer type can be set with the `type` keywod argument.
196 Example:
197 Adding a series without values to a [Data][ipyvizzu.animation.Data] class instance:
199 data = Data()
200 data.add_series("Genres")
202 Adding a series without values and with infer type to
203 a [Data][ipyvizzu.animation.Data] class instance:
205 data = Data()
206 data.add_series("Kinds", type="dimension")
208 Adding a series with values to a [Data][ipyvizzu.animation.Data] class instance:
210 data = Data()
211 data.add_series(
212 "Popularity", [114, 96, 127, 83]
213 )
214 """
216 self._add_named_value("series", name, values, **kwargs)
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.
223 Args:
224 series: List of series.
225 """
227 if series:
228 self.setdefault("series", []).extend(series)
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.
237 Args:
238 name: The name of the dimension.
239 values: The data values of the dimension.
240 **kwargs (Optional): Arbitrary keyword arguments.
242 Example:
243 Adding a dimension with values to a [Data][ipyvizzu.animation.Data] class instance:
245 data = Data()
246 data.add_dimension("Genres", ["Pop", "Rock"])
247 """
249 self._add_named_value("dimensions", name, values, **kwargs)
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.
258 Args:
259 name: The name of the measure.
260 values: The data values of the measure.
261 **kwargs (Optional): Arbitrary keyword arguments.
263 Example:
264 Adding a measure with values to a [Data][ipyvizzu.animation.Data] class instance:
266 data = Data()
267 data.add_measure(
268 "Popularity",
269 [
270 [114, 96],
271 [127, 83],
272 ],
273 )
274 """
276 self._add_named_value("measures", name, values, **kwargs)
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.
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`.)
311 Example:
312 Adding a data frame to a [Data][ipyvizzu.animation.Data] class instance:
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 """
325 # pylint: disable=too-many-arguments
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"]
346 converter = Converter(**arguments) # type: ignore
347 series_list = converter.get_series_list()
348 self.add_series_list(series_list)
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.
360 Add a `pandas` `DataFrame` or `Series` to an existing
361 [Data][ipyvizzu.animation.Data] class instance.
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 """
374 # pylint: disable=line-too-long
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 )
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.
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.
409 Example:
410 Adding a data frame's index to a
411 [Data][ipyvizzu.animation.Data] class instance:
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 """
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)
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.
438 Add the index of a `pandas` `DataFrame` as a series to an existing
439 [Data][ipyvizzu.animation.Data] class instance.
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 """
448 # pylint: disable=line-too-long
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)
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.
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.
483 Example:
484 Adding a data frame to a [Data][ipyvizzu.animation.Data] class instance:
486 np_array = np.zeros((3, 4))
487 data = Data()
488 data.add_np_array(np_array)
489 """
491 # pylint: disable=too-many-arguments
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)
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}
518 if values is not None:
519 value["values"] = values
521 self._add_value(dest, value)
523 def _add_value(self, dest: str, value: Union[dict, list]) -> None:
524 self.setdefault(dest, []).append(value)
526 def build(self) -> dict:
527 """
528 A method for validating and returning the data animation dictionary.
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 """
535 jsonschema.validate(self, DATA_SCHEMA)
536 return {"data": self}
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.
545 For information on all available chart presets see the
546 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/modules/Presets/#interfaces).
547 """
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
554 def _get_preset(cls, preset):
555 config = Config(RawJavaScript(f"lib.presets.{cls.name}({preset})"))
556 return config
559class Config(AbstractAnimation, metaclass=ConfigAttr):
560 """
561 A class for representing config animation.
562 It can build config option of the chart.
563 """
565 def __init__(self, data: Optional[Union[dict, RawJavaScript]]):
566 """
567 Config constructor.
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
576 self._data = data
578 def build(self) -> dict:
579 """
580 A method for returning the config animation dictionary.
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 """
587 return {"config": self._data}
590class Style(AbstractAnimation):
591 """
592 A class for representing style animation.
593 It can build style option of the chart.
594 """
596 def __init__(self, data: Optional[dict]):
597 """
598 Style constructor.
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
608 self._data = data
610 def build(self) -> dict:
611 """
612 A method for returning the style animation dictionary.
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 """
619 return {"style": self._data}
622class Keyframe(AbstractAnimation):
623 """
624 A class for representing keyframe animation.
625 It can build keyframe of the chart.
626 """
628 def __init__(
629 self,
630 *animations: AbstractAnimation,
631 **options: Optional[Union[str, int, float, dict]],
632 ):
633 """
634 Keyframe constructor.
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).
644 Raises:
645 ValueError: If `animations` is not set.
646 ValueError: If initialized with a `Keyframe`.
647 """ # pylint: disable=line-too-long
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.")
654 self._keyframe = {}
655 self._keyframe["target"] = AnimationMerger.merge_animations(animations).build()
656 if options:
657 self._keyframe["options"] = options
659 def build(self) -> dict:
660 """
661 A method for returning the keyframe animation dictionary.
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 """
669 return self._keyframe
672class Snapshot(AbstractAnimation):
673 """
674 A class for representing a stored chart state.
675 It can build the snapshot id of the chart.
676 """
678 def __init__(self, snapshot_id: str):
679 """
680 Snapshot constructor.
682 Args:
683 snapshot_id: A snapshot id.
684 """
686 self._snapshot_id = snapshot_id
688 def build(self) -> str: # type: ignore
689 """
690 A method for returning the snapshot id str.
692 Returns:
693 An str snapshot id that stored in the snapshot animation object.
694 """
696 return self._snapshot_id
699class Animation(Snapshot):
700 """
701 A class for representing a stored animation.
702 It can build the snapshot id of the animation.
703 """
706class AnimationMerger(AbstractAnimation):
707 """A class for merging different types of animations."""
709 def __init__(self) -> None:
710 """AnimationMerger constructor."""
712 self._dict: dict = {}
713 self._list: list = []
715 @classmethod
716 def merge_animations(
717 cls, animations: Tuple[AbstractAnimation, ...]
718 ) -> AbstractAnimation:
719 """
720 A class method for merging animations.
722 Args:
723 animations: List of `AbstractAnimation` inherited objects.
725 Returns:
726 An `AnimationMerger` class with the merged animations.
727 """
729 if len(animations) == 1 and not isinstance(animations[0], Keyframe):
730 return animations[0]
732 merger = cls()
733 for animation in animations:
734 merger.merge(animation)
736 return merger
738 def merge(self, animation: AbstractAnimation) -> None:
739 """
740 A method for merging an animation with the previously merged animations.
742 Args:
743 animation: An animation to be merged with with previously merged animations.
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 """
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)
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)
767 if common_keys:
768 raise ValueError(f"{common_keys} is already merged.")
770 return data
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.
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 """
785 if self._dict:
786 return self._dict
787 return self._list