Skip to content

Commit 97b07c6

Browse files
committed
feat: swap variable groups to reorder
1 parent 9e87e18 commit 97b07c6

2 files changed

Lines changed: 131 additions & 156 deletions

File tree

src/e3sm_compareview/app.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,6 @@ def _build_ui(self, **_):
194194
ToolbarAnimation=(self.toggle_toolbar, "['animation-controls']"),
195195
ToggleVariableSelection=(self.toggle_toolbar, "['select-fields']"),
196196
RemoveAllToolbars=(self.toggle_toolbar),
197-
ToggleGroups="layout_grouped = !layout_grouped",
198197
ProjectionEquidistant="projection = ['Cyl. Equidistant']",
199198
ProjectionRobinson="projection = ['Robinson']",
200199
ProjectionMollweide="projection = ['Mollweide']",
@@ -232,8 +231,6 @@ def _build_ui(self, **_):
232231
mt.bind("l", "ToolbarCrop")
233232
mt.bind("s", "ToolbarSelect")
234233
mt.bind("a", "ToolbarAnimation")
235-
mt.bind("g", "ToggleGroups")
236-
237234
mt.bind("v", "ToggleVariableSelection")
238235

239236
mt.bind("space", "ToggleViewLock", stop_propagation=True)
@@ -462,7 +459,6 @@ def download_state(self):
462459
state_content["variables-selection"] = self.state.variables_selected
463460
state_content["layout"] = {
464461
"aspect-ratio": self.state.aspect_ratio,
465-
"grouped": self.state.layout_grouped,
466462
"active": self.state.active_layout,
467463
"tools": self.state.active_tools,
468464
"help": not self.state.compact_drawer,
@@ -610,7 +606,6 @@ async def _import_state(self, state_content):
610606

611607
# Update layout
612608
self.state.aspect_ratio = state_content["layout"]["aspect-ratio"]
613-
self.state.layout_grouped = state_content["layout"]["grouped"]
614609
self.state.active_layout = state_content["layout"]["active"]
615610
self.state.active_tools = state_content["layout"]["tools"]
616611
self.state.compact_drawer = not state_content["layout"]["help"]
@@ -784,10 +779,6 @@ async def _data_load_variables(self):
784779
self.state.loading = False
785780
self.state.loading_time = t1 - t0
786781

787-
@change("layout_grouped")
788-
def _on_layout_change(self, **_):
789-
self._rebuild_active_layout()
790-
791782
@change("comparison_type")
792783
def _on_comparison_type_change(self, **_):
793784
if self.state.comparison_mode != "multi-sim" or not self.state.variables_loaded:

src/e3sm_compareview/view_manager.py

Lines changed: 131 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def __init__(self, server, source):
9090
self._var2view = {}
9191
self._last_vars = {}
9292
self._active_configs = {}
93+
self._group_orders = {}
9394

9495
rca.initialize(self.server)
9596
colormaps.initialize(self.server)
@@ -402,8 +403,7 @@ def swap_pair(array_name_a, array_name_b):
402403

403404
metadata_a = self.source.data_reader.get_array_metadata(variable_a) or {}
404405
metadata_b = self.source.data_reader.get_array_metadata(variable_b) or {}
405-
grouped_layout = self.state.layout_grouped
406-
if grouped_layout and self.state.comparison_mode == "multi-sim":
406+
if self.state.comparison_mode == "multi-sim":
407407
path_a = metadata_a.get("path")
408408
path_b = metadata_b.get("path")
409409

@@ -426,10 +426,6 @@ def swap_pair(array_name_a, array_name_b):
426426
self.state.simulation_configs = simulation_configs
427427
return
428428

429-
if not grouped_layout:
430-
swap_pair(variable_a, variable_b)
431-
return
432-
433429
base_variable_a = metadata_a.get("base_variable")
434430
base_variable_b = metadata_b.get("base_variable")
435431
slot_a = None
@@ -466,25 +462,37 @@ def swap_pair(array_name_a, array_name_b):
466462
view_specs[slot_b]["array_name"],
467463
)
468464

465+
@controller.set("swap_variable_groups")
466+
def swap_variable_groups(self, variable_a, variable_b):
467+
if not variable_a or not variable_b or variable_a == variable_b:
468+
return
469+
470+
if variable_a not in self._group_orders or variable_b not in self._group_orders:
471+
return
472+
473+
self._group_orders[variable_a], self._group_orders[variable_b] = (
474+
self._group_orders[variable_b],
475+
self._group_orders[variable_a],
476+
)
477+
478+
if self._last_vars:
479+
self.build_auto_layout(self._last_vars)
480+
self.render()
481+
469482
def apply_size(self, n_cols):
470483
if not self._last_vars:
471484
return
472485

