@pond-ts/charts 0.41.0 → 0.42.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 +74 -5
- package/dist/BarChart.d.ts +117 -54
- package/dist/BarChart.js +241 -104
- package/dist/ChartContainer.d.ts +52 -6
- package/dist/ChartContainer.js +81 -4
- package/dist/Layers.js +47 -8
- package/dist/ScatterChart.d.ts +15 -3
- package/dist/ScatterChart.js +23 -5
- package/dist/bars.d.ts +96 -5
- package/dist/bars.js +157 -10
- package/dist/context.d.ts +54 -19
- package/dist/data.d.ts +102 -1
- package/dist/data.js +147 -0
- package/dist/grid.d.ts +14 -0
- package/dist/grid.js +36 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -1
- package/dist/scatter.d.ts +9 -7
- package/dist/scatter.js +12 -8
- package/dist/theme.d.ts +7 -0
- package/dist/theme.js +1 -0
- package/dist/tradingTimeScale.d.ts +97 -0
- package/dist/tradingTimeScale.js +152 -0
- package/dist/viewport.d.ts +23 -0
- package/dist/viewport.js +51 -0
- package/package.json +3 -3
package/dist/ChartContainer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
3
3
|
import { scaleLinear, scaleTime } from 'd3-scale';
|
|
4
|
+
import { scaleTradingTime, } from './tradingTimeScale.js';
|
|
4
5
|
import { ContainerContext, } from './context.js';
|
|
5
6
|
import { maxSlotWidths, sum } from './slots.js';
|
|
6
7
|
import { computeLabelLanes } from './annotations.js';
|
|
@@ -31,7 +32,7 @@ function normalizeRange(range) {
|
|
|
31
32
|
* {@link TimeAxis} at the bottom, aligned under the plots. Y axes are per-row
|
|
32
33
|
* (`<YAxis>`).
|
|
33
34
|
*/
|
|
34
|
-
export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom = false, onTimeRangeChange, minDuration = 1, cursor = DEFAULT_CURSOR_MODE, cursorTime = false, crosshairSnap = true, editAnnotations = false, creating = null, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap = true, timeFormat, theme, children, }) {
|
|
35
|
+
export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom = false, onTimeRangeChange, minDuration = 1, cursor = DEFAULT_CURSOR_MODE, cursorTime = false, crosshairSnap = true, editAnnotations = false, creating = null, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap = true, timeFormat, theme, discontinuities, calendar, spacing, children, }) {
|
|
35
36
|
// The explicit base domain from `range` (a tuple or a TimeRange). `undefined`
|
|
36
37
|
// ⇒ auto-fit (resolved from the layers below). Pan/zoom seeds from it; `seed`
|
|
37
38
|
// is the placeholder while auto-fitting.
|
|
@@ -99,6 +100,29 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
99
100
|
return next;
|
|
100
101
|
});
|
|
101
102
|
}, []);
|
|
103
|
+
// Selectable-layer registry: an id-bearing Bar/Scatter registers here (keyed
|
|
104
|
+
// by its slot) so the container knows whether *any* series is selectable. Only
|
|
105
|
+
// used to power the dev-warn below — selection resolution itself walks the
|
|
106
|
+
// rows' layers, not this set. Backed by a **ref** (the synchronous source of
|
|
107
|
+
// truth) mirrored to state: a child layer's register effect runs before this
|
|
108
|
+
// parent's dev-warn effect in the same commit, so the ref is already settled
|
|
109
|
+
// there (reading state would lag a render). State only triggers the re-check.
|
|
110
|
+
const selectableRef = useRef(new Set());
|
|
111
|
+
const [selectableKeys, setSelectableKeys] = useState(selectableRef.current);
|
|
112
|
+
const registerSelectable = useCallback((key) => {
|
|
113
|
+
if (selectableRef.current.has(key))
|
|
114
|
+
return;
|
|
115
|
+
selectableRef.current = new Set(selectableRef.current).add(key);
|
|
116
|
+
setSelectableKeys(selectableRef.current);
|
|
117
|
+
}, []);
|
|
118
|
+
const unregisterSelectable = useCallback((key) => {
|
|
119
|
+
if (!selectableRef.current.has(key))
|
|
120
|
+
return;
|
|
121
|
+
const next = new Set(selectableRef.current);
|
|
122
|
+
next.delete(key);
|
|
123
|
+
selectableRef.current = next;
|
|
124
|
+
setSelectableKeys(next);
|
|
125
|
+
}, []);
|
|
102
126
|
// Annotations register here so the container can do what a mark can't in
|
|
103
127
|
// isolation: draw its guide line across other rows, order regions, serve snap
|
|
104
128
|
// targets. Keyed by per-instance slot key (same discipline as the sources).
|
|
@@ -174,6 +198,26 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
174
198
|
if (!controlledSelectionRef.current)
|
|
175
199
|
setInternalSelected(hit);
|
|
176
200
|
}, []);
|
|
201
|
+
// Dev-warn: selection is wired (`selected` and/or `onSelect`) but no layer
|
|
202
|
+
// carries an `id`, so nothing is selectable — `id` gates interactivity, so a
|
|
203
|
+
// consumer who forgot it gets a silent no-op click without this nudge. Fires
|
|
204
|
+
// once per wired-but-empty transition (guarded by a ref); child layers
|
|
205
|
+
// register before this parent effect runs, so the set is settled here.
|
|
206
|
+
const selectionWired = controlledSelection || onSelect !== undefined;
|
|
207
|
+
const warnedNoSelectableRef = useRef(false);
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (selectionWired && selectableRef.current.size === 0) {
|
|
210
|
+
if (!warnedNoSelectableRef.current) {
|
|
211
|
+
warnedNoSelectableRef.current = true;
|
|
212
|
+
console.warn('[pond-charts] `selected`/`onSelect` is set but no layer has an `id` — ' +
|
|
213
|
+
'nothing is selectable. Give a <BarChart>/<ScatterChart> an `id` to ' +
|
|
214
|
+
'make it interactive (an `id` gates selection + hover).');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
warnedNoSelectableRef.current = false;
|
|
219
|
+
}
|
|
220
|
+
}, [selectionWired, selectableKeys]);
|
|
177
221
|
// Hover-highlight: the transient mark under the pointer (distinct from the
|
|
178
222
|
// committed selection). Controlled (`hovered` prop) or uncontrolled (internal),
|
|
179
223
|
// mirroring selection; `onHover` notifies in both modes. Deduped by key+label
|
|
@@ -197,8 +241,8 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
197
241
|
const same = prev === hit ||
|
|
198
242
|
(prev !== null &&
|
|
199
243
|
hit !== null &&
|
|
200
|
-
prev.
|
|
201
|
-
prev.
|
|
244
|
+
prev.id === hit.id &&
|
|
245
|
+
prev.key === hit.key);
|
|
202
246
|
if (same)
|
|
203
247
|
return;
|
|
204
248
|
lastHoverRef.current = hit;
|
|
@@ -239,6 +283,22 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
239
283
|
// is the one formatter <TimeAxis> + the cursor readout share, so a tick and
|
|
240
284
|
// the cursor read identically. (The `formatTime` name predates the value axis
|
|
241
285
|
// — on a value axis it formats the value, not a time.)
|
|
286
|
+
// The trading-time provider only applies to a **time** axis — a value axis is
|
|
287
|
+
// always a plain `scaleLinear`. Gate it once here so the scale branch AND the
|
|
288
|
+
// frame (which pan/zoom read) agree: on a value axis the provider is dropped,
|
|
289
|
+
// so interactions use continuous value math, not trading-time math.
|
|
290
|
+
// Resolve the trading-time provider: the low-level `discontinuities` prop wins;
|
|
291
|
+
// otherwise derive it from the high-level `calendar` sugar at the chosen
|
|
292
|
+
// `spacing`. Memoized on `(calendar, spacing)` so a stable calendar yields a
|
|
293
|
+
// stable provider (the scale + frame only rebuild when it actually changes) —
|
|
294
|
+
// pan/zoom read the same provider identity as the low-level path would. Gated
|
|
295
|
+
// on a time axis so a value-axis chart never calls `calendar.discontinuities`.
|
|
296
|
+
const calendarProvider = useMemo(() => resolvedKind === 'time' &&
|
|
297
|
+
discontinuities === undefined &&
|
|
298
|
+
calendar !== undefined
|
|
299
|
+
? calendar.discontinuities(spacing ? { spacing } : undefined)
|
|
300
|
+
: undefined, [resolvedKind, discontinuities, calendar, spacing]);
|
|
301
|
+
const xDiscontinuities = resolvedKind === 'time' ? (discontinuities ?? calendarProvider) : undefined;
|
|
242
302
|
const { xScale, formatTime } = useMemo(() => {
|
|
243
303
|
if (resolvedKind === 'value') {
|
|
244
304
|
const s = scaleLinear().domain([d0, d1]).range([0, plotWidth]);
|
|
@@ -247,12 +307,23 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
247
307
|
formatTime: resolveAxisFormat(s, TIME_TICK_COUNT, timeFormat),
|
|
248
308
|
};
|
|
249
309
|
}
|
|
310
|
+
if (xDiscontinuities !== undefined) {
|
|
311
|
+
// Trading-time axis: closed-market gaps collapse, time proportional within
|
|
312
|
+
// sessions. Same tickFormat surface as scaleTime, so the readout is shared.
|
|
313
|
+
const s = scaleTradingTime(xDiscontinuities)
|
|
314
|
+
.domain([d0, d1])
|
|
315
|
+
.range([0, plotWidth]);
|
|
316
|
+
return {
|
|
317
|
+
xScale: s,
|
|
318
|
+
formatTime: resolveTimeFormat(s, TIME_TICK_COUNT, timeFormat),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
250
321
|
const s = scaleTime().domain([d0, d1]).range([0, plotWidth]);
|
|
251
322
|
return {
|
|
252
323
|
xScale: s,
|
|
253
324
|
formatTime: resolveTimeFormat(s, TIME_TICK_COUNT, timeFormat),
|
|
254
325
|
};
|
|
255
|
-
}, [resolvedKind, d0, d1, plotWidth, timeFormat]);
|
|
326
|
+
}, [resolvedKind, d0, d1, plotWidth, timeFormat, xDiscontinuities]);
|
|
256
327
|
// The crosshair pixel (see resolveCursorX). A stored hoverX is a *plot* pixel;
|
|
257
328
|
// if plotWidth changes mid-hover (a gutter reserving, or a width change) it's
|
|
258
329
|
// briefly stale until the next pointer move — rare, and the bounds check below
|
|
@@ -316,12 +387,15 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
316
387
|
formatTime,
|
|
317
388
|
registerTrackerSource,
|
|
318
389
|
unregisterTrackerSource,
|
|
390
|
+
registerSelectable,
|
|
391
|
+
unregisterSelectable,
|
|
319
392
|
registerAnnotation,
|
|
320
393
|
unregisterAnnotation,
|
|
321
394
|
annotations,
|
|
322
395
|
labelLanes,
|
|
323
396
|
xScale,
|
|
324
397
|
xKind: resolvedKind,
|
|
398
|
+
discontinuities: xDiscontinuities,
|
|
325
399
|
panZoom,
|
|
326
400
|
minDuration,
|
|
327
401
|
applyRange,
|
|
@@ -361,12 +435,15 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
361
435
|
formatTime,
|
|
362
436
|
registerTrackerSource,
|
|
363
437
|
unregisterTrackerSource,
|
|
438
|
+
registerSelectable,
|
|
439
|
+
unregisterSelectable,
|
|
364
440
|
registerAnnotation,
|
|
365
441
|
unregisterAnnotation,
|
|
366
442
|
annotations,
|
|
367
443
|
labelLanes,
|
|
368
444
|
xScale,
|
|
369
445
|
resolvedKind,
|
|
446
|
+
xDiscontinuities,
|
|
370
447
|
panZoom,
|
|
371
448
|
minDuration,
|
|
372
449
|
applyRange,
|
package/dist/Layers.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Children, cloneElement, isValidElement, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
3
3
|
import { Canvas } from './Canvas.js';
|
|
4
|
-
import { drawGrid } from './grid.js';
|
|
4
|
+
import { drawGrid, drawDividers, thinPixels } from './grid.js';
|
|
5
5
|
import { cursorParts } from './tracker.js';
|
|
6
6
|
import { resolveSelection } from './select.js';
|
|
7
|
-
import { panRange, zoomRange } from './viewport.js';
|
|
7
|
+
import { panRange, zoomRange, panRangeTrading, zoomRangeTrading, } from './viewport.js';
|
|
8
8
|
import { flagChipStyle, flagChipX, axisPillX, axisPillStyle } from './chip.js';
|
|
9
9
|
import { ContainerContext, LayersContext, RowContext, } from './context.js';
|
|
10
|
-
/** Gridline tick count
|
|
10
|
+
/** Gridline tick count. **Must match the axis label counts** (`XAxis`
|
|
11
|
+
* `TICK_COUNT`, `ChartContainer` `TIME_TICK_COUNT`, `YAxis`) — the grid, the
|
|
12
|
+
* session dividers, and the axis labels are all derived from `ticks(count)`, so
|
|
13
|
+
* they only line up while the counts agree. Kept at 5 across all four. */
|
|
11
14
|
const GRID_TICKS = 5;
|
|
15
|
+
/** Minimum px between session dividers — thins dense collapse points (e.g. a
|
|
16
|
+
* daily chart where every candle is a new session) so the axis never crowds. */
|
|
17
|
+
const MIN_DIVIDER_PX = 40;
|
|
12
18
|
/** Wheel-zoom sensitivity: `factor = exp(deltaY * k)` (one ~100px notch ≈ ±15%). */
|
|
13
19
|
const ZOOM_SENSITIVITY = 0.0015;
|
|
14
20
|
/** Pointer slop (px): a drag must exceed this before it pans, and a click within
|
|
@@ -63,11 +69,29 @@ export function Layers({ children }) {
|
|
|
63
69
|
// Explicit `<YAxis ticks>` drive the gridlines too, so they align with the
|
|
64
70
|
// axis labels; otherwise d3 auto-picks (the default).
|
|
65
71
|
const explicitY = tickValues.get(defaultAxisId);
|
|
66
|
-
const
|
|
72
|
+
const xTickVals = xScale.ticks(GRID_TICKS);
|
|
73
|
+
const xTicks = xTickVals.map((d) => xScale(+d));
|
|
67
74
|
const yTicks = gridY
|
|
68
75
|
? (explicitY ?? gridY.ticks(GRID_TICKS)).map((t) => gridY(t))
|
|
69
76
|
: [];
|
|
70
77
|
drawGrid(ctx, xTicks, yTicks, w, h, gridColor, gridDash);
|
|
78
|
+
// Session dividers: solid verticals at the trading calendar's collapse
|
|
79
|
+
// points (session/day opens), where closed time was removed from the axis.
|
|
80
|
+
// Draw them at the axis ticks that are collapse points — the same
|
|
81
|
+
// calendar-coarsened instants the axis labels — so a divider sits under
|
|
82
|
+
// each date/month/year label, not at every session (which crowds).
|
|
83
|
+
const disc = container.discontinuities;
|
|
84
|
+
if (disc?.boundaries) {
|
|
85
|
+
const [d0, d1] = container.timeRange;
|
|
86
|
+
// Call as a method (not a detached reference) so a class-based provider
|
|
87
|
+
// whose `boundaries` reads `this` keeps its receiver.
|
|
88
|
+
const collapse = new Set(disc.boundaries(d0, d1));
|
|
89
|
+
const bx = xTickVals
|
|
90
|
+
.filter((t) => collapse.has(+t))
|
|
91
|
+
.map((t) => xScale(+t));
|
|
92
|
+
const dividerColor = container.theme.axis.sessionDivider ?? gridColor;
|
|
93
|
+
drawDividers(ctx, thinPixels(bx, MIN_DIVIDER_PX), h, dividerColor);
|
|
94
|
+
}
|
|
71
95
|
for (const entry of layers) {
|
|
72
96
|
const yScale = yScales.get(entry.axisId ?? defaultAxisId);
|
|
73
97
|
if (yScale === undefined)
|
|
@@ -83,6 +107,8 @@ export function Layers({ children }) {
|
|
|
83
107
|
background,
|
|
84
108
|
gridColor,
|
|
85
109
|
gridDash,
|
|
110
|
+
container.discontinuities,
|
|
111
|
+
container.timeRange,
|
|
86
112
|
]);
|
|
87
113
|
// Interaction overlay: the cursor marks live on a DOM/SVG overlay above the
|
|
88
114
|
// data, so hovering never repaints the data canvas (whose `draw` doesn't depend
|
|
@@ -296,9 +322,17 @@ export function Layers({ children }) {
|
|
|
296
322
|
/* ignore (synthetic / already-released pointer) */
|
|
297
323
|
}
|
|
298
324
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
325
|
+
if (c.discontinuities) {
|
|
326
|
+
// Trading-time axis: pan by an equal amount of *trading* time so the
|
|
327
|
+
// drag feels uniform across collapsed gaps (a raw-ms shift jumps).
|
|
328
|
+
const fraction = c.plotWidth > 0 ? -dx / c.plotWidth : 0;
|
|
329
|
+
c.applyRange(panRangeTrading(drag.startRange, fraction, c.discontinuities));
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
const span = drag.startRange[1] - drag.startRange[0];
|
|
333
|
+
const dt = c.plotWidth > 0 ? -dx * (span / c.plotWidth) : 0;
|
|
334
|
+
c.applyRange(panRange(drag.startRange, dt));
|
|
335
|
+
}
|
|
302
336
|
return; // tracker suppressed during a pan
|
|
303
337
|
}
|
|
304
338
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
@@ -451,7 +485,12 @@ export function Layers({ children }) {
|
|
|
451
485
|
const localX = Math.max(0, Math.min(c.plotWidth, e.clientX - rect.left));
|
|
452
486
|
const pivot = +c.xScale.invert(localX);
|
|
453
487
|
const factor = Math.exp(e.deltaY * ZOOM_SENSITIVITY);
|
|
454
|
-
c.applyRange(
|
|
488
|
+
c.applyRange(c.discontinuities
|
|
489
|
+
? // minDuration is the zoom-in floor; on a trading-time axis it caps
|
|
490
|
+
// the minimum visible *trading* time (ms of open-market time) rather
|
|
491
|
+
// than wall-clock ms — the sensible meaning for this axis.
|
|
492
|
+
zoomRangeTrading(c.timeRange, pivot, factor, c.discontinuities, c.minDuration)
|
|
493
|
+
: zoomRange(c.timeRange, pivot, factor, c.minDuration));
|
|
455
494
|
};
|
|
456
495
|
el.addEventListener('wheel', onWheel, { passive: false });
|
|
457
496
|
return () => el.removeEventListener('wheel', onWheel);
|
package/dist/ScatterChart.d.ts
CHANGED
|
@@ -15,6 +15,17 @@ export interface ScatterChartProps<S extends SeriesSchema> {
|
|
|
15
15
|
* exception), not a per-component style override.
|
|
16
16
|
*/
|
|
17
17
|
as?: string;
|
|
18
|
+
/**
|
|
19
|
+
* The **stable series identity** for selection + hover. **Optional, and it
|
|
20
|
+
* gates interactivity:** the scatter is selectable/hoverable only when given an
|
|
21
|
+
* `id` — omit it and the points render + read out but can't be clicked (a click
|
|
22
|
+
* on them reads as empty space ⇒ deselect). Distinct from `as` (a theme role
|
|
23
|
+
* that can repeat): `id` must be unique among the selectable layers, and it is
|
|
24
|
+
* the key the controlled `selected` echo, dedup, and (later) multi-select all
|
|
25
|
+
* match on — so a selection survives a data update where a sample `key` goes
|
|
26
|
+
* stale.
|
|
27
|
+
*/
|
|
28
|
+
id?: string;
|
|
18
29
|
/**
|
|
19
30
|
* Which `<YAxis>` (by its `id`) this scatter scales against — picks the
|
|
20
31
|
* *scale*, where `as` picks the *style*. **Omitted ⇒ the row's default axis.**
|
|
@@ -66,8 +77,9 @@ export interface ScatterChartProps<S extends SeriesSchema> {
|
|
|
66
77
|
* (`sampleAt`), and that sample flows to the container's `onTrackerChanged` —
|
|
67
78
|
* the nearest-point readout. Scatter reuses the shared tracker rather than
|
|
68
79
|
* adding a separate `onNearest` channel, so a scatter reads out exactly like a
|
|
69
|
-
* line. Click selection hit-tests each point's disc (`hitTest`)
|
|
70
|
-
* point (matching
|
|
80
|
+
* line. Click selection hit-tests each point's disc (`hitTest`) — **opt-in via
|
|
81
|
+
* `id`**; the selected point (matching the selection's series `id` and the sample
|
|
82
|
+
* `key`) gets a highlight ring. Without an `id` the scatter is display-only.
|
|
71
83
|
*
|
|
72
84
|
* ```tsx
|
|
73
85
|
* <Layers>
|
|
@@ -80,5 +92,5 @@ export interface ScatterChartProps<S extends SeriesSchema> {
|
|
|
80
92
|
* </Layers>
|
|
81
93
|
* ```
|
|
82
94
|
*/
|
|
83
|
-
export declare function ScatterChart<S extends SeriesSchema>({ series, column, as: semantic, axis, radius, color, label, index, }: ScatterChartProps<S>): null;
|
|
95
|
+
export declare function ScatterChart<S extends SeriesSchema>({ series, column, as: semantic, id, axis, radius, color, label, index, }: ScatterChartProps<S>): null;
|
|
84
96
|
//# sourceMappingURL=ScatterChart.d.ts.map
|
package/dist/ScatterChart.js
CHANGED
|
@@ -16,8 +16,9 @@ import { useSlotKey } from './use-slot-key.js';
|
|
|
16
16
|
* (`sampleAt`), and that sample flows to the container's `onTrackerChanged` —
|
|
17
17
|
* the nearest-point readout. Scatter reuses the shared tracker rather than
|
|
18
18
|
* adding a separate `onNearest` channel, so a scatter reads out exactly like a
|
|
19
|
-
* line. Click selection hit-tests each point's disc (`hitTest`)
|
|
20
|
-
* point (matching
|
|
19
|
+
* line. Click selection hit-tests each point's disc (`hitTest`) — **opt-in via
|
|
20
|
+
* `id`**; the selected point (matching the selection's series `id` and the sample
|
|
21
|
+
* `key`) gets a highlight ring. Without an `id` the scatter is display-only.
|
|
21
22
|
*
|
|
22
23
|
* ```tsx
|
|
23
24
|
* <Layers>
|
|
@@ -30,7 +31,7 @@ import { useSlotKey } from './use-slot-key.js';
|
|
|
30
31
|
* </Layers>
|
|
31
32
|
* ```
|
|
32
33
|
*/
|
|
33
|
-
export function ScatterChart({ series, column, as: semantic, axis, radius, color, label, index = 0, }) {
|
|
34
|
+
export function ScatterChart({ series, column, as: semantic, id, axis, radius, color, label, index = 0, }) {
|
|
34
35
|
const container = useContext(ContainerContext);
|
|
35
36
|
if (container === null) {
|
|
36
37
|
throw new Error('<ScatterChart> must be rendered inside a <ChartContainer>');
|
|
@@ -101,8 +102,15 @@ export function ScatterChart({ series, column, as: semantic, axis, radius, color
|
|
|
101
102
|
},
|
|
102
103
|
];
|
|
103
104
|
},
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
// `id` gates interactivity: only an id-bearing layer wires a hitTest, so
|
|
106
|
+
// a no-id scatter is display-only (a click on it resolves to empty space).
|
|
107
|
+
// Omit the key entirely when there's no id (exactOptionalPropertyTypes).
|
|
108
|
+
...(id === undefined
|
|
109
|
+
? {}
|
|
110
|
+
: {
|
|
111
|
+
hitTest: (px, py, xScale, yScale) => hitTestScatter(cs, px, py, xScale, yScale, encoding, keyAt, id, seriesLabel),
|
|
112
|
+
}),
|
|
113
|
+
draw: (ctx, xScale, yScale) => drawScatter(ctx, cs, xScale, yScale, style, encoding, keyAt, labelAt, font, container.selected, id),
|
|
106
114
|
},
|
|
107
115
|
axisId: axis,
|
|
108
116
|
index,
|
|
@@ -112,6 +120,7 @@ export function ScatterChart({ series, column, as: semantic, axis, radius, color
|
|
|
112
120
|
column,
|
|
113
121
|
style,
|
|
114
122
|
seriesLabel,
|
|
123
|
+
id,
|
|
115
124
|
encoding,
|
|
116
125
|
keyAt,
|
|
117
126
|
labelAt,
|
|
@@ -134,6 +143,15 @@ export function ScatterChart({ series, column, as: semantic, axis, radius, color
|
|
|
134
143
|
useEffect(() => {
|
|
135
144
|
registerTrackerSource(slot, entry.layer);
|
|
136
145
|
}, [registerTrackerSource, slot, entry.layer]);
|
|
146
|
+
// Advertise selectability (only when an `id` was given) so the container can
|
|
147
|
+
// warn if selection is wired but nothing is selectable.
|
|
148
|
+
const { registerSelectable, unregisterSelectable } = container;
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (id === undefined)
|
|
151
|
+
return;
|
|
152
|
+
registerSelectable(slot);
|
|
153
|
+
return () => unregisterSelectable(slot);
|
|
154
|
+
}, [registerSelectable, unregisterSelectable, slot, id]);
|
|
137
155
|
return null;
|
|
138
156
|
}
|
|
139
157
|
//# sourceMappingURL=ScatterChart.js.map
|
package/dist/bars.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import type { BarSeries } from './data.js';
|
|
1
|
+
import type { BarSeries, StackedBarSeries } from './data.js';
|
|
2
2
|
import type { Scale } from './line.js';
|
|
3
3
|
import type { BarStyle } from './theme.js';
|
|
4
|
+
/**
|
|
5
|
+
* Bar growth direction — the histogram orientation. `'vertical'` bars grow **up**
|
|
6
|
+
* from a value baseline, bins on the x axis (the column / time-bucket look);
|
|
7
|
+
* `'horizontal'` bars grow **right**, bins on the y axis (the band look, e.g.
|
|
8
|
+
* heart-rate zones). The stacked geometry below transposes on this alone — the
|
|
9
|
+
* {@link StackedBarSeries} data is identical for both.
|
|
10
|
+
*/
|
|
11
|
+
export type Orientation = 'vertical' | 'horizontal';
|
|
4
12
|
/**
|
|
5
13
|
* The `[min, max]` vertical extent the bars occupy — the finite values of `cs.y`
|
|
6
14
|
* **widened to include `0`**, since a bar spans from its value to the baseline
|
|
@@ -49,7 +57,8 @@ export declare function barRect(cs: BarSeries, i: number, xScale: Scale, yScale:
|
|
|
49
57
|
* (inset by `gapPx`) from the resolved `baseline` to the value.
|
|
50
58
|
*
|
|
51
59
|
* A gap (non-finite value) is skipped — no bar, no zero-height sliver. A bar
|
|
52
|
-
* matching the current `selection` (same `
|
|
60
|
+
* matching the current `selection` (same sample `key` **and** the layer's own
|
|
61
|
+
* series `id` — `seriesId`; a no-id layer passes `undefined` and never matches)
|
|
53
62
|
* draws in the style's `highlight` colour **and outlined**, so a click reads back
|
|
54
63
|
* on the canvas; a bar matching `hovered` draws in `highlight` **without** the
|
|
55
64
|
* outline (a lighter "this bar is live" on pointer-over); all others use the flat
|
|
@@ -59,12 +68,12 @@ export declare function barRect(cs: BarSeries, i: number, xScale: Scale, yScale:
|
|
|
59
68
|
* O(N) over the events, one fill (+ optional stroke) per bar, no per-bar
|
|
60
69
|
* allocation beyond the rect tuple.
|
|
61
70
|
*/
|
|
62
|
-
export declare function drawBars(ctx: CanvasRenderingContext2D, cs: BarSeries, xScale: Scale, yScale: Scale, style: BarStyle, baseline: number, gapPx: number,
|
|
71
|
+
export declare function drawBars(ctx: CanvasRenderingContext2D, cs: BarSeries, xScale: Scale, yScale: Scale, style: BarStyle, baseline: number, gapPx: number, seriesId: string | undefined, selection: {
|
|
63
72
|
key: number;
|
|
64
|
-
|
|
73
|
+
id: string;
|
|
65
74
|
} | null, hovered: {
|
|
66
75
|
key: number;
|
|
67
|
-
|
|
76
|
+
id: string;
|
|
68
77
|
} | null): void;
|
|
69
78
|
/**
|
|
70
79
|
* The index of the bar whose key span `[begin, end]` contains `time` — the bar
|
|
@@ -93,4 +102,86 @@ export declare function barIndexAtTime(cs: BarSeries, time: number): number;
|
|
|
93
102
|
* series, so "first match" is unambiguous in practice.
|
|
94
103
|
*/
|
|
95
104
|
export declare function barAt(cs: BarSeries, px: number, py: number, xScale: Scale, yScale: Scale, baseline: number, gapPx: number, minWidthPx: number): [index: number, begin: number, value: number] | null;
|
|
105
|
+
/**
|
|
106
|
+
* A resolved per-group stack style: `fills` aligned index-for-index to
|
|
107
|
+
* {@link StackedBarSeries.groups} (segment `g` uses `fills[g]`), plus the shared
|
|
108
|
+
* `opacity` (applied to every resting segment) and `outlineWidth` (the selected
|
|
109
|
+
* segment's stroke). Assembled by `BarChart` from the theme's `bar` style + the
|
|
110
|
+
* `colors` override, so the draw layer stays theme-free (unit-testable).
|
|
111
|
+
*
|
|
112
|
+
* There is no separate highlight colour: a hovered / selected segment pops by
|
|
113
|
+
* drawing its **own** `fill` at full opacity (and, when selected, an outline in
|
|
114
|
+
* that same colour). Colour-agnostic, so it reads correctly whatever palette the
|
|
115
|
+
* `colors` override supplies.
|
|
116
|
+
*/
|
|
117
|
+
export interface StackStyle {
|
|
118
|
+
readonly fills: readonly string[];
|
|
119
|
+
readonly opacity: number;
|
|
120
|
+
readonly outlineWidth: number;
|
|
121
|
+
}
|
|
122
|
+
/** The narrowed selection / hover identity a stacked segment matches against:
|
|
123
|
+
* the series `id`, the bin's `begin` (its `key`), and the group (its `label`). */
|
|
124
|
+
export interface StackMark {
|
|
125
|
+
readonly id: string;
|
|
126
|
+
readonly key: number;
|
|
127
|
+
readonly label: string;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The `[min, max]` extent of the **value (stacked) axis** — always `[0, maxTotal]`,
|
|
131
|
+
* where `maxTotal` is the tallest bin's summed finite non-negative segments. `0` is
|
|
132
|
+
* pulled in so the stack rests on a visible baseline (the bar analog of
|
|
133
|
+
* {@link barExtent}). An empty / all-gap series returns `[0, 1]` so the axis still
|
|
134
|
+
* has a usable domain. Feeds the y auto-fit for a vertical histogram, the x
|
|
135
|
+
* auto-fit for a horizontal one.
|
|
136
|
+
*/
|
|
137
|
+
export declare function stackValueExtent(ss: StackedBarSeries): [number, number];
|
|
138
|
+
/**
|
|
139
|
+
* The `[min, max]` extent of the **bin axis** — the first bin's `begin` to the
|
|
140
|
+
* last bin's `end` (the slots are ascending). `null` for an empty series. Feeds
|
|
141
|
+
* the x auto-fit for a vertical histogram, the y auto-fit for a horizontal one.
|
|
142
|
+
*/
|
|
143
|
+
export declare function stackBinExtent(ss: StackedBarSeries): [number, number] | null;
|
|
144
|
+
/**
|
|
145
|
+
* The pixel rect `[x0, x1, yTop, yBottom]` (ascending on both axes) of bin `b`'s
|
|
146
|
+
* segment `g`, stacked so it sits atop `cumBefore` (the summed value of the
|
|
147
|
+
* segments below it, in value units). `null` for a gap (see below). Transposes on
|
|
148
|
+
* `orientation`:
|
|
149
|
+
*
|
|
150
|
+
* - **vertical** — the bin span is horizontal (`barSpanPx` on `xScale`); the
|
|
151
|
+
* segment runs vertically from `yScale(cumBefore)` to `yScale(cumBefore + v)`.
|
|
152
|
+
* - **horizontal** — the bin span is vertical (`barSpanPx` on `yScale`); the
|
|
153
|
+
* segment runs horizontally from `xScale(cumBefore)` to `xScale(cumBefore + v)`.
|
|
154
|
+
*
|
|
155
|
+
* `null` for a **gap** — a non-finite, negative, **or zero** value: none of them
|
|
156
|
+
* draw (a zero segment has no extent), and each contributes nothing to the running
|
|
157
|
+
* total. `minSpanPx` floors the **bin** span (bar thickness); the value direction
|
|
158
|
+
* is unfloored. Shared by {@link drawStacks} and {@link stackAt} so the drawn rect
|
|
159
|
+
* and the hit rect are identical.
|
|
160
|
+
*/
|
|
161
|
+
export declare function segmentRect(ss: StackedBarSeries, b: number, g: number, orientation: Orientation, xScale: Scale, yScale: Scale, cumBefore: number, gapPx: number, minSpanPx: number): [x0: number, x1: number, yTop: number, yBottom: number] | null;
|
|
162
|
+
/**
|
|
163
|
+
* Fill every segment of every bin in `ss`, stacking each bin's groups from the
|
|
164
|
+
* value baseline outward (bottom → top vertical, left → right horizontal). A gap
|
|
165
|
+
* (non-finite / negative) segment is skipped and adds nothing to the running
|
|
166
|
+
* total, so the segments above it close the space. A segment matching the current
|
|
167
|
+
* `selection` (same series `id`, bin `key` **and** group `label`) draws in its
|
|
168
|
+
* group's `highlight` **and** outlined; one matching `hover` draws in `highlight`
|
|
169
|
+
* without the outline; all others use the flat `fill`. `globalAlpha` carries the
|
|
170
|
+
* shared opacity and is restored.
|
|
171
|
+
*
|
|
172
|
+
* O(N·G) over bins × groups, one fill (+ optional stroke) per drawn segment.
|
|
173
|
+
*/
|
|
174
|
+
export declare function drawStacks(ctx: CanvasRenderingContext2D, ss: StackedBarSeries, orientation: Orientation, xScale: Scale, yScale: Scale, style: StackStyle, gapPx: number, minSpanPx: number, seriesId: string | undefined, selection: StackMark | null, hover: StackMark | null): void;
|
|
175
|
+
/**
|
|
176
|
+
* Hit-test plot-pixel `(px, py)` against `ss`'s stacked segments — the **first**
|
|
177
|
+
* segment whose rect contains the point, or `null`. The geometry is
|
|
178
|
+
* {@link segmentRect}, so the hit rect is exactly the drawn rect. The returned
|
|
179
|
+
* tuple is `[bin, group, begin, groupName, value]` for the chart to assemble a
|
|
180
|
+
* `SelectInfo` (it owns the colour). Orientation-agnostic — it reads `(px, py)`,
|
|
181
|
+
* so a horizontal histogram hit-tests the same way a vertical one does.
|
|
182
|
+
*
|
|
183
|
+
* O(N·G) over bins × groups (no spatial index — histogram bin/group counts are
|
|
184
|
+
* small; click / hover are cheap events).
|
|
185
|
+
*/
|
|
186
|
+
export declare function stackAt(ss: StackedBarSeries, px: number, py: number, orientation: Orientation, xScale: Scale, yScale: Scale, gapPx: number, minSpanPx: number): [bin: number, group: number, begin: number, name: string, value: number] | null;
|
|
96
187
|
//# sourceMappingURL=bars.d.ts.map
|