Coverage for src/ipyvizzu/chart.py: 100%

112 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-02-26 10:12 +0000

1"""A module for working with Vizzu charts.""" 

2 

3import pkgutil 

4import uuid 

5from typing import List, Optional, Union 

6 

7from IPython.display import display_javascript # type: ignore 

8from IPython import get_ipython # type: ignore 

9 

10from ipyvizzu.animation import AbstractAnimation, Snapshot, AnimationMerger 

11from ipyvizzu.animationcontrol import AnimationControl 

12from ipyvizzu.method import Animate, Feature, Plugin, Store, EventOn, EventOff, Log 

13from ipyvizzu.template import ( 

14 ChartProperty, 

15 DisplayTarget, 

16 DisplayTemplate, 

17 VIZZU as VIZZU_URL, 

18) 

19from ipyvizzu.event import EventHandler 

20from ipyvizzu.__version__ import __version__ 

21 

22 

23class Chart: 

24 """A class for representing a wrapper over Vizzu chart.""" 

25 

26 # pylint: disable=too-many-instance-attributes 

27 

28 VIZZU: str = VIZZU_URL 

29 """A variable for storing the default url of the `vizzu` package.""" 

30 

31 def __init__( 

32 self, 

33 vizzu: str = VIZZU, 

34 width: str = "800px", 

35 height: str = "480px", 

36 display: Union[DisplayTarget, str] = DisplayTarget.ACTUAL, 

37 ): 

38 """ 

39 Chart constructor. 

40 

41 Args: 

42 vizzu: The url of Vizzu JavaScript package. 

43 width: The width of the chart. 

44 height: The height of the chart. 

45 display: The display behaviour of the chart. 

46 """ 

47 

48 self._chart_id: str = uuid.uuid4().hex[:7] 

49 

50 self._vizzu: str = vizzu 

51 self._width: str = width 

52 self._height: str = height 

53 

54 self._display_target: DisplayTarget = DisplayTarget(display) 

55 self._calls: List[str] = [] 

56 self._last_anim: Optional[str] = None 

57 self._showed: bool = False 

58 

59 self._initialized: bool = False 

60 self._analytics: bool = True 

61 self._scroll_into_view: bool = False 

62 

63 @staticmethod 

64 def _register_events() -> None: 

65 ipy = get_ipython() 

66 if ipy is not None: 

67 ipy.events.register("pre_run_cell", Chart._register_pre_run_cell) 

68 

69 @staticmethod 

70 def _register_pre_run_cell( 

71 *args, **kwargs # pylint: disable=unused-argument 

72 ) -> None: 

73 display_javascript(DisplayTemplate.CLEAR_INHIBITSCROLL, raw=True) 

74 

75 @property 

76 def analytics(self) -> bool: 

77 """ 

78 A property for enabling/disabling the usage statistics feature. 

79 

80 The usage statistics feature allows aggregate usage data collection 

81 using Plausible's algorithm. 

82 Enabling this feature helps us follow the progress and overall trends of our library, 

83 allowing us to focus our resources effectively and better serve our users. 

84 

85 We do not track, collect, or store any personal data or personally identifiable information. 

86 All data is isolated to a single day, a single site, and a single device only. 

87 

88 Please note that even when this feature is enabled, 

89 publishing anything made with `ipyvizzu` remains GDPR compatible. 

90 

91 Returns: 

92 The value of the property (default `True`). 

93 """ 

94 

95 return self._analytics 

96 

97 @analytics.setter 

98 def analytics(self, analytics: Optional[bool]) -> None: 

99 self._analytics = bool(analytics) 

100 if self._initialized: 

101 self._display_analytics() 

102 

103 @property 

104 def scroll_into_view(self) -> bool: 

105 """ 

106 A property for turning on/off the scroll into view feature. 

107 

108 Returns: 

109 The value of the property (default `False`). 

110 """ 

111 

112 return self._scroll_into_view 

113 

114 @scroll_into_view.setter 

115 def scroll_into_view(self, scroll_into_view: Optional[bool]) -> None: 

116 self._scroll_into_view = bool(scroll_into_view) 

117 

118 @property 

119 def control(self) -> AnimationControl: 

120 """ 

121 A property for returning a control object of the last animation. 

122 

123 Raises: 

124 AssertionError: If called before any animation plays. 

125 

126 Returns: 

127 The control object of the last animation. 

128 """ 

129 assert self._last_anim, "must be used after an animation." 

