@pond-ts/charts 0.54.0 → 0.56.2
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 +451 -1
- package/dist/AreaChart.d.ts +47 -22
- package/dist/AreaChart.js +24 -1
- package/dist/BandChart.d.ts +29 -17
- package/dist/BarChart.d.ts +92 -49
- package/dist/BarChart.js +65 -8
- 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/ChartRow.js +57 -6
- package/dist/Layers.js +22 -2
- package/dist/LineChart.d.ts +29 -26
- package/dist/ListTable.d.ts +51 -0
- package/dist/ListTable.js +143 -0
- package/dist/ScatterChart.d.ts +27 -17
- package/dist/YAxis.d.ts +29 -1
- package/dist/YAxis.js +19 -5
- package/dist/area.js +46 -15
- package/dist/band.js +13 -0
- package/dist/bars.d.ts +89 -18
- package/dist/bars.js +141 -30
- package/dist/column-names.d.ts +74 -0
- package/dist/column-names.js +2 -0
- package/dist/context.d.ts +40 -2
- package/dist/data.d.ts +19 -0
- package/dist/data.js +46 -0
- package/dist/dev.d.ts +2 -0
- package/dist/dev.js +2 -0
- package/dist/domain.d.ts +54 -1
- package/dist/domain.js +195 -2
- package/dist/format.d.ts +20 -0
- package/dist/format.js +23 -10
- package/dist/gaps.d.ts +33 -0
- package/dist/gaps.js +49 -0
- package/dist/index.d.ts +19 -13
- package/dist/index.js +21 -13
- package/dist/line.js +10 -1
- 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 +42 -0
- package/dist/theme.js +24 -0
- package/dist/viewport.d.ts +9 -1
- package/dist/viewport.js +40 -4
- package/dist/yticks.d.ts +44 -0
- package/dist/yticks.js +55 -0
- package/package.json +3 -3
package/dist/BoxList.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { ValueSeries } from 'pond-ts';
|
|
4
|
+
import { listFraction, listRowsFromTimeSeries, listRowsFromValueSeries, resolveListDomain, sortListRows, validateBoxListColumn, } from './list.js';
|
|
5
|
+
import { isSeriesSource, } from './list-source.js';
|
|
6
|
+
import { ListTable, listInk } from './ListTable.js';
|
|
7
|
+
import { defaultTheme } from './theme.js';
|
|
8
|
+
/**
|
|
9
|
+
* A **distribution row list** — {@link BarList}'s sister, drawing a horizontal
|
|
10
|
+
* five-number box per configured column instead of a value bar: the light
|
|
11
|
+
* `lower→upper` range band, the stronger `q1→q3` body, the `median` line, and
|
|
12
|
+
* an optional **current-value tick** with a printed label (the esnet
|
|
13
|
+
* traffic-by-interface look: where traffic *ranges* vs where it *is now*).
|
|
14
|
+
*
|
|
15
|
+
* Same table contract as {@link BarList} (standalone, cells, sort, expander,
|
|
16
|
+
* one shared scale, gap-aware), same quantile vocabulary as the canvas
|
|
17
|
+
* `<BoxPlot>` (`lower`/`q1`/`median`/`q3`/`upper`, both-or-neither body,
|
|
18
|
+
* quantiles **pre-computed upstream** — `series.reduce` facts; the chart never
|
|
19
|
+
* computes them).
|
|
20
|
+
*
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <BoxList
|
|
23
|
+
* rows={ifaceRows}
|
|
24
|
+
* columns={[
|
|
25
|
+
* { lower: 'in_p5', q1: 'in_p25', median: 'in_p50', q3: 'in_p75',
|
|
26
|
+
* upper: 'in_p95', value: 'in_now', format: fmtBps },
|
|
27
|
+
* { lower: 'out_p5', q1: 'out_p25', median: 'out_p50', q3: 'out_p75',
|
|
28
|
+
* upper: 'out_p95', value: 'out_now', format: fmtBps, as: 'secondary' },
|
|
29
|
+
* ]}
|
|
30
|
+
* sortBy="in_now"
|
|
31
|
+
* />
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function BoxList(props) {
|
|
35
|
+
// One normalized view of the union — `isSeriesSource` is the runtime
|
|
36
|
+
// narrowing; the doors are mutually exclusive by construction now.
|
|
37
|
+
const source = props;
|
|
38
|
+
const { rows, series, label, columns, domain, sortBy, sortDirection = 'desc', sort, before, after, renderExpanded, defaultExpanded, onExpandToggle, selected, onRowClick, markers, barHeight = 10, divided, baseline = true, theme = defaultTheme, } = source;
|
|
39
|
+
// A runtime guard for JS consumers and `any`-typed call sites — the
|
|
40
|
+
// props union makes both branches unreachable from typed TS, but a
|
|
41
|
+
// silently-ignored source prop is a worse failure than a throw.
|
|
42
|
+
if (isSeriesSource(props) === (rows !== undefined)) {
|
|
43
|
+
throw new Error('<BoxList>: provide exactly one of `rows` (records) or `series` (one row per event)');
|
|
44
|
+
}
|
|
45
|
+
for (const col of columns)
|
|
46
|
+
validateBoxListColumn(col);
|
|
47
|
+
// The series door reads internally — starting from a pond series there is
|
|
48
|
+
// no shaping step (with `series`, R stays the default ListRow).
|
|
49
|
+
const allRows = useMemo(() => rows ??
|
|
50
|
+
(series instanceof ValueSeries
|
|
51
|
+
? listRowsFromValueSeries(series, label !== undefined ? { label } : {})
|
|
52
|
+
: listRowsFromTimeSeries(series, label !== undefined ? { label } : {})), [rows, series, label]);
|
|
53
|
+
const sorted = useMemo(() => sortListRows(allRows, sortBy, sortDirection, sort), [allRows, sortBy, sortDirection, sort]);
|
|
54
|
+
const scale = useMemo(() => resolveListDomain(allRows, columns.flatMap((c) => c.value !== undefined
|
|
55
|
+
? [c.lower, c.upper, c.value]
|
|
56
|
+
: [c.lower, c.upper]), domain, markers?.map((m) => m.value)), [allRows, columns, domain, markers]);
|
|
57
|
+
const resolvedMarkers = useMemo(() => markers?.map((m) => ({
|
|
58
|
+
frac: listFraction(m.value, scale),
|
|
59
|
+
...(m.label !== undefined ? { label: m.label } : {}),
|
|
60
|
+
})), [markers, scale]);
|
|
61
|
+
return (_jsx(ListTable, { rows: sorted, kind: "box", markers: resolvedMarkers, before: before, after: after, renderExpanded: renderExpanded, defaultExpanded: defaultExpanded, onExpandToggle: onExpandToggle, selected: selected, onRowClick: onRowClick, divided: divided, baseline: baseline, theme: theme, renderGlyphs: (row) => (_jsx(_Fragment, { children: columns.map((col, ci) => (_jsx(BoxLine
|
|
62
|
+
// Index-qualified so two lines over the same quantile names
|
|
63
|
+
// (say, styled differently) never collide.
|
|
64
|
+
, { row: row, col: col, scale: scale, height: barHeight, style: theme.box[col.as ?? 'default'] ?? theme.box.default, ink: listInk(theme), fontSize: theme.font.size }, `${ci} ${col.lower} ${col.upper}`))) })) }));
|
|
65
|
+
}
|
|
66
|
+
/** One horizontal box line: range band → body → median → current tick + label. */
|
|
67
|
+
function BoxLine({ row, col, scale, height, style, ink, fontSize, }) {
|
|
68
|
+
const at = (name) => name === undefined ? null : listFraction(row.values[name], scale);
|
|
69
|
+
const lo = at(col.lower);
|
|
70
|
+
const hi = at(col.upper);
|
|
71
|
+
const q1 = at(col.q1);
|
|
72
|
+
const q3 = at(col.q3);
|
|
73
|
+
const med = at(col.median);
|
|
74
|
+
const tick = at(col.value);
|
|
75
|
+
const raw = col.value !== undefined ? row.values[col.value] : undefined;
|
|
76
|
+
const label = col.format !== undefined && typeof raw === 'number' && Number.isFinite(raw)
|
|
77
|
+
? col.format(raw)
|
|
78
|
+
: null;
|
|
79
|
+
const pct = (f) => `${f * 100}%`;
|
|
80
|
+
// The row keeps its slot height even when everything is missing — a gap
|
|
81
|
+
// reads as an empty line, not a collapsed row.
|
|
82
|
+
return (_jsxs("div", { "data-list-boxline": "", style: { position: 'relative', height: height + 4, margin: '2px 0' }, children: [lo !== null && hi !== null && (_jsx("div", { "data-list-range": "", style: {
|
|
83
|
+
position: 'absolute',
|
|
84
|
+
top: 2,
|
|
85
|
+
bottom: 2,
|
|
86
|
+
left: pct(lo),
|
|
87
|
+
width: pct(Math.max(hi - lo, 0)),
|
|
88
|
+
background: style.whisker,
|
|
89
|
+
opacity: 0.55,
|
|
90
|
+
borderRadius: height / 2,
|
|
91
|
+
} })), q1 !== null && q3 !== null && (_jsx("div", { "data-list-body": "", style: {
|
|
92
|
+
position: 'absolute',
|
|
93
|
+
top: 2,
|
|
94
|
+
bottom: 2,
|
|
95
|
+
left: pct(q1),
|
|
96
|
+
width: pct(Math.max(q3 - q1, 0)),
|
|
97
|
+
background: style.fill,
|
|
98
|
+
opacity: Math.min(style.fillOpacity * 2, 1),
|
|
99
|
+
borderRadius: 1,
|
|
100
|
+
} })), med !== null && (_jsx("div", { "data-list-median": "", style: {
|
|
101
|
+
position: 'absolute',
|
|
102
|
+
top: 2,
|
|
103
|
+
bottom: 2,
|
|
104
|
+
left: `calc(${pct(med)} - ${style.medianWidth / 2}px)`,
|
|
105
|
+
width: style.medianWidth,
|
|
106
|
+
background: style.median,
|
|
107
|
+
} })), tick !== null && (_jsxs(_Fragment, { children: [_jsx("div", { "data-list-tick": "", style: {
|
|
108
|
+
position: 'absolute',
|
|
109
|
+
top: 0,
|
|
110
|
+
bottom: 0,
|
|
111
|
+
left: `calc(${pct(tick)} - 1.5px)`,
|
|
112
|
+
width: 3,
|
|
113
|
+
background: style.stroke,
|
|
114
|
+
borderRadius: 1,
|
|
115
|
+
} }), label !== null && (_jsx("span", { "data-list-value": "", style: {
|
|
116
|
+
position: 'absolute',
|
|
117
|
+
left: `calc(${pct(tick)} + 8px)`,
|
|
118
|
+
top: '50%',
|
|
119
|
+
transform: 'translateY(-50%)',
|
|
120
|
+
whiteSpace: 'nowrap',
|
|
121
|
+
fontSize: fontSize + 1,
|
|
122
|
+
color: ink,
|
|
123
|
+
}, children: label }))] }))] }));
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=BoxList.js.map
|
package/dist/BoxPlot.d.ts
CHANGED
|
@@ -1,37 +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 BoxShape } from './box.js';
|
|
4
5
|
import type { DecimateOption } from './decimate.js';
|
|
5
|
-
export interface
|
|
6
|
-
/**
|
|
7
|
-
* The source series. A `TimeSeries` plots against the time axis; a `ValueSeries`
|
|
8
|
-
* (`series.byValue('strike')`, or `ValueSeries.fromColumns` for natively
|
|
9
|
-
* value-keyed data — a per-strike IV distribution) against its value axis — the
|
|
10
|
-
* container infers which from the data, no axis-type prop (mirrors `<LineChart>`
|
|
11
|
-
* / `<ScatterChart>`). The box x-span is the key's `[begin, end)` for an
|
|
12
|
-
* interval-keyed `TimeSeries`, else synthesized from neighbour spacing (a
|
|
13
|
-
* point-keyed `TimeSeries`, or a `ValueSeries`) so the box keeps real width.
|
|
14
|
-
*/
|
|
15
|
-
series: TimeSeries<S> | ValueSeries<VS>;
|
|
16
|
-
/** Name of the numeric column for the lower whisker end (e.g. `p5` / `min`).
|
|
17
|
-
* **Required** — with `upper` it's the whisker reach. */
|
|
18
|
-
lower: string;
|
|
19
|
-
/**
|
|
20
|
-
* Name of the numeric column for the box bottom — first quartile (e.g. `p25`).
|
|
21
|
-
* **Optional:** omit `q1` **and** `q3` together for a **range-only** box — a
|
|
22
|
-
* whisker-only `lower→upper` segment, no body (a bid→ask IV mark). Giving just
|
|
23
|
-
* one of `q1`/`q3` throws.
|
|
24
|
-
*/
|
|
25
|
-
q1?: string;
|
|
26
|
-
/** Name of the numeric column for the median line (e.g. `p50`). **Optional** —
|
|
27
|
-
* omit for no centre line (independent of the box body). */
|
|
28
|
-
median?: string;
|
|
29
|
-
/** Name of the numeric column for the box top — third quartile (e.g. `p75`).
|
|
30
|
-
* **Optional** — omit with `q1` for a range-only box (see `q1`). */
|
|
31
|
-
q3?: string;
|
|
32
|
-
/** Name of the numeric column for the upper whisker end (e.g. `p95` / `max`).
|
|
33
|
-
* **Required** — with `lower` it's the whisker reach. */
|
|
34
|
-
upper: string;
|
|
6
|
+
export interface BoxPlotCommon<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
|
|
35
7
|
/**
|
|
36
8
|
* The box series' semantic identifier — what the spread _is_ (e.g. `latency`).
|
|
37
9
|
* The theme maps it to a {@link BoxStyle} (`theme.box[as] ?? theme.box.default`
|
|
@@ -122,6 +94,66 @@ export interface BoxPlotProps<S extends SeriesSchema = SeriesSchema, VS extends
|
|
|
122
94
|
*/
|
|
123
95
|
index?: number;
|
|
124
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* BoxPlot'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 BoxPlotSource<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
|
+
/** Lower whisker end (e.g. `p5` / `min`). **Required** — with `upper`
|
|
115
|
+
* it is the whisker reach. */
|
|
116
|
+
lower: NumericColumn<S>;
|
|
117
|
+
/**
|
|
118
|
+
* Box bottom — first quartile (e.g. `p25`). **Optional:** omit `q1`
|
|
119
|
+
* **and** `q3` together for a **range-only** box (a whisker-only
|
|
120
|
+
* `lower→upper` segment, no body — a bid→ask IV mark). Giving just one
|
|
121
|
+
* of the pair throws.
|
|
122
|
+
*/
|
|
123
|
+
q1?: NumericColumn<S>;
|
|
124
|
+
/** Median line (e.g. `p50`). **Optional** — omit for no centre line
|
|
125
|
+
* (independent of the box body). */
|
|
126
|
+
median?: NumericColumn<S>;
|
|
127
|
+
/** Box top — third quartile (e.g. `p75`). **Optional** — omit with `q1`
|
|
128
|
+
* for a range-only box. */
|
|
129
|
+
q3?: NumericColumn<S>;
|
|
130
|
+
/** Upper whisker end (e.g. `p95` / `max`). **Required** — with `lower`
|
|
131
|
+
* it is the whisker reach. */
|
|
132
|
+
upper: NumericColumn<S>;
|
|
133
|
+
} | {
|
|
134
|
+
series: ValueSeries<VS>;
|
|
135
|
+
/** Lower whisker end (e.g. `p5` / `min`). **Required** — with `upper`
|
|
136
|
+
* it is the whisker reach. */
|
|
137
|
+
lower: ValueNumericColumn<VS>;
|
|
138
|
+
/**
|
|
139
|
+
* Box bottom — first quartile (e.g. `p25`). **Optional:** omit `q1`
|
|
140
|
+
* **and** `q3` together for a **range-only** box (a whisker-only
|
|
141
|
+
* `lower→upper` segment, no body — a bid→ask IV mark). Giving just one
|
|
142
|
+
* of the pair throws.
|
|
143
|
+
*/
|
|
144
|
+
q1?: ValueNumericColumn<VS>;
|
|
145
|
+
/** Median line (e.g. `p50`). **Optional** — omit for no centre line
|
|
146
|
+
* (independent of the box body). */
|
|
147
|
+
median?: ValueNumericColumn<VS>;
|
|
148
|
+
/** Box top — third quartile (e.g. `p75`). **Optional** — omit with `q1`
|
|
149
|
+
* for a range-only box. */
|
|
150
|
+
q3?: ValueNumericColumn<VS>;
|
|
151
|
+
/** Upper whisker end (e.g. `p95` / `max`). **Required** — with `lower`
|
|
152
|
+
* it is the whisker reach. */
|
|
153
|
+
upper: ValueNumericColumn<VS>;
|
|
154
|
+
};
|
|
155
|
+
/** `<BoxPlot>`'s props: the shared knobs plus one series-kind source shape. */
|
|
156
|
+
export type BoxPlotProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = BoxPlotCommon<S, VS> & BoxPlotSource<S, VS>;
|
|
125
157
|
/**
|
|
126
158
|
* A discrete box-and-whisker draw layer — the bar-chart analog of the variance
|
|
127
159
|
* band. Reads **pre-computed quantile columns** of `series` (typically a
|
|
@@ -154,4 +186,5 @@ export interface BoxPlotProps<S extends SeriesSchema = SeriesSchema, VS extends
|
|
|
154
186
|
* ```
|
|
155
187
|
*/
|
|
156
188
|
export declare function BoxPlot<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, lower, q1, median, q3, upper, as: semantic, axis, gap, shape, showMedian, offset, capWidth, id, decimate, legend, index, }: BoxPlotProps<S, VS>): null;
|
|
189
|
+
export {};
|
|
157
190
|
//# sourceMappingURL=BoxPlot.d.ts.map
|
package/dist/Candlestick.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SeriesSchema, TimeSeries } from 'pond-ts';
|
|
2
|
+
import type { NumericColumn } from './column-names.js';
|
|
2
3
|
import type { DecimateOption } from './decimate.js';
|
|
3
4
|
import { type CandleVariant, type ColorBy } from './ohlc.js';
|
|
4
5
|
export interface CandlestickProps<S extends SeriesSchema> {
|
|
@@ -12,13 +13,13 @@ export interface CandlestickProps<S extends SeriesSchema> {
|
|
|
12
13
|
*/
|
|
13
14
|
series: TimeSeries<S>;
|
|
14
15
|
/** Opening-price column. **Omitted ⇒ `'open'`.** */
|
|
15
|
-
open?:
|
|
16
|
+
open?: NumericColumn<S>;
|
|
16
17
|
/** Session-high column. **Omitted ⇒ `'high'`.** */
|
|
17
|
-
high?:
|
|
18
|
+
high?: NumericColumn<S>;
|
|
18
19
|
/** Session-low column. **Omitted ⇒ `'low'`.** */
|
|
19
|
-
low?:
|
|
20
|
+
low?: NumericColumn<S>;
|
|
20
21
|
/** Closing-price column. **Omitted ⇒ `'close'`.** */
|
|
21
|
-
close?:
|
|
22
|
+
close?: NumericColumn<S>;
|
|
22
23
|
/**
|
|
23
24
|
* The series' semantic identifier — what the data _is_ (e.g. a ticker). The
|
|
24
25
|
* theme maps it to a {@link CandleStyle} (`theme.candle[as] ??
|
package/dist/ChartRow.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Children, cloneElement, isValidElement, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
|
|
3
|
-
import { scaleLinear } from 'd3-scale';
|
|
4
|
-
import {
|
|
2
|
+
import { Children, cloneElement, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
3
|
+
import { scaleLinear, scaleLog } from 'd3-scale';
|
|
4
|
+
import { isDev } from './dev.js';
|
|
5
|
+
import { logAxisWarning, needsExtents, resolveYDomain } from './domain.js';
|
|
5
6
|
import { resolveAxisFormat } from './format.js';
|
|
6
7
|
import { resolveYTickCount } from './yticks.js';
|
|
7
8
|
import { placeAxisSlots } from './slots.js';
|
|
@@ -39,6 +40,7 @@ function axisSpecEqual(a, b) {
|
|
|
39
40
|
return (a.id === b.id &&
|
|
40
41
|
a.side === b.side &&
|
|
41
42
|
a.width === b.width &&
|
|
43
|
+
a.scale === b.scale &&
|
|
42
44
|
// Object.is (not ===) so a degenerate NaN bound compares equal to itself and
|
|
43
45
|
// doesn't re-register every render.
|
|
44
46
|
Object.is(a.min, b.min) &&
|
|
@@ -171,6 +173,7 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
171
173
|
id: IMPLICIT_AXIS_ID,
|
|
172
174
|
side: 'left',
|
|
173
175
|
width: 0,
|
|
176
|
+
scale: 'linear',
|
|
174
177
|
min: undefined,
|
|
175
178
|
max: undefined,
|
|
176
179
|
pad: 0,
|
|
@@ -222,20 +225,68 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
222
225
|
const yScales = useMemo(() => {
|
|
223
226
|
const map = new Map();
|
|
224
227
|
for (const ax of effectiveAxes) {
|
|
225
|
-
const extents = ax
|
|
228
|
+
const extents = needsExtents(ax)
|
|
226
229
|
? layerList
|
|
227
230
|
.filter((entry) => (entry.axisId ?? defaultAxisId) === ax.id)
|
|
228
231
|
.map((entry) => entry.layer.yExtent())
|
|
229
232
|
: [];
|
|
230
|
-
const [lo, hi] = resolveYDomain(ax.min, ax.max, extents, ax.pad);
|
|
233
|
+
const [lo, hi] = resolveYDomain(ax.min, ax.max, extents, ax.pad, ax.scale);
|
|
231
234
|
// Reserve a header band at the top when any axis draws a `'top'` title,
|
|
232
235
|
// so the title clears the top tick + plot (the whole row shifts down
|
|
233
236
|
// uniformly, keeping stacked axes aligned). No top titles ⇒ range top 0,
|
|
234
237
|
// so nothing changes for existing charts.
|
|
235
|
-
|
|
238
|
+
// `scaleLog` and `scaleLinear` share the call/ticks/tickFormat/invert
|
|
239
|
+
// surface every consumer uses (see `YScale`), so choosing between them
|
|
240
|
+
// here is the whole of log support — no draw layer branches on it.
|
|
241
|
+
const base = ax.scale === 'log' ? scaleLog() : scaleLinear();
|
|
242
|
+
map.set(ax.id, base.domain([lo, hi]).range([height, topHeader]));
|
|
236
243
|
}
|
|
237
244
|
return map;
|
|
238
245
|
}, [effectiveAxes, layerList, height, defaultAxisId, topHeader]);
|
|
246
|
+
// Dev-mode diagnostics for a `scale="log"` axis (see `logAxisWarning`). Three
|
|
247
|
+
// things about *where* this sits are load-bearing, each of them a bug the
|
|
248
|
+
// first version shipped:
|
|
249
|
+
//
|
|
250
|
+
// - **An effect, not the scale memo.** Warning from inside `useMemo` is a
|
|
251
|
+
// side effect in a function React may call speculatively — and does call
|
|
252
|
+
// twice under StrictMode.
|
|
253
|
+
// - **Deduplicated by message, in a ref.** The comment on the original said
|
|
254
|
+
// "warn once per offending axis" and nothing implemented it, so a live
|
|
255
|
+
// chart re-warned on every appended sample. Keying on the message (not a
|
|
256
|
+
// bare "already warned" flag) still reports a *different* complaint if the
|
|
257
|
+
// data changes shape.
|
|
258
|
+
// - **`height` is not a dependency.** It is one for the scales, which is why
|
|
259
|
+
// the warning must not ride along: a drag-resize would otherwise emit a
|
|
260
|
+
// line per animation frame.
|
|
261
|
+
//
|
|
262
|
+
// Gated on `isDev` **and** on some axis actually being logarithmic, so a
|
|
263
|
+
// production build and every linear chart skip the extent walk entirely.
|
|
264
|
+
const warnedRef = useRef(new Map());
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
if (!isDev || !effectiveAxes.some((ax) => ax.scale === 'log'))
|
|
267
|
+
return;
|
|
268
|
+
const warned = warnedRef.current;
|
|
269
|
+
for (const ax of effectiveAxes) {
|
|
270
|
+
// A linear axis in the same row has nothing to say and must not pay the
|
|
271
|
+
// O(points) walk below just because a sibling is logarithmic.
|
|
272
|
+
if (ax.scale !== 'log')
|
|
273
|
+
continue;
|
|
274
|
+
// Always walk the extents here, even for a fully-explicit domain the
|
|
275
|
+
// scale memo skips them for: data that cannot be drawn is worth saying so
|
|
276
|
+
// about whether or not it happened to constrain the bounds — and the
|
|
277
|
+
// both-explicit axis was exactly the case the first version stayed silent
|
|
278
|
+
// about.
|
|
279
|
+
const message = logAxisWarning(ax, layerList
|
|
280
|
+
.filter((entry) => (entry.axisId ?? defaultAxisId) === ax.id)
|
|
281
|
+
.map((entry) => entry.layer.yExtent()));
|
|
282
|
+
if (message === null)
|
|
283
|
+
warned.delete(ax.id);
|
|
284
|
+
else if (warned.get(ax.id) !== message) {
|
|
285
|
+
warned.set(ax.id, message);
|
|
286
|
+
console.warn(message);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}, [effectiveAxes, layerList, defaultAxisId]);
|
|
239
290
|
// Resolved auto-tick count per axis — explicit `<YAxis tickCount>` else
|
|
240
291
|
// height-derived (see resolveYTickCount). The single source the `<YAxis>`
|
|
241
292
|
// labels, the readout formatter (below), and the `Layers` gridlines all read,
|
package/dist/Layers.js
CHANGED
|
@@ -5,6 +5,7 @@ import { drawGrid, drawDividers, dividerAlphas, thinPixels } from './grid.js';
|
|
|
5
5
|
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
|
+
import { yTickValues } from './yticks.js';
|
|
8
9
|
import { flagChipStyle, flagChipX, axisPillX, axisPillStyle } from './chip.js';
|
|
9
10
|
import { ContainerContext, CursorContext, LayersContext, RowContext, } from './context.js';
|
|
10
11
|
/** Fallback **y**-gridline tick count, used only before the row publishes its
|
|
@@ -101,6 +102,13 @@ export function Layers({ children }) {
|
|
|
101
102
|
// A category axis draws no vertical gridlines — a line through each bar
|
|
102
103
|
// centre reads as noise; the bars are the structure.
|
|
103
104
|
const xTickVals = container.xKind === 'category' ? [] : xScale.ticks(xTickCount);
|
|
105
|
+
// The same rule on the other axis: a **horizontal** categorical chart
|
|
106
|
+
// ([PND-HCAT]) puts its categories on y, where the `<YAxis>` labels slot
|
|
107
|
+
// *centres* while d3's auto ticks fall on slot *boundaries* — so drawing
|
|
108
|
+
// them would both mismatch the labels and stripe each bar. Suppressed,
|
|
109
|
+
// exactly as the categorical x axis already is.
|
|
110
|
+
const yIsCategory = layers.some((e) => (e.axisId ?? defaultAxisId) === defaultAxisId &&
|
|
111
|
+
(e.layer.binCategories?.() ?? null) !== null);
|
|
104
112
|
// The reference grid — behind the data, opt-out via `grid={false}` for
|
|
105
113
|
// a clean backdrop (session dividers below stay independent of it).
|
|
106
114
|
if (container.grid) {
|
|
@@ -108,8 +116,8 @@ export function Layers({ children }) {
|
|
|
108
116
|
// single source, height-derived or an explicit `<YAxis tickCount>`), so
|
|
109
117
|
// a gridline sits under every `<YAxis>` label and no more.
|
|
110
118
|
const yCount = tickCounts.get(defaultAxisId) ?? GRID_TICKS;
|
|
111
|
-
const yTicks = gridY
|
|
112
|
-
? (explicitY ?? gridY
|
|
119
|
+
const yTicks = gridY && !(yIsCategory && explicitY === undefined)
|
|
120
|
+
? (explicitY ?? yTickValues(gridY, yCount)).map((t) => gridY(t))
|
|
113
121
|
: [];
|
|
114
122
|
// On a calendar axis the verticals are the FULL grain populations —
|
|
115
123
|
// every day in the month, every month in the year, every aligned
|
|
@@ -260,6 +268,18 @@ export function Layers({ children }) {
|
|
|
260
268
|
// a full replot per mousemove.
|
|
261
269
|
container.timeRange,
|
|
262
270
|
container.reportDrawStats,
|
|
271
|
+
// Read inside `draw`, so they have to invalidate it. Omitting `grid` meant
|
|
272
|
+
// toggling `<ChartContainer grid>` changed nothing until some *other*
|
|
273
|
+
// dep moved — pan the plot a pixel and the gridlines you switched off
|
|
274
|
+
// finally vanished. All primitives, so no per-frame identity churn.
|
|
275
|
+
container.grid,
|
|
276
|
+
container.sessionDividers,
|
|
277
|
+
container.xKind,
|
|
278
|
+
// `container.theme` is read too. It is deliberately NOT listed: it is the
|
|
279
|
+
// caller's prop and an inline object literal would rebuild `draw` every
|
|
280
|
+
// render (a full replot per frame). The values `draw` actually takes from
|
|
281
|
+
// it — `background`, `gridColor`, `gridDash` — are extracted above and
|
|
282
|
+
// listed individually, so a theme swap still invalidates.
|
|
263
283
|
row.rowKey,
|
|
264
284
|
]);
|
|
265
285
|
// Interaction overlay: the cursor marks live on a DOM/SVG overlay above the
|
package/dist/LineChart.d.ts
CHANGED
|
@@ -1,34 +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;
|
|
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;
|
|
7
|
+
export interface LineChartCommon<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
|
|
32
8
|
/**
|
|
33
9
|
* The series' semantic identifier — what the data _is_ / how it should read
|
|
34
10
|
* (e.g. `heartrate`, `power`, or a role name like `foam`). The theme maps it
|
|
@@ -99,6 +75,32 @@ export interface LineChartProps<S extends SeriesSchema = SeriesSchema, VS extend
|
|
|
99
75
|
*/
|
|
100
76
|
index?: number;
|
|
101
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>;
|
|
102
104
|
/**
|
|
103
105
|
* A line draw layer. Reads `column` from `series` into a {@link ChartSeries}
|
|
104
106
|
* (columnar, gaps as NaN), registers itself into the enclosing {@link Layers}
|
|
@@ -106,4 +108,5 @@ export interface LineChartProps<S extends SeriesSchema = SeriesSchema, VS extend
|
|
|
106
108
|
* it. The line breaks at gaps rather than spanning them.
|
|
107
109
|
*/
|
|
108
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 {};
|
|
109
112
|
//# sourceMappingURL=LineChart.d.ts.map
|
|
@@ -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
|