Coverage for src/ipyvizzu/animation.py: 100%
147 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-10 09:30 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-10 09:30 +0000
1"""A module for working with chart animations."""
3import abc
4from enum import Enum
5from os import PathLike
6import json
7from typing import Optional, Union, List, Any, Tuple
8import jsonschema # type: ignore
10import pandas as pd # type: ignore
11from pandas.api.types import is_numeric_dtype # type: ignore
13from ipyvizzu.json import RawJavaScript, RawJavaScriptEncoder
14from ipyvizzu.schema import DATA_SCHEMA
17class AbstractAnimation:
18 """
19 An abstract class for representing animation objects
20 that have `dump` and `build` methods.
21 """
23 def dump(self) -> str:
24 """
25 A method for converting the built dictionary into string.
27 Returns:
28 An str that has been json dumped with
29 [RawJavaScriptEncoder][ipyvizzu.json.RawJavaScriptEncoder] from a dictionary.
30 """
32 return json.dumps(self.build(), cls=RawJavaScriptEncoder)
34 @abc.abstractmethod
35 def build(self) -> dict:
36 """
37 An abstract method for returning a dictionary with values
38 that can be converted into json string.
40 Returns:
41 A dictionary that stored in the animation object.
42 """
45class PlainAnimation(dict, AbstractAnimation):
46 """
47 A class for representing plain animation.
48 It can build any dictionary.
49 """
51 def build(self) -> dict:
52 """
53 A method for returning the plain animation dictionary.
55 Returns:
56 A dictionary that stored in the plain animation object.
57 """
59 return self
62class InferType(Enum):
63 """An enum class for storing data infer types."""
65 DIMENSION = "dimension"
66 """An enum key-value for storing dimension infer type."""
68 MEASURE = "measure"
69 """An enum key-value for storing measure infer type."""
72class Data(dict, AbstractAnimation):
73 """
74 A class for representing data animation.
75 It can build data option of the chart.
76 """
78 @classmethod
79 def filter(cls, filter_expr: Optional[str] = None): # -> Data:
80 """
81 A class method for creating a [Data][ipyvizzu.animation.Data]
82 class instance with a data filter.
84 Args:
85 filter_expr: The JavaScript data filter expression.
87 Returns:
88 (Data): A data animation instance that contains a data filter.
90 Example:
91 Create a [Data][ipyvizzu.animation.Data] class with a data filter:
93 filter = Data.filter("record['Genres'] == 'Pop'")
94 """
96 data = cls()
97 data.set_filter(filter_expr)
98 return data
100 def set_filter(self, filter_expr: Optional[str] = None) -> None:
101 """
102 A method for adding a filter to an existing
103 [Data][ipyvizzu.animation.Data] class instance.
105 Args:
106 filter_expr: The JavaScript data filter expression.
108 Example:
109 Add a data filter to a [Data][ipyvizzu.animation.Data] class instance:
111 data = Data()
112 data.set_filter("record['Genres'] == 'Pop'")
113 """
115 filter_expr_raw_js = (
116 RawJavaScript(f"record => {{ return ({' '.join(filter_expr.split())}) }}")
117 if filter_expr is not None
118 else filter_expr
119 )
120 self.update({"filter": filter_expr_raw_js})
122 @classmethod
123 def from_json(cls, filename: Union[str, bytes, PathLike]): # -> Data:
124 """
125 A method for returning a [Data][ipyvizzu.animation.Data]
126 class instance which has been created from a json file.
128 Args:
129 filename: The path of the data source json file.
131 Returns:
132 (Data): A data animation instance that has been created from a json file.
133 """
135 with open(filename, "r", encoding="utf8") as file_desc:
136 return cls(json.load(file_desc))
138 def add_record(self, record: list) -> None:
139 """
140 A method for adding a record to an existing
141 [Data][ipyvizzu.animation.Data] class instance.
143 Args:
144 record: A list that contains data values.
146 Example:
147 Adding a record to a [Data][ipyvizzu.animation.Data] class instance:
149 data = Data()
150 record = ["Pop", "Hard", 114]
151 data.add_record(record)
152 """
154 self._add_value("records", record)
156 def add_records(self, records: List[list]) -> None:
157 """
158 A method for adding records to an existing
159 [Data][ipyvizzu.animation.Data] class instance.
161 Args:
162 records: A list that contains data records.
164 Example:
165 Adding records to a [Data][ipyvizzu.animation.Data] class instance:
167 data = Data()
168 records = [
169 ["Pop", "Hard", 114],
170 ["Rock", "Hard", 96],
171 ["Pop", "Experimental", 127],
172 ["Rock", "Experimental", 83],
173 ]
174 data.add_records(records)
175 """
177 list(map(self.add_record, records))
179 def add_series(self, name: str, values: Optional[list] = None, **kwargs) -> None:
180 """
181 A method for adding a series to an existing
182 [Data][ipyvizzu.animation.Data] class instance.
184 Args:
185 name: The name of the series.
186 values: The data values of the series.
187 **kwargs (Optional):
188 Arbitrary keyword arguments.
190 For example infer type can be set with the `type` keywod argument.
192 Example:
193 Adding a series without values to a [Data][ipyvizzu.animation.Data] class instance:
195 data = Data()
196 data.add_series("Genres")
198 Adding a series without values and with infer type to
199 a [Data][ipyvizzu.animation.Data] class instance:
201 data = Data()
202 data.add_series("Kinds", type="dimension")
204 Adding a series with values to a [Data][ipyvizzu.animation.Data] class instance:
206 data = Data()
207 data.add_series(
208 "Popularity", [114, 96, 127, 83]
209 )
210 """
212 self._add_named_value("series", name, values, **kwargs)
214 def add_dimension(self, name: str, values: Optional[list] = None, **kwargs) -> None:
215 """
216 A method for adding a dimension to an existing
217 [Data][ipyvizzu.animation.Data] class instance.
219 Args:
220 name: The name of the dimension.
221 values: The data values of the dimension.
222 **kwargs (Optional): Arbitrary keyword arguments.
224 Example:
225 Adding a dimension with values to a [Data][ipyvizzu.animation.Data] class instance:
227 data = Data()
228 data.add_dimension("Genres", ["Pop", "Rock"])
229 """
231 self._add_named_value("dimensions", name, values, **kwargs)
233 def add_measure(self, name: str, values: Optional[list] = None, **kwargs) -> None:
234 """
235 A method for adding a measure to an existing
236 [Data][ipyvizzu.animation.Data] class instance.
238 Args:
239 name: The name of the measure.
240 values: The data values of the measure.
241 **kwargs (Optional): Arbitrary keyword arguments.
243 Example:
244 Adding a measure with values to a [Data][ipyvizzu.animation.Data] class instance:
246 data = Data()
247 data.add_measure(
248 "Popularity",
249 [
250 [114, 96],
251 [127, 83],
252 ],
253 )
254 """
256 self._add_named_value("measures", name, values, **kwargs)
258 def add_data_frame(
259 self,
260 data_frame: Union[pd.DataFrame, pd.Series],
261 default_measure_value: Optional[Any] = 0,
262 default_dimension_value: Optional[Any] = "",
263 ) -> None:
264 """
265 A method for adding data frame to an existing
266 [Data][ipyvizzu.animation.Data] class instance.
268 Args:
269 data_frame: The pandas data frame object.
270 default_measure_value: The default measure value to fill the empty values.
271 default_dimension_value: The default dimension value to fill the empty values.
273 Raises:
274 TypeError: If `data_frame` is not instance of [pd.DataFrame][pandas.DataFrame]
275 or [pd.Series][pandas.Series].
277 Example:
278 Adding a data frame to a [Data][ipyvizzu.animation.Data] class instance:
280 data_frame = pd.DataFrame(
281 {
282 "Genres": ["Pop", "Rock", "Pop", "Rock"],
283 "Kinds": ["Hard", "Hard", "Experimental", "Experimental"],
284 "Popularity": [114, 96, 127, 83],
285 }
286 )
287 data = Data()
288 data.add_data_frame(data_frame)
289 """
291 if not isinstance(data_frame, type(None)):
292 if isinstance(data_frame, pd.Series):
293 data_frame = pd.DataFrame(data_frame)
294 if not isinstance(data_frame, pd.DataFrame):
295 raise TypeError(
296 "data_frame must be instance of pandas.DataFrame or pandas.Series"
297 )
298 for name in data_frame.columns:
299 values = []
300 if is_numeric_dtype(data_frame[name].dtype):
301 infer_type = InferType.MEASURE
302 values = (
303 data_frame[name]
304 .fillna(default_measure_value)
305 .astype(float)
306 .values.tolist()
307 )
308 else:
309 infer_type = InferType.DIMENSION
310 values = (
311 data_frame[name]
312 .fillna(default_dimension_value)
313 .astype(str)
314 .values.tolist()
315 )
316 self.add_series(
317 name,
318 values,
319 type=infer_type.value,
320 )
322 def add_data_frame_index(
323 self,
324 data_frame: Union[pd.DataFrame, pd.Series],
325 name: Optional[str],
326 ) -> None:
327 """
328 A method for adding data frame's index to an existing
329 [Data][ipyvizzu.animation.Data] class instance.
331 Args:
332 data_frame: The pandas data frame object.
333 name: The name of the index series.
335 Raises:
336 TypeError: If `data_frame` is not instance of [pd.DataFrame][pandas.DataFrame]
337 or [pd.Series][pandas.Series].
339 Example:
340 Adding a data frame's index to a [Data][ipyvizzu.animation.Data] class instance:
342 data_frame = pd.DataFrame(
343 {"Popularity": [114, 96]},
344 index=["x", "y"]
345 )
346 data = Data()
347 data.add_data_frame_index(data_frame, "DataFrameIndex")
348 data.add_data_frame(data_frame)
349 """
351 if data_frame is not None:
352 if isinstance(data_frame, pd.Series):
353 data_frame = pd.DataFrame(data_frame)
354 if not isinstance(data_frame, pd.DataFrame):
355 raise TypeError(
356 "data_frame must be instance of pandas.DataFrame or pandas.Series"
357 )
358 self.add_series(
359 str(name),
360 [str(i) for i in data_frame.index],
361 type=InferType.DIMENSION.value,
362 )
364 def _add_named_value(
365 self, dest: str, name: str, values: Optional[list] = None, **kwargs
366 ) -> None:
367 value = {"name": name, **kwargs}
369 if values is not None:
370 value["values"] = values # type: ignore
372 self._add_value(dest, value)
374 def _add_value(self, dest: str, value: Union[dict, list]) -> None:
375 self.setdefault(dest, []).append(value)
377 def build(self) -> dict:
378 """
379 A method for validating and returning the data animation dictionary.
381 Returns:
382 A dictionary that stored in the data animation object.
383 It contains a `data` key whose value is the stored animation.
384 """
386 jsonschema.validate(self, DATA_SCHEMA)
387 return {"data": self}
390class ConfigAttr(type):
391 """
392 A metaclass class for the [Config][ipyvizzu.animation.Config] class.
393 Returns a [Config][ipyvizzu.animation.Config] class with a chart preset
394 if the `__getattr__` method called.
396 For information on all available chart presets see the
397 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/modules/presets/#interfaces).
398 """
400 @classmethod
401 def __getattr__(mcs, name):
402 config_attr = mcs("ConfigAttr", (object,), {"name": name})
403 return config_attr._get_preset # pylint: disable=no-member
405 def _get_preset(cls, preset):
406 config = Config(RawJavaScript(f"lib.presets.{cls.name}({preset})"))
407 return config
410class Config(AbstractAnimation, metaclass=ConfigAttr):
411 """
412 A class for representing config animation.
413 It can build config option of the chart.
414 """
416 def __init__(self, data: Optional[dict]):
417 """
418 Config constructor.
420 Args:
421 data:
422 A config animation dictionary.
423 For information on all available config parameters see the
424 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/interfaces/vizzu.Config.Chart/#properties).
425 """ # pylint: disable=line-too-long
427 self._data = data
429 def build(self) -> dict:
430 """
431 A method for returning the config animation dictionary.
433 Returns:
434 A dictionary that stored in the config animation object.
435 It contains a `config` key whose value is the stored animation.
436 """
438 return {"config": self._data}
441class Style(AbstractAnimation):
442 """
443 A class for representing style animation.
444 It can build style option of the chart.
445 """
447 def __init__(self, data: Optional[dict]):
448 """
449 Style constructor.
451 Args:
452 data:
453 A style animation dictionary.
454 For information on all available style parameters see the [Style][styling-properties]
455 chapter or the
456 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/interfaces/vizzu.Styles.Chart/#properties).
457 """ # pylint: disable=line-too-long
459 self._data = data
461 def build(self) -> dict:
462 """
463 A method for returning the style animation dictionary.
465 Returns:
466 A dictionary that stored in the style animation object.
467 It contains a `style` key whose value is the stored animation.
468 """
470 return {"style": self._data}
473class Keyframe(AbstractAnimation):
474 """
475 A class for representing keyframe animation.
476 It can build keyframe of the chart.
477 """
479 def __init__(
480 self,
481 *animations: AbstractAnimation,
482 **options: Optional[Union[str, int, float, dict]],
483 ):
484 """
485 Keyframe constructor.
487 Args:
488 *animations:
489 List of AbstractAnimation inherited objects such as [Data][ipyvizzu.animation.Data],
490 [Config][ipyvizzu.animation.Config] and [Style][ipyvizzu.animation.Style].
491 **options: Dictionary of animation options for example `duration=1`.
492 For information on all available animation options see the
493 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/interfaces/vizzu.Anim.Options/#properties).
495 Raises:
496 ValueError: If `animations` is not set.
497 ValueError: If initialized with a `Keyframe`.
498 """ # pylint: disable=line-too-long
500 if not animations:
501 raise ValueError("No animation was set.")
502 if [animation for animation in animations if isinstance(animation, Keyframe)]:
503 raise ValueError("A Keyframe cannot contain a Keyframe.")
505 self._keyframe = {}
506 self._keyframe["target"] = AnimationMerger.merge_animations(animations).build()
507 if options:
508 self._keyframe["options"] = options
510 def build(self) -> dict:
511 """
512 A method for returning the keyframe animation dictionary.
514 Returns:
515 A dictionary that stored in the keyframe animation object.
516 It contains a `target` key whose value is the stored animation
517 and an optional `options` key whose value is the stored animation options.
518 """
520 return self._keyframe
523class Snapshot(AbstractAnimation):
524 """
525 A class for representing a stored chart state.
526 It can build the snapshot id of the chart.
527 """
529 def __init__(self, snapshot_id: str):
530 """
531 Snapshot constructor.
533 Args:
534 snapshot_id: A snapshot id.
535 """
537 self._snapshot_id = snapshot_id
539 def build(self) -> str: # type: ignore
540 """
541 A method for returning the snapshot id str.
543 Returns:
544 An str snapshot id that stored in the snapshot animation object.
545 """
547 return self._snapshot_id
550class Animation(Snapshot):
551 """
552 A class for representing a stored animation.
553 It can build the snapshot id of the animation.
554 """
557class AnimationMerger(AbstractAnimation):
558 """A class for merging different types of animations."""
560 def __init__(self):
561 """AnimationMerger constructor."""
563 self._dict = {}
564 self._list = []
566 @classmethod
567 def merge_animations(
568 cls, animations: Tuple[AbstractAnimation, ...]
569 ) -> AbstractAnimation:
570 """
571 A class method for merging animations.
573 Args:
574 animations: List of `AbstractAnimation` inherited objects.
576 Returns:
577 An `AnimationMerger` class with the merged animations.
578 """
580 if len(animations) == 1 and not isinstance(animations[0], Keyframe):
581 return animations[0]
583 merger = cls()
584 for animation in animations:
585 merger.merge(animation)
587 return merger
589 def merge(self, animation: AbstractAnimation) -> None:
590 """
591 A method for merging an animation with the previously merged animations.
593 Args:
594 animation: An animation to be merged with with previously merged animations.
596 Raises:
597 ValueError: If the type of an animation is already merged.
598 ValueError: If `Keyframe` is merged with different type of animation.
599 """
601 if isinstance(animation, Keyframe):
602 if self._dict:
603 raise ValueError("Keyframe cannot be merged with other animations.")
604 data = animation.build()
605 self._list.append(data)
606 else:
607 if self._list:
608 raise ValueError("Keyframe cannot be merged with other animations.")
609 data = self._validate(animation)
610 self._dict.update(data)
612 def _validate(self, animation: AbstractAnimation) -> dict:
613 if isinstance(animation, Snapshot):
614 raise ValueError("Snapshot cannot be merged with other animations.")
615 data = animation.build()
616 common_keys = set(data).intersection(self._dict)
618 if common_keys:
619 raise ValueError(f"{common_keys} is already merged.")
621 return data
623 def build(self) -> Union[dict, list]: # type: ignore
624 """
625 A method for returning a merged list of `Keyframes`
626 or a merged dictionary from different types of animations.
628 Returns:
629 A merged list of [Keyframes][ipyvizzu.animation.Keyframe] or
630 a merged dictionary from
631 [Data][ipyvizzu.animation.Data],
632 [Config][ipyvizzu.animation.Config] and
633 [Style][ipyvizzu.animation.Style] animations.
634 """
636 if self._dict:
637 return self._dict
638 return self._list