Skip to content

Commit 9bf0046

Browse files
sbryngelsonclaude
andcommitted
Add variable picker to interactive viz UI
- Sidebar now has a Variable dropdown listing all available fields; switching it live re-renders the plot with auto-ranged color scale. - --var is now optional in --interactive mode (defaults to first available variable so ./mfc.sh viz <dir> --interactive just works). - read_step always loads all variables in interactive mode so the in-server cache serves any variable without re-reading files. - vmin/vmax inputs are cleared automatically when the variable changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 9bd6fbe commit 9bf0046

2 files changed

Lines changed: 44 additions & 32 deletions

File tree

toolchain/mfc/viz/interactive.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -208,15 +208,15 @@ def run_interactive( # pylint: disable=too-many-locals,too-many-statements
208208
suppress_callback_exceptions=True,
209209
)
210210

211-
# Load first step to know dimensionality and initial range
211+
# Load first step to know dimensionality and available variables
212212
init = _load(steps[0], read_func)
213213
ndim = init.ndim
214-
d0 = init.variables[varname]
215-
pos0 = d0[d0 > 0] if np.any(d0 > 0) else d0
216-
init_min = float(np.nanmin(pos0))
217-
init_max = float(np.nanmax(d0))
214+
all_varnames = sorted(init.variables.keys())
215+
if varname not in all_varnames:
216+
varname = all_varnames[0] if all_varnames else varname
218217

219218
step_opts = [{'label': str(s), 'value': s} for s in steps]
219+
var_opts = [{'label': v, 'value': v} for v in all_varnames]
220220
cmap_opts = [{'label': c, 'value': c} for c in _CMAPS]
221221

222222
if ndim == 3:
@@ -241,10 +241,19 @@ def run_interactive( # pylint: disable=too-many-locals,too-many-statements
241241
'fontSize': '16px', 'fontWeight': 'bold', 'color': _ACCENT,
242242
}),
243243
html.Div(
244-
f'var: {varname} · {ndim}D · {len(steps)} step{"s" if len(steps) != 1 else ""}',
244+
f'{ndim}D · {len(steps)} step{"s" if len(steps) != 1 else ""}',
245245
style={'fontSize': '11px', 'color': _MUTED},
246246
),
247247