130 return AnimationControl(self._chart_id, self._last_anim, self._display) 

131 

132 def initializing(self) -> None: 

133 """A method for initializing the chart.""" 

134 

135 if not self._initialized: 

136 self._initialized = True 

137 self._display_ipyvizzujs() 

138 self._display_analytics() 

139 if self._display_target != DisplayTarget.MANUAL: 

140 Chart._register_events() 

141 self._display_chart() 

142 

143 def _display_ipyvizzujs(self) -> None: 

144 ipyvizzurawjs = pkgutil.get_data(__name__, "templates/ipyvizzu.js") 

145 ipyvizzujs = ipyvizzurawjs.decode("utf-8").replace( # type: ignore 

146 "'__version__'", f"'{__version__}'" 

147 ) 

148 self._display(DisplayTemplate.IPYVIZZUJS.format(ipyvizzujs=ipyvizzujs)) 

149 

150 def _display_analytics(self) -> None: 

151 self._display( 

152 DisplayTemplate.CHANGE_ANALYTICS_TO.format( 

153 analytics=str(self._analytics).lower() 

154 ) 

155 ) 

156 

157 def _display_chart(self) -> None: 

158 self._display( 

159 DisplayTemplate.INIT.format( 

160 chart_id=self._chart_id, 

161 vizzu=self._vizzu, 

162 div_width=self._width, 

163 div_height=self._height, 

164 ) 

165 ) 

166 

167 def animate( 

168 self, 

169 *animations: AbstractAnimation, 

170 **options: Optional[Union[str, int, float, dict]], 

171 ) -> None: 

172 """ 

173 A method for changing the state of the chart. 

174 

175 Args: 

176 *animations: 

177 List of AbstractAnimation inherited objects such as [Data][ipyvizzu.animation.Data], 

178 [Config][ipyvizzu.animation.Config] and [Style][ipyvizzu.animation.Style]. 

179 **options: Dictionary of animation options for example `duration=1`. 

180 For information on all available animation options see the 

181 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/interfaces/types_anim.Options/). 

182 

183 Raises: 

184 ValueError: If `animations` is not set. 

185 

186 Example: 

187 Reset the chart styles: 

188 

189 chart.animate(Style(None)) 

190 """ # pylint: disable=line-too-long 

191 

192 if not animations: 

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

194 

195 animation = AnimationMerger.merge_animations(animations) 

196 animate = Animate(animation, options) 

197 

198 self._last_anim = uuid.uuid4().hex[:7] 

199 self._display( 

200 DisplayTemplate.ANIMATE.format( 

201 display_target=self._display_target.value, 

202 chart_id=self._chart_id, 

203 anim_id=self._last_anim, 

204 scroll=str(self._scroll_into_view).lower(), 

205 **animate.dump(), 

206 ) 

207 ) 

208 

209 def feature(self, name: str, enabled: bool) -> None: 

210 """ 

211 A method for turning on/off features of the chart. 

212 

213 Args: 

214 name: 

215 The name of the chart feature. 

216 For information on all available features see the 

217 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/modules/vizzu/#feature). 

218 enabled: The new state of the chart feature. 

219 

220 Example: 

221 Turn on `tooltip` of the chart: 

222 

223 chart.feature("tooltip", True) 

224 """ # pylint: disable=line-too-long 

225 

226 self._display( 

227 DisplayTemplate.FEATURE.format( 

228 chart_id=self._chart_id, 

229 **Feature(name, enabled).dump(), 

230 ) 

231 ) 

232 

233 def plugin( 

234 self, 

235 plugin: str, 

236 options: Optional[dict] = None, 

237 name: str = "default", 

238 enabled: bool = True, 

239 ) -> None: 

240 """ 

241 A method for register/unregister plugins of the chart. 

242 

243 Args: 

244 plugin: The package name or the url of the plugin. 

245 options: The plugin constructor options. 

246 name: The name of the plugin (default `default`). 

247 enabled: The state of the plugin (default `True`). 

248 """ 

249 

250 self._display( 

251 DisplayTemplate.PLUGIN.format( 

252 chart_id=self._chart_id, 

253 **Plugin(plugin, options, name, enabled).dump(), 

254 ) 

255 ) 

256 

257 def store(self) -> Snapshot: 

