This commit is contained in:
Vincent van der Wal
2026-07-19 16:13:51 +02:00
parent e031716ce6
commit baa4840b38
26 changed files with 1077 additions and 702 deletions
@@ -29,7 +29,6 @@
const CHART_GROUP = 'week-meteogram';
const SECONDS_PER_DAY = 24 * 3600;
let showCharts = $state(false);
let chartComponents: CanvasChart[] = $state([]);
// Timestamps from the service are in milliseconds; CanvasChart uses seconds.
@@ -37,20 +36,21 @@
let liveCharts = $derived(chartComponents.filter((chart) => chart != null));
export function scrollToDay(day: Date): void {
if (!data || liveCharts.length === 0) return;
function dayStartSec(day: Date): number | null {
if (!data) return null;
const tz = data.timezone;
const targetDayStr = formatZoned(day, tz, 'yyyy-MM-dd');
const firstHourIdx = data.hourlyDates.findIndex(
(d) => formatZoned(d, tz, 'yyyy-MM-dd') === targetDayStr
);
return firstHourIdx === -1 ? null : data.timestamps[firstHourIdx] / 1000;
}
if (firstHourIdx === -1) return;
const dayStart = data.timestamps[firstHourIdx] / 1000;
function setRangeDays(from: Date, days: number): void {
const start = dayStartSec(from);
if (start == null || liveCharts.length === 0) return;
// Charts share the group, so setting the range on one syncs all of them
liveCharts[0].setRange(dayStart, dayStart + SECONDS_PER_DAY);
liveCharts[0].setRange(start, start + days * SECONDS_PER_DAY);
}
function resetZoom(): void {
@@ -58,18 +58,15 @@
onResetZoom?.();
}
// Zoom to the selected day once all three charts are mounted
let scrolledOnMount = false;
$effect(() => {
if (!showCharts) {
scrolledOnMount = false;
return;
}
if (!scrolledOnMount && liveCharts.length === 3 && data) {
scrolledOnMount = true;
requestAnimationFrame(() => scrollToDay(selectedDay));
}
});
// Range presets: charts show the full week by default, these (or
// Ctrl+scroll) narrow the window.
const rangePresets = [
{ label: 'Today', apply: () => setRangeDays(new Date(), 1) },
{ label: 'Selected day', apply: () => setRangeDays(selectedDay, 1) },
{ label: '3 days', apply: () => setRangeDays(new Date(), 3) },
{ label: '5 days', apply: () => setRangeDays(new Date(), 5) },
{ label: 'All', apply: () => resetZoom() }
];
// ─── Series Building ────────────────────────────────────────────────────────
@@ -195,142 +192,68 @@
});
</script>
<div class="charts-toggle-section">
<button class="charts-toggle-btn" onclick={() => (showCharts = !showCharts)}>
<span>Detailed Meteogram Charts</span>
<svg
class="toggle-chevron {showCharts ? 'open' : ''}"
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
</div>
{#if showCharts}
<div class="detailed-charts" in:fade={{ duration: 200 }}>
<div class="charts-header">
<h3 class="charts-title">
{formatZoned(selectedDay, data.timezone, 'EEEE')}
<small>
{getRelativeDayLabel(selectedDay, data.timezone) !==
formatZoned(selectedDay, data.timezone, 'EEEE')
? ` (${getRelativeDayLabel(selectedDay, data.timezone)})`
: ''}
</small>
</h3>
<button type="button" class="zoom-reset-btn" title="Show all days (Esc)" onclick={resetZoom}>
Show All
</button>
</div>
<ChartContainer {loading} chartCount={3} chartHeight={300}>
{#each chartDefs as def, i (def.title)}
<CanvasChart
bind:this={chartComponents[i]}
timestamps={timestampsSec}
timezone={data.timezone}
series={def.series}
bands={data.daylightBands}
unit={def.unit}
unitRight={def.unitRight}
yMin={def.yMin}
yMinRight={def.yMinRight}
yMaxRight={def.yMaxRight}
invertRight={def.invertRight}
title={def.title}
showCredit={def.showCredit}
showLegend
height={300}
group={CHART_GROUP}
/>
{/each}
</ChartContainer>
<div class="mt-6 md:mt-10">
<ChartToolbar charts={liveCharts} fileName="week-forecast" />
<section class="mt-8" in:fade={{ duration: 200 }}>
<div class="mb-3 flex flex-wrap items-center justify-between gap-2">
<h3 class="text-lg font-bold">
Meteogram
<span class="font-semibold text-muted-foreground">
{formatZoned(selectedDay, data.timezone, 'EEEE')}{getRelativeDayLabel(
selectedDay,
data.timezone
) !== formatZoned(selectedDay, data.timezone, 'EEEE')
? ` (${getRelativeDayLabel(selectedDay, data.timezone)})`
: ''}
</span>
</h3>
<div class="flex flex-wrap items-center gap-3">
<span class="hidden text-xs text-muted-foreground md:inline">
<kbd class="rounded border border-border bg-muted px-1 py-0.5 font-sans text-[10px]"
>Ctrl</kbd
>
+ scroll to zoom
</span>
<div
class="inline-flex items-center rounded-lg bg-muted p-0.5 text-xs font-semibold"
role="group"
aria-label="Chart time range"
>
{#each rangePresets as preset (preset.label)}
<button
type="button"
class="cursor-pointer rounded-md px-2.5 py-1 whitespace-nowrap text-muted-foreground transition-colors hover:bg-background hover:text-foreground hover:shadow-sm"
onclick={preset.apply}
>
{preset.label}
</button>
{/each}
</div>
</div>
</div>
{/if}
<style>
.charts-toggle-section {
margin-top: 1.5rem;
margin-bottom: 0.5rem;
}
<ChartContainer {loading} chartCount={3} chartHeight={300}>
{#each chartDefs as def, i (def.title)}
<CanvasChart
bind:this={chartComponents[i]}
timestamps={timestampsSec}
timezone={data.timezone}
series={def.series}
bands={data.daylightBands}
unit={def.unit}
unitRight={def.unitRight}
yMin={def.yMin}
yMinRight={def.yMinRight}
yMaxRight={def.yMaxRight}
invertRight={def.invertRight}
title={def.title}
showCredit={def.showCredit}
showLegend
height={300}
group={CHART_GROUP}
/>
{/each}
</ChartContainer>
.charts-toggle-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 600;
color: hsl(var(--muted-foreground));
background: hsl(var(--muted) / 0.5);
border: 1px solid hsl(var(--border));
border-radius: var(--radius, 0.375rem);
cursor: pointer;
transition: all 150ms ease;
}
.charts-toggle-btn:hover {
color: hsl(var(--foreground));
background: hsl(var(--muted));
}
.toggle-chevron {
transition: transform 200ms;
}
.toggle-chevron.open {
transform: rotate(180deg);
}
.detailed-charts {
margin-top: 0.5rem;
}
.charts-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.charts-title {
font-size: 1.25rem;
font-weight: 700;
}
.zoom-reset-btn {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.625rem;
font-size: 0.75rem;
font-weight: 500;
color: hsl(var(--muted-foreground));
background: hsl(var(--muted) / 0.5);
border: 1px solid hsl(var(--border));
border-radius: var(--radius, 0.375rem);
cursor: pointer;
transition:
color 150ms ease,
background-color 150ms ease;
white-space: nowrap;
user-select: none;
}
.zoom-reset-btn:hover {
color: hsl(var(--foreground));
background: hsl(var(--muted));
}
</style>
<div class="mt-6 md:mt-10">
<ChartToolbar charts={liveCharts} fileName="week-forecast" />
</div>
</section>