grimoireplot 0.0.2__tar.gz → 0.0.3__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.
- {grimoireplot-0.0.2 → grimoireplot-0.0.3}/PKG-INFO +2 -2
- grimoireplot-0.0.3/grimoireplot/plot_live_update.py +212 -0
- {grimoireplot-0.0.2 → grimoireplot-0.0.3}/grimoireplot/server.py +13 -3
- {grimoireplot-0.0.2 → grimoireplot-0.0.3}/grimoireplot/ui.py +52 -1
- {grimoireplot-0.0.2 → grimoireplot-0.0.3}/grimoireplot/ui_elements.py +24 -17
- {grimoireplot-0.0.2 → grimoireplot-0.0.3}/pyproject.toml +2 -2
- {grimoireplot-0.0.2 → grimoireplot-0.0.3}/README.md +0 -0
- {grimoireplot-0.0.2 → grimoireplot-0.0.3}/grimoireplot/__init__.py +0 -0
- {grimoireplot-0.0.2 → grimoireplot-0.0.3}/grimoireplot/client.py +0 -0
- {grimoireplot-0.0.2 → grimoireplot-0.0.3}/grimoireplot/common.py +0 -0
- {grimoireplot-0.0.2 → grimoireplot-0.0.3}/grimoireplot/create_some_plots.py +0 -0
- {grimoireplot-0.0.2 → grimoireplot-0.0.3}/grimoireplot/main.py +0 -0
- {grimoireplot-0.0.2 → grimoireplot-0.0.3}/grimoireplot/models.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: grimoireplot
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: GrimoirePlot is a live dashboard of plotly-compatible plots of remote data
|
|
5
5
|
Author: William Droz
|
|
6
6
|
Author-email: William Droz <william.droz@idiap.ch>
|
|
7
7
|
Classifier: License :: OSI Approved :: MIT License
|
|
8
8
|
Requires-Dist: aiohttp[speedups]>=3.13.3
|
|
9
9
|
Requires-Dist: loguru>=0.7.3
|
|
10
|
-
Requires-Dist: nicegui>=3.
|
|
10
|
+
Requires-Dist: nicegui>=3.14.0,<4
|
|
11
11
|
Requires-Dist: plotly>=6.5.2
|
|
12
12
|
Requires-Dist: python-dotenv>=1.2.1
|
|
13
13
|
Requires-Dist: requests>=2.32.5
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright © 2026 Idiap Research Institute <contact@idiap.ch>
|
|
2
|
+
# SPDX-FileContributor: William Droz <william.droz@idiap.ch>
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""Live Plotly updates using NiceGUI's built-in plotly APIs.
|
|
6
|
+
|
|
7
|
+
Follows the patterns from NiceGUI's plotly documentation:
|
|
8
|
+
https://nicegui.io/documentation/plotly#plot_updates
|
|
9
|
+
|
|
10
|
+
- Dict figures with mutable trace lists (not go.Figure) for extendTraces
|
|
11
|
+
- ``plot.run_plot_method('extendTraces', ...)`` for append-only live data
|
|
12
|
+
- ``plot.run_plot_method('relayout', ...)`` for layout-only changes
|
|
13
|
+
- Mutate ``plot.figure`` in place + ``plot.update()`` for structural changes
|
|
14
|
+
(NiceGUI calls Plotly.react on the client when config is unchanged)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from nicegui import ui
|
|
22
|
+
|
|
23
|
+
from grimoireplot.ui_elements import style_plotly_figure
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def plot_uirevision(grimoire_name: str, chapter_name: str, plot_name: str) -> str:
|
|
27
|
+
"""Stable uirevision key so Plotly preserves axis ranges across updates."""
|
|
28
|
+
return f"{grimoire_name}/{chapter_name}/{plot_name}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def normalize_figure_dict(fig_data: dict) -> dict:
|
|
32
|
+
"""Return a dict figure suitable for NiceGUI live updates.
|
|
33
|
+
|
|
34
|
+
NiceGUI recommends the declarative dict interface so trace ``x``/``y`` stay
|
|
35
|
+
mutable lists that can be appended and synced with extendTraces.
|
|
36
|
+
"""
|
|
37
|
+
normalized: dict[str, Any] = {
|
|
38
|
+
"data": [],
|
|
39
|
+
"layout": dict(fig_data.get("layout", {})),
|
|
40
|
+
}
|
|
41
|
+
if "config" in fig_data:
|
|
42
|
+
normalized["config"] = dict(fig_data["config"])
|
|
43
|
+
|
|
44
|
+
for trace in fig_data.get("data", []):
|
|
45
|
+
trace_copy = dict(trace)
|
|
46
|
+
for axis in ("x", "y"):
|
|
47
|
+
values = trace.get(axis)
|
|
48
|
+
if isinstance(values, list):
|
|
49
|
+
trace_copy[axis] = list(values)
|
|
50
|
+
normalized["data"].append(trace_copy)
|
|
51
|
+
|
|
52
|
+
return normalized
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def prepare_figure(fig_data: dict, uirevision: str) -> dict:
|
|
56
|
+
"""Normalize, theme, and set uirevision on incoming figure data."""
|
|
57
|
+
styled = style_plotly_figure(normalize_figure_dict(fig_data))
|
|
58
|
+
styled["layout"]["uirevision"] = uirevision
|
|
59
|
+
return styled
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _title_text(layout: dict) -> str | None:
|
|
63
|
+
title = layout.get("title")
|
|
64
|
+
if isinstance(title, dict):
|
|
65
|
+
return title.get("text")
|
|
66
|
+
if isinstance(title, str):
|
|
67
|
+
return title
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _axis_title_text(layout: dict, axis: str) -> str | None:
|
|
72
|
+
axis_layout = layout.get(axis, {})
|
|
73
|
+
if not isinstance(axis_layout, dict):
|
|
74
|
+
return None
|
|
75
|
+
title = axis_layout.get("title")
|
|
76
|
+
if isinstance(title, dict):
|
|
77
|
+
return title.get("text")
|
|
78
|
+
if isinstance(title, str):
|
|
79
|
+
return title
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def layout_relayout_diff(old_layout: dict, new_layout: dict) -> dict[str, Any]:
|
|
84
|
+
"""Return plotly.js relayout keys for layout fields that changed."""
|
|
85
|
+
diff: dict[str, Any] = {}
|
|
86
|
+
|
|
87
|
+
new_title = _title_text(new_layout)
|
|
88
|
+
if _title_text(old_layout) != new_title:
|
|
89
|
+
diff["title.text"] = new_title
|
|
90
|
+
|
|
91
|
+
for axis in ("xaxis", "yaxis"):
|
|
92
|
+
new_axis_title = _axis_title_text(new_layout, axis)
|
|
93
|
+
if _axis_title_text(old_layout, axis) != new_axis_title:
|
|
94
|
+
diff[f"{axis}.title.text"] = new_axis_title
|
|
95
|
+
|
|
96
|
+
return diff
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _trace_type(trace: dict) -> str:
|
|
100
|
+
return trace.get("type", "scatter")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def trace_data_appended(
|
|
104
|
+
old_trace: dict, new_trace: dict
|
|
105
|
+
) -> tuple[list[Any], list[Any]] | None:
|
|
106
|
+
"""Return appended (x, y) if new_trace extends old_trace, else None."""
|
|
107
|
+
if _trace_type(old_trace) != _trace_type(new_trace):
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
old_x, old_y = old_trace.get("x", []), old_trace.get("y", [])
|
|
111
|
+
new_x, new_y = new_trace.get("x", []), new_trace.get("y", [])
|
|
112
|
+
|
|
113
|
+
if not isinstance(old_x, list) or not isinstance(new_x, list):
|
|
114
|
+
return None
|
|
115
|
+
if not isinstance(old_y, list) or not isinstance(new_y, list):
|
|
116
|
+
return None
|
|
117
|
+
if len(new_x) < len(old_x) or len(new_y) < len(old_y):
|
|
118
|
+
return None
|
|
119
|
+
if new_x[: len(old_x)] != old_x or new_y[: len(old_y)] != old_y:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
return (new_x[len(old_x) :], new_y[len(old_y) :])
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _figure_dict(chart: ui.plotly) -> dict | None:
|
|
126
|
+
figure = chart.figure
|
|
127
|
+
return figure if isinstance(figure, dict) else None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def try_extend_traces_update(
|
|
131
|
+
chart: ui.plotly, new_fig_data: dict, uirevision: str
|
|
132
|
+
) -> bool:
|
|
133
|
+
"""Use NiceGUI's run_plot_method('extendTraces') when traces only grow."""
|
|
134
|
+
current = _figure_dict(chart)
|
|
135
|
+
if current is None:
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
styled = prepare_figure(new_fig_data, uirevision)
|
|
139
|
+
|
|
140
|
+
old_data = current.get("data", [])
|
|
141
|
+
new_data = styled.get("data", [])
|
|
142
|
+
if len(old_data) != len(new_data) or not old_data:
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
extend_x: list[list[Any]] = []
|
|
146
|
+
extend_y: list[list[Any]] = []
|
|
147
|
+
trace_indices: list[int] = []
|
|
148
|
+
|
|
149
|
+
for index, (old_trace, new_trace) in enumerate(zip(old_data, new_data)):
|
|
150
|
+
appended = trace_data_appended(old_trace, new_trace)
|
|
151
|
+
if appended is None:
|
|
152
|
+
return False
|
|
153
|
+
added_x, added_y = appended
|
|
154
|
+
if added_x:
|
|
155
|
+
extend_x.append(added_x)
|
|
156
|
+
extend_y.append(added_y)
|
|
157
|
+
trace_indices.append(index)
|
|
158
|
+
|
|
159
|
+
relayout_diff = layout_relayout_diff(
|
|
160
|
+
current.get("layout", {}), styled.get("layout", {})
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if not trace_indices and not relayout_diff:
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
if trace_indices:
|
|
167
|
+
chart.run_plot_method(
|
|
168
|
+
"extendTraces", {"x": extend_x, "y": extend_y}, trace_indices
|
|
169
|
+
)
|
|
170
|
+
for trace_index, added_x, added_y in zip(trace_indices, extend_x, extend_y):
|
|
171
|
+
current["data"][trace_index]["x"].extend(added_x)
|
|
172
|
+
current["data"][trace_index]["y"].extend(added_y)
|
|
173
|
+
|
|
174
|
+
if relayout_diff:
|
|
175
|
+
chart.run_plot_method("relayout", relayout_diff)
|
|
176
|
+
|
|
177
|
+
current.setdefault("layout", {}).update(styled["layout"])
|
|
178
|
+
current["layout"]["uirevision"] = uirevision
|
|
179
|
+
if "config" in styled:
|
|
180
|
+
current["config"] = styled["config"]
|
|
181
|
+
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def mutate_figure_and_update(
|
|
186
|
+
chart: ui.plotly, new_fig_data: dict, uirevision: str
|
|
187
|
+
) -> None:
|
|
188
|
+
"""Mutate plot.figure in place and call plot.update() (Plotly.react on client)."""
|
|
189
|
+
styled = prepare_figure(new_fig_data, uirevision)
|
|
190
|
+
|
|
191
|
+
figure = _figure_dict(chart)
|
|
192
|
+
if figure is None:
|
|
193
|
+
chart.figure = styled
|
|
194
|
+
chart.update()
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
figure["data"] = styled["data"]
|
|
198
|
+
figure["layout"] = styled["layout"]
|
|
199
|
+
if "config" in styled:
|
|
200
|
+
figure["config"] = styled["config"]
|
|
201
|
+
chart.update()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def update_plotly_chart(chart: ui.plotly, new_fig_data: dict, uirevision: str) -> None:
|
|
205
|
+
"""Pick the lightest NiceGUI/Plotly update path for the incoming figure."""
|
|
206
|
+
if not try_extend_traces_update(chart, new_fig_data, uirevision):
|
|
207
|
+
mutate_figure_and_update(chart, new_fig_data, uirevision)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# Backwards-compatible aliases used by ui.py
|
|
211
|
+
try_incremental_update = try_extend_traces_update
|
|
212
|
+
apply_full_chart_update = mutate_figure_and_update
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# SPDX-FileContributor: William Droz <william.droz@idiap.ch>
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import time
|
|
6
7
|
import logging
|
|
7
8
|
from functools import wraps
|
|
@@ -18,7 +19,7 @@ from grimoireplot.models import (
|
|
|
18
19
|
delete_grimoire,
|
|
19
20
|
)
|
|
20
21
|
|
|
21
|
-
from grimoireplot.ui import dashboard_ui, refresh_chapter_plots
|
|
22
|
+
from grimoireplot.ui import dashboard_ui, refresh_chapter_plots, update_plot_chart
|
|
22
23
|
from grimoireplot.ui_elements import setup_theme
|
|
23
24
|
|
|
24
25
|
_GRIMOIRE_SECRET = get_grimoire_secret()
|
|
@@ -82,8 +83,17 @@ def my_app(host: str = "localhost", port: int = 8080):
|
|
|
82
83
|
plot_name=add_plot_request.plot_name,
|
|
83
84
|
json_data=add_plot_request.json_data,
|
|
84
85
|
)
|
|
85
|
-
#
|
|
86
|
-
|
|
86
|
+
# Update existing charts in place for live plots; fall back to UI refresh for new plots
|
|
87
|
+
fig_data = json.loads(add_plot_request.json_data)
|
|
88
|
+
if update_plot_chart(
|
|
89
|
+
add_plot_request.grimoire_name,
|
|
90
|
+
add_plot_request.chapter_name,
|
|
91
|
+
add_plot_request.plot_name,
|
|
92
|
+
fig_data,
|
|
93
|
+
):
|
|
94
|
+
return {"status": "success", "plot_name": plot.name}
|
|
95
|
+
|
|
96
|
+
# New plot or chapter not yet rendered: refresh chapter or whole dashboard
|
|
87
97
|
if not refresh_chapter_plots(
|
|
88
98
|
add_plot_request.grimoire_name, add_plot_request.chapter_name
|
|
89
99
|
):
|
|
@@ -32,10 +32,55 @@ from grimoireplot.ui_elements import (
|
|
|
32
32
|
create_btn_ghost,
|
|
33
33
|
create_btn_danger,
|
|
34
34
|
)
|
|
35
|
+
from grimoireplot.plot_live_update import plot_uirevision, update_plotly_chart
|
|
35
36
|
|
|
36
37
|
# Store refreshable instances for each chapter's plots
|
|
37
38
|
_chapter_plot_refreshables: dict[tuple[str, str], ui.refreshable] = {}
|
|
38
39
|
|
|
40
|
+
# Live plot charts keyed by (grimoire_name, chapter_name, plot_name)
|
|
41
|
+
_plot_charts: dict[tuple[str, str, str], ui.plotly] = {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _plot_key(
|
|
45
|
+
grimoire_name: str, chapter_name: str, plot_name: str
|
|
46
|
+
) -> tuple[str, str, str]:
|
|
47
|
+
return (grimoire_name, chapter_name, plot_name)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def clear_plot_charts_for_chapter(grimoire_name: str, chapter_name: str) -> None:
|
|
51
|
+
"""Remove chart references for a chapter before it is re-rendered."""
|
|
52
|
+
keys_to_remove = [
|
|
53
|
+
key
|
|
54
|
+
for key in _plot_charts
|
|
55
|
+
if key[0] == grimoire_name and key[1] == chapter_name
|
|
56
|
+
]
|
|
57
|
+
for key in keys_to_remove:
|
|
58
|
+
_plot_charts.pop(key, None)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def clear_all_plot_charts() -> None:
|
|
62
|
+
"""Remove all live plot chart references."""
|
|
63
|
+
_plot_charts.clear()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def update_plot_chart(
|
|
67
|
+
grimoire_name: str, chapter_name: str, plot_name: str, fig_data: dict
|
|
68
|
+
) -> bool:
|
|
69
|
+
"""Update an existing plot chart using NiceGUI's built-in plotly APIs.
|
|
70
|
+
|
|
71
|
+
Returns True if the chart was found and updated, False if a full refresh is needed.
|
|
72
|
+
"""
|
|
73
|
+
chart = _plot_charts.get(_plot_key(grimoire_name, chapter_name, plot_name))
|
|
74
|
+
if chart is None:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
update_plotly_chart(
|
|
78
|
+
chart,
|
|
79
|
+
fig_data,
|
|
80
|
+
plot_uirevision(grimoire_name, chapter_name, plot_name),
|
|
81
|
+
)
|
|
82
|
+
return True
|
|
83
|
+
|
|
39
84
|
|
|
40
85
|
def refresh_chapter_plots(grimoire_name: str, chapter_name: str) -> bool:
|
|
41
86
|
"""Refresh only the plots for a specific chapter.
|
|
@@ -44,6 +89,7 @@ def refresh_chapter_plots(grimoire_name: str, chapter_name: str) -> bool:
|
|
|
44
89
|
"""
|
|
45
90
|
key = (grimoire_name, chapter_name)
|
|
46
91
|
if key in _chapter_plot_refreshables:
|
|
92
|
+
clear_plot_charts_for_chapter(grimoire_name, chapter_name)
|
|
47
93
|
_chapter_plot_refreshables[key].refresh()
|
|
48
94
|
return True
|
|
49
95
|
return False
|
|
@@ -63,6 +109,7 @@ def clear_chapter_refreshable(grimoire_name: str, chapter_name: str):
|
|
|
63
109
|
def clear_all_chapter_refreshables():
|
|
64
110
|
"""Clear all chapter refreshables (called on full dashboard refresh)."""
|
|
65
111
|
_chapter_plot_refreshables.clear()
|
|
112
|
+
clear_all_plot_charts()
|
|
66
113
|
|
|
67
114
|
|
|
68
115
|
def _confirm_delete(name: str, delete_fn, on_deleted=None):
|
|
@@ -188,8 +235,12 @@ def render_plot(plot: Plot, grimoire_name: str, chapter_name: str):
|
|
|
188
235
|
|
|
189
236
|
try:
|
|
190
237
|
fig = json.loads(plot.json_data)
|
|
238
|
+
fig.setdefault("layout", {})["uirevision"] = plot_uirevision(
|
|
239
|
+
grimoire_name, chapter_name, plot.name
|
|
240
|
+
)
|
|
191
241
|
with create_plot_container():
|
|
192
|
-
create_plotly_chart(fig)
|
|
242
|
+
chart = create_plotly_chart(fig)
|
|
243
|
+
_plot_charts[_plot_key(grimoire_name, chapter_name, plot.name)] = chart
|
|
193
244
|
create_delete_badge(
|
|
194
245
|
lambda _, g=grimoire_name, c=chapter_name, p=plot: _confirm_delete(
|
|
195
246
|
p.name, lambda name: delete_plot(g, c, name), on_deleted=on_deleted
|
|
@@ -649,18 +649,8 @@ def create_confirm_dialog(
|
|
|
649
649
|
# ============================================================================
|
|
650
650
|
|
|
651
651
|
|
|
652
|
-
def
|
|
653
|
-
"""
|
|
654
|
-
|
|
655
|
-
Applies dark theme styling to the chart.
|
|
656
|
-
|
|
657
|
-
Args:
|
|
658
|
-
fig_data: Plotly figure data dict.
|
|
659
|
-
|
|
660
|
-
Returns:
|
|
661
|
-
Styled plotly component.
|
|
662
|
-
"""
|
|
663
|
-
# Ensure dark theme layout
|
|
652
|
+
def style_plotly_figure(fig_data: dict) -> dict:
|
|
653
|
+
"""Apply GrimoirePlot dark theme styling to a Plotly figure dict."""
|
|
664
654
|
if "layout" not in fig_data:
|
|
665
655
|
fig_data["layout"] = {}
|
|
666
656
|
|
|
@@ -671,10 +661,9 @@ def create_plotly_chart(fig_data: dict) -> ui.plotly:
|
|
|
671
661
|
layout.setdefault("height", 450)
|
|
672
662
|
layout.setdefault("autosize", True)
|
|
673
663
|
|
|
674
|
-
# Disable Plotly animations
|
|
664
|
+
# Disable Plotly animations for smoother live updates
|
|
675
665
|
layout.setdefault("transition", {"duration": 0})
|
|
676
666
|
|
|
677
|
-
# Style axes
|
|
678
667
|
for axis in ["xaxis", "yaxis"]:
|
|
679
668
|
if axis not in layout:
|
|
680
669
|
layout[axis] = {}
|
|
@@ -682,7 +671,6 @@ def create_plotly_chart(fig_data: dict) -> ui.plotly:
|
|
|
682
671
|
layout[axis].setdefault("linecolor", "rgba(139, 92, 246, 0.3)")
|
|
683
672
|
layout[axis].setdefault("tickcolor", "#64748B")
|
|
684
673
|
|
|
685
|
-
# Style legend
|
|
686
674
|
layout.setdefault(
|
|
687
675
|
"legend",
|
|
688
676
|
{
|
|
@@ -692,12 +680,31 @@ def create_plotly_chart(fig_data: dict) -> ui.plotly:
|
|
|
692
680
|
},
|
|
693
681
|
)
|
|
694
682
|
|
|
695
|
-
# Enable responsive mode via Plotly config
|
|
696
683
|
if "config" not in fig_data:
|
|
697
684
|
fig_data["config"] = {}
|
|
698
685
|
fig_data["config"].setdefault("responsive", True)
|
|
699
686
|
|
|
700
|
-
|
|
687
|
+
return fig_data
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def create_plotly_chart(fig_data: dict) -> ui.plotly:
|
|
691
|
+
"""Create a styled Plotly chart.
|
|
692
|
+
|
|
693
|
+
Uses the dict figure interface recommended by NiceGUI for live updates.
|
|
694
|
+
|
|
695
|
+
Args:
|
|
696
|
+
fig_data: Plotly figure data dict.
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
Styled plotly component.
|
|
700
|
+
"""
|
|
701
|
+
from grimoireplot.plot_live_update import normalize_figure_dict
|
|
702
|
+
|
|
703
|
+
chart = (
|
|
704
|
+
ui.plotly(style_plotly_figure(normalize_figure_dict(fig_data)))
|
|
705
|
+
.classes("w-full")
|
|
706
|
+
.style("height: 450px;")
|
|
707
|
+
)
|
|
701
708
|
return chart
|
|
702
709
|
|
|
703
710
|
|
|
@@ -8,7 +8,7 @@ build-backend = "uv_build"
|
|
|
8
8
|
|
|
9
9
|
[project]
|
|
10
10
|
name = "grimoireplot"
|
|
11
|
-
version = "0.0.
|
|
11
|
+
version = "0.0.3"
|
|
12
12
|
description = "GrimoirePlot is a live dashboard of plotly-compatible plots of remote data"
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
requires-python = ">=3.11"
|
|
@@ -20,7 +20,7 @@ classifiers = ["License :: OSI Approved :: MIT License"]
|
|
|
20
20
|
dependencies = [
|
|
21
21
|
"aiohttp[speedups]>=3.13.3",
|
|
22
22
|
"loguru>=0.7.3",
|
|
23
|
-
"nicegui>=3.
|
|
23
|
+
"nicegui>=3.14.0,<4",
|
|
24
24
|
"plotly>=6.5.2",
|
|
25
25
|
"python-dotenv>=1.2.1",
|
|
26
26
|
"requests>=2.32.5",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|