From 9107c3fbd0466197494ea2b2d6127de6634d5384 Mon Sep 17 00:00:00 2001 From: willdunklin Date: Mon, 25 May 2026 13:56:43 -0400 Subject: [PATCH 1/3] fix: swapping variable views changes simulation order --- src/e3sm_compareview/view_manager.py | 71 +++++++++++++++++++++------- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/src/e3sm_compareview/view_manager.py b/src/e3sm_compareview/view_manager.py index f7a5f35..928527f 100644 --- a/src/e3sm_compareview/view_manager.py +++ b/src/e3sm_compareview/view_manager.py @@ -267,7 +267,7 @@ def reset_view_orders(self, variables=None): if not variables: return - for _, var_names in variables.items(): + for var_names in variables.values(): for var_name in var_names: for view_spec in self.get_view_specs(var_name): view = self._var2view.get(view_spec["array_name"]) @@ -395,19 +395,67 @@ def swap_pair(array_name_a, array_name_b): config_a.order, config_b.order = config_b.order, config_a.order config_a.size, config_b.size = config_b.size, config_a.size config_a.offset, config_b.offset = config_b.offset, config_a.offset - config_a.break_row, config_b.break_row = config_b.break_row, config_a.break_row + config_a.break_row, config_b.break_row = ( + config_b.break_row, + config_a.break_row, + ) + + metadata_a = self.source.data_reader.get_array_metadata(variable_a) or {} + metadata_b = self.source.data_reader.get_array_metadata(variable_b) or {} + grouped_layout = self.state.layout_grouped + if grouped_layout and self.state.comparison_mode == "multi-sim": + path_a = metadata_a.get("path") + path_b = metadata_b.get("path") + + if path_a and path_b and path_a != path_b: + simulation_configs = list(self.state.simulation_configs or []) + index_a = None + index_b = None + for index, entry in enumerate(simulation_configs): + path = entry.get("path") + if path == path_a: + index_a = index + if path == path_b: + index_b = index + + if index_a is not None and index_b is not None and index_a != index_b: + simulation_configs[index_a], simulation_configs[index_b] = ( + simulation_configs[index_b], + simulation_configs[index_a], + ) + self.state.simulation_configs = simulation_configs + return - if not self.state.layout_grouped: + if not grouped_layout: swap_pair(variable_a, variable_b) return - slot_a = self._slot_index_for_array(variable_a) - slot_b = self._slot_index_for_array(variable_b) + base_variable_a = metadata_a.get("base_variable") + base_variable_b = metadata_b.get("base_variable") + slot_a = None + slot_b = None + + if base_variable_a: + for slot_index, view_spec in enumerate( + self.get_view_specs(base_variable_a) + ): + if view_spec["array_name"] == variable_a: + slot_a = slot_index + break + + if base_variable_b: + for slot_index, view_spec in enumerate( + self.get_view_specs(base_variable_b) + ): + if view_spec["array_name"] == variable_b: + slot_b = slot_index + break + if slot_a is None or slot_b is None or slot_a == slot_b: swap_pair(variable_a, variable_b) return - for _, var_names in self._last_vars.items(): + for var_names in self._last_vars.values(): for var_name in var_names: view_specs = self.get_view_specs(var_name) if slot_a >= len(view_specs) or slot_b >= len(view_specs): @@ -418,17 +466,6 @@ def swap_pair(array_name_a, array_name_b): view_specs[slot_b]["array_name"], ) - def _slot_index_for_array(self, array_name): - metadata = self.source.data_reader.get_array_metadata(array_name) or {} - base_variable = metadata.get("base_variable") - if not base_variable: - return None - - for slot_index, view_spec in enumerate(self.get_view_specs(base_variable)): - if view_spec["array_name"] == array_name: - return slot_index - return None - def apply_size(self, n_cols): if not self._last_vars: return From 6fa4247f8c50962a45ad5586f3e7c508235ff289 Mon Sep 17 00:00:00 2001 From: willdunklin Date: Mon, 25 May 2026 14:16:31 -0400 Subject: [PATCH 2/3] feat: add Simulation Controls toolbar --- src/e3sm_compareview/app.py | 3 +- .../components/simulation_selection.py | 281 --------------- src/e3sm_compareview/components/toolbars.py | 333 +++++++++++++++++- src/e3sm_compareview/components/tools.py | 17 +- 4 files changed, 346 insertions(+), 288 deletions(-) delete mode 100644 src/e3sm_compareview/components/simulation_selection.py diff --git a/src/e3sm_compareview/app.py b/src/e3sm_compareview/app.py index 629936b..e35d0be 100644 --- a/src/e3sm_compareview/app.py +++ b/src/e3sm_compareview/app.py @@ -30,7 +30,6 @@ doc, drawers, file_browser, - simulation_selection, toolbars, ) from e3sm_compareview.components import ( @@ -256,7 +255,6 @@ def _build_ui(self, **_): with v3.VMain(): dialogs.FileOpen(self.file_browser) dialogs.StateDownload() - simulation_selection.SimulationSelection() drawers.FieldSelection(load_variables=self.data_load_variables) with v3.VContainer(classes="h-100 pa-0", fluid=True): @@ -273,6 +271,7 @@ def _build_ui(self, **_): pan=self.view_manager.pan, reset_camera=self.view_manager.reset_camera, ) + toolbars.SimulationControls() toolbars.ComparisonMode() toolbars.Cropping() toolbars.DataSelection() diff --git a/src/e3sm_compareview/components/simulation_selection.py b/src/e3sm_compareview/components/simulation_selection.py deleted file mode 100644 index ae6866d..0000000 --- a/src/e3sm_compareview/components/simulation_selection.py +++ /dev/null @@ -1,281 +0,0 @@ -from trame.widgets import html -from trame.widgets import vuetify3 as v3 - -from e3sm_compareview.components.drawer_utils import drawer_style -from e3sm_compareview.comparison import normalize_two_sim_target -from e3sm_quickview.utils import js - -SIMULATION_DRAG_HANDLE_EVENTS = """ -{ - dragstart: (event) => { - dragged_simulation_path = entry.path; - event.dataTransfer.effectAllowed = 'move'; - event.dataTransfer.setData('text/plain', entry.path); - - const row = event.currentTarget && event.currentTarget.closest('.simulation-entry-row'); - if (row && event.dataTransfer && event.dataTransfer.setDragImage) { - const rowRect = row.getBoundingClientRect(); - event.dataTransfer.setDragImage(row, 220, Math.round(rowRect.height / 2)); - } - }, - dragend: () => { - dragged_simulation_path = ''; - } -} -""" -SIMULATION_DROP_EVENTS = """ -{ - dragover: (event) => { - event.preventDefault(); - event.dataTransfer.dropEffect = 'move'; - }, - drop: (event) => { - event.preventDefault(); - const draggedPath = dragged_simulation_path || event.dataTransfer.getData('text/plain'); - if (!draggedPath || draggedPath === entry.path) { - dragged_simulation_path = ''; - return; - } - - const fromIndex = simulation_configs.findIndex((sim) => sim.path === draggedPath); - const toIndex = simulation_configs.findIndex((sim) => sim.path === entry.path); - if (fromIndex < 0 || toIndex < 0) { - dragged_simulation_path = ''; - return; - } - - const nextConfigs = [...simulation_configs]; - const [moved] = nextConfigs.splice(fromIndex, 1); - nextConfigs.splice(toIndex, 0, moved); - simulation_configs = nextConfigs; - dragged_simulation_path = ''; - } -} -""" - - -class SimulationSelection(v3.VNavigationDrawer): - def __init__(self): - super().__init__( - model_value=(js.is_active("select-simulations"),), - width=500, - permanent=True, - style=(drawer_style("select-simulations"),), - ) - - with self: - with html.Div(style="position:fixed;top:0;width: 500px;"): - with v3.VToolbar( - color="white", - density="compact", - classes="border-b-thin", - ): - v3.VIcon("mdi-database-cog-outline", classes="ml-4 mr-2") - v3.VLabel("Simulation selection", classes="text-subtitle-2") - v3.VSpacer() - with v3.VTooltip(): - with v3.Template(v_slot_activator="{ props }"): - html.Div( - "{{ simulation_configs.length }} loaded", - v_bind="props", - classes="text-caption mr-4", - ) - html.Div( - "{{ (() => { if (!simulation_configs.length) return 'Loaded simulations:\\nnone'; return `Loaded simulations:\\n${simulation_configs.map(sim => `${sim.label || sim.path.split('/').pop()}${sim.path === control_simulation_file ? ' (ctrl)' : ''}`).join('\\n')}`; })() }}", - style="white-space: pre-line;", - ) - with html.Div(classes="px-3 py-2 border-b-thin"): - with v3.VBtnToggle( - v_model=("comparison_mode", "multi-sim"), - mandatory=True, - density="compact", - divided=True, - classes="w-100 d-flex", - style="width: 100%;", - ): - with v3.VTooltip( - text="Compare two simulations (control/test)", - ): - with v3.Template(v_slot_activator="{ props }"): - v3.VBtn( - "Two Sim", - v_bind="props", - value="two-sim", - variant="outlined", - color=( - "comparison_mode === 'two-sim' ? 'primary' : 'default'", - ), - classes=( - "`text-none ${comparison_mode === 'two-sim' ? '' : 'text-medium-emphasis'}`", - ), - style="flex: 1 1 0;", - ) - with v3.VTooltip( - text="Compare multiple simulations against control", - ): - with v3.Template(v_slot_activator="{ props }"): - v3.VBtn( - "Multi Sim", - v_bind="props", - value="multi-sim", - variant="outlined", - color=( - "comparison_mode === 'multi-sim' ? 'primary' : 'default'", - ), - classes=( - "`text-none ${comparison_mode === 'multi-sim' ? '' : 'text-medium-emphasis'}`", - ), - style="flex: 1 1 0;", - ) - - with html.Div(v_if="simulation_configs.length === 0", classes="pa-4"): - html.Div( - "Load simulation files first, then choose the control and comparison runs here.", - classes="text-body-2 text-medium-emphasis", - ) - - with html.Div( - v_else=True, - classes="pa-2", - style="max-height: calc(100vh - 48px); overflow-y: auto;", - ): - with html.Div( - v_for="(entry, idx) in simulation_configs", - key="`${entry.path}-card`", - classes="simulation-entry-row pb-2 d-flex align-center", - v_on=SIMULATION_DROP_EVENTS, - ): - with html.Div( - draggable="true", - style="cursor: grab; user-select: none;", - classes="d-flex align-center justify-center mr-1", - v_on=SIMULATION_DRAG_HANDLE_EVENTS, - ): - with v3.VTooltip(text="Drag to reorder simulations"): - with v3.Template(v_slot_activator="{ props }"): - v3.VIcon( - "mdi-drag-vertical-variant", - v_bind="props", - size="small", - color="default", - classes="opacity-60", - ) - with v3.VCard( - variant="outlined", - classes="flex-grow-1", - ): - with v3.VCardText(classes="pa-3"): - with v3.VRow(dense=True, classes="align-center"): - with v3.VCol(cols=12, md=6): - v3.VTextField( - model_value=("entry.label",), - update_modelValue=""" -simulation_configs = simulation_configs.map((sim) => - sim.path === entry.path ? ({ ...sim, label: $event }) : sim -); -""", - label="Label", - density="compact", - variant="outlined", - hide_details=True, - ) - with v3.VCol(cols=6, md=3): - with v3.VTooltip( - text=( - "control_simulation_file === entry.path ? 'Current control simulation' : 'Set this simulation as control'", - ), - ): - with v3.Template( - v_slot_activator="{ props }" - ): - v3.VBtn( - v_bind="props", - text=( - "control_simulation_file === entry.path ? 'Control' : 'Set control'", - ), - variant="outlined", - color=( - "control_simulation_file === entry.path ? 'primary' : 'default'", - ), - classes=( - "`text-none w-100 ${control_simulation_file === entry.path ? '' : 'text-medium-emphasis'}`", - ), - style="min-width: 112px;", - size="small", - click=( - self._on_control_selected, - "[entry.path]", - ), - ) - with v3.VCol(cols=6, md=3): - with v3.Template( - v_if="comparison_mode === 'multi-sim'" - ): - with v3.VTooltip( - text="Toggle simulation inclusion", - ): - with v3.Template( - v_slot_activator="{ props }" - ): - v3.VCheckbox( - v_bind="props", - model_value=( - "control_simulation_file === entry.path ? true : entry.include", - ), - update_modelValue=""" -simulation_configs = simulation_configs.map((sim) => - sim.path === entry.path ? ({ ...sim, include: !!$event }) : sim -); -""", - label="Include", - density="compact", - hide_details=True, - disabled=( - "control_simulation_file === entry.path", - ), - ) - with v3.Template(v_else=True): - with v3.VTooltip( - text=( - "control_simulation_file === entry.path ? 'Control run cannot also be test' : (two_sim_test_simulation_file === entry.path ? 'Current test simulation' : 'Set this simulation as test')", - ), - ): - with v3.Template( - v_slot_activator="{ props }" - ): - v3.VBtn( - v_bind="props", - text=( - "two_sim_test_simulation_file === entry.path ? 'Test' : 'Set test'", - ), - variant="outlined", - color=( - "two_sim_test_simulation_file === entry.path ? 'primary' : 'default'", - ), - classes=( - "`text-none w-100 ${two_sim_test_simulation_file === entry.path ? '' : 'text-medium-emphasis'}`", - ), - style="min-width: 112px;", - size="small", - disabled=( - "control_simulation_file === entry.path", - ), - click="two_sim_test_simulation_file = entry.path", - ) - html.Div( - "{{ entry.path }}", - classes="text-caption text-medium-emphasis mt-2", - style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; direction: rtl; text-align: left;", - title=("entry.path",), - ) - - def _on_control_selected(self, control_path, **_): - self.state.control_simulation_file = control_path - if self.state.comparison_mode != "two-sim": - return - - self.state.two_sim_test_simulation_file = normalize_two_sim_target( - self.state.simulation_configs, - control_path, - self.state.two_sim_test_simulation_file, - ) diff --git a/src/e3sm_compareview/components/toolbars.py b/src/e3sm_compareview/components/toolbars.py index f48d5d4..924917d 100644 --- a/src/e3sm_compareview/components/toolbars.py +++ b/src/e3sm_compareview/components/toolbars.py @@ -5,12 +5,17 @@ from trame.widgets import client, html from trame.widgets import vuetify3 as v3 -from e3sm_compareview.comparison import COMPARISON_TYPES, COMPARISON_TYPE_LABELS +from e3sm_compareview.comparison import ( + COMPARISON_TYPES, + COMPARISON_TYPE_LABELS, + normalize_two_sim_target, +) from e3sm_quickview.components.toolbars import Layout as Layout from e3sm_quickview.utils import js DENSITY = { "adjust-layout": "compact", + "simulation-controls": "compact", "comparison-controls": "compact", "adjust-databounds": "default", "select-slice-time": "default", @@ -282,6 +287,332 @@ def __init__(self): ) +class SimulationControls(v3.VToolbar): + def __init__(self): + super().__init__(**to_kwargs("simulation-controls")) + + with self: + v3.VIcon("mdi-database-cog-outline", classes="pl-6 opacity-50") + with v3.VBtnToggle( + v_model=("comparison_mode", "multi-sim"), + mandatory=True, + density="compact", + divided=True, + classes="mx-3", + ): + v3.VBtn( + "Two Sim", + value="two-sim", + variant="outlined", + size="small", + color="default", + style=( + "`border-width: ${comparison_mode === 'two-sim' ? '2px' : '1px'}; " + "border-style: solid; " + "border-color: ${comparison_mode === 'two-sim' ? 'rgb(var(--v-theme-primary))' : 'rgba(var(--v-border-color), var(--v-border-opacity))'}; " + "background-color: ${comparison_mode === 'two-sim' ? 'rgba(var(--v-theme-primary), 0.14)' : 'transparent'}; " + "color: ${comparison_mode === 'two-sim' ? '#000000' : 'rgba(var(--v-theme-on-surface), 0.7)'};`", + ), + classes=( + "`text-none ${comparison_mode === 'two-sim' ? 'font-weight-bold' : 'text-medium-emphasis'}`", + ), + ) + v3.VBtn( + "Multi Sim", + value="multi-sim", + variant="outlined", + size="small", + color="default", + style=( + "`border-width: ${comparison_mode === 'multi-sim' ? '2px' : '1px'}; " + "border-style: solid; " + "border-color: ${comparison_mode === 'multi-sim' ? 'rgb(var(--v-theme-primary))' : 'rgba(var(--v-border-color), var(--v-border-opacity))'}; " + "background-color: ${comparison_mode === 'multi-sim' ? 'rgba(var(--v-theme-primary), 0.14)' : 'transparent'}; " + "color: ${comparison_mode === 'multi-sim' ? '#000000' : 'rgba(var(--v-theme-on-surface), 0.7)'};`", + ), + classes=( + "`text-none ${comparison_mode === 'multi-sim' ? 'font-weight-bold' : 'text-medium-emphasis'}`", + ), + ) + + v3.VDivider(vertical=True, classes="mx-2") + with v3.VSelect( + v_model=("control_simulation_file", ""), + items=("simulation_configs", []), + item_title="label", + item_value="path", + label="Set control", + chips=True, + density="compact", + variant="solo", + hide_details=True, + disabled=("simulation_configs.length === 0",), + classes="mx-1", + style="min-width: 14rem; max-width: 18rem;", + ): + with v3.Template(v_slot_selection="{ item }"): + with html.Div(classes="d-flex align-center py-1"): + v3.VChip( + "{{ item.raw.label || item.raw.path.split('/').pop() }}", + size="small", + color="primary", + variant="outlined", + ) + with v3.VSelect( + v_if="comparison_mode === 'two-sim'", + v_model=("two_sim_test_simulation_file", ""), + items=( + "simulation_configs.filter(sim => sim.path !== control_simulation_file)", + ), + item_title="label", + item_value="path", + label="Set test", + chips=True, + density="compact", + variant="solo", + hide_details=True, + disabled=( + "simulation_configs.filter(sim => sim.path !== control_simulation_file).length === 0", + ), + classes="mx-1", + style="min-width: 14rem; max-width: 18rem;", + ): + with v3.Template(v_slot_selection="{ item }"): + with html.Div(classes="d-flex align-center py-1"): + v3.VChip( + "{{ item.raw.label || item.raw.path.split('/').pop() }}", + size="small", + color="primary", + variant="outlined", + ) + + v3.VSpacer() + + v3.VBtn( + "{{ (() => { const included = simulation_configs.filter(sim => sim.path === control_simulation_file || sim.include); return `Edit simulations (${included.length}/${simulation_configs.length || 0})`; })() }}", + size="small", + variant="outlined", + classes="text-none mr-3", + prepend_icon="mdi-pencil", + click="simulation_controls_dialog = true", + ) + + with v3.VDialog( + v_model=("simulation_controls_dialog", False), + max_width=720, + scrollable=True, + ): + with v3.VCard(style="max-height: 88vh;"): + with v3.VToolbar( + color="white", + density="compact", + classes="border-b-thin", + ): + v3.VIcon("mdi-database-cog-outline", classes="ml-4 mr-2") + v3.VLabel("Simulation selection", classes="text-subtitle-2") + v3.VSpacer() + with v3.VTooltip(): + with v3.Template(v_slot_activator="{ props }"): + html.Div( + "{{ simulation_configs.length }} loaded", + v_bind="props", + classes="text-caption mr-4", + ) + html.Div( + "{{ (() => { if (!simulation_configs.length) return 'Loaded simulations:\\nnone'; return `Loaded simulations:\\n${simulation_configs.map(sim => `${sim.label || sim.path.split('/').pop()}${sim.path === control_simulation_file ? ' (ctrl)' : ''}`).join('\\n')}`; })() }}", + style="white-space: pre-line;", + ) + v3.VBtn( + icon="mdi-close", + variant="text", + size="small", + classes="mr-2", + click="simulation_controls_dialog = false", + ) + with html.Div( + v_if="simulation_configs.length === 0", classes="pa-4" + ): + html.Div( + "Load simulation files first, then choose the control and comparison runs here.", + classes="text-body-2 text-medium-emphasis", + ) + + with html.Div( + v_else=True, + classes="pa-3", + style="max-height: calc(86vh - 64px); overflow-y: auto;", + ): + with html.Div( + v_for="(entry, idx) in simulation_configs", + key="`${entry.path}-card`", + classes="simulation-entry-row pb-2 d-flex align-center", + ): + with html.Div( + classes="d-flex flex-column align-center justify-center mr-2 ga-1", + ): + with v3.VTooltip(text="Move up"): + with v3.Template(v_slot_activator="{ props }"): + v3.VBtn( + v_bind="props", + icon="mdi-chevron-up", + size="small", + variant="outlined", + density="comfortable", + color="primary", + style="min-width: 34px; width: 34px; height: 34px;", + disabled=("idx === 0",), + click=""" +if (idx > 0) { + const nextConfigs = [...simulation_configs]; + const [moved] = nextConfigs.splice(idx, 1); + nextConfigs.splice(idx - 1, 0, moved); + simulation_configs = nextConfigs; +} +""", + ) + with v3.VTooltip(text="Move down"): + with v3.Template(v_slot_activator="{ props }"): + v3.VBtn( + v_bind="props", + icon="mdi-chevron-down", + size="small", + variant="outlined", + density="comfortable", + color="primary", + style="min-width: 34px; width: 34px; height: 34px;", + disabled=( + "idx >= simulation_configs.length - 1", + ), + click=""" +if (idx < simulation_configs.length - 1) { + const nextConfigs = [...simulation_configs]; + const [moved] = nextConfigs.splice(idx, 1); + nextConfigs.splice(idx + 1, 0, moved); + simulation_configs = nextConfigs; +} +""", + ) + with v3.VCard( + variant="outlined", + classes="flex-grow-1", + ): + with v3.VCardText(classes="pa-3"): + with v3.VRow(dense=True, classes="align-center"): + with v3.VCol(cols=12, md=6): + v3.VTextField( + model_value=("entry.label",), + update_modelValue=""" +simulation_configs = simulation_configs.map((sim) => + sim.path === entry.path ? ({ ...sim, label: $event }) : sim +); +""", + label="Label", + density="compact", + variant="outlined", + hide_details=True, + ) + with v3.VCol(cols=6, md=3): + with v3.VTooltip( + text=( + "control_simulation_file === entry.path ? 'Current control simulation' : 'Set this simulation as control'", + ), + ): + with v3.Template( + v_slot_activator="{ props }" + ): + v3.VBtn( + v_bind="props", + text=( + "control_simulation_file === entry.path ? 'Control' : 'Set control'", + ), + variant="outlined", + color=( + "control_simulation_file === entry.path ? 'primary' : 'default'", + ), + classes=( + "`text-none w-100 ${control_simulation_file === entry.path ? '' : 'text-medium-emphasis'}`", + ), + style="min-width: 112px;", + size="small", + click=( + self._on_control_selected, + "[entry.path]", + ), + ) + with v3.VCol(cols=6, md=3): + with v3.Template( + v_if="comparison_mode === 'multi-sim'" + ): + with v3.VTooltip( + text="Toggle simulation inclusion", + ): + with v3.Template( + v_slot_activator="{ props }" + ): + v3.VCheckbox( + v_bind="props", + model_value=( + "control_simulation_file === entry.path ? true : entry.include", + ), + update_modelValue=""" +simulation_configs = simulation_configs.map((sim) => + sim.path === entry.path ? ({ ...sim, include: !!$event }) : sim +); +""", + label="Include", + density="compact", + hide_details=True, + disabled=( + "control_simulation_file === entry.path", + ), + ) + with v3.Template(v_else=True): + with v3.VTooltip( + text=( + "control_simulation_file === entry.path ? 'Control run cannot also be test' : (two_sim_test_simulation_file === entry.path ? 'Current test simulation' : 'Set this simulation as test')", + ), + ): + with v3.Template( + v_slot_activator="{ props }" + ): + v3.VBtn( + v_bind="props", + text=( + "two_sim_test_simulation_file === entry.path ? 'Test' : 'Set test'", + ), + variant="outlined", + color=( + "two_sim_test_simulation_file === entry.path ? 'primary' : 'default'", + ), + classes=( + "`text-none w-100 ${two_sim_test_simulation_file === entry.path ? '' : 'text-medium-emphasis'}`", + ), + style="min-width: 112px;", + size="small", + disabled=( + "control_simulation_file === entry.path", + ), + click="two_sim_test_simulation_file = entry.path", + ) + html.Div( + "{{ entry.path }}", + classes="text-caption text-medium-emphasis mt-2", + style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; direction: rtl; text-align: left;", + title=("entry.path",), + ) + + def _on_control_selected(self, control_path, **_): + self.state.control_simulation_file = control_path + if self.state.comparison_mode != "two-sim": + return + + self.state.two_sim_test_simulation_file = normalize_two_sim_target( + self.state.simulation_configs, + control_path, + self.state.two_sim_test_simulation_file, + ) + + class DataSelection(html.Div): def __init__(self): style = to_kwargs("select-slice-time") diff --git a/src/e3sm_compareview/components/tools.py b/src/e3sm_compareview/components/tools.py index d74af66..8641d50 100644 --- a/src/e3sm_compareview/components/tools.py +++ b/src/e3sm_compareview/components/tools.py @@ -108,7 +108,13 @@ def __init__(self, reset_camera=None, toggle_toolbar=None): select_strategy="independent", v_model_selected=( "active_tools", - ["load-data", "select-slice-time", "comparison-controls"], + [ + "load-data", + "adjust-layout", + "select-slice-time", + "simulation-controls", + "comparison-controls", + ], ), ): AppLogo() @@ -118,15 +124,18 @@ def __init__(self, reset_camera=None, toggle_toolbar=None): qv_tools.StateImportExport() qv_tools.OpenFile() - SimulationSelectionTool( - click=(toggle_toolbar, "['select-simulations']") - ) v3.VDivider(classes="my-1") FieldSelectionTool(click=(toggle_toolbar, "['select-fields']")) qv_tools.DataSelection() qv_tools.Animation() + qv_tools.ToggleButton( + compact="compact_drawer", + title="Simulation Controls", + icon="mdi-database-cog-outline", + value="simulation-controls", + ) qv_tools.ToggleButton( compact="compact_drawer", title="Comparison mode", From 7cff12679f627e45462b87aac3396e9cf72e70b6 Mon Sep 17 00:00:00 2001 From: willdunklin Date: Mon, 25 May 2026 14:40:48 -0400 Subject: [PATCH 3/3] fix: make view swap use view label as name --- src/e3sm_compareview/view_manager.py | 39 ++++++++++++++++++---------- src/e3sm_compareview/view_panel.py | 10 +++---- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/e3sm_compareview/view_manager.py b/src/e3sm_compareview/view_manager.py index 928527f..dbbd29a 100644 --- a/src/e3sm_compareview/view_manager.py +++ b/src/e3sm_compareview/view_manager.py @@ -535,18 +535,25 @@ def build_auto_layout(self, variables=None): group_cols = max( 1, math.floor(12 / views_per_row) ) - group_names = [ - view_spec["array_name"] + group_swap_items = [ + { + "name": view_spec["array_name"], + "label": view_spec.get( + "label", view_spec["array_name"] + ), + } for view_spec in view_specs ] for view_spec in view_specs: view = self.get_view(view_spec, var_type) view.config.swap_group = sorted( [ - name - for name in group_names - if name != view_spec["array_name"] - ] + item + for item in group_swap_items + if item["name"] + != view_spec["array_name"] + ], + key=lambda item: item["name"], ) with view.config.provide_as("config"): v3.VCol( @@ -575,12 +582,17 @@ def build_auto_layout(self, variables=None): ): client.ServerTemplate(name=view.name) else: - all_names = [] + all_swap_items = [] for var_name_list in variables.values(): for var_name in var_name_list: - all_names.extend( + all_swap_items.extend( [ - view_spec["array_name"] + { + "name": view_spec["array_name"], + "label": view_spec.get( + "label", view_spec["array_name"] + ), + } for view_spec in self.get_view_specs(var_name) ] ) @@ -591,10 +603,11 @@ def build_auto_layout(self, variables=None): view = self.get_view(view_spec, var_type) view.config.swap_group = sorted( [ - array_name - for array_name in all_names - if array_name != view_spec["array_name"] - ] + item + for item in all_swap_items + if item["name"] != view_spec["array_name"] + ], + key=lambda item: item["name"], ) with view.config.provide_as("config"): v3.VCol( diff --git a/src/e3sm_compareview/view_panel.py b/src/e3sm_compareview/view_panel.py index 93bc58e..afc0c1e 100644 --- a/src/e3sm_compareview/view_panel.py +++ b/src/e3sm_compareview/view_panel.py @@ -22,7 +22,7 @@ class ViewConfiguration(dataclass.StateDataModel): size: int = dataclass.Sync(int, 4) offset: int = dataclass.Sync(int, 0) break_row: bool = dataclass.Sync(bool, False) - swap_group: list[str] = dataclass.Sync(list[str], list) + swap_group: list[dict[str, str]] = dataclass.Sync(list[dict[str, str]], list) class VariableView(TrameComponent): @@ -251,12 +251,12 @@ def _build_ui(self): ): with self.config.provide_as("config"): v3.VListItem( - subtitle=("name",), - v_for="name, idx in config.swap_group", - key="name", + title=("swap.label || swap.name",), + v_for="swap, idx in config.swap_group", + key="swap.name", click=( self.ctrl.swap_variables, - "[config.variable, name]", + "[config.variable, swap.name]", ), )