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/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")