flet-charts 0.85.0.dev2__tar.gz → 0.85.0.dev4__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.
Files changed (51) hide show
  1. {flet_charts-0.85.0.dev2/src/flet_charts.egg-info → flet_charts-0.85.0.dev4}/PKG-INFO +2 -2
  2. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/pyproject.toml +2 -2
  3. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/__init__.py +6 -0
  4. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/line_chart.py +4 -0
  5. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/matplotlib_chart.py +33 -33
  6. flet_charts-0.85.0.dev4/src/flet_charts/matplotlib_chart_canvas.py +72 -0
  7. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4/src/flet_charts.egg-info}/PKG-INFO +2 -2
  8. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts.egg-info/SOURCES.txt +2 -0
  9. flet_charts-0.85.0.dev4/src/flet_charts.egg-info/requires.txt +1 -0
  10. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flutter/flet_charts/lib/src/extension.dart +3 -0
  11. flet_charts-0.85.0.dev4/src/flutter/flet_charts/lib/src/matplotlib_chart_canvas.dart +457 -0
  12. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flutter/flet_charts/lib/src/utils/line_chart.dart +1 -1
  13. flet_charts-0.85.0.dev2/src/flet_charts.egg-info/requires.txt +0 -1
  14. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/LICENSE +0 -0
  15. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/README.md +0 -0
  16. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/setup.cfg +0 -0
  17. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/bar_chart.py +0 -0
  18. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/bar_chart_group.py +0 -0
  19. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/bar_chart_rod.py +0 -0
  20. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/bar_chart_rod_stack_item.py +0 -0
  21. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/candlestick_chart.py +0 -0
  22. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/candlestick_chart_spot.py +0 -0
  23. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/chart_axis.py +0 -0
  24. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/line_chart_data.py +0 -0
  25. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/line_chart_data_point.py +0 -0
  26. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/matplotlib_backends/backend_flet_agg.py +0 -0
  27. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/matplotlib_chart_with_toolbar.py +0 -0
  28. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/pie_chart.py +0 -0
  29. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/pie_chart_section.py +0 -0
  30. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/plotly_chart.py +0 -0
  31. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/radar_chart.py +0 -0
  32. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/radar_data_set.py +0 -0
  33. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/scatter_chart.py +0 -0
  34. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/scatter_chart_spot.py +0 -0
  35. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts/types.py +0 -0
  36. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts.egg-info/dependency_links.txt +0 -0
  37. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flet_charts.egg-info/top_level.txt +0 -0
  38. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flutter/flet_charts/lib/flet_charts.dart +0 -0
  39. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flutter/flet_charts/lib/src/bar_chart.dart +0 -0
  40. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flutter/flet_charts/lib/src/candlestick_chart.dart +0 -0
  41. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flutter/flet_charts/lib/src/line_chart.dart +0 -0
  42. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flutter/flet_charts/lib/src/pie_chart.dart +0 -0
  43. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flutter/flet_charts/lib/src/radar_chart.dart +0 -0
  44. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flutter/flet_charts/lib/src/scatter_chart.dart +0 -0
  45. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flutter/flet_charts/lib/src/utils/bar_chart.dart +0 -0
  46. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flutter/flet_charts/lib/src/utils/candlestick_chart.dart +0 -0
  47. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flutter/flet_charts/lib/src/utils/charts.dart +0 -0
  48. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flutter/flet_charts/lib/src/utils/pie_chart.dart +0 -0
  49. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flutter/flet_charts/lib/src/utils/radar_chart.dart +0 -0
  50. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/src/flutter/flet_charts/lib/src/utils/scatter_chart.dart +0 -0
  51. {flet_charts-0.85.0.dev2 → flet_charts-0.85.0.dev4}/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.dev2
3
+ Version: 0.85.0.dev4
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.dev2
14
+ Requires-Dist: flet==0.85.0.dev4
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.dev2"
3
+ version = "0.85.0.dev4"
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.dev2",
10
+ "flet==0.85.0.dev4",
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
 
