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

110 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-12 08:13 +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, Store, EventOn, EventOff, Log 

13from ipyvizzu.template import ChartProperty, DisplayTarget, DisplayTemplate 

14from ipyvizzu.event import EventHandler 

15from ipyvizzu.__version__ import __version__ 

16 

17 

18class Chart: 

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

20 

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

22 

23 VIZZU: str = "https://cdn.jsdelivr.net/npm/vizzu@0.8/dist/vizzu.min.js" 

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

25 

26 def __init__( 

27 self, 

28 vizzu: str = VIZZU, 

29 width: str = "800px", 

30 height: str = "480px", 

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

32 ): 

33 """ 

34 Chart constructor. 

35 

36 Args: 

37 vizzu: The url of Vizzu JavaScript package. 

38 width: The width of the chart. 

39 height: The height of the chart. 

40 display: The display behaviour of the chart. 

41 """ 

42 

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

44 

45 self._vizzu: str = vizzu 

46 self._width: str = width 

47 self._height: str = height 

48 

49 self._display_target: DisplayTarget = DisplayTarget(display) 

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

51 self._last_anim: Optional[str] = None 

52 self._showed: bool = False 

53 

54 self._initialized: bool = False 

55 self._analytics: bool = True 

56 self._scroll_into_view: bool = False 

57 

58 @staticmethod 

59 def _register_events() -> None: 

60 ipy = get_ipython() 

61 if ipy is not None: 

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

63 

64 @staticmethod 

65 def _register_pre_run_cell() -> None: 

66 display_javascript(DisplayTemplate.CLEAR_INHIBITSCROLL, raw=True) 

67 

68 @property 

69 def analytics(self) -> bool: 

70 """ 

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

72 

73 The usage statistics feature allows aggregate usage data collection 

74 using Plausible's algorithm. 

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

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

77 

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

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

80 

81 Please note that even when this feature is enabled, 

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

83 

84 Returns: 

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

86 """ 

87 

88 return self._analytics 

89 

90 @analytics.setter 

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

92 self._analytics = bool(analytics) 

93 if self._initialized: 

94 self._display_analytics() 

95 

96 @property 

97 def scroll_into_view(self) -> bool: 

98 """ 

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

100 

101 Returns: 

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

103 """ 

104 

105 return self._scroll_into_view 

106 

107 @scroll_into_view.setter 

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

109 self._scroll_into_view = bool(scroll_into_view) 

110 

111 @property 

112 def control(self) -> AnimationControl: 

113 """ 

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

115 

116 Raises: 

117 AssertionError: If called before any animation plays. 

118 

119 Returns: 

120 The control object of the last animation. 

121 """ 

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

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

124 

125 def initializing(self) -> None: 

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

127 

128 if not self._initialized: 

129 self._initialized = True 

130 self._display_ipyvizzujs() 

131 self._display_analytics() 

132 if self._display_target != DisplayTarget.MANUAL: 

133 Chart._register_events() 

134 self._display_chart() 

135 

136 def _display_ipyvizzujs(self) -> None: 

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

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

139 "'__version__'", f"'{__version__}'" 

140 ) 

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

142 

143 def _display_analytics(self) -> None: 

144 self._display( 

145 DisplayTemplate.CHANGE_ANALYTICS_TO.format( 

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

147 ) 

148 ) 

149 

150 def _display_chart(self) -> None: 

151 self._display( 

152 DisplayTemplate.INIT.format( 

153 chart_id=self._chart_id, 

154 vizzu=self._vizzu, 

155 div_width=self._width, 

156 div_height=self._height, 

157 ) 

158 ) 

159 

160 def animate( 

161 self, 

162 *animations: AbstractAnimation, 

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

164 ) -> None: 

