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
« prev ^ index » next coverage.py v7.4.3, created at 2024-02-26 10:12 +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 """
299 Add a `pandas` `DataFrame`, `Series` or a `pyspark` `DataFrame`
300 to an existing [Data][ipyvizzu.animation.Data] class instance.
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`.
319 Example:
320 Adding a data frame to a [Data][ipyvizzu.animation.Data] class instance:
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 """
333 # pylint: disable=too-many-arguments
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"]
355 converter = Converter(**arguments) # type: ignore
356 series_list = converter.get_series_list()
357 self.add_series_list(series_list)
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.
369 Add a `pandas` `DataFrame` or `Series` to an existing
370 [Data][ipyvizzu.animation.Data] class instance.
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 """
383 # pylint: disable=line-too-long
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 )
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.
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.
418 Example:
419 Adding a data frame's index to a
420 [Data][ipyvizzu.animation.Data] class instance:
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 """
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)
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.
447 Add the index of a `pandas` `DataFrame` as a series to an existing
448 [Data][ipyvizzu.animation.Data] class instance.
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 """
457 # pylint: disable=line-too-long
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)
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.
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`.
495 Example:
496 Adding a data frame to a [Data][ipyvizzu.animation.Data] class instance:
498 np_array = np.zeros((3, 4))
499 data = Data()
500 data.add_np_array(np_array)
501 """
503 # pylint: disable=too-many-arguments
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)
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}
531 if values is not None:
532 value["values"] = values
534 self._add_value(dest, value)
536 def _add_value(self, dest: str, value: Union[dict, list]) -> None:
537 self.setdefault(dest, []).append(value)
539 def build(self) -> dict:
540 """
541 A method for validating and returning the data animation dictionary.
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 """
548 jsonschema.validate(self, DATA_SCHEMA)
549 return {"data": self}
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.
558 For information on all available chart presets see the
559 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/modules/types_presets/).
560 """
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
567 def _get_preset(cls, preset):
568 config = Config(RawJavaScript(f"lib.presets.{cls.name}({preset})"))
569 return config
572class Config(AbstractAnimation, metaclass=ConfigAttr):
573 """
574 A class for representing config animation.
575 It can build config option of the chart.
576 """
578 def __init__(self, data: Optional[Union[dict, RawJavaScript]]):
579 """
580 Config constructor.
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
589 self._data = data
591 def build(self) -> dict:
592 """
593 A method for returning the config animation dictionary.
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 """
600 return {"config": self._data}
603class Style(AbstractAnimation):
604 """
605 A class for representing style animation.
606 It can build style option of the chart.
607 """
609 def __init__(self, data: Optional[dict]):
610 """
611 Style constructor.
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
621 self._data = data
623 def build(self) -> dict:
624 """
625 A method for returning the style animation dictionary.
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 """
632 return {"style": self._data}
635class Keyframe(AbstractAnimation):
636 """
637 A class for representing keyframe animation.
638 It can build keyframe of the chart.
639 """
641 def __init__(
642 self,
643 *animations: AbstractAnimation,
644 **options: Optional[Union[str, int, float, dict]],
645 ):
646 """
647 Keyframe constructor.
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/).
657 Raises:
658 ValueError: If `animations` is not set.
659 ValueError: If initialized with a `Keyframe`.
660 """ # pylint: disable=line-too-long
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.")
667 self._keyframe = {}
668 self._keyframe["target"] = AnimationMerger.merge_animations(animations).build()
669 if options:
670 self._keyframe["options"] = options
672 def build(self) -> dict:
673 """
674 A method for returning the keyframe animation dictionary.
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 """
682 return self._keyframe
685class Snapshot(AbstractAnimation):
686 """
687 A class for representing a stored chart state.
688 It can build the snapshot id of the chart.
689 """
691 def __init__(self, snapshot_id: str):
692 """
693 Snapshot constructor.
695 Args:
696 snapshot_id: A snapshot id.
697 """
699 self._snapshot_id = snapshot_id
701 def build(self) -> str: # type: ignore
702 """
703 A method for returning the snapshot id str.
705 Returns:
706 An str snapshot id that stored in the snapshot animation object.
707 """
709 return self._snapshot_id
712class Animation(Snapshot):
713 """
714 A class for representing a stored animation.
715 It can build the snapshot id of the animation.
716 """
719class AnimationMerger(AbstractAnimation):
720 """A class for merging different types of animations."""
722 def __init__(self) -> None:
723 """AnimationMerger constructor."""
725 self._dict: dict = {}
726 self._list: list = []
728 @classmethod
729 def merge_animations(
730 cls, animations: Tuple[AbstractAnimation, ...]
731 ) -> AbstractAnimation:
732 """
733 A class method for merging animations.
735 Args:
736 animations: List of `AbstractAnimation` inherited objects.
738 Returns:
739 An `AnimationMerger` class with the merged animations.
740 """
742 if len(animations) == 1 and not isinstance(animations[0], Keyframe):
743 return animations[0]
745 merger = cls()
746 for animation in animations:
747 merger.merge(animation)
749 return merger
751 def merge(self, animation: AbstractAnimation) -> None:
752 """
753 A method for merging an animation with the previously merged animations.
755 Args:
756 animation: An animation to be merged with with previously merged animations.
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 """
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)
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)
780 if common_keys:
781 raise ValueError(f"{common_keys} is already merged.")
783 return data
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.
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 """
798 if self._dict:
799 return self._dict
800 return self._list