Coverage for src/ipyvizzustory/storylib/story.py: 100%
92 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-25 13:56 +0100
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-25 13:56 +0100
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 `ipyvizzu.Data`, `ipyvizzu.Config` and `ipyvizzu.Style` objects.
34 A `Step` can contain each of the above once.
35 **anim_options: Animation options such as duration.
37 Raises:
38 ValueError: If `animations` are not set.
39 NotImplementedError: If `anim_options` are set.
40 """
42 super().__init__()
43 if not animations:
44 raise ValueError("No animation was set.")
45 self._update(*animations)
47 if anim_options:
48 # self["animOptions"] = PlainAnimation(**anim_options).build()
49 raise NotImplementedError("Anim options are not supported.")
51 def _update(self, *animations: Union[Data, Style, Config]) -> None:
52 for animation in animations:
53 if not animation or type(animation) not in [
54 Data,
55 Style,
56 Config,
57 ]: # pylint: disable=unidiomatic-typecheck
58 raise TypeError("Type must be Data, Style or Config.")
59 if type(animation) == Data: # pylint: disable=unidiomatic-typecheck
60 animation = DataFilter(animation)
62 builded_animation = animation.build()
63 common_keys = set(builded_animation).intersection(set(self))
64 if common_keys:
65 raise ValueError(f"Animation is already merged: {common_keys}")
66 self.update(builded_animation)
69class Slide(list):
70 """A class for representing a slide of a presentation story."""
72 def __init__(self, step: Optional[Step] = None):
73 """
74 Slide constructor.
76 Args:
77 step: The first step can also be added to the slide in the constructor.
78 """
80 super().__init__()
81 if step:
82 self.add_step(step)
84 def add_step(self, step: Step) -> None:
85 """
86 A method for adding a step for the slide.
88 Args:
89 step: The next step of the slide.
91 Raises:
92 TypeError: If the type of the `step` is not `Step`.
93 """
95 if not step or type(step) != Step: # pylint: disable=unidiomatic-typecheck
96 raise TypeError("Type must be Step.")
97 self.append(step)
100class StorySize:
101 """A class for representing the size of a presentation story."""
103 def __init__(self, width: Optional[str] = None, height: Optional[str] = None):
104 """
105 StorySize constructor.
107 Args:
108 width: The width of a presentation story.
109 height: The height of a presentation story.
110 """
111 self._width = width
112 self._height = height
114 self._style = ""
115 if any([width is not None, height is not None]):
116 width = "" if width is None else f"width: {width};"
117 height = "" if height is None else f"height: {height};"
118 self._style = f"vizzuPlayer.style.cssText = '{width}{height}'"
120 @property
121 def width(self) -> Optional[str]:
122 """
123 A property for storing the width of a presentation story.
125 Returns:
126 The width of a presentation story.
127 """
129 return self._width
131 @property
132 def height(self) -> Optional[str]:
133 """
134 A property for storing the height of a presentation story.
136 Returns:
137 The height of a presentation story.
138 """
140 return self._height
142 @property
143 def style(self) -> str:
144 """
145 A property for storing the style of a presentation story.
147 Note:
148 If `width` and `height` are not set it returns an empty string.
150 Returns:
151 The cssText width and height of a presentation story.
152 """
154 return self._style
156 @staticmethod
157 def is_pixel(value: Any) -> bool:
158 """
159 A static method for checking the type of the given value.
161 Args:
162 value: The value to check.
164 Returns:
165 True if the value is pixel, False otherwise.
166 """
168 value_is_pixel = False
169 if isinstance(value, str):
170 if value.endswith("px"):
171 value_is_pixel = value[:-2].isnumeric()
172 return value_is_pixel
175class Story(dict):
176 """A class for representing a presentation story."""
178 def __init__(self, data: Data, style: Optional[Style] = None):
179 """
180 Presentation Story constructor.
182 Args:
183 data: Data set for the whole presentation story.
184 After initialization `data` can not be modified,
185 but it can be filtered.
186 style: Style settings for the presentation story.
187 `style` can be changed at each presentation step.
189 Raises:
190 TypeError: If the type of the `data` is not `ipyvizzu.Data`.
191 TypeError: If the type of the `style` is not `ipyvizzu.Style`.
192 """
194 super().__init__()
196 self._size: StorySize = StorySize()
198 self._features: List[str] = []
199 self._events: List[str] = []
201 if not data or type(data) != Data: # pylint: disable=unidiomatic-typecheck
202 raise TypeError("Type must be Data.")
203 self.update(data.build())
205 if style:
206 if type(style) != Style: # pylint: disable=unidiomatic-typecheck
207 raise TypeError("Type must be Style.")
208 self.update(style.build())
210 self["slides"] = []
212 def add_slide(self, slide: Slide) -> None:
213 """
214 A method for adding a slide for the story.
216 Args:
217 slide: The next slide of the story.
219 Raises:
220 TypeError: If the type of the `slide` is not `Slide`.
221 """
223 if not slide or type(slide) != Slide: # pylint: disable=unidiomatic-typecheck
224 raise TypeError("Type must be Slide.")
225 self["slides"].append(slide)
227 def set_feature(self, name: str, enabled: bool) -> None:
228 """
229 A method for enabling or disabling a feature of the story.
231 Args:
232 name: The name of the feature.
233 enabled: True if enabled or False if disabled.
234 """
236 self._features.append(f"chart.feature('{name}', {json.dumps(enabled)});")
238 def add_event(self, event: str, handler: str) -> None:
239 """
240 A method for creating and turning on an event handler.
242 Args:
243 event: The name of the event.
244 handler: The handler JavaScript expression as string.
245 """
247 self._events.append(
248 f"chart.on('{event}', event => {{{' '.join(handler.split())}}});"
249 )
251 def set_size(
252 self, width: Optional[str] = None, height: Optional[str] = None
253 ) -> None:
254 """
255 A method for setting width/height settings.
257 Args:
258 width: The width of the presentation story.
259 height: The height of the presentation story.
260 """
262 self._size = StorySize(width=width, height=height)
264 def _repr_html_(self) -> str:
265 return self.to_html()
267 def to_html(self) -> str:
268 """
269 A method for assembling the html code.
271 Returns:
272 The assembled html code as string.
273 """
275 vizzu_player_data = f"{json.dumps(self, cls=RawJavaScriptEncoder)}"
276 return DISPLAY_TEMPLATE.format(
277 id=uuid.uuid4().hex[:7],
278 vizzu_story=VIZZU_STORY,
279 vizzu_player_data=vizzu_player_data,
280 chart_size=self._size.style,
281 chart_features=f"\n{DISPLAY_INDENT * 3}".join(self._features),
282 chart_events=f"\n{DISPLAY_INDENT * 3}".join(self._events),
283 )
285 def export_to_html(self, filename: PathLike) -> None:
286 """
287 A method for exporting the story into html file.
289 Args:
290 filename: The path of the target html file.
291 """
293 with open(filename, "w", encoding="utf8") as file_desc:
294 file_desc.write(self.to_html())