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
« prev ^ index » next coverage.py v7.4.3, created at 2024-02-26 10:12 +0000
1"""A module for working with Vizzu charts."""
3import pkgutil
4import uuid
5from typing import List, Optional, Union
7from IPython.display import display_javascript # type: ignore
8from IPython import get_ipython # type: ignore
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__
23class Chart:
24 """A class for representing a wrapper over Vizzu chart."""
26 # pylint: disable=too-many-instance-attributes
28 VIZZU: str = VIZZU_URL
29 """A variable for storing the default url of the `vizzu` package."""
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.
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 """
48 self._chart_id: str = uuid.uuid4().hex[:7]
50 self._vizzu: str = vizzu
51 self._width: str = width
52 self._height: str = height
54 self._display_target: DisplayTarget = DisplayTarget(display)
55 self._calls: List[str] = []
56 self._last_anim: Optional[str] = None
57 self._showed: bool = False
59 self._initialized: bool = False
60 self._analytics: bool = True
61 self._scroll_into_view: bool = False
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)
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)
75 @property
76 def analytics(self) -> bool:
77 """
78 A property for enabling/disabling the usage statistics feature.
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.
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.
88 Please note that even when this feature is enabled,
89 publishing anything made with `ipyvizzu` remains GDPR compatible.
91 Returns:
92 The value of the property (default `True`).
93 """
95 return self._analytics
97 @analytics.setter
98 def analytics(self, analytics: Optional[bool]) -> None:
99 self._analytics = bool(analytics)
100 if self._initialized:
101 self._display_analytics()
103 @property
104 def scroll_into_view(self) -> bool:
105 """
106 A property for turning on/off the scroll into view feature.
108 Returns:
109 The value of the property (default `False`).
110 """
112 return self._scroll_into_view
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)
118 @property
119 def control(self) -> AnimationControl:
120 """
121 A property for returning a control object of the last animation.
123 Raises:
124 AssertionError: If called before any animation plays.
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)
132 def initializing(self) -> None:
133 """A method for initializing the chart."""
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()
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))
150 def _display_analytics(self) -> None:
151 self._display(
152 DisplayTemplate.CHANGE_ANALYTICS_TO.format(
153 analytics=str(self._analytics).lower()
154 )
155 )
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 )
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.
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/).
183 Raises:
184 ValueError: If `animations` is not set.
186 Example:
187 Reset the chart styles:
189 chart.animate(Style(None))
190 """ # pylint: disable=line-too-long
192 if not animations:
193 raise ValueError("No animation was set.")
195 animation = AnimationMerger.merge_animations(animations)
196 animate = Animate(animation, options)
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 )
209 def feature(self, name: str, enabled: bool) -> None:
210 """
211 A method for turning on/off features of the chart.
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.
220 Example:
221 Turn on `tooltip` of the chart:
223 chart.feature("tooltip", True)
224 """ # pylint: disable=line-too-long
226 self._display(
227 DisplayTemplate.FEATURE.format(
228 chart_id=self._chart_id,
229 **Feature(name, enabled).dump(),
230 )
231 )
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.
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 """
250 self._display(
251 DisplayTemplate.PLUGIN.format(
252 chart_id=self._chart_id,
253 **Plugin(plugin, options, name, enabled).dump(),
254 )
255 )
257 def store(self) -> Snapshot:
258 """
259 A method for saving and storing the actual state of the chart.
261 Returns:
262 A Snapshot object wich stores the actual state of the chart.
264 Example:
265 Save and restore the actual state of the chart:
267 snapshot = chart.store()
268 ...
269 chart.animate(snapshot)
270 """
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)
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.
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.
293 Returns:
294 The turned on event handler object.
296 Example:
297 Turn on an event handler which prints an alert message
298 when someone clicks on the chart:
300 handler = chart.on("click", "alert(JSON.stringify(event.data));")
301 """ # pylint: disable=line-too-long
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
312 def off(self, event_handler: EventHandler) -> None:
313 """
314 A method for turning off an event handler.
316 Args:
317 event_handler: A previously created event handler object.
319 Example:
320 Turn off a previously created event handler:
322 chart.off(handler)
323 """
325 self._display(
326 DisplayTemplate.CLEAR_EVENT.format(
327 chart_id=self._chart_id,
328 **EventOff(event_handler).dump(),
329 )
330 )
332 def log(self, chart_property: ChartProperty) -> None:
333 """
334 A method for printing chart properties to the browser console.
336 Args:
337 chart_property:
338 A chart property such as
339 [CONFIG][ipyvizzu.template.ChartProperty] and
340 [STYLE][ipyvizzu.template.ChartProperty].
342 Example:
343 Log the actual style of the chart to the browser console:
345 chart.log(ChartProperty.STYLE)
346 """
348 self._display(
349 DisplayTemplate.LOG.format(
350 chart_id=self._chart_id, **Log(chart_property).dump()
351 )
352 )
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>'
372 def show(self) -> None:
373 """
374 A method for displaying the assembled JavaScript code.
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 """
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
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)