flet-charts 0.85.0.dev2__tar.gz → 0.85.0.dev3__tar.gz
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.
- {flet_charts-0.85.0.dev2/src/flet_charts.egg-info → flet_charts-0.85.0.dev3}/PKG-INFO +2 -2
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/pyproject.toml +2 -2
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/__init__.py +6 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/line_chart.py +4 -0
- flet_charts-0.85.0.dev3/src/flet_charts/matplotlib_backends/backend_flet_agg.py +54 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/matplotlib_chart.py +98 -36
- flet_charts-0.85.0.dev3/src/flet_charts/matplotlib_chart_canvas.py +72 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3/src/flet_charts.egg-info}/PKG-INFO +2 -2
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts.egg-info/SOURCES.txt +2 -0
- flet_charts-0.85.0.dev3/src/flet_charts.egg-info/requires.txt +1 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/src/extension.dart +3 -0
- flet_charts-0.85.0.dev3/src/flutter/flet_charts/lib/src/matplotlib_chart_canvas.dart +276 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/src/utils/line_chart.dart +1 -1
- flet_charts-0.85.0.dev2/src/flet_charts/matplotlib_backends/backend_flet_agg.py +0 -20
- flet_charts-0.85.0.dev2/src/flet_charts.egg-info/requires.txt +0 -1
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/LICENSE +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/README.md +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/setup.cfg +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/bar_chart.py +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/bar_chart_group.py +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/bar_chart_rod.py +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/bar_chart_rod_stack_item.py +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/candlestick_chart.py +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/candlestick_chart_spot.py +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/chart_axis.py +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/line_chart_data.py +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/line_chart_data_point.py +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/matplotlib_chart_with_toolbar.py +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/pie_chart.py +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/pie_chart_section.py +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/plotly_chart.py +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/radar_chart.py +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/radar_data_set.py +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/scatter_chart.py +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/scatter_chart_spot.py +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/types.py +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts.egg-info/dependency_links.txt +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts.egg-info/top_level.txt +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/flet_charts.dart +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/src/bar_chart.dart +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/src/candlestick_chart.dart +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/src/line_chart.dart +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/src/pie_chart.dart +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/src/radar_chart.dart +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/src/scatter_chart.dart +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/src/utils/bar_chart.dart +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/src/utils/candlestick_chart.dart +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/src/utils/charts.dart +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/src/utils/pie_chart.dart +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/src/utils/radar_chart.dart +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/src/utils/scatter_chart.dart +0 -0
- {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/pubspec.yaml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flet-charts
|
|
3
|
-
Version: 0.85.0.
|
|
3
|
+
Version: 0.85.0.dev3
|
|
4
4
|
Summary: Interactive chart controls for Flet apps.
|
|
5
5
|
Author-email: Flet contributors <hello@flet.dev>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -11,7 +11,7 @@ Project-URL: Issues, https://github.com/flet-dev/flet/issues
|
|
|
11
11
|
Requires-Python: >=3.10
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist: flet==0.85.0.
|
|
14
|
+
Requires-Dist: flet==0.85.0.dev3
|
|
15
15
|
Dynamic: license-file
|
|
16
16
|
|
|
17
17
|
# flet-charts
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flet-charts"
|
|
3
|
-
version = "0.85.0.
|
|
3
|
+
version = "0.85.0.dev3"
|
|
4
4
|
description = "Interactive chart controls for Flet apps."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Flet contributors", email = "hello@flet.dev" }]
|
|
7
7
|
license = "Apache-2.0"
|
|
8
8
|
requires-python = ">=3.10"
|
|
9
9
|
dependencies = [
|
|
10
|
-
"flet==0.85.0.
|
|
10
|
+
"flet==0.85.0.dev3",
|
|
11
11
|
]
|
|
12
12
|
|
|
13
13
|
[project.urls]
|
|
@@ -33,6 +33,10 @@ from flet_charts.matplotlib_chart import (
|
|
|
33
33
|
MatplotlibChartMessageEvent,
|
|
34
34
|
MatplotlibChartToolbarButtonsUpdateEvent,
|
|
35
35
|
)
|
|
36
|
+
from flet_charts.matplotlib_chart_canvas import (
|
|
37
|
+
MatplotlibChartCanvas,
|
|
38
|
+
MatplotlibChartCanvasResizeEvent,
|
|
39
|
+
)
|
|
36
40
|
from flet_charts.matplotlib_chart_with_toolbar import MatplotlibChartWithToolbar
|
|
37
41
|
from flet_charts.pie_chart import PieChart, PieChartEvent
|
|
38
42
|
from flet_charts.pie_chart_section import PieChartSection
|
|
@@ -95,6 +99,8 @@ __all__ = [
|
|
|
95
99
|
"LineChartEventSpot",
|
|
96
100
|
"LineChartTooltip",
|
|
97
101
|
"MatplotlibChart",
|
|
102
|
+
"MatplotlibChartCanvas",
|
|
103
|
+
"MatplotlibChartCanvasResizeEvent",
|
|
98
104
|
"MatplotlibChartMessageEvent",
|
|
99
105
|
"MatplotlibChartToolbarButtonsUpdateEvent",
|
|
100
106
|
"MatplotlibChartWithToolbar",
|
|
@@ -59,6 +59,10 @@ class LineChartEvent(ft.Event["LineChart"]):
|
|
|
59
59
|
spots: list[LineChartEventSpot]
|
|
60
60
|
"""
|
|
61
61
|
Spots on which the event occurred.
|
|
62
|
+
|
|
63
|
+
Note:
|
|
64
|
+
This list is empty when the event does not target a concrete point, for
|
|
65
|
+
example when the pointer hovers over or taps empty chart space.
|
|
62
66
|
"""
|
|
63
67
|
|
|
64
68
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from matplotlib import _api
|
|
4
|
+
from matplotlib.backends import backend_webagg_core
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TimerFletAsyncio(backend_webagg_core.TimerAsyncio):
|
|
8
|
+
"""Asyncio timer that's safe to start from a worker thread.
|
|
9
|
+
|
|
10
|
+
Matplotlib's stock `TimerAsyncio._timer_start` calls
|
|
11
|
+
`asyncio.ensure_future`, which requires the calling thread to have a
|
|
12
|
+
current event loop. Flet's matplotlib chart runs `canvas.draw()` in a
|
|
13
|
+
worker thread to keep the asyncio loop free for input events; that
|
|
14
|
+
thread has no event loop. We capture the loop at construction time and
|
|
15
|
+
schedule via `run_coroutine_threadsafe` when invoked from off-loop.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, *args, **kwargs):
|
|
19
|
+
super().__init__(*args, **kwargs)
|
|
20
|
+
try:
|
|
21
|
+
self._loop = asyncio.get_running_loop()
|
|
22
|
+
except RuntimeError:
|
|
23
|
+
self._loop = asyncio.get_event_loop_policy().get_event_loop()
|
|
24
|
+
|
|
25
|
+
def _timer_start(self):
|
|
26
|
+
self._timer_stop()
|
|
27
|
+
coro = self._timer_task(max(self.interval / 1_000.0, 1e-6))
|
|
28
|
+
try:
|
|
29
|
+
current = asyncio.get_running_loop()
|
|
30
|
+
except RuntimeError:
|
|
31
|
+
current = None
|
|
32
|
+
if current is self._loop:
|
|
33
|
+
self._task = self._loop.create_task(coro)
|
|
34
|
+
else:
|
|
35
|
+
self._task = asyncio.run_coroutine_threadsafe(coro, self._loop)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FigureCanvasFletAgg(backend_webagg_core.FigureCanvasWebAggCore):
|
|
39
|
+
"""Canvas implementation used to render Matplotlib figures in Flet."""
|
|
40
|
+
|
|
41
|
+
manager_class = _api.classproperty(lambda cls: FigureManagerFletAgg)
|
|
42
|
+
supports_blit = False
|
|
43
|
+
_timer_cls = TimerFletAsyncio
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class FigureManagerFletAgg(backend_webagg_core.FigureManagerWebAgg):
|
|
47
|
+
"""Figure manager binding Matplotlib WebAgg tooling to Flet transport."""
|
|
48
|
+
|
|
49
|
+
_toolbar2_class = backend_webagg_core.NavigationToolbar2WebAgg
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
FigureCanvas = FigureCanvasFletAgg
|
|
53
|
+
FigureManager = FigureManagerFletAgg
|
|
54
|
+
interactive = True
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
+
import sys
|
|
4
|
+
import threading
|
|
3
5
|
from dataclasses import dataclass, field
|
|
4
6
|
from io import BytesIO
|
|
5
7
|
from typing import Any, Optional
|
|
6
8
|
|
|
7
9
|
import flet as ft
|
|
8
|
-
|
|
10
|
+
from flet_charts.matplotlib_chart_canvas import (
|
|
11
|
+
MatplotlibChartCanvas,
|
|
12
|
+
MatplotlibChartCanvasResizeEvent,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Pyodide / WASM has no real threads — `asyncio.to_thread` runs synchronously
|
|
16
|
+
# on the same thread there, providing no benefit. Fall back to a same-loop
|
|
17
|
+
# render path on those platforms.
|
|
18
|
+
_HAS_THREADS = sys.platform != "emscripten"
|
|
9
19
|
|
|
10
20
|
_MATPLOTLIB_IMPORT_ERROR: Optional[ImportError] = None
|
|
11
21
|
|
|
@@ -124,13 +134,21 @@ class MatplotlibChart(ft.GestureDetector):
|
|
|
124
134
|
logger.debug(f"DPR: {self.__dpr}")
|
|
125
135
|
self.__image_mode = "full"
|
|
126
136
|
|
|
127
|
-
self.
|
|
128
|
-
# resize_interval=10,
|
|
137
|
+
self.mpl_canvas = MatplotlibChartCanvas(
|
|
129
138
|
on_resize=self._on_canvas_resize,
|
|
130
139
|
expand=True,
|
|
131
140
|
)
|
|
141
|
+
# Rubberband (zoom selection) overlay drawn on top of the chart image.
|
|
142
|
+
self._rubberband = ft.Container(
|
|
143
|
+
visible=False,
|
|
144
|
+
border=ft.Border.all(1, ft.Colors.with_opacity(0.6, ft.Colors.GREY)),
|
|
145
|
+
)
|
|
146
|
+
self._stack = ft.Stack(
|
|
147
|
+
controls=[self.mpl_canvas, self._rubberband],
|
|
148
|
+
expand=True,
|
|
149
|
+
)
|
|
132
150
|
self.keyboard_listener = ft.KeyboardListener(
|
|
133
|
-
self.
|
|
151
|
+
self._stack,
|
|
134
152
|
autofocus=True,
|
|
135
153
|
on_key_down=self._on_key_down,
|
|
136
154
|
on_key_up=self._on_key_up,
|
|
@@ -151,6 +169,12 @@ class MatplotlibChart(ft.GestureDetector):
|
|
|
151
169
|
self._width = 0
|
|
152
170
|
self._height = 0
|
|
153
171
|
self._waiting = False
|
|
172
|
+
# Serializes worker-thread renders against main-thread matplotlib
|
|
173
|
+
# operations like `figure.savefig()` (download). matplotlib's
|
|
174
|
+
# print_figure temporarily nulls `canvas.manager` while saving, which
|
|
175
|
+
# would crash an in-flight `canvas.draw()` running in our render
|
|
176
|
+
# thread.
|
|
177
|
+
self._mpl_lock = threading.Lock()
|
|
154
178
|
|
|
155
179
|
def _on_key_down(self, e: ft.KeyboardEvent) -> None:
|
|
156
180
|
"""
|
|
@@ -405,7 +429,8 @@ class MatplotlibChart(ft.GestureDetector):
|
|
|
405
429
|
"""
|
|
406
430
|
logger.debug(f"Download in format: {format}")
|
|
407
431
|
buff = BytesIO()
|
|
408
|
-
|
|
432
|
+
with self._mpl_lock:
|
|
433
|
+
self.figure.savefig(buff, format=format, dpi=self.figure.dpi * self.__dpr)
|
|
409
434
|
return buff.getvalue()
|
|
410
435
|
|
|
411
436
|
async def _receive_loop(self):
|
|
@@ -419,23 +444,41 @@ class MatplotlibChart(ft.GestureDetector):
|
|
|
419
444
|
|
|
420
445
|
while True:
|
|
421
446
|
is_binary, content = await self._receive_queue.get()
|
|
447
|
+
|
|
448
|
+
# Coalesce stale items so interaction stays snappy:
|
|
449
|
+
# - Drop a binary frame if a newer one is queued behind it.
|
|
450
|
+
# - Drop a "draw" request if another is queued — the latest one
|
|
451
|
+
# will trigger the render with the most up-to-date state.
|
|
452
|
+
# Without this, every pointer event during pan/zoom triggers its
|
|
453
|
+
# own render and the chart visibly "plays back" buffered motion
|
|
454
|
+
# after the user releases the mouse.
|
|
422
455
|
if is_binary:
|
|
456
|
+
if any(it[0] for it in self._receive_queue._queue):
|
|
457
|
+
continue
|
|
458
|
+
elif (
|
|
459
|
+
isinstance(content, dict)
|
|
460
|
+
and content.get("type") == "draw"
|
|
461
|
+
and any(
|
|
462
|
+
not it[0]
|
|
463
|
+
and isinstance(it[1], dict)
|
|
464
|
+
and it[1].get("type") == "draw"
|
|
465
|
+
for it in self._receive_queue._queue
|
|
466
|
+
)
|
|
467
|
+
):
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
if is_binary:
|
|
471
|
+
assert isinstance(content, (bytes, bytearray))
|
|
423
472
|
logger.debug(f"receive_binary({len(content)})")
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
height=self.figure.bbox.size[1] / self.__dpr,
|
|
434
|
-
)
|
|
435
|
-
]
|
|
436
|
-
ft.context.disable_auto_update()
|
|
437
|
-
self.canvas.update()
|
|
438
|
-
await self.canvas.capture()
|
|
473
|
+
is_full = self.__image_mode == "full"
|
|
474
|
+
# Hand the frame to the client widget — full PNG replaces the
|
|
475
|
+
# backbuffer, diff PNG composites onto it. Awaiting naturally
|
|
476
|
+
# rate-limits this loop to the client's processing speed and
|
|
477
|
+
# yields the asyncio loop for incoming events.
|
|
478
|
+
if is_full:
|
|
479
|
+
await self.mpl_canvas.apply_full(bytes(content))
|
|
480
|
+
else:
|
|
481
|
+
await self.mpl_canvas.apply_diff(bytes(content))
|
|
439
482
|
self.img_count += 1
|
|
440
483
|
self._waiting = False
|
|
441
484
|
else:
|
|
@@ -447,10 +490,23 @@ class MatplotlibChart(ft.GestureDetector):
|
|
|
447
490
|
self.update()
|
|
448
491
|
elif content["type"] == "draw" and not self._waiting:
|
|
449
492
|
self._waiting = True
|
|
450
|
-
|
|
493
|
+
if _HAS_THREADS:
|
|
494
|
+
# Native runtime: render in a worker thread so the
|
|
495
|
+
# asyncio loop stays free for input events. handle_draw
|
|
496
|
+
# ends up in Agg/PIL C code that releases the GIL, so
|
|
497
|
+
# threading is effective. _waiting + the queue-dedupe
|
|
498
|
+
# above ensure only one render is ever in flight.
|
|
499
|
+
# The lock prevents overlap with main-thread savefig.
|
|
500
|
+
asyncio.create_task(asyncio.to_thread(self._draw_locked))
|
|
501
|
+
else:
|
|
502
|
+
# Pyodide / WASM: no real threads available. Render
|
|
503
|
+
# synchronously on the loop. Yield first so any
|
|
504
|
+
# backed-up pointer events can update matplotlib state
|
|
505
|
+
# before the (blocking) render runs.
|
|
506
|
+
for _ in range(10):
|
|
507
|
+
await asyncio.sleep(0)
|
|
508
|
+
self.send_message({"type": "draw"})
|
|
451
509
|
elif content["type"] == "rubberband":
|
|
452
|
-
if len(self.canvas.shapes) == 2:
|
|
453
|
-
self.canvas.shapes.pop()
|
|
454
510
|
if (
|
|
455
511
|
content["x0"] != -1
|
|
456
512
|
and content["y0"] != -1
|
|
@@ -461,18 +517,14 @@ class MatplotlibChart(ft.GestureDetector):
|
|
|
461
517
|
y0 = self._height - content["y0"] / self.__dpr
|
|
462
518
|
x1 = content["x1"] / self.__dpr
|
|
463
519
|
y1 = self._height - content["y1"] / self.__dpr
|
|
464
|
-
self.
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
),
|
|
473
|
-
)
|
|
474
|
-
)
|
|
475
|
-
self.canvas.update()
|
|
520
|
+
self._rubberband.left = min(x0, x1)
|
|
521
|
+
self._rubberband.top = min(y0, y1)
|
|
522
|
+
self._rubberband.width = abs(x1 - x0)
|
|
523
|
+
self._rubberband.height = abs(y1 - y0)
|
|
524
|
+
self._rubberband.visible = True
|
|
525
|
+
else:
|
|
526
|
+
self._rubberband.visible = False
|
|
527
|
+
self._rubberband.update()
|
|
476
528
|
elif content["type"] == "resize":
|
|
477
529
|
self.send_message({"type": "refresh"})
|
|
478
530
|
elif content["type"] == "message":
|
|
@@ -495,6 +547,16 @@ class MatplotlibChart(ft.GestureDetector):
|
|
|
495
547
|
if manager is not None:
|
|
496
548
|
manager.handle_json(message)
|
|
497
549
|
|
|
550
|
+
def _draw_locked(self):
|
|
551
|
+
"""Worker-thread entry point for triggering a render.
|
|
552
|
+
|
|
553
|
+
Holds `_mpl_lock` for the duration of the synchronous draw so it
|
|
554
|
+
can't overlap with main-thread `figure.savefig()`, which temporarily
|
|
555
|
+
nulls `canvas.manager` and would crash an in-flight render.
|
|
556
|
+
"""
|
|
557
|
+
with self._mpl_lock:
|
|
558
|
+
self.send_message({"type": "draw"})
|
|
559
|
+
|
|
498
560
|
def send_json(self, content):
|
|
499
561
|
"""Sends a JSON message to the front end."""
|
|
500
562
|
logger.debug(f"send_json: {content}")
|
|
@@ -508,7 +570,7 @@ class MatplotlibChart(ft.GestureDetector):
|
|
|
508
570
|
lambda: self._receive_queue.put_nowait((True, blob))
|
|
509
571
|
)
|
|
510
572
|
|
|
511
|
-
async def _on_canvas_resize(self, e:
|
|
573
|
+
async def _on_canvas_resize(self, e: MatplotlibChartCanvasResizeEvent):
|
|
512
574
|
"""
|
|
513
575
|
Handle canvas resize and initialize backend session on first resize.
|
|
514
576
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import flet as ft
|
|
5
|
+
|
|
6
|
+
__all__ = ["MatplotlibChartCanvas", "MatplotlibChartCanvasResizeEvent"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class MatplotlibChartCanvasResizeEvent(ft.Event["MatplotlibChartCanvas"]):
|
|
11
|
+
"""
|
|
12
|
+
Event emitted when the canvas reports a new rendered size.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
width: float = field(metadata={"data_field": "w"})
|
|
16
|
+
"""New width of the canvas in logical pixels."""
|
|
17
|
+
|
|
18
|
+
height: float = field(metadata={"data_field": "h"})
|
|
19
|
+
"""New height of the canvas in logical pixels."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@ft.control("MatplotlibChartCanvas")
|
|
23
|
+
class MatplotlibChartCanvas(ft.LayoutControl):
|
|
24
|
+
"""
|
|
25
|
+
Display widget for matplotlib WebAgg-style image streams.
|
|
26
|
+
|
|
27
|
+
Receives full and incremental "diff" PNG frames and composites them in
|
|
28
|
+
CPU memory, holding at most one decoded image for display at a time.
|
|
29
|
+
Avoids the per-frame `Picture.toImage` allocations that the generic
|
|
30
|
+
`flet.canvas.Canvas` capture path uses, which on Flutter web
|
|
31
|
+
(CanvasKit/WASM) accumulate and aren't promptly reclaimed by the JS GC
|
|
32
|
+
during animation, causing browser memory growth.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
resize_interval: ft.Number = 10
|
|
36
|
+
"""
|
|
37
|
+
Sampling interval in milliseconds for `on_resize` event.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
on_resize: Optional[ft.EventHandler[MatplotlibChartCanvasResizeEvent]] = None
|
|
41
|
+
"""
|
|
42
|
+
Called when the size of this canvas has changed.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
async def apply_full(self, image_bytes: bytes) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Replace the current displayed image with a full PNG frame.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
image_bytes: PNG bytes of the complete frame.
|
|
51
|
+
"""
|
|
52
|
+
await self._invoke_method("apply_full", arguments={"bytes": image_bytes})
|
|
53
|
+
|
|
54
|
+
async def apply_diff(self, image_bytes: bytes) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Composite an incremental "diff" PNG frame onto the current image.
|
|
57
|
+
|
|
58
|
+
Pixels with non-zero alpha replace the corresponding pixels in the
|
|
59
|
+
existing backbuffer; transparent pixels leave the backbuffer
|
|
60
|
+
unchanged. If no backbuffer exists yet, the diff is treated as a
|
|
61
|
+
full frame.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
image_bytes: PNG bytes of the diff frame.
|
|
65
|
+
"""
|
|
66
|
+
await self._invoke_method("apply_diff", arguments={"bytes": image_bytes})
|
|
67
|
+
|
|
68
|
+
async def clear(self) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Clear the displayed image and discard the backbuffer.
|
|
71
|
+
"""
|
|
72
|
+
await self._invoke_method("clear")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flet-charts
|
|
3
|
-
Version: 0.85.0.
|
|
3
|
+
Version: 0.85.0.dev3
|
|
4
4
|
Summary: Interactive chart controls for Flet apps.
|
|
5
5
|
Author-email: Flet contributors <hello@flet.dev>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -11,7 +11,7 @@ Project-URL: Issues, https://github.com/flet-dev/flet/issues
|
|
|
11
11
|
Requires-Python: >=3.10
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist: flet==0.85.0.
|
|
14
|
+
Requires-Dist: flet==0.85.0.dev3
|
|
15
15
|
Dynamic: license-file
|
|
16
16
|
|
|
17
17
|
# flet-charts
|
|
@@ -13,6 +13,7 @@ src/flet_charts/line_chart.py
|
|
|
13
13
|
src/flet_charts/line_chart_data.py
|
|
14
14
|
src/flet_charts/line_chart_data_point.py
|
|
15
15
|
src/flet_charts/matplotlib_chart.py
|
|
16
|
+
src/flet_charts/matplotlib_chart_canvas.py
|
|
16
17
|
src/flet_charts/matplotlib_chart_with_toolbar.py
|
|
17
18
|
src/flet_charts/pie_chart.py
|
|
18
19
|
src/flet_charts/pie_chart_section.py
|
|
@@ -34,6 +35,7 @@ src/flutter/flet_charts/lib/src/bar_chart.dart
|
|
|
34
35
|
src/flutter/flet_charts/lib/src/candlestick_chart.dart
|
|
35
36
|
src/flutter/flet_charts/lib/src/extension.dart
|
|
36
37
|
src/flutter/flet_charts/lib/src/line_chart.dart
|
|
38
|
+
src/flutter/flet_charts/lib/src/matplotlib_chart_canvas.dart
|
|
37
39
|
src/flutter/flet_charts/lib/src/pie_chart.dart
|
|
38
40
|
src/flutter/flet_charts/lib/src/radar_chart.dart
|
|
39
41
|
src/flutter/flet_charts/lib/src/scatter_chart.dart
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flet==0.85.0.dev3
|
{flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/src/extension.dart
RENAMED
|
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|
|
4
4
|
import 'bar_chart.dart';
|
|
5
5
|
import 'candlestick_chart.dart';
|
|
6
6
|
import 'line_chart.dart';
|
|
7
|
+
import 'matplotlib_chart_canvas.dart';
|
|
7
8
|
import 'radar_chart.dart';
|
|
8
9
|
import 'pie_chart.dart';
|
|
9
10
|
import 'scatter_chart.dart';
|
|
@@ -18,6 +19,8 @@ class Extension extends FletExtension {
|
|
|
18
19
|
return CandlestickChartControl(key: key, control: control);
|
|
19
20
|
case "LineChart":
|
|
20
21
|
return LineChartControl(key: key, control: control);
|
|
22
|
+
case "MatplotlibChartCanvas":
|
|
23
|
+
return MatplotlibChartCanvasControl(key: key, control: control);
|
|
21
24
|
case "RadarChart":
|
|
22
25
|
return RadarChartControl(key: key, control: control);
|
|
23
26
|
case "PieChart":
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import 'dart:async';
|
|
2
|
+
import 'dart:typed_data';
|
|
3
|
+
import 'dart:ui' as ui;
|
|
4
|
+
|
|
5
|
+
import 'package:flet/flet.dart';
|
|
6
|
+
import 'package:flutter/material.dart';
|
|
7
|
+
|
|
8
|
+
/// Display widget for matplotlib WebAgg-style image streams.
|
|
9
|
+
///
|
|
10
|
+
/// Receives full and incremental "diff" PNG frames via control method calls
|
|
11
|
+
/// and composites them in CPU memory. Holds at most one [ui.Image] for
|
|
12
|
+
/// display at a time, replacing it on each apply. This avoids the per-frame
|
|
13
|
+
/// `Picture.toImage` allocations that the generic Canvas+capture path uses,
|
|
14
|
+
/// which on Flutter web (CanvasKit/WASM) accumulate and are not promptly
|
|
15
|
+
/// reclaimed by the JS GC during animations.
|
|
16
|
+
class MatplotlibChartCanvasControl extends StatefulWidget {
|
|
17
|
+
final Control control;
|
|
18
|
+
|
|
19
|
+
MatplotlibChartCanvasControl({Key? key, required this.control})
|
|
20
|
+
: super(key: key ?? ValueKey("control_${control.id}"));
|
|
21
|
+
|
|
22
|
+
@override
|
|
23
|
+
State<MatplotlibChartCanvasControl> createState() =>
|
|
24
|
+
_MatplotlibChartCanvasState();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class _MatplotlibChartCanvasState extends State<MatplotlibChartCanvasControl> {
|
|
28
|
+
ui.Image? _displayImage;
|
|
29
|
+
Uint8List? _backbuffer;
|
|
30
|
+
int _bbWidth = 0;
|
|
31
|
+
int _bbHeight = 0;
|
|
32
|
+
|
|
33
|
+
// Serialize concurrent apply_full / apply_diff calls. Each invocation
|
|
34
|
+
// awaits the previous one so the backbuffer mutations happen in order.
|
|
35
|
+
Future<void>? _applyChain;
|
|
36
|
+
|
|
37
|
+
Size _lastSize = Size.zero;
|
|
38
|
+
int _lastResize = DateTime.now().millisecondsSinceEpoch;
|
|
39
|
+
|
|
40
|
+
@override
|
|
41
|
+
void initState() {
|
|
42
|
+
super.initState();
|
|
43
|
+
widget.control.addInvokeMethodListener(_invokeMethod);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@override
|
|
47
|
+
void dispose() {
|
|
48
|
+
widget.control.removeInvokeMethodListener(_invokeMethod);
|
|
49
|
+
_displayImage?.dispose();
|
|
50
|
+
_displayImage = null;
|
|
51
|
+
_backbuffer = null;
|
|
52
|
+
super.dispose();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
Future<dynamic> _invokeMethod(String name, dynamic args) async {
|
|
56
|
+
switch (name) {
|
|
57
|
+
case "apply_full":
|
|
58
|
+
await _enqueue(() => _applyFull(_extractBytes(args)));
|
|
59
|
+
return;
|
|
60
|
+
case "apply_diff":
|
|
61
|
+
await _enqueue(() => _applyDiff(_extractBytes(args)));
|
|
62
|
+
return;
|
|
63
|
+
case "clear":
|
|
64
|
+
await _enqueue(() async {
|
|
65
|
+
_disposeDisplay();
|
|
66
|
+
_backbuffer = null;
|
|
67
|
+
_bbWidth = 0;
|
|
68
|
+
_bbHeight = 0;
|
|
69
|
+
if (mounted) setState(() {});
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
default:
|
|
73
|
+
throw Exception("Unknown MatplotlibChartCanvas method: $name");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
Uint8List _extractBytes(dynamic args) {
|
|
78
|
+
final v = args is Map ? args["bytes"] : args;
|
|
79
|
+
if (v is Uint8List) return v;
|
|
80
|
+
if (v is ByteData) {
|
|
81
|
+
return v.buffer.asUint8List(v.offsetInBytes, v.lengthInBytes);
|
|
82
|
+
}
|
|
83
|
+
if (v is List<int>) return Uint8List.fromList(v);
|
|
84
|
+
if (v is List && v.every((e) => e is int)) {
|
|
85
|
+
return Uint8List.fromList(v.cast<int>());
|
|
86
|
+
}
|
|
87
|
+
throw ArgumentError("Expected bytes for image data, got ${v.runtimeType}");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Chains apply operations so they run sequentially. Without this,
|
|
91
|
+
// overlapping awaits could let a later diff be composited before an
|
|
92
|
+
// earlier full frame finished decoding, producing tearing.
|
|
93
|
+
Future<void> _enqueue(Future<void> Function() task) {
|
|
94
|
+
final prev = _applyChain ?? Future.value();
|
|
95
|
+
final next = prev.then((_) => task());
|
|
96
|
+
_applyChain = next.catchError((_) {});
|
|
97
|
+
return next;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
Future<void> _applyFull(Uint8List bytes) async {
|
|
101
|
+
final decoded = await _decodeRgba(bytes);
|
|
102
|
+
if (decoded == null) return;
|
|
103
|
+
|
|
104
|
+
_backbuffer = decoded.bytes;
|
|
105
|
+
_bbWidth = decoded.width;
|
|
106
|
+
_bbHeight = decoded.height;
|
|
107
|
+
|
|
108
|
+
final image = await _makeImage(decoded.bytes, decoded.width, decoded.height);
|
|
109
|
+
_swapDisplay(image);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
Future<void> _applyDiff(Uint8List bytes) async {
|
|
113
|
+
if (_backbuffer == null) {
|
|
114
|
+
// No baseline yet — treat as full so we don't render a transparent
|
|
115
|
+
// diff with no underlying frame.
|
|
116
|
+
await _applyFull(bytes);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
final decoded = await _decodeRgba(bytes);
|
|
121
|
+
if (decoded == null) return;
|
|
122
|
+
|
|
123
|
+
// Diffs from matplotlib are sized to the figure buffer. If the frame
|
|
124
|
+
// size has changed since the last full frame (e.g. resize race),
|
|
125
|
+
// promote to a full replace.
|
|
126
|
+
if (decoded.width != _bbWidth ||
|
|
127
|
+
decoded.height != _bbHeight ||
|
|
128
|
+
decoded.bytes.length != _backbuffer!.length) {
|
|
129
|
+
_backbuffer = decoded.bytes;
|
|
130
|
+
_bbWidth = decoded.width;
|
|
131
|
+
_bbHeight = decoded.height;
|
|
132
|
+
final image =
|
|
133
|
+
await _makeImage(decoded.bytes, decoded.width, decoded.height);
|
|
134
|
+
_swapDisplay(image);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Composite: matplotlib's diff PNG has alpha=0 for unchanged pixels.
|
|
139
|
+
// Where alpha != 0, copy the new pixel into the backbuffer.
|
|
140
|
+
final bb = _backbuffer!.buffer.asUint32List();
|
|
141
|
+
final df = decoded.bytes.buffer.asUint32List();
|
|
142
|
+
for (int i = 0; i < df.length; i++) {
|
|
143
|
+
// RGBA8888 on little-endian: alpha is the highest byte (0xFF000000).
|
|
144
|
+
if ((df[i] & 0xFF000000) != 0) {
|
|
145
|
+
bb[i] = df[i];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
final image = await _makeImage(_backbuffer!, _bbWidth, _bbHeight);
|
|
150
|
+
_swapDisplay(image);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
Future<_DecodedRgba?> _decodeRgba(Uint8List bytes) async {
|
|
154
|
+
if (bytes.isEmpty) {
|
|
155
|
+
debugPrint("MatplotlibChartCanvas: skipping empty image bytes");
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
// Take a defensive copy. msgpack_dart sometimes hands us a Uint8List
|
|
159
|
+
// backed by a buffer that's reused/freed by Safari's WASM runtime,
|
|
160
|
+
// causing CanvasKit's async decoder to throw "EncodingError: Loading
|
|
161
|
+
// error." after the original buffer is gone.
|
|
162
|
+
final owned = Uint8List.fromList(bytes);
|
|
163
|
+
ui.Codec? codec;
|
|
164
|
+
ui.Image? img;
|
|
165
|
+
try {
|
|
166
|
+
codec = await ui.instantiateImageCodec(owned, allowUpscaling: false);
|
|
167
|
+
final frame = await codec.getNextFrame();
|
|
168
|
+
img = frame.image;
|
|
169
|
+
final byteData =
|
|
170
|
+
await img.toByteData(format: ui.ImageByteFormat.rawRgba);
|
|
171
|
+
if (byteData == null) return null;
|
|
172
|
+
return _DecodedRgba(
|
|
173
|
+
bytes: byteData.buffer.asUint8List(),
|
|
174
|
+
width: img.width,
|
|
175
|
+
height: img.height,
|
|
176
|
+
);
|
|
177
|
+
} catch (e) {
|
|
178
|
+
debugPrint(
|
|
179
|
+
"MatplotlibChartCanvas: decode failed (${owned.length} bytes): $e");
|
|
180
|
+
rethrow;
|
|
181
|
+
} finally {
|
|
182
|
+
img?.dispose();
|
|
183
|
+
codec?.dispose();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
Future<ui.Image> _makeImage(Uint8List rgba, int width, int height) {
|
|
188
|
+
final completer = Completer<ui.Image>();
|
|
189
|
+
ui.decodeImageFromPixels(
|
|
190
|
+
rgba,
|
|
191
|
+
width,
|
|
192
|
+
height,
|
|
193
|
+
ui.PixelFormat.rgba8888,
|
|
194
|
+
completer.complete,
|
|
195
|
+
);
|
|
196
|
+
return completer.future;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
void _swapDisplay(ui.Image newImage) {
|
|
200
|
+
final old = _displayImage;
|
|
201
|
+
_displayImage = newImage;
|
|
202
|
+
if (mounted) setState(() {});
|
|
203
|
+
if (old != null) {
|
|
204
|
+
// Defer disposal to the next frame so any in-flight paint that still
|
|
205
|
+
// references the old image completes first.
|
|
206
|
+
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
207
|
+
old.dispose();
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
void _disposeDisplay() {
|
|
213
|
+
final old = _displayImage;
|
|
214
|
+
_displayImage = null;
|
|
215
|
+
if (old != null) {
|
|
216
|
+
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
217
|
+
old.dispose();
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
void _maybeReportResize(Size size) {
|
|
223
|
+
final resizeInterval = widget.control.getInt("resize_interval", 10)!;
|
|
224
|
+
final now = DateTime.now().millisecondsSinceEpoch;
|
|
225
|
+
if ((now - _lastResize > resizeInterval && _lastSize != size) ||
|
|
226
|
+
_lastSize.isEmpty) {
|
|
227
|
+
_lastSize = size;
|
|
228
|
+
_lastResize = now;
|
|
229
|
+
widget.control.triggerEvent("resize", {"w": size.width, "h": size.height});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@override
|
|
234
|
+
Widget build(BuildContext context) {
|
|
235
|
+
return LayoutBuilder(
|
|
236
|
+
builder: (context, constraints) {
|
|
237
|
+
// Fire on_resize on layout. matplotlib uses this to know the target
|
|
238
|
+
// figure size.
|
|
239
|
+
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
240
|
+
if (!mounted) return;
|
|
241
|
+
_maybeReportResize(constraints.biggest);
|
|
242
|
+
});
|
|
243
|
+
return CustomPaint(
|
|
244
|
+
size: constraints.biggest,
|
|
245
|
+
painter: _MatplotlibImagePainter(_displayImage),
|
|
246
|
+
);
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
class _DecodedRgba {
|
|
253
|
+
final Uint8List bytes;
|
|
254
|
+
final int width;
|
|
255
|
+
final int height;
|
|
256
|
+
_DecodedRgba({required this.bytes, required this.width, required this.height});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
class _MatplotlibImagePainter extends CustomPainter {
|
|
260
|
+
final ui.Image? image;
|
|
261
|
+
|
|
262
|
+
_MatplotlibImagePainter(this.image);
|
|
263
|
+
|
|
264
|
+
@override
|
|
265
|
+
void paint(Canvas canvas, Size size) {
|
|
266
|
+
final img = image;
|
|
267
|
+
if (img == null) return;
|
|
268
|
+
final src =
|
|
269
|
+
Rect.fromLTWH(0, 0, img.width.toDouble(), img.height.toDouble());
|
|
270
|
+
final dst = Rect.fromLTWH(0, 0, size.width, size.height);
|
|
271
|
+
canvas.drawImageRect(img, src, dst, Paint());
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
@override
|
|
275
|
+
bool shouldRepaint(_MatplotlibImagePainter old) => old.image != image;
|
|
276
|
+
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
from matplotlib import _api
|
|
2
|
-
from matplotlib.backends import backend_webagg_core
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class FigureCanvasFletAgg(backend_webagg_core.FigureCanvasWebAggCore):
|
|
6
|
-
"""Canvas implementation used to render Matplotlib figures in Flet."""
|
|
7
|
-
|
|
8
|
-
manager_class = _api.classproperty(lambda cls: FigureManagerFletAgg)
|
|
9
|
-
supports_blit = False
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class FigureManagerFletAgg(backend_webagg_core.FigureManagerWebAgg):
|
|
13
|
-
"""Figure manager binding Matplotlib WebAgg tooling to Flet transport."""
|
|
14
|
-
|
|
15
|
-
_toolbar2_class = backend_webagg_core.NavigationToolbar2WebAgg
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
FigureCanvas = FigureCanvasFletAgg
|
|
19
|
-
FigureManager = FigureManagerFletAgg
|
|
20
|
-
interactive = True
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
flet==0.85.0.dev2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/bar_chart_rod_stack_item.py
RENAMED
|
File without changes
|
|
File without changes
|
{flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/candlestick_chart_spot.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/line_chart_data_point.py
RENAMED
|
File without changes
|
{flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts/matplotlib_chart_with_toolbar.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flet_charts.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/flet_charts.dart
RENAMED
|
File without changes
|
{flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/src/bar_chart.dart
RENAMED
|
File without changes
|
|
File without changes
|
{flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/src/line_chart.dart
RENAMED
|
File without changes
|
{flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/src/pie_chart.dart
RENAMED
|
File without changes
|
{flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev3}/src/flutter/flet_charts/lib/src/radar_chart.dart
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|