feat: multimodel improvements #9
@@ -293,6 +293,7 @@
|
|||||||
"compare_selected_models": "Ausgewählte Modelle",
|
"compare_selected_models": "Ausgewählte Modelle",
|
||||||
"compare_selection_pending": "Auswahl geändert",
|
"compare_selection_pending": "Auswahl geändert",
|
||||||
"compare_apply_selection": "Anwenden & Diagramme neu laden",
|
"compare_apply_selection": "Anwenden & Diagramme neu laden",
|
||||||
|
"compare_discard_changes": "Änderungen verwerfen",
|
||||||
"compare_customize_models": "Modelle anpassen",
|
"compare_customize_models": "Modelle anpassen",
|
||||||
"compare_edit_models": "Modelle bearbeiten",
|
"compare_edit_models": "Modelle bearbeiten",
|
||||||
"compare_edit_variables": "Variablen bearbeiten",
|
"compare_edit_variables": "Variablen bearbeiten",
|
||||||
@@ -300,6 +301,8 @@
|
|||||||
"compare_edit_variables_description": "Variablen suchen oder ganze Themengruppen auswählen.",
|
"compare_edit_variables_description": "Variablen suchen oder ganze Themengruppen auswählen.",
|
||||||
"compare_search_models": "Modelle suchen",
|
"compare_search_models": "Modelle suchen",
|
||||||
"compare_search_variables": "Variablen suchen",
|
"compare_search_variables": "Variablen suchen",
|
||||||
|
"compare_show_model_names": "Modellnamen anzeigen",
|
||||||
|
"compare_hide_model_names": "Modellnamen ausblenden",
|
||||||
"compare_only_selected": "Nur ausgewählte",
|
"compare_only_selected": "Nur ausgewählte",
|
||||||
"compare_select_group": "Alle auswählen",
|
"compare_select_group": "Alle auswählen",
|
||||||
"compare_clear_group": "Alle abwählen",
|
"compare_clear_group": "Alle abwählen",
|
||||||
|
|||||||
@@ -293,6 +293,7 @@
|
|||||||
"compare_selected_models": "Selected models",
|
"compare_selected_models": "Selected models",
|
||||||
"compare_selection_pending": "Selection changed",
|
"compare_selection_pending": "Selection changed",
|
||||||
"compare_apply_selection": "Apply & reload charts",
|
"compare_apply_selection": "Apply & reload charts",
|
||||||
|
"compare_discard_changes": "Discard changes",
|
||||||
"compare_customize_models": "Customize models",
|
"compare_customize_models": "Customize models",
|
||||||
"compare_edit_models": "Edit models",
|
"compare_edit_models": "Edit models",
|
||||||
"compare_edit_variables": "Edit variables",
|
"compare_edit_variables": "Edit variables",
|
||||||
@@ -300,6 +301,8 @@
|
|||||||
"compare_edit_variables_description": "Search variables or select complete thematic groups.",
|
"compare_edit_variables_description": "Search variables or select complete thematic groups.",
|
||||||
"compare_search_models": "Search models",
|
"compare_search_models": "Search models",
|
||||||
"compare_search_variables": "Search variables",
|
"compare_search_variables": "Search variables",
|
||||||
|
"compare_show_model_names": "Show model names",
|
||||||
|
"compare_hide_model_names": "Hide model names",
|
||||||
"compare_only_selected": "Only selected",
|
"compare_only_selected": "Only selected",
|
||||||
"compare_select_group": "Select all",
|
"compare_select_group": "Select all",
|
||||||
"compare_clear_group": "Clear all",
|
"compare_clear_group": "Clear all",
|
||||||
|
|||||||
@@ -293,6 +293,7 @@
|
|||||||
"compare_selected_models": "Modelos seleccionados",
|
"compare_selected_models": "Modelos seleccionados",
|
||||||
"compare_selection_pending": "Selección modificada",
|
"compare_selection_pending": "Selección modificada",
|
||||||
"compare_apply_selection": "Aplicar y recargar gráficos",
|
"compare_apply_selection": "Aplicar y recargar gráficos",
|
||||||
|
"compare_discard_changes": "Descartar cambios",
|
||||||
"compare_customize_models": "Personalizar modelos",
|
"compare_customize_models": "Personalizar modelos",
|
||||||
"compare_edit_models": "Editar modelos",
|
"compare_edit_models": "Editar modelos",
|
||||||
"compare_edit_variables": "Editar variables",
|
"compare_edit_variables": "Editar variables",
|
||||||
@@ -300,6 +301,8 @@
|
|||||||
"compare_edit_variables_description": "Busca variables o selecciona grupos temáticos completos.",
|
"compare_edit_variables_description": "Busca variables o selecciona grupos temáticos completos.",
|
||||||
"compare_search_models": "Buscar modelos",
|
"compare_search_models": "Buscar modelos",
|
||||||
"compare_search_variables": "Buscar variables",
|
"compare_search_variables": "Buscar variables",
|
||||||
|
"compare_show_model_names": "Mostrar nombres de modelos",
|
||||||
|
"compare_hide_model_names": "Ocultar nombres de modelos",
|
||||||
"compare_only_selected": "Solo seleccionados",
|
"compare_only_selected": "Solo seleccionados",
|
||||||
"compare_select_group": "Seleccionar todo",
|
"compare_select_group": "Seleccionar todo",
|
||||||
"compare_clear_group": "Deseleccionar todo",
|
"compare_clear_group": "Deseleccionar todo",
|
||||||
|
|||||||
@@ -293,6 +293,7 @@
|
|||||||
"compare_selected_models": "Modèles sélectionnés",
|
"compare_selected_models": "Modèles sélectionnés",
|
||||||
"compare_selection_pending": "Sélection modifiée",
|
"compare_selection_pending": "Sélection modifiée",
|
||||||
"compare_apply_selection": "Appliquer et recharger les graphiques",
|
"compare_apply_selection": "Appliquer et recharger les graphiques",
|
||||||
|
"compare_discard_changes": "Annuler les modifications",
|
||||||
"compare_customize_models": "Personnaliser les modèles",
|
"compare_customize_models": "Personnaliser les modèles",
|
||||||
"compare_edit_models": "Modifier les modèles",
|
"compare_edit_models": "Modifier les modèles",
|
||||||
"compare_edit_variables": "Modifier les variables",
|
"compare_edit_variables": "Modifier les variables",
|
||||||
@@ -300,6 +301,8 @@
|
|||||||
"compare_edit_variables_description": "Recherchez des variables ou sélectionnez des groupes thématiques entiers.",
|
"compare_edit_variables_description": "Recherchez des variables ou sélectionnez des groupes thématiques entiers.",
|
||||||
"compare_search_models": "Rechercher des modèles",
|
"compare_search_models": "Rechercher des modèles",
|
||||||
"compare_search_variables": "Rechercher des variables",
|
"compare_search_variables": "Rechercher des variables",
|
||||||
|
"compare_show_model_names": "Afficher les noms des modèles",
|
||||||
|
"compare_hide_model_names": "Masquer les noms des modèles",
|
||||||
"compare_only_selected": "Sélection uniquement",
|
"compare_only_selected": "Sélection uniquement",
|
||||||
"compare_select_group": "Tout sélectionner",
|
"compare_select_group": "Tout sélectionner",
|
||||||
"compare_clear_group": "Tout désélectionner",
|
"compare_clear_group": "Tout désélectionner",
|
||||||
|
|||||||
@@ -293,6 +293,7 @@
|
|||||||
"compare_selected_models": "Modelli selezionati",
|
"compare_selected_models": "Modelli selezionati",
|
||||||
"compare_selection_pending": "Selezione modificata",
|
"compare_selection_pending": "Selezione modificata",
|
||||||
"compare_apply_selection": "Applica e ricarica i grafici",
|
"compare_apply_selection": "Applica e ricarica i grafici",
|
||||||
|
"compare_discard_changes": "Annulla modifiche",
|
||||||
"compare_customize_models": "Personalizza modelli",
|
"compare_customize_models": "Personalizza modelli",
|
||||||
"compare_edit_models": "Modifica modelli",
|
"compare_edit_models": "Modifica modelli",
|
||||||
"compare_edit_variables": "Modifica variabili",
|
"compare_edit_variables": "Modifica variabili",
|
||||||
@@ -300,6 +301,8 @@
|
|||||||
"compare_edit_variables_description": "Cerca variabili o seleziona interi gruppi tematici.",
|
"compare_edit_variables_description": "Cerca variabili o seleziona interi gruppi tematici.",
|
||||||
"compare_search_models": "Cerca modelli",
|
"compare_search_models": "Cerca modelli",
|
||||||
"compare_search_variables": "Cerca variabili",
|
"compare_search_variables": "Cerca variabili",
|
||||||
|
"compare_show_model_names": "Mostra nomi dei modelli",
|
||||||
|
"compare_hide_model_names": "Nascondi nomi dei modelli",
|
||||||
"compare_only_selected": "Solo selezionati",
|
"compare_only_selected": "Solo selezionati",
|
||||||
"compare_select_group": "Seleziona tutto",
|
"compare_select_group": "Seleziona tutto",
|
||||||
"compare_clear_group": "Deseleziona tutto",
|
"compare_clear_group": "Deseleziona tutto",
|
||||||
|
|||||||
@@ -209,6 +209,8 @@
|
|||||||
yTicks?: number[];
|
yTicks?: number[];
|
||||||
/** Custom left-axis tick formatter. */
|
/** Custom left-axis tick formatter. */
|
||||||
yTickFormat?: (value: number) => string;
|
yTickFormat?: (value: number) => string;
|
||||||
|
/** Use abbreviated y-axis labels and omit verbose gutter labels. */
|
||||||
|
compactYAxis?: boolean;
|
||||||
/** Force the derived left axis to include zero (default true) */
|
/** Force the derived left axis to include zero (default true) */
|
||||||
zeroBaseLeft?: boolean;
|
zeroBaseLeft?: boolean;
|
||||||
/** Fixed right-axis minimum (default 0) */
|
/** Fixed right-axis minimum (default 0) */
|
||||||
@@ -260,6 +262,7 @@
|
|||||||
yMax,
|
yMax,
|
||||||
yTicks,
|
yTicks,
|
||||||
yTickFormat,
|
yTickFormat,
|
||||||
|
compactYAxis = false,
|
||||||
yPadTop,
|
yPadTop,
|
||||||
yPadBottom,
|
yPadBottom,
|
||||||
zeroBaseLeft = true,
|
zeroBaseLeft = true,
|
||||||
@@ -497,7 +500,7 @@
|
|||||||
let iconRows = $derived(isNarrow ? ownIconRows : Math.max(ownIconRows, reserveTopRows));
|
let iconRows = $derived(isNarrow ? ownIconRows : Math.max(ownIconRows, reserveTopRows));
|
||||||
// tighter top/bottom gutters on mobile so charts don't waste vertical space
|
// tighter top/bottom gutters on mobile so charts don't waste vertical space
|
||||||
const iconRowH = $derived(isNarrow ? 30 : ICON_ROW_H);
|
const iconRowH = $derived(isNarrow ? 30 : ICON_ROW_H);
|
||||||
let padTop = $derived((title ? (subtitle ? 66 : 46) : isNarrow ? 14 : 28) + iconRows * iconRowH);
|
let padTop = $derived((title ? (subtitle ? 66 : 46) : isNarrow ? 22 : 28) + iconRows * iconRowH);
|
||||||
let agreementStripOffset = $derived(
|
let agreementStripOffset = $derived(
|
||||||
agreementStrip ? AGREEMENT_STRIP_HEIGHT + AGREEMENT_STRIP_GAP + 2 : 0
|
agreementStrip ? AGREEMENT_STRIP_HEIGHT + AGREEMENT_STRIP_GAP + 2 : 0
|
||||||
);
|
);
|
||||||
@@ -1087,7 +1090,13 @@
|
|||||||
ctx.lineTo(plotRight, y);
|
ctx.lineTo(plotRight, y);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.fillStyle = textColor;
|
ctx.fillStyle = textColor;
|
||||||
ctx.fillText(yTickFormat?.(v) ?? v.toFixed(tickDecimals(leftScale.step)), padLeft - 8, y);
|
ctx.fillText(
|
||||||
|
compactYAxis
|
||||||
|
? v.toFixed(tickDecimals(leftScale.step))
|
||||||
|
: (yTickFormat?.(v) ?? v.toFixed(tickDecimals(leftScale.step))),
|
||||||
|
padLeft - 8,
|
||||||
|
y
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right axis labels in the gutter (desktop only). On mobile there is no
|
// Right axis labels in the gutter (desktop only). On mobile there is no
|
||||||
@@ -1543,16 +1552,18 @@
|
|||||||
ctx.strokeStyle = withAlpha(gridColor, 0.8);
|
ctx.strokeStyle = withAlpha(gridColor, 0.8);
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
ctx.strokeRect(padLeft, stripTop + 0.5, plotW, AGREEMENT_STRIP_HEIGHT - 1);
|
ctx.strokeRect(padLeft, stripTop + 0.5, plotW, AGREEMENT_STRIP_HEIGHT - 1);
|
||||||
ctx.fillStyle = textColor;
|
if (!compactYAxis) {
|
||||||
ctx.font = '600 9px system-ui, sans-serif';
|
ctx.fillStyle = textColor;
|
||||||
ctx.textAlign = 'right';
|
ctx.font = '600 9px system-ui, sans-serif';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textAlign = 'right';
|
||||||
ctx.fillText(
|
ctx.textBaseline = 'middle';
|
||||||
agreementStrip.label,
|
ctx.fillText(
|
||||||
padLeft - 6,
|
agreementStrip.label,
|
||||||
stripTop + AGREEMENT_STRIP_HEIGHT / 2,
|
padLeft - 6,
|
||||||
Math.max(24, padLeft - 10)
|
stripTop + AGREEMENT_STRIP_HEIGHT / 2,
|
||||||
);
|
Math.max(24, padLeft - 10)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Axis unit labels
|
// Axis unit labels
|
||||||
|
|||||||
@@ -108,10 +108,9 @@ export async function downloadChartsPng(
|
|||||||
}
|
}
|
||||||
if (row.length > 0) legendRows.push(row);
|
if (row.length > 0) legendRows.push(row);
|
||||||
}
|
}
|
||||||
const headerHeight =
|
const titleHeight = options.title ? 34 * dpr : 0;
|
||||||
(options.title ? 34 * dpr : 0) +
|
const legendHeight = legendRows.length > 0 ? legendRows.length * rowHeight + 8 * dpr : 0;
|
||||||
(legendRows.length > 0 ? legendRows.length * rowHeight + 8 * dpr : 0);
|
const totalHeight = titleHeight + canvases.reduce((sum, c) => sum + c.height, 0) + legendHeight;
|
||||||
const totalHeight = headerHeight + canvases.reduce((sum, c) => sum + c.height, 0);
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = maxWidth;
|
canvas.width = maxWidth;
|
||||||
@@ -134,7 +133,12 @@ export async function downloadChartsPng(
|
|||||||
ctx.fillText(options.title, padding, 17 * dpr);
|
ctx.fillText(options.title, padding, 17 * dpr);
|
||||||
y += 34 * dpr;
|
y += 34 * dpr;
|
||||||
}
|
}
|
||||||
|
for (const c of canvases) {
|
||||||
|
ctx.drawImage(c, 0, y);
|
||||||
|
y += c.height;
|
||||||
|
}
|
||||||
if (legendRows.length > 0) {
|
if (legendRows.length > 0) {
|
||||||
|
y += 8 * dpr;
|
||||||
ctx.font = `${12 * dpr}px system-ui, -apple-system, sans-serif`;
|
ctx.font = `${12 * dpr}px system-ui, -apple-system, sans-serif`;
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
for (const row of legendRows) {
|
for (const row of legendRows) {
|
||||||
@@ -165,11 +169,6 @@ export async function downloadChartsPng(
|
|||||||
}
|
}
|
||||||
y += rowHeight;
|
y += rowHeight;
|
||||||
}
|
}
|
||||||
y += 8 * dpr;
|
|
||||||
}
|
|
||||||
for (const c of canvases) {
|
|
||||||
ctx.drawImage(c, 0, y);
|
|
||||||
y += c.height;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerDownload(canvas.toDataURL('image/png'), `${fileName}.png`);
|
triggerDownload(canvas.toDataURL('image/png'), `${fileName}.png`);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount, untrack } from 'svelte';
|
import { onDestroy, onMount, tick, untrack } from 'svelte';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { fade } from 'svelte/transition';
|
|
||||||
|
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
@@ -17,9 +16,6 @@
|
|||||||
type ExportableChart,
|
type ExportableChart,
|
||||||
type ExportLegendItem
|
type ExportLegendItem
|
||||||
} from '$lib/components/charts';
|
} from '$lib/components/charts';
|
||||||
import { Label } from '$lib/components/ui/label';
|
|
||||||
import { Switch } from '$lib/components/ui/switch';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CHART_COLORS,
|
CHART_COLORS,
|
||||||
CanvasChart,
|
CanvasChart,
|
||||||
@@ -88,17 +84,21 @@
|
|||||||
let location = $derived(data.location);
|
let location = $derived(data.location);
|
||||||
let mounted = $state(false);
|
let mounted = $state(false);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let refreshing = $state(false);
|
|
||||||
let loadError = $state<FriendlyWeatherError | null>(null);
|
let loadError = $state<FriendlyWeatherError | null>(null);
|
||||||
let retryNonce = $state(0);
|
let retryNonce = $state(0);
|
||||||
let requestVersion = 0;
|
let requestVersion = 0;
|
||||||
let activeController: AbortController | null = null;
|
let activeController: AbortController | null = null;
|
||||||
let chartComponents: CanvasChart[] = $state([]);
|
let chartComponents: CanvasChart[] = $state([]);
|
||||||
let pictogramExporter: ExportableChart | null = $state(null);
|
let pictogramExporter: ExportableChart | null = $state(null);
|
||||||
let showLegend = $state(true);
|
|
||||||
|
|
||||||
let isMobile = $state(false);
|
let isMobile = $state(false);
|
||||||
let comparisonPlotInsetLeft = $derived(isMobile ? 92 : 116);
|
let showModelNames = $state(true);
|
||||||
|
let modelNamesInitialized = false;
|
||||||
|
let initialRangeInitialized = false;
|
||||||
|
let compactYAxis = $derived(!showModelNames);
|
||||||
|
let comparisonPlotInsetLeft = $derived(
|
||||||
|
showModelNames ? (isMobile ? 92 : 116) : isMobile ? 34 : 40
|
||||||
|
);
|
||||||
let comparisonPlotInsetRight = $derived(isMobile ? 8 : 20);
|
let comparisonPlotInsetRight = $derived(isMobile ? 8 : 20);
|
||||||
let params = $state({
|
let params = $state({
|
||||||
...defaultParameters,
|
...defaultParameters,
|
||||||
@@ -132,6 +132,15 @@
|
|||||||
return left.length === right.length && left.every((value, index) => value === right[index]);
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changedValueCount(left: string[], right: string[]): number {
|
||||||
|
const leftValues = new Set(left);
|
||||||
|
const rightValues = new Set(right);
|
||||||
|
let count = 0;
|
||||||
|
for (const value of leftValues) if (!rightValues.has(value)) count++;
|
||||||
|
for (const value of rightValues) if (!leftValues.has(value)) count++;
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
function orderModels(values: string[]): string[] {
|
function orderModels(values: string[]): string[] {
|
||||||
return [...values].sort(
|
return [...values].sort(
|
||||||
(left, right) =>
|
(left, right) =>
|
||||||
@@ -159,6 +168,8 @@
|
|||||||
return current !== null && !hasSameOrder(current.selection.models, appliedModels);
|
return current !== null && !hasSameOrder(current.selection.models, appliedModels);
|
||||||
});
|
});
|
||||||
let comparisonSelectionDirty = $derived(variablesDirty || modelsDirty);
|
let comparisonSelectionDirty = $derived(variablesDirty || modelsDirty);
|
||||||
|
let pendingVariableChanges = $derived(changedValueCount(params.hourly, appliedHourly));
|
||||||
|
let pendingModelChanges = $derived(changedValueCount(params.models, appliedModels));
|
||||||
let comparisonMuted = $derived(
|
let comparisonMuted = $derived(
|
||||||
comparisonSelectionDirty || displayedVariablesStale || displayedModelsStale
|
comparisonSelectionDirty || displayedVariablesStale || displayedModelsStale
|
||||||
);
|
);
|
||||||
@@ -198,6 +209,10 @@
|
|||||||
const media = window.matchMedia('(max-width: 639px)');
|
const media = window.matchMedia('(max-width: 639px)');
|
||||||
const applyMedia = () => {
|
const applyMedia = () => {
|
||||||
isMobile = media.matches;
|
isMobile = media.matches;
|
||||||
|
if (!modelNamesInitialized) {
|
||||||
|
showModelNames = !media.matches;
|
||||||
|
modelNamesInitialized = true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
applyMedia();
|
applyMedia();
|
||||||
media.addEventListener('change', applyMedia);
|
media.addEventListener('change', applyMedia);
|
||||||
@@ -236,14 +251,12 @@
|
|||||||
if (modelList.length === 0 || hourlyVars.length === 0) {
|
if (modelList.length === 0 || hourlyVars.length === 0) {
|
||||||
fetchedData = null;
|
fetchedData = null;
|
||||||
loading = false;
|
loading = false;
|
||||||
refreshing = false;
|
|
||||||
loadError = null;
|
loadError = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasFetchedData = untrack(() => fetchedData !== null);
|
const hasFetchedData = untrack(() => fetchedData !== null);
|
||||||
loading = !hasFetchedData;
|
loading = !hasFetchedData;
|
||||||
refreshing = hasFetchedData;
|
|
||||||
loadError = null;
|
loadError = null;
|
||||||
const timer = window.setTimeout(() => {
|
const timer = window.setTimeout(() => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
@@ -266,15 +279,20 @@
|
|||||||
)
|
)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (version !== requestVersion) return;
|
if (version !== requestVersion) return;
|
||||||
|
const applyInitialMobileRange = !initialRangeInitialized && isMobile;
|
||||||
|
initialRangeInitialized = true;
|
||||||
fetchedData = { result, selection: { models: modelList, hourly: hourlyVars } };
|
fetchedData = { result, selection: { models: modelList, hourly: hourlyVars } };
|
||||||
loading = false;
|
loading = false;
|
||||||
refreshing = false;
|
if (applyInitialMobileRange) {
|
||||||
|
void tick().then(() => {
|
||||||
|
if (version === requestVersion && groupRange(CHART_GROUP) === null) setRangeDays(3);
|
||||||
|
});
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
if (version !== requestVersion || controller.signal.aborted) return;
|
if (version !== requestVersion || controller.signal.aborted) return;
|
||||||
loadError = humanizeWeatherError(error);
|
loadError = humanizeWeatherError(error);
|
||||||
loading = false;
|
loading = false;
|
||||||
refreshing = false;
|
|
||||||
});
|
});
|
||||||
}, FETCH_DEBOUNCE_MS);
|
}, FETCH_DEBOUNCE_MS);
|
||||||
|
|
||||||
@@ -536,6 +554,11 @@
|
|||||||
if (!variablesChanged && !modelsChanged) retryNonce++;
|
if (!variablesChanged && !modelsChanged) retryNonce++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function discardComparisonSelection(): void {
|
||||||
|
params.hourly = [...appliedHourly];
|
||||||
|
params.models = [...appliedModels];
|
||||||
|
}
|
||||||
|
|
||||||
const variableGroupLabels = [
|
const variableGroupLabels = [
|
||||||
m.compare_group_temperature,
|
m.compare_group_temperature,
|
||||||
m.compare_group_precipitation,
|
m.compare_group_precipitation,
|
||||||
@@ -568,7 +591,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{m.page_compare_title()} | Drizz.li</title>
|
<title>Drizz.li | {m.page_compare_title()}</title>
|
||||||
<link rel="canonical" href="https://drizz.li/weather/compare" />
|
<link rel="canonical" href="https://drizz.li/weather/compare" />
|
||||||
<meta name="description" content={m.page_compare_description()} />
|
<meta name="description" content={m.page_compare_description()} />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
@@ -689,6 +712,7 @@
|
|||||||
yMax={def.yMax}
|
yMax={def.yMax}
|
||||||
yTicks={def.yTicks}
|
yTicks={def.yTicks}
|
||||||
yTickFormat={def.yTickFormat}
|
yTickFormat={def.yTickFormat}
|
||||||
|
{compactYAxis}
|
||||||
plotInsetLeft={comparisonPlotInsetLeft}
|
plotInsetLeft={comparisonPlotInsetLeft}
|
||||||
plotInsetRight={comparisonPlotInsetRight}
|
plotInsetRight={comparisonPlotInsetRight}
|
||||||
showCredit={i === chartDefs.length - 1}
|
showCredit={i === chartDefs.length - 1}
|
||||||
@@ -711,14 +735,6 @@
|
|||||||
<span class="text-xs font-semibold text-muted-foreground">
|
<span class="text-xs font-semibold text-muted-foreground">
|
||||||
{m.compare_selection_pending()}
|
{m.compare_selection_pending()}
|
||||||
</span>
|
</span>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="min-h-10 rounded-lg bg-primary px-4 py-2 text-sm font-bold text-primary-foreground hover:bg-primary/90 disabled:cursor-wait disabled:opacity-70"
|
|
||||||
disabled={(displayedVariablesStale || displayedModelsStale) && refreshing}
|
|
||||||
onclick={applyComparisonSelection}
|
|
||||||
>
|
|
||||||
{m.compare_apply_selection()}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -748,6 +764,8 @@
|
|||||||
group={CHART_GROUP}
|
group={CHART_GROUP}
|
||||||
plotInsetLeft={comparisonPlotInsetLeft}
|
plotInsetLeft={comparisonPlotInsetLeft}
|
||||||
plotInsetRight={comparisonPlotInsetRight}
|
plotInsetRight={comparisonPlotInsetRight}
|
||||||
|
{showModelNames}
|
||||||
|
onToggleModelNames={() => (showModelNames = !showModelNames)}
|
||||||
registerExporter={(exporter) => (pictogramExporter = exporter)}
|
registerExporter={(exporter) => (pictogramExporter = exporter)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -759,36 +777,25 @@
|
|||||||
? 'pointer-events-none opacity-40'
|
? 'pointer-events-none opacity-40'
|
||||||
: ''}"
|
: ''}"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between gap-3">
|
<p class="text-xs font-semibold text-muted-foreground">{m.compare_model_colors()}</p>
|
||||||
<p class="text-xs font-semibold text-muted-foreground">{m.compare_model_colors()}</p>
|
<div class="-mx-1 mt-2 flex gap-3 overflow-x-auto px-1 pb-1 sm:flex-wrap">
|
||||||
<div class="flex items-center gap-2">
|
{#each displayedModels as model (model.modelId)}
|
||||||
<Switch id="show_legend" bind:checked={showLegend} />
|
<span class="flex min-h-8 shrink-0 items-center gap-1.5 text-xs">
|
||||||
<Label for="show_legend" class="text-xs">{m.legend_show()}</Label>
|
<span
|
||||||
</div>
|
class="size-2.5 rounded-full"
|
||||||
|
style:background-color={modelColor(model.modelId, fetchedData.selection.models)}
|
||||||
|
></span>
|
||||||
|
{modelLabel(model.modelId)}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
{#if chartDefs.some( (def) => def.series.some((series) => series.name === m.compare_model_mean()) )}
|
||||||
|
<span class="flex shrink-0 items-center gap-1.5 text-xs">
|
||||||
|
<span class="w-4 border-t-2 border-dashed" style:border-color={CHART_COLORS.average}
|
||||||
|
></span>
|
||||||
|
{m.compare_model_mean()}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if showLegend}
|
|
||||||
<div
|
|
||||||
class="-mx-1 mt-2 flex gap-3 overflow-x-auto px-1 pb-1 sm:flex-wrap"
|
|
||||||
transition:fade={{ duration: 150 }}
|
|
||||||
>
|
|
||||||
{#each displayedModels as model (model.modelId)}
|
|
||||||
<span class="flex min-h-8 shrink-0 items-center gap-1.5 text-xs">
|
|
||||||
<span
|
|
||||||
class="size-2.5 rounded-full"
|
|
||||||
style:background-color={modelColor(model.modelId, fetchedData.selection.models)}
|
|
||||||
></span>
|
|
||||||
{modelLabel(model.modelId)}
|
|
||||||
</span>
|
|
||||||
{/each}
|
|
||||||
{#if chartDefs.some( (def) => def.series.some((series) => series.name === m.compare_model_mean()) )}
|
|
||||||
<span class="flex shrink-0 items-center gap-1.5 text-xs">
|
|
||||||
<span class="w-4 border-t-2 border-dashed" style:border-color={CHART_COLORS.average}
|
|
||||||
></span>
|
|
||||||
{m.compare_model_mean()}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -826,10 +833,8 @@
|
|||||||
mode="variables"
|
mode="variables"
|
||||||
groups={variableSelectionGroups}
|
groups={variableSelectionGroups}
|
||||||
selected={params.hourly}
|
selected={params.hourly}
|
||||||
dirty={variablesDirty}
|
|
||||||
onToggle={(value) => toggleSelection('hourly', value)}
|
onToggle={(value) => toggleSelection('hourly', value)}
|
||||||
onToggleGroup={(values, select) => toggleGroupSelection('hourly', values, select)}
|
onToggleGroup={(values, select) => toggleGroupSelection('hourly', values, select)}
|
||||||
onApply={applyComparisonSelection}
|
|
||||||
onRestoreDefaults={restoreStandardVariables}
|
onRestoreDefaults={restoreStandardVariables}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
@@ -853,9 +858,46 @@
|
|||||||
mode="models"
|
mode="models"
|
||||||
groups={modelSelectionGroups}
|
groups={modelSelectionGroups}
|
||||||
selected={params.models}
|
selected={params.models}
|
||||||
dirty={modelsDirty}
|
|
||||||
onToggle={(value) => toggleSelection('models', value)}
|
onToggle={(value) => toggleSelection('models', value)}
|
||||||
onToggleGroup={(values, select) => toggleGroupSelection('models', values, select)}
|
onToggleGroup={(values, select) => toggleGroupSelection('models', values, select)}
|
||||||
onApply={applyComparisonSelection}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{#if comparisonSelectionDirty}
|
||||||
|
<div class="h-24" aria-hidden="true"></div>
|
||||||
|
<div
|
||||||
|
class="pointer-events-none fixed right-3 bottom-3 left-3 z-50 md:left-auto md:w-[42rem] md:max-w-[calc(100vw-2rem)] lg:right-8"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="pointer-events-auto flex flex-col gap-3 rounded-2xl border border-primary/30 bg-background/95 p-3 shadow-2xl backdrop-blur sm:flex-row sm:items-center sm:justify-between sm:p-4"
|
||||||
|
>
|
||||||
|
<div class="min-w-0" aria-live="polite">
|
||||||
|
<p class="text-sm font-bold">{m.compare_selection_pending()}</p>
|
||||||
|
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
||||||
|
{#if variablesDirty}
|
||||||
|
<span>{m.compare_variables_heading()}: {pendingVariableChanges}</span>
|
||||||
|
{/if}
|
||||||
|
{#if modelsDirty}
|
||||||
|
<span>{m.compare_models_heading()}: {pendingModelChanges}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid shrink-0 grid-cols-2 gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="min-h-10 rounded-lg border border-border bg-background px-3 py-2 text-sm font-semibold hover:bg-muted"
|
||||||
|
onclick={discardComparisonSelection}
|
||||||
|
>
|
||||||
|
{m.compare_discard_changes()}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="min-h-10 rounded-lg bg-primary px-4 py-2 text-sm font-bold text-primary-foreground hover:bg-primary/90"
|
||||||
|
onclick={applyComparisonSelection}
|
||||||
|
>
|
||||||
|
{m.compare_apply_selection()}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -21,23 +21,12 @@
|
|||||||
mode: 'models' | 'variables';
|
mode: 'models' | 'variables';
|
||||||
groups: OptionGroup[];
|
groups: OptionGroup[];
|
||||||
selected: string[];
|
selected: string[];
|
||||||
dirty: boolean;
|
|
||||||
onToggle: (value: string) => void;
|
onToggle: (value: string) => void;
|
||||||
onToggleGroup: (values: string[], select: boolean) => void;
|
onToggleGroup: (values: string[], select: boolean) => void;
|
||||||
onApply: () => void;
|
|
||||||
onRestoreDefaults?: () => void;
|
onRestoreDefaults?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { mode, groups, selected, onToggle, onToggleGroup, onRestoreDefaults }: Props = $props();
|
||||||
mode,
|
|
||||||
groups,
|
|
||||||
selected,
|
|
||||||
dirty,
|
|
||||||
onToggle,
|
|
||||||
onToggleGroup,
|
|
||||||
onApply,
|
|
||||||
onRestoreDefaults
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
let search = $state('');
|
let search = $state('');
|
||||||
let onlySelected = $state(false);
|
let onlySelected = $state(false);
|
||||||
@@ -118,17 +107,6 @@
|
|||||||
<span class="shrink-0 text-[11px] text-muted-foreground tabular-nums">
|
<span class="shrink-0 text-[11px] text-muted-foreground tabular-nums">
|
||||||
{selection.count}/{fullGroup.options.length}
|
{selection.count}/{fullGroup.options.length}
|
||||||
</span>
|
</span>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="shrink-0 rounded-md px-2 py-1 text-[11px] font-semibold text-primary hover:bg-primary/10"
|
|
||||||
onclick={() =>
|
|
||||||
onToggleGroup(
|
|
||||||
fullGroup.options.map((option) => option.value),
|
|
||||||
!selection.all
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{selection.all ? m.compare_clear_group() : m.compare_select_group()}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
{#each visibleGroup.options as option (option.value)}
|
{#each visibleGroup.options as option (option.value)}
|
||||||
@@ -164,27 +142,15 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
{#if mode === 'variables' && onRestoreDefaults}
|
||||||
class="flex flex-wrap items-center justify-between gap-2 border-t border-border bg-muted/20 px-3 py-3 sm:px-4"
|
<div class="border-t border-border bg-muted/20 px-3 py-3 sm:px-4">
|
||||||
>
|
<button
|
||||||
<div>
|
type="button"
|
||||||
{#if mode === 'variables' && onRestoreDefaults}
|
class="min-h-9 rounded-lg border border-border bg-background px-3 py-1.5 text-xs font-semibold hover:bg-muted"
|
||||||
<button
|
onclick={onRestoreDefaults}
|
||||||
type="button"
|
>
|
||||||
class="min-h-9 rounded-lg border border-border bg-background px-3 py-1.5 text-xs font-semibold hover:bg-muted"
|
{m.compare_restore_defaults()}
|
||||||
onclick={onRestoreDefaults}
|
</button>
|
||||||
>
|
|
||||||
{m.compare_restore_defaults()}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
{/if}
|
||||||
type="button"
|
|
||||||
class="min-h-10 rounded-lg bg-primary px-4 py-2 text-sm font-bold text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
|
||||||
disabled={!dirty}
|
|
||||||
onclick={onApply}
|
|
||||||
>
|
|
||||||
{m.compare_apply_selection()}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,6 +26,8 @@
|
|||||||
group: string;
|
group: string;
|
||||||
plotInsetLeft: number;
|
plotInsetLeft: number;
|
||||||
plotInsetRight: number;
|
plotInsetRight: number;
|
||||||
|
showModelNames: boolean;
|
||||||
|
onToggleModelNames: () => void;
|
||||||
registerExporter?: (exporter: TimelineExporter | null) => void;
|
registerExporter?: (exporter: TimelineExporter | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +70,8 @@
|
|||||||
group,
|
group,
|
||||||
plotInsetLeft,
|
plotInsetLeft,
|
||||||
plotInsetRight,
|
plotInsetRight,
|
||||||
|
showModelNames,
|
||||||
|
onToggleModelNames,
|
||||||
registerExporter
|
registerExporter
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
@@ -600,9 +604,16 @@
|
|||||||
context.beginPath();
|
context.beginPath();
|
||||||
context.arc(9, rowCenter, 4, 0, Math.PI * 2);
|
context.arc(9, rowCenter, 4, 0, Math.PI * 2);
|
||||||
context.fill();
|
context.fill();
|
||||||
context.fillStyle = foreground;
|
if (showModelNames) {
|
||||||
context.textAlign = 'left';
|
context.fillStyle = foreground;
|
||||||
context.fillText(modelLabel(model.modelId), 18, rowCenter, Math.max(20, plotInsetLeft - 24));
|
context.textAlign = 'left';
|
||||||
|
context.fillText(
|
||||||
|
modelLabel(model.modelId),
|
||||||
|
18,
|
||||||
|
rowCenter,
|
||||||
|
Math.max(20, plotInsetLeft - 24)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
context.save();
|
context.save();
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
@@ -659,6 +670,7 @@
|
|||||||
void daySeparators;
|
void daySeparators;
|
||||||
void sunlightSegments;
|
void sunlightSegments;
|
||||||
void displayModels;
|
void displayModels;
|
||||||
|
void showModelNames;
|
||||||
if (!target || currentWidth <= 0 || currentHeight <= 0) return;
|
if (!target || currentWidth <= 0 || currentHeight <= 0) return;
|
||||||
|
|
||||||
const version = ++screenRenderVersion;
|
const version = ++screenRenderVersion;
|
||||||
@@ -684,11 +696,21 @@
|
|||||||
class="mt-7 -mx-3 border-y border-border/70 bg-card shadow-sm lg:mx-0 lg:rounded-2xl lg:border"
|
class="mt-7 -mx-3 border-y border-border/70 bg-card shadow-sm lg:mx-0 lg:rounded-2xl lg:border"
|
||||||
aria-labelledby="model-pictogram-title"
|
aria-labelledby="model-pictogram-title"
|
||||||
>
|
>
|
||||||
<div class="px-3 pt-3 pb-2 lg:px-4">
|
<div class="flex items-start justify-between gap-3 px-3 pt-3 pb-2 lg:px-4">
|
||||||
<h2 id="model-pictogram-title" class="text-sm font-bold tracking-tight">
|
<div class="min-w-0">
|
||||||
{m.compare_timeline_title()}
|
<h2 id="model-pictogram-title" class="text-sm font-bold tracking-tight">
|
||||||
</h2>
|
{m.compare_timeline_title()}
|
||||||
<p class="text-xs text-muted-foreground">{m.compare_timeline_hint()}</p>
|
</h2>
|
||||||
|
<p class="text-xs text-muted-foreground">{m.compare_timeline_hint()}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="min-h-8 shrink-0 rounded-lg border border-border bg-background px-2.5 py-1 text-xs font-semibold whitespace-nowrap text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||||
|
aria-pressed={showModelNames}
|
||||||
|
onclick={onToggleModelNames}
|
||||||
|
>
|
||||||
|
{showModelNames ? m.compare_hide_model_names() : m.compare_show_model_names()}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lg:px-4">
|
<div class="lg:px-4">
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user