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

93 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-10 09:30 +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 

15 

16 

17class Chart: 

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

19 

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

21 

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

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

24 

25 def __init__( 

26 self, 

27 vizzu: str = VIZZU, 

28 width: str = "800px", 

29 height: str = "480px", 

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

31 ): 

32 """ 

33 Chart constructor. 

34 

35 Args: 

36 vizzu: The url of Vizzu JavaScript package. 

37 width: The width of the chart. 

38 height: The height of the chart. 

39 display: The display behaviour of the chart. 

40 """ 

41 

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

43 

44 self._vizzu: str = vizzu 

45 self._width: str = width 

46 self._height: str = height 

47 

48 self._display_target: DisplayTarget = DisplayTarget(display) 

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

50 self._last_anim: Optional[str] = None 

51 self._showed: bool = False 

52 

53 self._initialized: bool = False 

54 self._scroll_into_view: bool = False 

55 

56 @staticmethod 

57 def _register_events() -> None: 

58 ipy = get_ipython() 

59 if ipy is not None: 

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

61 

62 @staticmethod 

63 def _register_pre_run_cell() -> None: 

64 display_javascript(DisplayTemplate.CLEAR_INHIBITSCROLL, raw=True) 

65 

66 @property 

67 def scroll_into_view(self) -> bool: 

68 """ 

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

70 

71 Returns: 

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

73 """ 

74 

75 return self._scroll_into_view 

76 

77 @scroll_into_view.setter 

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

79 self._scroll_into_view = bool(scroll_into_view) 

80 

81 @property 

82 def control(self) -> AnimationControl: 

83 """ 

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

85 

86 Raises: 

87 AssertionError: If called before any animation plays. 

88 

89 Returns: 

90 The control object of the last animation. 

91 """ 

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

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

94 

95 def initializing(self) -> None: 

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

97 

98 if not self._initialized: 

99 self._initialized = True 

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

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

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

103 

104 if self._display_target != DisplayTarget.MANUAL: 

105 Chart._register_events() 

106 

107 self._display( 

108 DisplayTemplate.INIT.format( 

109 chart_id=self._chart_id, 

110 vizzu=self._vizzu, 

111 div_width=self._width, 

112 div_height=self._height, 

113 ) 

114 ) 

115 

116 def animate( 

117 self, 

118 *animations: AbstractAnimation, 

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

120 ) -> None: 

121 """ 

122 A method for changing the state of the chart. 

123 

124 Args: 

125 *animations: 

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

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

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

129 For information on all available animation options see the 

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

131 

132 Raises: 

133 ValueError: If `animations` is not set. 

134 

135 Example: 

136 Reset the chart styles: 

137 

138 chart.animate(Style(None)) 

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

140 

141 if not animations: 

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

143 

144 animation = AnimationMerger.merge_animations(animations) 

145 animate = Animate(animation, options) 

146 

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

148 self._display( 

149 DisplayTemplate.ANIMATE.format( 

150 display_target=self._display_target.value, 

151 chart_id=self._chart_id, 

152 anim_id=self._last_anim, 

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

154 **animate.dump(), 

155 ) 

156 ) 

157 

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

159 """ 

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

161 

162 Args: 

163 name: 

164 The name of the chart feature. 

165 For information on all available features see the 

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

167 enabled: The new state of the chart feature. 

168 

169 Example: 

170 Turn on `tooltip` of the chart: 

171 

172 chart.feature("tooltip", True) 

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

174 

175 self._display( 

176 DisplayTemplate.FEATURE.format( 

177 chart_id=self._chart_id, 

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

179 ) 

180 ) 

181 

182 def store(self) -> Snapshot: 

183 """ 

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

185 

186 Returns: 

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

188 

189 Example: 

190 Save and restore the actual state of the chart: 

191 

192 snapshot = chart.store() 

193 ... 

194 chart.animate(snapshot) 

195 """ 

196 

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

198 self._display( 

199 DisplayTemplate.STORE.format( 

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

201 ) 

202 ) 

203 return Snapshot(snapshot_id) 

204 

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

206 self, event: str, handler: str 

207 ) -> EventHandler: 

208 """ 

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

210 

211 Args: 

212 event: 

213 The type of the event. 

214 For information on all available events see the 

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

216 handler: The JavaScript method of the event. 

217 

218 Returns: 

219 The turned on event handler object. 

220 

221 Example: 

222 Turn on an event handler which prints an alert message 

223 when someone clicks on the chart: 

224 

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

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

227 

228 event_handler = EventHandler(event, handler) 

229 self._display( 

230 DisplayTemplate.SET_EVENT.format( 

231 chart_id=self._chart_id, 

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

233 ) 

234 ) 

235 return event_handler 

236 

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

238 """ 

239 A method for turning off an event handler. 

240 

241 Args: 

242 event_handler: A previously created event handler object. 

243 

244 Example: 

245 Turn off a previously created event handler: 

246 

247 chart.off(handler) 

248 """ 

249 

250 self._display( 

251 DisplayTemplate.CLEAR_EVENT.format( 

252 chart_id=self._chart_id, 

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

254 ) 

255 ) 

256 

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

258 """ 

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

260 

261 Args: 

262 chart_property: 

263 A chart property such as 

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

265 [STYLE][ipyvizzu.template.ChartProperty]. 

266 

267 Example: 

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

269 

270 chart.log(ChartProperty.STYLE) 

271 """ 

272 

273 self._display( 

274 DisplayTemplate.LOG.format( 

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

276 ) 

277 ) 

278 

279 def _repr_html_(self) -> str: 

280 assert ( 

281 self._display_target == DisplayTarget.MANUAL 

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

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

284 self._showed = True 

285 if not self._initialized: 

286 return "" 

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

288 script = ( 

289 self._calls[0] 

290 + "\n" 

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

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

293 ) 

294 ) 

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

296 

297 def show(self) -> None: 

298 """ 

299 A method for displaying the assembled JavaScript code. 

300 

301 Raises: 

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

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

304 AssertionError: If chart already has been displayed. 

305 """ 

306 

307 assert ( 

308 self._display_target == DisplayTarget.MANUAL 

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

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

311 display_javascript( 

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

313 raw=True, 

314 ) 

315 self._showed = True 

316 

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

318 if not self._initialized: 

319 self.initializing() 

320 if self._display_target != DisplayTarget.MANUAL: 

321 display_javascript( 

322 javascript, 

323 raw=True, 

324 ) 

325 else: 

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

327 self._calls.append(javascript)