@@ -5,7 +5,10 @@ from io import BytesIO
5
5
  from typing import Any, Optional
6
6
 
7
7
  import flet as ft
8
- import flet.canvas as fc
8
+ from flet_charts.matplotlib_chart_canvas import (
9
+ MatplotlibChartCanvas,
10
+ MatplotlibChartCanvasResizeEvent,
11
+ )
9
12
 
10
13
  _MATPLOTLIB_IMPORT_ERROR: Optional[ImportError] = None
11
14
 
@@ -124,13 +127,21 @@ class MatplotlibChart(ft.GestureDetector):
124
127
  logger.debug(f"DPR: {self.__dpr}")
125
128
  self.__image_mode = "full"
126
129
 
127
- self.canvas = fc.Canvas(
128
- # resize_interval=10,
130
+ self.mpl_canvas = MatplotlibChartCanvas(
129
131
  on_resize=self._on_canvas_resize,
130
132
  expand=True,
131
133
  )
134
+ # Rubberband (zoom selection) overlay drawn on top of the chart image.
135
+ self._rubberband = ft.Container(
136
+ visible=False,
137
+ border=ft.Border.all(1, ft.Colors.with_opacity(0.6, ft.Colors.GREY)),
138
+ )
139
+ self._stack = ft.Stack(
140
+ controls=[self.mpl_canvas, self._rubberband],
141
+ expand=True,
142
+ )
132
143
  self.keyboard_listener = ft.KeyboardListener(
133
- self.canvas,
144
+ self._stack,
134
145
  autofocus=True,
135
146
  on_key_down=self._on_key_down,
136
147
  on_key_up=self._on_key_up,
@@ -419,23 +430,18 @@ class MatplotlibChart(ft.GestureDetector):
419
430
 
420
431
  while True:
421
432
  is_binary, content = await self._receive_queue.get()
433
+
422
434
  if is_binary:
435
+ assert isinstance(content, (bytes, bytearray))
423
436
  logger.debug(f"receive_binary({len(content)})")
437
+ # Hand the frame to the client widget — full PNG replaces the
438
+ # backbuffer, diff PNG composites onto it. Awaiting naturally
439
+ # rate-limits this loop to the client's processing speed and
440
+ # yields the asyncio loop for incoming events.
424
441
  if self.__image_mode == "full":
425
- await self.canvas.clear_capture()
426
-
427
- self.canvas.shapes = [
428
- fc.Image(
429
- src=content,
430
- x=0,
431
- y=0,
432
- width=self.figure.bbox.size[0] / self.__dpr,
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()
442
+ await self.mpl_canvas.apply_full(bytes(content))
443
+ else:
444
+ await self.mpl_canvas.apply_diff(bytes(content))
439
445
  self.img_count += 1
440
446
  self._waiting = False
441
447
  else:
@@ -449,8 +455,6 @@ class MatplotlibChart(ft.GestureDetector):
449
455
  self._waiting = True
450
456
  self.send_message({"type": "draw"})
451
457
  elif content["type"] == "rubberband":
452
- if len(self.canvas.shapes) == 2:
453
- self.canvas.shapes.pop()
454
458
  if (
455
459
  content["x0"] != -1
456
460
  and content["y0"] != -1
@@ -461,18 +465,14 @@ class MatplotlibChart(ft.GestureDetector):
461
465
  y0 = self._height - content["y0"] / self.__dpr
462
466
  x1 = content["x1"] / self.__dpr
463
467
  y1 = self._height - content["y1"] / self.__dpr
464
- self.canvas.shapes.append(
465
- fc.Rect(
466
- x=x0,
467
- y=y0,
468
- width=x1 - x0,
469
- height=y1 - y0,
470
- paint=ft.Paint(
471
- stroke_width=1, style=ft.PaintingStyle.STROKE
472
- ),
473
- )
474
- )
475
- self.canvas.update()
468
+ self._rubberband.left = min(x0, x1)
469
+ self._rubberband.top = min(y0, y1)
470
+ self._rubberband.width = abs(x1 - x0)
471
+ self._rubberband.height = abs(y1 - y0)
472
+ self._rubberband.visible = True
473
+ else:
474
+ self._rubberband.visible = False
475
+ self._rubberband.update()
476
476
  elif content["type"] == "resize":
477
477
  self.send_message({"type": "refresh"})
478
478
  elif content["type"] == "message":
@@ -508,7 +508,7 @@ class MatplotlibChart(ft.GestureDetector):
508
508
  lambda: self._receive_queue.put_nowait((True, blob))
509
509
  )
510
510
 
511
- async def _on_canvas_resize(self, e: fc.CanvasResizeEvent):
511
+ async def _on_canvas_resize(self, e: MatplotlibChartCanvasResizeEvent):
512
512
  """
513
513
  Handle canvas resize and initialize backend session on first resize.
514
514
 
@@ -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.dev2
3
+ Version: 0.85.0.dev4
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.dev2
14
+ Requires-Dist: flet==0.85.0.dev4
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.dev4
@@ -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,457 @@
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/foundation.dart' show kIsWeb;
7
+ import 'package:flutter/material.dart';
8
+
9
+ /// Display widget for matplotlib WebAgg-style image streams.
10
+ ///
11
+ /// Two rendering strategies, picked at runtime by platform:
12
+ ///
13
+ /// - **GPU + flatten** (native): keeps an `_backdrop` plus a list of pending
14
+ /// diff `ui.Image`s, paints all of them per frame, and bakes them into a
15
+ /// fresh backdrop via `Picture.toImage` every [_GpuMatplotlibChartCanvasState._flattenInterval]
16
+ /// diffs. Fast (no GPU↔CPU readback) and memory-stable on native runtimes
17
+ /// where Dart GC is aggressive enough to reclaim layer-held SkImage refs.
18
+ ///
19
+ /// - **CPU composite** (web): decodes each PNG to RGBA bytes, composites
20
+ /// onto a single backbuffer in Dart, and uploads ONE fresh `ui.Image` per
21
+ /// frame. Slower per frame, but holds at most one `ui.Image` at a time so
22
+ /// layer-ref accumulation stays bounded under Flutter web (CanvasKit/WASM)
23
+ /// where Dart GC doesn't promptly reclaim native SkImage refs.
24
+ class MatplotlibChartCanvasControl extends StatefulWidget {
25
+ final Control control;
26
+
27
+ MatplotlibChartCanvasControl({Key? key, required this.control})
28
+ : super(key: key ?? ValueKey("control_${control.id}"));
29
+
30
+ @override
31
+ // ignore: no_logic_in_create_state
32
+ State<MatplotlibChartCanvasControl> createState() => kIsWeb
33
+ ? _CpuMatplotlibChartCanvasState()
34
+ : _GpuMatplotlibChartCanvasState();
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Shared helpers
39
+ // ---------------------------------------------------------------------------
40
+
41
+ Uint8List _extractBytes(dynamic args) {
42
+ final v = args is Map ? args["bytes"] : args;
43
+ if (v is Uint8List) return v;
44
+ if (v is ByteData) {
45
+ return v.buffer.asUint8List(v.offsetInBytes, v.lengthInBytes);
46
+ }
47
+ if (v is List<int>) return Uint8List.fromList(v);
48
+ if (v is List && v.every((e) => e is int)) {
49
+ return Uint8List.fromList(v.cast<int>());
50
+ }
51
+ throw ArgumentError("Expected bytes for image data, got ${v.runtimeType}");
52
+ }
53
+
54
+ abstract class _MatplotlibChartCanvasStateBase
55
+ extends State<MatplotlibChartCanvasControl> {
56
+ // Serialize concurrent apply_full / apply_diff calls so backdrop mutations
57
+ // happen in arrival order.
58
+ Future<void>? _applyChain;
59
+
60
+ Size _lastSize = Size.zero;
61
+ int _lastResize = DateTime.now().millisecondsSinceEpoch;
62
+
63
+ @override
64
+ void initState() {
65
+ super.initState();
66
+ widget.control.addInvokeMethodListener(_invokeMethod);
67
+ }
68
+
69
+ @override
70
+ void dispose() {
71
+ widget.control.removeInvokeMethodListener(_invokeMethod);
72
+ disposeResources();
73
+ super.dispose();
74
+ }
75
+
76
+ /// Subclass hook — release any held `ui.Image`s / backbuffers.
77
+ void disposeResources();
78
+
79
+ Future<void> applyFull(Uint8List bytes);
80
+ Future<void> applyDiff(Uint8List bytes);
81
+ Future<void> clearAll();
82
+ CustomPainter buildPainter();
83
+
84
+ Future<dynamic> _invokeMethod(String name, dynamic args) async {
85
+ switch (name) {
86
+ case "apply_full":
87
+ await _enqueue(() => applyFull(_extractBytes(args)));
88
+ return;
89
+ case "apply_diff":
90
+ await _enqueue(() => applyDiff(_extractBytes(args)));
91
+ return;
92
+ case "clear":
93
+ await _enqueue(clearAll);
94
+ return;
95
+ default:
96
+ throw Exception("Unknown MatplotlibChartCanvas method: $name");
97
+ }
98
+ }
99
+
100
+ Future<void> _enqueue(Future<void> Function() task) {
101
+ final prev = _applyChain ?? Future.value();
102
+ final next = prev.then((_) => task());
103
+ _applyChain = next.catchError((_) {});
104
+ return next;
105
+ }
106
+
107
+ void _maybeReportResize(Size size) {
108
+ final resizeInterval = widget.control.getInt("resize_interval", 10)!;
109
+ final now = DateTime.now().millisecondsSinceEpoch;
110
+ if ((now - _lastResize > resizeInterval && _lastSize != size) ||
111
+ _lastSize.isEmpty) {
112
+ _lastSize = size;
113
+ _lastResize = now;
114
+ widget.control
115
+ .triggerEvent("resize", {"w": size.width, "h": size.height});
116
+ }
117
+ }
118
+
119
+ @override
120
+ Widget build(BuildContext context) {
121
+ return LayoutBuilder(
122
+ builder: (context, constraints) {
123
+ WidgetsBinding.instance.addPostFrameCallback((_) {
124
+ if (!mounted) return;
125
+ _maybeReportResize(constraints.biggest);
126
+ });
127
+ return CustomPaint(
128
+ size: constraints.biggest,
129
+ painter: buildPainter(),
130
+ );
131
+ },
132
+ );
133
+ }
134
+ }
135
+
136
+ /// Decodes PNG bytes to a [ui.Image], staying GPU-resident.
137
+ Future<ui.Image?> _decodeImage(Uint8List bytes) async {
138
+ if (bytes.isEmpty) {
139
+ debugPrint("MatplotlibChartCanvas: skipping empty image bytes");
140
+ return null;
141
+ }
142
+ // Defensive copy; Safari's WASM runtime can free underlying buffers across
143
+ // async boundaries and trigger "EncodingError: Loading error.".
144
+ final owned = Uint8List.fromList(bytes);
145
+ ui.Codec? codec;
146
+ try {
147
+ codec = await ui.instantiateImageCodec(owned, allowUpscaling: false);
148
+ final frame = await codec.getNextFrame();
149
+ return frame.image;
150
+ } catch (e) {
151
+ debugPrint(
152
+ "MatplotlibChartCanvas: decode failed (${owned.length} bytes): $e");
153
+ rethrow;
154
+ } finally {
155
+ codec?.dispose();
156
+ }
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // GPU + flatten strategy (native runtimes)
161
+ // ---------------------------------------------------------------------------
162
+
163
+ class _GpuMatplotlibChartCanvasState
164
+ extends _MatplotlibChartCanvasStateBase {
165
+ // Number of diffs to accumulate before flattening into a fresh backdrop.
166
+ // Larger = fewer Picture.toImage calls; smaller = lower transient memory.
167
+ static const int _flattenInterval = 10;
168
+
169
+ ui.Image? _backdrop;
170
+ final List<ui.Image> _diffs = [];
171
+
172
+ @override
173
+ void disposeResources() {
174
+ _backdrop?.dispose();
175
+ _backdrop = null;
176
+ for (final img in _diffs) {
177
+ img.dispose();
178
+ }
179
+ _diffs.clear();
180
+ }
181
+
182
+ @override
183
+ Future<void> applyFull(Uint8List bytes) async {
184
+ final image = await _decodeImage(bytes);
185
+ if (image == null) return;
186
+ _replaceBackdrop(image);
187
+ _disposeDiffs();
188
+ if (mounted) setState(() {});
189
+ }
190
+
191
+ @override
192
+ Future<void> applyDiff(Uint8List bytes) async {
193
+ if (_backdrop == null) {
194
+ // No baseline yet — first frame must be a full.
195
+ await applyFull(bytes);
196
+ return;
197
+ }
198
+ final image = await _decodeImage(bytes);
199
+ if (image == null) return;
200
+ _diffs.add(image);
201
+ if (_diffs.length >= _flattenInterval) {
202
+ await _flatten();
203
+ }
204
+ if (mounted) setState(() {});
205
+ }
206
+
207
+ @override
208
+ Future<void> clearAll() async {
209
+ _replaceBackdrop(null);
210
+ _disposeDiffs();
211
+ if (mounted) setState(() {});
212
+ }
213
+
214
+ /// Bakes [_backdrop] + pending [_diffs] into a single new backdrop via
215
+ /// `Picture.toImage`, replaces [_backdrop], and drops the diffs.
216
+ Future<void> _flatten() async {
217
+ final backdrop = _backdrop;
218
+ if (backdrop == null || _diffs.isEmpty) return;
219
+
220
+ final w = backdrop.width;
221
+ final h = backdrop.height;
222
+
223
+ final recorder = ui.PictureRecorder();
224
+ final canvas = Canvas(recorder);
225
+ final paint = Paint();
226
+ canvas.drawImage(backdrop, Offset.zero, paint);
227
+ for (final diff in _diffs) {
228
+ canvas.drawImage(diff, Offset.zero, paint);
229
+ }
230
+
231
+ final picture = recorder.endRecording();
232
+ final ui.Image newBackdrop;
233
+ try {
234
+ newBackdrop = await picture.toImage(w, h);
235
+ } finally {
236
+ picture.dispose();
237
+ }
238
+
239
+ _replaceBackdrop(newBackdrop);
240
+ _disposeDiffs();
241
+ }
242
+
243
+ void _replaceBackdrop(ui.Image? image) {
244
+ final old = _backdrop;
245
+ _backdrop = image;
246
+ if (old != null) {
247
+ WidgetsBinding.instance.addPostFrameCallback((_) {
248
+ old.dispose();
249
+ });
250
+ }
251
+ }
252
+
253
+ void _disposeDiffs() {
254
+ if (_diffs.isEmpty) return;
255
+ final old = List<ui.Image>.of(_diffs);
256
+ _diffs.clear();
257
+ WidgetsBinding.instance.addPostFrameCallback((_) {
258
+ for (final img in old) {
259
+ img.dispose();
260
+ }
261
+ });
262
+ }
263
+
264
+ @override
265
+ CustomPainter buildPainter() => _GpuMatplotlibImagePainter(
266
+ backdrop: _backdrop,
267
+ diffs: List<ui.Image>.unmodifiable(_diffs),
268
+ );
269
+ }
270
+
271
+ class _GpuMatplotlibImagePainter extends CustomPainter {
272
+ final ui.Image? backdrop;
273
+ final List<ui.Image> diffs;
274
+
275
+ const _GpuMatplotlibImagePainter({
276
+ required this.backdrop,
277
+ required this.diffs,
278
+ });
279
+
280
+ @override
281
+ void paint(Canvas canvas, Size size) {
282
+ final bg = backdrop;
283
+ if (bg == null) return;
284
+ final dst = Rect.fromLTWH(0, 0, size.width, size.height);
285
+ final paint = Paint();
286
+ final bgSrc =
287
+ Rect.fromLTWH(0, 0, bg.width.toDouble(), bg.height.toDouble());
288
+ canvas.drawImageRect(bg, bgSrc, dst, paint);
289
+ for (final diff in diffs) {
290
+ final src =
291
+ Rect.fromLTWH(0, 0, diff.width.toDouble(), diff.height.toDouble());
292
+ canvas.drawImageRect(diff, src, dst, paint);
293
+ }
294
+ }
295
+
296
+ @override
297
+ bool shouldRepaint(_GpuMatplotlibImagePainter old) {
298
+ return backdrop != old.backdrop || diffs.length != old.diffs.length;
299
+ }
300
+ }
301
+
302
+ // ---------------------------------------------------------------------------
303
+ // CPU composite strategy (web — CanvasKit/WASM)
304
+ // ---------------------------------------------------------------------------
305
+
306
+ class _CpuMatplotlibChartCanvasState
307
+ extends _MatplotlibChartCanvasStateBase {
308
+ ui.Image? _displayImage;
309
+ Uint8List? _backbuffer;
310
+ int _bbWidth = 0;
311
+ int _bbHeight = 0;
312
+
313
+ @override
314
+ void disposeResources() {
315
+ _displayImage?.dispose();
316
+ _displayImage = null;
317
+ _backbuffer = null;
318
+ }
319
+
320
+ @override
321
+ Future<void> applyFull(Uint8List bytes) async {
322
+ final decoded = await _decodeRgba(bytes);
323
+ if (decoded == null) return;
324
+
325
+ _backbuffer = decoded.bytes;
326
+ _bbWidth = decoded.width;
327
+ _bbHeight = decoded.height;
328
+
329
+ final image = await _makeImage(decoded.bytes, decoded.width, decoded.height);
330
+ _swapDisplay(image);
331
+ }
332
+
333
+ @override
334
+ Future<void> applyDiff(Uint8List bytes) async {
335
+ if (_backbuffer == null) {
336
+ // No baseline yet — treat as full.
337
+ await applyFull(bytes);
338
+ return;
339
+ }
340
+
341
+ final decoded = await _decodeRgba(bytes);
342
+ if (decoded == null) return;
343
+
344
+ // Promote to a full replace if the frame size changed.
345
+ if (decoded.width != _bbWidth ||
346
+ decoded.height != _bbHeight ||
347
+ decoded.bytes.length != _backbuffer!.length) {
348
+ _backbuffer = decoded.bytes;
349
+ _bbWidth = decoded.width;
350
+ _bbHeight = decoded.height;
351
+ final image =
352
+ await _makeImage(decoded.bytes, decoded.width, decoded.height);
353
+ _swapDisplay(image);
354
+ return;
355
+ }
356
+
357
+ // Composite: matplotlib's diff PNG has alpha=0 for unchanged pixels.
358
+ // Where alpha != 0, copy the new pixel into the backbuffer.
359
+ final bb = _backbuffer!.buffer.asUint32List();
360
+ final df = decoded.bytes.buffer.asUint32List();
361
+ for (int i = 0; i < df.length; i++) {
362
+ // RGBA8888 on little-endian: alpha is the highest byte (0xFF000000).
363
+ if ((df[i] & 0xFF000000) != 0) {
364
+ bb[i] = df[i];
365
+ }
366
+ }
367
+
368
+ final image = await _makeImage(_backbuffer!, _bbWidth, _bbHeight);
369
+ _swapDisplay(image);
370
+ }
371
+
372
+ @override
373
+ Future<void> clearAll() async {
374
+ final old = _displayImage;
375
+ _displayImage = null;
376
+ _backbuffer = null;
377
+ _bbWidth = 0;
378
+ _bbHeight = 0;
379
+ if (old != null) {
380
+ WidgetsBinding.instance.addPostFrameCallback((_) {
381
+ old.dispose();
382
+ });
383
+ }
384
+ if (mounted) setState(() {});
385
+ }
386
+
387
+ Future<_DecodedRgba?> _decodeRgba(Uint8List bytes) async {
388
+ final img = await _decodeImage(bytes);
389
+ if (img == null) return null;
390
+ try {
391
+ final byteData =
392
+ await img.toByteData(format: ui.ImageByteFormat.rawRgba);
393
+ if (byteData == null) return null;
394
+ return _DecodedRgba(
395
+ bytes: byteData.buffer.asUint8List(),
396
+ width: img.width,
397
+ height: img.height,
398
+ );
399
+ } finally {
400
+ img.dispose();
401
+ }
402
+ }
403
+
404
+ Future<ui.Image> _makeImage(Uint8List rgba, int width, int height) {
405
+ final completer = Completer<ui.Image>();
406
+ ui.decodeImageFromPixels(
407
+ rgba,
408
+ width,
409
+ height,
410
+ ui.PixelFormat.rgba8888,
411
+ completer.complete,
412
+ );
413
+ return completer.future;
414
+ }
415
+
416
+ void _swapDisplay(ui.Image newImage) {
417
+ final old = _displayImage;
418
+ _displayImage = newImage;
419
+ if (mounted) setState(() {});
420
+ if (old != null) {
421
+ WidgetsBinding.instance.addPostFrameCallback((_) {
422
+ old.dispose();
423
+ });
424
+ }
425
+ }
426
+
427
+ @override
428
+ CustomPainter buildPainter() =>
429
+ _CpuMatplotlibImagePainter(image: _displayImage);
430
+ }
431
+
432
+ class _DecodedRgba {
433
+ final Uint8List bytes;
434
+ final int width;
435
+ final int height;
436
+ _DecodedRgba(
437
+ {required this.bytes, required this.width, required this.height});
438
+ }
439
+
440
+ class _CpuMatplotlibImagePainter extends CustomPainter {
441
+ final ui.Image? image;
442
+
443
+ const _CpuMatplotlibImagePainter({required this.image});
444
+
445
+ @override
446
+ void paint(Canvas canvas, Size size) {
447
+ final img = image;
448
+ if (img == null) return;
449
+ final src =
450
+ Rect.fromLTWH(0, 0, img.width.toDouble(), img.height.toDouble());
451
+ final dst = Rect.fromLTWH(0, 0, size.width, size.height);
452
+ canvas.drawImageRect(img, src, dst, Paint());
453
+ }
454
+
455
+ @override
456
+ bool shouldRepaint(_CpuMatplotlibImagePainter old) => old.image != image;
457
+ }
@@ -25,7 +25,7 @@ class LineChartEventData extends Equatable {
25
25
 
26
26
  Map<String, dynamic> toMap() => <String, dynamic>{
27
27
  'type': eventType,
28
- 'spots': barSpots,
28
+ 'spots': barSpots.map((spot) => spot.toMap()).toList(),
29
29
  };
30
30
 
31
31
  @override
@@ -1 +0,0 @@
1
- flet==0.85.0.dev2