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

1"""A module for working with presentation stories.""" 

2 

3from typing import Optional, Union, List, Any 

4from os import PathLike 

5import json 

6import uuid 

7 

8from ipyvizzu import RawJavaScriptEncoder, Data, Style, Config # , PlainAnimation 

9 

10from ipyvizzustory.storylib.animation import DataFilter 

11from ipyvizzustory.storylib.template import ( 

12 VIZZU_STORY, 

13 DISPLAY_TEMPLATE, 

14 DISPLAY_INDENT, 

15) 

16 

17 

18class Step(dict): 

19 """A class for representing a step of a slide.""" 

20 

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. 

28 

29 Note: 

30 Do not set `anim_options` argument, it will raise `NotImplementedError` error. 

31 

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. 

36 

37 Raises: 

38 ValueError: If `animations` are not set. 

39 NotImplementedError: If `anim_options` are set. 

40 """ 

41 

42 super().__init__() 

43 if not animations: 

44 raise ValueError("No animation was set.") 

45 self._update(*animations) 

46 

47 if anim_options: 

48 # self["animOptions"] = PlainAnimation(**anim_options).build() 

49 raise NotImplementedError("Anim options are not supported.") 

50 

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) 

61 

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) 

67 

68 

69class Slide(list): 

70 """A class for representing a slide of a presentation story.""" 

71 

72 def __init__(self, step: Optional[Step] = None): 

73 """ 

74 Slide constructor. 

75 

76 Args: 

77 step: The first step can also be added to the slide in the constructor. 

78 """ 

79 

80 super().__init__() 

81 if step: 

82 self.add_step(step) 

83 

84 def add_step(self, step: Step) -> None: 

85 """ 

86 A method for adding a step for the slide. 

87 

88 Args: 

89 step: The next step of the slide. 

90 

91 Raises: 

92 TypeError: If the type of the `step` is not `Step`. 

93 """ 

94 

95 if not step or type(step) != Step: # pylint: disable=unidiomatic-typecheck 

96 raise TypeError("Type must be Step.") 

97 self.append(step) 

98 

99 

100class StorySize: 

101 """A class for representing the size of a presentation story.""" 

102 

103 def __init__(self, width: Optional[str] = None, height: Optional[str] = None): 

104 """ 

105 StorySize constructor. 

106 

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 

113 

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}'" 

119 

120 @property 

121 def width(self) -> Optional[str]: 

122 """ 

123 A property for storing the width of a presentation story. 

124 

125 Returns: 

126 The width of a presentation story. 

127 """ 

128 

129 return self._width 

130 

131 @property 

132 def height(self) -> Optional[str]: 

133 """ 

134 A property for storing the height of a presentation story. 

135 

136 Returns: 

137 The height of a presentation story. 

138 """ 

139 

140 return self._height 

141 

142 @property 

143 def style(self) -> str: 

144 """ 

145 A property for storing the style of a presentation story. 

146 

147 Note: 

148 If `width` and `height` are not set it returns an empty string. 

149 

150 Returns: 

151 The cssText width and height of a presentation story. 

152 """ 

153 

154 return self._style 

155 

156 @staticmethod 

157 def is_pixel(value: Any) -> bool: 

158 """ 

159 A static method for checking the type of the given value. 

160 

161 Args: 

162 value: The value to check. 

163 

164 Returns: 

165 True if the value is pixel, False otherwise. 

166 """ 

167 

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 

173 

174 

175class Story(dict): 

176 """A class for representing a presentation story.""" 

177 

178 def __init__(self, data: Data, style: Optional[Style] = None): 

179 """ 

180 Presentation Story constructor. 

181 

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. 

188 

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 """ 

193 

194 super().__init__() 

195 

196 self._size: StorySize = StorySize() 

197 

198 self._features: List[str] = [] 

199 self._events: List[str] = [] 

200 

201 if not data or type(data) != Data: # pylint: disable=unidiomatic-typecheck 

202 raise TypeError("Type must be Data.") 

203 self.update(data.build()) 

204 

205 if style: 

206 if type(style) != Style: # pylint: disable=unidiomatic-typecheck 

207 raise TypeError("Type must be Style.") 

208 self.update(style.build()) 

209 

210 self["slides"] = [] 

211 

212 def add_slide(self, slide: Slide) -> None: 

213 """ 

214 A method for adding a slide for the story. 

215 

216 Args: 

217 slide: The next slide of the story. 

218 

219 Raises: 

220 TypeError: If the type of the `slide` is not `Slide`. 

221 """ 

222 

223 if not slide or type(slide) != Slide: # pylint: disable=unidiomatic-typecheck 

224 raise TypeError("Type must be Slide.") 

225 self["slides"].append(slide) 

226 

227 def set_feature(self, name: str, enabled: bool) -> None: 

228 """ 

229 A method for enabling or disabling a feature of the story. 

230 

231 Args: 

232 name: The name of the feature. 

233 enabled: True if enabled or False if disabled. 

234 """ 

235 

236 self._features.append(f"chart.feature('{name}', {json.dumps(enabled)});") 

237 

238 def add_event(self, event: str, handler: str) -> None: 

239 """ 

240 A method for creating and turning on an event handler. 

241 

242 Args: 

243 event: The name of the event. 

244 handler: The handler JavaScript expression as string. 

245 """ 

246 

247 self._events.append( 

248 f"chart.on('{event}', event => {{{' '.join(handler.split())}}});" 

249 ) 

250 

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. 

256 

257 Args: 

258 width: The width of the presentation story. 

259 height: The height of the presentation story. 

260 """ 

261 

262 self._size = StorySize(width=width, height=height) 

263 

264 def _repr_html_(self) -> str: 

265 return self.to_html() 

266 

267 def to_html(self) -> str: 

268 """ 

269 A method for assembling the html code. 

270 

271 Returns: 

272 The assembled html code as string. 

273 """ 

274 

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 ) 

284 

285 def export_to_html(self, filename: PathLike) -> None: 

286 """ 

287 A method for exporting the story into html file. 

288 

289 Args: 

290 filename: The path of the target html file. 

291 """ 

292 

293 with open(filename, "w", encoding="utf8") as file_desc: 

294 file_desc.write(self.to_html())