strip more seamless and margin improvements

This commit is contained in:
Vincent van der Wal
2026-08-01 11:38:02 +02:00
parent 99c1a89537
commit a0379a1fd6
13 changed files with 706 additions and 188 deletions
+44 -10
View File
@@ -152,6 +152,10 @@
group?: string;
/** Fixed left-axis minimum (otherwise derived from data, including 0) */
yMin?: number;
/** Minimum breathing room (axis units) above the left-axis data range. */
yPadTop?: number;
/** Minimum breathing room (axis units) below the left-axis data range. */
yPadBottom?: number;
/** Fixed left-axis maximum */
yMax?: number;
/** Force the derived left axis to include zero (default true) */
@@ -196,6 +200,8 @@
group,
yMin,
yMax,
yPadTop,
yPadBottom,
zeroBaseLeft = true,
yMinRight,
yMaxRight,
@@ -302,9 +308,11 @@
// and the right-axis labels are drawn overlaid on top instead (see below).
let padRight = $derived(isNarrow ? 6 : hasRightAxis || reserveRightAxis ? 56 : 20);
// Icon rows across the top: pictograms and/or wind arrows. reserveTopRows keeps
// a stacked row of charts the same height even if some have fewer icon rows.
// a stacked row of charts the same height even if some have fewer icon rows
// but only on wide screens: on mobile that uniform band wastes precious
// vertical space, so each chart reserves just its own rows there.
let ownIconRows = $derived((pictograms.length > 0 ? 1 : 0) + (windArrows.length > 0 ? 1 : 0));
let iconRows = $derived(Math.max(ownIconRows, reserveTopRows));
let iconRows = $derived(isNarrow ? ownIconRows : Math.max(ownIconRows, reserveTopRows));
// tighter top/bottom gutters on mobile so charts don't waste vertical space
const iconRowH = $derived(isNarrow ? 30 : ICON_ROW_H);
let padTop = $derived((title ? (subtitle ? 66 : 46) : isNarrow ? 14 : 28) + iconRows * iconRowH);
@@ -353,16 +361,38 @@
return [lo, hi];
}
function buildScale(lo: number, hi: number, loFixed: boolean, hiFixed: boolean): Scale {
function buildScale(
lo: number,
hi: number,
loFixed: boolean,
hiFixed: boolean,
halfStepBounds = false
): Scale {
const step = niceNum(niceNum(Math.max(hi - lo, 1e-9), false) / 4, true);
const min = loFixed ? lo : Math.floor(lo / step) * step;
const max = hiFixed ? hi : Math.ceil(hi / step) * step;
// Padded axes (e.g. temperature) may end on HALF steps — 5° when ticks
// are every 10° — so the requested margin isn't inflated to a whole step.
// Tick drawing starts at the first full-step multiple, so a half-step
// bound gets no label or gridline of its own.
const snap = halfStepBounds ? step / 2 : step;
const min = loFixed ? lo : Math.floor(lo / snap) * snap;
const max = hiFixed ? hi : Math.ceil(hi / snap) * snap;
return { min, max: max > min ? max : min + step, step };
}
/** First tick at or above the scale minimum (bounds may sit on half steps). */
function firstTick(scale: Scale): number {
return Math.ceil((scale.min - 1e-9) / scale.step) * scale.step;
}
let leftScale = $derived.by((): Scale => {
const [dLo, dHi] = dataExtent('left', zeroBaseLeft);
return buildScale(yMin ?? dLo, yMax ?? dHi, yMin !== undefined, yMax !== undefined);
let [dLo, dHi] = dataExtent('left', zeroBaseLeft);
// requested breathing room around the data (e.g. temperature): at least
// the given units, growing with wide ranges so it stays proportionate
const span = dHi - dLo;
const padded = yPadTop != null || yPadBottom != null;
if (yMax === undefined && yPadTop) dHi += Math.max(yPadTop, span * 0.08);
if (yMin === undefined && yPadBottom) dLo -= Math.max(yPadBottom, span * 0.12);
return buildScale(yMin ?? dLo, yMax ?? dHi, yMin !== undefined, yMax !== undefined, padded);
});
let rightScale = $derived.by((): Scale => {
@@ -839,7 +869,11 @@
ctx.font = font;
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
for (let v = leftScale.min; v <= leftScale.max + leftScale.step / 2; v += leftScale.step) {
for (
let v = firstTick(leftScale);
v <= leftScale.max + leftScale.step / 2;
v += leftScale.step
) {
const y = yPix(v, 'left');
ctx.strokeStyle = gridColor;
ctx.lineWidth = 1;
@@ -858,7 +892,7 @@
ctx.textAlign = 'left';
ctx.fillStyle = textColor;
for (
let v = rightScale.min;
let v = firstTick(rightScale);
v <= rightScale.max + rightScale.step / 2;
v += rightScale.step
) {
@@ -1153,7 +1187,7 @@
ctx.lineWidth = 3;
ctx.lineJoin = 'round';
for (
let v = rightScale.min;
let v = firstTick(rightScale);
v <= rightScale.max + rightScale.step / 2;
v += rightScale.step
) {