flet-charts 0.2.0.dev35__py3-none-any.whl → 0.70.0.dev6551__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of flet-charts might be problematic. Click here for more details.

Files changed (46) hide show
  1. flet_charts/__init__.py +59 -17
  2. flet_charts/bar_chart.py +87 -30
  3. flet_charts/bar_chart_group.py +1 -2
  4. flet_charts/bar_chart_rod.py +36 -5
  5. flet_charts/candlestick_chart.py +269 -0
  6. flet_charts/candlestick_chart_spot.py +98 -0
  7. flet_charts/chart_axis.py +29 -9
  8. flet_charts/line_chart.py +76 -14
  9. flet_charts/line_chart_data.py +30 -5
  10. flet_charts/line_chart_data_point.py +33 -4
  11. flet_charts/matplotlib_backends/backend_flet_agg.py +16 -0
  12. flet_charts/matplotlib_chart.py +396 -36
  13. flet_charts/matplotlib_chart_with_toolbar.py +125 -0
  14. flet_charts/pie_chart.py +3 -6
  15. flet_charts/pie_chart_section.py +25 -18
  16. flet_charts/plotly_chart.py +17 -6
  17. flet_charts/radar_chart.py +214 -0
  18. flet_charts/radar_data_set.py +66 -0
  19. flet_charts/scatter_chart.py +75 -29
  20. flet_charts/scatter_chart_spot.py +44 -6
  21. flet_charts/types.py +159 -16
  22. flet_charts-0.70.0.dev6551.dist-info/METADATA +67 -0
  23. flet_charts-0.70.0.dev6551.dist-info/RECORD +47 -0
  24. flutter/flet_charts/CHANGELOG.md +1 -1
  25. flutter/flet_charts/LICENSE +1 -1
  26. flutter/flet_charts/analysis_options.yaml +1 -1
  27. flutter/flet_charts/lib/src/bar_chart.dart +2 -0
  28. flutter/flet_charts/lib/src/candlestick_chart.dart +129 -0
  29. flutter/flet_charts/lib/src/extension.dart +6 -0
  30. flutter/flet_charts/lib/src/radar_chart.dart +104 -0
  31. flutter/flet_charts/lib/src/scatter_chart.dart +22 -21
  32. flutter/flet_charts/lib/src/utils/bar_chart.dart +137 -73
  33. flutter/flet_charts/lib/src/utils/candlestick_chart.dart +118 -0
  34. flutter/flet_charts/lib/src/utils/charts.dart +12 -0
  35. flutter/flet_charts/lib/src/utils/line_chart.dart +15 -3
  36. flutter/flet_charts/lib/src/utils/pie_chart.dart +1 -0
  37. flutter/flet_charts/lib/src/utils/radar_chart.dart +90 -0
  38. flutter/flet_charts/lib/src/utils/scatter_chart.dart +22 -21
  39. flutter/flet_charts/pubspec.lock +85 -71
  40. flutter/flet_charts/pubspec.yaml +10 -9
  41. flet_charts-0.2.0.dev35.dist-info/METADATA +0 -69
  42. flet_charts-0.2.0.dev35.dist-info/RECORD +0 -38
  43. flutter/flet_charts/README.md +0 -3
  44. {flet_charts-0.2.0.dev35.dist-info → flet_charts-0.70.0.dev6551.dist-info}/WHEEL +0 -0
  45. {flet_charts-0.2.0.dev35.dist-info → flet_charts-0.70.0.dev6551.dist-info}/licenses/LICENSE +0 -0
  46. {flet_charts-0.2.0.dev35.dist-info → flet_charts-0.70.0.dev6551.dist-info}/top_level.txt +0 -0
@@ -1,27 +1,78 @@
1
- import io
2
- import re
3
- import xml.etree.ElementTree as ET
4
- from dataclasses import field
1
+ import asyncio
2
+ import logging
3
+ from dataclasses import dataclass, field
4
+ from io import BytesIO
5
+ from typing import Any, Optional
5
6
 
6
7
  import flet as ft
8
+ import flet.canvas as fc
9
+
10
+ _MATPLOTLIB_IMPORT_ERROR: Optional[ImportError] = None
7
11
 
8
12
  try:
