@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.
@@ -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.key === hit.key &&
201
- prev.label === hit.label);
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 matches the axes (`YAxis`/`TimeAxis`) so they align. */
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 xTicks = xScale.ticks(GRID_TICKS).map((d) => xScale(d));
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
- const span = drag.startRange[1] - drag.startRange[0];
300
- const dt = c.plotWidth > 0 ? -dx * (span / c.plotWidth) : 0;
301
- c.applyRange(panRange(drag.startRange, dt));
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(zoomRange(c.timeRange, pivot, factor, c.minDuration));
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);
@@ -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`); the selected
70
- * point (matching both its key and this series' label) gets a highlight ring.
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
@@ -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`); the selected
20
- * point (matching both its key and this series' label) gets a highlight ring.
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
- hitTest: (px, py, xScale, yScale) => hitTestScatter(cs, px, py, xScale, yScale, encoding, keyAt, seriesLabel),
105
- draw: (ctx, xScale, yScale) => drawScatter(ctx, cs, xScale, yScale, style, encoding, keyAt, labelAt, font, container.selected, seriesLabel),
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 `begin` **and** the layer's own `label`)
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, label: string, selection: {
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
- label: string;
73
+ id: string;
65
74
  } | null, hovered: {
66
75
  key: number;
67
- label: string;
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