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.
- flet_charts/__init__.py +59 -17
- flet_charts/bar_chart.py +87 -30
- flet_charts/bar_chart_group.py +1 -2
- flet_charts/bar_chart_rod.py +36 -5
- flet_charts/candlestick_chart.py +269 -0
- flet_charts/candlestick_chart_spot.py +98 -0
- flet_charts/chart_axis.py +29 -9
- flet_charts/line_chart.py +76 -14
- flet_charts/line_chart_data.py +30 -5
- flet_charts/line_chart_data_point.py +33 -4
- flet_charts/matplotlib_backends/backend_flet_agg.py +16 -0
- flet_charts/matplotlib_chart.py +396 -36
- flet_charts/matplotlib_chart_with_toolbar.py +125 -0
- flet_charts/pie_chart.py +3 -6
- flet_charts/pie_chart_section.py +25 -18
- flet_charts/plotly_chart.py +17 -6
- flet_charts/radar_chart.py +214 -0
- flet_charts/radar_data_set.py +66 -0
- flet_charts/scatter_chart.py +75 -29
- flet_charts/scatter_chart_spot.py +44 -6
- flet_charts/types.py +159 -16
- flet_charts-0.70.0.dev6551.dist-info/METADATA +67 -0
- flet_charts-0.70.0.dev6551.dist-info/RECORD +47 -0
- flutter/flet_charts/CHANGELOG.md +1 -1
- flutter/flet_charts/LICENSE +1 -1
- flutter/flet_charts/analysis_options.yaml +1 -1
- flutter/flet_charts/lib/src/bar_chart.dart +2 -0
- flutter/flet_charts/lib/src/candlestick_chart.dart +129 -0
- flutter/flet_charts/lib/src/extension.dart +6 -0
- flutter/flet_charts/lib/src/radar_chart.dart +104 -0
- flutter/flet_charts/lib/src/scatter_chart.dart +22 -21
- flutter/flet_charts/lib/src/utils/bar_chart.dart +137 -73
- flutter/flet_charts/lib/src/utils/candlestick_chart.dart +118 -0
- flutter/flet_charts/lib/src/utils/charts.dart +12 -0
- flutter/flet_charts/lib/src/utils/line_chart.dart +15 -3
- flutter/flet_charts/lib/src/utils/pie_chart.dart +1 -0
- flutter/flet_charts/lib/src/utils/radar_chart.dart +90 -0
- flutter/flet_charts/lib/src/utils/scatter_chart.dart +22 -21
- flutter/flet_charts/pubspec.lock +85 -71
- flutter/flet_charts/pubspec.yaml +10 -9
- flet_charts-0.2.0.dev35.dist-info/METADATA +0 -69
- flet_charts-0.2.0.dev35.dist-info/RECORD +0 -38
- flutter/flet_charts/README.md +0 -3
- {flet_charts-0.2.0.dev35.dist-info → flet_charts-0.70.0.dev6551.dist-info}/WHEEL +0 -0
- {flet_charts-0.2.0.dev35.dist-info → flet_charts-0.70.0.dev6551.dist-info}/licenses/LICENSE +0 -0
- {flet_charts-0.2.0.dev35.dist-info → flet_charts-0.70.0.dev6551.dist-info}/top_level.txt +0 -0
flet_charts/matplotlib_chart.py
CHANGED
|
@@ -1,27 +1,78 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
from
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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.
|
|
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/)
|
|
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
|
-
|
|
86
|
+
on_message: Optional[ft.EventHandler[MatplotlibChartMessageEvent]] = None
|
|
36
87
|
"""
|
|
37
|
-
|
|
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
|
-
|
|
91
|
+
on_toolbar_buttons_update: Optional[
|
|
92
|
+
ft.EventHandler[MatplotlibChartToolbarButtonsUpdateEvent]
|
|
93
|
+
] = None
|
|
43
94
|
"""
|
|
44
|
-
|
|
95
|
+
Triggers when toolbar buttons status is updated.
|
|
45
96
|
"""
|
|
46
97
|
|
|
47
98
|
def init(self):
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
self.
|
|
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 .
|
|
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.
|
|
35
|
+
class PieChart(ft.LayoutControl):
|
|
37
36
|
"""
|
|
38
37
|
A pie chart control displaying multiple sections as slices of a circle.
|
|
39
|
-
|
|
40
|
-

|
|
41
38
|
"""
|
|
42
39
|
|
|
43
40
|
sections: list[PieChartSection] = field(default_factory=list)
|
flet_charts/pie_chart_section.py
CHANGED
|
@@ -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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
+
)
|