@pond-ts/charts 0.59.0 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +21 -16
- package/CHANGELOG.md +238 -1
- package/dist/AreaChart.d.ts +53 -1
- package/dist/AreaChart.js +16 -3
- package/dist/BarChart.js +6 -61
- package/dist/BarList.d.ts +22 -0
- package/dist/BarList.js +42 -9
- package/dist/CategoryAxis.d.ts +8 -4
- package/dist/CategoryAxis.js +8 -4
- package/dist/ChartContainer.d.ts +175 -3
- package/dist/ChartContainer.js +190 -11
- package/dist/ChartRow.js +2 -0
- package/dist/Layers.js +14 -4
- package/dist/XAxis.d.ts +47 -3
- package/dist/XAxis.js +165 -41
- package/dist/YAxis.d.ts +15 -1
- package/dist/YAxis.js +31 -4
- package/dist/area.d.ts +43 -1
- package/dist/area.js +122 -5
- package/dist/axis-events.d.ts +106 -0
- package/dist/axis-events.js +56 -0
- package/dist/context.d.ts +35 -2
- package/dist/format.d.ts +1 -1
- package/dist/format.js +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +6 -0
- package/dist/theme.d.ts +22 -0
- package/dist/theme.js +3 -0
- package/dist/use-band-ladder.d.ts +30 -0
- package/dist/use-band-ladder.js +81 -0
- package/dist/useChartFrame.d.ts +122 -0
- package/dist/useChartFrame.js +155 -0
- package/dist/useChartLegend.d.ts +8 -0
- package/dist/viewport.d.ts +35 -2
- package/dist/viewport.js +53 -6
- package/dist/yticks.d.ts +5 -1
- package/dist/yticks.js +5 -1
- package/package.json +3 -3
package/dist/area.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { area as d3area, curveLinear } from 'd3-shape';
|
|
2
|
-
import { baselinePxFromScale, strokeAffinePolyline, TRACE_HIT_PX, } from './line.js';
|
|
2
|
+
import { baselinePxFromScale, plotExtentOf, strokeAffinePolyline, TRACE_HIT_PX, } from './line.js';
|
|
3
3
|
import { bridgeGaps, collectGapEdges, drawGapBridges, drawGapFades, drawGapSteps, gapUnscalable, withAlpha, DEFAULT_GAP_MODE, DEFAULT_GAP_CONNECTOR_OPACITY, } from './gaps.js';
|
|
4
4
|
import { cullChartSeries } from './culling.js';
|
|
5
5
|
import { decimateM4Cached } from './decimate.js';
|
|
@@ -155,7 +155,7 @@ export function areaExtent(cs, baseline) {
|
|
|
155
155
|
* bracketed by `save`/`restore` so they don't leak into later layers. Gap edges
|
|
156
156
|
* are collected by one O(N) walk ({@link collectGapEdges}).
|
|
157
157
|
*/
|
|
158
|
-
export function drawArea(ctx, cs, xScale, yScale, style, baselineValue, curve = curveLinear, gaps = DEFAULT_GAP_MODE, gapConnectorOpacity = DEFAULT_GAP_CONNECTOR_OPACITY, decimate = true) {
|
|
158
|
+
export function drawArea(ctx, cs, xScale, yScale, style, baselineValue, curve = curveLinear, gaps = DEFAULT_GAP_MODE, gapConnectorOpacity = DEFAULT_GAP_CONNECTOR_OPACITY, decimate = true, banding) {
|
|
159
159
|
const sourceCount = cs.length; // pre-cull, pre-decimation (for draw stats)
|
|
160
160
|
const baselinePx = yScale(baselineValue);
|
|
161
161
|
// The fill gradient's vertical extent is computed from the **full** series (a
|
|
@@ -168,7 +168,13 @@ export function drawArea(ctx, cs, xScale, yScale, style, baselineValue, curve =
|
|
|
168
168
|
// instead of re-walking O(N) — the mountain@1M ceiling the bench profile
|
|
169
169
|
// flagged. A `'none'` bridge only fills interior gaps with interpolated values
|
|
170
170
|
// that stay within the finite extent, so the plain extent is exact for it too.
|
|
171
|
-
|
|
171
|
+
//
|
|
172
|
+
// **Banded** ([PND-BANDAREA]): one hard-stop pixel-space gradient carries the
|
|
173
|
+
// whole ladder for the fill AND the outline — see {@link buildBandGradient}
|
|
174
|
+
// for why a gradient rather than one clipped redraw per band.
|
|
175
|
+
const fill = banding !== undefined
|
|
176
|
+
? buildBandGradient(ctx, yScale, plotExtentOf(ctx, xScale, yScale).height, banding)
|
|
177
|
+
: buildGradient(ctx, columnFiniteExtent(cs.y, cs.length), yScale, baselinePx, style);
|
|
172
178
|
// Clip `cs` to what draws. **Decimated** (linear curve, `decimate !== false`):
|
|
173
179
|
// cull to the visible slice, then the same {@link decimateM4} pre-pass shrinks
|
|
174
180
|
// the fill + outline + gap-bridge work to O(plot width) once dense (the §2.2
|
|
@@ -237,14 +243,17 @@ export function drawArea(ctx, cs, xScale, yScale, style, baselineValue, curve =
|
|
|
237
243
|
ctx.fill();
|
|
238
244
|
ctx.restore();
|
|
239
245
|
// The outline on top: the area's top edge as a line (breaks at the same gaps
|
|
240
|
-
// as the fill), at full opacity over the graded fill.
|
|
246
|
+
// as the fill), at full opacity over the graded fill. Banded, it strokes with
|
|
247
|
+
// the same hard-stop gradient the fill used, so the line switches hue exactly
|
|
248
|
+
// where it crosses a threshold — the whole point of the ladder is that the
|
|
249
|
+
// reader sees *where* the value sits, and the edge is the value.
|
|
241
250
|
ctx.save();
|
|
242
251
|
ctx.beginPath();
|
|
243
252
|
if (outline !== null)
|
|
244
253
|
outline(ys);
|
|
245
254
|
else
|
|
246
255
|
strokeAffinePolyline(ctx, cs.x, ys, ax, ay);
|
|
247
|
-
ctx.strokeStyle = style.color;
|
|
256
|
+
ctx.strokeStyle = banding !== undefined ? fill : style.color;
|
|
248
257
|
ctx.lineWidth = style.width;
|
|
249
258
|
ctx.stroke();
|
|
250
259
|
ctx.restore();
|
|
@@ -345,6 +354,114 @@ function buildGradient(ctx, valueExtent, yScale, baselinePx, style) {
|
|
|
345
354
|
}
|
|
346
355
|
return grad;
|
|
347
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* The banded fill + stroke for `<AreaChart thresholds>` ([PND-BANDAREA]): one
|
|
359
|
+
* vertical **hard-stop gradient in pixel space**, a colour switch at every
|
|
360
|
+
* threshold crossing — `colors[0]` between `-t0` and `+t0`, `colors[k]` over
|
|
361
|
+
* magnitudes `[t(k-1), tk)` on both sides of zero. `thresholds`/`colors` arrive
|
|
362
|
+
* as a resolved {@link BandLadder} (ascending, positive, `n + 1` colours), the
|
|
363
|
+
* same currency `drawBars` takes.
|
|
364
|
+
*
|
|
365
|
+
* A gradient rather than one clipped redraw per band, and that is the
|
|
366
|
+
* load-bearing choice: K + 1 clipped passes walk the path K + 1 times and meet
|
|
367
|
+
* themselves at every boundary with an antialiased seam, where a gradient
|
|
368
|
+
* draws the identical single path once and costs O(K) colour stops. It also
|
|
369
|
+
* bands the **outline for free** — `strokeStyle` takes the same gradient, so
|
|
370
|
+
* the value line switches hue exactly at a crossing, which no per-band clip
|
|
371
|
+
* can do without shearing the stroke.
|
|
372
|
+
*
|
|
373
|
+
* The ladder is walked on the **magnitude** and mirrored below zero, exactly
|
|
374
|
+
* as `bandSpan` does for a bar: the boundary at `±tk` separates band `k` (the
|
|
375
|
+
* zero side) from band `k + 1` (the away side). Whether "away from zero" is up
|
|
376
|
+
* or down the canvas is probed from the scale itself (`t0` vs `t0 + 1`, both
|
|
377
|
+
* positive and finite by construction), so a flipped axis bands correctly. A
|
|
378
|
+
* boundary with **no position** on the scale contributes no crossing — on a
|
|
379
|
+
* log axis the negative mirrors (and zero) simply don't exist, which is the
|
|
380
|
+
* right reading. A crossing **off the plot** clamps to the gradient's ends
|
|
381
|
+
* (a real canvas throws on stops outside `[0, 1]`), which is also what makes a
|
|
382
|
+
* zoomed-in view honest: with every visible pixel inside one band, the clamp
|
|
383
|
+
* degenerates the other stops and the whole plot paints that band's colour.
|
|
384
|
+
*
|
|
385
|
+
* Falls back to the top band's flat colour when there is nothing to anchor on
|
|
386
|
+
* (no plot height, or no boundary with a position at all) — reachable only
|
|
387
|
+
* with a degenerate scale stub, since every real axis positions a positive
|
|
388
|
+
* finite value; any flat colour is equally (in)correct there, and the top
|
|
389
|
+
* band's is at least stable.
|
|
390
|
+
*
|
|
391
|
+
* Like the bar ladder, breakpoints are **absolute data values**, so the
|
|
392
|
+
* baseline plays no part here: an area resting on a non-zero floor still bands
|
|
393
|
+
* at the same heights as its neighbours — measuring from the resolved baseline
|
|
394
|
+
* instead would silently shift every breakpoint by the floor, the quiet
|
|
395
|
+
* wrongness [PND-BANDBAR2] exists to remove.
|
|
396
|
+
*/
|
|
397
|
+
export function buildBandGradient(ctx, yScale, plotHeight, banding) {
|
|
398
|
+
const { thresholds, colors } = banding;
|
|
399
|
+
const fallback = colors[colors.length - 1];
|
|
400
|
+
// Guards NaN too — `!(x > 0)`, not `x <= 0`.
|
|
401
|
+
if (!(plotHeight > 0))
|
|
402
|
+
return fallback;
|
|
403
|
+
// Axis direction: does value increase toward smaller pixels (the canvas
|
|
404
|
+
// norm)? Probed on the ladder's own first breakpoint — positive and finite
|
|
405
|
+
// by construction, so it has a position on every axis kind (linear, log,
|
|
406
|
+
// symlog). Non-finite or equal probes default to the norm.
|
|
407
|
+
const pA = yScale(thresholds[0]);
|
|
408
|
+
const pB = yScale(thresholds[0] + 1);
|
|
409
|
+
const higherValueAtSmallerPx = !(Number.isFinite(pA) &&
|
|
410
|
+
Number.isFinite(pB) &&
|
|
411
|
+
pB > pA);
|
|
412
|
+
const crossings = [];
|
|
413
|
+
for (let k = 0; k < thresholds.length; k += 1) {
|
|
414
|
+
const zeroSide = colors[k];
|
|
415
|
+
const awaySide = colors[k + 1];
|
|
416
|
+
for (const sign of [1, -1]) {
|
|
417
|
+
const v = sign * thresholds[k];
|
|
418
|
+
const px = yScale(v);
|
|
419
|
+
if (!Number.isFinite(px))
|
|
420
|
+
continue; // no position — no crossing
|
|
421
|
+
const awayAbove = sign > 0 === higherValueAtSmallerPx;
|
|
422
|
+
crossings.push(awayAbove
|
|
423
|
+
? { px, above: awaySide, below: zeroSide, k, sign }
|
|
424
|
+
: { px, above: zeroSide, below: awaySide, k, sign });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (crossings.length === 0)
|
|
428
|
+
return fallback;
|
|
429
|
+
// Sort by pixel; same-pixel same-sign crossings (a duplicate breakpoint's
|
|
430
|
+
// empty band, or distinct breakpoints collapsed by an extreme zoom) order
|
|
431
|
+
// **away-band-outermost**: the away side's colour first coming from the
|
|
432
|
+
// away direction, the zero side's first coming from zero. That makes the
|
|
433
|
+
// walk below telescope — the seed reads the true outermost band and the
|
|
434
|
+
// last stop at the pixel is the true inner colour, with the skipped bands
|
|
435
|
+
// as zero-width ghosts in between — instead of seeding one band short and
|
|
436
|
+
// blending across the region below. Bars skip an empty band the same way
|
|
437
|
+
// (`bandSpanInto` clips it to nothing); a same-pixel *opposite-sign* pair
|
|
438
|
+
// (a folded scale) has no defined order and keeps insertion order.
|
|
439
|
+
crossings.sort((a, b) => {
|
|
440
|
+
if (a.px !== b.px)
|
|
441
|
+
return a.px - b.px;
|
|
442
|
+
if (a.sign !== b.sign)
|
|
443
|
+
return 0;
|
|
444
|
+
const awayFirst = a.sign > 0 === higherValueAtSmallerPx;
|
|
445
|
+
return awayFirst ? b.k - a.k : a.k - b.k;
|
|
446
|
+
});
|
|
447
|
+
const grad = ctx.createLinearGradient(0, 0, 0, plotHeight);
|
|
448
|
+
const offsetOf = (px) => {
|
|
449
|
+
const o = px / plotHeight;
|
|
450
|
+
return o < 0 ? 0 : o > 1 ? 1 : o;
|
|
451
|
+
};
|
|
452
|
+
// Each crossing is a hard stop: two stops at one offset, old colour then
|
|
453
|
+
// new. The region above the first crossing seeds the walk; clamped
|
|
454
|
+
// off-plot crossings collapse to zero-height regions at the ends, leaving
|
|
455
|
+
// the visible span in the band it actually occupies.
|
|
456
|
+
grad.addColorStop(0, crossings[0].above);
|
|
457
|
+
for (const c of crossings) {
|
|
458
|
+
const off = offsetOf(c.px);
|
|
459
|
+
grad.addColorStop(off, c.above);
|
|
460
|
+
grad.addColorStop(off, c.below);
|
|
461
|
+
}
|
|
462
|
+
grad.addColorStop(1, crossings[crossings.length - 1].below);
|
|
463
|
+
return grad;
|
|
464
|
+
}
|
|
348
465
|
/**
|
|
349
466
|
* **Is the pointer inside this area?** The filled-region counterpart of
|
|
350
467
|
* `traceHitIndex` ([PND-TRACESEL]) — returns the nearest sample's index as
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { MouseEvent as ReactMouseEvent } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* What an axis hands its {@link AxisMouseHandler} — the raw mouse event, plus
|
|
4
|
+
* the **axis coordinate under the pointer**, which is the part a consumer
|
|
5
|
+
* cannot compute for itself (the scale lives inside the container).
|
|
6
|
+
*/
|
|
7
|
+
export interface AxisMouseEvent {
|
|
8
|
+
/**
|
|
9
|
+
* The React mouse event, verbatim — `type` says which one fired
|
|
10
|
+
* (`'click'`, `'mousemove'`, `'contextmenu'`, …), and the modifier keys,
|
|
11
|
+
* `button`, `preventDefault()` and `stopPropagation()` are all the ordinary
|
|
12
|
+
* ones. **A single handler receives every mouse event on the strip**, so
|
|
13
|
+
* switch on `event.type` (or ignore the ones you don't want).
|
|
14
|
+
*/
|
|
15
|
+
event: ReactMouseEvent<HTMLDivElement>;
|
|
16
|
+
/** Which axis fired — so one handler can serve both. */
|
|
17
|
+
axis: 'x' | 'y';
|
|
18
|
+
/**
|
|
19
|
+
* The axis's `id`, when it has one. A `<YAxis>` always does (it's required —
|
|
20
|
+
* charts link to it); an `<XAxis>` has none, so this is `undefined` there.
|
|
21
|
+
* To tell two stacked x-axes apart, close over the distinction at the call
|
|
22
|
+
* site (`onMouseEvent={(e) => onAxis('delta', e)}`).
|
|
23
|
+
*/
|
|
24
|
+
id?: string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* The axis value under the pointer, **in the axis's own data units** — epoch
|
|
27
|
+
* ms on a time axis, the number on a value axis, the slot value on a
|
|
28
|
+
* categorical one. Read {@link label} for what the axis would *print* there.
|
|
29
|
+
*
|
|
30
|
+
* Not clamped to a tick — it is the continuous inverse of the pixel, so it
|
|
31
|
+
* lands between ticks. The one exception is a **categorical x-axis**, whose
|
|
32
|
+
* scale inverts to the nearest band **centre** (`i + 0.5`); a categorical
|
|
33
|
+
* *row* (horizontal bars on the y-axis) is a plain linear slot scale and does
|
|
34
|
+
* not snap, so `Math.floor(value)` is its slot index.
|
|
35
|
+
*
|
|
36
|
+
* On a `transform`ed x-axis this is the **underlying** value, not the derived
|
|
37
|
+
* unit — apply the same `transform.to` you passed the axis to get the unit
|
|
38
|
+
* its ticks read in.
|
|
39
|
+
*/
|
|
40
|
+
value: number;
|
|
41
|
+
/**
|
|
42
|
+
* {@link value} formatted the way this axis reads it — the category name on a
|
|
43
|
+
* categorical axis, the axis's `format` (or the container's shared formatter)
|
|
44
|
+
* elsewhere.
|
|
45
|
+
*
|
|
46
|
+
* Precisely: it is the axis's **readout** channel, the one the cursor pill
|
|
47
|
+
* uses — so it always agrees with the pill at that pixel, and a container
|
|
48
|
+
* `cursorFormat` shapes it exactly as it shapes the pill. That is the
|
|
49
|
+
* documented precedence (`cursorFormat` → axis `format` → container), and it
|
|
50
|
+
* is the one case where `label` can read differently from the tick text: a
|
|
51
|
+
* chart with a precise `cursorFormat` over terse ticks gets the precise form
|
|
52
|
+
* here, which is the readout it asked for.
|
|
53
|
+
*/
|
|
54
|
+
label: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A single handler for every mouse event on an axis strip — see
|
|
58
|
+
* {@link AxisMouseEvent}. Passed as `onMouseEvent` to `<XAxis>` / `<YAxis>`.
|
|
59
|
+
*/
|
|
60
|
+
export type AxisMouseHandler = (info: AxisMouseEvent) => void;
|
|
61
|
+
/** The mouse props an axis strip spreads onto its root element. */
|
|
62
|
+
type AxisMouseProps = {
|
|
63
|
+
onClick?: (e: ReactMouseEvent<HTMLDivElement>) => void;
|
|
64
|
+
onDoubleClick?: (e: ReactMouseEvent<HTMLDivElement>) => void;
|
|
65
|
+
onContextMenu?: (e: ReactMouseEvent<HTMLDivElement>) => void;
|
|
66
|
+
onMouseDown?: (e: ReactMouseEvent<HTMLDivElement>) => void;
|
|
67
|
+
onMouseUp?: (e: ReactMouseEvent<HTMLDivElement>) => void;
|
|
68
|
+
onMouseMove?: (e: ReactMouseEvent<HTMLDivElement>) => void;
|
|
69
|
+
onMouseEnter?: (e: ReactMouseEvent<HTMLDivElement>) => void;
|
|
70
|
+
onMouseLeave?: (e: ReactMouseEvent<HTMLDivElement>) => void;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Build the mouse props for an axis strip: every mouse event routed to the one
|
|
74
|
+
* `onMouseEvent` handler, each carrying the axis coordinate `at` resolves from
|
|
75
|
+
* the pointer.
|
|
76
|
+
*
|
|
77
|
+
* With no handler this returns `{}` — **nothing is attached**, so an axis that
|
|
78
|
+
* doesn't opt in keeps costing nothing (no per-move callback, no listeners).
|
|
79
|
+
*
|
|
80
|
+
* `at` returns `null` when the pointer maps to no value — the transient render
|
|
81
|
+
* before a `<YAxis>` has a resolved scale — and the event is then dropped
|
|
82
|
+
* rather than reported at a made-up coordinate.
|
|
83
|
+
*/
|
|
84
|
+
export declare function axisMouseProps(onMouseEvent: AxisMouseHandler | undefined, axis: 'x' | 'y', id: string | undefined, at: (event: ReactMouseEvent<HTMLDivElement>) => {
|
|
85
|
+
value: number;
|
|
86
|
+
label: string;
|
|
87
|
+
} | null): AxisMouseProps;
|
|
88
|
+
/**
|
|
89
|
+
* The pointer's position along an axis strip, in **strip-local pixels** — the
|
|
90
|
+
* coordinate the scale inverts. Read from the strip's own client rect
|
|
91
|
+
* (`currentTarget`, so it is the strip whichever tick label was hit), which is
|
|
92
|
+
* laid out flush with the plot on that dimension: the x strip carries the left
|
|
93
|
+
* gutter as a margin and is exactly `plotWidth` wide, and the y gutter is
|
|
94
|
+
* exactly the row's height.
|
|
95
|
+
*
|
|
96
|
+
* Clamped to **the scale's range, not the strip's box** — the two are not
|
|
97
|
+
* always the same. A row carrying a `labelPlacement="top"` axis reserves a
|
|
98
|
+
* header, so its y scales run `[height, topHeader]` while the gutter box still
|
|
99
|
+
* starts at 0; clamping to the box would invert the header band to values
|
|
100
|
+
* *above* the domain and report a coordinate the axis never draws. Passing the
|
|
101
|
+
* range means a `mouseleave` off the edge — or a press in that header — still
|
|
102
|
+
* reports a value the scale actually holds.
|
|
103
|
+
*/
|
|
104
|
+
export declare function axisPointerPx(event: ReactMouseEvent<HTMLDivElement>, axis: 'x' | 'y', range: readonly [number, number]): number;
|
|
105
|
+
export {};
|
|
106
|
+
//# sourceMappingURL=axis-events.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the mouse props for an axis strip: every mouse event routed to the one
|
|
3
|
+
* `onMouseEvent` handler, each carrying the axis coordinate `at` resolves from
|
|
4
|
+
* the pointer.
|
|
5
|
+
*
|
|
6
|
+
* With no handler this returns `{}` — **nothing is attached**, so an axis that
|
|
7
|
+
* doesn't opt in keeps costing nothing (no per-move callback, no listeners).
|
|
8
|
+
*
|
|
9
|
+
* `at` returns `null` when the pointer maps to no value — the transient render
|
|
10
|
+
* before a `<YAxis>` has a resolved scale — and the event is then dropped
|
|
11
|
+
* rather than reported at a made-up coordinate.
|
|
12
|
+
*/
|
|
13
|
+
export function axisMouseProps(onMouseEvent, axis, id, at) {
|
|
14
|
+
if (onMouseEvent === undefined)
|
|
15
|
+
return {};
|
|
16
|
+
const fire = (event) => {
|
|
17
|
+
const hit = at(event);
|
|
18
|
+
if (hit === null)
|
|
19
|
+
return;
|
|
20
|
+
onMouseEvent({ event, axis, id, value: hit.value, label: hit.label });
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
onClick: fire,
|
|
24
|
+
onDoubleClick: fire,
|
|
25
|
+
onContextMenu: fire,
|
|
26
|
+
onMouseDown: fire,
|
|
27
|
+
onMouseUp: fire,
|
|
28
|
+
onMouseMove: fire,
|
|
29
|
+
onMouseEnter: fire,
|
|
30
|
+
onMouseLeave: fire,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The pointer's position along an axis strip, in **strip-local pixels** — the
|
|
35
|
+
* coordinate the scale inverts. Read from the strip's own client rect
|
|
36
|
+
* (`currentTarget`, so it is the strip whichever tick label was hit), which is
|
|
37
|
+
* laid out flush with the plot on that dimension: the x strip carries the left
|
|
38
|
+
* gutter as a margin and is exactly `plotWidth` wide, and the y gutter is
|
|
39
|
+
* exactly the row's height.
|
|
40
|
+
*
|
|
41
|
+
* Clamped to **the scale's range, not the strip's box** — the two are not
|
|
42
|
+
* always the same. A row carrying a `labelPlacement="top"` axis reserves a
|
|
43
|
+
* header, so its y scales run `[height, topHeader]` while the gutter box still
|
|
44
|
+
* starts at 0; clamping to the box would invert the header band to values
|
|
45
|
+
* *above* the domain and report a coordinate the axis never draws. Passing the
|
|
46
|
+
* range means a `mouseleave` off the edge — or a press in that header — still
|
|
47
|
+
* reports a value the scale actually holds.
|
|
48
|
+
*/
|
|
49
|
+
export function axisPointerPx(event, axis, range) {
|
|
50
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
51
|
+
const px = axis === 'x' ? event.clientX - rect.left : event.clientY - rect.top;
|
|
52
|
+
const lo = Math.min(range[0], range[1]);
|
|
53
|
+
const hi = Math.max(range[0], range[1]);
|
|
54
|
+
return Math.max(lo, Math.min(hi, px));
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=axis-events.js.map
|
package/dist/context.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
-
import type { ScaleContinuousNumeric, ScaleLinear, ScaleTime } from 'd3-scale';
|
|
2
|
+
import type { ScaleContinuousNumeric, ScaleLinear, ScaleLogarithmic, ScaleSymLog, ScaleTime } from 'd3-scale';
|
|
3
3
|
import type { ChartTheme } from './theme.js';
|
|
4
4
|
import type { AxisFormat, CursorFormat } from './format.js';
|
|
5
5
|
import type { LegendItemSpec } from './swatch.js';
|
|
@@ -25,6 +25,19 @@ export interface LabelPlacement {
|
|
|
25
25
|
readonly lane: number;
|
|
26
26
|
readonly label: string | null;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The container's shared x→pixel scale, in every kind it can resolve to. All
|
|
30
|
+
* five are callable (`value → px`) and expose `invert` / `ticks` /
|
|
31
|
+
* `tickFormat`, which is the whole surface the library — and a consumer
|
|
32
|
+
* reading the frame ({@link ChartFrame.xScale}) — uses; that shared shape is
|
|
33
|
+
* what lets a trading-time or ordinal axis drop in where a linear one went.
|
|
34
|
+
*
|
|
35
|
+
* Named (rather than written inline on {@link ContainerFrame.xScale}) because
|
|
36
|
+
* {@link useChartFrame} publishes it: a consumer positioning DOM chrome over
|
|
37
|
+
* the plot needs to be able to *write down the type* of the scale it maps
|
|
38
|
+
* through.
|
|
39
|
+
*/
|
|
40
|
+
export type ChartXScale = ScaleTime<number, number> | ScaleLinear<number, number> | ScaleLogarithmic<number, number> | ScaleSymLog<number, number> | TradingTimeScale | ScaleBand | ElapsedScale;
|
|
28
41
|
export interface ContainerFrame {
|
|
29
42
|
readonly timeRange: readonly [number, number];
|
|
30
43
|
readonly width: number;
|
|
@@ -355,7 +368,16 @@ export interface ContainerFrame {
|
|
|
355
368
|
* (axis labels, gridlines, cursor pill) reads durations without any consumer
|
|
356
369
|
* knowing about the mode.
|
|
357
370
|
*/
|
|
358
|
-
readonly xScale:
|
|
371
|
+
readonly xScale: ChartXScale;
|
|
372
|
+
/**
|
|
373
|
+
* Is the x scale **logarithmic** (`log` or `symlog`)?
|
|
374
|
+
*
|
|
375
|
+
* Detected structurally rather than threaded from the prop: `base()` exists
|
|
376
|
+
* on d3's log scales and on no other continuous scale, which is the same test
|
|
377
|
+
* `yticks.ts` already uses on the y side. Read by the viewport gestures,
|
|
378
|
+
* which must do their arithmetic in log space (see `ViewportOptions`).
|
|
379
|
+
*/
|
|
380
|
+
readonly xIsLog: boolean;
|
|
359
381
|
/**
|
|
360
382
|
* The discontinuity provider backing a **trading-time** x axis, if one was
|
|
361
383
|
* supplied to the container — closed-market time (weekends, holidays,
|
|
@@ -1655,6 +1677,17 @@ export interface AxisSpec {
|
|
|
1655
1677
|
*/
|
|
1656
1678
|
export interface RowFrame {
|
|
1657
1679
|
readonly height: number;
|
|
1680
|
+
/**
|
|
1681
|
+
* The plot's **top inset** within the row's box, in px — the header band
|
|
1682
|
+
* reserved when any axis in the row draws a `labelPlacement="top"` title,
|
|
1683
|
+
* and `0` when none does. The y-scales' range is `[height, topInset]`, so
|
|
1684
|
+
* the drawable plot is `topInset … height`.
|
|
1685
|
+
*
|
|
1686
|
+
* Carried on the frame (rather than staying a local in `ChartRow`) because
|
|
1687
|
+
* {@link useChartFrame} publishes it — without it a consumer placing an
|
|
1688
|
+
* overlay inside the plot would silently sit under a top axis title.
|
|
1689
|
+
*/
|
|
1690
|
+
readonly topInset: number;
|
|
1658
1691
|
readonly yScales: ReadonlyMap<string, YScale>;
|
|
1659
1692
|
/** Value formatter per axis id (resolved from the axis's {@link AxisSpec.format}
|
|
1660
1693
|
* against its scale) — used by both the tick labels and the cursor readout, so
|
package/dist/format.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ interface Tickable {
|
|
|
77
77
|
* render blank for almost every real number. A linear scale's `tickFormat`
|
|
78
78
|
* applies the specifier to whatever it is handed, which is what every consumer
|
|
79
79
|
* of this function actually wants; the axis's own tick *thinning* is handled by
|
|
80
|
-
* `
|
|
80
|
+
* `tickValues`, not here.
|
|
81
81
|
*
|
|
82
82
|
* **A symlog scale's precision comes from its knee, not its span** ([PND-SYMLOG]).
|
|
83
83
|
* `scaleSymlog.tickFormat` is `linearish`, so it derives precision from the
|
package/dist/format.js
CHANGED
|
@@ -24,7 +24,7 @@ import { scaleLinear } from 'd3-scale';
|
|
|
24
24
|
* render blank for almost every real number. A linear scale's `tickFormat`
|
|
25
25
|
* applies the specifier to whatever it is handed, which is what every consumer
|
|
26
26
|
* of this function actually wants; the axis's own tick *thinning* is handled by
|
|
27
|
-
* `
|
|
27
|
+
* `tickValues`, not here.
|
|
28
28
|
*
|
|
29
29
|
* **A symlog scale's precision comes from its knee, not its span** ([PND-SYMLOG]).
|
|
30
30
|
* `scaleSymlog.tickFormat` is `linearish`, so it derives precision from the
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export type { YAxisProps } from './YAxis.js';
|
|
|
31
31
|
export { XAxis } from './XAxis.js';
|
|
32
32
|
export type { XAxisProps } from './XAxis.js';
|
|
33
33
|
export type { AxisTransform } from './derivedTicks.js';
|
|
34
|
+
export type { AxisMouseEvent, AxisMouseHandler } from './axis-events.js';
|
|
34
35
|
export { TimeAxis } from './TimeAxis.js';
|
|
35
36
|
export { CategoryAxis } from './CategoryAxis.js';
|
|
36
37
|
export { HeatMap } from './HeatMap.js';
|
|
@@ -64,6 +65,9 @@ export type { LegendProps, LegendPlacement } from './Legend.js';
|
|
|
64
65
|
export type { SwatchSpec, LegendItemInput } from './swatch.js';
|
|
65
66
|
export { useChartLegend } from './useChartLegend.js';
|
|
66
67
|
export type { ChartLegend, LegendRow, LegendItem } from './useChartLegend.js';
|
|
68
|
+
export { useChartFrame } from './useChartFrame.js';
|
|
69
|
+
export type { ChartFrame, ChartFrameRow, ChartBands, ChartBand, } from './useChartFrame.js';
|
|
70
|
+
export type { ChartXScale } from './context.js';
|
|
67
71
|
export { scaleTradingTime } from './tradingTimeScale.js';
|
|
68
72
|
export type { TradingTimeScale, DiscontinuityProvider, TimeGrain, } from './tradingTimeScale.js';
|
|
69
73
|
export { scaleBand } from './bandScale.js';
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,12 @@ export { Legend } from './Legend.js';
|
|
|
52
52
|
// consumers who design their own key (horizontal strips, ticker-compare,
|
|
53
53
|
// values-in-the-legend).
|
|
54
54
|
export { useChartLegend } from './useChartLegend.js';
|
|
55
|
+
// The resolved plot geometry — the plot rect, the axis gutters, the shared x
|
|
56
|
+
// scale, a row's y scales, and (on a category axis) the ordinal slot edges.
|
|
57
|
+
// What a consumer aligning DOM chrome to the plot would otherwise re-derive
|
|
58
|
+
// by mirroring the library's own gutter arithmetic — a duplicate that drifts
|
|
59
|
+
// silently the moment the library changes how a gutter is sized.
|
|
60
|
+
export { useChartFrame } from './useChartFrame.js';
|
|
55
61
|
export { scaleTradingTime } from './tradingTimeScale.js';
|
|
56
62
|
// The ordinal category (band) scale — the transpose view's "columns on x" axis.
|
|
57
63
|
export { scaleBand } from './bandScale.js';
|
package/dist/theme.d.ts
CHANGED
|
@@ -720,6 +720,28 @@ export interface AreaStyle {
|
|
|
720
720
|
/** Ink for a swept window's emphasised portion (edge + fill).
|
|
721
721
|
* **Omitted ⇒ the area keeps its own colours and only strengthens.** */
|
|
722
722
|
readonly spanColor?: string;
|
|
723
|
+
/**
|
|
724
|
+
* The **threshold-band ladder** — ordered fills for an area coloured *along
|
|
725
|
+
* its height* against `<AreaChart thresholds>`: `bands[0]` up to the first
|
|
726
|
+
* threshold, `bands[1]` between the first and second, and so on. A ladder of
|
|
727
|
+
* `n` thresholds reads `n + 1` entries. The fill **and** the outline take
|
|
728
|
+
* the band hues (one hard-stop gradient in pixel space), and the grade to
|
|
729
|
+
* transparent is dropped: the fade encoded distance-from-the-baseline, which
|
|
730
|
+
* is exactly what the ladder now states discretely — two encodings of one
|
|
731
|
+
* thing would fight.
|
|
732
|
+
*
|
|
733
|
+
* Lives on `AreaStyle` for {@link BarStyle.bands}' reason: `theme.area` is a
|
|
734
|
+
* semantic **map**, so a top-level key would collide with a role of that
|
|
735
|
+
* name — and per-role is the more useful shape (`area.default.bands` and a
|
|
736
|
+
* capacity role's ladder can differ).
|
|
737
|
+
*
|
|
738
|
+
* **Overridden by `<AreaChart bandColors>`** at the call site. If neither
|
|
739
|
+
* resolves enough entries for the ladder, the shortfall falls back to the
|
|
740
|
+
* flat {@link fill} and (in dev) warns — the same contract as the bar
|
|
741
|
+
* ladder, because a silently-unbanded chart is the failure mode the feature
|
|
742
|
+
* exists to remove.
|
|
743
|
+
*/
|
|
744
|
+
readonly bands?: readonly string[];
|
|
723
745
|
}
|
|
724
746
|
/**
|
|
725
747
|
* A resolved bar style: the flat `fill` (scaled by `opacity`, 0–1) plus the
|
package/dist/theme.js
CHANGED
|
@@ -78,6 +78,9 @@ export const defaultTheme = {
|
|
|
78
78
|
selectedFillOpacity: 0.55,
|
|
79
79
|
dimmedOpacity: 0.32,
|
|
80
80
|
spanColor: '#3F5BE0',
|
|
81
|
+
// The same ok → warning → alarm ladder `bar.default.bands` carries, so a
|
|
82
|
+
// banded area and a banded bar over one dataset read as one system.
|
|
83
|
+
bands: ['#2A9D8F', '#e8a13c', '#d64545'],
|
|
81
84
|
},
|
|
82
85
|
in: { color: '#0284c7', width: 1.5, fill: '#0284c7', fillOpacity: 0.3 },
|
|
83
86
|
out: { color: '#e8836b', width: 1.5, fill: '#e8836b', fillOpacity: 0.3 },
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type BandLadder } from './bars.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a component's `thresholds` / `bandColors` props against its theme
|
|
4
|
+
* role's band ramp into a {@link BandLadder} — or `undefined` when there is no
|
|
5
|
+
* usable ladder, so the caller keeps its flat path.
|
|
6
|
+
*
|
|
7
|
+
* Extracted from `<BarChart>`'s [PND-BANDBAR2] block verbatim when
|
|
8
|
+
* `<AreaChart thresholds>` arrived ([PND-BANDAREA]): the resolution rules and
|
|
9
|
+
* every dev warning are one contract across banded marks, differing only in
|
|
10
|
+
* the component named by the warning text.
|
|
11
|
+
*
|
|
12
|
+
* Resolved once here rather than per mark per frame: normalize the breakpoints
|
|
13
|
+
* (sort, drop non-finite / non-positive), then pair them with `bandColors` →
|
|
14
|
+
* the role's `bands`. Everything that can go wrong with the pairing is a
|
|
15
|
+
* *silent* wrong-looking chart, so each case dev-warns — this feature exists
|
|
16
|
+
* because a quietly-unbanded mark was the workaround's failure mode.
|
|
17
|
+
*
|
|
18
|
+
* The two array props are **value-compared** rather than identity-compared:
|
|
19
|
+
* `thresholds={[1, 2]}` inline is the documented usage and the shape every
|
|
20
|
+
* story and doc example uses — and a fresh array each render would rebuild
|
|
21
|
+
* the ladder, hence the caller's layer entry, hence a `registerLayer` call
|
|
22
|
+
* **every render**. That is a repaint treadmill, not just a noisy warning.
|
|
23
|
+
* The same value-compare-on-registration reasoning `<YAxis ticks>` applies.
|
|
24
|
+
*
|
|
25
|
+
* A short colour supply pads with `styleFill` (the role's flat fill) so the
|
|
26
|
+
* draw path can index freely; `undefined` comes back only when there are no
|
|
27
|
+
* usable breakpoints or no colours at all.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useBandLadder(component: 'BarChart' | 'AreaChart', thresholds: readonly number[] | undefined, bandColors: readonly string[] | undefined, styleBands: readonly string[] | undefined, styleFill: string): BandLadder | undefined;
|
|
30
|
+
//# sourceMappingURL=use-band-ladder.d.ts.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { normalizeThresholds } from './bars.js';
|
|
3
|
+
import { isDev } from './dev.js';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a component's `thresholds` / `bandColors` props against its theme
|
|
6
|
+
* role's band ramp into a {@link BandLadder} — or `undefined` when there is no
|
|
7
|
+
* usable ladder, so the caller keeps its flat path.
|
|
8
|
+
*
|
|
9
|
+
* Extracted from `<BarChart>`'s [PND-BANDBAR2] block verbatim when
|
|
10
|
+
* `<AreaChart thresholds>` arrived ([PND-BANDAREA]): the resolution rules and
|
|
11
|
+
* every dev warning are one contract across banded marks, differing only in
|
|
12
|
+
* the component named by the warning text.
|
|
13
|
+
*
|
|
14
|
+
* Resolved once here rather than per mark per frame: normalize the breakpoints
|
|
15
|
+
* (sort, drop non-finite / non-positive), then pair them with `bandColors` →
|
|
16
|
+
* the role's `bands`. Everything that can go wrong with the pairing is a
|
|
17
|
+
* *silent* wrong-looking chart, so each case dev-warns — this feature exists
|
|
18
|
+
* because a quietly-unbanded mark was the workaround's failure mode.
|
|
19
|
+
*
|
|
20
|
+
* The two array props are **value-compared** rather than identity-compared:
|
|
21
|
+
* `thresholds={[1, 2]}` inline is the documented usage and the shape every
|
|
22
|
+
* story and doc example uses — and a fresh array each render would rebuild
|
|
23
|
+
* the ladder, hence the caller's layer entry, hence a `registerLayer` call
|
|
24
|
+
* **every render**. That is a repaint treadmill, not just a noisy warning.
|
|
25
|
+
* The same value-compare-on-registration reasoning `<YAxis ticks>` applies.
|
|
26
|
+
*
|
|
27
|
+
* A short colour supply pads with `styleFill` (the role's flat fill) so the
|
|
28
|
+
* draw path can index freely; `undefined` comes back only when there are no
|
|
29
|
+
* usable breakpoints or no colours at all.
|
|
30
|
+
*/
|
|
31
|
+
export function useBandLadder(component, thresholds, bandColors, styleBands, styleFill) {
|
|
32
|
+
const thresholdKey = thresholds === undefined ? '' : thresholds.join(',');
|
|
33
|
+
const bandColorKey = bandColors === undefined ? '' : bandColors.join(',');
|
|
34
|
+
return useMemo(() => {
|
|
35
|
+
const steps = normalizeThresholds(thresholds);
|
|
36
|
+
if (steps === null) {
|
|
37
|
+
if (isDev && thresholds !== undefined && thresholds.length > 0) {
|
|
38
|
+
console.warn(`<${component} thresholds>: no usable breakpoints, so no banding ` +
|
|
39
|
+
'was applied — each must be finite and greater than zero. The ' +
|
|
40
|
+
'chart draws in the flat fill.');
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
// Some, but not all, entries dropped. Silently banding on a subset of what
|
|
45
|
+
// the caller wrote is exactly the class of quiet wrongness this feature is
|
|
46
|
+
// meant to remove, so say so.
|
|
47
|
+
if (isDev && thresholds !== undefined && steps.length < thresholds.length) {
|
|
48
|
+
console.warn(`<${component} thresholds>: dropped ${thresholds.length - steps.length} ` +
|
|
49
|
+
'breakpoint(s) that were not finite and greater than zero. The ' +
|
|
50
|
+
'ladder is walked on the magnitude and mirrored onto whichever side ' +
|
|
51
|
+
`of zero the value is on, so a negative breakpoint has no meaning; ` +
|
|
52
|
+
`banding on [${steps.join(', ')}].`);
|
|
53
|
+
}
|
|
54
|
+
const want = steps.length + 1;
|
|
55
|
+
const supplied = bandColors ?? styleBands;
|
|
56
|
+
if (supplied === undefined || supplied.length === 0) {
|
|
57
|
+
if (isDev) {
|
|
58
|
+
console.warn(`<${component} thresholds>: ${steps.length} breakpoint(s) need ` +
|
|
59
|
+
`${want} band colours, but neither \`bandColors\` nor the theme ` +
|
|
60
|
+
`role’s \`bands\` supplies any. The chart draws in the flat fill.`);
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
if (supplied.length < want && isDev) {
|
|
65
|
+
console.warn(`<${component} thresholds>: ${steps.length} breakpoint(s) need ` +
|
|
66
|
+
`${want} band colours but only ${supplied.length} were supplied; ` +
|
|
67
|
+
'bands above the last colour fall back to the flat fill.');
|
|
68
|
+
}
|
|
69
|
+
// Pad a short ladder with the flat fill so the draw path can index freely.
|
|
70
|
+
const resolved = supplied.length >= want
|
|
71
|
+
? supplied.slice(0, want)
|
|
72
|
+
: [
|
|
73
|
+
...supplied,
|
|
74
|
+
...Array.from({ length: want - supplied.length }, () => styleFill),
|
|
75
|
+
];
|
|
76
|
+
return { thresholds: steps, colors: resolved };
|
|
77
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- `thresholdKey` /
|
|
78
|
+
// `bandColorKey` are the value-compared stand-ins for the array props.
|
|
79
|
+
}, [component, thresholdKey, bandColorKey, styleBands, styleFill]);
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=use-band-ladder.js.map
|