feat: week view overhaul (#8)

This commit was merged in pull request #8.
This commit is contained in:
2026-07-25 09:53:40 +02:00
parent bac4759662
commit 19686ee107
14 changed files with 753 additions and 185 deletions
@@ -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-1 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}