@pond-ts/charts 0.53.1 → 0.54.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/CHANGELOG.md +864 -4
- package/dist/AreaChart.d.ts +9 -1
- package/dist/AreaChart.js +46 -5
- package/dist/BarChart.d.ts +17 -8
- package/dist/BarChart.js +17 -4
- package/dist/LineChart.d.ts +12 -1
- package/dist/LineChart.js +46 -5
- package/dist/affine.d.ts +35 -14
- package/dist/affine.js +34 -17
- package/dist/area.js +4 -4
- package/dist/bars.d.ts +42 -15
- package/dist/bars.js +97 -23
- package/dist/context.d.ts +28 -7
- package/dist/data.d.ts +58 -0
- package/dist/data.js +85 -22
- package/dist/decimate.js +7 -7
- package/dist/line.js +2 -2
- package/package.json +3 -3
package/dist/AreaChart.d.ts
CHANGED
|
@@ -17,6 +17,14 @@ export interface AreaChartProps<S extends SeriesSchema = SeriesSchema, VS extend
|
|
|
17
17
|
series: TimeSeries<S> | ValueSeries<VS>;
|
|
18
18
|
/** Name of the numeric value column to fill from. */
|
|
19
19
|
column: string;
|
|
20
|
+
/**
|
|
21
|
+
* Optional column to **read out** at the cursor instead of the plotted
|
|
22
|
+
* `column` — the area still fills `column`, but each tracker sample also
|
|
23
|
+
* carries this column's value as {@link TrackerSample.readout}, so an
|
|
24
|
+
* off-chart readout can show a **source** value while the area draws a derived
|
|
25
|
+
* one. Mirrors `<LineChart readout>`. **Omitted ⇒ no readout channel.**
|
|
26
|
+
*/
|
|
27
|
+
readout?: string;
|
|
20
28
|
/**
|
|
21
29
|
* The series' semantic identifier — what the data _is_ / how it should read
|
|
22
30
|
* (e.g. `elevation`, or a signed-traffic role like `in` / `out`). The theme
|
|
@@ -108,5 +116,5 @@ export interface AreaChartProps<S extends SeriesSchema = SeriesSchema, VS extend
|
|
|
108
116
|
* </Layers>
|
|
109
117
|
* ```
|
|
110
118
|
*/
|
|
111
|
-
export declare function AreaChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, as: semantic, axis, baseline, curve, gaps, decimate, legend, index, }: AreaChartProps<S, VS>): null;
|
|
119
|
+
export declare function AreaChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, readout, as: semantic, axis, baseline, curve, gaps, decimate, legend, index, }: AreaChartProps<S, VS>): null;
|
|
112
120
|
//# sourceMappingURL=AreaChart.d.ts.map
|
package/dist/AreaChart.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useContext, useEffect, useMemo } from 'react';
|
|
2
2
|
import { ValueSeries } from 'pond-ts';
|
|
3
|
-
import { fromTimeSeries, fromValueSeries } from './data.js';
|
|
3
|
+
import { assertNumericColumn, fromTimeSeries, fromValueSeries, } from './data.js';
|
|
4
4
|
import { areaExtent, drawArea } from './area.js';
|
|
5
5
|
import { resolveCurve } from './curve.js';
|
|
6
6
|
import { DEFAULT_GAP_MODE, DEFAULT_GAP_CONNECTOR_OPACITY, } from './gaps.js';
|
|
@@ -36,7 +36,7 @@ function domainFloor(yScale) {
|
|
|
36
36
|
* </Layers>
|
|
37
37
|
* ```
|
|
38
38
|
*/
|
|
39
|
-
export function AreaChart({ series, column, as: semantic, axis, baseline, curve, gaps = DEFAULT_GAP_MODE, decimate = true, legend, index = 0, }) {
|
|
39
|
+
export function AreaChart({ series, column, readout, as: semantic, axis, baseline, curve, gaps = DEFAULT_GAP_MODE, decimate = true, legend, index = 0, }) {
|
|
40
40
|
const container = useContext(ContainerContext);
|
|
41
41
|
if (container === null) {
|
|
42
42
|
throw new Error('<AreaChart> must be rendered inside a <ChartContainer>');
|
|
@@ -48,6 +48,22 @@ export function AreaChart({ series, column, as: semantic, axis, baseline, curve,
|
|
|
48
48
|
const cs = useMemo(() => series instanceof ValueSeries
|
|
49
49
|
? fromValueSeries(series, column)
|
|
50
50
|
: fromTimeSeries(series, column), [series, column]);
|
|
51
|
+
// Readout column values for a value-axis series (time path reads it off the
|
|
52
|
+
// event) — the tracker reports it alongside the plotted fill so an off-chart
|
|
53
|
+
// readout can show a source value. See AreaChartProps.readout.
|
|
54
|
+
//
|
|
55
|
+
// The time path buffers nothing (it has an event, not an index), so it
|
|
56
|
+
// validates the name here instead, so a mistyped `readout` fails the same way
|
|
57
|
+
// on both axis kinds rather than throwing on one and silently doing nothing
|
|
58
|
+
// on the other. Mirrors `<LineChart>`.
|
|
59
|
+
const readoutY = useMemo(() => {
|
|
60
|
+
if (readout === undefined)
|
|
61
|
+
return undefined;
|
|
62
|
+
if (series instanceof ValueSeries)
|
|
63
|
+
return fromValueSeries(series, readout).y;
|
|
64
|
+
assertNumericColumn(series, readout);
|
|
65
|
+
return undefined;
|
|
66
|
+
}, [series, readout]);
|
|
51
67
|
// Styling: semantic identifier → theme area style. The single styling channel.
|
|
52
68
|
const { area } = container.theme;
|
|
53
69
|
const style = (semantic !== undefined ? area[semantic] : undefined) ?? area.default;
|
|
@@ -77,8 +93,19 @@ export function AreaChart({ series, column, as: semantic, axis, baseline, curve,
|
|
|
77
93
|
if (i < 0)
|
|
78
94
|
return [];
|
|
79
95
|
const v = cs.y[i];
|
|
96
|
+
const rv = readoutY?.[i];
|
|
80
97
|
return Number.isFinite(v)
|
|
81
|
-
? [
|
|
98
|
+
? [
|
|
99
|
+
{
|
|
100
|
+
x: cs.x[i],
|
|
101
|
+
value: v,
|
|
102
|
+
color: style.color,
|
|
103
|
+
label,
|
|
104
|
+
...(rv !== undefined && Number.isFinite(rv)
|
|
105
|
+
? { readout: rv }
|
|
106
|
+
: {}),
|
|
107
|
+
},
|
|
108
|
+
]
|
|
82
109
|
: [];
|
|
83
110
|
}
|
|
84
111
|
const e = series.nearest(x);
|
|
@@ -87,11 +114,23 @@ export function AreaChart({ series, column, as: semantic, axis, baseline, curve,
|
|
|
87
114
|
// get() wants a literal key; column is a runtime string. Cast the
|
|
88
115
|
// *event* (not the method — that would detach `this`) to a
|
|
89
116
|
// string-keyed get; runtime-safe read + guard.
|
|
90
|
-
const
|
|
117
|
+
const ev = e;
|
|
118
|
+
const v = ev.get(column);
|
|
119
|
+
const rv = readout !== undefined ? ev.get(readout) : undefined;
|
|
91
120
|
// The readout dot rides the value line (not the baseline), coloured by
|
|
92
121
|
// the outline stroke. A gap yields no readout (like the fill).
|
|
93
122
|
return typeof v === 'number' && Number.isFinite(v)
|
|
94
|
-
? [
|
|
123
|
+
? [
|
|
124
|
+
{
|
|
125
|
+
x: e.begin(),
|
|
126
|
+
value: v,
|
|
127
|
+
color: style.color,
|
|
128
|
+
label,
|
|
129
|
+
...(typeof rv === 'number' && Number.isFinite(rv)
|
|
130
|
+
? { readout: rv }
|
|
131
|
+
: {}),
|
|
132
|
+
},
|
|
133
|
+
]
|
|
95
134
|
: [];
|
|
96
135
|
},
|
|
97
136
|
draw: (ctx, xScale, yScale) => drawArea(ctx, cs, xScale, yScale, style,
|
|
@@ -106,6 +145,8 @@ export function AreaChart({ series, column, as: semantic, axis, baseline, curve,
|
|
|
106
145
|
cs,
|
|
107
146
|
series,
|
|
108
147
|
column,
|
|
148
|
+
readout,
|
|
149
|
+
readoutY,
|
|
109
150
|
style,
|
|
110
151
|
label,
|
|
111
152
|
baseline,
|
package/dist/BarChart.d.ts
CHANGED
|
@@ -68,14 +68,21 @@ export interface BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends
|
|
|
68
68
|
*/
|
|
69
69
|
colors?: Readonly<Record<string, string>>;
|
|
70
70
|
/**
|
|
71
|
-
* **Per-
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
71
|
+
* **Per-bar** colours for a single-series chart — `binColors[i]` fills bar
|
|
72
|
+
* `i` (aligned to the bars / bins in order), overriding the `as`/theme fill.
|
|
73
|
+
* This is the way to colour heart-rate / power **zones** or value bands each
|
|
74
|
+
* their own colour, and the **direction-coloured financial volume row** — a
|
|
75
|
+
* time-axis `series` derives `binColors` from its own data (rising / falling
|
|
76
|
+
* off open vs close) so volume bars read green / red under the candles (the
|
|
77
|
+
* `colors` map above is per-**group**, for stacks). An `undefined`/short
|
|
78
|
+
* entry falls back to the theme fill. Works on any **single-series** shape —
|
|
79
|
+
* a time / value `series` (vertical or horizontal) or `bins`; on a
|
|
77
80
|
* multi-group stack it would tint every segment of a bin alike, so it's not
|
|
78
|
-
* the tool there.
|
|
81
|
+
* the tool there. A per-bar-coloured bar keeps its own colour under hover /
|
|
82
|
+
* selection (the highlight pops opacity instead of swapping the fill), and
|
|
83
|
+
* the hover / click readout reports the bar's own colour. **Disables the
|
|
84
|
+
* dense-bar envelope decimation** (see `decimate`) — an envelope rect can't
|
|
85
|
+
* carry many bars' colours, so every visible bar draws.
|
|
79
86
|
*/
|
|
80
87
|
binColors?: readonly (string | undefined)[];
|
|
81
88
|
/**
|
|
@@ -130,7 +137,9 @@ export interface BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends
|
|
|
130
137
|
* at that density (a perf knob, not a style); interaction still reads the source
|
|
131
138
|
* bars. Pass `false` to always draw every bar, or `{ threshold }` to tune the
|
|
132
139
|
* samples-per-pixel factor. **No-op for a stacked / multi-group histogram** (the
|
|
133
|
-
* categorical case is low-count; only the single-series path decimates)
|
|
140
|
+
* categorical case is low-count; only the single-series path decimates) — and
|
|
141
|
+
* **no-op when `binColors` is set** (an envelope rect would repaint its bars
|
|
142
|
+
* one flat colour; a per-bar-coloured layer draws every visible bar).
|
|
134
143
|
*/
|
|
135
144
|
decimate?: DecimateOption;
|
|
136
145
|
/**
|
package/dist/BarChart.js
CHANGED
|
@@ -244,7 +244,9 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
|
|
|
244
244
|
{
|
|
245
245
|
x: (bs.begin[i] + bs.end[i]) / 2,
|
|
246
246
|
value: v,
|
|
247
|
-
|
|
247
|
+
// A per-bar colour wins over the flat fill, so the readout
|
|
248
|
+
// pill reads the bar's own colour (as the stacked path does).
|
|
249
|
+
color: binColors?.[i] ?? singleStyle.fill,
|
|
248
250
|
label,
|
|
249
251
|
},
|
|
250
252
|
];
|
|
@@ -257,17 +259,27 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
|
|
|
257
259
|
const hit = barAt(bs, px, py, xScale, yScale, baseline, gapPx, singleStyle.minWidth);
|
|
258
260
|
if (hit === null)
|
|
259
261
|
return null;
|
|
260
|
-
const [, begin, value] = hit;
|
|
262
|
+
const [bi, begin, value] = hit;
|
|
263
|
+
// The bar's stable `mark` (its own axis key) rides the
|
|
264
|
+
// selection, so the highlight match and a controlled echo key
|
|
265
|
+
// on the *sample* rather than on the `begin` edge — which on a
|
|
266
|
+
// point-keyed series is derived geometry, not the sample's key
|
|
267
|
+
// (see BarSeries.marks). `bi` is the exact bar index from the
|
|
268
|
+
// hit, as the stacked path uses it.
|
|
269
|
+
const stableMark = bs.marks?.[bi];
|
|
261
270
|
return {
|
|
262
271
|
id,
|
|
263
272
|
key: begin,
|
|
264
273
|
value,
|
|
265
|
-
|
|
274
|
+
// The bar's own colour when per-bar coloured (stacked-path
|
|
275
|
+
// parity: the readout pill matches the pixels).
|
|
276
|
+
color: binColors?.[bi] ?? singleStyle.fill,
|
|
266
277
|
label,
|
|
278
|
+
...(stableMark !== undefined ? { mark: stableMark } : {}),
|
|
267
279
|
};
|
|
268
280
|
},
|
|
269
281
|
}),
|
|
270
|
-
draw: (ctx, xScale, yScale) => drawBars(ctx, bs, xScale, yScale, singleStyle, resolveBarBaseline(yScale), gapPx, id, selection, hover, decimate),
|
|
282
|
+
draw: (ctx, xScale, yScale) => drawBars(ctx, bs, xScale, yScale, singleStyle, resolveBarBaseline(yScale), gapPx, id, selection, hover, decimate, binColors),
|
|
271
283
|
},
|
|
272
284
|
axisId: axis,
|
|
273
285
|
index,
|
|
@@ -335,6 +347,7 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
|
|
|
335
347
|
orientation,
|
|
336
348
|
singleStyle,
|
|
337
349
|
stackStyle,
|
|
350
|
+
binColors,
|
|
338
351
|
label,
|
|
339
352
|
id,
|
|
340
353
|
gapPx,
|
package/dist/LineChart.d.ts
CHANGED
|
@@ -18,6 +18,17 @@ export interface LineChartProps<S extends SeriesSchema = SeriesSchema, VS extend
|
|
|
18
18
|
series: TimeSeries<S> | ValueSeries<VS>;
|
|
19
19
|
/** Name of the numeric value column to plot. */
|
|
20
20
|
column: string;
|
|
21
|
+
/**
|
|
22
|
+
* Optional column to **read out** at the cursor instead of the plotted
|
|
23
|
+
* `column`. The layer still plots `column`; each tracker sample additionally
|
|
24
|
+
* carries this column's value as {@link TrackerSample.readout}, so an
|
|
25
|
+
* off-chart readout can show the **source** value while the line draws a
|
|
26
|
+
* derived one — a smoothed / transformed / normalized plot with a raw-value
|
|
27
|
+
* readout (estela plots pace-space + Gaussian-smoothed, reads the native m/s).
|
|
28
|
+
* The plotted `value` (hence the in-chart cursor dot) is unchanged.
|
|
29
|
+
* **Omitted ⇒ no readout channel** (`readout` is `undefined` on the sample).
|
|
30
|
+
*/
|
|
31
|
+
readout?: string;
|
|
21
32
|
/**
|
|
22
33
|
* The series' semantic identifier — what the data _is_ / how it should read
|
|
23
34
|
* (e.g. `heartrate`, `power`, or a role name like `foam`). The theme maps it
|
|
@@ -94,5 +105,5 @@ export interface LineChartProps<S extends SeriesSchema = SeriesSchema, VS extend
|
|
|
94
105
|
* (scaling against its `axis`), and renders nothing to the DOM — the row draws
|
|
95
106
|
* it. The line breaks at gaps rather than spanning them.
|
|
96
107
|
*/
|
|
97
|
-
export declare function LineChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, as: semantic, axis, curve, gaps, sessionBreaks, decimate, legend, index, }: LineChartProps<S, VS>): null;
|
|
108
|
+
export declare function LineChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, readout, as: semantic, axis, curve, gaps, sessionBreaks, decimate, legend, index, }: LineChartProps<S, VS>): null;
|
|
98
109
|
//# sourceMappingURL=LineChart.d.ts.map
|
package/dist/LineChart.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useContext, useEffect, useMemo } from 'react';
|
|
2
2
|
import { ValueSeries } from 'pond-ts';
|
|
3
|
-
import { fromTimeSeries, fromValueSeries } from './data.js';
|
|
3
|
+
import { assertNumericColumn, fromTimeSeries, fromValueSeries, } from './data.js';
|
|
4
4
|
import { drawLine, yExtent } from './line.js';
|
|
5
5
|
import { resolveCurve } from './curve.js';
|
|
6
6
|
import { DEFAULT_GAP_MODE, DEFAULT_GAP_CONNECTOR_OPACITY, } from './gaps.js';
|
|
@@ -16,7 +16,7 @@ const NO_BREAKS = [];
|
|
|
16
16
|
* (scaling against its `axis`), and renders nothing to the DOM — the row draws
|
|
17
17
|
* it. The line breaks at gaps rather than spanning them.
|
|
18
18
|
*/
|
|
19
|
-
export function LineChart({ series, column, as: semantic, axis, curve, gaps = DEFAULT_GAP_MODE, sessionBreaks = false, decimate = true, legend, index = 0, }) {
|
|
19
|
+
export function LineChart({ series, column, readout, as: semantic, axis, curve, gaps = DEFAULT_GAP_MODE, sessionBreaks = false, decimate = true, legend, index = 0, }) {
|
|
20
20
|
const container = useContext(ContainerContext);
|
|
21
21
|
if (container === null) {
|
|
22
22
|
throw new Error('<LineChart> must be rendered inside a <ChartContainer>');
|
|
@@ -28,6 +28,22 @@ export function LineChart({ series, column, as: semantic, axis, curve, gaps = DE
|
|
|
28
28
|
const cs = useMemo(() => series instanceof ValueSeries
|
|
29
29
|
? fromValueSeries(series, column)
|
|
30
30
|
: fromTimeSeries(series, column), [series, column]);
|
|
31
|
+
// Readout column values for a value-axis series (the time path reads it off
|
|
32
|
+
// the event in `sampleAt`). Built once per (series, readout) so the tracker
|
|
33
|
+
// can report a source value the line doesn't plot — see LineChartProps.readout.
|
|
34
|
+
//
|
|
35
|
+
// The time path buffers nothing (it has an event, not an index), so it
|
|
36
|
+
// validates the name here instead: otherwise a mistyped `readout` throws on a
|
|
37
|
+
// value axis but silently yields no readout on a time axis, and the same typo
|
|
38
|
+
// fails two different ways. Both now throw the reader's errors.
|
|
39
|
+
const readoutY = useMemo(() => {
|
|
40
|
+
if (readout === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (series instanceof ValueSeries)
|
|
43
|
+
return fromValueSeries(series, readout).y;
|
|
44
|
+
assertNumericColumn(series, readout);
|
|
45
|
+
return undefined;
|
|
46
|
+
}, [series, readout]);
|
|
31
47
|
// Styling: semantic identifier → theme style. The single styling channel.
|
|
32
48
|
const { line } = container.theme;
|
|
33
49
|
const style = (semantic !== undefined ? line[semantic] : undefined) ?? line.default;
|
|
@@ -68,8 +84,19 @@ export function LineChart({ series, column, as: semantic, axis, curve, gaps = DE
|
|
|
68
84
|
if (i < 0)
|
|
69
85
|
return [];
|
|
70
86
|
const v = cs.y[i];
|
|
87
|
+
const rv = readoutY?.[i];
|
|
71
88
|
return Number.isFinite(v)
|
|
72
|
-
? [
|
|
89
|
+
? [
|
|
90
|
+
{
|
|
91
|
+
x: cs.x[i],
|
|
92
|
+
value: v,
|
|
93
|
+
color: style.color,
|
|
94
|
+
label,
|
|
95
|
+
...(rv !== undefined && Number.isFinite(rv)
|
|
96
|
+
? { readout: rv }
|
|
97
|
+
: {}),
|
|
98
|
+
},
|
|
99
|
+
]
|
|
73
100
|
: [];
|
|
74
101
|
}
|
|
75
102
|
const e = series.nearest(x);
|
|
@@ -78,9 +105,21 @@ export function LineChart({ series, column, as: semantic, axis, curve, gaps = DE
|
|
|
78
105
|
// get() wants a literal key; column is a runtime string. Cast the
|
|
79
106
|
// *event* (not the method — that would detach `this`) to a
|
|
80
107
|
// string-keyed get; runtime-safe read + guard.
|
|
81
|
-
const
|
|
108
|
+
const ev = e;
|
|
109
|
+
const v = ev.get(column);
|
|
110
|
+
const rv = readout !== undefined ? ev.get(readout) : undefined;
|
|
82
111
|
return typeof v === 'number' && Number.isFinite(v)
|
|
83
|
-
? [
|
|
112
|
+
? [
|
|
113
|
+
{
|
|
114
|
+
x: e.begin(),
|
|
115
|
+
value: v,
|
|
116
|
+
color: style.color,
|
|
117
|
+
label,
|
|
118
|
+
...(typeof rv === 'number' && Number.isFinite(rv)
|
|
119
|
+
? { readout: rv }
|
|
120
|
+
: {}),
|
|
121
|
+
},
|
|
122
|
+
]
|
|
84
123
|
: [];
|
|
85
124
|
},
|
|
86
125
|
draw: (ctx, xScale, yScale) => drawLine(ctx, cs, xScale, yScale, style, curveFactory, gaps, gapConnectorOpacity, sessionBreakInstants, decimate),
|
|
@@ -91,6 +130,8 @@ export function LineChart({ series, column, as: semantic, axis, curve, gaps = DE
|
|
|
91
130
|
cs,
|
|
92
131
|
series,
|
|
93
132
|
column,
|
|
133
|
+
readout,
|
|
134
|
+
readoutY,
|
|
94
135
|
style,
|
|
95
136
|
label,
|
|
96
137
|
curveFactory,
|
package/dist/affine.d.ts
CHANGED
|
@@ -2,13 +2,25 @@
|
|
|
2
2
|
* Affine-scale fast path (charts perf, [PND-AFFINE]). A chart's continuous
|
|
3
3
|
* scales — `scaleLinear` (value axis, every y axis), `scaleTime`, and the
|
|
4
4
|
* **gap-free** `scaleTradingTime(identityProvider())` (the default continuous
|
|
5
|
-
* time axis) — map data→pixels
|
|
6
|
-
*
|
|
5
|
+
* time axis) — map data→pixels affinely. The per-point draw loops in
|
|
6
|
+
* `drawLine` / `drawArea` can then evaluate the map inline over the typed
|
|
7
7
|
* arrays instead of paying a d3-scale closure call (deinterpolate → interpolate)
|
|
8
8
|
* per point — the ~37% of stroke-bound frame self-time the 2026-07 external
|
|
9
9
|
* bench profile attributed to `scale()` (see
|
|
10
10
|
* `docs/notes/charts-bench-vs-scichart-suite-2026-07.md`, finding 1).
|
|
11
11
|
*
|
|
12
|
+
* The map is stored and evaluated in the **rebased** form
|
|
13
|
+
* `px = (v − v0)·k + p0` (v0 = the domain's low endpoint, p0 = scale(v0)) —
|
|
14
|
+
* the same association d3's own deinterpolate → interpolate uses — never
|
|
15
|
+
* expanded to `k·v + b`. The expanded form is catastrophically ill-conditioned
|
|
16
|
+
* on epoch-millisecond domains: with t ≈ 1.8e12 and a deeply zoomed window,
|
|
17
|
+
* `k·t` and `b` are huge near-cancelling terms whose rounding (½ ULP of `k·t`)
|
|
18
|
+
* survives the cancellation — ~0.16 px reconstruction error at a 1 ms window,
|
|
19
|
+
* ~24 px at 1 µs (measured). Under the expanded form the interior probe below
|
|
20
|
+
* caught that drift and *rejected* the scale, so deep-zoomed frames silently
|
|
21
|
+
* lost the fast path; the rebased form evaluates to ≲1e-9 px of the exact
|
|
22
|
+
* d3 path at every zoom depth, so the fast path stays engaged.
|
|
23
|
+
*
|
|
12
24
|
* The affine coefficients are recovered from the scale's own domain/range
|
|
13
25
|
* endpoints, then **verified affine** by probing interior points: a scale that
|
|
14
26
|
* deviates (a `scaleTradingTime` with *collapsed* gaps, or a future
|
|
@@ -19,23 +31,32 @@
|
|
|
19
31
|
* a straight line.
|
|
20
32
|
*/
|
|
21
33
|
import type { Scale } from './line.js';
|
|
22
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Coefficients of an affine pixel map in rebased form:
|
|
36
|
+
* `px = (value − v0)·k + p0`. `v0` is the domain's low endpoint and `p0` its
|
|
37
|
+
* pixel image, so the multiply sees the O(span) offset `value − v0` (exact for
|
|
38
|
+
* in-domain values, by Sterbenz cancellation) rather than an O(1e12) absolute
|
|
39
|
+
* epoch value — see the module comment for why the expanded `k·value + b`
|
|
40
|
+
* form must not be reintroduced.
|
|
41
|
+
*/
|
|
23
42
|
export interface Affine {
|
|
24
43
|
readonly k: number;
|
|
25
|
-
readonly
|
|
44
|
+
readonly v0: number;
|
|
45
|
+
readonly p0: number;
|
|
26
46
|
}
|
|
27
47
|
/**
|
|
28
|
-
* The affine coefficients `{ k,
|
|
29
|
-
* `null` when the scale is not affine over its domain (a
|
|
30
|
-
* `scaleTradingTime`, a non-linear axis) or exposes no numeric
|
|
31
|
-
* bare `(v) => v` test stub, a `scaleBand` category axis).
|
|
32
|
-
* keeps the d3-scale path.
|
|
48
|
+
* The affine coefficients `{ k, v0, p0 }` with `scale(v) === (v − v0)·k + p0`
|
|
49
|
+
* for all `v`, or `null` when the scale is not affine over its domain (a
|
|
50
|
+
* real-gap `scaleTradingTime`, a non-linear axis) or exposes no numeric
|
|
51
|
+
* domain/range (a bare `(v) => v` test stub, a `scaleBand` category axis).
|
|
52
|
+
* `null` ⇒ the caller keeps the d3-scale path.
|
|
33
53
|
*
|
|
34
|
-
* Recovered from the domain/range endpoints (`k` from the two extremes,
|
|
35
|
-
* pinning the low end), then verified at
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
54
|
+
* Recovered from the domain/range endpoints (`k` from the two extremes, the
|
|
55
|
+
* `(v0, p0)` base pinning the low end), then verified at
|
|
56
|
+
* {@link PROBE_FRACTIONS}. Every probe must map finite and within
|
|
57
|
+
* {@link PROBE_EPSILON} of the reconstruction — so a scale that returns
|
|
58
|
+
* non-numbers for an interior value (a `scaleBand`) or bends away from the
|
|
59
|
+
* endpoint line (trading gaps, log) is rejected.
|
|
39
60
|
*/
|
|
40
61
|
export declare function affineOf(scale: Scale): Affine | null;
|
|
41
62
|
//# sourceMappingURL=affine.d.ts.map
|
package/dist/affine.js
CHANGED
|
@@ -2,13 +2,25 @@
|
|
|
2
2
|
* Affine-scale fast path (charts perf, [PND-AFFINE]). A chart's continuous
|
|
3
3
|
* scales — `scaleLinear` (value axis, every y axis), `scaleTime`, and the
|
|
4
4
|
* **gap-free** `scaleTradingTime(identityProvider())` (the default continuous
|
|
5
|
-
* time axis) — map data→pixels
|
|
6
|
-
*
|
|
5
|
+
* time axis) — map data→pixels affinely. The per-point draw loops in
|
|
6
|
+
* `drawLine` / `drawArea` can then evaluate the map inline over the typed
|
|
7
7
|
* arrays instead of paying a d3-scale closure call (deinterpolate → interpolate)
|
|
8
8
|
* per point — the ~37% of stroke-bound frame self-time the 2026-07 external
|
|
9
9
|
* bench profile attributed to `scale()` (see
|
|
10
10
|
* `docs/notes/charts-bench-vs-scichart-suite-2026-07.md`, finding 1).
|
|
11
11
|
*
|
|
12
|
+
* The map is stored and evaluated in the **rebased** form
|
|
13
|
+
* `px = (v − v0)·k + p0` (v0 = the domain's low endpoint, p0 = scale(v0)) —
|
|
14
|
+
* the same association d3's own deinterpolate → interpolate uses — never
|
|
15
|
+
* expanded to `k·v + b`. The expanded form is catastrophically ill-conditioned
|
|
16
|
+
* on epoch-millisecond domains: with t ≈ 1.8e12 and a deeply zoomed window,
|
|
17
|
+
* `k·t` and `b` are huge near-cancelling terms whose rounding (½ ULP of `k·t`)
|
|
18
|
+
* survives the cancellation — ~0.16 px reconstruction error at a 1 ms window,
|
|
19
|
+
* ~24 px at 1 µs (measured). Under the expanded form the interior probe below
|
|
20
|
+
* caught that drift and *rejected* the scale, so deep-zoomed frames silently
|
|
21
|
+
* lost the fast path; the rebased form evaluates to ≲1e-9 px of the exact
|
|
22
|
+
* d3 path at every zoom depth, so the fast path stays engaged.
|
|
23
|
+
*
|
|
12
24
|
* The affine coefficients are recovered from the scale's own domain/range
|
|
13
25
|
* endpoints, then **verified affine** by probing interior points: a scale that
|
|
14
26
|
* deviates (a `scaleTradingTime` with *collapsed* gaps, or a future
|
|
@@ -30,22 +42,25 @@ const PROBE_FRACTIONS = [0.1213, 0.2857, 0.4391, 0.6137, 0.7649, 0.8831];
|
|
|
30
42
|
/**
|
|
31
43
|
* Pixel tolerance for the affinity probe. Far below a sub-pixel (so a real
|
|
32
44
|
* non-affine deviation — a collapsed trading gap or a log curve is many pixels)
|
|
33
|
-
* yet far above the float-reconstruction noise of
|
|
34
|
-
* (
|
|
45
|
+
* yet far above the float-reconstruction noise of the rebased
|
|
46
|
+
* `(v − v0)·k + p0` against d3's own evaluation (≲1e-9 px at any domain
|
|
47
|
+
* magnitude or zoom depth — both sides subtract the domain origin before
|
|
48
|
+
* multiplying), so an exactly-affine scale is never rejected.
|
|
35
49
|
*/
|
|
36
50
|
const PROBE_EPSILON = 1e-3;
|
|
37
51
|
/**
|
|
38
|
-
* The affine coefficients `{ k,
|
|
39
|
-
* `null` when the scale is not affine over its domain (a
|
|
40
|
-
* `scaleTradingTime`, a non-linear axis) or exposes no numeric
|
|
41
|
-
* bare `(v) => v` test stub, a `scaleBand` category axis).
|
|
42
|
-
* keeps the d3-scale path.
|
|
52
|
+
* The affine coefficients `{ k, v0, p0 }` with `scale(v) === (v − v0)·k + p0`
|
|
53
|
+
* for all `v`, or `null` when the scale is not affine over its domain (a
|
|
54
|
+
* real-gap `scaleTradingTime`, a non-linear axis) or exposes no numeric
|
|
55
|
+
* domain/range (a bare `(v) => v` test stub, a `scaleBand` category axis).
|
|
56
|
+
* `null` ⇒ the caller keeps the d3-scale path.
|
|
43
57
|
*
|
|
44
|
-
* Recovered from the domain/range endpoints (`k` from the two extremes,
|
|
45
|
-
* pinning the low end), then verified at
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
58
|
+
* Recovered from the domain/range endpoints (`k` from the two extremes, the
|
|
59
|
+
* `(v0, p0)` base pinning the low end), then verified at
|
|
60
|
+
* {@link PROBE_FRACTIONS}. Every probe must map finite and within
|
|
61
|
+
* {@link PROBE_EPSILON} of the reconstruction — so a scale that returns
|
|
62
|
+
* non-numbers for an interior value (a `scaleBand`) or bends away from the
|
|
63
|
+
* endpoint line (trading gaps, log) is rejected.
|
|
49
64
|
*/
|
|
50
65
|
export function affineOf(scale) {
|
|
51
66
|
const s = scale;
|
|
@@ -63,15 +78,17 @@ export function affineOf(scale) {
|
|
|
63
78
|
if (!Number.isFinite(pLo) || !Number.isFinite(pHi))
|
|
64
79
|
return null;
|
|
65
80
|
const k = (pHi - pLo) / (hi - lo);
|
|
66
|
-
const b = pLo - k * lo;
|
|
67
81
|
const span = hi - lo;
|
|
68
82
|
for (const t of PROBE_FRACTIONS) {
|
|
69
83
|
const v = lo + t * span;
|
|
70
84
|
const p = scale(v);
|
|
71
|
-
|
|
85
|
+
// Probe the exact rebased expression the draw loops evaluate, so what is
|
|
86
|
+
// verified is what runs.
|
|
87
|
+
if (!Number.isFinite(p) ||
|
|
88
|
+
Math.abs(p - ((v - lo) * k + pLo)) > PROBE_EPSILON) {
|
|
72
89
|
return null;
|
|
73
90
|
}
|
|
74
91
|
}
|
|
75
|
-
return { k,
|
|
92
|
+
return { k, v0: lo, p0: pLo };
|
|
76
93
|
}
|
|
77
94
|
//# sourceMappingURL=affine.js.map
|
package/dist/area.js
CHANGED
|
@@ -65,8 +65,8 @@ export function fillAffineArea(ctx, xs, ys, baselinePx, ax, ay) {
|
|
|
65
65
|
for (let j = 0; j <= n; j += 1) {
|
|
66
66
|
const finite = j < n && Number.isFinite(ys[j]);
|
|
67
67
|
if (finite) {
|
|
68
|
-
const px = ax.
|
|
69
|
-
const py = ay.
|
|
68
|
+
const px = (xs[j] - ax.v0) * ax.k + ax.p0;
|
|
69
|
+
const py = (ys[j] - ay.v0) * ay.k + ay.p0;
|
|
70
70
|
if (runStart < 0) {
|
|
71
71
|
runStart = j;
|
|
72
72
|
ctx.moveTo(px, py);
|
|
@@ -78,8 +78,8 @@ export function fillAffineArea(ctx, xs, ys, baselinePx, ax, ay) {
|
|
|
78
78
|
else if (runStart >= 0) {
|
|
79
79
|
// Close the run: drop to the baseline under the last point, run flat back
|
|
80
80
|
// to the first point's x, close. (j-1 is the run's last finite index.)
|
|
81
|
-
ctx.lineTo(
|
|
82
|
-
ctx.lineTo(ax.
|
|
81
|
+
ctx.lineTo((xs[j - 1] - ax.v0) * ax.k + ax.p0, baselinePx);
|
|
82
|
+
ctx.lineTo((xs[runStart] - ax.v0) * ax.k + ax.p0, baselinePx);
|
|
83
83
|
ctx.closePath();
|
|
84
84
|
runStart = -1;
|
|
85
85
|
}
|
package/dist/bars.d.ts
CHANGED
|
@@ -54,22 +54,52 @@ export declare function resolveBarBaseline(yScale: Scale): number;
|
|
|
54
54
|
* hit rect are the same geometry.
|
|
55
55
|
*/
|
|
56
56
|
export declare function barRect(cs: BarSeries, i: number, xScale: Scale, yScale: Scale, baseline: number, gapPx: number, minWidthPx: number): [x0: number, x1: number, yTop: number, yBottom: number] | null;
|
|
57
|
+
/**
|
|
58
|
+
* The narrowed selection / hover identity a **single-series** bar matches
|
|
59
|
+
* against: the layer's series `id`, the sample's `key` (its `begin`), and — when
|
|
60
|
+
* the series carries {@link BarSeries.marks} — the stable per-bar `mark`. The
|
|
61
|
+
* single-series sibling of {@link StackMark}, which additionally carries the
|
|
62
|
+
* stack's group `label` (a single-series bar has no group to disambiguate).
|
|
63
|
+
*/
|
|
64
|
+
export interface BarMark {
|
|
65
|
+
readonly id: string;
|
|
66
|
+
readonly key: number;
|
|
67
|
+
readonly mark?: string;
|
|
68
|
+
}
|
|
57
69
|
/**
|
|
58
70
|
* Fill one rectangle per bar in `cs`, each spanning its key's `[begin, end]`
|
|
59
71
|
* (inset by `gapPx`) from the resolved `baseline` to the value.
|
|
60
72
|
*
|
|
61
73
|
* A gap (non-finite value) is skipped — no bar, no zero-height sliver. A bar
|
|
62
|
-
* matching the current `selection` (
|
|
63
|
-
*
|
|
64
|
-
* draws in the style's `highlight` colour **and
|
|
65
|
-
* on the canvas; a bar matching `hovered`
|
|
66
|
-
* outline (a lighter "this bar is live" on
|
|
67
|
-
* `fill`. `globalAlpha` carries the fill
|
|
68
|
-
* leak into later layers.
|
|
74
|
+
* matching the current `selection` (the layer's own series `id` — `seriesId`; a
|
|
75
|
+
* no-id layer passes `undefined` and never matches — plus the bar's identity,
|
|
76
|
+
* see {@link barMatches}) draws in the style's `highlight` colour **and
|
|
77
|
+
* outlined**, so a click reads back on the canvas; a bar matching `hovered`
|
|
78
|
+
* draws in `highlight` **without** the outline (a lighter "this bar is live" on
|
|
79
|
+
* pointer-over); all others use the flat `fill`. `globalAlpha` carries the fill
|
|
80
|
+
* opacity and is restored so it doesn't leak into later layers.
|
|
81
|
+
*
|
|
82
|
+
* **Which identity.** A selection carrying a `mark` matches against the series'
|
|
83
|
+
* stable per-bar name ({@link BarSeries.marks} — the sample's own axis key,
|
|
84
|
+
* which the readers always supply); one without falls back to the sample `key`
|
|
85
|
+
* (the bar's `begin`). The mark path is what lets a caller pin a bar on a
|
|
86
|
+
* **point-keyed** series without re-deriving the neighbour-spaced span, since
|
|
87
|
+
* there `begin` is not the sample's key but an edge computed from it.
|
|
69
88
|
*
|
|
70
89
|
* O(N) over the events, one fill (+ optional stroke) per bar, no per-bar
|
|
71
90
|
* allocation beyond the rect tuple.
|
|
72
91
|
*
|
|
92
|
+
* **Per-bar fills (`binFills`):** an optional colour array aligned
|
|
93
|
+
* index-for-index to the source bars — bar `i` fills with `binFills[i]`
|
|
94
|
+
* (an `undefined` entry falls back to the flat `fill`). This is the
|
|
95
|
+
* direction-coloured financial volume row (rising / falling) and the
|
|
96
|
+
* value-band case on a time axis. Highlight follows {@link drawStacks}'s
|
|
97
|
+
* binFills convention rather than the flat path's: the bar **keeps its own
|
|
98
|
+
* colour** under hover / selection — the highlight pops `globalAlpha` to 1
|
|
99
|
+
* (and outlines the selection in the bar's own fill) — so a red / green bar
|
|
100
|
+
* stays red / green while live, instead of swapping to the single
|
|
101
|
+
* `highlight` colour and losing its meaning.
|
|
102
|
+
*
|
|
73
103
|
* **M4 column decimation ([PND-MARKDEC]):** once the *visible* bars are denser
|
|
74
104
|
* than ~2 per device pixel, they overplot into a solid silhouette, so
|
|
75
105
|
* `decimate !== false` replaces them with one **envelope rect per pixel column**
|
|
@@ -79,15 +109,12 @@ export declare function barRect(cs: BarSeries, i: number, xScale: Scale, yScale:
|
|
|
79
109
|
* columns aren't individually selectable, so per-bar selection/hover highlight is
|
|
80
110
|
* suppressed (a <1px bar's ring wouldn't be visible anyway); interaction still
|
|
81
111
|
* reads the **source** bars via {@link barAt} (§2.3). Pass `decimate={false}` to
|
|
82
|
-
* always draw every bar.
|
|
112
|
+
* always draw every bar. **`binFills` disables the envelope pass** — a single
|
|
113
|
+
* envelope rect spans many differently-coloured bars, so decimating would
|
|
114
|
+
* repaint them one flat colour; per-bar-coloured layers draw every visible bar.
|
|
115
|
+
* Returns {@link LayerDrawStats} for `onDrawStats`.
|
|
83
116
|
*/
|
|
84
|
-
export declare function drawBars(ctx: CanvasRenderingContext2D, cs: BarSeries, xScale: Scale, yScale: Scale, style: BarStyle, baseline: number, gapPx: number, seriesId: string | undefined, selection:
|
|
85
|
-
key: number;
|
|
86
|
-
id: string;
|
|
87
|
-
} | null, hovered: {
|
|
88
|
-
key: number;
|
|
89
|
-
id: string;
|
|
90
|
-
} | null, decimate?: DecimateOption): LayerDrawStats;
|
|
117
|
+
export declare function drawBars(ctx: CanvasRenderingContext2D, cs: BarSeries, xScale: Scale, yScale: Scale, style: BarStyle, baseline: number, gapPx: number, seriesId: string | undefined, selection: BarMark | null, hovered: BarMark | null, decimate?: DecimateOption, binFills?: readonly (string | undefined)[]): LayerDrawStats;
|
|
91
118
|
/**
|
|
92
119
|
* The index of the bar whose key span `[begin, end]` contains `time` — the bar
|
|
93
120
|
* **under the cursor** — or `-1` if `time` falls in no bar's span. This is the
|