473486
if n_cols == 0:
474487
# Auto size views based on the number of comparison panels being shown.
475-
if self.state.layout_grouped:
476-
for var_type, var_names in self._last_vars.items():
477-
for var_name in var_names:
478-
view_specs = self.get_view_specs(var_name)
479-
if not view_specs:
480-
continue
481-
size = auto_size_to_col(len(view_specs))
482-
for view_spec in view_specs:
483-
self.get_view(view_spec, var_type).config.size = size
484-
else:
485-
size = auto_size_to_col(len(self._active_configs))
486-
for config in self._active_configs.values():
487-
config.size = size
488+
for var_type, var_names in self._last_vars.items():
489+
for var_name in var_names:
490+
view_specs = self.get_view_specs(var_name)
491+
if not view_specs:
492+
continue
493+
size = auto_size_to_col(len(view_specs))
494+
for view_spec in view_specs:
495+
self.get_view(view_spec, var_type).config.size = size
488496
else:
489497
# Apply a uniform size to all active views.
490498
for config in self._active_configs.values():
@@ -505,113 +513,89 @@ def build_auto_layout(self, variables=None):
505513
export_items = []
506514
# Build a lookup from variable type to the matching group border color.
507515
type_to_color = {vt["name"]: vt["color"] for vt in self.state.variable_types}
516+
flat_vars = []
517+
for var_type, var_names in variables.items():
518+
for var_name in var_names:
519+
view_specs = self.get_view_specs(var_name)
520+
if not view_specs:
521+
continue
522+
flat_vars.append((var_type, var_name, view_specs, self._group_orders.get(var_name)))
523+
524+
current_group_order = {var_name: saved for _, var_name, _, saved in flat_vars if saved is not None}
525+
next_group_order_idx = (max(current_group_order.values()) + 1) if current_group_order else 1
526+
for _, var_name, _, saved in flat_vars:
527+
if saved is None:
528+
current_group_order[var_name] = next_group_order_idx
529+
next_group_order_idx += 1
530+
531+
self._group_orders = current_group_order
532+
533+
grouped_entries = sorted(
534+
(
535+
(current_group_order[var_name], var_type, var_name, view_specs)
536+
for var_type, var_name, view_specs, _ in flat_vars
537+
),
538+
key=lambda item: item[0],
539+
)
540+
508541
with DivLayout(self.server, template_name="auto_layout") as self.ui:
509-
if self.state.layout_grouped:
510-
with v3.VCol(classes="pa-1"):
511-
for var_type, var_names in variables.items():
512-
for var_name in var_names:
513-
view_specs = self.get_view_specs(var_name)
514-
if not view_specs:
515-
continue
516-
517-
type_name = (
518-
", ".join(var_type)
519-
if isinstance(var_type, (list, tuple))
520-
else str(var_type)
521-
)
522-
border_color = type_to_color.get(type_name, "primary")
523-
with v3.VAlert(
524-
border="start",
525-
classes="pr-1 py-1 pl-3 mb-6",
526-
variant="flat",
527-
border_color=border_color,
528-
):
529-
html.Div(
530-
var_name,
531-
classes="text-subtitle-2 font-weight-medium mb-1",
532-
)
533-
with v3.VRow(dense=True):
534-
use_config_size = (
535-
self.state.comparison_mode == "multi-sim"
536-
)
537-
if not use_config_size:
538-
views_per_row = max(1, len(view_specs))
539-
group_cols = max(
540-
1, math.floor(12 / views_per_row)
541-
)
542-
group_swap_items = [
543-
{
544-
"name": view_spec["array_name"],
545-
"label": view_spec.get(
546-
"label", view_spec["array_name"]
542+
group_names = [var_name for _, _, var_name, _ in grouped_entries]
543+
544+
with v3.VCol(classes="pa-1"):
545+
for _, var_type, var_name, view_specs in grouped_entries:
546+
type_name = (
547+
", ".join(var_type)
548+
if isinstance(var_type, (list, tuple))
549+
else str(var_type)
550+
)
551+
border_color = type_to_color.get(type_name, "primary")
552+
with v3.VAlert(
553+
border="start",
554+
classes="pr-1 py-1 pl-3 mb-6",
555+
variant="flat",
556+
border_color=border_color,
557+
key=f"group-{var_name}",
558+
):
559+
with html.Div(
560+
var_name,
561+
classes=(
562+
"text-subtitle-2 "
563+
"font-weight-medium mb-1 d-inline-block"
564+
),
565+
style="user-select: none; cursor: pointer;",
566+
):
567+
with v3.VMenu(activator="parent"):
568+
with v3.VList(
569+
density="compact",
570+
style="max-height: 40vh;",
571+
):
572+
for swap_name in group_names:
573+
if swap_name == var_name:
574+
continue
575+
v3.VListItem(
576+
title=swap_name,
577+
click=(
578+
self.ctrl.swap_variable_groups,
579+
f"['{var_name}', '{swap_name}']",
547580
),
548-
}
549-
for view_spec in view_specs
550-
]
551-
for view_spec in view_specs:
552-
view = self.get_view(view_spec, var_type)
553-
export_items.append(
554-
{
555-
"title": view_spec.get(
556-
"label", view_spec["array_name"]
557-
),
558-
"value": view_spec["array_name"],
559-
}
560581
)
561-
view.config.swap_group = sorted(
562-
[
563-
item
564-
for item in group_swap_items
565-
if item["name"]
566-
!= view_spec["array_name"]
567-
],
568-
key=lambda item: item["name"],
569-
)
570-
with view.config.provide_as("config"):
571-
v3.VCol(
572-
v_if="config.break_row",
573-
cols=12,
574-
classes="pa-0",
575-
style=("`order: ${config.order};`",),
576-
)
577-
# For flow handling
578-
with v3.Template(v_if="!config.size"):
579-
v3.VCol(
580-
v_for="i in config.offset",
581-
key="i",
582-
style=("{ order: config.order }",),
583-
)
584-
with v3.VCol(
585-
offset=(
586-
"config.size ? config.offset * config.size : 0",
587-
)
588-
if use_config_size
589-
else ("config.offset * config.size",),
590-
cols=("config.size",)
591-
if use_config_size
592-
else group_cols,
593-
style=("`order: ${config.order};`",),
594-
):
595-
client.ServerTemplate(name=view.name)
596-
else:
597-
all_swap_items = []
598-
for var_name_list in variables.values():
599-
for var_name in var_name_list:
600-
all_swap_items.extend(
601-
[
582+
with v3.VRow(dense=True):
583+
use_config_size = (
584+
self.state.comparison_mode == "multi-sim"
585+
)
586+
if not use_config_size:
587+
views_per_row = max(1, len(view_specs))
588+
group_cols = max(
589+
1, math.floor(12 / views_per_row)
590+
)
591+
panel_options = [
602592
{
603-
"name": view_spec["array_name"],
604-
"label": view_spec.get(
605-
"label", view_spec["array_name"]
606-
),
593+
"name": vs["array_name"],
594+
"label": vs.get("label", vs["array_name"]),
607595
}
608-
for view_spec in self.get_view_specs(var_name)
596+
for vs in view_specs
609597
]
610-
)
611-
with v3.VRow(dense=True, classes="pa-2"):
612-
for var_type, var_names in variables.items():
613-
for name in var_names:
614-
for view_spec in self.get_view_specs(name):
598+
for view_spec in view_specs:
615599
view = self.get_view(view_spec, var_type)
616600
export_items.append(
617601
{
@@ -624,7 +608,7 @@ def build_auto_layout(self, variables=None):
624608
view.config.swap_group = sorted(
625609
[
626610
item
627-
for item in all_swap_items
611+
for item in panel_options
628612
if item["name"] != view_spec["array_name"]
629613
],
630614
key=lambda item: item["name"],
@@ -636,7 +620,6 @@ def build_auto_layout(self, variables=None):
636620
classes="pa-0",
637621
style=("`order: ${config.order};`",),
638622
)
639-
640623
# For flow handling
641624
with v3.Template(v_if="!config.size"):
642625
v3.VCol(
@@ -647,39 +630,40 @@ def build_auto_layout(self, variables=None):
647630
with v3.VCol(
648631
offset=(
649632
"config.size ? config.offset * config.size : 0",
650-
),
651-
cols=("config.size",),
633+
)
634+
if use_config_size
635+
else ("config.offset * config.size",),
636+
cols=("config.size",)
637+
if use_config_size
638+
else group_cols,
652639
style=("`order: ${config.order};`",),
640+
key=view_spec["array_name"],
653641
):
654642
client.ServerTemplate(name=view.name)
655643

656644
self.state.animation_export_items = export_items
657645

658-
# Assign any missing order.
659646
self._active_configs = {}
660-
existed_order = set()
661-
order_max = 0
662-
orders_to_update = []
663-
for var_type, var_names in variables.items():
664-
for var_name in var_names:
665-
for view_spec in self.get_view_specs(var_name):
666-
config = self.get_view(view_spec, var_type).config
667-
name = view_spec["array_name"]
668-
self._active_configs[name] = config
669-
if config.order:
670-
if config.order in existed_order:
671-
config.order = 0
672-
orders_to_update.append(config)
673-
continue
674-
order_max = max(order_max, config.order)
675-
existed_order.add(config.order)
676-
else:
677-
orders_to_update.append(config)
647+
next_order_idx = 1
648+
for _, var_type, _, view_specs in grouped_entries:
649+
view_items = [
650+
(index, view_spec, self.get_view(view_spec, var_type))
651+
for index, view_spec in enumerate(view_specs)
652+
]
653+
if self.state.comparison_mode != "multi-sim":
654+
view_items = sorted(
655+
view_items,
656+
key=lambda item: (
657+
item[2].config.order or len(view_specs) + item[0],
658+
item[0],
659+
),
660+
)
678661

679-
next_order = order_max + 1
680-
for config in orders_to_update:
681-
config.order = next_order
682-
next_order += 1
662+
for _, view_spec, view in view_items:
663+
config = view.config
664+
config.order = next_order_idx
665+
self._active_configs[view_spec["array_name"]] = config
666+
next_order_idx += 1
683667

684668
self.layout_dirty = True
685669
self.compute_layout()

0 commit comments

Comments
 (0)