redesign model and variable selection

This commit is contained in:
terraputix
2026-08-02 22:35:36 +02:00
parent ea9eb17d75
commit 77af339df4
7 changed files with 358 additions and 98 deletions
@@ -18,7 +18,6 @@
type ExportableChart,
type ExportLegendItem
} from '$lib/components/charts';
import { Checkbox } from '$lib/components/ui/checkbox';
import { Label } from '$lib/components/ui/label';
import { Switch } from '$lib/components/ui/switch';
@@ -41,6 +40,7 @@
import { useHeroActions } from '../../hero.svelte';
import { defaultParameters, hourly, modelGroups, models as modelOptions } from '../../options';
import ComparisonSelectionDrawer from './ComparisonSelectionDrawer.svelte';
import ModelPictogramTimeline from './ModelPictogramTimeline.svelte';
import {
cardinalDirection,
@@ -67,6 +67,9 @@
group.filter((variable) => variable.value !== 'cloud_cover')
);
const VARIABLE_IDS = new Set(COMPARISON_HOURLY.flat().map((variable) => variable.value));
const VARIABLE_ORDER = new Map(
COMPARISON_HOURLY.flat().map((variable, index) => [variable.value, index])
);
const SECONDS_PER_HOUR = 3600;
const FETCH_DEBOUNCE_MS = 300;
const STANDARD_COMPARE_VARIABLES = [
@@ -95,8 +98,7 @@
let isMobile = $state(false);
let comparisonPlotInsetLeft = $derived(isMobile ? 92 : 116);
let comparisonPlotInsetRight = $derived(isMobile ? 8 : 20);
let modelsOpen = $state(false);
let customVariablesOpen = $state(false);
let selectionDrawerMode = $state<'models' | 'variables' | null>(null);
let params = $state({
...defaultParameters,
@@ -138,6 +140,14 @@
);
}
function orderVariables(values: string[]): string[] {
return [...values].sort(
(left, right) =>
(VARIABLE_ORDER.get(left) ?? Number.MAX_SAFE_INTEGER) -
(VARIABLE_ORDER.get(right) ?? Number.MAX_SAFE_INTEGER)
);
}
let variablesDirty = $derived(!hasSameVariables(params.hourly, appliedHourly));
let modelsDirty = $derived(!hasSameOrder(params.models, appliedModels));
let displayedVariablesStale = $derived(
@@ -501,7 +511,19 @@
const nextValues = values.includes(value)
? values.filter((item) => item !== value)
: [...values, value];
params[list] = list === 'models' ? orderModels(nextValues) : nextValues;
params[list] = list === 'models' ? orderModels(nextValues) : orderVariables(nextValues);
}
function toggleGroupSelection(
list: 'models' | 'hourly',
values: string[],
select: boolean
): void {
const groupValues = new Set(values);
const nextValues = select
? [...new Set([...params[list], ...values])]
: params[list].filter((value) => !groupValues.has(value));
params[list] = list === 'models' ? orderModels(nextValues) : orderVariables(nextValues);
}
function restoreStandardVariables(): void {
@@ -535,10 +557,15 @@
return model ? [model] : [];
})
);
let modelSelectionGroups = $derived(
let modelDrawerGroups = $derived(
modelGroups.map((group) => ({
...group,
selectedCount: group.models.filter((model) => params.models.includes(model.value)).length
value: group.value,
label: group.label,
options: group.models.map((model) => ({
value: model.value,
label: model.label,
metadata: [model.resolution, model.update].filter(Boolean).join(' · ')
}))
}))
);
let selectedVariables = $derived(
@@ -551,11 +578,14 @@
params.hourly.length === STANDARD_COMPARE_VARIABLES.length &&
STANDARD_COMPARE_VARIABLES.every((variable) => params.hourly.includes(variable))
);
let variableSelectionGroups = $derived(
let variableDrawerGroups = $derived(
COMPARISON_HOURLY.map((variables, index) => ({
label: variableGroupLabels[index],
variables,
selectedCount: variables.filter((variable) => params.hourly.includes(variable.value)).length
value: `variables_${index}`,
label: variableGroupLabels[index](),
options: variables.map((variable) => ({
value: variable.value,
label: variableLabel(variable.value)
}))
}))
);
</script>
@@ -812,7 +842,7 @@
</div>
{/if}
<!-- Compact ordered selection summaries with expandable model/variable pickers. -->
<!-- Compact summaries keep the page scannable; full editing happens in the shared drawer. -->
<section class="mt-6 border-t border-border/60 pt-5 md:mt-10" aria-labelledby="models">
<div class="flex items-baseline justify-between gap-3">
<h2 id="models" class="text-2xl md:text-3xl">{m.compare_models_heading()}</h2>
@@ -870,11 +900,10 @@
<button
type="button"
class="min-h-9 rounded-lg border border-border px-3 py-1.5 text-xs font-semibold hover:bg-muted"
onclick={() => (modelsOpen = !modelsOpen)}
aria-expanded={modelsOpen}
aria-controls="model-options"
onclick={() => (selectionDrawerMode = 'models')}
aria-haspopup="dialog"
>
{modelsOpen ? m.action_close() : m.compare_customize_models()}
{m.compare_customize_models()}
</button>
{#if modelsDirty}
<button
@@ -887,47 +916,6 @@
{/if}
</div>
</div>
{#if modelsOpen}
<div id="model-options" class="mt-3 border-t border-border/60 pt-3">
<div class="grid items-start gap-2 sm:grid-cols-2 lg:grid-cols-3">
{#each modelSelectionGroups as group (group.value)}
<details class="rounded-lg border border-border/60 bg-background/40">
<summary
class="flex min-h-10 cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 text-sm font-bold marker:content-none"
>
<span>{group.label}</span>
<span class="flex items-center gap-2 text-xs font-normal text-muted-foreground">
<span>{group.selectedCount}/{group.models.length}</span>
<span aria-hidden="true"></span>
</span>
</summary>
<div class="border-t border-border/50 p-2">
{#each group.models as model (model.value)}
<div class="flex min-h-9 items-center rounded-md px-1 hover:bg-muted/60">
<Checkbox
id="{model.value}_model"
value={model.value}
checked={params.models.includes(model.value)}
onCheckedChange={() => toggleSelection('models', model.value)}
/>
<Label
for="{model.value}_model"
class="min-w-0 flex-1 cursor-pointer py-1.5 pl-2 text-xs"
>
<span class="font-medium">{model.label}</span>
<span class="text-muted-foreground">
· {model.resolution} · {model.update}
</span>
</Label>
</div>
{/each}
</div>
</details>
{/each}
</div>
</div>
{/if}
</div>
</section>
@@ -999,11 +987,10 @@
<button
type="button"
class="min-h-9 rounded-lg border border-border px-3 py-1.5 text-xs font-semibold hover:bg-muted"
onclick={() => (customVariablesOpen = !customVariablesOpen)}
aria-expanded={customVariablesOpen}
aria-controls="variable-options"
onclick={() => (selectionDrawerMode = 'variables')}
aria-haspopup="dialog"
>
{customVariablesOpen ? m.action_close() : m.compare_customize_variables()}
{m.compare_customize_variables()}
</button>
{#if variablesDirty}
<button
@@ -1016,42 +1003,32 @@
{/if}
</div>
</div>
{#if customVariablesOpen}
<div id="variable-options" class="border-t border-border/60 p-3 sm:p-4">
<div class="grid items-start gap-2 sm:grid-cols-2 lg:grid-cols-3">
{#each variableSelectionGroups as variableGroup (variableGroup.label())}
<details class="rounded-lg border border-border/60 bg-background/40">
<summary
class="flex min-h-10 cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 text-sm font-bold marker:content-none"
>
<span>{variableGroup.label()}</span>
<span class="flex items-center gap-2 text-xs font-normal text-muted-foreground">
<span>{variableGroup.selectedCount}/{variableGroup.variables.length}</span>
<span aria-hidden="true"></span>
</span>
</summary>
<div class="border-t border-border/50 p-2">
{#each variableGroup.variables as variable (variable.value)}
<div class="flex min-h-10 items-center rounded-md px-1 hover:bg-muted/60">
<Checkbox
id="{variable.value}_hourly"
checked={params.hourly.includes(variable.value)}
onCheckedChange={() => toggleSelection('hourly', variable.value)}
/>
<Label
for="{variable.value}_hourly"
class="min-w-0 flex-1 cursor-pointer py-2 pl-2 text-sm"
>
{variableLabel(variable.value)}
</Label>
</div>
{/each}
</div>
</details>
{/each}
</div>
</div>
{/if}
</div>
</section>
{#if selectionDrawerMode === 'models'}
<ComparisonSelectionDrawer
open
mode="models"
groups={modelDrawerGroups}
selected={params.models}
dirty={comparisonSelectionDirty}
onToggle={(value) => toggleSelection('models', value)}
onToggleGroup={(values, select) => toggleGroupSelection('models', values, select)}
onApply={applyComparisonSelection}
onClose={() => (selectionDrawerMode = null)}
/>
{:else if selectionDrawerMode === 'variables'}
<ComparisonSelectionDrawer
open
mode="variables"
groups={variableDrawerGroups}
selected={params.hourly}
dirty={comparisonSelectionDirty}
onToggle={(value) => toggleSelection('hourly', value)}
onToggleGroup={(values, select) => toggleGroupSelection('hourly', values, select)}
onApply={applyComparisonSelection}
onClose={() => (selectionDrawerMode = null)}
onRestoreDefaults={restoreStandardVariables}
/>
{/if}