fallback and 404

This commit is contained in:
Vincent van der Wal
2026-07-19 22:47:05 +02:00
parent baa4840b38
commit cd1a47b6c3
11 changed files with 580 additions and 212 deletions
+72 -30
View File
@@ -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<Array<[number, number]>> = [];
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<Array<[number, number, number]>> = [];
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 @@
</script>
<div bind:this={containerEl} class="relative w-full select-none {className}">
{#if showLegend && series.length > 0}
<div class="mb-1 flex flex-wrap items-center gap-x-3 gap-y-1 px-1">
{#each series.filter((s) => s.showInLegend !== false) as s (s.name)}
<button
type="button"
class="flex cursor-pointer items-center gap-1.5 text-xs transition-opacity {legendHidden.has(
s.name
)
? 'opacity-40'
: ''}"
onclick={() => toggleSeries(s.name)}
title="Toggle {s.name}"
>
<span
class="inline-block h-2.5 w-2.5 shrink-0 rounded-full"
style:background-color={s.color}
></span>
<span class="text-muted-foreground">{s.name}</span>
</button>
{/each}
</div>
{/if}
<div class="relative">
<canvas
bind:this={canvasEl}
@@ -856,4 +874,28 @@
</div>
{/if}
</div>
<!-- Legend sits below the graph -->
{#if showLegend && series.length > 0}
<div class="mt-1.5 flex flex-wrap items-center justify-center gap-x-3 gap-y-1 px-1">
{#each series.filter((s) => s.showInLegend !== false) as s (s.name)}
<button
type="button"
class="flex cursor-pointer items-center gap-1.5 text-xs transition-opacity {legendHidden.has(
s.name
)
? 'opacity-40'
: ''}"
onclick={() => toggleSeries(s.name)}
title="Toggle {s.name}"
>
<span
class="inline-block h-2.5 w-2.5 shrink-0 rounded-full"
style:background-color={s.color}
></span>
<span class="text-muted-foreground">{s.name}</span>
</button>
{/each}
</div>
{/if}
</div>