9
- from matplotlib.figure import Figure
10
- except ImportError as e:
11
- raise Exception(
12
- 'Install "matplotlib" Python package to use MatplotlibChart control.'
13
- ) from e
13
+ import matplotlib # type: ignore[import]
14
+ from matplotlib.figure import Figure # type: ignore[import]
15
+ except ImportError as e: # pragma: no cover - depends on optional dependency
16
+ matplotlib = None # type: ignore[assignment]
17
+ Figure = Any # type: ignore[assignment]
18
+ _MATPLOTLIB_IMPORT_ERROR = e
19
+ else:
20
+ matplotlib.use("module://flet_charts.matplotlib_backends.backend_flet_agg")
21
+
22
+ __all__ = [
23
+ "MatplotlibChart",
24
+ "MatplotlibChartMessageEvent",
25
+ "MatplotlibChartToolbarButtonsUpdateEvent",
26
+ ]
27
+
28
+ logger = logging.getLogger("flet-charts.matplotlib")
29
+
30
+ figure_cursors = {
31
+ "default": None,
32
+ "pointer": ft.MouseCursor.CLICK,
33
+ "crosshair": ft.MouseCursor.PRECISE,
34
+ "move": ft.MouseCursor.MOVE,
35
+ "wait": ft.MouseCursor.WAIT,
36
+ "ew-resize": ft.MouseCursor.RESIZE_LEFT_RIGHT,
37
+ "ns-resize": ft.MouseCursor.RESIZE_UP_DOWN,
38
+ }
39
+
40
+
41
+ def _require_matplotlib() -> None:
42
+ if matplotlib is None:
43
+ raise ModuleNotFoundError(
44
+ 'Install "matplotlib" Python package to use MatplotlibChart control.'
45
+ ) from _MATPLOTLIB_IMPORT_ERROR
46
+
47
+
48
+ @dataclass
49
+ class MatplotlibChartMessageEvent(ft.Event["MatplotlibChart"]):
50
+ message: str
51
+ """
52
+ Message text.
53
+ """
54
+
14
55
 
15
- __all__ = ["MatplotlibChart"]
56
+ @dataclass
57
+ class MatplotlibChartToolbarButtonsUpdateEvent(ft.Event["MatplotlibChart"]):
58
+ back_enabled: bool
59
+ """
60
+ Whether Back button is enabled or not.
61
+ """
62
+ forward_enabled: bool
63
+ """
64
+ Whether Forward button is enabled or not.
65
+ """
16
66
 
17
67
 
18
- @ft.control(kw_only=True)
19
- class MatplotlibChart(ft.Container):
68
+ @ft.control(kw_only=True, isolated=True)
69
+ class MatplotlibChart(ft.GestureDetector):
20
70
  """
21
71
  Displays a [Matplotlib](https://matplotlib.org/) chart.
22
72
 
23
73
  Warning:
24
- This control requires the [`matplotlib`](https://matplotlib.org/) Python package to be installed.
74
+ This control requires the [`matplotlib`](https://matplotlib.org/)
75
+ Python package to be installed.
25
76
 
26
77
  See this [installation guide](index.md#installation) for more information.
27
78
  """
