@pond-ts/charts 0.48.0 → 0.49.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 +215 -1
- package/dist/AreaChart.d.ts +18 -1
- package/dist/AreaChart.js +23 -2
- package/dist/BandChart.d.ts +21 -2
- package/dist/BandChart.js +68 -9
- package/dist/BarChart.d.ts +12 -1
- package/dist/BarChart.js +34 -1
- package/dist/BoxPlot.d.ts +18 -1
- package/dist/BoxPlot.js +60 -3
- package/dist/Candlestick.d.ts +8 -1
- package/dist/Candlestick.js +40 -6
- package/dist/ChartContainer.d.ts +23 -13
- package/dist/ChartContainer.js +86 -32
- package/dist/ChartRow.js +22 -3
- package/dist/Layers.js +37 -14
- package/dist/Legend.d.ts +62 -0
- package/dist/Legend.js +169 -0
- package/dist/LineChart.d.ts +20 -1
- package/dist/LineChart.js +23 -2
- package/dist/ScatterChart.d.ts +8 -1
- package/dist/ScatterChart.js +24 -1
- package/dist/XAxis.js +9 -2
- package/dist/YAxis.d.ts +9 -1
- package/dist/YAxis.js +27 -6
- package/dist/annotations.d.ts +21 -3
- package/dist/annotations.js +36 -15
- package/dist/area.d.ts +2 -1
- package/dist/area.js +29 -4
- package/dist/band.d.ts +2 -1
- package/dist/band.js +18 -1
- package/dist/bars.js +8 -1
- package/dist/box.d.ts +14 -1
- package/dist/box.js +56 -2
- package/dist/context.d.ts +51 -4
- package/dist/culling.d.ts +165 -0
- package/dist/culling.js +286 -0
- package/dist/data.d.ts +3 -1
- package/dist/decimate.d.ts +193 -0
- package/dist/decimate.js +359 -0
- package/dist/format.d.ts +20 -11
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/line.d.ts +2 -1
- package/dist/line.js +38 -3
- package/dist/ohlc.js +6 -1
- package/dist/scatter.js +42 -7
- package/dist/swatch.d.ts +104 -0
- package/dist/swatch.js +96 -0
- package/dist/theme.d.ts +27 -0
- package/dist/theme.js +12 -0
- package/dist/useChartLegend.d.ts +106 -0
- package/dist/useChartLegend.js +122 -0
- package/dist/yticks.d.ts +20 -0
- package/dist/yticks.js +28 -0
- package/package.json +3 -3
package/dist/BoxPlot.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useContext, useEffect, useMemo } from 'react';
|
|
2
2
|
import { ValueSeries } from 'pond-ts';
|
|
3
3
|
import { boxFromTimeSeries, boxFromValueSeries } from './data.js';
|
|
4
|
-
import { boxExtent, boxIndexAtTime, drawBox, isFiniteBox, } from './box.js';
|
|
4
|
+
import { boxAt, boxExtent, boxIndexAtTime, drawBox, isFiniteBox, } from './box.js';
|
|
5
5
|
import { ContainerContext, LayersContext, } from './context.js';
|
|
6
|
+
import { legendLabelFor, useLegendItems, } from './swatch.js';
|
|
6
7
|
import { useSlotKey } from './use-slot-key.js';
|
|
7
8
|
/** Whisker collapse floor (px) — a too-thin box still draws a 1px mark. */
|
|
8
9
|
const MIN_BOX_WIDTH_PX = 1;
|
|
@@ -37,7 +38,7 @@ const MIN_BOX_WIDTH_PX = 1;
|
|
|
37
38
|
* </Layers>
|
|
38
39
|
* ```
|
|
39
40
|
*/
|
|
40
|
-
export function BoxPlot({ series, lower, q1, median, q3, upper, as: semantic, axis, gap = 0, shape = 'whisker', showMedian = true, offset = 0, capWidth, index = 0, }) {
|
|
41
|
+
export function BoxPlot({ series, lower, q1, median, q3, upper, as: semantic, axis, gap = 0, shape = 'whisker', showMedian = true, offset = 0, capWidth, id, legend, index = 0, }) {
|
|
41
42
|
const container = useContext(ContainerContext);
|
|
42
43
|
if (container === null) {
|
|
43
44
|
throw new Error('<BoxPlot> must be rendered inside a <ChartContainer>');
|
|
@@ -60,6 +61,16 @@ export function BoxPlot({ series, lower, q1, median, q3, upper, as: semantic, ax
|
|
|
60
61
|
const qLabel = useMemo(() => {
|
|
61
62
|
return (col, role) => semantic !== undefined ? `${semantic} ${role}` : (col ?? role);
|
|
62
63
|
}, [semantic]);
|
|
64
|
+
// The series identity for selection/legend: the `as` role, else the range
|
|
65
|
+
// columns as a span (matches the legend row's label).
|
|
66
|
+
const label = semantic ?? `${lower}–${upper}`;
|
|
67
|
+
// Current selection / hover narrowed to this layer's box key (its `x`), or
|
|
68
|
+
// `null` — matched by the series `id`, so a change re-registers the layer and
|
|
69
|
+
// the canvas repaints the outline. A no-`id` layer never matches.
|
|
70
|
+
const sel = container.selected;
|
|
71
|
+
const hov = container.hovered;
|
|
72
|
+
const selectedKey = id !== undefined && sel !== null && sel.id === id ? sel.key : null;
|
|
73
|
+
const hoveredKey = id !== undefined && hov !== null && hov.id === id ? hov.key : null;
|
|
63
74
|
const entry = useMemo(() => ({
|
|
64
75
|
layer: {
|
|
65
76
|
yExtent: () => boxExtent(bx),
|
|
@@ -116,7 +127,22 @@ export function BoxPlot({ series, lower, q1, median, q3, upper, as: semantic, ax
|
|
|
116
127
|
lines,
|
|
117
128
|
};
|
|
118
129
|
},
|
|
119
|
-
|
|
130
|
+
// Selection hit-test (opt-in via `id`, like Bar/Scatter): the first box
|
|
131
|
+
// whose bounding rect contains the click. A box is a discrete interval
|
|
132
|
+
// mark, so this is rect-containment (`boxAt`) — not the continuous
|
|
133
|
+
// nearest-point threshold. `key` is the box's `x`; `value` its `upper`.
|
|
134
|
+
...(id === undefined
|
|
135
|
+
? {}
|
|
136
|
+
: {
|
|
137
|
+
hitTest: (px, py, xScale, yScale) => {
|
|
138
|
+
const hit = boxAt(bx, px, py, xScale, yScale, gap, MIN_BOX_WIDTH_PX, offset);
|
|
139
|
+
if (hit === null)
|
|
140
|
+
return null;
|
|
141
|
+
const [, begin, value] = hit;
|
|
142
|
+
return { id, key: begin, value, color: style.whisker, label };
|
|
143
|
+
},
|
|
144
|
+
}),
|
|
145
|
+
draw: (ctx, xScale, yScale) => drawBox(ctx, bx, xScale, yScale, style, gap, MIN_BOX_WIDTH_PX, shape, showMedian, offset, capWidth, selectedKey, hoveredKey),
|
|
120
146
|
},
|
|
121
147
|
axisId: axis,
|
|
122
148
|
index,
|
|
@@ -136,6 +162,10 @@ export function BoxPlot({ series, lower, q1, median, q3, upper, as: semantic, ax
|
|
|
136
162
|
showMedian,
|
|
137
163
|
offset,
|
|
138
164
|
capWidth,
|
|
165
|
+
id,
|
|
166
|
+
label,
|
|
167
|
+
selectedKey,
|
|
168
|
+
hoveredKey,
|
|
139
169
|
axis,
|
|
140
170
|
index,
|
|
141
171
|
]);
|
|
@@ -146,6 +176,15 @@ export function BoxPlot({ series, lower, q1, median, q3, upper, as: semantic, ax
|
|
|
146
176
|
useEffect(() => {
|
|
147
177
|
layers.registerLayer(slot, entry);
|
|
148
178
|
}, [layers, slot, entry]);
|
|
179
|
+
// Advertise selectability (only when an `id` was given) — powers the
|
|
180
|
+
// container's "wired but nothing selectable" dev-warn, like Bar/Scatter.
|
|
181
|
+
const { registerSelectable, unregisterSelectable } = container;
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (id === undefined)
|
|
184
|
+
return;
|
|
185
|
+
registerSelectable(slot);
|
|
186
|
+
return () => unregisterSelectable(slot);
|
|
187
|
+
}, [registerSelectable, unregisterSelectable, slot, id]);
|
|
149
188
|
// Also a tracker source: the container fans in the box quantiles at the cursor
|
|
150
189
|
// for the (outside-the-chart) readout.
|
|
151
190
|
const { registerTrackerSource, unregisterTrackerSource } = container;
|
|
@@ -153,6 +192,24 @@ export function BoxPlot({ series, lower, q1, median, q3, upper, as: semantic, ax
|
|
|
153
192
|
useEffect(() => {
|
|
154
193
|
registerTrackerSource(slot, entry.layer);
|
|
155
194
|
}, [registerTrackerSource, slot, entry.layer]);
|
|
195
|
+
// And a legend row: the series identity (`as`, else the range columns as a
|
|
196
|
+
// span) + the resolved whisker style, so a `<Legend>` swatch can never drift.
|
|
197
|
+
const legendRows = useMemo(() => {
|
|
198
|
+
const name = legendLabelFor(legend, semantic ?? `${lower}–${upper}`);
|
|
199
|
+
return name === null
|
|
200
|
+
? null
|
|
201
|
+
: [
|
|
202
|
+
{
|
|
203
|
+
label: name,
|
|
204
|
+
swatch: {
|
|
205
|
+
kind: 'box',
|
|
206
|
+
whisker: style.whisker,
|
|
207
|
+
whiskerWidth: style.whiskerWidth,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
}, [legend, semantic, lower, upper, style]);
|
|
212
|
+
useLegendItems(container, slot, index, legendRows);
|
|
156
213
|
return null;
|
|
157
214
|
}
|
|
158
215
|
/** Append a tracker sample for a quantile, skipping a non-finite value. */
|
package/dist/Candlestick.d.ts
CHANGED
|
@@ -56,6 +56,13 @@ export interface CandlestickProps<S extends SeriesSchema> {
|
|
|
56
56
|
* for a compact legend; the full quote is opt-in for a dense hover readout.
|
|
57
57
|
*/
|
|
58
58
|
showOHLC?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* This layer's `<Legend>` row: `false` ⇒ no row (opt out), a string ⇒ the
|
|
61
|
+
* row's display name. **Omitted ⇒ a row named by the layer's readout
|
|
62
|
+
* identity** (`as` ?? the `close` column). The swatch is the resolved
|
|
63
|
+
* up/down candle pair.
|
|
64
|
+
*/
|
|
65
|
+
legend?: boolean | string;
|
|
59
66
|
/**
|
|
60
67
|
* @internal Declaration position among the `<Layers>` children, injected by
|
|
61
68
|
* `Layers` so z-order follows JSX order. Do not set.
|
|
@@ -89,5 +96,5 @@ export interface CandlestickProps<S extends SeriesSchema> {
|
|
|
89
96
|
* </Layers>
|
|
90
97
|
* ```
|
|
91
98
|
*/
|
|
92
|
-
export declare function Candlestick<S extends SeriesSchema>({ series, open, high, low, close, as: semantic, axis, variant, colorBy, gap, showOHLC, index, }: CandlestickProps<S>): null;
|
|
99
|
+
export declare function Candlestick<S extends SeriesSchema>({ series, open, high, low, close, as: semantic, axis, variant, colorBy, gap, showOHLC, legend, index, }: CandlestickProps<S>): null;
|
|
93
100
|
//# sourceMappingURL=Candlestick.d.ts.map
|
package/dist/Candlestick.js
CHANGED
|
@@ -2,6 +2,7 @@ import { useContext, useEffect, useMemo } from 'react';
|
|
|
2
2
|
import { ohlcFromTimeSeries } from './data.js';
|
|
3
3
|
import { drawCandles, isFiniteOhlc, ohlcExtent, ohlcIndexAtTime, resolveCandleStyle, } from './ohlc.js';
|
|
4
4
|
import { ContainerContext, LayersContext, } from './context.js';
|
|
5
|
+
import { legendLabelFor, useLegendItems, } from './swatch.js';
|
|
5
6
|
import { useSlotKey } from './use-slot-key.js';
|
|
6
7
|
/**
|
|
7
8
|
* A first-class OHLC **candlestick** draw layer — the financial sibling of
|
|
@@ -30,7 +31,7 @@ import { useSlotKey } from './use-slot-key.js';
|
|
|
30
31
|
* </Layers>
|
|
31
32
|
* ```
|
|
32
33
|
*/
|
|
33
|
-
export function Candlestick({ series, open = 'open', high = 'high', low = 'low', close = 'close', as: semantic, axis, variant = 'candle', colorBy = 'direction', gap = 0, showOHLC = false, index = 0, }) {
|
|
34
|
+
export function Candlestick({ series, open = 'open', high = 'high', low = 'low', close = 'close', as: semantic, axis, variant = 'candle', colorBy = 'direction', gap = 0, showOHLC = false, legend, index = 0, }) {
|
|
34
35
|
const container = useContext(ContainerContext);
|
|
35
36
|
if (container === null) {
|
|
36
37
|
throw new Error('<Candlestick> must be rendered inside a <ChartContainer>');
|
|
@@ -70,11 +71,15 @@ export function Candlestick({ series, open = 'open', high = 'high', low = 'low',
|
|
|
70
71
|
}
|
|
71
72
|
// Opt-in full quote: four value pills (body colour for open/close, wick
|
|
72
73
|
// colour for the high/low extremes). Each is a value-only axis pill.
|
|
74
|
+
// With a semantic `as`, each reads under the series name + role
|
|
75
|
+
// (`SPY high`) — BoxPlot's qLabel convention, so two quoted series
|
|
76
|
+
// don't merge readout keys on the bare role words (F-charts-8 §3).
|
|
77
|
+
const role = (r) => semantic !== undefined ? `${semantic} ${r}` : r;
|
|
73
78
|
const samples = [
|
|
74
|
-
{ x: at, value: ohlc.high[i], color: wick, label: 'high' },
|
|
75
|
-
{ x: at, value: ohlc.open[i], color: body, label: 'open' },
|
|
76
|
-
{ x: at, value: ohlc.close[i], color: body, label: 'close' },
|
|
77
|
-
{ x: at, value: ohlc.low[i], color: wick, label: 'low' },
|
|
79
|
+
{ x: at, value: ohlc.high[i], color: wick, label: role('high') },
|
|
80
|
+
{ x: at, value: ohlc.open[i], color: body, label: role('open') },
|
|
81
|
+
{ x: at, value: ohlc.close[i], color: body, label: role('close') },
|
|
82
|
+
{ x: at, value: ohlc.low[i], color: wick, label: role('low') },
|
|
78
83
|
];
|
|
79
84
|
return samples;
|
|
80
85
|
},
|
|
@@ -82,7 +87,18 @@ export function Candlestick({ series, open = 'open', high = 'high', low = 'low',
|
|
|
82
87
|
},
|
|
83
88
|
axisId: axis,
|
|
84
89
|
index,
|
|
85
|
-
}), [
|
|
90
|
+
}), [
|
|
91
|
+
ohlc,
|
|
92
|
+
style,
|
|
93
|
+
label,
|
|
94
|
+
semantic,
|
|
95
|
+
variant,
|
|
96
|
+
colorBy,
|
|
97
|
+
gap,
|
|
98
|
+
showOHLC,
|
|
99
|
+
axis,
|
|
100
|
+
index,
|
|
101
|
+
]);
|
|
86
102
|
// Stable per-instance slot (see useSlotKey): keeps this candle layer's
|
|
87
103
|
// z-position + identity across prop updates; the injected index drives the sort.
|
|
88
104
|
const slot = useSlotKey();
|
|
@@ -97,6 +113,24 @@ export function Candlestick({ series, open = 'open', high = 'high', low = 'low',
|
|
|
97
113
|
useEffect(() => {
|
|
98
114
|
registerTrackerSource(slot, entry.layer);
|
|
99
115
|
}, [registerTrackerSource, slot, entry.layer]);
|
|
116
|
+
// And a legend row: the series identity + the resolved up/down pair, so a
|
|
117
|
+
// `<Legend>` swatch can never drift from the drawn candles.
|
|
118
|
+
const legendRows = useMemo(() => {
|
|
119
|
+
const name = legendLabelFor(legend, label);
|
|
120
|
+
return name === null
|
|
121
|
+
? null
|
|
122
|
+
: [
|
|
123
|
+
{
|
|
124
|
+
label: name,
|
|
125
|
+
swatch: {
|
|
126
|
+
kind: 'candle',
|
|
127
|
+
up: style.rising.body,
|
|
128
|
+
down: style.falling.body,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
}, [legend, label, style]);
|
|
133
|
+
useLegendItems(container, slot, index, legendRows);
|
|
100
134
|
return null;
|
|
101
135
|
}
|
|
102
136
|
//# sourceMappingURL=Candlestick.js.map
|
package/dist/ChartContainer.d.ts
CHANGED
|
@@ -300,21 +300,31 @@ export interface ChartContainerProps {
|
|
|
300
300
|
*/
|
|
301
301
|
timeFormat?: AxisFormat;
|
|
302
302
|
/**
|
|
303
|
-
* The **cursor / marker readout** format — the crosshair
|
|
303
|
+
* The **cursor / marker readout** format — the crosshair x pill, marker
|
|
304
304
|
* axis indicators, and annotation auto-labels — **independent of the tick
|
|
305
|
-
* labels
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
305
|
+
* labels** on both axis kinds: it does **not** disqualify the `dateStyle`
|
|
306
|
+
* ladder (time), and it never moves the tick labels (value). It beats an
|
|
307
|
+
* explicit `<XAxis format>` for the **readout only** — pill precedence is
|
|
308
|
+
* `cursorFormat → axis format → container` — so terse ticks can pair with a
|
|
309
|
+
* precise readout (`+2.0σ` labels, `+1.83σ` pill).
|
|
310
310
|
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
* axis
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
* {@link
|
|
317
|
-
*
|
|
311
|
+
* **Omitted ⇒ the axis's own formatter.** On a time axis that default is
|
|
312
|
+
* grain-aware: the readout formats at the axis's granularity, so a
|
|
313
|
+
* day-or-coarser axis reads a **date** (never a time-of-day) and a sub-day
|
|
314
|
+
* axis reads date + clock — a daily bar at a foreign-tz midnight no longer
|
|
315
|
+
* renders as `02 AM`. On a value axis it is the tick formatter
|
|
316
|
+
* ({@link timeFormat}-shaped, else the d3 default).
|
|
317
|
+
*
|
|
318
|
+
* A d3 specifier **string** formats uniformly (time specifier on a time
|
|
319
|
+
* axis, number specifier on a value axis); a **function**
|
|
320
|
+
* `(value, { grain, defaultText }) => string` receives the axis's resolved
|
|
321
|
+
* coarse {@link TimeGrain} (`undefined` on a value axis) and the default
|
|
322
|
+
* readout text, so it can branch on the zoom level and pass `defaultText`
|
|
323
|
+
* through for grains it doesn't override (no re-deriving the grain from the
|
|
324
|
+
* range). See {@link CursorFormat}. This is the independent readout channel;
|
|
325
|
+
* {@link timeFormat} owns the labels. (A category axis reads names, and a
|
|
326
|
+
* `transform`ed axis's pill speaks its derived unit — neither consults
|
|
327
|
+
* `cursorFormat`.)
|
|
318
328
|
*/
|
|
319
329
|
cursorFormat?: CursorFormat;
|
|
320
330
|
/** Visual theme for all rows; defaults to {@link defaultTheme}. */
|
package/dist/ChartContainer.js
CHANGED
|
@@ -151,6 +151,20 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
151
151
|
});
|
|
152
152
|
}, []);
|
|
153
153
|
const annotations = useMemo(() => Array.from(annotationMap.values()), [annotationMap]);
|
|
154
|
+
// Legend rows register here (label + resolved swatch per layer, see
|
|
155
|
+
// useLegendItem) so a `<Legend>` anywhere in the container can enumerate
|
|
156
|
+
// every layer across rows. Same per-instance-slot discipline as the sources.
|
|
157
|
+
const [legendItems, setLegendItems] = useState(() => new Map());
|
|
158
|
+
const registerLegendItem = useCallback((key, item) => setLegendItems((m) => new Map(m).set(key, item)), []);
|
|
159
|
+
const unregisterLegendItem = useCallback((key) => {
|
|
160
|
+
setLegendItems((m) => {
|
|
161
|
+
if (!m.has(key))
|
|
162
|
+
return m;
|
|
163
|
+
const next = new Map(m);
|
|
164
|
+
next.delete(key);
|
|
165
|
+
return next;
|
|
166
|
+
});
|
|
167
|
+
}, []);
|
|
154
168
|
// The shared x scale's kind, **inferred from the registered layers**: a
|
|
155
169
|
// ValueSeries row plots on a value axis, a TimeSeries on time. A container
|
|
156
170
|
// has one shared x (the synced cursor's whole point), so the rows must agree
|
|
@@ -243,8 +257,8 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
243
257
|
if (!warnedNoSelectableRef.current) {
|
|
244
258
|
warnedNoSelectableRef.current = true;
|
|
245
259
|
console.warn('[pond-charts] `selected`/`onSelect` is set but no layer has an `id` — ' +
|
|
246
|
-
'nothing is selectable. Give a <BarChart>/<ScatterChart> an
|
|
247
|
-
'make it interactive (an `id` gates selection + hover).');
|
|
260
|
+
'nothing is selectable. Give a <BarChart>/<ScatterChart>/<BoxPlot> an ' +
|
|
261
|
+
'`id` to make it interactive (an `id` gates selection + hover).');
|
|
248
262
|
}
|
|
249
263
|
}
|
|
250
264
|
else {
|
|
@@ -309,13 +323,19 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
309
323
|
// (t0/t1); otherwise the auto-fit extent (→ [0, 1] before any layer registers,
|
|
310
324
|
// the two-pass settle). This is what the scale + cursor + axis read.
|
|
311
325
|
const [d0, d1] = explicitDomain !== undefined ? [t0, t1] : (autoExtent ?? [0, 1]);
|
|
312
|
-
// The shared x scale +
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
// the
|
|
318
|
-
//
|
|
326
|
+
// The shared x scale + its two formatting channels, built together so each
|
|
327
|
+
// branch keeps its concrete scale type (no casts): a value axis is a
|
|
328
|
+
// `scaleLinear` formatted by `resolveAxisFormat`, time is a `scaleTime`
|
|
329
|
+
// formatted by d3's multi-scale `resolveTimeFormat`.
|
|
330
|
+
//
|
|
331
|
+
// - `formatTime` is the **label** channel — what `<XAxis>` ticks fall back to
|
|
332
|
+
// — shaped by `timeFormat` only, never `cursorFormat` (so a readout format
|
|
333
|
+
// can't move the tick labels). (The name predates the value axis — on a
|
|
334
|
+
// value axis it formats the value, not a time.)
|
|
335
|
+
// - `formatReadout` is the **readout** channel — the crosshair pill, marker
|
|
336
|
+
// indicators, and annotation auto-labels — defined only when `cursorFormat`
|
|
337
|
+
// is set; consumers read `formatReadout ?? <their label formatter>`, which
|
|
338
|
+
// is how "the readout matches the axis" stays the default.
|
|
319
339
|
// The trading-time provider only applies to a **time** axis — a value axis is
|
|
320
340
|
// always a plain `scaleLinear`. Gate it once here so the scale branch AND the
|
|
321
341
|
// frame (which pan/zoom read) agree: on a value axis the provider is dropped,
|
|
@@ -342,35 +362,52 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
342
362
|
const xTickCount = resolvedKind === 'time'
|
|
343
363
|
? Math.max(2, Math.floor(plotWidth / TRADING_TICK_PX))
|
|
344
364
|
: TIME_TICK_COUNT;
|
|
345
|
-
const { xScale, formatTime } = useMemo(() => {
|
|
365
|
+
const { xScale, formatTime, formatReadout } = useMemo(() => {
|
|
346
366
|
if (resolvedKind === 'category') {
|
|
347
367
|
// Ordinal column-domain axis: a band scale over the category slots. The
|
|
348
368
|
// domain is **always** `[0, n]` (one unit slot per category) — NOT the
|
|
349
369
|
// resolved `[d0, d1]`: a category axis ignores an explicit `range` (its
|
|
350
370
|
// slots are absolute `0..n`, matching `categoryStack`), so an out-of-`[0,n]`
|
|
351
371
|
// range can't silently offset the labels from the bars. The pixel mapping
|
|
352
|
-
// stays linear; the formatter is the category-name lookup.
|
|
372
|
+
// stays linear; the formatter is the category-name lookup. A category
|
|
373
|
+
// reads by **name** — `cursorFormat` has nothing to format, so the
|
|
374
|
+
// readout channel stays unset.
|
|
353
375
|
const cats = categories ?? [];
|
|
354
376
|
const s = scaleBand(cats).domain([0, cats.length]).range([0, plotWidth]);
|
|
355
|
-
return { xScale: s, formatTime: (v) => s.label(v) };
|
|
356
|
-
}
|
|
357
|
-
if (resolvedKind === 'value') {
|
|
358
|
-
const s = scaleLinear().domain([d0, d1]).range([0, plotWidth]);
|
|
359
377
|
return {
|
|
360
378
|
xScale: s,
|
|
361
|
-
formatTime:
|
|
379
|
+
formatTime: (v) => s.label(v),
|
|
380
|
+
formatReadout: undefined,
|
|
362
381
|
};
|
|
363
382
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
383
|
+
if (resolvedKind === 'value') {
|
|
384
|
+
const s = scaleLinear().domain([d0, d1]).range([0, plotWidth]);
|
|
385
|
+
const labels = resolveAxisFormat(s, xTickCount, timeFormat);
|
|
386
|
+
// The value-axis readout channel: a `cursorFormat` **string** is a d3
|
|
387
|
+
// *number* specifier here (resolved through the linear scale, exactly as
|
|
388
|
+
// a tick format would be); a **function** gets `grain: undefined` (no
|
|
389
|
+
// time grain to hand over) and the label formatter's text as its
|
|
390
|
+
// pass-through default.
|
|
391
|
+
const readout = typeof cursorFormat === 'function'
|
|
392
|
+
? (v) => cursorFormat(v, { grain: undefined, defaultText: labels(v) })
|
|
393
|
+
: cursorFormat !== undefined
|
|
394
|
+
? resolveAxisFormat(s, xTickCount, cursorFormat)
|
|
395
|
+
: undefined;
|
|
396
|
+
return { xScale: s, formatTime: labels, formatReadout: readout };
|
|
397
|
+
}
|
|
398
|
+
// Time axis, label channel: a container `timeFormat` when set (it owns the
|
|
399
|
+
// labels and opts them out of the ladder); else the scale's **grain-aware**
|
|
400
|
+
// default (a day-or-coarser axis reads a date, not a time-of-day — the
|
|
401
|
+
// F-charts-7 `02 AM` fix), never d3's multi-scale default.
|
|
402
|
+
const timeLabels = (s) => timeFormat !== undefined
|
|
403
|
+
? resolveTimeFormat(s, xTickCount, timeFormat)
|
|
404
|
+
: s.readoutFormat(xTickCount);
|
|
405
|
+
// Time axis, readout channel — only when `cursorFormat` is set (otherwise
|
|
406
|
+
// the readout falls back to the label channel at the consumer). A
|
|
407
|
+
// **function** gets the axis's resolved coarse grain and the grain-aware
|
|
408
|
+
// default text per instant, so it can branch on zoom and pass the default
|
|
409
|
+
// through. A **string** formats uniformly (d3 time specifier).
|
|
370
410
|
const timeReadout = (s) => {
|
|
371
|
-
// A `cursorFormat` **function** gets the axis's resolved coarse grain and
|
|
372
|
-
// the grain-aware default text per instant, so it can branch on zoom and
|
|
373
|
-
// pass the default through. A **string** formats uniformly (d3 specifier).
|
|
374
411
|
if (typeof cursorFormat === 'function') {
|
|
375
412
|
const grain = s.grain(xTickCount);
|
|
376
413
|
const def = s.readoutFormat(xTickCount);
|
|
@@ -379,11 +416,7 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
379
416
|
if (cursorFormat !== undefined) {
|
|
380
417
|
return resolveTimeFormat(s, xTickCount, cursorFormat);
|
|
381
418
|
}
|
|
382
|
-
|
|
383
|
-
// (back-compat); else the scale's grain-aware default.
|
|
384
|
-
return timeFormat !== undefined
|
|
385
|
-
? resolveTimeFormat(s, xTickCount, timeFormat)
|
|
386
|
-
: s.readoutFormat(xTickCount);
|
|
419
|
+
return undefined;
|
|
387
420
|
};
|
|
388
421
|
if (xDiscontinuities !== undefined) {
|
|
389
422
|
// Trading-time axis: closed-market gaps collapse, time proportional within
|
|
@@ -393,7 +426,11 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
393
426
|
const s = scaleTradingTime(xDiscontinuities)
|
|
394
427
|
.domain([d0, d1])
|
|
395
428
|
.range([0, plotWidth]);
|
|
396
|
-
return {
|
|
429
|
+
return {
|
|
430
|
+
xScale: s,
|
|
431
|
+
formatTime: timeLabels(s),
|
|
432
|
+
formatReadout: timeReadout(s),
|
|
433
|
+
};
|
|
397
434
|
}
|
|
398
435
|
// Plain continuous time axis: the same trading-time scale over the
|
|
399
436
|
// gap-free identity provider, so it runs the same logical tick ladder
|
|
@@ -404,7 +441,11 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
404
441
|
const s = scaleTradingTime(identityProvider())
|
|
405
442
|
.domain([d0, d1])
|
|
406
443
|
.range([0, plotWidth]);
|
|
407
|
-
return {
|
|
444
|
+
return {
|
|
445
|
+
xScale: s,
|
|
446
|
+
formatTime: timeLabels(s),
|
|
447
|
+
formatReadout: timeReadout(s),
|
|
448
|
+
};
|
|
408
449
|
}, [
|
|
409
450
|
resolvedKind,
|
|
410
451
|
categories,
|
|
@@ -527,6 +568,7 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
527
568
|
onHoverAnnotation,
|
|
528
569
|
onEditAnnotation,
|
|
529
570
|
formatTime,
|
|
571
|
+
formatReadout,
|
|
530
572
|
xFormatCustom: timeFormat !== undefined,
|
|
531
573
|
xTickCount,
|
|
532
574
|
registerTrackerSource,
|
|
@@ -535,6 +577,10 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
535
577
|
unregisterSelectable,
|
|
536
578
|
registerAnnotation,
|
|
537
579
|
unregisterAnnotation,
|
|
580
|
+
registerLegendItem,
|
|
581
|
+
unregisterLegendItem,
|
|
582
|
+
legendItems,
|
|
583
|
+
rowOrder: rowKeys,
|
|
538
584
|
annotations,
|
|
539
585
|
labelLanes,
|
|
540
586
|
xScale,
|
|
@@ -584,6 +630,7 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
584
630
|
onHoverAnnotation,
|
|
585
631
|
onEditAnnotation,
|
|
586
632
|
formatTime,
|
|
633
|
+
formatReadout,
|
|
587
634
|
timeFormat,
|
|
588
635
|
xTickCount,
|
|
589
636
|
registerTrackerSource,
|
|
@@ -592,6 +639,10 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
592
639
|
unregisterSelectable,
|
|
593
640
|
registerAnnotation,
|
|
594
641
|
unregisterAnnotation,
|
|
642
|
+
registerLegendItem,
|
|
643
|
+
unregisterLegendItem,
|
|
644
|
+
legendItems,
|
|
645
|
+
rowKeys,
|
|
595
646
|
annotations,
|
|
596
647
|
labelLanes,
|
|
597
648
|
xScale,
|
|
@@ -610,6 +661,9 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
610
661
|
display: 'flex',
|
|
611
662
|
flexDirection: 'column',
|
|
612
663
|
gap: `${rowGap}px`,
|
|
664
|
+
// The positioned ancestor for overlay chrome (`<Legend>`): the
|
|
665
|
+
// card anchors to the rows block, never the axis strip below.
|
|
666
|
+
position: 'relative',
|
|
613
667
|
}, children: children }), showAxis && _jsx(TimeAxis, {})] }) }));
|
|
614
668
|
}
|
|
615
669
|
//# sourceMappingURL=ChartContainer.js.map
|
package/dist/ChartRow.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Children, cloneElement, isValidElement, useCallback, useContext, useEff
|
|
|
3
3
|
import { scaleLinear } from 'd3-scale';
|
|
4
4
|
import { resolveYDomain } from './domain.js';
|
|
5
5
|
import { resolveAxisFormat } from './format.js';
|
|
6
|
+
import { resolveYTickCount } from './yticks.js';
|
|
6
7
|
import { placeAxisSlots } from './slots.js';
|
|
7
8
|
import { useSlotKey } from './use-slot-key.js';
|
|
8
9
|
import { YAxis } from './YAxis.js';
|
|
@@ -45,6 +46,7 @@ function axisSpecEqual(a, b) {
|
|
|
45
46
|
a.pad === b.pad &&
|
|
46
47
|
a.labelPlacement === b.labelPlacement &&
|
|
47
48
|
a.index === b.index &&
|
|
49
|
+
a.tickCount === b.tickCount &&
|
|
48
50
|
Object.is(a.format, b.format) &&
|
|
49
51
|
numberArraysEqual(a.tickValues, b.tickValues));
|
|
50
52
|
}
|
|
@@ -175,6 +177,7 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
175
177
|
labelPlacement: 'rotated',
|
|
176
178
|
format: undefined,
|
|
177
179
|
tickValues: undefined,
|
|
180
|
+
tickCount: undefined,
|
|
178
181
|
index: 0,
|
|
179
182
|
},
|
|
180
183
|
], [realAxes]);
|
|
@@ -233,17 +236,31 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
233
236
|
}
|
|
234
237
|
return map;
|
|
235
238
|
}, [effectiveAxes, layerList, height, defaultAxisId, topHeader]);
|
|
239
|
+
// Resolved auto-tick count per axis — explicit `<YAxis tickCount>` else
|
|
240
|
+
// height-derived (see resolveYTickCount). The single source the `<YAxis>`
|
|
241
|
+
// labels, the readout formatter (below), and the `Layers` gridlines all read,
|
|
242
|
+
// so label / readout / gridline stay on one `ticks(count)`.
|
|
243
|
+
const tickCounts = useMemo(() => {
|
|
244
|
+
const map = new Map();
|
|
245
|
+
for (const ax of effectiveAxes) {
|
|
246
|
+
map.set(ax.id, resolveYTickCount(height, ax.tickCount));
|
|
247
|
+
}
|
|
248
|
+
return map;
|
|
249
|
+
}, [effectiveAxes, height]);
|
|
236
250
|
// A value formatter per axis (its `format` resolved against its scale) — shared
|
|
237
251
|
// by the axis tick labels and the cursor readout so a value reads the same.
|
|
252
|
+
// Calibrated to the axis's resolved tick count (density), like the labels.
|
|
238
253
|
const formats = useMemo(() => {
|
|
239
254
|
const map = new Map();
|
|
240
255
|
for (const ax of effectiveAxes) {
|
|
241
256
|
const sc = yScales.get(ax.id);
|
|
242
|
-
if (sc)
|
|
243
|
-
|
|
257
|
+
if (sc) {
|
|
258
|
+
const count = tickCounts.get(ax.id) ?? AXIS_TICK_COUNT;
|
|
259
|
+
map.set(ax.id, resolveAxisFormat(sc, count, ax.format));
|
|
260
|
+
}
|
|
244
261
|
}
|
|
245
262
|
return map;
|
|
246
|
-
}, [effectiveAxes, yScales]);
|
|
263
|
+
}, [effectiveAxes, yScales, tickCounts]);
|
|
247
264
|
// Explicit tick values per axis (axes that set `<YAxis ticks>`) — so Layers
|
|
248
265
|
// draws the row's gridlines at the same positions the axis labels, instead of
|
|
249
266
|
// d3's auto-picked ticks.
|
|
@@ -271,6 +288,7 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
271
288
|
yScales,
|
|
272
289
|
formats,
|
|
273
290
|
tickValues,
|
|
291
|
+
tickCounts,
|
|
274
292
|
axisSides,
|
|
275
293
|
defaultAxisId,
|
|
276
294
|
axisSlots,
|
|
@@ -287,6 +305,7 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
287
305
|
yScales,
|
|
288
306
|
formats,
|
|
289
307
|
tickValues,
|
|
308
|
+
tickCounts,
|
|
290
309
|
axisSides,
|
|
291
310
|
defaultAxisId,
|
|
292
311
|
axisSlots,
|