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

82 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-25 15:04 +0100

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

2 

3import pkgutil 

4import uuid 

5from typing import List, Optional, Union, Tuple 

6 

7from IPython.display import display_javascript # type: ignore 

8from IPython import get_ipython # type: ignore 

9 

10from ipyvizzu.animation import Animation, Snapshot, AnimationMerger 

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

12from ipyvizzu.template import ChartProperty, DisplayTarget, DisplayTemplate 

13from ipyvizzu.event import EventHandler 

14 

15 

16class Chart: 

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

18 

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

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

21 

22 def __init__( 

23 self, 

24 vizzu: Optional[str] = VIZZU, 

25 width: Optional[str] = "800px", 

26 height: Optional[str] = "480px", 

27 display: Optional[Union[DisplayTarget, str]] = DisplayTarget.ACTUAL, 

28 ): 

29 """ 

30 Chart constructor. 

31 

32 Args: 

33 vizzu: The url of Vizzu JavaScript package. 

34 width: The width of the chart. 

35 height: The height of the chart. 

36 display: The display behaviour of the chart. 

37 """ 

38 

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

40 

41 self._display_target = DisplayTarget(display) 

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

43 self._showed = False 

44 

45 self._scroll_into_view = False 

46 

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

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

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

50 

51 self._display( 

52 DisplayTemplate.INIT.format( 

53 chart_id=self._chart_id, 

54 vizzu=vizzu, 

55 div_width=width, 

56 div_height=height, 

57 ) 

58 ) 

59 

60 if self._display_target != DisplayTarget.MANUAL: 

61 self._register_events() 

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() -> None: 

71 display_javascript(DisplayTemplate.CLEAR_INHIBITSCROLL, raw=True) 

72 

73 @property 

74 def scroll_into_view(self) -> bool: 

75 """ 

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

77 

78 Returns: 

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

80 """ 

81 

82 return self._scroll_into_view 

83 

84 @scroll_into_view.setter 

85 def scroll_into_view(self, scroll_into_view: Optional[bool]): 

86 self._scroll_into_view = bool(scroll_into_view) 

87 

88 def animate( 

89 self, *animations: Animation, **options: Optional[Union[str, int, float, dict]] 

90 ) -> None: 

91 """ 

92 A method for changing the state of the chart. 

93 

94 Args: 

95 *animations: 

96 List of Animation objects such as [Data][ipyvizzu.animation.Data], 

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

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

99 For information on all available animation options see the 

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

101 

102 Raises: 

103 ValueError: If `animations` is not set. 

104 

105 Example: 

106 Reset the chart styles: 

107 

108 chart.animate(Style(None)) 

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

110 

111 if not animations: 

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

113 

114 animation = self._merge_animations(animations) 

115 animate = Animate(animation, options) 

116 

117 self._display( 

118 DisplayTemplate.ANIMATE.format( 

119 display_target=self._display_target.value, 

120 chart_id=self._chart_id, 

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

122 **animate.dump(), 

123 ) 

124 ) 

125 

126 @staticmethod 

127 def _merge_animations( 

128 animations: Tuple[Animation, ...], 

129 ) -> Union[Animation, AnimationMerger]: 

130 if len(animations) == 1: 

131 return animations[0] 

132 

133 merger = AnimationMerger() 

134 for animation in animations: 

135 merger.merge(animation) 

136 

137 return merger 

138 

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

140 """ 

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

142 

143 Args: 

144 name: 

145 The name of the chart feature. 

146 For information on all available features see the 

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

148 enabled: The new state of the chart feature. 

149 

150 Example: 

151 Turn on `tooltip` of the chart: 

152 

153 chart.feature("tooltip", True) 

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

155 

156 self._display( 

157 DisplayTemplate.FEATURE.format( 

158 chart_id=self._chart_id, 

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

160 ) 

161 ) 

162 

163 def store(self) -> Snapshot: 

164 """ 

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

166 

167 Returns: 

168 A snapshot animation object wich stores the actual state of the chart. 

169 

170 Example: 

171 Save and restore the actual state of the chart: 

172 

173 snapshot = chart.store() 

174 ... 

175 chart.animate(snapshot) 

176 """ 

177 

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

179 self._display( 

180 DisplayTemplate.STORE.format( 

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

182 ) 

183 ) 

184 return Snapshot(snapshot_id) 

185 

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

187 self, event: str, handler: str 

188 ) -> EventHandler: 

189 """ 

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

191 

192 Args: 

193 event: 

194 The type of the event. 

195 For information on all available events see the 

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

197 handler: The JavaScript method of the event. 

198 

199 Returns: 

200 The turned on event handler object. 

201 

202 Example: 

203 Turn on an event handler which prints an alert message 

204 when someone clicks on the chart: 

205 

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

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

208 

209 event_handler = EventHandler(event, handler) 

210 self._display( 

211 DisplayTemplate.SET_EVENT.format( 

212 chart_id=self._chart_id, 

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

214 ) 

215 ) 

216 return event_handler 

217 

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

219 """ 

220 A method for turning off an event handler. 

221 

222 Args: 

223 event_handler: A previously created event handler object. 

224 

225 Example: 

226 Turn off a previously created event handler: 

227 

228 chart.off(handler) 

229 """ 

230 

231 self._display( 

232 DisplayTemplate.CLEAR_EVENT.format( 

233 chart_id=self._chart_id, 

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

235 ) 

236 ) 

237 

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

239 """ 

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

241 

242 Args: 

243 chart_property: 

244 A chart property such as 

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

246 [STYLE][ipyvizzu.template.ChartProperty]. 

247 

248 Example: 

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

250 

251 chart.log(ChartProperty.STYLE) 

252 """ 

253 

254 self._display( 

255 DisplayTemplate.LOG.format( 

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

257 ) 

258 ) 

259 

260 def _repr_html_(self) -> str: 

261 assert ( 

262 self._display_target == DisplayTarget.MANUAL 

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

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

265 self._showed = True 

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

267 script = ( 

268 self._calls[0] 

269 + "\n" 

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

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

272 ) 

273 ) 

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

275 

276 def show(self) -> None: 

277 """ 

278 A method for displaying the assembled JavaScript code. 

279 

280 Raises: 

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

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

283 AssertionError: If chart already has been displayed. 

284 """ 

285 

286 assert ( 

287 self._display_target == DisplayTarget.MANUAL 

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

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

290 display_javascript( 

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

292 raw=True, 

293 ) 

294 self._showed = True 

295 

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

297 if self._display_target != DisplayTarget.MANUAL: 

298 display_javascript( 

299 javascript, 

300 raw=True, 

301 ) 

302 else: 

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

304 self._calls.append(javascript)