42 ? "max-h-48 overflow-y-auto pr-1" : ""}`}
+ >
+ {years.map((year) => {
const isSelected = selected.has(year);
// Other years stay clickable mid-load so they can be dropped at once.
const isLocked = isSelected && loading && selectedYears.length === 1;
@@ -67,7 +96,7 @@ export function YearPicker({
aria-label={String(year)}
className="aria-pressed:bg-brand/15 aria-pressed:text-brand"
>
- {year.toString().slice(2)}
+ {oneCentury ? year.toString().slice(2) : year}
);
})}
diff --git a/src/components/plots/FingerprintPlot.tsx b/src/components/plots/FingerprintPlot.tsx
index a0dbe0a..b7d0432 100644
--- a/src/components/plots/FingerprintPlot.tsx
+++ b/src/components/plots/FingerprintPlot.tsx
@@ -6,6 +6,7 @@ import {
COLORMAPS,
type ColormapId,
fingerprintColorScale,
+ labelExtent,
} from "@/lib/map/fingerprintScale";
import {
formatSeriesValue,
@@ -13,7 +14,9 @@ import {
} from "@/lib/plots/chartTheme";
import {
drawFingerprint,
+ columnLabel,
fingerprintGrid,
+ rowLabel,
type FingerprintLayout,
FINGERPRINT_STRIP,
FINGERPRINT_STRIP_MAX,
@@ -27,7 +30,6 @@ import {
import {
binCountLabel,
type BinUnit,
- formatBinLabel,
formatBinShortLabel,
} from "@/lib/plots/timeBins";
import { shortTimeBasis } from "@/lib/zarr/localTime";
@@ -278,6 +280,11 @@ export function FingerprintPlot({
setHover(target ? { target, left, top, flip, anchor } : null);
};
+ const rows = grid.hoursPerDay;
+ const seasonal = grid.seasonal !== undefined;
+ const suffix =
+ timeBasisLabel && !seasonal ? ` ${shortTimeBasis(timeBasisLabel)}` : "";
+
const stripPointAt = (
kind: "column" | "hour",
index: number,
@@ -289,21 +296,22 @@ export function FingerprintPlot({
const std = formatSeriesValue(stats.std[index] as number);
if (kind === "column") {
const bin = grid.bins[index]!;
+ const across = seasonal
+ ? binCountLabel(count, "year")
+ : `${count} ${rows === 1 ? "day" : "step"}${count === 1 ? "" : "s"}`;
return {
- label: formatBinLabel(bin, scale),
- shortLabel: formatBinShortLabel(bin, scale),
+ label: columnLabel(grid, index),
+ shortLabel: seasonal
+ ? columnLabel(grid, index)
+ : formatBinShortLabel(bin, scale),
value: stats.mean[index] as number,
- spread:
- count > 0
- ? `Std ${std} across ${count} hour${count === 1 ? "" : "s"}`
- : undefined,
+ spread: count > 0 ? `Std ${std} across ${across}` : undefined,
};
}
- const time = `${String(index).padStart(2, "0")}:00`;
- const suffix = timeBasisLabel ? ` ${shortTimeBasis(timeBasisLabel)}` : "";
+ const row = rowLabel(grid, index);
return {
- label: `${time}${suffix}`,
- shortLabel: time,
+ label: `${row}${suffix}`,
+ shortLabel: row,
value: stats.mean[index] as number,
spread:
count > 0 ? `Std ${std} across ${binCountLabel(count, scale)}` : undefined,
@@ -317,14 +325,15 @@ export function FingerprintPlot({
): TooltipPoint | undefined => {
const bin = grid.bins[col];
if (!bin || hour < 0) return undefined;
- const at = col * hoursPerDay + hour;
+ const at = col * rows + hour;
const count = grid.count[at] ?? 0;
- const time = `${String(hour).padStart(2, "0")}:00`;
- const suffix = timeBasisLabel ? ` ${shortTimeBasis(timeBasisLabel)}` : "";
+ const row = rowLabel(grid, hour);
+ const column = columnLabel(grid, col);
+ const columnShort = seasonal ? column : formatBinShortLabel(bin, scale);
return {
- label: `${formatBinLabel(bin, scale)} · ${time}${suffix}`,
+ label: seasonal ? `${column} ${row}` : `${column} · ${row}${suffix}`,
shortLabel:
- short === "time" ? time : `${formatBinShortLabel(bin, scale)} ${time}`,
+ short === "time" ? row : seasonal ? `${column} ${row}` : `${columnShort} ${row}`,
value: grid.mean[at] as number,
spread:
scale !== "day" && count > 0
@@ -343,7 +352,7 @@ export function FingerprintPlot({
if (target.kind === "hour") return stripPointAt("hour", target.hour - 1);
return target.hour > 0
? pointAt(target.col, target.hour - 1, "time")
- : pointAt(target.col - 1, hoursPerDay - 1, "full");
+ : pointAt(target.col - 1, rows - 1, "full");
};
// A change against a pin only reads sensibly between like targets.
const comparable = (a: Hover, b: Hover) => a.target.kind === b.target.kind;
@@ -484,22 +493,28 @@ export function FingerprintPlot({
/>
- {formatSeriesValue(-extents.negMax)}
-
-
- 0
+ {formatSeriesValue(extents.pivot - labelExtent(extents.negMax))}
+ {legend.zeroPct > 12 && legend.zeroPct < 88 ? (
+
+ {extents.pivot === 0 ? "0" : formatSeriesValue(extents.pivot)}
+
+ ) : null}
- {formatSeriesValue(extents.posMax)}
+ {formatSeriesValue(extents.pivot + labelExtent(extents.posMax))}
- {binCountLabel(nCols, scale)} × {hoursPerDay} hours
- {timeBasisLabel ? ` · ${timeBasisLabel}` : ""}
+ {grid.seasonal
+ ? `${binCountLabel(grid.seasonal.years.length, "year")} × ${binCountLabel(nCols, scale)} of the year`
+ : `${binCountLabel(nCols, scale)} × ${rows} ${
+ rows === 24 ? "hours" : rows === 1 ? "step a day" : "steps a day"
+ }`}
+ {timeBasisLabel && !grid.seasonal ? ` · ${timeBasisLabel}` : ""}
{units ? ` · ${units}` : ""}
{margins ? " · strips show the mean ±1 std of each row and column" : ""}
diff --git a/src/components/plots/MapSideTooltip.tsx b/src/components/plots/MapSideTooltip.tsx
index 2c217fb..aaeaeb8 100644
--- a/src/components/plots/MapSideTooltip.tsx
+++ b/src/components/plots/MapSideTooltip.tsx
@@ -84,11 +84,16 @@ export type TooltipPoint = {
spread?: string;
};
+/**
+ * The difference in the variable's own units. A percentage means nothing for
+ * kelvin, degrees Celsius or a signed flux, and blows up near zero.
+ */
function formatChange(point: TooltipPoint, base?: TooltipPoint) {
- if (!base || base.value === 0 || !Number.isFinite(base.value)) return null;
- if (!Number.isFinite(point.value)) return null;
- const change = ((point.value - base.value) / Math.abs(base.value)) * 100;
- return `${change >= 0 ? "+" : "−"}${Math.abs(change).toFixed(1)}% vs ${base.shortLabel}`;
+ if (!base || !Number.isFinite(base.value) || !Number.isFinite(point.value)) {
+ return null;
+ }
+ const change = point.value - base.value;
+ return `${change >= 0 ? "+" : "−"}${formatSeriesValue(Math.abs(change))} vs ${base.shortLabel}`;
}
function PointRows({
diff --git a/src/components/plots/TimeSeriesPlot.tsx b/src/components/plots/TimeSeriesPlot.tsx
index 9e08e19..bb4f58a 100644
--- a/src/components/plots/TimeSeriesPlot.tsx
+++ b/src/components/plots/TimeSeriesPlot.tsx
@@ -1,6 +1,7 @@
"use client";
import { useMemo, useRef, useState, type Ref } from "react";
+import { activeSource } from "@/lib/zarr/activeSource";
import {
Area,
CartesianGrid,
@@ -12,9 +13,12 @@ import {
} from "recharts";
import {
formatSeriesValue,
+ formatTickValue,
TIME_SERIES_CHART_MARGIN,
TIME_SERIES_PLOT_HEIGHT,
+ valueDomain,
} from "@/lib/plots/chartTheme";
+import { niceTicks } from "@/lib/plots/fingerprintMargins";
import {
lineSeries,
type LinePoint,
@@ -48,12 +52,16 @@ const MEANS_LABEL: Record