revamp weather UI: cards, meteograms, units in header, location favorites and range controls
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
|
||||
import { ChartContainer, ChartToolbar } from '$lib/components/charts';
|
||||
|
||||
import { CanvasChart } from '$lib/charts';
|
||||
import { CanvasChart, groupRange } from '$lib/charts';
|
||||
|
||||
import { getWeatherIconName } from '../../utils/weather-codes';
|
||||
import ChartCustomizer from './ChartCustomizer.svelte';
|
||||
@@ -39,8 +39,22 @@
|
||||
$storedChartLayout.filter((p) => p.variables.some((k) => VARIABLE_BY_KEY.has(k)))
|
||||
);
|
||||
|
||||
// When an extended range runs past the model's horizon the service pads with
|
||||
// zeros; trim the axis to the last hour that actually has data so the charts
|
||||
// cut off instead of flat-lining to zero.
|
||||
let validLength = $derived.by((): number => {
|
||||
const temp = data.hourly.temperature_2m ?? [];
|
||||
const n = data.timestamps.length;
|
||||
if (temp.length === 0) return n;
|
||||
let last = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (temp[i] != null && !isNaN(temp[i]) && temp[i] !== 0) last = i + 1;
|
||||
}
|
||||
return last || n;
|
||||
});
|
||||
|
||||
// Timestamps from the service are in milliseconds; CanvasChart uses seconds.
|
||||
let timestampsSec = $derived(data.timestamps.map((t) => t / 1000));
|
||||
let timestampsSec = $derived(data.timestamps.slice(0, validLength).map((t) => t / 1000));
|
||||
|
||||
function dayStartSec(day: Date): number | null {
|
||||
if (!data) return null;
|
||||
@@ -96,6 +110,21 @@
|
||||
return out;
|
||||
});
|
||||
|
||||
// Wind-direction arrows for panels showing wind (deg = direction from North).
|
||||
let windArrowMarks = $derived.by((): { t: number; deg: number }[] => {
|
||||
const dirs = data.hourly.winddirection_10m ?? [];
|
||||
const out: { t: number; deg: number }[] = [];
|
||||
for (let i = 0; i < timestampsSec.length; i++) {
|
||||
const d = dirs[i];
|
||||
if (d == null || !isFinite(d)) continue;
|
||||
out.push({ t: timestampsSec[i], deg: d });
|
||||
}
|
||||
return out;
|
||||
});
|
||||
|
||||
// True while the shared group is zoomed in (not the full range).
|
||||
let zoomActive = $derived(groupRange(CHART_GROUP) != null);
|
||||
|
||||
// ─── Panel definitions ──────────────────────────────────────────────────────
|
||||
|
||||
interface RenderPanel extends ChartPanel {
|
||||
@@ -112,6 +141,16 @@
|
||||
return { ...p, def, title, titleShort };
|
||||
})
|
||||
);
|
||||
|
||||
// Uniform sizing across every panel: reserve the right-axis gutter and the
|
||||
// tallest icon-row count so all meteograms share one plot rectangle.
|
||||
let anyRightAxis = $derived(renderPanels.some((p) => p.def.unitRight != null));
|
||||
let maxTopRows = $derived(
|
||||
Math.max(
|
||||
0,
|
||||
...renderPanels.map((p) => (p.def.hasPictograms ? 1 : 0) + (p.def.hasWindArrows ? 1 : 0))
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<section class="mt-8" in:fade={{ duration: 200 }}>
|
||||
@@ -129,11 +168,31 @@
|
||||
</h3>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<span class="hidden text-xs text-muted-foreground md:inline">
|
||||
drag or
|
||||
<kbd class="rounded border border-border bg-muted px-1 py-0.5 font-sans text-[10px]"
|
||||
>Ctrl</kbd
|
||||
>
|
||||
+ scroll to zoom
|
||||
</span>
|
||||
{#if zoomActive}
|
||||
<button
|
||||
type="button"
|
||||
class="flex cursor-pointer items-center gap-1.5 rounded-lg border border-primary/50 bg-primary/10 px-2.5 py-1 text-xs font-semibold text-primary transition-colors hover:bg-primary/15"
|
||||
onclick={resetZoom}
|
||||
>
|
||||
<svg
|
||||
class="h-3.5 w-3.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v6h6M20 20v-6h-6" />
|
||||
<path stroke-linecap="round" d="M20 10a8 8 0 0 0-14-4M4 14a8 8 0 0 0 14 4" />
|
||||
</svg>
|
||||
Reset zoom
|
||||
</button>
|
||||
{/if}
|
||||
<div
|
||||
class="inline-flex items-center rounded-lg bg-muted p-0.5 text-xs font-semibold"
|
||||
role="group"
|
||||
@@ -180,14 +239,23 @@
|
||||
{:else}
|
||||
<div class="flex flex-col gap-6">
|
||||
{#each renderPanels as panel, i (panel.id)}
|
||||
<div class="rounded-2xl border border-border/70 bg-card p-3 shadow-sm md:p-4">
|
||||
<div class="mb-1 flex items-center justify-between px-1">
|
||||
<!-- full-bleed to the screen edges on mobile; a contained card on md+ -->
|
||||
<div
|
||||
class="-mx-5 border-y border-border/70 bg-card px-0 py-3 shadow-sm md:mx-0 md:rounded-2xl md:border md:px-4 md:py-4"
|
||||
>
|
||||
<div class="mb-1 flex items-center justify-between px-3 md:px-1">
|
||||
<h4 class="truncate text-sm font-bold text-muted-foreground">
|
||||
<span class="hidden md:inline">{panel.title}</span>
|
||||
<span class="md:hidden">{panel.titleShort}</span>
|
||||
</h4>
|
||||
</div>
|
||||
<ChartContainer {loading} chartCount={1} chartHeight={CHART_HEIGHT} minWidth={520}>
|
||||
<ChartContainer
|
||||
{loading}
|
||||
chartCount={1}
|
||||
chartHeight={CHART_HEIGHT}
|
||||
minWidth={520}
|
||||
bleed={false}
|
||||
>
|
||||
<CanvasChart
|
||||
bind:this={chartComponents[i]}
|
||||
timestamps={timestampsSec}
|
||||
@@ -195,6 +263,9 @@
|
||||
series={panel.def.series}
|
||||
bands={data.daylightBands}
|
||||
pictograms={panel.def.hasPictograms ? pictograms : []}
|
||||
windArrows={panel.def.hasWindArrows ? windArrowMarks : []}
|
||||
reserveRightAxis={anyRightAxis}
|
||||
reserveTopRows={maxTopRows}
|
||||
highlight={selectedDayHighlight}
|
||||
unit={panel.def.unit}
|
||||
unitRight={panel.def.unitRight}
|
||||
|
||||
Reference in New Issue
Block a user