arbok-inspector 0.0.0__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.

Potentially problematic release.


This version of arbok-inspector might be problematic. Click here for more details.

@@ -0,0 +1,159 @@
1
+ import time
2
+ from nicegui import ui, app
3
+ from arbok_inspector.state import inspector
4
+ from arbok_inspector.pages.run_view import run_page
5
+ from arbok_inspector.widgets.update_day_selecter import update_day_selecter
6
+ from arbok_inspector.widgets.update_run_selecter import update_run_selecter
7
+ from arbok_inspector.classes.run import Run
8
+
9
+ DAY_GRID_COLUMN_DEFS = [
10
+ {'headerName': 'Day', 'field': 'day'},
11
+ ]
12
+
13
+ small_col_width = 30
14
+ RUN_GRID_COLUMN_DEFS = [
15
+ {'headerName': 'Run ID', 'field': 'run_id', "width": small_col_width},
16
+ {'headerName': 'Name', 'field': 'name'},
17
+ {'headerName': 'Exp ID', 'field': 'exp_id', "width": small_col_width},
18
+ {'headerName': '# Results', 'field': 'result_counter', "width": small_col_width},
19
+ {'headerName': 'Started', 'field': 'run_timestamp', "width": small_col_width},
20
+ {'headerName': 'Finish', 'field': 'completed_timestamp', "width": small_col_width},
21
+ ]
22
+ RUN_PARAM_DICT = {
23
+ 'run_id': 'Run ID',
24
+ 'exp_id': 'Experiment ID',
25
+ 'result_counter': '# results',
26
+ 'run_timestamp': 'Started',
27
+ 'completed_timestamp': 'Completed',
28
+ }
29
+ AGGRID_STYLE = 'height: 95%; min-height: 0;'
30
+ EXPANSION_CLASSES = 'w-full p-0 gap-1 border border-gray-400 rounded-lg no-wrap items-start'
31
+
32
+ shared_data = {}
33
+
34
+ @ui.page('/browser')
35
+ async def database_browser_page():
36
+ _ = await ui.context.client.connected()
37
+ app.storage.general["avg_axis"] = None
38
+ app.storage.general["result_keywords"] = None
39
+ app.storage.tab["avg_axis_input"] = None
40
+ app.storage.tab["result_keyword_input"] = None
41
+ offset_minutes = await ui.run_javascript('new Date().getTimezoneOffset()')
42
+ offset_hours = -float(offset_minutes) / 60
43
+ app.storage.general["timezone"] = offset_hours
44
+ print(f"TIMEZONE: UTC{offset_hours}")
45
+
46
+ """Database general page showing the selected database"""
47
+ grids = {'day': None, 'run': None}
48
+ with ui.column().classes('w-full h-screen'):
49
+ ui.add_head_html('<title>Arbok Inspector - Database general</title>')
50
+ with ui.row().classes('w-full items-center justify-between'):
51
+ ui.label('Arbok Inspector').classes('text-3xl font-bold mb-1')
52
+
53
+ with ui.expansion('Database info and settings', icon='info', value=True)\
54
+ .classes(EXPANSION_CLASSES).props('expand-separator'):
55
+ build_database_info_section(grids)
56
+
57
+ with ui.row().classes('w-full flex-1'):
58
+ build_day_selecter(grids)
59
+ build_run_selecter(grids)
60
+
61
+ def open_run_page(run_id: int):
62
+ # with ui.dialog() as dialog, ui.card():
63
+ # ui.label(f'loading run {run_id} ...').classes(
64
+ # 'text-3xl font-bold mb-1')
65
+ # app.storage.general["run"] = Run(run_id)
66
+ # time.sleep(2)
67
+ # dialog.close()
68
+ shared_data['test'] = "hello"
69
+ app.storage.general["avg_axis"] = app.storage.tab["avg_axis_input"].value
70
+ app.storage.general["result_keywords"] = app.storage.tab["result_keyword_input"].value
71
+ print(f"Result Keywords:")
72
+ print(app.storage.general['result_keywords'])
73
+ ui.navigate.to(f'/run/{run_id}', new_tab=True)
74
+
75
+ def build_database_info_section(grids):
76
+ """Build the database information and settings section."""
77
+ with ui.row().classes('w-full'):
78
+ build_info_section()
79
+ build_settings_section(grids)
80
+
81
+ def build_info_section():
82
+ """Build the database information section."""
83
+ with ui.column().classes('w-1/3'):
84
+ ui.label('Database Information').classes('text-xl font-semibold mb-4')
85
+ if inspector.database_path:
86
+ ui.label(f'Database Path: {str(inspector.database_path)}').classes()
87
+ else:
88
+ ui.label('No database selected').classes('text-lg text-red-500')
89
+
90
+ def build_settings_section(grids):
91
+ """Build the database settings section."""
92
+ with ui.column().classes('w-1/2'):
93
+ with ui.row().classes('w-full'):
94
+ app.storage.tab["result_keyword_input"] = ui.input(
95
+ label = 'auto-plot keywords',
96
+ placeholder="e.g:\t\t[ ( 'Q1' , 'state' ), 'feedback' ]"
97
+ ).props('rounded outlined dense')\
98
+ .classes('w-full')\
99
+ .tooltip("""
100
+ Selects all results that contain the specified keywords in their name.<br>
101
+ Can be a single keyword (string) or a tuple of keywords.<br>
102
+ The latter one requires all keywords to be present in the result name.<br>
103
+
104
+ The given example would select all results that contain 'Q1' and 'state' in their name<br>
105
+ or all results that contain 'feedback' in their name.
106
+ """).props('v-html')
107
+ app.storage.tab["avg_axis_input"] = ui.input(
108
+ label = 'average-axis keyword',
109
+ value = "iteration"
110
+ ).props('rounded outlined dense')\
111
+ .classes('w-full')
112
+ with ui.row().classes('w-full justify-start'):
113
+ ui.button(
114
+ text = 'Select New Database',
115
+ on_click=lambda: ui.navigate.to('/'),
116
+ color='purple').classes()
117
+ ui.button(
118
+ text = 'Reload',
119
+ on_click=lambda: update_day_selecter(grids['day']),
120
+ color = '#4BA701'
121
+ ).classes()
122
+
123
+ def build_day_selecter(grids):
124
+ """Build the day selecter grid."""
125
+ with ui.column().style('width: 120px;').classes('h-full'):
126
+ grids['day'] = ui.aggrid(
127
+ {
128
+ 'defaultColDef': {'flex': 1},
129
+ 'columnDefs': DAY_GRID_COLUMN_DEFS,
130
+ 'rowData': {},
131
+ 'rowSelection': 'multiple',
132
+ },
133
+ theme = 'ag-theme-balham-dark')\
134
+ .classes('text-sm ag-theme-balham-dark')\
135
+ .style(AGGRID_STYLE)\
136
+ .on(
137
+ type = 'cellClicked',
138
+ handler = lambda event: update_run_selecter(
139
+ grids['run'], event.args["value"], RUN_GRID_COLUMN_DEFS)
140
+ )
141
+ update_day_selecter(grids['day'])
142
+
143
+ def build_run_selecter(grids):
144
+ """Build the run selecter grid."""
145
+ with ui.column().classes('flex-1').classes('h-full'):
146
+ grids['run'] = ui.aggrid(
147
+ {
148
+ 'defaultColDef': {'flex': 1},
149
+ 'columnDefs': RUN_GRID_COLUMN_DEFS,
150
+ 'rowData': {},
151
+ 'rowSelection': 'multiple',
152
+ },
153
+ #theme = 'ag-theme-balham-dark'
154
+ ).classes('ag-theme-balham-dark').style(
155
+ AGGRID_STYLE
156
+ ).on(
157
+ 'cellClicked',
158
+ lambda event: open_run_page(event.args['data']['run_id'])
159
+ )
@@ -0,0 +1,35 @@
1
+
2
+ from nicegui import ui
3
+ from arbok_inspector.state import inspector
4
+
5
+ @ui.page('/')
6
+ async def greeter_page():
7
+ """Main page that starts with file selection"""
8
+ ui.add_head_html('<title>Arbok Inspector 🐍</title>')
9
+
10
+ # Show initial file selection dialog
11
+ with ui.dialog() as dialog, ui.card().classes('w-96'):
12
+ # Store dialog reference so we can close it later
13
+ inspector.initial_dialog = dialog
14
+ dialog.props('persistent')
15
+
16
+ ui.label('Welcome to Arbok Inspector').classes('text-h6 mb-4')
17
+ ui.image('https://microsoft.github.io/Qcodes/_images/qcodes_logo.png')
18
+ ui.label('Please enter the path to your QCoDeS database file'
19
+ ).classes('text-body1 mb-4')
20
+ ui.label('Database File Path').classes('text-subtitle2 mb-2')
21
+ path_input = ui.input(
22
+ label='Database file path',
23
+ placeholder='C:/path/to/your/database.db'
24
+ ).classes('w-full mb-2')
25
+ ui.button(
26
+ text = 'Load Database',
27
+ on_click=lambda: inspector.handle_path_input(path_input),
28
+ icon='folder_open',
29
+ color='purple').classes('mb-4 w-full')
30
+ ui.separator()
31
+ ui.label('Supported formats: .db, .sqlite, .sqlite3'
32
+ ).classes('text-caption text-grey')
33
+
34
+ # Auto-open the dialog
35
+ dialog.open()
@@ -0,0 +1,280 @@
1
+ """Run view page showing the data and plots for a specific run"""
2
+ from __future__ import annotations
3
+ from typing import TYPE_CHECKING
4
+ import json
5
+ import importlib.resources as resources
6
+
7
+ from nicegui import ui, app
8
+
9
+ from arbok_inspector.widgets.build_xarray_grid import build_xarray_grid
10
+ from arbok_inspector.widgets.build_xarray_html import build_xarray_html
11
+ from arbok_inspector.widgets.json_plot_settings_dialog import JsonPlotSettingsDialog
12
+ from arbok_inspector.helpers.unit_formater import unit_formatter
13
+ from arbok_inspector.classes.run import Run
14
+
15
+ from arbok_inspector.classes.dim import Dim
16
+
17
+ RUN_TABLE_COLUMNS = [
18
+ {'field': 'name', 'filter': 'agTextColumnFilter', 'floatingFilter': True},
19
+ {'field': 'size'},
20
+ {'field': 'x', 'checkboxSelection': True},
21
+ {'field': 'y', 'checkboxSelection': True},
22
+ {'field': 'average', 'checkboxSelection': True},
23
+ ]
24
+
25
+ AXIS_OPTIONS = ['average', 'select_value', 'y-axis', 'x-axis']
26
+
27
+ EXPANSION_CLASSES = 'w-full p-0 gap-1 border border-gray-400 rounded-lg no-wrap items-start'
28
+
29
+
30
+ @ui.page('/run/{run_id}')
31
+ async def run_page(run_id: str):
32
+ """
33
+ Page showing the details and plots for a specific run.
34
+
35
+ Args:
36
+ run_id (str): ID of the run to display
37
+ """
38
+ ui.page_title(f"{run_id}")
39
+ _ = await ui.context.client.connected()
40
+ # run = app.storage.general["run"] # Run(int(run_id))
41
+ run = Run(int(run_id))
42
+
43
+ app.storage.tab["placeholders"] = {'plots': None}
44
+ app.storage.tab["run"] = run
45
+ with resources.files("arbok_inspector.configurations").joinpath("1d_plot.json").open("r") as f:
46
+ app.storage.tab["plot_dict_1D"] = json.load(f)
47
+ with resources.files("arbok_inspector.configurations").joinpath("2d_plot.json").open("r") as f:
48
+ app.storage.tab["plot_dict_2D"] = json.load(f)
49
+
50
+ ui.label(f'Run Page for ID: {run_id}').classes('text-2xl font-bold')
51
+ with ui.column().classes('w-full gap-1'):
52
+ with ui.expansion('Coordinates and results', icon='checklist', value=True)\
53
+ .classes(EXPANSION_CLASSES).props('expand-separator'):
54
+ with ui.row().classes('w-full gap-4 no-wrap items-start'):
55
+ with ui.column().classes('w-2/3 gap-2'):
56
+ ui.label("Coordinates:").classes('text-lg font-semibold')
57
+ for i, _ in run.parallel_sweep_axes.items():
58
+ add_dim_dropdown(sweep_idx = i)
59
+ with ui.column().classes('w-1/3 gap-2'):
60
+ ui.label("Results:").classes('text-lg font-semibold')
61
+ for i, result in enumerate(run.full_data_set):
62
+ value = False
63
+ if result in run.plot_selection:
64
+ value = True
65
+ ui.checkbox(
66
+ text = result.replace("__", "."),
67
+ value = value,
68
+ on_change = lambda e, r=result: run.update_plot_selection(e.value, r),
69
+ ).classes('text-sm h-4').props('color=purple')
70
+ with ui.expansion('Plots', icon='stacked_line_chart', value=True)\
71
+ .classes(EXPANSION_CLASSES):
72
+ with ui.row().classes('w-full p-2 gap-2 items-center rounded-md border border-neutral-600 bg-neutral-800 text-sm'):
73
+
74
+ ui.button(
75
+ text='Update',
76
+ icon='refresh',
77
+ color='green',
78
+ on_click=lambda: build_xarray_grid(),
79
+ ).classes('h-8 px-2')
80
+
81
+ ui.button(
82
+ text='Debug',
83
+ icon='info',
84
+ color='red',
85
+ on_click=lambda: print_debug(run),
86
+ ).classes('h-8 px-2')
87
+
88
+ dialog_1d = JsonPlotSettingsDialog('plot_dict_1D')
89
+ dialog_2d = JsonPlotSettingsDialog('plot_dict_2D')
90
+
91
+ ui.button(
92
+ text='1D settings',
93
+ color='pink',
94
+ on_click=dialog_1d.open,
95
+ ).classes('h-8 px-2')
96
+
97
+ ui.button(
98
+ text='2D settings',
99
+ color='orange',
100
+ on_click=dialog_2d.open,
101
+ ).classes('h-8 px-2')
102
+
103
+ ui.number(
104
+ label='# per col',
105
+ value=2,
106
+ format='%.0f',
107
+ on_change=lambda e: set_plots_per_column(e.value),
108
+ ).props('dense outlined').classes('w-20 h-8 text-xs mb-2')
109
+ ui.button(
110
+ icon = 'file_download',
111
+ text = 'full dataset',
112
+ color = 'blue',
113
+ on_click=download_full_dataset,
114
+ ).classes('h-8 px-2')
115
+ ui.button(
116
+ icon = 'file_download',
117
+ text = 'data selection',
118
+ color='darkblue',
119
+ on_click=download_data_selection,
120
+ ).classes('h-8 px-2')
121
+ #.style('line-height: 1rem; padding-top: 0; padding-bottom: 0;')
122
+ app.storage.tab["placeholders"]["plots"] = ui.row().classes('w-full p-4')
123
+ build_xarray_grid()
124
+ with ui.expansion('xarray summary', icon='summarize', value=False)\
125
+ .classes(EXPANSION_CLASSES):
126
+ build_xarray_html()
127
+ with ui.expansion('analysis', icon='science', value=False)\
128
+ .classes(EXPANSION_CLASSES):
129
+ with ui.row():
130
+ ui.label("Working on it! -Andi").classes('text-lg font-semibold')
131
+ with ui.expansion('metadata', icon='numbers', value=False)\
132
+ .classes(EXPANSION_CLASSES):
133
+ ui.row().classes('w-full p-4')
134
+
135
+
136
+ def add_dim_dropdown(sweep_idx: int):
137
+ """
138
+ Add a dropdown to select the dimension option for a given sweep index.
139
+
140
+ Args:
141
+ sweep_idx (int): Index of the sweep to add the dropdown for
142
+ """
143
+ run = app.storage.tab["run"]
144
+ width = 'w-1/2' if run.together_sweeps else 'w-full'
145
+ dim = run.sweep_dict[sweep_idx]
146
+ local_placeholder = {"slider": None}
147
+ with ui.column().classes('w-full no-wrap items-center gap-1'):
148
+ ui_element = ui.select(
149
+ options = AXIS_OPTIONS,
150
+ value = str(dim.option),
151
+ label = f'{dim.name.replace("__", ".")}',
152
+ on_change = lambda e: update_dim_selection(
153
+ dim, e.value, local_placeholder["slider"])
154
+ ).classes(f"{width} text-sm m-0 p-0").props('dense')
155
+ dim.ui_selector = ui_element
156
+ if run.together_sweeps:
157
+ dims_names = run.parallel_sweep_axes[sweep_idx]
158
+ ui.radio(
159
+ options = dims_names,
160
+ value=dim.name,
161
+ on_change = lambda e: update_sweep_dim_name(dim, e.value)
162
+ ).classes(width).props('dense')
163
+ local_placeholder["slider"] = ui.column().classes('w-full')
164
+ if dim.option == 'select_value':
165
+ build_dim_slider(run, dim, local_placeholder["slider"])
166
+
167
+ def update_dim_selection(dim: Dim, value: str, slider_placeholder):
168
+ """
169
+ Update the dimension/sweep selection and rebuild the plot grid.
170
+
171
+ Args:
172
+ dim (Dim): The dimension object to update
173
+ value (str): The new selection value
174
+ slider_placeholder: The UI placeholder to update
175
+ """
176
+ run = app.storage.tab["run"]
177
+ if slider_placeholder is not None:
178
+ slider_placeholder.clear()
179
+ print(value)
180
+ if value == 'average':
181
+ run.update_subset_dims(dim, 'average')
182
+ dim.option = 'average'
183
+ if value == 'select_value':
184
+ with slider_placeholder:
185
+ build_dim_slider(run, dim, slider_placeholder)
186
+ else:
187
+ run.update_subset_dims(dim, value)
188
+ dim.option = value
189
+ build_xarray_grid()
190
+
191
+ def build_dim_slider(run: Runm, dim: Dim, slider_placeholder):
192
+ """
193
+ Build a slider for selecting the index of a dimension.
194
+
195
+ Args:
196
+ dim (Dim): The dimension object
197
+ slider_placeholder: The UI placeholder to add the slider to
198
+ """
199
+ dim_size = run.full_data_set.sizes[dim.name]
200
+ with ui.row().classes("w-full items-center"):
201
+ with ui.column().classes('flex-grow'):
202
+ slider = ui.slider(
203
+ min=0, max=dim_size - 1, step=1, value=0,
204
+ on_change=lambda e: run.update_subset_dims(dim, 'select_value', e.value),
205
+ ).classes('flex-grow')\
206
+ .props('color="purple" markers label-always')
207
+ label = ui.html('').classes('shrink-0 text-right px-2 py-1 bg-purple text-white rounded-lg text-xs font-normal text-center')
208
+ update_value_from_dim_slider(label, slider, dim, plot = False)
209
+ slider.on(
210
+ 'update:model-value',
211
+ lambda e: update_value_from_dim_slider(label, slider, dim),
212
+ throttle=0.2, leading_events=False)
213
+
214
+ def update_value_from_dim_slider(label, slider, dim: Dim, plot = True):
215
+ """
216
+ Update the label next to the slider with the current value and unit.
217
+
218
+ Args:
219
+ label: The UI label to update
220
+ slider: The UI slider to get the value from
221
+ dim (Dim): The dimension object
222
+ """
223
+ run = app.storage.tab["run"]
224
+ label_txt = f' {unit_formatter(run, dim, slider.value)} '
225
+ label.set_content(label_txt)
226
+ if plot:
227
+ build_xarray_grid()
228
+
229
+ def set_plots_per_column(value: int):
230
+ """
231
+ Set the number of plots to display per column.
232
+
233
+ Args:
234
+ value (int): The number of plots per column
235
+ """
236
+ run = app.storage.tab["run"]
237
+ ui.notify(f'Setting plots per column to {value}', position='top-right')
238
+ run.plots_per_column = int(value)
239
+ build_xarray_grid()
240
+
241
+
242
+ def update_sweep_dim_name(dim: Dim, new_name: str):
243
+ """
244
+ Update the name of the dimension in the sweep dict and the dim object.
245
+
246
+ Args:
247
+ dim (Dim): The dimension object to update
248
+ new_name (str): The new name for the dimension
249
+ """
250
+ run = app.storage.tab["run"]
251
+ dim.name = new_name
252
+ dim.ui_selector.label = new_name.replace("__", ".")
253
+ build_xarray_grid()
254
+
255
+ def print_debug(run: Run):
256
+ print("\nDebugging Run:")
257
+ for key, val in run.dim_axis_option.items():
258
+ if isinstance(val, list):
259
+ val_str = str([d.name for d in val])
260
+ elif isinstance(val, Dim):
261
+ val_str = val.name
262
+ else:
263
+ val_str = str(val)
264
+ print(f"{key}: \t {val_str}")
265
+
266
+ def download_full_dataset():
267
+ """Download the full dataset as a NetCDF file."""
268
+ run = app.storage.tab["run"]
269
+ local_path = f'./run_{run.run_id}.nc'
270
+ run.full_data_set.to_netcdf(local_path)
271
+ ui.download.file(local_path)
272
+ os.remove(local_path)
273
+
274
+ def download_data_selection():
275
+ """Download the current data selection as a NetCDF file."""
276
+ run = app.storage.tab["run"]
277
+ local_path = f'./run_{run.run_id}_selection.nc'
278
+ run.last_subset.to_netcdf(local_path)
279
+ ui.download.file(local_path)
280
+ os.remove(local_path)
@@ -0,0 +1,56 @@
1
+ from nicegui import ui
2
+ from pathlib import Path
3
+ import asyncio
4
+ from typing import Optional
5
+ import sqlite3
6
+ from qcodes.dataset import initialise_or_create_database_at
7
+
8
+ class ArbokInspector:
9
+ def __init__(self):
10
+ self.database_path: Optional[Path] = None
11
+ self.initial_dialog = None
12
+ self.csonn = None
13
+ self.cursor = None
14
+
15
+ def connect_database(self):
16
+ self.conn = sqlite3.connect(self.database_path)
17
+ self.conn.row_factory = sqlite3.Row
18
+ self.cursor = self.conn.cursor()
19
+ initialise_or_create_database_at(self.database_path)
20
+
21
+ def handle_path_input(self, path_input):
22
+ """Handle manual path input"""
23
+ if path_input.value:
24
+ try:
25
+ file_path = Path(path_input.value)
26
+ if file_path.exists():
27
+ self.database_path = file_path
28
+ ui.notify(f'Database path set: {file_path.name}', type='positive')
29
+ # Only close dialog if path is valid and exists
30
+ try:
31
+ self.connect_database()
32
+ if self.initial_dialog:
33
+ self.initial_dialog.close()
34
+ self.show_database_path()
35
+ except sqlite3.Error as e:
36
+ ui.notify(f'Error connecting to database: {str(e)}', type='negative')
37
+ # Don't close dialog - let user try again
38
+ else:
39
+ ui.notify('File does not exist', type='negative')
40
+ # Don't close dialog - let user try again
41
+ except Exception as ex:
42
+ ui.notify(f'Error: {str(ex)}', type='negative')
43
+ # Don't close dialog on error
44
+ else:
45
+ ui.notify('Please enter a file path', type='warning')
46
+ # Don't close dialog if no path entered
47
+
48
+ def show_database_path(self):
49
+ """Display the database path on the main page"""
50
+ # Navigate to a database browser page instead of clearing
51
+ ui.navigate.to('/browser')
52
+
53
+ # Initialize the application
54
+ inspector = ArbokInspector()
55
+ inspector.database_path = 'test.db' # For development purposes only
56
+ inspector.connect_database()
@@ -0,0 +1,65 @@
1
+ import numpy as np
2
+ import xarray as xr
3
+ import plotly.express as px
4
+ from nicegui import ui
5
+
6
+ # --- Create a sample 4D DataArray ---
7
+ data = np.random.rand(5, 10, 20, 30) # shape: time, depth, y, x
8
+ coords = {
9
+ 'time': np.arange(5),
10
+ 'depth': np.linspace(0, 100, 10),
11
+ 'y': np.linspace(-5, 5, 20),
12
+ 'x': np.linspace(-5, 5, 30),
13
+ }
14
+ array = xr.DataArray(data, dims=('time', 'depth', 'y', 'x'), coords=coords)
15
+
16
+ # --- Define axes to plot ---
17
+ x_dim = 'x'
18
+ y_dim = 'y'
19
+ slider_dims = [dim for dim in array.dims if dim not in (x_dim, y_dim)]
20
+
21
+ # --- UI State ---
22
+ slider_values = {dim: 0 for dim in slider_dims}
23
+ plot_container = ui.row().classes('w-full justify-center')
24
+
25
+ # --- Heatmap update function ---
26
+ def update_plot():
27
+ # Index the array using current slider values
28
+ sel = {dim: slider_values[dim] for dim in slider_dims}
29
+ slice_2d = array.isel(**sel)
30
+
31
+ # Convert to plotly figure
32
+ fig = px.imshow(
33
+ slice_2d.values,
34
+ labels={'x': x_dim, 'y': y_dim},
35
+ x=array.coords[x_dim].values,
36
+ y=array.coords[y_dim].values,
37
+ color_continuous_scale='Viridis',
38
+ )
39
+ fig.update_layout(title=f'{x_dim} vs {y_dim} | ' + ', '.join([f'{dim}={slider_values[dim]}' for dim in slider_dims]))
40
+
41
+ plot_container.clear()
42
+ with plot_container:
43
+ ui.plotly(fig).classes('max-w-3xl max-h-96')
44
+
45
+ # --- Create sliders for all non-plotted dimensions ---
46
+ for dim in slider_dims:
47
+ max_index = len(array.coords[dim]) - 1
48
+ def make_slider(d=dim):
49
+ def on_change(val):
50
+ slider_values[d] = int(val)
51
+ update_plot()
52
+ ui.slider(
53
+ min=0,
54
+ max=max_index,
55
+ value=0,
56
+ step=1,
57
+ on_change=on_change,
58
+ #abel=f'{d} ({array.coords[d].values[0]})'
59
+ ).props('label-always').classes('w-full')
60
+ make_slider()
61
+
62
+ # --- Initial plot ---
63
+ update_plot()
64
+
65
+ ui.run()