@pond-ts/charts 0.50.0 → 0.52.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 +187 -1
- package/dist/AreaChart.js +1 -0
- package/dist/BandChart.js +1 -0
- package/dist/BarChart.d.ts +14 -1
- package/dist/BarChart.js +5 -2
- package/dist/BoxPlot.js +1 -0
- package/dist/Candlestick.js +1 -0
- package/dist/ChartContainer.d.ts +61 -11
- package/dist/ChartContainer.js +70 -23
- package/dist/Layers.js +53 -14
- package/dist/Legend.js +5 -2
- package/dist/LineChart.js +1 -0
- package/dist/ScatterChart.d.ts +12 -1
- package/dist/ScatterChart.js +4 -2
- package/dist/XAxis.js +3 -2
- package/dist/affine.d.ts +41 -0
- package/dist/affine.js +77 -0
- package/dist/area.d.ts +20 -2
- package/dist/area.js +151 -45
- package/dist/band.d.ts +2 -1
- package/dist/band.js +5 -0
- package/dist/bars.d.ts +14 -1
- package/dist/bars.js +42 -1
- package/dist/box.d.ts +2 -1
- package/dist/box.js +8 -3
- package/dist/context.d.ts +121 -22
- package/dist/context.js +8 -0
- package/dist/data.d.ts +8 -0
- package/dist/decimate.d.ts +117 -1
- package/dist/decimate.js +241 -1
- package/dist/encoding.d.ts +9 -0
- package/dist/encoding.js +5 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -3
- package/dist/line.d.ts +15 -1
- package/dist/line.js +84 -30
- package/dist/ohlc.d.ts +2 -1
- package/dist/ohlc.js +8 -3
- package/dist/scatter.d.ts +3 -2
- package/dist/scatter.js +61 -1
- package/dist/tracker.d.ts +17 -4
- package/dist/tracker.js +19 -6
- package/dist/useChartLegend.d.ts +2 -2
- package/dist/useChartLegend.js +8 -7
- package/dist/viewport.d.ts +19 -0
- package/dist/viewport.js +32 -0
- package/package.json +3 -3
package/dist/Layers.js
CHANGED
|
@@ -6,7 +6,7 @@ import { cursorParts, bandRect, regionSpan } from './tracker.js';
|
|
|
6
6
|
import { resolveSelection } from './select.js';
|
|
7
7
|
import { panRange, zoomRange, panRangeTrading, zoomRangeTrading, } from './viewport.js';
|
|
8
8
|
import { flagChipStyle, flagChipX, axisPillX, axisPillStyle } from './chip.js';
|
|
9
|
-
import { ContainerContext, LayersContext, RowContext, } from './context.js';
|
|
9
|
+
import { ContainerContext, CursorContext, LayersContext, RowContext, } from './context.js';
|
|
10
10
|
/** Fallback **y**-gridline tick count, used only before the row publishes its
|
|
11
11
|
* resolved `tickCounts` (pre-registration). Normally the gridlines read the
|
|
12
12
|
* default axis's resolved count from `row.tickCounts` — the same value the
|
|
@@ -69,6 +69,9 @@ export function Layers({ children }) {
|
|
|
69
69
|
if (container === null) {
|
|
70
70
|
throw new Error('<Layers> must be rendered inside a <ChartContainer>');
|
|
71
71
|
}
|
|
72
|
+
// Per-move cursor state (own context, so this overlay re-renders on hover
|
|
73
|
+
// without re-identifying the container frame — [PND-HOVCTX]).
|
|
74
|
+
const cursor = useContext(CursorContext);
|
|
72
75
|
const row = useContext(RowContext);
|
|
73
76
|
if (row === null) {
|
|
74
77
|
throw new Error('<Layers> must be rendered inside a <ChartRow>');
|
|
@@ -204,11 +207,40 @@ export function Layers({ children }) {
|
|
|
204
207
|
drawDividers(ctx, thinPixels(bx, MIN_DIVIDER_PX), h, dividerColor);
|
|
205
208
|
}
|
|
206
209
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
// Draw the layers. When a draw-stats consumer is subscribed
|
|
211
|
+
// (`reportDrawStats` defined), time each layer and collect its reported
|
|
212
|
+
// {@link LayerDrawStats}; otherwise the plain loop with zero timing
|
|
213
|
+
// overhead. Fires one {@link DrawStatsFrame} per repaint of THIS row.
|
|
214
|
+
const report = container.reportDrawStats;
|
|
215
|
+
if (report === undefined) {
|
|
216
|
+
for (const entry of layers) {
|
|
217
|
+
const yScale = yScales.get(entry.axisId ?? defaultAxisId);
|
|
218
|
+
if (yScale === undefined)
|
|
219
|
+
continue;
|
|
220
|
+
entry.layer.draw(ctx, xScale, yScale);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const infos = [];
|
|
225
|
+
let totalDrawMs = 0;
|
|
226
|
+
for (const entry of layers) {
|
|
227
|
+
const yScale = yScales.get(entry.axisId ?? defaultAxisId);
|
|
228
|
+
if (yScale === undefined)
|
|
229
|
+
continue;
|
|
230
|
+
const t0 = performance.now();
|
|
231
|
+
const stats = entry.layer.draw(ctx, xScale, yScale);
|
|
232
|
+
const drawMs = performance.now() - t0;
|
|
233
|
+
totalDrawMs += drawMs;
|
|
234
|
+
infos.push({
|
|
235
|
+
as: entry.layer.as,
|
|
236
|
+
index: entry.index,
|
|
237
|
+
drawMs,
|
|
238
|
+
sourceCount: stats?.sourceCount,
|
|
239
|
+
drawnCount: stats?.drawnCount,
|
|
240
|
+
decimated: stats?.decimated,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
report({ rowKey: row.rowKey, layers: infos, totalDrawMs });
|
|
212
244
|
}
|
|
213
245
|
}, [
|
|
214
246
|
layers,
|
|
@@ -222,7 +254,13 @@ export function Layers({ children }) {
|
|
|
222
254
|
gridColor,
|
|
223
255
|
gridDash,
|
|
224
256
|
container.discontinuities,
|
|
257
|
+
// Identity-stable across cursor moves (memoized tuple in ChartContainer;
|
|
258
|
+
// e2e `hover sweep never repaints the data canvas` pins this). A fresh
|
|
259
|
+
// array per frame rebuild here would re-fire the Canvas draw effect —
|
|
260
|
+
// a full replot per mousemove.
|
|
225
261
|
container.timeRange,
|
|
262
|
+
container.reportDrawStats,
|
|
263
|
+
row.rowKey,
|
|
226
264
|
]);
|
|
227
265
|
// Interaction overlay: the cursor marks live on a DOM/SVG overlay above the
|
|
228
266
|
// data, so hovering never repaints the data canvas (whose `draw` doesn't depend
|
|
@@ -230,7 +268,8 @@ export function Layers({ children }) {
|
|
|
230
268
|
// pointer is over — syncs the cursor across every row for free. cursorX is a
|
|
231
269
|
// *pixel*, so it stays put while a live window slides; the time + values under
|
|
232
270
|
// it derive from the current xScale.
|
|
233
|
-
const {
|
|
271
|
+
const { cursorTime: showCursorTime, formatTime } = container;
|
|
272
|
+
const { cursorX } = cursor;
|
|
234
273
|
// Cursor mode: the row's override, else the container default. One mode per
|
|
235
274
|
// row (the synced vertical line is shared across rows); each layer renders the
|
|
236
275
|
// mode in its own way. `parts` decomposes it into {line, dots, chip}.
|
|
@@ -392,7 +431,7 @@ export function Layers({ children }) {
|
|
|
392
431
|
if (c.cursor === 'region' &&
|
|
393
432
|
c.onRegionSelect &&
|
|
394
433
|
(c.xKind === 'time' || c.xKind === 'value')) {
|
|
395
|
-
const needsShift = c.regionSelectModifier === 'shift' && c.
|
|
434
|
+
const needsShift = c.regionSelectModifier === 'shift' && c.panEnabled;
|
|
396
435
|
if (!needsShift || e.shiftKey) {
|
|
397
436
|
const px = Math.max(0, Math.min(c.plotWidth, e.clientX - e.currentTarget.getBoundingClientRect().left));
|
|
398
437
|
regionAnchorRef.current = +c.xScale.invert(px);
|
|
@@ -408,7 +447,7 @@ export function Layers({ children }) {
|
|
|
408
447
|
}
|
|
409
448
|
// Modifier required but not held → fall through to pan.
|
|
410
449
|
}
|
|
411
|
-
if (!c.
|
|
450
|
+
if (!c.panEnabled || c.xKind === 'category')
|
|
412
451
|
return;
|
|
413
452
|
const r = c.timeRange;
|
|
414
453
|
// Arm a potential pan: record the anchor, but DON'T capture the pointer or
|
|
@@ -663,7 +702,7 @@ export function Layers({ children }) {
|
|
|
663
702
|
return;
|
|
664
703
|
const onWheel = (e) => {
|
|
665
704
|
const c = containerRef.current;
|
|
666
|
-
if (!c.
|
|
705
|
+
if (!c.zoomEnabled || c.xKind === 'category')
|
|
667
706
|
return;
|
|
668
707
|
e.preventDefault();
|
|
669
708
|
const rect = el.getBoundingClientRect();
|
|
@@ -724,14 +763,14 @@ export function Layers({ children }) {
|
|
|
724
763
|
const reticle = (() => {
|
|
725
764
|
if (parts.chip !== 'axis' || !cursorInBounds)
|
|
726
765
|
return null;
|
|
727
|
-
const hoveredRow =
|
|
728
|
-
const cy =
|
|
766
|
+
const hoveredRow = cursor.cursorRowKey === row.rowKey;
|
|
767
|
+
const cy = cursor.cursorY;
|
|
729
768
|
if (container.crosshairSnap) {
|
|
730
769
|
if (trackerSamples.length === 0)
|
|
731
770
|
return null;
|
|
732
771
|
const pick = hoveredRow && cy !== null
|
|
733
772
|
? trackerSamples.reduce((a, b) => Math.abs(b.py - cy) < Math.abs(a.py - cy) ? b : a)
|
|
734
|
-
:
|
|
773
|
+
: cursor.cursorRowKey === null
|
|
735
774
|
? trackerSamples[0]
|
|
736
775
|
: null;
|
|
737
776
|
return pick
|
|
@@ -824,7 +863,7 @@ export function Layers({ children }) {
|
|
|
824
863
|
? `inset 0 0 0 1px ${guideColor}`
|
|
825
864
|
: undefined,
|
|
826
865
|
// Let pan/zoom own touch gestures (no native scroll) when enabled.
|
|
827
|
-
touchAction: container.
|
|
866
|
+
touchAction: container.panEnabled || container.zoomEnabled ? 'none' : 'auto',
|
|
828
867
|
}, onPointerMove: handlePointerMove, onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, onPointerCancel: handlePointerUp, onPointerLeave: handlePointerLeave, onClick: handleClick, children: [_jsx(Canvas, { width: plotWidth, height: row.height, draw: draw }), guideXs.length > 0 && (_jsx("svg", { width: plotWidth, height: row.height, style: {
|
|
829
868
|
position: 'absolute',
|
|
830
869
|
top: 0,
|
package/dist/Legend.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useContext } from 'react';
|
|
3
|
-
import { ContainerContext, RowContext } from './context.js';
|
|
3
|
+
import { ContainerContext, CursorContext, RowContext } from './context.js';
|
|
4
4
|
import { buildChartLegend } from './useChartLegend.js';
|
|
5
5
|
import { defaultTheme } from './theme.js';
|
|
6
6
|
/** One 20×12 swatch glyph, drawn from the layer's resolved style. */
|
|
@@ -83,6 +83,7 @@ function placementStyle(placement, leftGutter, rightGutter) {
|
|
|
83
83
|
*/
|
|
84
84
|
export function Legend({ placement = 'top-right', items, onRowClick, onRowHover, theme: themeProp, }) {
|
|
85
85
|
const container = useContext(ContainerContext);
|
|
86
|
+
const cursor = useContext(CursorContext);
|
|
86
87
|
if (container === null && items === undefined) {
|
|
87
88
|
throw new Error('<Legend> must be inside a <ChartContainer> (or be given explicit `items`)');
|
|
88
89
|
}
|
|
@@ -99,7 +100,9 @@ export function Legend({ placement = 'top-right', items, onRowClick, onRowHover,
|
|
|
99
100
|
// The shared headless core (also `useChartLegend`'s) — rows + the id-gated
|
|
100
101
|
// hover/select verbs — so the built-in card and a custom-rendered legend
|
|
101
102
|
// can never disagree. `null` in standalone `items` mode (no chart to sync).
|
|
102
|
-
const legend = container !== null
|
|
103
|
+
const legend = container !== null
|
|
104
|
+
? buildChartLegend(container, cursor, row?.rowKey)
|
|
105
|
+
: null;
|
|
103
106
|
// The card renders a flat item list — `rows` is grouped by chart row, so
|
|
104
107
|
// flatten it (a scoped legend has one group anyway).
|
|
105
108
|
const entries = items ?? legend.rows.flatMap((r) => r.items);
|
package/dist/LineChart.js
CHANGED
|
@@ -50,6 +50,7 @@ export function LineChart({ series, column, as: semantic, axis, curve, gaps = DE
|
|
|
50
50
|
}, [sessionBreaks, container.discontinuities, cs]);
|
|
51
51
|
const entry = useMemo(() => ({
|
|
52
52
|
layer: {
|
|
53
|
+
as: semantic,
|
|
53
54
|
yExtent: () => yExtent(cs),
|
|
54
55
|
// The container infers the shared x scale's kind + auto-fit domain from
|
|
55
56
|
// its layers: a ValueSeries plots on a value axis, a TimeSeries on time.
|
package/dist/ScatterChart.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ValueSeries } from 'pond-ts';
|
|
2
2
|
import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
|
|
3
3
|
import { type ColorEncoding, type RadiusEncoding } from './encoding.js';
|
|
4
|
+
import type { DecimateOption } from './decimate.js';
|
|
4
5
|
export interface ScatterChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
|
|
5
6
|
/**
|
|
6
7
|
* The source series. A `TimeSeries` scatters against the time axis; a
|
|
@@ -85,6 +86,16 @@ export interface ScatterChartProps<S extends SeriesSchema = SeriesSchema, VS ext
|
|
|
85
86
|
* together, so a nudged point still selects.
|
|
86
87
|
*/
|
|
87
88
|
offset?: number;
|
|
89
|
+
/**
|
|
90
|
+
* Collapse dense, **uniform** marks to one representative per pixel cell —
|
|
91
|
+
* lossless at that density, so a scatter of 100k+ points stays interactive.
|
|
92
|
+
* **Default `true`.** It engages only when the marks are a fixed size + colour
|
|
93
|
+
* (no data-driven `radius`/`color`), the fill is opaque, and the visible points
|
|
94
|
+
* are denser than the pixel grid; otherwise every point draws. Interaction
|
|
95
|
+
* (hover / click / tracker) always reads the source points. `decimate={false}`
|
|
96
|
+
* draws every mark; `{ threshold }` tunes the samples-per-pixel trigger.
|
|
97
|
+
*/
|
|
98
|
+
decimate?: DecimateOption;
|
|
88
99
|
/**
|
|
89
100
|
* This layer's `<Legend>` row: `false` ⇒ no row (opt out), a string ⇒ the
|
|
90
101
|
* row's display name. **Omitted ⇒ a row named by the layer's readout
|
|
@@ -126,5 +137,5 @@ export interface ScatterChartProps<S extends SeriesSchema = SeriesSchema, VS ext
|
|
|
126
137
|
* </Layers>
|
|
127
138
|
* ```
|
|
128
139
|
*/
|
|
129
|
-
export declare function ScatterChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, as: semantic, id, axis, radius, color, label, offset, legend, index, }: ScatterChartProps<S, VS>): null;
|
|
140
|
+
export declare function ScatterChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, as: semantic, id, axis, radius, color, label, offset, decimate, legend, index, }: ScatterChartProps<S, VS>): null;
|
|
130
141
|
//# sourceMappingURL=ScatterChart.d.ts.map
|
package/dist/ScatterChart.js
CHANGED
|
@@ -34,7 +34,7 @@ import { useSlotKey } from './use-slot-key.js';
|
|
|
34
34
|
* </Layers>
|
|
35
35
|
* ```
|
|
36
36
|
*/
|
|
37
|
-
export function ScatterChart({ series, column, as: semantic, id, axis, radius, color, label, offset = 0, legend, index = 0, }) {
|
|
37
|
+
export function ScatterChart({ series, column, as: semantic, id, axis, radius, color, label, offset = 0, decimate = true, legend, index = 0, }) {
|
|
38
38
|
const container = useContext(ContainerContext);
|
|
39
39
|
if (container === null) {
|
|
40
40
|
throw new Error('<ScatterChart> must be rendered inside a <ChartContainer>');
|
|
@@ -95,6 +95,7 @@ export function ScatterChart({ series, column, as: semantic, id, axis, radius, c
|
|
|
95
95
|
const keyAt = useMemo(() => (i) => cs.x[i], [cs]);
|
|
96
96
|
const entry = useMemo(() => ({
|
|
97
97
|
layer: {
|
|
98
|
+
as: semantic,
|
|
98
99
|
yExtent: () => scatterExtent(cs),
|
|
99
100
|
// The container infers the shared x scale's kind + auto-fit domain from
|
|
100
101
|
// its layers: a ValueSeries scatters on a value axis, a TimeSeries on time.
|
|
@@ -130,7 +131,7 @@ export function ScatterChart({ series, column, as: semantic, id, axis, radius, c
|
|
|
130
131
|
: {
|
|
131
132
|
hitTest: (px, py, xScale, yScale) => hitTestScatter(cs, px, py, xScale, yScale, encoding, keyAt, id, seriesLabel, offset),
|
|
132
133
|
}),
|
|
133
|
-
draw: (ctx, xScale, yScale) => drawScatter(ctx, cs, xScale, yScale, style, encoding, keyAt, labelAt, font, container.selected, id, offset),
|
|
134
|
+
draw: (ctx, xScale, yScale) => drawScatter(ctx, cs, xScale, yScale, style, encoding, keyAt, labelAt, font, container.selected, id, offset, decimate),
|
|
134
135
|
},
|
|
135
136
|
axisId: axis,
|
|
136
137
|
index,
|
|
@@ -147,6 +148,7 @@ export function ScatterChart({ series, column, as: semantic, id, axis, radius, c
|
|
|
147
148
|
font,
|
|
148
149
|
container.selected,
|
|
149
150
|
offset,
|
|
151
|
+
decimate,
|
|
150
152
|
axis,
|
|
151
153
|
index,
|
|
152
154
|
]);
|
package/dist/XAxis.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Fragment, useContext } from 'react';
|
|
3
3
|
import { scaleLinear } from 'd3-scale';
|
|
4
4
|
import { derivedTicks } from './derivedTicks.js';
|
|
5
|
-
import { ContainerContext } from './context.js';
|
|
5
|
+
import { ContainerContext, CursorContext } from './context.js';
|
|
6
6
|
import { axisPillStyle } from './chip.js';
|
|
7
7
|
import { resolveAxisFormat, resolveTimeFormat, } from './format.js';
|
|
8
8
|
/** Tick strip height (mark + value label) in CSS px. */
|
|
@@ -60,6 +60,7 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
60
60
|
if (container === null) {
|
|
61
61
|
throw new Error('<XAxis> must be rendered inside a <ChartContainer>');
|
|
62
62
|
}
|
|
63
|
+
const cursor = useContext(CursorContext);
|
|
63
64
|
// `xTickCount` is the container's shared x-side count — the same value the x
|
|
64
65
|
// gridlines and `formatTime` use, so labels and grid stay on the same instants
|
|
65
66
|
// (width-derived on a trading-time axis).
|
|
@@ -68,7 +69,7 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
68
69
|
// cursor is live in-bounds, pin the hovered time to this axis (covering the
|
|
69
70
|
// tick behind it), matching the on-axis y value pills the rows draw. Gated on
|
|
70
71
|
// the container default, so a per-row `cursor` override doesn't reach here.
|
|
71
|
-
const cursorX =
|
|
72
|
+
const cursorX = cursor.cursorX;
|
|
72
73
|
const showCursorTag = container.cursor === 'crosshair' &&
|
|
73
74
|
cursorX !== null &&
|
|
74
75
|
cursorX >= 0 &&
|
package/dist/affine.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Affine-scale fast path (charts perf, [PND-AFFINE]). A chart's continuous
|
|
3
|
+
* scales — `scaleLinear` (value axis, every y axis), `scaleTime`, and the
|
|
4
|
+
* **gap-free** `scaleTradingTime(identityProvider())` (the default continuous
|
|
5
|
+
* time axis) — map data→pixels by a single `px = k·v + b`. The per-point draw
|
|
6
|
+
* loops in `drawLine` / `drawArea` can then multiply-add inline over the typed
|
|
7
|
+
* arrays instead of paying a d3-scale closure call (deinterpolate → interpolate)
|
|
8
|
+
* per point — the ~37% of stroke-bound frame self-time the 2026-07 external
|
|
9
|
+
* bench profile attributed to `scale()` (see
|
|
10
|
+
* `docs/notes/charts-bench-vs-scichart-suite-2026-07.md`, finding 1).
|
|
11
|
+
*
|
|
12
|
+
* The affine coefficients are recovered from the scale's own domain/range
|
|
13
|
+
* endpoints, then **verified affine** by probing interior points: a scale that
|
|
14
|
+
* deviates (a `scaleTradingTime` with *collapsed* gaps, or a future
|
|
15
|
+
* log/pow/sqrt axis) is rejected — the caller falls back to the exact d3-scale
|
|
16
|
+
* path — while a genuinely affine scale (including a gap-free trading axis) is
|
|
17
|
+
* accepted and reproduced to floating-point precision. The verification is what
|
|
18
|
+
* keeps the fast path a pure optimization: it never draws a non-affine scale as
|
|
19
|
+
* a straight line.
|
|
20
|
+
*/
|
|
21
|
+
import type { Scale } from './line.js';
|
|
22
|
+
/** Coefficients of an affine pixel map `px = k·value + b`. */
|
|
23
|
+
export interface Affine {
|
|
24
|
+
readonly k: number;
|
|
25
|
+
readonly b: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The affine coefficients `{ k, b }` with `scale(v) === k·v + b` for all `v`, or
|
|
29
|
+
* `null` when the scale is not affine over its domain (a real-gap
|
|
30
|
+
* `scaleTradingTime`, a non-linear axis) or exposes no numeric domain/range (a
|
|
31
|
+
* bare `(v) => v` test stub, a `scaleBand` category axis). `null` ⇒ the caller
|
|
32
|
+
* keeps the d3-scale path.
|
|
33
|
+
*
|
|
34
|
+
* Recovered from the domain/range endpoints (`k` from the two extremes, `b`
|
|
35
|
+
* pinning the low end), then verified at {@link PROBE_FRACTIONS}. Every probe
|
|
36
|
+
* must map finite and within {@link PROBE_EPSILON} of the reconstruction — so a
|
|
37
|
+
* scale that returns non-numbers for an interior value (a `scaleBand`) or bends
|
|
38
|
+
* away from the endpoint line (trading gaps, log) is rejected.
|
|
39
|
+
*/
|
|
40
|
+
export declare function affineOf(scale: Scale): Affine | null;
|
|
41
|
+
//# sourceMappingURL=affine.d.ts.map
|
package/dist/affine.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Affine-scale fast path (charts perf, [PND-AFFINE]). A chart's continuous
|
|
3
|
+
* scales — `scaleLinear` (value axis, every y axis), `scaleTime`, and the
|
|
4
|
+
* **gap-free** `scaleTradingTime(identityProvider())` (the default continuous
|
|
5
|
+
* time axis) — map data→pixels by a single `px = k·v + b`. The per-point draw
|
|
6
|
+
* loops in `drawLine` / `drawArea` can then multiply-add inline over the typed
|
|
7
|
+
* arrays instead of paying a d3-scale closure call (deinterpolate → interpolate)
|
|
8
|
+
* per point — the ~37% of stroke-bound frame self-time the 2026-07 external
|
|
9
|
+
* bench profile attributed to `scale()` (see
|
|
10
|
+
* `docs/notes/charts-bench-vs-scichart-suite-2026-07.md`, finding 1).
|
|
11
|
+
*
|
|
12
|
+
* The affine coefficients are recovered from the scale's own domain/range
|
|
13
|
+
* endpoints, then **verified affine** by probing interior points: a scale that
|
|
14
|
+
* deviates (a `scaleTradingTime` with *collapsed* gaps, or a future
|
|
15
|
+
* log/pow/sqrt axis) is rejected — the caller falls back to the exact d3-scale
|
|
16
|
+
* path — while a genuinely affine scale (including a gap-free trading axis) is
|
|
17
|
+
* accepted and reproduced to floating-point precision. The verification is what
|
|
18
|
+
* keeps the fast path a pure optimization: it never draws a non-affine scale as
|
|
19
|
+
* a straight line.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Irregular interior sample fractions for the affinity probe. Deliberately not
|
|
23
|
+
* `[0.25, 0.5, 0.75]` — a piecewise-linear scale (trading time) can have
|
|
24
|
+
* breakpoints that a symmetric, round-fraction probe set slips between; the
|
|
25
|
+
* jittered spread makes a false "affine" verdict on a real-gap scale
|
|
26
|
+
* astronomically unlikely (and the e2e visual-regression layer is the backstop,
|
|
27
|
+
* the same net that guards M4).
|
|
28
|
+
*/
|
|
29
|
+
const PROBE_FRACTIONS = [0.1213, 0.2857, 0.4391, 0.6137, 0.7649, 0.8831];
|
|
30
|
+
/**
|
|
31
|
+
* Pixel tolerance for the affinity probe. Far below a sub-pixel (so a real
|
|
32
|
+
* non-affine deviation — a collapsed trading gap or a log curve is many pixels)
|
|
33
|
+
* yet far above the float-reconstruction noise of `k·v + b` on wide domains
|
|
34
|
+
* (~1e-9 px), so an exactly-affine scale is never rejected.
|
|
35
|
+
*/
|
|
36
|
+
const PROBE_EPSILON = 1e-3;
|
|
37
|
+
/**
|
|
38
|
+
* The affine coefficients `{ k, b }` with `scale(v) === k·v + b` for all `v`, or
|
|
39
|
+
* `null` when the scale is not affine over its domain (a real-gap
|
|
40
|
+
* `scaleTradingTime`, a non-linear axis) or exposes no numeric domain/range (a
|
|
41
|
+
* bare `(v) => v` test stub, a `scaleBand` category axis). `null` ⇒ the caller
|
|
42
|
+
* keeps the d3-scale path.
|
|
43
|
+
*
|
|
44
|
+
* Recovered from the domain/range endpoints (`k` from the two extremes, `b`
|
|
45
|
+
* pinning the low end), then verified at {@link PROBE_FRACTIONS}. Every probe
|
|
46
|
+
* must map finite and within {@link PROBE_EPSILON} of the reconstruction — so a
|
|
47
|
+
* scale that returns non-numbers for an interior value (a `scaleBand`) or bends
|
|
48
|
+
* away from the endpoint line (trading gaps, log) is rejected.
|
|
49
|
+
*/
|
|
50
|
+
export function affineOf(scale) {
|
|
51
|
+
const s = scale;
|
|
52
|
+
const d = s.domain?.();
|
|
53
|
+
const r = s.range?.();
|
|
54
|
+
if (d === undefined || r === undefined || d.length < 2 || r.length < 2) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const lo = +d[0];
|
|
58
|
+
const hi = +d[d.length - 1];
|
|
59
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi) || lo === hi)
|
|
60
|
+
return null;
|
|
61
|
+
const pLo = scale(lo);
|
|
62
|
+
const pHi = scale(hi);
|
|
63
|
+
if (!Number.isFinite(pLo) || !Number.isFinite(pHi))
|
|
64
|
+
return null;
|
|
65
|
+
const k = (pHi - pLo) / (hi - lo);
|
|
66
|
+
const b = pLo - k * lo;
|
|
67
|
+
const span = hi - lo;
|
|
68
|
+
for (const t of PROBE_FRACTIONS) {
|
|
69
|
+
const v = lo + t * span;
|
|
70
|
+
const p = scale(v);
|
|
71
|
+
if (!Number.isFinite(p) || Math.abs(p - (k * v + b)) > PROBE_EPSILON) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { k, b };
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=affine.js.map
|
package/dist/area.d.ts
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import { type CurveFactory } from 'd3-shape';
|
|
2
2
|
import type { ChartSeries } from './data.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type Scale } from './line.js';
|
|
4
4
|
import type { AreaStyle } from './theme.js';
|
|
5
|
+
import type { LayerDrawStats } from './context.js';
|
|
5
6
|
import { type GapMode } from './gaps.js';
|
|
6
7
|
import { type DecimateOption } from './decimate.js';
|
|
8
|
+
import { type Affine } from './affine.js';
|
|
9
|
+
export declare function columnFiniteExtent(y: Float64Array, length: number): readonly [number, number] | null;
|
|
10
|
+
/**
|
|
11
|
+
* Fill the area between an affine-mapped value polyline and a constant baseline
|
|
12
|
+
* pixel — the [PND-AFFINE] fast path for {@link drawArea}'s fill, the counterpart
|
|
13
|
+
* to {@link strokeAffinePolyline} for its outline. Emits one **independent closed
|
|
14
|
+
* polygon per finite run** (matching `d3.area`'s `.defined(Number.isFinite)`
|
|
15
|
+
* segmentation for a linear curve + constant `y0`): per run `[a, b)`,
|
|
16
|
+
* `moveTo(top_a)` → `lineTo(top…)` along the value edge → `lineTo(x_{b-1}, base)`
|
|
17
|
+
* → `lineTo(x_a, base)` → `closePath`. That is the same filled region `d3.area`
|
|
18
|
+
* draws — its flat backward baseline edge only adds collinear interior vertices,
|
|
19
|
+
* which don't change the fill — without the per-point `scale()` / d3-shape
|
|
20
|
+
* closures. A signed value edge crossing the baseline stays one polygon (no NaN),
|
|
21
|
+
* filled correctly on both sides. The caller brackets `beginPath`/`fill`;
|
|
22
|
+
* `xs`/`ys` are aligned index-for-index.
|
|
23
|
+
*/
|
|
24
|
+
export declare function fillAffineArea(ctx: CanvasRenderingContext2D, xs: Float64Array, ys: Float64Array, baselinePx: number, ax: Affine, ay: Affine): void;
|
|
7
25
|
/**
|
|
8
26
|
* The `[min, max]` vertical extent an area occupies — the finite values of
|
|
9
27
|
* `cs.y` widened to include `baseline`, since the fill spans from each value to
|
|
@@ -51,5 +69,5 @@ export declare function areaExtent(cs: ChartSeries, baseline: number | undefined
|
|
|
51
69
|
* bracketed by `save`/`restore` so they don't leak into later layers. Gap edges
|
|
52
70
|
* are collected by one O(N) walk ({@link collectGapEdges}).
|
|
53
71
|
*/
|
|
54
|
-
export declare function drawArea(ctx: CanvasRenderingContext2D, cs: ChartSeries, xScale: Scale, yScale: Scale, style: AreaStyle, baselineValue: number, curve?: CurveFactory, gaps?: GapMode, gapConnectorOpacity?: number, decimate?: DecimateOption):
|
|
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;
|
|
55
73
|
//# sourceMappingURL=area.d.ts.map
|