@@ -32,33 +83,342 @@ class MatplotlibChart(ft.Container):
32
83
  [`matplotlib.figure.Figure`](https://matplotlib.org/stable/api/_as_gen/matplotlib.figure.Figure.html#matplotlib.figure.Figure).
33
84
  """
34
85
 
35
- original_size: bool = False
86
+ on_message: Optional[ft.EventHandler[MatplotlibChartMessageEvent]] = None
36
87
  """
37
- Whether to display chart in original size.
38
-
39
- Set to `False` to display a chart that fits configured bounds.
88
+ The event is triggered on figure message update.
40
89
  """
41
90
 
42
- transparent: bool = False
91
+ on_toolbar_buttons_update: Optional[
92
+ ft.EventHandler[MatplotlibChartToolbarButtonsUpdateEvent]
93
+ ] = None
43
94
  """
44
- Whether to remove the background from the chart.
95
+ Triggers when toolbar buttons status is updated.
45
96
  """
46
97
 
47
98
  def init(self):
48
- self.alignment = ft.Alignment.CENTER
49
- self.__img = ft.Image(fit=ft.BoxFit.FILL)
50
- self.content = self.__img
51
-
52
- def before_update(self):
53
- super().before_update()
54
- if self.figure is not None:
55
- s = io.StringIO()
56
- self.figure.savefig(s, format="svg", transparent=self.transparent)
57
- svg = s.getvalue()
58
-
59
- if not self.original_size:
60
- root = ET.fromstring(svg)
61
- w = float(re.findall(r"\d+", root.attrib["width"])[0])
62
- h = float(re.findall(r"\d+", root.attrib["height"])[0])
63
- self.__img.aspect_ratio = w / h
64
- self.__img.src = svg
99
+ _require_matplotlib()
100
+ super().init()
101
+
102
+ def build(self):
103
+ self.mouse_cursor = ft.MouseCursor.WAIT
104
+ self.__started = False
105
+ self.__dpr = self.page.media.device_pixel_ratio
106
+ logger.debug(f"DPR: {self.__dpr}")
107
+ self.__image_mode = "full"
108
+
109
+ self.canvas = fc.Canvas(
110
+ # resize_interval=10,
111
+ on_resize=self._on_canvas_resize,
112
+ expand=True,
113
+ )
114
+ self.keyboard_listener = ft.KeyboardListener(
115
+ self.canvas,
116
+ autofocus=True,
117
+ on_key_down=self._on_key_down,
118
+ on_key_up=self._on_key_up,
119
+ )
120
+ self.content = self.keyboard_listener
121
+ self.on_enter = self._on_enter
122
+ self.on_hover = self._on_hover
123
+ self.on_exit = self._on_exit
124
+ self.on_pan_start = self._pan_start
125
+ self.on_pan_update = self._pan_update
126
+ self.on_pan_end = self._pan_end
127
+ self.on_right_pan_start = self._right_pan_start
128
+ self.on_right_pan_update = self._right_pan_update
129
+ self.on_right_pan_end = self._right_pan_end
130
+ self.img_count = 1
131
+ self._receive_queue = asyncio.Queue()
132
+ self._main_loop = asyncio.get_event_loop()
133
+ self._width = 0
134
+ self._height = 0
135
+ self._waiting = False
136
+
137
+ def _on_key_down(self, e):
138
+ logger.debug(f"ON KEY DOWN: {e}")
139
+
140
+ def _on_key_up(self, e):
141
+ logger.debug(f"ON KEY UP: {e}")
142
+
143
+ def _on_enter(self, e: ft.HoverEvent):
144
+ logger.debug(f"_on_enter: {e.local_position.x}, {e.local_position.y}")
145
+ self.send_message(
146
+ {
147
+ "type": "figure_enter",
148
+ "x": e.local_position.x * self.__dpr,
149
+ "y": e.local_position.y * self.__dpr,
150
+ "button": 0,
151
+ "buttons": 0,
152
+ "modifiers": [],
153
+ }
154
+ )
155
+
156
+ def _on_hover(self, e: ft.HoverEvent):
157
+ logger.debug(f"_on_hover: {e.local_position.x}, {e.local_position.y}")
158
+ self.send_message(
159
+ {
160
+ "type": "motion_notify",
161
+ "x": e.local_position.x * self.__dpr,
162
+ "y": e.local_position.y * self.__dpr,
163
+ "button": 0,
164
+ "buttons": 0,
165
+ "modifiers": [],
166
+ }
167
+ )
168
+
169
+ def _on_exit(self, e: ft.HoverEvent):
170
+ logger.debug(f"_on_exit: {e.local_position.x}, {e.local_position.y}")
171
+ self.send_message(
172
+ {
173
+ "type": "figure_leave",
174
+ "x": e.local_position.x * self.__dpr,
175
+ "y": e.local_position.y * self.__dpr,
176
+ "button": 0,
177
+ "buttons": 0,
178
+ "modifiers": [],
179
+ }
180
+ )
181
+
182
+ def _pan_start(self, e: ft.DragStartEvent):
183
+ logger.debug(f"_pan_start: {e.local_position.x}, {e.local_position.y}")
184
+ asyncio.create_task(self.keyboard_listener.focus())
185
+ self.send_message(
186
+ {
187
+ "type": "button_press",
188
+ "x": e.local_position.x * self.__dpr,
189
+ "y": e.local_position.y * self.__dpr,
190
+ "button": 0,
191
+ "buttons": 1,
192
+ "modifiers": [],
193
+ }
194
+ )
195
+
196
+ def _pan_update(self, e: ft.DragUpdateEvent):
197
+ logger.debug(f"_pan_update: {e.local_position.x}, {e.local_position.y}")
198
+ self.send_message(
199
+ {
200
+ "type": "motion_notify",
201
+ "x": e.local_position.x * self.__dpr,
202
+ "y": e.local_position.y * self.__dpr,
203
+ "button": 0,
204
+ "buttons": 1,
205
+ "modifiers": [],
206
+ }
207
+ )
208
+
209
+ def _pan_end(self, e: ft.DragEndEvent):
210
+ logger.debug(f"_pan_end: {e.local_position.x}, {e.local_position.y}")
211
+ self.send_message(
212
+ {
213
+ "type": "button_release",
214
+ "x": e.local_position.x * self.__dpr,
215
+ "y": e.local_position.y * self.__dpr,
216
+ "button": 0,
217
+ "buttons": 0,
218
+ "modifiers": [],
219
+ }
220
+ )
221
+
222
+ def _right_pan_start(self, e: ft.PointerEvent):
223
+ logger.debug(f"_pan_start: {e.local_position.x}, {e.local_position.y}")
224
+ self.send_message(
225
+ {
226
+ "type": "button_press",
227
+ "x": e.local_position.x * self.__dpr,
228
+ "y": e.local_position.y * self.__dpr,
229
+ "button": 2,
230
+ "buttons": 2,
231
+ "modifiers": [],
232
+ }
233
+ )
234
+
235
+ def _right_pan_update(self, e: ft.PointerEvent):
236
+ logger.debug(f"_pan_update: {e.local_position.x}, {e.local_position.y}")
237
+ self.send_message(
238
+ {
239
+ "type": "motion_notify",
240
+ "x": e.local_position.x * self.__dpr,
241
+ "y": e.local_position.y * self.__dpr,
242
+ "button": 0,
243
+ "buttons": 2,
244
+ "modifiers": [],
245
+ }
246
+ )
247
+
248
+ def _right_pan_end(self, e: ft.PointerEvent):
249
+ logger.debug(f"_pan_end: {e.local_position.x}, {e.local_position.y}")
250
+ self.send_message(
251
+ {
252
+ "type": "button_release",
253
+ "x": e.local_position.x * self.__dpr,
254
+ "y": e.local_position.y * self.__dpr,
255
+ "button": 2,
256
+ "buttons": 0,
257
+ "modifiers": [],
258
+ }
259
+ )
260
+
261
+ def will_unmount(self):
262
+ """
263
+ Called when the control is about to be removed from the page.
264
+ """
265
+ self.figure.canvas.manager.remove_web_socket(self)
266
+
267
+ def home(self):
268
+ """
269
+ Resets the view to the original state.
270
+ """
271
+ logger.debug("home)")
272
+ self.send_message({"type": "toolbar_button", "name": "home"})
273
+
274
+ def back(self):
275
+ """
276
+ Goes back to the previous view.
277
+ """
278
+ logger.debug("back()")
279
+ self.send_message({"type": "toolbar_button", "name": "back"})
280
+
281
+ def forward(self):
282
+ """
283
+ Goes forward to the next view.
284
+ """
285
+ logger.debug("forward)")
286
+ self.send_message({"type": "toolbar_button", "name": "forward"})
287
+
288
+ def pan(self):
289
+ """
290
+ Activates the pan tool.
291
+ """
292
+ logger.debug("pan()")
293
+ self.send_message({"type": "toolbar_button", "name": "pan"})
294
+
295
+ def zoom(self):
296
+ """
297
+ Activates the zoom tool.
298
+ """
299
+ logger.debug("zoom()")
300
+ self.send_message({"type": "toolbar_button", "name": "zoom"})
301
+
302
+ def download(self, format) -> bytes:
303
+ """
304
+ Downloads the current figure in the specified format.
305
+ Args:
306
+ format (str): The format to download the figure in (e.g., 'png',
307
+ 'jpg', 'svg', etc.).
308
+ Returns:
309
+ bytes: The figure image in the specified format as a byte array.
310
+ """
311
+ logger.debug(f"Download in format: {format}")
312
+ buff = BytesIO()
313
+ self.figure.savefig(buff, format=format, dpi=self.figure.dpi * self.__dpr)
314
+ return buff.getvalue()
315
+
316
+ async def _receive_loop(self):
317
+ while True:
318
+ is_binary, content = await self._receive_queue.get()
319
+ if is_binary:
320
+ logger.debug(f"receive_binary({len(content)})")
321
+ if self.__image_mode == "full":
322
+ await self.canvas.clear_capture()
323
+
324
+ self.canvas.shapes = [
325
+ fc.Image(
326
+ src_bytes=content,
327
+ x=0,
328
+ y=0,
329
+ width=self.figure.bbox.size[0] / self.__dpr,
330
+ height=self.figure.bbox.size[1] / self.__dpr,
331
+ )
332
+ ]
333
+ ft.context.disable_auto_update()
334
+ self.canvas.update()
335
+ await self.canvas.capture()
336
+ self.img_count += 1
337
+ self._waiting = False
338
+ else:
339
+ logger.debug(f"receive_json({content})")
340
+ if content["type"] == "image_mode":
341
+ self.__image_mode = content["mode"]
342
+ elif content["type"] == "cursor":
343
+ self.mouse_cursor = figure_cursors[content["cursor"]]
344
+ self.update()
345
+ elif content["type"] == "draw" and not self._waiting:
346
+ self._waiting = True
347
+ self.send_message({"type": "draw"})
348
+ elif content["type"] == "rubberband":
349
+ if len(self.canvas.shapes) == 2:
350
+ self.canvas.shapes.pop()
351
+ if (
352
+ content["x0"] != -1
353
+ and content["y0"] != -1
354
+ and content["x1"] != -1
355
+ and content["y1"] != -1
356
+ ):
357
+ x0 = content["x0"] / self.__dpr
358
+ y0 = self._height - content["y0"] / self.__dpr
359
+ x1 = content["x1"] / self.__dpr
360
+ y1 = self._height - content["y1"] / self.__dpr
361
+ self.canvas.shapes.append(
362
+ fc.Rect(
363
+ x=x0,
364
+ y=y0,
365
+ width=x1 - x0,
366
+ height=y1 - y0,
367
+ paint=ft.Paint(
368
+ stroke_width=1, style=ft.PaintingStyle.STROKE
369
+ ),
370
+ )
371
+ )
372
+ self.canvas.update()
373
+ elif content["type"] == "resize":
374
+ self.send_message({"type": "refresh"})
375
+ elif content["type"] == "message":
376
+ await self._trigger_event(
377
+ "message", {"message": content["message"]}
378
+ )
379
+ elif content["type"] == "history_buttons":
380
+ await self._trigger_event(
381
+ "toolbar_buttons_update",
382
+ {
383
+ "back_enabled": content["Back"],
384
+ "forward_enabled": content["Forward"],
385
+ },
386
+ )
387
+
388
+ def send_message(self, message):
389
+ """Sends a message to the figure's canvas manager."""
390
+ logger.debug(f"send_message({message})")
391
+ manager = self.figure.canvas.manager
392
+ if manager is not None:
393
+ manager.handle_json(message)
394
+
395
+ def send_json(self, content):
396
+ """Sends a JSON message to the front end."""
397
+ logger.debug(f"send_json: {content}")
398
+ self._main_loop.call_soon_threadsafe(
399
+ lambda: self._receive_queue.put_nowait((False, content))
400
+ )
401
+
402
+ def send_binary(self, blob):
403
+ """Sends a binary message to the front end."""
404
+ self._main_loop.call_soon_threadsafe(
405
+ lambda: self._receive_queue.put_nowait((True, blob))
406
+ )
407
+
408
+ async def _on_canvas_resize(self, e: fc.CanvasResizeEvent):
409
+ logger.debug(f"on_canvas_resize: {e.width}, {e.height}")
410
+
411
+ if not self.__started:
412
+ self.__started = True
413
+ asyncio.create_task(self._receive_loop())
414
+ self.figure.canvas.manager.add_web_socket(self)
415
+ self.send_message({"type": "send_image_mode"})
416
+ self.send_message(
417
+ {"type": "set_device_pixel_ratio", "device_pixel_ratio": self.__dpr}
418
+ )
419
+ self.send_message({"type": "refresh"})
420
+ self._width = e.width
421
+ self._height = e.height
422
+ self.send_message(
423
+ {"type": "resize", "width": self._width, "height": self._height}
424
+ )
@@ -0,0 +1,125 @@
1
+ from dataclasses import field
2
+ from typing import Any, Optional
3
+
4
+ import flet as ft
5
+ import flet_charts
6
+
7
+ _MATPLOTLIB_IMPORT_ERROR: Optional[ImportError] = None
8
+
9
+ try:
10
+ from matplotlib.figure import Figure # type: ignore
11
+ except ImportError as e: # pragma: no cover - depends on optional dependency
12
+ Figure = Any # type: ignore[assignment]
13
+ _MATPLOTLIB_IMPORT_ERROR = e
14
+
15
+ _download_formats = [
16
+ "eps",
17
+ "jpeg",
18
+ "pgf",
19
+ "pdf",
20
+ "png",
21
+ "ps",
22
+ "raw",
23
+ "svg",
24
+ "tif",
25
+ "webp",
26
+ ]
27
+
28
+
29
+ def _require_matplotlib() -> None:
30
+ if _MATPLOTLIB_IMPORT_ERROR is not None:
31
+ raise ModuleNotFoundError(
32
+ 'Install "matplotlib" Python package to use MatplotlibChart control.'
33
+ ) from _MATPLOTLIB_IMPORT_ERROR
34
+
35
+
36
+ @ft.control(kw_only=True, isolated=True)
37
+ class MatplotlibChartWithToolbar(ft.Column):
38
+ figure: Figure = field(metadata={"skip": True})
39
+ """
40
+ Matplotlib figure to draw - an instance of
41
+ [`matplotlib.figure.Figure`](https://matplotlib.org/stable/api/_as_gen/matplotlib.figure.Figure.html#matplotlib.figure.Figure).
42
+ """
43
+
44
+ def build(self):
45
+ _require_matplotlib()
46
+ self.mpl = flet_charts.MatplotlibChart(
47
+ figure=self.figure,
48
+ expand=True,
49
+ on_message=self.on_message,
50
+ on_toolbar_buttons_update=self.on_toolbar_update,
51
+ )
52
+ self.home_btn = ft.IconButton(ft.Icons.HOME, on_click=lambda: self.mpl.home())
53
+ self.back_btn = ft.IconButton(
54
+ ft.Icons.ARROW_BACK_ROUNDED, on_click=lambda: self.mpl.back()
55
+ )
56
+ self.fwd_btn = ft.IconButton(
57
+ ft.Icons.ARROW_FORWARD_ROUNDED, on_click=lambda: self.mpl.forward()
58
+ )
59
+ self.pan_btn = ft.IconButton(
60
+ ft.Icons.OPEN_WITH,
61
+ selected_icon=ft.Icons.OPEN_WITH,
62
+ selected_icon_color=ft.Colors.AMBER_800,
63
+ on_click=self.pan_click,
64
+ )
65
+ self.zoom_btn = ft.IconButton(
66
+ ft.Icons.ZOOM_IN,
67
+ selected_icon=ft.Icons.ZOOM_IN,
68
+ selected_icon_color=ft.Colors.AMBER_800,
69
+ on_click=self.zoom_click,
70
+ )
71
+ self.download_btn = ft.IconButton(
72
+ ft.Icons.DOWNLOAD, on_click=self.download_click
73
+ )
74
+ self.download_fmt = ft.Dropdown(
75
+ value="png",
76
+ options=[ft.DropdownOption(fmt) for fmt in _download_formats],
77
+ )
78
+ self.msg = ft.Text()
79
+ self.controls = [
80
+ ft.Row(
81
+ controls=[
82
+ self.home_btn,
83
+ self.back_btn,
84
+ self.fwd_btn,
85
+ self.pan_btn,
86
+ self.zoom_btn,
87
+ self.download_btn,
88
+ self.download_fmt,
89
+ self.msg,
90
+ ]
91
+ ),
92
+ self.mpl,
93
+ ]
94
+ if not self.expand:
95
+ if not self.height:
96
+ self.height = self.figure.bbox.height
97
+ if not self.width:
98
+ self.width = self.figure.bbox.width
99
+
100
+ def on_message(self, e: flet_charts.MatplotlibChartMessageEvent):
101
+ self.msg.value = e.message
102
+ self.msg.update()
103
+
104
+ def on_toolbar_update(
105
+ self, e: flet_charts.MatplotlibChartToolbarButtonsUpdateEvent
106
+ ):
107
+ self.back_btn.disabled = not e.back_enabled
108
+ self.fwd_btn.disabled = not e.forward_enabled
109
+ self.update()
110
+
111
+ def pan_click(self):
112
+ self.mpl.pan()
113
+ self.pan_btn.selected = not self.pan_btn.selected
114
+ self.zoom_btn.selected = False
115
+
116
+ def zoom_click(self):
117
+ self.mpl.zoom()
118
+ self.pan_btn.selected = False
119
+ self.zoom_btn.selected = not self.zoom_btn.selected
120
+
121
+ async def download_click(self):
122
+ fmt = self.download_fmt.value
123
+ buffer = self.mpl.download(fmt)
124
+ title = self.figure.canvas.manager.get_window_title()
125
+ await ft.FilePicker().save_file(file_name=f"{title}.{fmt}", src_bytes=buffer)
flet_charts/pie_chart.py CHANGED
@@ -2,9 +2,8 @@ from dataclasses import dataclass, field
2
2
  from typing import Optional
3
3
 
4
4
  import flet as ft
5
-
6
- from .pie_chart_section import PieChartSection
7
- from .types import ChartEventType
5
+ from flet_charts.pie_chart_section import PieChartSection
6
+ from flet_charts.types import ChartEventType
8
7
 
9
8
  __all__ = ["PieChart", "PieChartEvent"]
10
9
 
@@ -33,11 +32,9 @@ class PieChartEvent(ft.Event["PieChart"]):
33
32
 
34
33
 
35
34
  @ft.control("PieChart")
36
- class PieChart(ft.ConstrainedControl):
35
+ class PieChart(ft.LayoutControl):
37
36
  """
38
37
  A pie chart control displaying multiple sections as slices of a circle.
39
-
40
- ![Overview](assets/pie-chart/diagram.svg)
41
38
  """
42
39
 
43
40
  sections: list[PieChartSection] = field(default_factory=list)
@@ -10,10 +10,6 @@ __all__ = ["PieChartSection"]
10
10
  class PieChartSection(ft.BaseControl):
11
11
  """
12
12
  Configures a [PieChart][(p).] section.
13
-
14
- Raises:
15
- AssertionError: If [`title_position`][(c).] or
16
- [`badge_position`][(c).] is not between `0.0` and `1.0` inclusive.
17
13
  """
18
14
 
19
15
  value: ft.Number
@@ -51,11 +47,13 @@ class PieChartSection(ft.BaseControl):
51
47
  """
52
48
  The position/offset of the title relative to the section's center.
53
49
 
50
+ - `0.0`: near the center
51
+ - `1.0`: near the outside of the chart
52
+
54
53
  By default the title is drawn in the middle of the section.
55
54
 
56
- Note:
57
- Must be between `0.0` (near the center)
58
- and `1.0`(near the outside of the chart) inclusive.
55
+ Raises:
56
+ ValueError: If it is not between `0.0` and `1.0` inclusive.
59
57
  """
60
58
 
61
59
  badge: Optional[ft.Control] = None
@@ -67,20 +65,29 @@ class PieChartSection(ft.BaseControl):
67
65
  """
68
66
  The position/offset of the badge relative to the section's center.
69
67
 
68
+ - `0.0`: near the center
69
+ - `1.0`: near the outside of the chart
70
+
70
71
  By default the badge is drawn in the middle of the section.
71
72
 
72
- Note:
73
- Must be between `0.0` (near the center)
74
- and `1.0`(near the outside of the chart) inclusive.
73
+ Raises:
74
+ ValueError: If it is not between `0.0` and `1.0` inclusive.
75
+ """
76
+
77
+ gradient: Optional[ft.Gradient] = None
78
+ """
79
+ Defines the gradient of section. If specified, overrides the color setting.
75
80
  """
76
81
 
77
82
  def before_update(self):
78
83
  super().before_update()
79
- assert self.title_position is None or (0.0 <= self.title_position <= 1.0), (
80
- f"title_position must be between 0.0 and 1.0 inclusive, "
81
- f"got {self.title_position}"
82
- )
83
- assert self.badge_position is None or (0.0 <= self.badge_position <= 1.0), (
84
- f"badge_position must be between 0.0 and 1.0 inclusive, "
85
- f"got {self.badge_position}"
86
- )
84
+ if self.title_position is not None and not (0.0 <= self.title_position <= 1.0):
85
+ raise ValueError(
86
+ "title_position must be between 0.0 and 1.0 inclusive, "
87
+ f"got {self.title_position}"
88
+ )
89
+ if self.badge_position is not None and not (0.0 <= self.badge_position <= 1.0):
90
+ raise ValueError(
91
+ "badge_position must be between 0.0 and 1.0 inclusive, "
92
+ f"got {self.badge_position}"
93
+ )