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

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 [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. 

37 

38 Raises: 

39 ValueError: If `animations` are not set. 

40 NotImplementedError: If `anim_options` are set. 

41 

42 Example: 

43 Initialize a step with a [Config][ipyvizzu.Config] object: 

44 

45 step = Step( 

46 Config({"x": "Foo", "y": "Bar"}) 

47 ) 

48 """ 

49 

50 super().__init__() 

51 if not animations: 

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

53 self._update(*animations) 

54 

55 if anim_options: 

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

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

58 

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) 

69 

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) 

75 

76 

77class Slide(list): 

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

79 

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

81 """ 

82 Slide constructor. 

83 

84 Args: 

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

86 

87 Example: 

88 Initialize a slide without step: 

89 

90 slide = Slide() 

91 

92 Initialize a slide with a step: 

93 

94 slide = Slide( 

95 Step( 

96 Config({"x": "Foo", "y": "Bar"}) 

97 ) 

98 ) 

99 """ 

100 

101 super().__init__() 

102 if step: 

103 self.add_step(step) 

104 

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

106 """ 

107 A method for adding a step for the slide. 

108 

109 Args: 

110 step: The next step of the slide. 

111 

112 Raises: 

113 TypeError: If the type of the `step` is not 

114 [Step][ipyvizzustory.storylib.story.Step]. 

115 

116 Example: 

117 Add steps to a slide: 

118 

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

131 

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

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

134 self.append(step) 

135 

136 

137class StorySize: 

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

139 

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

141 """ 

142 StorySize constructor. 

143 

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 

150 

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

156 

157 @property 

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

159 """ 

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

161 

162 Returns: 

163 The width of a presentation story. 

164 """ 

165 

166 return self._width 

167 

168 @property 

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

170 """ 

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

172 

173 Returns: 

174 The height of a presentation story. 

175 """ 

176 

177 return self._height 

178 

179 @property 

180 def style(self) -> str: 

181 """ 

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

183 

184 Note: 

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

186 

187 Returns: 

188 The cssText width and height of a presentation story. 

189 """ 

190 

191 return self._style 

192 

193 @staticmethod 

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

195 """ 

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

197 

198 Args: 

199 value: The value to check. 

200 

201 Returns: 

202 `True` if the value is pixel, `False` otherwise. 

203 """ 

204 

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 

210 

211 

212class Story(dict): 

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

214 

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

216 """ 

217 Presentation Story constructor. 

218 

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. 

225 

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`. 

229 

230 Example: 

231 Initialize a story with data and without style: 

232 

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]) 

237 

238 story = Story(data=data) 

239 """ 

240 

241 super().__init__() 

242 

243 self._vizzu: Optional[str] = None 

244 self._vizzu_story: str = VIZZU_STORY 

245 

246 self._size: StorySize = StorySize() 

247 

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

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

250 

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

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

253 self.update(data.build()) 

254 

255 if style: 

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

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

258 self.update(style.build()) 

259 

260 self["slides"] = [] 

261 

262 @property 

263 def vizzu(self) -> Optional[str]: 

264 """ 

265 A property for changing `vizzu` url. 

266 

267 Note: 

268 If `None`, vizzu url is set by `vizzu-story`. 

269 

270 Returns: 

271 `Vizzu` url. 

272 """ 

273 

274 return self._vizzu 

275 

276 @vizzu.setter 

277 def vizzu(self, url: str) -> None: 

278 self._vizzu = url 

279 

280 @property 

281 def vizzu_story(self) -> str: 

282 """ 

283 A property for changing `vizzu-story` url. 

284 

285 Returns: 

286 `Vizzu-story` url. 

287 """ 

288 

289 return self._vizzu_story 

290 

291 @vizzu_story.setter 

292 def vizzu_story(self, url: str) -> None: 

293 self._vizzu_story = url 

294 

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

296 """ 

297 A method for adding a slide for the story. 

298 

299 Args: 

300 slide: The next slide of the story. 

301 

302 Raises: 

303 TypeError: If the type of the `slide` is not 

304 [Slide][ipyvizzustory.storylib.story.Slide]. 

305 

306 Example: 

307 Add a slide to the story: 

308 

309 story.add_slide( 

310 Slide( 

311 Step( 

312 Config({"x": "Foo", "y": "Bar"}) 

313 ) 

314 ) 

315 ) 

316 """ 

317 

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

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

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

321 

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

323 """ 

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

325 

326 Args: 

327 name: The name of the feature. 

328 enabled: `True` if enabled or `False` if disabled. 

329 

330 Example: 

331 Set a feature of the story, for example enable the tooltip: 

332 

333 story.set_feature("tooltip", True) 

334 """ 

335 

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

337 

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

339 """ 

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

341 

342 Args: 

343 event: The type of the event. 

344 handler: The handler `JavaScript` expression as string. 

345 

346 Example: 

347 Add an event handler to the story: 

348 

349 story.add_event("click", "alert(JSON.stringify(event.data));") 

350 """ 

351 

352 self._events.append( 

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

354 ) 

355 

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. 

361 

362 Args: 

363 width: The width of the presentation story. 

364 height: The height of the presentation story. 

365 

366 Example: 

367 Change the size of the story: 

368 

369 story.set_size("100%", "400px") 

370 """ 

371 

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

373 

374 def _repr_html_(self) -> str: 

375 return self.to_html() 

376 

377 def to_html(self) -> str: 

378 """ 

379 A method for assembling the `HTML` code. 

380 

381 Returns: 

382 The assembled `HTML` code as string. 

383 """ 

384 

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 ) 

395 

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

397 """ 

398 A method for exporting the story into `HTML` file. 

399 

400 Args: 

401 filename: The path of the target `HTML` file. 

402 """ 

403 

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

405 file_desc.write(self.to_html())