165 """ 

166 A method for changing the state of the chart. 

167 

168 Args: 

169 *animations: 

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

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

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

173 For information on all available animation options see the 

174 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/interfaces/Anim.Options/#properties). 

175 

176 Raises: 

177 ValueError: If `animations` is not set. 

178 

179 Example: 

180 Reset the chart styles: 

181 

182 chart.animate(Style(None)) 

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

184 

185 if not animations: 

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

187 

188 animation = AnimationMerger.merge_animations(animations) 

189 animate = Animate(animation, options) 

190 

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

192 self._display( 

193 DisplayTemplate.ANIMATE.format( 

194 display_target=self._display_target.value, 

195 chart_id=self._chart_id, 

196 anim_id=self._last_anim, 

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

198 **animate.dump(), 

199 ) 

200 ) 

201 

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

203 """ 

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

205 

206 Args: 

207 name: 

208 The name of the chart feature. 

209 For information on all available features see the 

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

211 enabled: The new state of the chart feature. 

212 

213 Example: 

214 Turn on `tooltip` of the chart: 

215 

216 chart.feature("tooltip", True) 

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

218 

219 self._display( 

220 DisplayTemplate.FEATURE.format( 

221 chart_id=self._chart_id, 

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

223 ) 

224 ) 

225 

226 def store(self) -> Snapshot: 

227 """ 

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

229 

230 Returns: 

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

232 

233 Example: 

234 Save and restore the actual state of the chart: 

235 

236 snapshot = chart.store() 

237 ... 

238 chart.animate(snapshot) 

239 """ 

240 

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

242 self._display( 

243 DisplayTemplate.STORE.format( 

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

245 ) 

246 ) 

247 return Snapshot(snapshot_id) 

248 

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

250 self, event: str, handler: str 

251 ) -> EventHandler: 

252 """ 

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

254 

255 Args: 

256 event: 

257 The type of the event. 

258 For information on all available events see the 

259 [Vizzu Code reference](https://lib.vizzuhq.com/latest/reference/modules/Event/#type). 

260 handler: The JavaScript method of the event. 

261 

262 Returns: 

263 The turned on event handler object. 

264 

265 Example: 

266 Turn on an event handler which prints an alert message 

267 when someone clicks on the chart: 

268 

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

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

271 

272 event_handler = EventHandler(event, handler) 

273 self._display( 

274 DisplayTemplate.SET_EVENT.format( 

275 chart_id=self._chart_id, 

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

277 ) 

278 ) 

279 return event_handler 

280 

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

282 """ 

283 A method for turning off an event handler. 

284 

285 Args: 

286 event_handler: A previously created event handler object. 

287 

288 Example: 

289 Turn off a previously created event handler: 

290 

291 chart.off(handler) 

292 """ 

293 

294 self._display( 

295 DisplayTemplate.CLEAR_EVENT.format( 

296 chart_id=self._chart_id, 

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

298 ) 

299 ) 

300 

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

302 """ 

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

304 

305 Args: 

306 chart_property: 

307 A chart property such as 

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

309 [STYLE][ipyvizzu.template.ChartProperty]. 

310 

311 Example: 

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

313 

314 chart.log(ChartProperty.STYLE) 

315 """ 

316 

317 self._display( 

318 DisplayTemplate.LOG.format( 

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

320 ) 

321 ) 

322 

323 def _repr_html_(self) -> str: 

324 assert ( 

325 self._display_target == DisplayTarget.MANUAL 

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

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

328 self._showed = True 

329 if not self._initialized: 

330 return "" 

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

332 script = ( 

333 self._calls[0] 

334 + "\n" 

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

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

337 ) 

338 ) 

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

340 

341 def show(self) -> None: 

342 """ 

343 A method for displaying the assembled JavaScript code. 

344 

345 Raises: 

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

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

348 AssertionError: If chart already has been displayed. 

349 """ 

350 

351 assert ( 

352 self._display_target == DisplayTarget.MANUAL 

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

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

355 display_javascript( 

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

357 raw=True, 

358 ) 

359 self._showed = True 

360 

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

362 if not self._initialized: 

363 self.initializing() 

364 if self._display_target != DisplayTarget.MANUAL: 

365 display_javascript( 

366 javascript, 

367 raw=True, 

368 ) 

369 else: 

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

371 self._calls.append(javascript)