redesign model and variable selection
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user