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
« prev ^ index » next coverage.py v7.3.2, created at 2023-10-12 08:13 +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, Store, EventOn, EventOff, Log
13from ipyvizzu.template import ChartProperty, DisplayTarget, DisplayTemplate
14from ipyvizzu.event import EventHandler
15from ipyvizzu.__version__ import __version__
18class Chart:
19 """A class for representing a wrapper over Vizzu chart."""
21 # pylint: disable=too-many-instance-attributes
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."""
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.
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 """
43 self._chart_id: str = uuid.uuid4().hex[:7]
45 self._vizzu: str = vizzu
46 self._width: str = width
47 self._height: str = height
49 self._display_target: DisplayTarget = DisplayTarget(display)
50 self._calls: List[str] = []
51 self._last_anim: Optional[str] = None
52 self._showed: bool = False
54 self._initialized: bool = False
55 self._analytics: bool = True
56 self._scroll_into_view: bool = False
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)
64 @staticmethod
65 def _register_pre_run_cell() -> None:
66 display_javascript(DisplayTemplate.CLEAR_INHIBITSCROLL, raw=True)
68 @property
69 def analytics(self) -> bool:
70 """
71 A property for enabling/disabling the usage statistics feature.
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.
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.
81 Please note that even when this feature is enabled,
82 publishing anything made with `ipyvizzu` remains GDPR compatible.
84 Returns:
85 The value of the property (default `True`).
86 """
88 return self._analytics
90 @analytics.setter
91 def analytics(self, analytics: Optional[bool]) -> None:
92 self._analytics = bool(analytics)
93 if self._initialized:
94 self._display_analytics()
96 @property
97 def scroll_into_view(self) -> bool:
98 """
99 A property for turning on/off the scroll into view feature.
101 Returns:
102 The value of the property (default `False`).
103 """
105 return self._scroll_into_view
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)
111 @property
112 def control(self) -> AnimationControl:
113 """
114 A property for returning a control object of the last animation.
116 Raises:
117 AssertionError: If called before any animation plays.
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)
125 def initializing(self) -> None:
126 """A method for initializing the chart."""
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()
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))
143 def _display_analytics(self) -> None:
144 self._display(
145 DisplayTemplate.CHANGE_ANALYTICS_TO.format(
146 analytics=str(self._analytics).lower()
147 )
148 )
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 )
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.
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).
176 Raises:
177 ValueError: If `animations` is not set.
179 Example:
180 Reset the chart styles:
182 chart.animate(Style(None))
183 """ # pylint: disable=line-too-long
185 if not animations:
186 raise ValueError("No animation was set.")
188 animation = AnimationMerger.merge_animations(animations)
189 animate = Animate(animation, options)
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 )
202 def feature(self, name: str, enabled: bool) -> None:
203 """
204 A method for turning on/off features of the chart.
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.
213 Example:
214 Turn on `tooltip` of the chart:
216 chart.feature("tooltip", True)
217 """ # pylint: disable=line-too-long
219 self._display(
220 DisplayTemplate.FEATURE.format(
221 chart_id=self._chart_id,
222 **Feature(name, enabled).dump(),
223 )
224 )
226 def store(self) -> Snapshot:
227 """
228 A method for saving and storing the actual state of the chart.
230 Returns:
231 A Snapshot object wich stores the actual state of the chart.
233 Example:
234 Save and restore the actual state of the chart:
236 snapshot = chart.store()
237 ...
238 chart.animate(snapshot)
239 """
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)
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.
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.
262 Returns:
263 The turned on event handler object.
265 Example:
266 Turn on an event handler which prints an alert message
267 when someone clicks on the chart:
269 handler = chart.on("click", "alert(JSON.stringify(event.data));")
270 """ # pylint: disable=line-too-long
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
281 def off(self, event_handler: EventHandler) -> None:
282 """
283 A method for turning off an event handler.
285 Args:
286 event_handler: A previously created event handler object.
288 Example:
289 Turn off a previously created event handler:
291 chart.off(handler)
292 """
294 self._display(
295 DisplayTemplate.CLEAR_EVENT.format(
296 chart_id=self._chart_id,
297 **EventOff(event_handler).dump(),
298 )
299 )
301 def log(self, chart_property: ChartProperty) -> None:
302 """
303 A method for printing chart properties to the browser console.
305 Args:
306 chart_property:
307 A chart property such as
308 [CONFIG][ipyvizzu.template.ChartProperty] and
309 [STYLE][ipyvizzu.template.ChartProperty].
311 Example:
312 Log the actual style of the chart to the browser console:
314 chart.log(ChartProperty.STYLE)
315 """
317 self._display(
318 DisplayTemplate.LOG.format(
319 chart_id=self._chart_id, **Log(chart_property).dump()
320 )
321 )
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>'
341 def show(self) -> None:
342 """
343 A method for displaying the assembled JavaScript code.
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 """
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
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)