Coverage for src/ipyvizzu/animation.py: 100%
113 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-25 15:04 +0100
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-25 15:04 +0100
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
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 Animation:
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, Animation):
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, Animation):
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(Animation, 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(Animation):
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 Snapshot(Animation):
474 """
475 A class for representing snapshot animation.
476 It can build the snapshot id of the chart.
477 """
479 def __init__(self, name: str):
480 """
481 Snapshot constructor.
483 Args:
484 name: A snapshot id.
485 """
487 self._name = name
489 def dump(self) -> str:
490 """
491 A method for overwriting the
492 [Animation.dump][ipyvizzu.animation.Animation.dump] method.
493 It dumps the stored snapshot id as a string.
495 Returns:
496 An str that contains the stored snapshot id.
497 """
499 return f"'{self._name}'"
501 def build(self):
502 """
503 A method for preventing to merge [Snapshot][ipyvizzu.animation.Snapshot]
504 with other animations.
506 Raises:
507 NotImplementedError: If the [build][ipyvizzu.animation.Snapshot.build] method
508 has been called, because [Snapshot][ipyvizzu.animation.Snapshot]
509 cannot be merged with other animations.
510 """
512 raise NotImplementedError("Snapshot cannot be merged with other animations")
515class AnimationMerger(dict, Animation):
516 """A class for merging different types of animations."""
518 def merge(self, animation: Animation) -> None:
519 """
520 A method for merging an animation with the previously merged animations.
522 Args:
523 animation: An animation to be merged with with previously merged animations.
525 Raises:
526 ValueError: If the type of an animation is already merged.
527 """
529 data = self._validate(animation)
530 self.update(data)
532 def _validate(self, animation: Animation) -> dict:
533 data = animation.build()
534 common_keys = set(data).intersection(self)
536 if common_keys:
537 raise ValueError(f"Animation is already merged: {common_keys}")
539 return data
541 def build(self) -> dict:
542 """
543 A method for returning a merged dictionary from different types of animations.
545 Returns:
546 A merged dictionary from
547 [Data][ipyvizzu.animation.Data],
548 [Config][ipyvizzu.animation.Config] and
549 [Style][ipyvizzu.animation.Style] animations.
550 """
552 return self