248+
# ── Variable ──────────────────────────────────────────────────
249+
_section('Variable',
250+
dcc.Dropdown(
251+
id='var-sel', options=var_opts, value=varname, clearable=False,
252+
style={'fontSize': '12px', 'backgroundColor': _OVER,
253+
'border': f'1px solid {_BORD}'},
254+
),
255+
),
256+
248257
# ── Timestep ──────────────────────────────────────────────────
249258
_section('Timestep',
250259
dcc.Dropdown(
@@ -454,14 +463,16 @@ def _toggle_controls(mode):
454463
Output('vmin-inp', 'value'),
455464
Output('vmax-inp', 'value'),
456465
Input('reset-btn', 'n_clicks'),
466+
Input('var-sel', 'value'),
457467
prevent_initial_call=True,
458468
)
459-
def _reset_range(_):
469+
def _reset_range(_reset, _var):
460470
return None, None
461471

462472
@app.callback( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals,too-many-branches,too-many-statements
463473
Output('viz-graph', 'figure'),
464474
Output('status-bar', 'children'),
475+
Input('var-sel', 'value'),
465476
Input('step-sel', 'value'),
466477
Input('mode-sel', 'value'),
467478
Input('slice-axis', 'value'),
@@ -479,14 +490,15 @@ def _reset_range(_):
479490
Input('vmin-inp', 'value'),
480491
Input('vmax-inp', 'value'),
481492
)
482-
def _update(step, mode,
493+
def _update(var_sel, step, mode,
483494
slice_axis, slice_pos,
484495
iso_min_frac, iso_max_frac, iso_n, iso_caps,
485496
vol_opacity, vol_nsurf, vol_min_frac, vol_max_frac,
486497
cmap, log_chk, vmin_in, vmax_in):
487498

499+
selected_var = var_sel or varname
488500
ad = _load(step, read_func)
489-
raw = ad.variables[varname]
501+
raw = ad.variables[selected_var]
490502
log = bool(log_chk and 'log' in log_chk)
491503
cmap = cmap or 'viridis'
492504

@@ -508,18 +520,18 @@ def _tf(arr):
508520
return np.where(arr > 0, np.log10(np.maximum(arr, 1e-300)), np.nan)
509521
cmin = float(np.log10(max(vmin, 1e-300)))
510522
cmax = float(np.log10(max(vmax, 1e-300)))
511-
cbar_title = f'log\u2081\u2080({varname})'
523+
cbar_title = f'log\u2081\u2080({selected_var})'
512524
else:
513525
def _tf(arr): return arr
514526
cmin, cmax = vmin, vmax
515-
cbar_title = varname
527+
cbar_title = selected_var
516528

517529
fig = go.Figure()
518530
title = ''
519531

520532
if ad.ndim == 3:
521533
trace, title = _build_3d(
522-
ad, raw, varname, step, mode, cmap, _tf, cmin, cmax, cbar_title,
534+
ad, raw, selected_var, step, mode, cmap, _tf, cmin, cmax, cbar_title,
523535
slice_axis or 'z', float(slice_pos or 0.5),
524536
float(iso_min_frac or 0.2), float(iso_max_frac or 0.8),
525537
int(iso_n or 3), bool(iso_caps and 'caps' in iso_caps),
@@ -557,21 +569,21 @@ def _tf(arr): return arr
557569
yaxis=dict(title='y', color=_TEXT, gridcolor=_OVER),
558570
plot_bgcolor=_BG,
559571
)
560-
title = f'{varname} · step {step}'
572+
title = f'{selected_var} · step {step}'
561573

562574
else: # 1D
563575
plot_y = _tf(raw) if log else raw
564576
fig.add_trace(go.Scatter(
565577
x=ad.x_cc, y=plot_y, mode='lines',
566-
line=dict(color=_ACCENT, width=2), name=varname,
578+
line=dict(color=_ACCENT, width=2), name=selected_var,
567579
))
568580
fig.update_layout(
569581
xaxis=dict(title='x', color=_TEXT, gridcolor=_OVER),
570582
yaxis=dict(title=cbar_title, color=_TEXT, gridcolor=_OVER,
571583
range=[cmin, cmax] if (vmin_in or vmax_in) else None),
572584
plot_bgcolor=_BG,
573585
)
574-
title = f'{varname} · step {step}'
586+
title = f'{selected_var} · step {step}'
575587

576588
fig.update_layout(
577589
title=dict(text=title, font=dict(color=_TEXT, size=13, family='monospace')),

toolchain/mfc/viz/viz.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ def viz(): # pylint: disable=too-many-locals,too-many-statements,too-many-branc
7171
cons.print(f"[bold]Format:[/bold] {fmt}")
7272

7373
# Quick guide when no action is specified
74-
if not ARG('list_steps') and not ARG('list_vars') and ARG('var') is None:
74+
if not ARG('list_steps') and not ARG('list_vars') and ARG('var') is None \
75+
and not ARG('interactive'):
7576
cons.print()
7677
d = case_dir
7778
cons.print("[bold]Quick start:[/bold]")
@@ -141,7 +142,7 @@ def viz(): # pylint: disable=too-many-locals,too-many-statements,too-many-branc
141142
varname = ARG('var')
142143
step_arg = ARG('step')
143144

144-
if varname is None:
145+
if varname is None and not ARG('interactive'):
145146
raise MFCException("--var is required for rendering. "
146147
"Use --list-vars to see available variables.")
147148

@@ -190,31 +191,30 @@ def viz(): # pylint: disable=too-many-locals,too-many-statements,too-many-branc
190191
if slice_value is not None:
191192
render_opts['slice_value'] = float(slice_value)
192193

193-
# Choose read function based on format
194-
def read_step(step):
195-
if fmt == 'silo':
196-
from .silo_reader import assemble_silo # pylint: disable=import-outside-toplevel
197-
return assemble_silo(case_dir, step, var=varname)
198-
return assemble(case_dir, step, fmt, var=varname)
194+
interactive = ARG('interactive')
199195

200-
# Validate variable name by reading the first timestep (without var filter)
201-
def read_step_all_vars(step):
196+
# Interactive mode always loads all variables (user can switch in UI).
197+
# Non-interactive mode can filter to just the requested variable for speed.
198+
def read_step(step):
202199
if fmt == 'silo':
203200
from .silo_reader import assemble_silo # pylint: disable=import-outside-toplevel
204-
return assemble_silo(case_dir, step)
205-
return assemble(case_dir, step, fmt)
201+
return assemble_silo(case_dir, step, var=None if interactive else varname)
202+
return assemble(case_dir, step, fmt, var=None if interactive else varname)
206203

207-
test_assembled = read_step_all_vars(requested_steps[0])
208-
if varname not in test_assembled.variables:
209-
avail = sorted(test_assembled.variables.keys())
204+
# Validate variable name / discover available variables
205+
test_assembled = read_step(requested_steps[0])
206+
avail = sorted(test_assembled.variables.keys())
207+
if not interactive and varname not in test_assembled.variables:
210208
raise MFCException(f"Variable '{varname}' not found. "
211209
f"Available variables: {', '.join(avail)}")
212210

213211
# Interactive mode — launch Dash web server
214-
if ARG('interactive'):
212+
if interactive:
215213
from .interactive import run_interactive # pylint: disable=import-outside-toplevel
216214
port = ARG('port') or 8050
217-
run_interactive(varname, requested_steps, read_step, port=int(port))
215+
# Default to first available variable if --var was not specified
216+
init_var = varname if varname in avail else (avail[0] if avail else None)
217+
run_interactive(init_var, requested_steps, read_step, port=int(port))
218218
return
219219

220220
# Create output directory

0 commit comments

Comments
 (0)