Coverage for src/ipyvizzustory/storylib/story.py: 100%
106 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-04-05 08:13 +0200
« prev ^ index » next coverage.py v7.2.2, created at 2023-04-05 08:13 +0200
1"""A module for working with presentation stories."""
3from typing import Optional, Union, List, Any
4from os import PathLike
5import json
6import uuid
8from ipyvizzu import RawJavaScriptEncoder, Data, Style, Config # , PlainAnimation
10from ipyvizzustory.storylib.animation import DataFilter
11from ipyvizzustory.storylib.template import (
12 VIZZU_STORY,
13 DISPLAY_TEMPLATE,
14 DISPLAY_INDENT,
15)
18class Step(dict):
19 """A class for representing a step of a slide."""
21 def __init__(
22 self,
23 *animations: Union[Data, Style, Config],
24 **anim_options: Optional[Union[str, int, float, dict]],
25 ):
26 """
27 Step constructor.
29 Note:
30 Do not set `anim_options` argument, it will raise `NotImplementedError` error.
32 Args:
33 *animations: List of [Data][ipyvizzu.Data],
34 [Config][ipyvizzu.Config] and [Style][ipyvizzu.Style] objects.
35 A `Step` can contain each of the above once.
36 **anim_options: Animation options such as duration.
38 Raises:
39 ValueError: If `animations` are not set.
40 NotImplementedError: If `anim_options` are set.
42 Example:
43 Initialize a step with a [Config][ipyvizzu.Config] object:
45 step = Step(
46 Config({"x": "Foo", "y": "Bar"})
47 )
48 """
50 super().__init__()
51 if not animations:
52 raise ValueError("No animation was set.")
53 self._update(*animations)
55 if anim_options:
56 # self["animOptions"] = PlainAnimation(**anim_options).build()
57 raise NotImplementedError("Anim options are not supported.")
59 def _update(self, *animations: Union[Data, Style, Config]) -> None:
60 for animation in animations:
61 if not animation or type(animation) not in [
62 Data,
63 Style,
64 Config,
65 ]: # pylint: disable=unidiomatic-typecheck
66 raise TypeError("Type must be Data, Style or Config.")
67 if type(animation) == Data: # pylint: disable=unidiomatic-typecheck
68 animation = DataFilter(animation)
70 builded_animation = animation.build()
71 common_keys = set(builded_animation).intersection(set(self))
72 if common_keys:
73 raise ValueError(f"Animation is already merged: {common_keys}")
74 self.update(builded_animation)
77class Slide(list):
78 """A class for representing a slide of a presentation story."""
80 def __init__(self, step: Optional[Step] = None):
81 """
82 Slide constructor.
84 Args:
85 step: The first step can also be added to the slide in the constructor.
87 Example:
88 Initialize a slide without step:
90 slide = Slide()
92 Initialize a slide with a step:
94 slide = Slide(
95 Step(
96 Config({"x": "Foo", "y": "Bar"})
97 )
98 )
99 """
101 super().__init__()
102 if step:
103 self.add_step(step)
105 def add_step(self, step: Step) -> None:
106 """
107 A method for adding a step for the slide.
109 Args:
110 step: The next step of the slide.
112 Raises:
113 TypeError: If the type of the `step` is not
114 [Step][ipyvizzustory.storylib.story.Step].
116 Example:
117 Add steps to a slide:
119 slide = Slide()
120 slide.add_step(
121 Step(
122 Config({"x": "Foo", "y": "Bar"})
123 )
124 )
125 slide.add_step(
126 Step(
127 Config({"color": "Foo", "x": "Baz", "geometry": "circle"})
128 )
129 )
130 """
132 if not step or type(step) != Step: # pylint: disable=unidiomatic-typecheck
133 raise TypeError("Type must be Step.")
134 self.append(step)
137class StorySize:
138 """A class for representing the size of a presentation story."""
140 def __init__(self, width: Optional[str] = None, height: Optional[str] = None):
141 """
142 StorySize constructor.
144 Args:
145 width: The width of a presentation story.
146 height: The height of a presentation story.
147 """
148 self._width = width
149 self._height = height
151 self._style = ""
152 if any([width is not None, height is not None]):
153 width = "" if width is None else f"width: {width};"
154 height = "" if height is None else f"height: {height};"
155 self._style = f"vp.style.cssText = '{width}{height}'"
157 @property
158 def width(self) -> Optional[str]:
159 """
160 A property for storing the width of a presentation story.
162 Returns:
163 The width of a presentation story.
164 """
166 return self._width
168 @property
169 def height(self) -> Optional[str]:
170 """
171 A property for storing the height of a presentation story.
173 Returns:
174 The height of a presentation story.
175 """
177 return self._height
179 @property
180 def style(self) -> str:
181 """
182 A property for storing the style of a presentation story.
184 Note:
185 If `width` and `height` are not set it returns an empty string.
187 Returns:
188 The cssText width and height of a presentation story.
189 """
191 return self._style
193 @staticmethod
194 def is_pixel(value: Any) -> bool:
195 """
196 A static method for checking the type of the given value.
198 Args:
199 value: The value to check.
201 Returns:
202 `True` if the value is pixel, `False` otherwise.
203 """
205 value_is_pixel = False
206 if isinstance(value, str):
207 if value.endswith("px"):
208 value_is_pixel = value[:-2].isnumeric()
209 return value_is_pixel
212class Story(dict):
213 """A class for representing a presentation story."""
215 def __init__(self, data: Data, style: Optional[Style] = None):
216 """
217 Presentation Story constructor.
219 Args:
220 data: Data set for the whole presentation story.
221 After initialization `data` can not be modified,
222 but it can be filtered.
223 style: Style settings for the presentation story.
224 `style` can be changed at each presentation step.
226 Raises:
227 TypeError: If the type of the `data` is not `ipyvizzu.Data`.
228 TypeError: If the type of the `style` is not `ipyvizzu.Style`.
230 Example:
231 Initialize a story with data and without style:
233 data = Data()
234 data.add_series("Foo", ["Alice", "Bob", "Ted"])
235 data.add_series("Bar", [15, 32, 12])
236 data.add_series("Baz", [5, 3, 2])
238 story = Story(data=data)
239 """
241 super().__init__()
243 self._vizzu: Optional[str] = None
244 self._vizzu_story: str = VIZZU_STORY
246 self._size: StorySize = StorySize()
248 self._features: List[str] = []
249 self._events: List[str] = []
251 if not data or type(data) != Data: # pylint: disable=unidiomatic-typecheck
252 raise TypeError("Type must be Data.")
253 self.update(data.build())
255 if style:
256 if type(style) != Style: # pylint: disable=unidiomatic-typecheck
257 raise TypeError("Type must be Style.")
258 self.update(style.build())
260 self["slides"] = []
262 @property
263 def vizzu(self) -> Optional[str]:
264 """
265 A property for changing `vizzu` url.
267 Note:
268 If `None`, vizzu url is set by `vizzu-story`.
270 Returns:
271 `Vizzu` url.
272 """
274 return self._vizzu
276 @vizzu.setter
277 def vizzu(self, url: str) -> None:
278 self._vizzu = url
280 @property
281 def vizzu_story(self) -> str:
282 """
283 A property for changing `vizzu-story` url.
285 Returns:
286 `Vizzu-story` url.
287 """
289 return self._vizzu_story
291 @vizzu_story.setter
292 def vizzu_story(self, url: str) -> None:
293 self._vizzu_story = url
295 def add_slide(self, slide: Slide) -> None:
296 """
297 A method for adding a slide for the story.
299 Args:
300 slide: The next slide of the story.
302 Raises:
303 TypeError: If the type of the `slide` is not
304 [Slide][ipyvizzustory.storylib.story.Slide].
306 Example:
307 Add a slide to the story:
309 story.add_slide(
310 Slide(
311 Step(
312 Config({"x": "Foo", "y": "Bar"})
313 )
314 )
315 )
316 """
318 if not slide or type(slide) != Slide: # pylint: disable=unidiomatic-typecheck
319 raise TypeError("Type must be Slide.")
320 self["slides"].append(slide)
322 def set_feature(self, name: str, enabled: bool) -> None:
323 """
324 A method for enabling or disabling a feature of the story.
326 Args:
327 name: The name of the feature.
328 enabled: `True` if enabled or `False` if disabled.
330 Example:
331 Set a feature of the story, for example enable the tooltip:
333 story.set_feature("tooltip", True)
334 """
336 self._features.append(f"chart.feature('{name}', {json.dumps(enabled)});")
338 def add_event(self, event: str, handler: str) -> None:
339 """
340 A method for creating and turning on an event handler.
342 Args:
343 event: The type of the event.
344 handler: The handler `JavaScript` expression as string.
346 Example:
347 Add an event handler to the story:
349 story.add_event("click", "alert(JSON.stringify(event.data));")
350 """
352 self._events.append(
353 f"chart.on('{event}', event => {{{' '.join(handler.split())}}});"
354 )
356 def set_size(
357 self, width: Optional[str] = None, height: Optional[str] = None
358 ) -> None:
359 """
360 A method for setting width/height settings.
362 Args:
363 width: The width of the presentation story.
364 height: The height of the presentation story.
366 Example:
367 Change the size of the story:
369 story.set_size("100%", "400px")
370 """
372 self._size = StorySize(width=width, height=height)
374 def _repr_html_(self) -> str:
375 return self.to_html()
377 def to_html(self) -> str:
378 """
379 A method for assembling the `HTML` code.
381 Returns:
382 The assembled `HTML` code as string.
383 """
385 vizzu_player_data = f"{json.dumps(self, cls=RawJavaScriptEncoder)}"
386 return DISPLAY_TEMPLATE.format(
387 id=uuid.uuid4().hex[:7],
388 vizzu_attribute=f'vizzu-url="{self._vizzu}"' if self._vizzu else "",
389 vizzu_story=self._vizzu_story,
390 vizzu_player_data=vizzu_player_data,
391 chart_size=self._size.style,
392 chart_features=f"\n{DISPLAY_INDENT * 3}".join(self._features),
393 chart_events=f"\n{DISPLAY_INDENT * 3}".join(self._events),
394 )
396 def export_to_html(self, filename: PathLike) -> None:
397 """
398 A method for exporting the story into `HTML` file.
400 Args:
401 filename: The path of the target `HTML` file.
402 """
404 with open(filename, "w", encoding="utf8") as file_desc:
405 file_desc.write(self.to_html())