temperature chart and table alignments
This commit is contained in:
@@ -31,6 +31,9 @@
|
||||
width?: number;
|
||||
/** Draw a low-alpha area fill below (or above, on inverted axes) the line */
|
||||
fill?: boolean;
|
||||
/** Area fill coloured by the value scale (segmentColor), fading to
|
||||
* transparent ~20px below the series minimum. */
|
||||
gradientFill?: 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)[];
|
||||
@@ -50,6 +53,9 @@
|
||||
shortName?: string;
|
||||
/** Colour each line segment by value (e.g. a temperature colour scale) */
|
||||
segmentColor?: (value: number, index: number) => string;
|
||||
/** Draw the line itself in the theme foreground (black/white), ignoring
|
||||
* segmentColor for the stroke (segmentColor still colours any fill) */
|
||||
foregroundLine?: boolean;
|
||||
/** Draw a contrasting halo (black in light mode, white in dark) under the line */
|
||||
outline?: boolean;
|
||||
/** Annotate local minima / maxima with their value */
|
||||
@@ -101,6 +107,14 @@
|
||||
export function groupRange(name: string): { start: number; end: number } | null {
|
||||
return groups[name]?.range ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current shared crosshair time of a group (epoch seconds, or null), reactive.
|
||||
* Lets outside UI (e.g. the hourly table) mirror the chart's hovered timestep.
|
||||
*/
|
||||
export function groupHover(name: string): number | null {
|
||||
return groups[name]?.hover ?? null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -202,12 +216,66 @@
|
||||
const MIN_SPAN = 2 * 3600; // minimum zoom window: 2 hours
|
||||
const HOUR = 3600;
|
||||
// Top icon rows (weather pictograms / wind arrows)
|
||||
const ICON_ROW_H = 30; // reserved height per icon row
|
||||
const ICON_BAND_H = 28; // visible band height
|
||||
const ICON_PX = 26; // pictogram size
|
||||
const ICON_ROW_H = 40; // reserved height per icon row
|
||||
const ICON_BAND_H = 38; // visible band height
|
||||
const ICON_PX = 25; // pictogram size (a touch smaller than the arrows)
|
||||
const ARROW_PX = 36; // wind-direction arrow size
|
||||
// edge inset used for BOTH rows so pictograms and arrows clamp to the same
|
||||
// centre and stay aligned with each other
|
||||
const ICON_EDGE = ARROW_PX / 2;
|
||||
// Puffy cloud band: 100% cover hangs 40px from the top of the plot
|
||||
const CLOUD_BAND_MAX = 40;
|
||||
|
||||
/** Return a colour string with the given alpha (handles rgb/rgba/#hex). */
|
||||
function withAlpha(color: string, alpha: number): string {
|
||||
const m = color.match(/rgba?\(([^)]+)\)/);
|
||||
if (m) {
|
||||
const [r, g, b] = m[1].split(',').map((p) => parseFloat(p));
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
if (color[0] === '#') {
|
||||
const h = color.slice(1);
|
||||
const n =
|
||||
h.length === 3
|
||||
? h
|
||||
.split('')
|
||||
.map((c) => c + c)
|
||||
.join('')
|
||||
: h;
|
||||
const r = parseInt(n.slice(0, 2), 16);
|
||||
const g = parseInt(n.slice(2, 4), 16);
|
||||
const b = parseInt(n.slice(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
// Multiply an rgb/hex colour toward black by `amount` (0 = unchanged, 1 = black).
|
||||
function darken(color: string, amount: number): string {
|
||||
const f = 1 - amount;
|
||||
const m = color.match(/rgba?\(([^)]+)\)/);
|
||||
if (m) {
|
||||
const [r, g, b, a] = m[1].split(',').map((p) => parseFloat(p));
|
||||
const alpha = isNaN(a) ? 1 : a;
|
||||
return `rgba(${Math.round(r * f)}, ${Math.round(g * f)}, ${Math.round(b * f)}, ${alpha})`;
|
||||
}
|
||||
if (color[0] === '#') {
|
||||
const h = color.slice(1);
|
||||
const n =
|
||||
h.length === 3
|
||||
? h
|
||||
.split('')
|
||||
.map((c) => c + c)
|
||||
.join('')
|
||||
: h;
|
||||
const r = Math.round(parseInt(n.slice(0, 2), 16) * f);
|
||||
const g = Math.round(parseInt(n.slice(2, 4), 16) * f);
|
||||
const b = Math.round(parseInt(n.slice(4, 6), 16) * f);
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
// ─── State ──────────────────────────────────────────────────────────────────
|
||||
|
||||
let containerEl: HTMLDivElement | undefined = $state();
|
||||
@@ -455,6 +523,8 @@
|
||||
for (const p of pictograms) {
|
||||
if (p.t < viewStart || p.t > viewEnd) continue;
|
||||
const x = iconBandX(p.t);
|
||||
// keep clear of the band edges so the first/last never bunch or clip
|
||||
if (x < ICON_EDGE || x > iconBandWidth - ICON_EDGE) continue;
|
||||
if (x - lastX < 40) continue;
|
||||
out.push({ x, icon: p.icon });
|
||||
lastX = x;
|
||||
@@ -470,6 +540,7 @@
|
||||
for (const a of windArrows) {
|
||||
if (a.t < viewStart || a.t > viewEnd) continue;
|
||||
const x = iconBandX(a.t);
|
||||
if (x < ICON_EDGE || x > iconBandWidth - ICON_EDGE) continue;
|
||||
if (x - lastX < 40) continue;
|
||||
out.push({ x, deg: a.deg });
|
||||
lastX = x;
|
||||
@@ -582,7 +653,9 @@
|
||||
const gridColor = cssColor('--border', 'rgba(0, 0, 0, 0.1)');
|
||||
const bgColor = cssColor('--card', '#ffffff');
|
||||
const dark = document.documentElement.classList.contains('dark');
|
||||
const outlineColor = dark ? '#ffffff' : '#000000';
|
||||
// halo matches the background (white in light mode, dark in dark mode) so a
|
||||
// line reads clearly where it crosses others
|
||||
const outlineColor = bgColor;
|
||||
|
||||
const plotRight = padLeft + plotW;
|
||||
const plotBottom = padTop + plotH;
|
||||
@@ -753,6 +826,59 @@
|
||||
for (const points of runs) {
|
||||
if (points.length === 0) continue;
|
||||
|
||||
if (s.gradientFill && points.length > 1) {
|
||||
// Coloured fill anchored at the curve, fading out towards zero: down
|
||||
// for positive values, up for all-negative values. Full opacity near
|
||||
// the far-from-zero extreme, fading ~30px past the near-zero extreme.
|
||||
let minV = Infinity;
|
||||
let maxV = -Infinity;
|
||||
for (const p of points) {
|
||||
const val = s.data[p[3]] as number;
|
||||
if (val < minV) minV = val;
|
||||
if (val > maxV) maxV = val;
|
||||
}
|
||||
const goUp = maxV <= 0; // all non-positive → fill toward zero (upward)
|
||||
const maxYp = yPix(maxV, axis);
|
||||
const minYp = yPix(minV, axis);
|
||||
// anchor = opaque end (far-from-zero extreme); fade = transparent end
|
||||
const anchorY = goUp ? minYp : maxYp;
|
||||
const nearY = goUp ? maxYp : minYp;
|
||||
// fade runs a good stretch past the near extreme, but stops short of
|
||||
// the plot edge (~60% of the way there)
|
||||
const fadeY = goUp
|
||||
? Math.max(padTop, maxYp - (maxYp - padTop) * 0.6)
|
||||
: Math.min(plotBottom, minYp + (plotBottom - minYp) * 0.6);
|
||||
const anchorV = goUp ? minV : maxV;
|
||||
const nearV = goUp ? maxV : minV;
|
||||
const span = fadeY - anchorY;
|
||||
// begin the fade a touch (~10px) BEFORE the near-zero extreme
|
||||
const fadeStartY = nearY - Math.sign(nearY - anchorY) * 10;
|
||||
const fadeStart =
|
||||
span !== 0 ? Math.max(0, Math.min(0.95, (fadeStartY - anchorY) / span)) : 0.6;
|
||||
const colorFn = s.segmentColor ?? (() => s.color);
|
||||
const FULL = 0.8;
|
||||
const grad = ctx.createLinearGradient(0, anchorY, 0, fadeY);
|
||||
// full colour from the curve down to just before the near extreme…
|
||||
const STOPS = 6;
|
||||
for (let k = 0; k <= STOPS; k++) {
|
||||
const t = k / STOPS;
|
||||
const val = anchorV + t * (nearV - anchorV);
|
||||
grad.addColorStop(t * fadeStart, withAlpha(colorFn(val, 0), FULL));
|
||||
}
|
||||
// …then a long, gentle fade out to the plot edge
|
||||
grad.addColorStop(fadeStart, withAlpha(colorFn(nearV, 0), FULL));
|
||||
grad.addColorStop(1, withAlpha(colorFn(nearV, 0), 0));
|
||||
|
||||
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], fadeY);
|
||||
ctx.lineTo(points[0][0], fadeY);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
if (s.fill && points.length > 1) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(points[0][0], points[0][1]);
|
||||
@@ -775,7 +901,7 @@
|
||||
if (lineWidth > 0) {
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.lineCap = 'round';
|
||||
ctx.setLineDash(s.dashed ? [6, 4] : []);
|
||||
ctx.setLineDash(s.dashed ? [8, 8] : []);
|
||||
|
||||
// Contrasting halo drawn under the line so a multi-colour line
|
||||
// stays legible over any background.
|
||||
@@ -789,7 +915,7 @@
|
||||
}
|
||||
|
||||
ctx.lineWidth = lineWidth;
|
||||
if (s.segmentColor) {
|
||||
if (s.segmentColor && !s.foregroundLine) {
|
||||
// Colour each segment by its value (temperature colour scale).
|
||||
// `idx[i]` maps a run point back to its source data index.
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
@@ -800,6 +926,37 @@
|
||||
ctx.lineTo(points[i][0], points[i][1]);
|
||||
ctx.stroke();
|
||||
}
|
||||
} else if (s.foregroundLine) {
|
||||
// Foreground line drawn on top of its own fill. Stroke the whole
|
||||
// path once with a horizontal gradient sampled from the value
|
||||
// colour scale, so the colour flows smoothly along the line
|
||||
// instead of stepping at each data point.
|
||||
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]);
|
||||
if (s.segmentColor && points.length > 1) {
|
||||
const x0 = points[0][0];
|
||||
const x1 = points[points.length - 1][0];
|
||||
const span = x1 - x0 || 1;
|
||||
const grad = ctx.createLinearGradient(x0, 0, x1, 0);
|
||||
let prevT = -1;
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
let t = (points[i][0] - x0) / span;
|
||||
t = t < 0 ? 0 : t > 1 ? 1 : t;
|
||||
if (t <= prevT) t = prevT + 1e-6; // keep stops strictly increasing
|
||||
if (t > 1) t = 1;
|
||||
prevT = t;
|
||||
// a touch darker than the fill so the line reads as its edge
|
||||
grad.addColorStop(
|
||||
t,
|
||||
darken(s.segmentColor(s.data[points[i][3]] as number, points[i][3]), 0.05)
|
||||
);
|
||||
}
|
||||
ctx.strokeStyle = grad;
|
||||
} else {
|
||||
ctx.strokeStyle = strongColor;
|
||||
}
|
||||
ctx.stroke();
|
||||
} else {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(points[0][0], points[0][1]);
|
||||
@@ -829,7 +986,12 @@
|
||||
const x = xPix(t);
|
||||
const y = yPix(v, axis);
|
||||
const label = fmt(v);
|
||||
const ly = ext.type === 'max' ? y - off : y + off + 8;
|
||||
// keep the label inside the plot vertically (never under the icon
|
||||
// band above or clipped at the bottom)
|
||||
const ly = Math.max(
|
||||
padTop + 11,
|
||||
Math.min(plotBottom - 3, ext.type === 'max' ? y - off : y + off + 8)
|
||||
);
|
||||
// keep the centred label fully inside the plot so it never clips
|
||||
const halfW = ctx.measureText(label).width / 2 + 2;
|
||||
const lx = Math.max(padLeft + halfW, Math.min(plotRight - halfW, x));
|
||||
@@ -1142,11 +1304,12 @@
|
||||
style:height="{ICON_BAND_H}px"
|
||||
>
|
||||
{#each visiblePictograms as p (p.x)}
|
||||
{@const cx = Math.max(ICON_EDGE, Math.min(iconBandWidth - ICON_EDGE, p.x))}
|
||||
<svg
|
||||
class="absolute top-px fill-foreground"
|
||||
class="absolute top-1/2 -translate-y-1/2 fill-foreground"
|
||||
width={ICON_PX}
|
||||
height={ICON_PX}
|
||||
style:left="{Math.max(2, Math.min(iconBandWidth - ICON_PX - 2, p.x - ICON_PX / 2))}px"
|
||||
style:left="{cx - ICON_PX / 2}px"
|
||||
>
|
||||
<use xlink:href="/images/weather-icons/{p.icon}.svg#Layer_1"></use>
|
||||
</svg>
|
||||
@@ -1164,14 +1327,15 @@
|
||||
style:height="{ICON_BAND_H}px"
|
||||
>
|
||||
{#each visibleWindArrows as a (a.x)}
|
||||
{@const cx = Math.max(ICON_EDGE, Math.min(iconBandWidth - ICON_EDGE, a.x))}
|
||||
<span
|
||||
class="absolute top-px inline-flex items-center justify-center"
|
||||
style:width="{ICON_PX}px"
|
||||
style:height="{ICON_PX}px"
|
||||
style:left="{Math.max(2, Math.min(iconBandWidth - ICON_PX - 2, a.x - ICON_PX / 2))}px"
|
||||
style:transform="rotate({a.deg}deg)"
|
||||
class="absolute top-1/2 inline-flex items-center justify-center"
|
||||
style:width="{ARROW_PX}px"
|
||||
style:height="{ARROW_PX}px"
|
||||
style:left="{cx - ARROW_PX / 2}px"
|
||||
style:transform="translateY(-50%) rotate({a.deg}deg)"
|
||||
>
|
||||
<svg class="fill-foreground/80" width="22" height="22">
|
||||
<svg class="fill-foreground/80" width={ARROW_PX} height={ARROW_PX}>
|
||||
<use xlink:href="/images/weather-icons/wi-direction-down.svg#Layer_1"></use>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
* import { CanvasChart, buildDaylightBands, SERIES_COLORS } from '$lib/charts';
|
||||
*/
|
||||
|
||||
export { default as CanvasChart, setGroupHover, groupRange } from './CanvasChart.svelte';
|
||||
export {
|
||||
default as CanvasChart,
|
||||
setGroupHover,
|
||||
groupRange,
|
||||
groupHover
|
||||
} from './CanvasChart.svelte';
|
||||
export type { ChartSeries } from './CanvasChart.svelte';
|
||||
|
||||
export { buildDaylightBands } from './bands';
|
||||
|
||||
@@ -106,12 +106,18 @@
|
||||
column on md+ (main has 2rem padding) for extra readability. */
|
||||
margin-left: -0.75rem;
|
||||
margin-right: -0.75rem;
|
||||
/* overflow-y is pinned (never `visible`): a bare `overflow-x: auto`
|
||||
makes the browser compute overflow-y as `auto` too, which turns the
|
||||
chart into a 1-2px vertical micro-scroller that swallows page scroll. */
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.chart-bleed.no-bleed {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
/* content fits the column, so no horizontal scroller is needed */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
|
||||
@@ -19,13 +19,12 @@
|
||||
</ChartToolbar>
|
||||
-->
|
||||
<script module lang="ts">
|
||||
/** Anything that can export itself as a PNG data URL (e.g. a CanvasChart). */
|
||||
export interface DownloadableChart {
|
||||
getPngDataUrl(): string | null;
|
||||
}
|
||||
export type { DownloadableChart } from './downloadChartsPng';
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { type DownloadableChart, downloadChartsPng } from './downloadChartsPng';
|
||||
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
// ─── Props ──────────────────────────────────────────────────────────────────
|
||||
@@ -58,70 +57,11 @@
|
||||
|
||||
// ─── Download ───────────────────────────────────────────────────────────────
|
||||
|
||||
function loadImage(src: string): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = () => resolve(img);
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
/** Resolves the page background so exports match the current theme. */
|
||||
function exportBackground(): string {
|
||||
const bg = getComputedStyle(document.documentElement).getPropertyValue('--background').trim();
|
||||
return bg || '#ffffff';
|
||||
}
|
||||
|
||||
function triggerDownload(url: string, name: string): void {
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = name;
|
||||
link.style.display = 'none';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
requestAnimationFrame(() => {
|
||||
document.body.removeChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleDownload(): Promise<void> {
|
||||
if (!hasCharts || downloading) return;
|
||||
|
||||
downloading = true;
|
||||
|
||||
try {
|
||||
const dataUrls = charts
|
||||
.filter((chart): chart is DownloadableChart => chart != null)
|
||||
.map((chart) => chart.getPngDataUrl())
|
||||
.filter((url): url is string => url !== null);
|
||||
if (dataUrls.length === 0) return;
|
||||
|
||||
const images = (await Promise.all(dataUrls.map(loadImage))).filter(
|
||||
(img) => img.naturalWidth > 0
|
||||
);
|
||||
if (images.length === 0) return;
|
||||
|
||||
const maxWidth = Math.max(...images.map((img) => img.naturalWidth));
|
||||
const totalHeight = images.reduce((sum, img) => sum + img.naturalHeight, 0);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = maxWidth;
|
||||
canvas.height = totalHeight;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
ctx.fillStyle = exportBackground();
|
||||
ctx.fillRect(0, 0, maxWidth, totalHeight);
|
||||
|
||||
let y = 0;
|
||||
for (const img of images) {
|
||||
ctx.drawImage(img, 0, y);
|
||||
y += img.naturalHeight;
|
||||
}
|
||||
|
||||
triggerDownload(canvas.toDataURL('image/png'), `${fileName}.png`);
|
||||
await downloadChartsPng(charts, fileName);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
downloading = false;
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Shared PNG export for charts.
|
||||
*
|
||||
* Stitches one or more chart images vertically onto a single canvas (over the
|
||||
* current theme background) and triggers a download. Used by both the standalone
|
||||
* ChartToolbar and the inline toolbar buttons.
|
||||
*/
|
||||
|
||||
/** Anything that can export itself as a PNG data URL (e.g. a CanvasChart). */
|
||||
export interface DownloadableChart {
|
||||
getPngDataUrl(): string | null;
|
||||
}
|
||||
|
||||
function loadImage(src: string): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = () => resolve(img);
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
/** Resolves the page background so exports match the current theme. */
|
||||
function exportBackground(): string {
|
||||
const bg = getComputedStyle(document.documentElement).getPropertyValue('--background').trim();
|
||||
return bg || '#ffffff';
|
||||
}
|
||||
|
||||
function triggerDownload(url: string, name: string): void {
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = name;
|
||||
link.style.display = 'none';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
requestAnimationFrame(() => {
|
||||
document.body.removeChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stitch the given charts into one PNG and download it. Resolves once the
|
||||
* download has been triggered (or immediately if there is nothing to export).
|
||||
*/
|
||||
export async function downloadChartsPng(
|
||||
charts: Array<DownloadableChart | undefined | null>,
|
||||
fileName: string
|
||||
): Promise<void> {
|
||||
const dataUrls = charts
|
||||
.filter((chart): chart is DownloadableChart => chart != null)
|
||||
.map((chart) => chart.getPngDataUrl())
|
||||
.filter((url): url is string => url !== null);
|
||||
if (dataUrls.length === 0) return;
|
||||
|
||||
const images = (await Promise.all(dataUrls.map(loadImage))).filter((img) => img.naturalWidth > 0);
|
||||
if (images.length === 0) return;
|
||||
|
||||
const maxWidth = Math.max(...images.map((img) => img.naturalWidth));
|
||||
const totalHeight = images.reduce((sum, img) => sum + img.naturalHeight, 0);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = maxWidth;
|
||||
canvas.height = totalHeight;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
ctx.fillStyle = exportBackground();
|
||||
ctx.fillRect(0, 0, maxWidth, totalHeight);
|
||||
|
||||
let y = 0;
|
||||
for (const img of images) {
|
||||
ctx.drawImage(img, 0, y);
|
||||
y += img.naturalHeight;
|
||||
}
|
||||
|
||||
triggerDownload(canvas.toDataURL('image/png'), `${fileName}.png`);
|
||||
}
|
||||
@@ -9,3 +9,4 @@
|
||||
|
||||
export { default as ChartContainer } from './ChartContainer.svelte';
|
||||
export { default as ChartToolbar } from './ChartToolbar.svelte';
|
||||
export { downloadChartsPng, type DownloadableChart } from './downloadChartsPng';
|
||||
|
||||
@@ -65,7 +65,14 @@ export const defaultVariablePrefs: VariablePrefs = {
|
||||
wind: true,
|
||||
humidity: true,
|
||||
clouds: true,
|
||||
precipitation: true
|
||||
precipitation: true,
|
||||
// extra rows, off by default
|
||||
dew_point: false,
|
||||
gusts: false,
|
||||
pressure: false,
|
||||
uv: false,
|
||||
visibility: false,
|
||||
snowfall: false
|
||||
},
|
||||
charts: {
|
||||
temperature: true,
|
||||
@@ -79,6 +86,9 @@ export const defaultVariablePrefs: VariablePrefs = {
|
||||
|
||||
export const storedVariablePrefs = persisted<VariablePrefs>('variable_prefs', defaultVariablePrefs);
|
||||
|
||||
/** Hourly table interval: 3-hourly (default) or 1-hourly. */
|
||||
export const storedHourlyInterval = persisted<1 | 3>('hourly_interval', 3);
|
||||
|
||||
/**
|
||||
* Meteogram layout: an ordered list of chart panels, each holding an ordered
|
||||
* list of variable keys (see the chart variable registry). Users drag
|
||||
@@ -90,9 +100,9 @@ export interface ChartPanel {
|
||||
}
|
||||
|
||||
export const defaultChartLayout: ChartPanel[] = [
|
||||
{ id: 'panel-1', variables: ['weather_icons', 'temperature', 'cloud_cover'] },
|
||||
{ id: 'panel-2', variables: ['precipitation', 'precipitation_probability'] },
|
||||
{ id: 'panel-3', variables: ['wind', 'wind_direction', 'humidity'] }
|
||||
{ id: 'panel-1', variables: ['temperature', 'weather_icons'] },
|
||||
{ id: 'panel-2', variables: ['precipitation', 'precipitation_probability', 'cloud_cover'] },
|
||||
{ id: 'panel-3', variables: ['wind', 'wind_gusts', 'wind_direction'] }
|
||||
];
|
||||
|
||||
export const storedChartLayout = persisted<ChartPanel[]>('chart_layout_v1', defaultChartLayout);
|
||||
|
||||
Reference in New Issue
Block a user