@pond-ts/charts 0.53.0 → 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 +900 -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/ChartContainer.js +10 -1
- package/dist/LineChart.d.ts +12 -1
- package/dist/LineChart.js +46 -5
- package/dist/XAxis.js +13 -1
- 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 +39 -7
- package/dist/data.d.ts +58 -0
- package/dist/data.js +85 -22
- package/dist/decimate.js +7 -7
- package/dist/elapsed.js +43 -1
- 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/ChartContainer.js
CHANGED
|
@@ -510,7 +510,14 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
510
510
|
})
|
|
511
511
|
: cursorFormat !== undefined
|
|
512
512
|
? resolveTimeFormat(e, xTickCount, cursorFormat)
|
|
513
|
-
:
|
|
513
|
+
: // A container `timeFormat` shapes the readout too when no
|
|
514
|
+
// `cursorFormat` is set — its documented back-compat behaviour,
|
|
515
|
+
// and the same inversion as the `<XAxis format>` one a rung down:
|
|
516
|
+
// a custom format that owns the labels must own the pill rather
|
|
517
|
+
// than watch the elapsed default overrule it (PR #541 review).
|
|
518
|
+
timeFormat !== undefined
|
|
519
|
+
? labels
|
|
520
|
+
: fine;
|
|
514
521
|
return { xScale: e, formatTime: labels, formatReadout: readout };
|
|
515
522
|
};
|
|
516
523
|
if (xDiscontinuities !== undefined) {
|
|
@@ -688,6 +695,7 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
688
695
|
formatTime,
|
|
689
696
|
formatReadout,
|
|
690
697
|
xFormatCustom: timeFormat !== undefined,
|
|
698
|
+
xReadoutCustom: cursorFormat !== undefined,
|
|
691
699
|
xTickCount,
|
|
692
700
|
registerTrackerSource,
|
|
693
701
|
unregisterTrackerSource,
|
|
@@ -749,6 +757,7 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
749
757
|
formatTime,
|
|
750
758
|
formatReadout,
|
|
751
759
|
timeFormat,
|
|
760
|
+
cursorFormat,
|
|
752
761
|
xTickCount,
|
|
753
762
|
registerTrackerSource,
|
|
754
763
|
unregisterTrackerSource,
|
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/XAxis.js
CHANGED
|
@@ -165,7 +165,19 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
165
165
|
// own channel, so precise-pill-over-terse-ticks works on any axis kind. A
|
|
166
166
|
// `transform`ed axis is exempt — its pill speaks the derived unit, which a
|
|
167
167
|
// data-unit `cursorFormat` can't address.
|
|
168
|
-
|
|
168
|
+
//
|
|
169
|
+
// The middle rung is why `xReadoutCustom` exists: `formatReadout` carries
|
|
170
|
+
// either a real `cursorFormat` (which outranks this axis's `format`) or the
|
|
171
|
+
// axis kind's own default readout (which does not — an elapsed container's
|
|
172
|
+
// finer `00:05:12`). Reading the field alone can't tell them apart, so a
|
|
173
|
+
// wall-clock strip declared as `<XAxis format="%H:%M">` under an elapsed
|
|
174
|
+
// container used to label its ticks `11:33` and pill them `00:05:12`
|
|
175
|
+
// (issue #540).
|
|
176
|
+
const readoutFmt = transform !== undefined
|
|
177
|
+
? fmt
|
|
178
|
+
: format !== undefined && !container.xReadoutCustom
|
|
179
|
+
? fmt
|
|
180
|
+
: (container.formatReadout ?? fmt);
|
|
169
181
|
// Marker annotations that opted into an axis indicator (`<Marker indicator>`)
|
|
170
182
|
// pin their **time** to this shared x-axis — a pill at `at`, in the annotation
|
|
171
183
|
// colour, reading like a tick. An indicator always shows the axis coordinate
|
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
|
}
|