Coverage for src/ipyvizzustory/storylib/story.py: 100%
113 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-10 09:08 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-10 09:08 +0000
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 Args:
30 *animations: List of [Data][ipyvizzu.Data],
31 [Config][ipyvizzu.Config] and [Style][ipyvizzu.Style] objects.
32 A `Step` can contain each of the above once.
33 **anim_options: Animation options such as duration.
35 Raises:
36 ValueError: If `animations` are not set.
38 Example:
39 Initialize a step with a [Config][ipyvizzu.Config] object:
41 step = Step(
42 Config({"x": "Foo", "y": "Bar"})
43 )
44 """
46 super().__init__()
47 if not animations:
48 raise ValueError("No animation was set.")
49 self._update(*animations)
51 if anim_options:
52 self["animOptions"] = anim_options
54 def _update(self, *animations: Union[Data, Style, Config]) -> None:
55 for animation in animations:
56 if not animation or type(animation) not in [
57 Data,
58 Style,
59 Config,
60 ]: # pylint: disable=unidiomatic-typecheck
61 raise TypeError("Type must be Data, Style or Config.")
62 if type(animation) == Data: # pylint: disable=unidiomatic-typecheck
63 animation = DataFilter(animation)
65 builded_animation = animation.build()
66 common_keys = set(builded_animation).intersection(set(self))
67 if common_keys:
68 raise ValueError(f"Animation is already merged: {common_keys}")
69 self.update(builded_animation)
72class Slide(list):
73 """A class for representing a slide of a presentation story."""
75 def __init__(self, step: Optional[Step] = None):
76 """
77 Slide constructor.
79 Args:
80 step: The first step can also be added to the slide in the constructor.
82 Example:
83 Initialize a slide without step:
85 slide = Slide()
87 Initialize a slide with a step:
89 slide = Slide(
90 Step(
91 Config({"x": "Foo", "y": "Bar"})
92 )
93 )
94 """
96 super().__init__()
97 if step:
98 self.add_step(step)
100 def add_step(self, step: Step) -> None:
101 """
102 A method for adding a step for the slide.
104 Args:
105 step: The next step of the slide.
107 Raises:
108 TypeError: If the type of the `step` is not
109 [Step][ipyvizzustory.storylib.story.Step].
111 Example:
112 Add steps to a slide:
114 slide = Slide()
115 slide.add_step(
116 Step(
117 Config({"x": "Foo", "y": "Bar"})
118 )
119 )
120 slide.add_step(
121 Step(
122 Config({"color": "Foo", "x": "Baz", "geometry": "circle"})
123 )
124 )
125 """
127 if not step or type(step) != Step: # pylint: disable=unidiomatic-typecheck
128 raise TypeError("Type must be Step.")
129 self.append(step)
132class StorySize:
133 """A class for representing the size of a presentation story."""
135 def __init__(self, width: Optional[str] = None, height: Optional[str] = None):
136 """
137 StorySize constructor.
139 Args:
140 width: The width of a presentation story.
141 height: The height of a presentation story.
142 """
143 self._width = width
144 self._height = height
146 self._style = ""
147 if any([width is not None, height is not None]):
148 width = "" if width is None else f"width: {width};"
149 height = "" if height is None else f"height: {height};"
150 self._style = f"vp.style.cssText = '{width}{height}'"
152 @property
153 def width(self) -> Optional[str]:
154 """
155 A property for storing the width of a presentation story.
157 Returns:
158 The width of a presentation story.
159 """
161 return self._width
163 @property
164 def height(self) -> Optional[str]:
165 """
166 A property for storing the height of a presentation story.
168 Returns:
169 The height of a presentation story.
170 """
172 return self._height
174 @property
175 def style(self) -> str:
176 """
177 A property for storing the style of a presentation story.
179 Note:
180 If `width` and `height` are not set it returns an empty string.
182 Returns:
183 The cssText width and height of a presentation story.
184 """
186 return self._style
188 @staticmethod
189 def is_pixel(value: Any) -> bool:
190 """
191 A static method for checking the type of the given value.
193 Args:
194 value: The value to check.
196 Returns:
197 `True` if the value is pixel, `False` otherwise.
198 """
200 value_is_pixel = False
201 if isinstance(value, str):
202 if value.endswith("px"):
203 value_is_pixel = value[:-2].isnumeric()
204 return value_is_pixel
207class Story(dict):
208 """A class for representing a presentation story."""
210 # pylint: disable=too-many-instance-attributes
212 def __init__(self, data: Data, style: Optional[Style] = None):
213 """
214 Presentation Story constructor.
216 Args:
217 data: Data set for the whole presentation story.
218 After initialization `data` can not be modified,
219 but it can be filtered.
220 style: Style settings for the presentation story.
221 `style` can be changed at each presentation step.
223 Raises:
224 TypeError: If the type of the `data` is not `ipyvizzu.Data`.
225 TypeError: If the type of the `style` is not `ipyvizzu.Style`.
227 Example:
228 Initialize a story with data and without style:
230 data = Data()
231 data.add_series("Foo", ["Alice", "Bob", "Ted"])
232 data.add_series("Bar", [15, 32, 12])
233 data.add_series("Baz", [5, 3, 2])
235 story = Story(data=data)
236 """
238 super().__init__()
240 self._vizzu: Optional[str] = None
241 self._vizzu_story: str = VIZZU_STORY
242 self._start_slide: Optional[int] = None
244 self._size: StorySize = StorySize()
246 self._features: List[str] = []
247 self._events: List[str] = []
249 if not data or type(data) != Data: # pylint: disable=unidiomatic-typecheck
250 raise TypeError("Type must be Data.")
251 self.update(data.build())
253 if style:
254 if type(style) != Style: # pylint: disable=unidiomatic-typecheck
255 raise TypeError("Type must be Style.")
256 self.update(style.build())
258 self["slides"] = []
260 @property
261 def vizzu(self) -> Optional[str]:
262 """
263 A property for changing `vizzu` url.
265 Note:
266 If `None`, vizzu url is set by `vizzu-story`.
268 Returns:
269 `Vizzu` url.
270 """
272 return self._vizzu
274 @vizzu.setter
275 def vizzu(self, url: str) -> None:
276 self._vizzu = url
278 @property
279 def vizzu_story(self) -> str:
280 """
281 A property for changing `vizzu-story` url.
283 Returns:
284 `Vizzu-story` url.
285 """
287 return self._vizzu_story
289 @vizzu_story.setter
290 def vizzu_story(self, url: str) -> None:
291 self._vizzu_story = url
293 @property
294 def start_slide(self) -> Optional[int]:
295 """
296 A property for setting the starter slide.
298 Returns:
299 Number of the starter slide.
300 """
302 return self._start_slide
304 @start_slide.setter
305 def start_slide(self, number: int) -> None:
306 self._start_slide = number
308 def add_slide(self, slide: Slide) -> None:
309 """
310 A method for adding a slide for the story.
312 Args:
313 slide: The next slide of the story.
315 Raises:
316 TypeError: If the type of the `slide` is not
317 [Slide][ipyvizzustory.storylib.story.Slide].
319 Example:
320 Add a slide to the story:
322 story.add_slide(
323 Slide(
324 Step(
325 Config({"x": "Foo", "y": "Bar"})
326 )
327 )
328 )
329 """
331 if not slide or type(slide) != Slide: # pylint: disable=unidiomatic-typecheck
332 raise TypeError("Type must be Slide.")
333 self["slides"].append(slide)
335 def set_feature(self, name: str, enabled: bool) -> None:
336 """
337 A method for enabling or disabling a feature of the story.
339 Args:
340 name: The name of the feature.
341 enabled: `True` if enabled or `False` if disabled.
343 Example:
344 Set a feature of the story, for example enable the tooltip:
346 story.set_feature("tooltip", True)
347 """
349 self._features.append(f"chart.feature('{name}', {json.dumps(enabled)});")
351 def add_event(self, event: str, handler: str) -> None:
352 """
353 A method for creating and turning on an event handler.
355 Args:
356 event: The type of the event.
357 handler: The handler `JavaScript` expression as string.
359 Example:
360 Add an event handler to the story:
362 story.add_event("click", "alert(JSON.stringify(event.data));")
363 """
365 self._events.append(
366 f"chart.on('{event}', event => {{{' '.join(handler.split())}}});"
367 )
369 def set_size(
370 self, width: Optional[str] = None, height: Optional[str] = None
371 ) -> None:
372 """
373 A method for setting width/height settings.
375 Args:
376 width: The width of the presentation story.
377 height: The height of the presentation story.
379 Example:
380 Change the size of the story:
382 story.set_size("100%", "400px")
383 """
385 self._size = StorySize(width=width, height=height)
387 def _repr_html_(self) -> str:
388 return self.to_html()
390 def to_html(self) -> str:
391 """
392 A method for assembling the `HTML` code.
394 Returns:
395 The assembled `HTML` code as string.
396 """
398 vizzu_player_data = f"{json.dumps(self, cls=RawJavaScriptEncoder)}"
399 return DISPLAY_TEMPLATE.format(
400 id=uuid.uuid4().hex[:7],
401 vizzu_attribute=f'vizzu-url="{self._vizzu}"' if self._vizzu else "",
402 start_slide=f'start-slide="{self._start_slide}"'
403 if self._start_slide
404 else "",
405 vizzu_story=self._vizzu_story,
406 vizzu_player_data=vizzu_player_data,
407 chart_size=self._size.style,
408 chart_features=f"\n{DISPLAY_INDENT * 3}".join(self._features),
409 chart_events=f"\n{DISPLAY_INDENT * 3}".join(self._events),
410 )
412 def export_to_html(self, filename: PathLike) -> None:
413 """
414 A method for exporting the story into `HTML` file.
416 Args:
417 filename: The path of the target `HTML` file.
418 """
420 with open(filename, "w", encoding="utf8") as file_desc:
421 file_desc.write(self.to_html())