@pond-ts/charts 0.53.1 → 0.55.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 +1175 -4
- package/dist/AreaChart.d.ts +30 -15
- package/dist/AreaChart.js +46 -5
- package/dist/BandChart.d.ts +29 -17
- package/dist/BarChart.d.ts +109 -57
- package/dist/BarChart.js +82 -12
- package/dist/BarList.d.ts +127 -0
- package/dist/BarList.js +84 -0
- package/dist/BoxList.d.ts +108 -0
- package/dist/BoxList.js +125 -0
- package/dist/BoxPlot.d.ts +63 -30
- package/dist/Candlestick.d.ts +5 -4
- package/dist/Layers.js +8 -1
- package/dist/LineChart.d.ts +30 -16
- package/dist/LineChart.js +46 -5
- package/dist/ListTable.d.ts +51 -0
- package/dist/ListTable.js +143 -0
- package/dist/ScatterChart.d.ts +27 -17
- package/dist/YAxis.js +15 -4
- package/dist/affine.d.ts +35 -14
- package/dist/affine.js +34 -17
- package/dist/area.js +4 -4
- package/dist/bars.d.ts +107 -25
- package/dist/bars.js +204 -39
- package/dist/column-names.d.ts +74 -0
- package/dist/column-names.js +2 -0
- package/dist/context.d.ts +50 -7
- package/dist/data.d.ts +77 -0
- package/dist/data.js +131 -22
- package/dist/decimate.js +7 -7
- package/dist/index.d.ts +19 -13
- package/dist/index.js +21 -13
- package/dist/line.js +2 -2
- package/dist/list-source.d.ts +61 -0
- package/dist/list-source.js +5 -0
- package/dist/list.d.ts +205 -0
- package/dist/list.js +165 -0
- package/dist/theme.d.ts +30 -0
- package/dist/theme.js +24 -0
- package/package.json +3 -3
package/dist/LineChart.d.ts
CHANGED
|
@@ -1,23 +1,10 @@
|
|
|
1
1
|
import { ValueSeries } from 'pond-ts';
|
|
2
2
|
import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
|
|
3
|
+
import type { NumericColumn, ValueNumericColumn } from './column-names.js';
|
|
3
4
|
import type { DecimateOption } from './decimate.js';
|
|
4
5
|
import { type Curve } from './curve.js';
|
|
5
6
|
import { type GapMode } from './gaps.js';
|
|
6
|
-
export interface
|
|
7
|
-
/**
|
|
8
|
-
* The source series. A `TimeSeries` plots against the time axis; a
|
|
9
|
-
* `ValueSeries` (`series.byValue('cumDist')`) against its value axis — the
|
|
10
|
-
* container infers which from the data, no axis-type prop. Either way the key
|
|
11
|
-
* / axis column supplies x and `column` supplies y.
|
|
12
|
-
*
|
|
13
|
-
* **Live charts:** `series.byValue(…)` mints a *fresh* projection each call, so
|
|
14
|
-
* passing `series={s.byValue('dist')}` inline re-registers this layer every
|
|
15
|
-
* render — on a frequently re-rendering (e.g. scrub-driven) chart, memoize the
|
|
16
|
-
* projection (`useMemo`) so the layer isn't rebuilt each frame.
|
|
17
|
-
*/
|
|
18
|
-
series: TimeSeries<S> | ValueSeries<VS>;
|
|
19
|
-
/** Name of the numeric value column to plot. */
|
|
20
|
-
column: string;
|
|
7
|
+
export interface LineChartCommon<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
|
|
21
8
|
/**
|
|
22
9
|
* The series' semantic identifier — what the data _is_ / how it should read
|
|
23
10
|
* (e.g. `heartrate`, `power`, or a role name like `foam`). The theme maps it
|
|
@@ -88,11 +75,38 @@ export interface LineChartProps<S extends SeriesSchema = SeriesSchema, VS extend
|
|
|
88
75
|
*/
|
|
89
76
|
index?: number;
|
|
90
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* LineChart's source + column props, a **union over the series kind** so the
|
|
80
|
+
* column names are checked against the schema that was actually passed
|
|
81
|
+
* ([PND-CHARTAPI]). A single member carrying `NumericColumn<S> |
|
|
82
|
+
* ValueNumericColumn<VS>` would silently widen to `string`: only one of the
|
|
83
|
+
* two generics is ever inferred, and the other falls back (measured in
|
|
84
|
+
* `spikes/charts-type-seam/`). Loosely-typed series still accept any name.
|
|
85
|
+
*/
|
|
86
|
+
type LineChartSource<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = {
|
|
87
|
+
/**
|
|
88
|
+
* The source series. **Live charts:** `series.byValue(…)` mints a
|
|
89
|
+
* *fresh* projection each call, so an inline `series={s.byValue('d')}`
|
|
90
|
+
* re-registers this layer every render — on a frequently re-rendering
|
|
91
|
+
* (e.g. scrub-driven) chart, memoize the projection (`useMemo`) so the
|
|
92
|
+
* layer isn't rebuilt each frame.
|
|
93
|
+
*/
|
|
94
|
+
series: TimeSeries<S>;
|
|
95
|
+
column: NumericColumn<S>;
|
|
96
|
+
readout?: NumericColumn<S>;
|
|
97
|
+
} | {
|
|
98
|
+
series: ValueSeries<VS>;
|
|
99
|
+
column: ValueNumericColumn<VS>;
|
|
100
|
+
readout?: ValueNumericColumn<VS>;
|
|
101
|
+
};
|
|
102
|
+
/** `<LineChart>`'s props: the shared knobs plus one series-kind source shape. */
|
|
103
|
+
export type LineChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = LineChartCommon<S, VS> & LineChartSource<S, VS>;
|
|
91
104
|
/**
|
|
92
105
|
* A line draw layer. Reads `column` from `series` into a {@link ChartSeries}
|
|
93
106
|
* (columnar, gaps as NaN), registers itself into the enclosing {@link Layers}
|
|
94
107
|
* (scaling against its `axis`), and renders nothing to the DOM — the row draws
|
|
95
108
|
* it. The line breaks at gaps rather than spanning them.
|
|
96
109
|
*/
|
|
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;
|
|
110
|
+
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;
|
|
111
|
+
export {};
|
|
98
112
|
//# 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,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal The shared row-table shell behind {@link BarList} / {@link BoxList}.
|
|
3
|
+
*
|
|
4
|
+
* Renders a real `<table>` — the point of the list family is table semantics
|
|
5
|
+
* (label cells that can be links, aligned data cells, a `colSpan` detail row
|
|
6
|
+
* for the expander, screen-reader-legible rows), which a canvas plot can't
|
|
7
|
+
* carry and which hand-rolled flex rows re-implement badly (per-row cell
|
|
8
|
+
* alignment is exactly what table layout solves). The glyph cell takes
|
|
9
|
+
* `width: 100%` so it absorbs the free width; every text cell shrinks to fit.
|
|
10
|
+
*
|
|
11
|
+
* Not exported from the package: the public surface is the two sisters, so the
|
|
12
|
+
* shared shell can evolve without a compatibility contract.
|
|
13
|
+
*/
|
|
14
|
+
import { type ReactNode } from 'react';
|
|
15
|
+
import type { ListCellSpec, ListRow } from './list.js';
|
|
16
|
+
import type { ChartTheme } from './theme.js';
|
|
17
|
+
export interface ListTableProps<R extends ListRow> {
|
|
18
|
+
/** Rows in display order (the caller sorts). */
|
|
19
|
+
readonly rows: readonly R[];
|
|
20
|
+
/** Which sister is rendering — stamped as `data-list` for styling/tests. */
|
|
21
|
+
readonly kind: 'bar' | 'box';
|
|
22
|
+
/** The glyph cell's content for one row (the bar / box lines). */
|
|
23
|
+
readonly renderGlyphs: (row: R) => ReactNode;
|
|
24
|
+
readonly before?: readonly ListCellSpec<R>[] | undefined;
|
|
25
|
+
readonly after?: readonly ListCellSpec<R>[] | undefined;
|
|
26
|
+
readonly renderExpanded?: ((row: R) => ReactNode) | undefined;
|
|
27
|
+
readonly defaultExpanded?: readonly string[] | undefined;
|
|
28
|
+
readonly onExpandToggle?: ((key: string, expanded: boolean) => void) | undefined;
|
|
29
|
+
readonly selected?: string | null | undefined;
|
|
30
|
+
readonly onRowClick?: ((row: R) => void) | undefined;
|
|
31
|
+
readonly divided?: boolean | undefined;
|
|
32
|
+
/** Draw the vertical **baseline rule** at the scale origin (the glyph
|
|
33
|
+
* cell's left edge) — the shared reference the eye aligns rows against. */
|
|
34
|
+
readonly baseline?: boolean | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Reference markers, **pre-resolved to track fractions** by the caller (the
|
|
37
|
+
* shell knows pixels, not the scale): each draws a dotted vertical rule
|
|
38
|
+
* through every row's glyph area, plus a label strip above the list when
|
|
39
|
+
* any carries a `label`. A `null` fraction (out-of-scale gap) is skipped.
|
|
40
|
+
*/
|
|
41
|
+
readonly markers?: ReadonlyArray<{
|
|
42
|
+
readonly frac: number | null;
|
|
43
|
+
readonly label?: string;
|
|
44
|
+
}> | undefined;
|
|
45
|
+
readonly theme: ChartTheme;
|
|
46
|
+
}
|
|
47
|
+
/** The shared text ink: the band-label tone when the theme has one (stronger
|
|
48
|
+
* than tick labels — these cells are primary content), else the tick ink. */
|
|
49
|
+
export declare function listInk(theme: ChartTheme): string;
|
|
50
|
+
export declare function ListTable<R extends ListRow>({ rows, kind, renderGlyphs, before, after, renderExpanded, defaultExpanded, onExpandToggle, selected, onRowClick, divided, baseline, markers, theme, }: ListTableProps<R>): import("react/jsx-runtime").JSX.Element;
|
|
51
|
+
//# sourceMappingURL=ListTable.d.ts.map
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @internal The shared row-table shell behind {@link BarList} / {@link BoxList}.
|
|
4
|
+
*
|
|
5
|
+
* Renders a real `<table>` — the point of the list family is table semantics
|
|
6
|
+
* (label cells that can be links, aligned data cells, a `colSpan` detail row
|
|
7
|
+
* for the expander, screen-reader-legible rows), which a canvas plot can't
|
|
8
|
+
* carry and which hand-rolled flex rows re-implement badly (per-row cell
|
|
9
|
+
* alignment is exactly what table layout solves). The glyph cell takes
|
|
10
|
+
* `width: 100%` so it absorbs the free width; every text cell shrinks to fit.
|
|
11
|
+
*
|
|
12
|
+
* Not exported from the package: the public surface is the two sisters, so the
|
|
13
|
+
* shared shell can evolve without a compatibility contract.
|
|
14
|
+
*/
|
|
15
|
+
import { Fragment, useState } from 'react';
|
|
16
|
+
/** The turquoise the selected-row edge falls back to when the theme has no
|
|
17
|
+
* annotation register — the same built-in the annotation layer uses. */
|
|
18
|
+
const FALLBACK_ACCENT = '#0d9488';
|
|
19
|
+
/** The shared text ink: the band-label tone when the theme has one (stronger
|
|
20
|
+
* than tick labels — these cells are primary content), else the tick ink. */
|
|
21
|
+
export function listInk(theme) {
|
|
22
|
+
return theme.axis.band?.label ?? theme.axis.label;
|
|
23
|
+
}
|
|
24
|
+
export function ListTable({ rows, kind, renderGlyphs, before = [], after = [], renderExpanded, defaultExpanded, onExpandToggle, selected, onRowClick, divided = true, baseline = false, markers = [], theme, }) {
|
|
25
|
+
// Uncontrolled expansion, keyed on row identity so it survives a re-sort.
|
|
26
|
+
const [expanded, setExpanded] = useState(() => new Set(defaultExpanded ?? []));
|
|
27
|
+
const [hovered, setHovered] = useState(null);
|
|
28
|
+
const interactive = onRowClick !== undefined;
|
|
29
|
+
const toggle = (key) => {
|
|
30
|
+
const open = !expanded.has(key);
|
|
31
|
+
setExpanded((prev) => {
|
|
32
|
+
const next = new Set(prev);
|
|
33
|
+
if (open)
|
|
34
|
+
next.add(key);
|
|
35
|
+
else
|
|
36
|
+
next.delete(key);
|
|
37
|
+
return next;
|
|
38
|
+
});
|
|
39
|
+
onExpandToggle?.(key, open);
|
|
40
|
+
};
|
|
41
|
+
const ink = listInk(theme);
|
|
42
|
+
const accent = theme.annotation?.color ?? FALLBACK_ACCENT;
|
|
43
|
+
const divider = divided ? `1px solid ${theme.axis.grid}` : undefined;
|
|
44
|
+
// Label + before + glyph + after (+ expander) — the detail row spans them all.
|
|
45
|
+
const span = 2 + before.length + after.length + (renderExpanded ? 1 : 0);
|
|
46
|
+
const textCell = (align) => ({
|
|
47
|
+
padding: '6px 12px',
|
|
48
|
+
whiteSpace: 'nowrap',
|
|
49
|
+
textAlign: align ?? 'left',
|
|
50
|
+
verticalAlign: 'middle',
|
|
51
|
+
});
|
|
52
|
+
// The glyph cell's shared horizontal geometry — the label strip must use
|
|
53
|
+
// the SAME left/right padding (and baseline border) as the data rows, or
|
|
54
|
+
// its percentages would resolve against a different content width and the
|
|
55
|
+
// labels would sit off their rules.
|
|
56
|
+
const glyphCellStyle = (vertical) => ({
|
|
57
|
+
width: '100%',
|
|
58
|
+
padding: baseline ? `${vertical} 8px ${vertical} 5px` : `${vertical} 8px`,
|
|
59
|
+
verticalAlign: 'middle',
|
|
60
|
+
borderLeft: baseline ? `1px solid ${theme.axis.grid}` : undefined,
|
|
61
|
+
});
|
|
62
|
+
const drawnMarkers = markers.filter((m) => m.frac !== null);
|
|
63
|
+
return (_jsx("table", { "data-list": kind, style: {
|
|
64
|
+
width: '100%',
|
|
65
|
+
borderCollapse: 'collapse',
|
|
66
|
+
font: `${theme.font.size}px/${1.5} ${theme.font.family}`,
|
|
67
|
+
color: ink,
|
|
68
|
+
background: theme.background,
|
|
69
|
+
}, children: _jsxs("tbody", { children: [drawnMarkers.some((m) => m.label !== undefined) && (
|
|
70
|
+
// The marker label strip: one synthetic row above the data, its
|
|
71
|
+
// glyph cell sharing the data rows' horizontal geometry so each
|
|
72
|
+
// label centres exactly on its rule below.
|
|
73
|
+
_jsxs("tr", { "data-list-marker-labels": "", children: [_jsx("td", { style: textCell() }), before.map((cell) => (_jsx("td", { style: textCell(cell.align) }, cell.key))), _jsx("td", { style: glyphCellStyle('0px'), children: _jsx("div", { style: {
|
|
74
|
+
position: 'relative',
|
|
75
|
+
height: theme.font.size + 6,
|
|
76
|
+
}, children: drawnMarkers.map((m, mi) => m.label !== undefined && (_jsx("span", { "data-list-marker-label": "", style: {
|
|
77
|
+
position: 'absolute',
|
|
78
|
+
left: `${m.frac * 100}%`,
|
|
79
|
+
bottom: 0,
|
|
80
|
+
transform: 'translateX(-50%)',
|
|
81
|
+
whiteSpace: 'nowrap',
|
|
82
|
+
color: accent,
|
|
83
|
+
}, children: m.label }, mi))) }) }), after.map((cell) => (_jsx("td", { style: textCell(cell.align) }, cell.key))), renderExpanded !== undefined && _jsx("td", {})] })), rows.map((row, i) => {
|
|
84
|
+
const isSelected = selected != null && selected === row.key;
|
|
85
|
+
const isOpen = renderExpanded !== undefined && expanded.has(row.key);
|
|
86
|
+
return (_jsxs(Fragment, { children: [_jsxs("tr", { "data-list-row": row.key, ...(isSelected ? { 'data-selected': '' } : {}), onClick: onRowClick === undefined ? undefined : () => onRowClick(row),
|
|
87
|
+
// A clickable row is keyboard-reachable too: focusable, and
|
|
88
|
+
// Enter / Space activate it (Space's default scroll is eaten).
|
|
89
|
+
tabIndex: interactive ? 0 : undefined, onKeyDown: onRowClick === undefined
|
|
90
|
+
? undefined
|
|
91
|
+
: (e) => {
|
|
92
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
onRowClick(row);
|
|
95
|
+
}
|
|
96
|
+
}, onPointerEnter: interactive ? () => setHovered(row.key) : undefined, onPointerLeave: interactive ? () => setHovered(null) : undefined, style: {
|
|
97
|
+
borderTop: i > 0 ? divider : undefined,
|
|
98
|
+
cursor: interactive ? 'pointer' : undefined,
|
|
99
|
+
background: interactive && hovered === row.key
|
|
100
|
+
? (theme.legend?.border ?? theme.axis.grid)
|
|
101
|
+
: undefined,
|
|
102
|
+
// The selection accent: an inset edge in the annotation
|
|
103
|
+
// register (a *user's* mark, so it takes the marks colour,
|
|
104
|
+
// not a data hue) — reads on any ground, moves no layout.
|
|
105
|
+
boxShadow: isSelected ? `inset 3px 0 0 ${accent}` : undefined,
|
|
106
|
+
}, children: [_jsx("td", { "data-list-cell": "label", style: textCell(), children: row.label ?? row.key }), before.map((cell) => (_jsx("td", { "data-list-cell": cell.key, style: textCell(cell.align), children: cell.render(row) }, cell.key))), _jsx("td", { "data-list-cell": "glyphs",
|
|
107
|
+
// 100% absorbs the table's free width; every text cell
|
|
108
|
+
// shrinks to its content, staying aligned down the list.
|
|
109
|
+
// The baseline rule marks the scale origin: left padding
|
|
110
|
+
// narrows to a 5px breath so the glyphs start just off the
|
|
111
|
+
// rule, and border-collapse joins the rows' rules into one
|
|
112
|
+
// continuous vertical — the same thin `axis.grid` ink as
|
|
113
|
+
// the row dividers, so the two read as one quiet grid.
|
|
114
|
+
style: glyphCellStyle('6px'), children: _jsxs("div", { style: { position: 'relative' }, children: [renderGlyphs(row), drawnMarkers.map((m, mi) => (
|
|
115
|
+
// One dotted segment per row, bleeding through the
|
|
116
|
+
// row's vertical padding (+ divider) so adjacent rows'
|
|
117
|
+
// segments join into one continuous rule. Annotation
|
|
118
|
+
// register — a reference is a user's mark, not data.
|
|
119
|
+
_jsx("div", { "data-list-marker": "", style: {
|
|
120
|
+
position: 'absolute',
|
|
121
|
+
top: -7,
|
|
122
|
+
bottom: -7,
|
|
123
|
+
left: `calc(${m.frac * 100}% - 0.5px)`,
|
|
124
|
+
width: 0,
|
|
125
|
+
borderLeft: `1px dotted ${accent}`,
|
|
126
|
+
pointerEvents: 'none',
|
|
127
|
+
} }, mi)))] }) }), after.map((cell) => (_jsx("td", { "data-list-cell": cell.key, style: textCell(cell.align), children: cell.render(row) }, cell.key))), renderExpanded !== undefined && (_jsx("td", { style: { padding: '0 4px', verticalAlign: 'middle' }, children: _jsx("button", { type: "button", "data-list-expander": "", "aria-expanded": isOpen, "aria-label": isOpen ? 'Collapse row' : 'Expand row', onClick: (e) => {
|
|
128
|
+
// The chevron toggles; it must not double as a row click.
|
|
129
|
+
e.stopPropagation();
|
|
130
|
+
toggle(row.key);
|
|
131
|
+
}, style: {
|
|
132
|
+
background: 'none',
|
|
133
|
+
border: 'none',
|
|
134
|
+
cursor: 'pointer',
|
|
135
|
+
color: theme.axis.label,
|
|
136
|
+
font: 'inherit',
|
|
137
|
+
padding: '2px 6px',
|
|
138
|
+
transform: isOpen ? 'rotate(90deg)' : undefined,
|
|
139
|
+
transition: 'transform 120ms',
|
|
140
|
+
}, children: "\u25B8" }) }))] }), isOpen && (_jsx("tr", { "data-list-detail": row.key, children: _jsx("td", { colSpan: span, style: { padding: '2px 12px 12px' }, children: renderExpanded(row) }) }))] }, row.key));
|
|
141
|
+
})] }) }));
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=ListTable.js.map
|
package/dist/ScatterChart.d.ts
CHANGED
|
@@ -1,24 +1,9 @@
|
|
|
1
1
|
import { ValueSeries } from 'pond-ts';
|
|
2
2
|
import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
|
|
3
|
+
import type { NumericColumn, ValueNumericColumn } from './column-names.js';
|
|
3
4
|
import { type ColorEncoding, type RadiusEncoding } from './encoding.js';
|
|
4
5
|
import type { DecimateOption } from './decimate.js';
|
|
5
|
-
export interface
|
|
6
|
-
/**
|
|
7
|
-
* The source series. A `TimeSeries` scatters against the time axis; a
|
|
8
|
-
* `ValueSeries` (`series.byValue('cumDist')`, or `ValueSeries.fromColumns`
|
|
9
|
-
* for natively value-keyed data — IV marks keyed by strike) against its
|
|
10
|
-
* value axis — the container infers which from the data, no axis-type prop
|
|
11
|
-
* (mirrors `<LineChart>`). Either way the key / axis column supplies each
|
|
12
|
-
* point's x and `column` supplies y.
|
|
13
|
-
*
|
|
14
|
-
* **Live charts:** `series.byValue(…)` mints a *fresh* projection each call,
|
|
15
|
-
* so passing `series={s.byValue('dist')}` inline re-registers this layer
|
|
16
|
-
* every render — memoize the projection (`useMemo`) on a frequently
|
|
17
|
-
* re-rendering chart.
|
|
18
|
-
*/
|
|
19
|
-
series: TimeSeries<S> | ValueSeries<VS>;
|
|
20
|
-
/** Name of the numeric value column — each point's y. */
|
|
21
|
-
column: string;
|
|
6
|
+
export interface ScatterChartCommon<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
|
|
22
7
|
/**
|
|
23
8
|
* The scatter's semantic identifier — what the marks _are_ / how they should
|
|
24
9
|
* read. The theme maps it to a {@link ScatterStyle} (`theme.scatter[as] ??
|
|
@@ -109,6 +94,30 @@ export interface ScatterChartProps<S extends SeriesSchema = SeriesSchema, VS ext
|
|
|
109
94
|
*/
|
|
110
95
|
index?: number;
|
|
111
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* ScatterChart's source + column props, a **union over the series kind** so the
|
|
99
|
+
* column names are checked against the schema that was actually passed
|
|
100
|
+
* ([PND-CHARTAPI]). A single member carrying `NumericColumn<S> |
|
|
101
|
+
* ValueNumericColumn<VS>` would silently widen to `string`: only one of the
|
|
102
|
+
* two generics is ever inferred, and the other falls back (measured in
|
|
103
|
+
* `spikes/charts-type-seam/`). Loosely-typed series still accept any name.
|
|
104
|
+
*/
|
|
105
|
+
type ScatterChartSource<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = {
|
|
106
|
+
/**
|
|
107
|
+
* The source series. **Live charts:** `series.byValue(…)` mints a
|
|
108
|
+
* *fresh* projection each call, so an inline `series={s.byValue('d')}`
|
|
109
|
+
* re-registers this layer every render — on a frequently re-rendering
|
|
110
|
+
* (e.g. scrub-driven) chart, memoize the projection (`useMemo`) so the
|
|
111
|
+
* layer isn't rebuilt each frame.
|
|
112
|
+
*/
|
|
113
|
+
series: TimeSeries<S>;
|
|
114
|
+
column: NumericColumn<S>;
|
|
115
|
+
} | {
|
|
116
|
+
series: ValueSeries<VS>;
|
|
117
|
+
column: ValueNumericColumn<VS>;
|
|
118
|
+
};
|
|
119
|
+
/** `<ScatterChart>`'s props: the shared knobs plus one series-kind source shape. */
|
|
120
|
+
export type ScatterChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = ScatterChartCommon<S, VS> & ScatterChartSource<S, VS>;
|
|
112
121
|
/**
|
|
113
122
|
* A scatter draw layer: one mark per finite point at `(x, column-value)`
|
|
114
123
|
* — x from the series' key / axis column (time or value axis) —
|
|
@@ -138,4 +147,5 @@ export interface ScatterChartProps<S extends SeriesSchema = SeriesSchema, VS ext
|
|
|
138
147
|
* ```
|
|
139
148
|
*/
|
|
140
149
|
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;
|
|
150
|
+
export {};
|
|
141
151
|
//# sourceMappingURL=ScatterChart.d.ts.map
|
package/dist/YAxis.js
CHANGED
|
@@ -72,14 +72,25 @@ export function YAxis({ id, side = 'left', label, min, max, format, ticks, tickC
|
|
|
72
72
|
// a cursor value read identically. `count` calibrates the default formatter's
|
|
73
73
|
// precision to the tick density, exactly as the axis is.
|
|
74
74
|
const fmt = yScale ? resolveAxisFormat(yScale, count, format) : String;
|
|
75
|
+
// A **horizontal categorical** layer on this axis supplies its category
|
|
76
|
+
// names ([PND-HCAT]); with no explicit `ticks`, label one per unit slot at
|
|
77
|
+
// its centre (`i + 0.5`) instead of the scale's numeric ticks — a slot index
|
|
78
|
+
// is not a number anyone wants to read. Explicit `ticks` still win, and a
|
|
79
|
+
// non-categorical row is unaffected (no layer answers, so this is `null`).
|
|
80
|
+
const layerCategories = row.layers
|
|
81
|
+
.filter((e) => (e.axisId ?? row.defaultAxisId) === id)
|
|
82
|
+
.map((e) => e.layer.binCategories?.() ?? null)
|
|
83
|
+
.find((c) => c !== null) ?? null;
|
|
75
84
|
// Explicit `{ at, label }` ticks render verbatim (each label at its `at`),
|
|
76
85
|
// overriding the auto-picked d3 ticks; otherwise label the scale's ticks via `fmt`.
|
|
77
86
|
const tickList = ticks
|
|
78
87
|
? ticks.map((t) => ({ value: t.at, label: t.label }))
|
|
79
|
-
:
|
|
80
|
-
value:
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
: layerCategories !== null
|
|
89
|
+
? layerCategories.map((label, i) => ({ value: i + 0.5, label }))
|
|
90
|
+
: (yScale ? yScale.ticks(count) : []).map((t) => ({
|
|
91
|
+
value: t,
|
|
92
|
+
label: fmt(t),
|
|
93
|
+
}));
|
|
83
94
|
// The row reserves a slot per axis column (the widest in that column across
|
|
84
95
|
// rows). Size the box to the slot and align this axis's own (narrower)
|
|
85
96
|
// content toward the plot — left axes flush right, right axes flush left — so
|
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
|
}
|