grimoireplot 0.0.1__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.
- grimoireplot/__init__.py +5 -0
- grimoireplot/client.py +166 -0
- grimoireplot/common.py +32 -0
- grimoireplot/create_some_plots.py +352 -0
- grimoireplot/main.py +284 -0
- grimoireplot/models.py +210 -0
- grimoireplot/server.py +90 -0
- grimoireplot/ui.py +200 -0
- grimoireplot/ui_elements.py +772 -0
- grimoireplot-0.0.1.dist-info/METADATA +217 -0
- grimoireplot-0.0.1.dist-info/RECORD +13 -0
- grimoireplot-0.0.1.dist-info/WHEEL +4 -0
- grimoireplot-0.0.1.dist-info/entry_points.txt +3 -0
grimoireplot/ui.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
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
|
+
"""module for nicegui UI components"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from nicegui import ui
|
|
9
|
+
from grimoireplot.models import (
|
|
10
|
+
get_all_grimoires,
|
|
11
|
+
delete_grimoire,
|
|
12
|
+
delete_chapter,
|
|
13
|
+
delete_plot,
|
|
14
|
+
get_plots_for_chapter,
|
|
15
|
+
Grimoire,
|
|
16
|
+
Chapter,
|
|
17
|
+
Plot,
|
|
18
|
+
)
|
|
19
|
+
from grimoireplot.ui_elements import (
|
|
20
|
+
create_tabs,
|
|
21
|
+
create_tab_panels,
|
|
22
|
+
create_tab_panel,
|
|
23
|
+
create_delete_badge,
|
|
24
|
+
create_plot_grid,
|
|
25
|
+
create_plot_container,
|
|
26
|
+
create_plotly_chart,
|
|
27
|
+
create_empty_state,
|
|
28
|
+
create_header,
|
|
29
|
+
create_page_container,
|
|
30
|
+
create_glass_card,
|
|
31
|
+
create_label,
|
|
32
|
+
create_btn_ghost,
|
|
33
|
+
create_btn_danger,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Store refreshable instances for each chapter's plots
|
|
37
|
+
_chapter_plot_refreshables: dict[tuple[str, str], ui.refreshable] = {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def refresh_chapter_plots(grimoire_name: str, chapter_name: str) -> bool:
|
|
41
|
+
"""Refresh only the plots for a specific chapter.
|
|
42
|
+
|
|
43
|
+
Returns True if the chapter was found and refreshed, False otherwise.
|
|
44
|
+
"""
|
|
45
|
+
key = (grimoire_name, chapter_name)
|
|
46
|
+
if key in _chapter_plot_refreshables:
|
|
47
|
+
_chapter_plot_refreshables[key].refresh()
|
|
48
|
+
return True
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def refresh_all_chapter_plots():
|
|
53
|
+
"""Refresh all chapter plot areas."""
|
|
54
|
+
for refreshable in _chapter_plot_refreshables.values():
|
|
55
|
+
refreshable.refresh()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def clear_chapter_refreshable(grimoire_name: str, chapter_name: str):
|
|
59
|
+
"""Remove a chapter's refreshable from the registry."""
|
|
60
|
+
_chapter_plot_refreshables.pop((grimoire_name, chapter_name), None)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def clear_all_chapter_refreshables():
|
|
64
|
+
"""Clear all chapter refreshables (called on full dashboard refresh)."""
|
|
65
|
+
_chapter_plot_refreshables.clear()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _confirm_delete(name: str, delete_fn, on_deleted=None):
|
|
69
|
+
"""Show a confirmation dialog before deleting.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
name: The name of the item to delete.
|
|
73
|
+
delete_fn: Function to call to perform the deletion.
|
|
74
|
+
on_deleted: Optional callback to call after deletion. If None, refreshes the whole dashboard.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def do_delete():
|
|
78
|
+
delete_fn(name)
|
|
79
|
+
dialog.close()
|
|
80
|
+
if on_deleted:
|
|
81
|
+
on_deleted()
|
|
82
|
+
else:
|
|
83
|
+
dashboard_ui.refresh()
|
|
84
|
+
|
|
85
|
+
with ui.dialog() as dialog:
|
|
86
|
+
with create_glass_card():
|
|
87
|
+
create_label(f"Delete '{name}'?").classes("text-xl font-bold mb-4")
|
|
88
|
+
create_label("This action cannot be undone.").classes("text-slate-400 mb-6")
|
|
89
|
+
with ui.row().classes("justify-end gap-3"):
|
|
90
|
+
create_btn_ghost("Cancel", on_click=dialog.close)
|
|
91
|
+
create_btn_danger("Delete", on_click=do_delete, icon="delete")
|
|
92
|
+
dialog.open()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@ui.refreshable
|
|
96
|
+
def dashboard_ui():
|
|
97
|
+
"""Top level component that builds the dashboard from database."""
|
|
98
|
+
# Clear stale refreshables since we're rebuilding the entire UI
|
|
99
|
+
clear_all_chapter_refreshables()
|
|
100
|
+
|
|
101
|
+
with create_page_container():
|
|
102
|
+
# Header
|
|
103
|
+
create_header("GrimoirePlot", "Your magical data visualization dashboard")
|
|
104
|
+
|
|
105
|
+
grimoires = get_all_grimoires()
|
|
106
|
+
|
|
107
|
+
if not grimoires:
|
|
108
|
+
create_empty_state(
|
|
109
|
+
"No grimoires found. Add some plots to get started!",
|
|
110
|
+
icon="auto_stories",
|
|
111
|
+
)
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# Create tabs for each grimoire
|
|
115
|
+
with create_tabs() as grimoire_tabs:
|
|
116
|
+
for grimoire in grimoires:
|
|
117
|
+
with ui.tab(grimoire.name).classes("rounded-lg") as tab:
|
|
118
|
+
tab.style("color: #94A3B8;")
|
|
119
|
+
create_delete_badge(
|
|
120
|
+
lambda _, g=grimoire: _confirm_delete(g.name, delete_grimoire)
|
|
121
|
+
)
|
|
122
|
+
tabs = list(grimoire_tabs)
|
|
123
|
+
|
|
124
|
+
# Create tab panels for each grimoire
|
|
125
|
+
with create_tab_panels(grimoire_tabs, value=tabs[0] if tabs else None):
|
|
126
|
+
for grimoire in grimoires:
|
|
127
|
+
with create_tab_panel(grimoire.name):
|
|
128
|
+
render_grimoire(grimoire)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def render_grimoire(grimoire: Grimoire):
|
|
132
|
+
"""Render a grimoire with its chapters."""
|
|
133
|
+
if not grimoire.chapters:
|
|
134
|
+
create_empty_state(f"No chapters in {grimoire.name}", icon="book")
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
grimoire_name = grimoire.name
|
|
138
|
+
|
|
139
|
+
# Create tabs for each chapter
|
|
140
|
+
with create_tabs() as chapter_tabs:
|
|
141
|
+
for chapter in grimoire.chapters:
|
|
142
|
+
with ui.tab(chapter.name).classes("rounded-lg") as tab:
|
|
143
|
+
tab.style("color: #94A3B8;")
|
|
144
|
+
create_delete_badge(
|
|
145
|
+
lambda _, g=grimoire_name, c=chapter: _confirm_delete(
|
|
146
|
+
c.name, lambda name: delete_chapter(g, name)
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
tabs = list(chapter_tabs)
|
|
150
|
+
|
|
151
|
+
# Create tab panels for each chapter
|
|
152
|
+
with create_tab_panels(chapter_tabs, value=tabs[0] if tabs else None):
|
|
153
|
+
for chapter in grimoire.chapters:
|
|
154
|
+
with create_tab_panel(chapter.name):
|
|
155
|
+
render_chapter(chapter, grimoire_name)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def render_chapter(chapter: Chapter, grimoire_name: str):
|
|
159
|
+
"""Render a chapter with its plots using a refreshable grid."""
|
|
160
|
+
chapter_name = chapter.name
|
|
161
|
+
|
|
162
|
+
@ui.refreshable
|
|
163
|
+
def chapter_plots_ui():
|
|
164
|
+
"""Refreshable UI for the plots in this chapter."""
|
|
165
|
+
plots = get_plots_for_chapter(grimoire_name, chapter_name)
|
|
166
|
+
if not plots:
|
|
167
|
+
create_empty_state(f"No plots in {chapter_name}", icon="insert_chart")
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
# Display plots in a responsive grid
|
|
171
|
+
with create_plot_grid(columns=2):
|
|
172
|
+
for plot in plots:
|
|
173
|
+
render_plot(plot, grimoire_name, chapter_name)
|
|
174
|
+
|
|
175
|
+
# Store the refreshable for this chapter
|
|
176
|
+
_chapter_plot_refreshables[(grimoire_name, chapter_name)] = chapter_plots_ui
|
|
177
|
+
|
|
178
|
+
# Render the plots
|
|
179
|
+
chapter_plots_ui()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def render_plot(plot: Plot, grimoire_name: str, chapter_name: str):
|
|
183
|
+
"""Render a single plot."""
|
|
184
|
+
|
|
185
|
+
def on_deleted():
|
|
186
|
+
"""Callback after plot is deleted - refresh only this chapter's plots."""
|
|
187
|
+
refresh_chapter_plots(grimoire_name, chapter_name)
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
fig = json.loads(plot.json_data)
|
|
191
|
+
with create_plot_container():
|
|
192
|
+
create_plotly_chart(fig)
|
|
193
|
+
create_delete_badge(
|
|
194
|
+
lambda _, g=grimoire_name, c=chapter_name, p=plot: _confirm_delete(
|
|
195
|
+
p.name, lambda name: delete_plot(g, c, name), on_deleted=on_deleted
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
except json.JSONDecodeError:
|
|
199
|
+
with create_plot_container():
|
|
200
|
+
create_label(f"Invalid plot data: {plot.name}").classes("text-red-400")
|