Coverage for src/ipyvizzustory/storylib/story.py: 100%
79 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-25 14:02 +0100
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-25 14:02 +0100
1"""A module for working with ipyvizzu-story presentations."""
3from typing import Optional, Union, List
4import json
5import uuid
7from ipyvizzu import RawJavaScriptEncoder, Data, Style, Config # , PlainAnimation
9from ipyvizzustory.storylib.animation import DataFilter
10from ipyvizzustory.storylib.template import (
11 VIZZU_STORY,
12 DISPLAY_TEMPLATE,
13 DISPLAY_INDENT,
14)
17class Step(dict):
18 """A class for representing a step of a slide."""
20 def __init__(
21 self,
22 *animations: Union[Data, Style, Config],
23 **anim_options: Optional[Union[str, int, float, dict]],
24 ):
25 super().__init__()
26 if not animations:
27 raise ValueError("No animation was set.")
28 self._update(*animations)
30 if anim_options:
31 # self["animOptions"] = PlainAnimation(**anim_options).build()
32 raise NotImplementedError("Anim options are not supported.")
34 def _update(self, *animations: Union[Data, Style, Config]) -> None:
35 for animation in animations:
36 if not animation or type(animation) not in [
37 Data,
38 Style,
39 Config,
40 ]: # pylint: disable=unidiomatic-typecheck
41 raise TypeError("Type must be Data, Style or Config.")
42 if type(animation) == Data: # pylint: disable=unidiomatic-typecheck
43 animation = DataFilter(animation)
45 builded_animation = animation.build()
46 common_keys = set(builded_animation).intersection(set(self))
47 if common_keys:
48 raise ValueError(f"Animation is already merged: {common_keys}")
49 self.update(builded_animation)
52class Slide(list):
53 """A class for representing a slide of a story."""
55 def __init__(self, step: Optional[Step] = None):
56 super().__init__()
57 if step:
58 self.add_step(step)
60 def add_step(self, step: Step) -> None:
61 """A method for adding a step for the slide."""
63 if not step or type(step) != Step: # pylint: disable=unidiomatic-typecheck
64 raise TypeError("Type must be Step.")
65 self.append(step)
68class StorySize:
69 """A class for representing a story's size."""
71 def __init__(self, width: Optional[str] = None, height: Optional[str] = None):
72 self._width = width
73 self._height = height
75 self._style = ""
76 if any([width is not None, height is not None]):
77 width = "" if width is None else f"width: {width};"
78 height = "" if height is None else f"height: {height};"
79 self._style = f"vizzuPlayer.style.cssText = '{width}{height}'"
81 @property
82 def width(self) -> Optional[str]:
83 """A property for storing story's width."""
85 return self._width
87 @property
88 def height(self) -> Optional[str]:
89 """A property for storing story's height."""
91 return self._height
93 @property
94 def style(self) -> str:
95 """A property for storing story's height."""
97 return self._style
100class Story(dict):
101 """A class for representing a presentation story."""
103 def __init__(self, data: Data, style: Optional[Style] = None):
104 super().__init__()
106 self._size: StorySize = StorySize()
108 self._features: List[str] = []
109 self._events: List[str] = []
111 if not data or type(data) != Data: # pylint: disable=unidiomatic-typecheck
112 raise TypeError("Type must be Data.")
113 self.update(data.build())
115 if style:
116 if type(style) != Style: # pylint: disable=unidiomatic-typecheck
117 raise TypeError("Type must be Style.")
118 self.update(style.build())
120 self["slides"] = []
122 def add_slide(self, slide: Slide) -> None:
123 """A method for adding a slide for the story."""
125 if not slide or type(slide) != Slide: # pylint: disable=unidiomatic-typecheck
126 raise TypeError("Type must be Slide.")
127 self["slides"].append(slide)
129 def set_feature(self, name: str, enabled: bool) -> None:
130 """A method for turning on/off a feature of the story."""
131 self._features.append(f"chart.feature('{name}', {json.dumps(enabled)});")
133 def add_event(self, event: str, handler: str) -> None:
134 """A method for creating and turning on an event handler."""
135 self._events.append(
136 f"chart.on('{event}', event => {{{' '.join(handler.split())}}});"
137 )
139 def set_size(
140 self, width: Optional[str] = None, height: Optional[str] = None
141 ) -> None:
142 """A method for setting width/height settings."""
144 self._size = StorySize(width=width, height=height)
146 def to_html(self) -> str:
147 """A method for assembling the html code."""
149 vizzu_player_data = f"{json.dumps(self, cls=RawJavaScriptEncoder)}"
150 return DISPLAY_TEMPLATE.format(
151 id=uuid.uuid4().hex[:7],
152 vizzu_story=VIZZU_STORY,
153 vizzu_player_data=vizzu_player_data,
154 chart_size=self._size.style,
155 chart_features=f"\n{DISPLAY_INDENT * 3}".join(self._features),
156 chart_events=f"\n{DISPLAY_INDENT * 3}".join(self._events),
157 )