258 """ 

259 A method for saving and storing the actual state of the chart. 

260 

261 Returns: 

262 A Snapshot object wich stores the actual state of the chart. 

263 

264 Example: 

265 Save and restore the actual state of the chart: 

266 

267 snapshot = chart.store() 

268 ... 

269 chart.animate(snapshot) 

270 """ 

271 

272 snapshot_id = uuid.uuid4().hex[:7] 

273 self._display( 

274 DisplayTemplate.STORE.format( 

275 chart_id=self._chart_id, **Store(snapshot_id).dump() 

276 ) 

277 ) 

278 return Snapshot(snapshot_id) 

279 

280 def on( # pylint: disable=invalid-name 

281 self, event: str, handler: str 

282 ) -> EventHandler: 

283 """ 

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

285 

286 Args: 

287 event: 

288 The type of the event. 

289 For information on all available events see the 

290 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/modules/events/). 

291 handler: The JavaScript method of the event. 

292 

293 Returns: 

294 The turned on event handler object. 

295 

296 Example: 

297 Turn on an event handler which prints an alert message 

298 when someone clicks on the chart: 

299 

300 handler = chart.on("click", "alert(JSON.stringify(event.data));") 

301 """ # pylint: disable=line-too-long 

302 

303 event_handler = EventHandler(event, handler) 

304 self._display( 

305 DisplayTemplate.SET_EVENT.format( 

306 chart_id=self._chart_id, 

307 **EventOn(event_handler).dump(), 

308 ) 

309 ) 

310 return event_handler 

311 

312 def off(self, event_handler: EventHandler) -> None: 

313 """ 

314 A method for turning off an event handler. 

315 

316 Args: 

317 event_handler: A previously created event handler object. 

318 

319 Example: 

320 Turn off a previously created event handler: 

321 

322 chart.off(handler) 

323 """ 

324 

325 self._display( 

326 DisplayTemplate.CLEAR_EVENT.format( 

327 chart_id=self._chart_id, 

328 **EventOff(event_handler).dump(), 

329 ) 

330 ) 

331 

332 def log(self, chart_property: ChartProperty) -> None: 

333 """ 

334 A method for printing chart properties to the browser console. 

335 

336 Args: 

337 chart_property: 

338 A chart property such as 

339 [CONFIG][ipyvizzu.template.ChartProperty] and 

340 [STYLE][ipyvizzu.template.ChartProperty]. 

341 

342 Example: 

343 Log the actual style of the chart to the browser console: 

344 

345 chart.log(ChartProperty.STYLE) 

346 """ 

347 

348 self._display( 

349 DisplayTemplate.LOG.format( 

350 chart_id=self._chart_id, **Log(chart_property).dump() 

351 ) 

352 ) 

353 

354 def _repr_html_(self) -> str: 

355 assert ( 

356 self._display_target == DisplayTarget.MANUAL 

357 ), "chart._repr_html_() can be used with display=DisplayTarget.MANUAL only" 

358 assert not self._showed, "cannot be used after chart displayed." 

359 self._showed = True 

360 if not self._initialized: 

361 return "" 

362 html_id = uuid.uuid4().hex[:7] 

363 script = ( 

364 self._calls[0] 

365 + "\n" 

366 + "\n".join(self._calls[1:]).replace( 

367 "element", f'document.getElementById("{html_id}")' 

368 ) 

369 ) 

370 return f'<div id="{html_id}"><script>{script}</script></div>' 

371 

372 def show(self) -> None: 

373 """ 

374 A method for displaying the assembled JavaScript code. 

375 

376 Raises: 

377 AssertionError: If [display][ipyvizzu.Chart.__init__] 

378 is not [DisplayTarget.MANUAL][ipyvizzu.template.DisplayTarget]. 

379 AssertionError: If chart already has been displayed. 

380 """ 

381 

382 assert ( 

383 self._display_target == DisplayTarget.MANUAL 

384 ), "chart.show() can be used with display=DisplayTarget.MANUAL only" 

385 assert not self._showed, "cannot be used after chart displayed" 

386 display_javascript( 

387 "\n".join(self._calls), 

388 raw=True, 

389 ) 

390 self._showed = True 

391 

392 def _display(self, javascript: str) -> None: 

393 if not self._initialized: 

394 self.initializing() 

395 if self._display_target != DisplayTarget.MANUAL: 

396 display_javascript( 

397 javascript, 

398 raw=True, 

399 ) 

400 else: 

401 assert not self._showed, "cannot be used after chart displayed" 

402 self._calls.append(javascript)