@pond-ts/charts 0.59.0 → 0.60.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 +20 -16
- package/CHANGELOG.md +196 -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/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.js +19 -14
- package/dist/YAxis.js +2 -2
- package/dist/area.d.ts +43 -1
- package/dist/area.js +122 -5
- 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 +3 -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.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type CurveFactory } from 'd3-shape';
|
|
2
2
|
import type { ChartSeries } from './data.js';
|
|
3
3
|
import { type Scale, type TraceState } from './line.js';
|
|
4
|
+
import type { BandLadder } from './bars.js';
|
|
4
5
|
import type { AreaStyle } from './theme.js';
|
|
5
6
|
import type { LayerDrawStats } from './context.js';
|
|
6
7
|
import { type GapMode } from './gaps.js';
|
|
@@ -69,7 +70,48 @@ export declare function areaExtent(cs: ChartSeries, baseline: number | undefined
|
|
|
69
70
|
* bracketed by `save`/`restore` so they don't leak into later layers. Gap edges
|
|
70
71
|
* are collected by one O(N) walk ({@link collectGapEdges}).
|
|
71
72
|
*/
|
|
72
|
-
export declare function drawArea(ctx: CanvasRenderingContext2D, cs: ChartSeries, xScale: Scale, yScale: Scale, style: AreaStyle, baselineValue: number, curve?: CurveFactory, gaps?: GapMode, gapConnectorOpacity?: number, decimate?: DecimateOption): LayerDrawStats;
|
|
73
|
+
export declare function drawArea(ctx: CanvasRenderingContext2D, cs: ChartSeries, xScale: Scale, yScale: Scale, style: AreaStyle, baselineValue: number, curve?: CurveFactory, gaps?: GapMode, gapConnectorOpacity?: number, decimate?: DecimateOption, banding?: BandLadder): LayerDrawStats;
|
|
74
|
+
/**
|
|
75
|
+
* The banded fill + stroke for `<AreaChart thresholds>` ([PND-BANDAREA]): one
|
|
76
|
+
* vertical **hard-stop gradient in pixel space**, a colour switch at every
|
|
77
|
+
* threshold crossing — `colors[0]` between `-t0` and `+t0`, `colors[k]` over
|
|
78
|
+
* magnitudes `[t(k-1), tk)` on both sides of zero. `thresholds`/`colors` arrive
|
|
79
|
+
* as a resolved {@link BandLadder} (ascending, positive, `n + 1` colours), the
|
|
80
|
+
* same currency `drawBars` takes.
|
|
81
|
+
*
|
|
82
|
+
* A gradient rather than one clipped redraw per band, and that is the
|
|
83
|
+
* load-bearing choice: K + 1 clipped passes walk the path K + 1 times and meet
|
|
84
|
+
* themselves at every boundary with an antialiased seam, where a gradient
|
|
85
|
+
* draws the identical single path once and costs O(K) colour stops. It also
|
|
86
|
+
* bands the **outline for free** — `strokeStyle` takes the same gradient, so
|
|
87
|
+
* the value line switches hue exactly at a crossing, which no per-band clip
|
|
88
|
+
* can do without shearing the stroke.
|
|
89
|
+
*
|
|
90
|
+
* The ladder is walked on the **magnitude** and mirrored below zero, exactly
|
|
91
|
+
* as `bandSpan` does for a bar: the boundary at `±tk` separates band `k` (the
|
|
92
|
+
* zero side) from band `k + 1` (the away side). Whether "away from zero" is up
|
|
93
|
+
* or down the canvas is probed from the scale itself (`t0` vs `t0 + 1`, both
|
|
94
|
+
* positive and finite by construction), so a flipped axis bands correctly. A
|
|
95
|
+
* boundary with **no position** on the scale contributes no crossing — on a
|
|
96
|
+
* log axis the negative mirrors (and zero) simply don't exist, which is the
|
|
97
|
+
* right reading. A crossing **off the plot** clamps to the gradient's ends
|
|
98
|
+
* (a real canvas throws on stops outside `[0, 1]`), which is also what makes a
|
|
99
|
+
* zoomed-in view honest: with every visible pixel inside one band, the clamp
|
|
100
|
+
* degenerates the other stops and the whole plot paints that band's colour.
|
|
101
|
+
*
|
|
102
|
+
* Falls back to the top band's flat colour when there is nothing to anchor on
|
|
103
|
+
* (no plot height, or no boundary with a position at all) — reachable only
|
|
104
|
+
* with a degenerate scale stub, since every real axis positions a positive
|
|
105
|
+
* finite value; any flat colour is equally (in)correct there, and the top
|
|
106
|
+
* band's is at least stable.
|
|
107
|
+
*
|
|
108
|
+
* Like the bar ladder, breakpoints are **absolute data values**, so the
|
|
109
|
+
* baseline plays no part here: an area resting on a non-zero floor still bands
|
|
110
|
+
* at the same heights as its neighbours — measuring from the resolved baseline
|
|
111
|
+
* instead would silently shift every breakpoint by the floor, the quiet
|
|
112
|
+
* wrongness [PND-BANDBAR2] exists to remove.
|
|
113
|
+
*/
|
|
114
|
+
export declare function buildBandGradient(ctx: CanvasRenderingContext2D, yScale: Scale, plotHeight: number, banding: BandLadder): CanvasGradient | string;
|
|
73
115
|
/**
|
|
74
116
|
* **Is the pointer inside this area?** The filled-region counterpart of
|
|
75
117
|
* `traceHitIndex` ([PND-TRACESEL]) — returns the nearest sample's index as
|
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
|
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
|
@@ -64,6 +64,9 @@ export type { LegendProps, LegendPlacement } from './Legend.js';
|
|
|
64
64
|
export type { SwatchSpec, LegendItemInput } from './swatch.js';
|
|
65
65
|
export { useChartLegend } from './useChartLegend.js';
|
|
66
66
|
export type { ChartLegend, LegendRow, LegendItem } from './useChartLegend.js';
|
|
67
|
+
export { useChartFrame } from './useChartFrame.js';
|
|
68
|
+
export type { ChartFrame, ChartFrameRow, ChartBands, ChartBand, } from './useChartFrame.js';
|
|
69
|
+
export type { ChartXScale } from './context.js';
|
|
67
70
|
export { scaleTradingTime } from './tradingTimeScale.js';
|
|
68
71
|
export type { TradingTimeScale, DiscontinuityProvider, TimeGrain, } from './tradingTimeScale.js';
|
|
69
72
|
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
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { type ChartXScale, type YScale } from './context.js';
|
|
2
|
+
/**
|
|
3
|
+
* One ordinal slot on a category axis — its pixel span within the plot, its
|
|
4
|
+
* centre (where a mark is drawn and a tick is labelled), and its name.
|
|
5
|
+
*
|
|
6
|
+
* `x0`/`x1`/`center` are **plot-relative** (`0 … plot.width`); add
|
|
7
|
+
* {@link ChartFrame.plot}`.x` for container-relative DOM placement.
|
|
8
|
+
*/
|
|
9
|
+
export interface ChartBand {
|
|
10
|
+
/** The slot's left edge in px. */
|
|
11
|
+
readonly x0: number;
|
|
12
|
+
/** The slot's right edge in px. `x1 - x0` is {@link ChartBands.pitch}. */
|
|
13
|
+
readonly x1: number;
|
|
14
|
+
/** The slot's centre in px — where a bar centres and a tick labels. */
|
|
15
|
+
readonly center: number;
|
|
16
|
+
/** The category name at this slot. */
|
|
17
|
+
readonly label: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The ordinal slot geometry of a `'category'` x axis — `null` on a `'time'`
|
|
21
|
+
* or `'value'` axis, which has no slots.
|
|
22
|
+
*
|
|
23
|
+
* **The pitch is not `plot.width / count`.** `<ChartContainer maxBandWidth>`
|
|
24
|
+
* caps it and `bandAlign` places the resulting narrower block within the
|
|
25
|
+
* plot, so the packed band run can be inset from both plot edges. Reading
|
|
26
|
+
* `pitch` and `at(i)` rather than recomputing is the difference between
|
|
27
|
+
* chrome that tracks that packing and chrome that ignores it.
|
|
28
|
+
*/
|
|
29
|
+
export interface ChartBands {
|
|
30
|
+
/** Number of slots — the category count. */
|
|
31
|
+
readonly count: number;
|
|
32
|
+
/** One slot's width in px (the pitch; slots are contiguous and equal). */
|
|
33
|
+
readonly pitch: number;
|
|
34
|
+
/** The ordered category names, index-aligned with the slots. */
|
|
35
|
+
readonly labels: readonly string[];
|
|
36
|
+
/** The slot at `index`, or `null` when `index` is not a real slot. */
|
|
37
|
+
at(index: number): ChartBand | null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The **row-scoped** half of the frame: y geometry and the row's value
|
|
41
|
+
* scales. `null` on {@link ChartFrame.row} when the hook is called outside a
|
|
42
|
+
* `<ChartRow>`.
|
|
43
|
+
*/
|
|
44
|
+
export interface ChartFrameRow {
|
|
45
|
+
/**
|
|
46
|
+
* The plot's top inset within the row's box in px — the band reserved by a
|
|
47
|
+
* `labelPlacement="top"` axis title, `0` when no axis draws one. An overlay
|
|
48
|
+
* that ignores it sits under the title.
|
|
49
|
+
*/
|
|
50
|
+
readonly topInset: number;
|
|
51
|
+
/**
|
|
52
|
+
* The plot's drawable height in px, below {@link topInset}. The row's own
|
|
53
|
+
* `height` prop is `topInset + height`, and the y-scales' pixel range is
|
|
54
|
+
* `[topInset + height, topInset]` (inverted — pixels grow downward).
|
|
55
|
+
*/
|
|
56
|
+
readonly height: number;
|
|
57
|
+
/**
|
|
58
|
+
* One value→pixel scale per `<YAxis id>`, each mapping into
|
|
59
|
+
* `[topInset + height, topInset]`. A row with no explicit `<YAxis>` has one
|
|
60
|
+
* entry under the implicit default id.
|
|
61
|
+
*/
|
|
62
|
+
readonly yScales: ReadonlyMap<string, YScale>;
|
|
63
|
+
/** Which gutter each axis id sits in — so chrome hugs the right edge. */
|
|
64
|
+
readonly axisSides: ReadonlyMap<string, 'left' | 'right'>;
|
|
65
|
+
}
|
|
66
|
+
/** The resolved geometry of a chart, as published by {@link useChartFrame}. */
|
|
67
|
+
export interface ChartFrame {
|
|
68
|
+
/**
|
|
69
|
+
* The plot's **x** geometry in px, relative to the container's own box:
|
|
70
|
+
* `x` is the left gutter (where the plot starts) and `width` is the plot's
|
|
71
|
+
* width after both gutters. Shared by every row.
|
|
72
|
+
*/
|
|
73
|
+
readonly plot: {
|
|
74
|
+
readonly x: number;
|
|
75
|
+
readonly width: number;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* The reserved axis gutters in px — how far the plot is inset on each side.
|
|
79
|
+
* `left` equals {@link plot}`.x`; both are published because chrome above
|
|
80
|
+
* the plot pads by `left` while chrome sized to the container subtracts
|
|
81
|
+
* both.
|
|
82
|
+
*/
|
|
83
|
+
readonly gutters: {
|
|
84
|
+
readonly left: number;
|
|
85
|
+
readonly right: number;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* The shared x→pixel scale, mapping into `[0, plot.width]`. Callable
|
|
89
|
+
* (`value → px`) with `invert` / `ticks` / `tickFormat`, whichever kind the
|
|
90
|
+
* container resolved (see {@link ChartXScale}).
|
|
91
|
+
*
|
|
92
|
+
* **Not the same thing as `<ChartContainer xScale>`**, which is a string
|
|
93
|
+
* naming how a *value* axis spaces itself (`'linear' | 'log' | 'symlog'`).
|
|
94
|
+
* This is the built scale object that choice — along with the data's kind,
|
|
95
|
+
* `origin`, `discontinuities` and `categories` — resolves to.
|
|
96
|
+
*/
|
|
97
|
+
readonly xScale: ChartXScale;
|
|
98
|
+
/**
|
|
99
|
+
* Which **kind** of x axis resolved: `'time'`, `'value'` or `'category'`.
|
|
100
|
+
* `'category'` is exactly when {@link bands} is non-null.
|
|
101
|
+
*
|
|
102
|
+
* Distinct from `<ChartContainer xScale>` again: that picks the spacing
|
|
103
|
+
* *within* a value axis, this says whether the axis is a value axis at all.
|
|
104
|
+
*/
|
|
105
|
+
readonly xKind: 'time' | 'value' | 'category';
|
|
106
|
+
/** Ordinal slot geometry on a `'category'` axis; `null` otherwise. */
|
|
107
|
+
readonly bands: ChartBands | null;
|
|
108
|
+
/** Row-scoped y geometry — `null` outside a `<ChartRow>`. */
|
|
109
|
+
readonly row: ChartFrameRow | null;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Read the container's resolved plot geometry — the plot rect, the axis
|
|
113
|
+
* gutters, the shared x scale, the row's y scales, and (on a category axis)
|
|
114
|
+
* the ordinal slot edges. See the module docblock for the x/y split, the
|
|
115
|
+
* placement-scoped `row` half, and which box each pixel value is relative to.
|
|
116
|
+
*
|
|
117
|
+
* Must be called under a `<ChartContainer>`; throws otherwise. To render the
|
|
118
|
+
* markup *outside* the chart's box, portal it out (`createPortal`) — context
|
|
119
|
+
* flows through portals.
|
|
120
|
+
*/
|
|
121
|
+
export declare function useChartFrame(): ChartFrame;
|
|
122
|
+
//# sourceMappingURL=useChartFrame.d.ts.map
|