diff --git a/src/lib/charts/CanvasChart.svelte b/src/lib/charts/CanvasChart.svelte index 84d306f..cf0b02c 100644 --- a/src/lib/charts/CanvasChart.svelte +++ b/src/lib/charts/CanvasChart.svelte @@ -31,6 +31,9 @@ width?: number; /** Draw a low-alpha area fill below (or above, on inverted axes) the line */ fill?: boolean; + /** With `fill`, fill the area between this line and another data array + * instead of the baseline (e.g. an ensemble min-max band) */ + bandTo?: (number | null)[]; /** Opacity of the area fill (default 0.15) */ fillOpacity?: number; /** Draw the line dashed */ @@ -92,6 +95,8 @@ series: ChartSeries[]; /** Background bands (epoch seconds), e.g. daylight */ bands?: { start: number; end: number }[]; + /** Highlighted time range (epoch seconds), e.g. the selected day */ + highlight?: { start: number; end: number }; /** Unit label for the left y axis (also used in tooltip values) */ unit?: string; /** Unit label for the right y axis; when set, right-axis labels are drawn */ @@ -129,6 +134,7 @@ timezone, series, bands = [], + highlight, unit = '', unitRight, height = 300, @@ -443,6 +449,33 @@ if (x2 > x1) ctx.fillRect(x1, padTop, x2 - x1, plotH); } + // Selected-day highlight: soft tint + dashed edge lines + if (highlight && highlight.end > viewStart && highlight.start < viewEnd) { + const accent = cssColor('--primary', '#e08a3c'); + const x1 = Math.max(PAD_LEFT, xPix(highlight.start)); + const x2 = Math.min(plotRight, xPix(highlight.end)); + if (x2 > x1) { + ctx.save(); + ctx.globalAlpha = 0.08; + ctx.fillStyle = accent; + ctx.fillRect(x1, padTop, x2 - x1, plotH); + ctx.globalAlpha = 0.55; + ctx.strokeStyle = accent; + ctx.lineWidth = 1.5; + ctx.setLineDash([5, 4]); + ctx.beginPath(); + for (const edge of [highlight.start, highlight.end]) { + const x = xPix(edge); + if (x >= PAD_LEFT && x <= plotRight) { + ctx.moveTo(x, padTop); + ctx.lineTo(x, plotBottom); + } + } + ctx.stroke(); + ctx.restore(); + } + } + // Horizontal grid lines + left axis labels ctx.font = font; ctx.textAlign = 'right'; @@ -530,17 +563,20 @@ } // Line series: draw fill and stroke per contiguous non-null run - // (points outside the view are handled by the clip rect) - const runs: Array> = []; - let run: Array<[number, number]> = []; + // (points outside the view are handled by the clip rect). Each point + // is [x, y, yBand] — yBand only used when s.bandTo is set. + const runs: Array> = []; + let run: Array<[number, number, number]> = []; for (let i = 0; i < timestamps.length; i++) { const v = s.data[i]; - if (v === null || v === undefined || !isFinite(v)) { + const b = s.bandTo?.[i]; + const bandInvalid = s.bandTo != null && (b === null || b === undefined || !isFinite(b)); + if (v === null || v === undefined || !isFinite(v) || bandInvalid) { if (run.length > 0) runs.push(run); run = []; continue; } - run.push([xPix(timestamps[i]), yPix(v, axis)]); + run.push([xPix(timestamps[i]), yPix(v, axis), s.bandTo ? yPix(b as number, axis) : 0]); } if (run.length > 0) runs.push(run); @@ -551,8 +587,13 @@ ctx.beginPath(); ctx.moveTo(points[0][0], points[0][1]); for (let i = 1; i < points.length; i++) ctx.lineTo(points[i][0], points[i][1]); - ctx.lineTo(points[points.length - 1][0], baseline); - ctx.lineTo(points[0][0], baseline); + if (s.bandTo) { + // close the polygon along the second line, walked backwards + for (let i = points.length - 1; i >= 0; i--) ctx.lineTo(points[i][0], points[i][2]); + } else { + ctx.lineTo(points[points.length - 1][0], baseline); + ctx.lineTo(points[0][0], baseline); + } ctx.closePath(); ctx.globalAlpha = s.fillOpacity ?? 0.15; ctx.fillStyle = s.color; @@ -789,29 +830,6 @@
- {#if showLegend && series.length > 0} -
- {#each series.filter((s) => s.showInLegend !== false) as s (s.name)} - - {/each} -
- {/if} -
{/if}
+ + + {#if showLegend && series.length > 0} +
+ {#each series.filter((s) => s.showInLegend !== false) as s (s.name)} + + {/each} +
+ {/if}
diff --git a/src/lib/components/charts/ChartContainer.svelte b/src/lib/components/charts/ChartContainer.svelte index 4cfab83..7710f99 100644 --- a/src/lib/components/charts/ChartContainer.svelte +++ b/src/lib/components/charts/ChartContainer.svelte @@ -61,7 +61,7 @@
('theme', 'system'); + +/** Selected forecast model, shared across the whole site. */ +export const storedModel = persisted('selected_model', 'best_match'); + +/** Which variables are visible in the hourly table and the meteograms. */ +export interface VariablePrefs { + table: Record; + charts: Record; +} + +export const defaultVariablePrefs: VariablePrefs = { + table: { + icons: true, + temperature: true, + feels: true, + wind: true, + humidity: true, + clouds: true, + precipitation: true + }, + charts: { + temperature: true, + cloud_cover: true, + precipitation: true, + precipitation_probability: true, + wind: true, + humidity: true + } +}; + +export const storedVariablePrefs = persisted('variable_prefs', defaultVariablePrefs); diff --git a/src/routes/layout.css b/src/routes/layout.css index b3a64a6..2ab538a 100644 --- a/src/routes/layout.css +++ b/src/routes/layout.css @@ -118,6 +118,14 @@ } @layer base { + /* color-scheme drives native widgets AND propagates into embedded + iframes (the open-meteo map reads it via prefers-color-scheme) */ + :root { + color-scheme: light; + } + .dark { + color-scheme: dark; + } * { @apply border-border outline-ring/50; } diff --git a/src/routes/weather/compare/[location]/+page.svelte b/src/routes/weather/compare/[location]/+page.svelte index 3780627..aeb8ddd 100644 --- a/src/routes/weather/compare/[location]/+page.svelte +++ b/src/routes/weather/compare/[location]/+page.svelte @@ -1,8 +1,9 @@ @@ -15,8 +15,9 @@ - -
+ +