@pond-ts/charts 0.48.1 → 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 +200 -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/Layers.js
CHANGED
|
@@ -7,12 +7,12 @@ import { resolveSelection } from './select.js';
|
|
|
7
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
|
-
/** **
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* width-derived on a trading-time axis. */
|
|
10
|
+
/** Fallback **y**-gridline tick count, used only before the row publishes its
|
|
11
|
+
* resolved `tickCounts` (pre-registration). Normally the gridlines read the
|
|
12
|
+
* default axis's resolved count from `row.tickCounts` — the same value the
|
|
13
|
+
* `<YAxis>` labels use — so they line up by construction (was three hardcoded
|
|
14
|
+
* `5`s agreeing by convention). The **x** side reads the container's shared
|
|
15
|
+
* `xTickCount`, width-derived on a trading-time axis. */
|
|
16
16
|
const GRID_TICKS = 5;
|
|
17
17
|
/** Minimum px between `'labeled'` session dividers — thins dense collapse
|
|
18
18
|
* points (e.g. a daily chart where every candle is a new session) so the axis
|
|
@@ -79,7 +79,7 @@ export function Layers({ children }) {
|
|
|
79
79
|
}), [row.registerLayer, row.unregisterLayer]);
|
|
80
80
|
const background = container.theme.background;
|
|
81
81
|
const { grid: gridColor, gridDash } = container.theme.axis;
|
|
82
|
-
const { layers, yScales, formats, defaultAxisId, tickValues, axisSides } = row;
|
|
82
|
+
const { layers, yScales, formats, defaultAxisId, tickValues, tickCounts, axisSides, } = row;
|
|
83
83
|
// x geometry is shared and lives on the container (uniform across rows), and
|
|
84
84
|
// so is the x tick count — vertical gridlines must sit under the `<XAxis>`
|
|
85
85
|
// labels, which pass the same `xTickCount` to the same scale.
|
|
@@ -101,8 +101,12 @@ export function Layers({ children }) {
|
|
|
101
101
|
// The reference grid — behind the data, opt-out via `grid={false}` for
|
|
102
102
|
// a clean backdrop (session dividers below stay independent of it).
|
|
103
103
|
if (container.grid) {
|
|
104
|
+
// Auto gridlines use the default axis's RESOLVED tick count (the row's
|
|
105
|
+
// single source, height-derived or an explicit `<YAxis tickCount>`), so
|
|
106
|
+
// a gridline sits under every `<YAxis>` label and no more.
|
|
107
|
+
const yCount = tickCounts.get(defaultAxisId) ?? GRID_TICKS;
|
|
104
108
|
const yTicks = gridY
|
|
105
|
-
? (explicitY ?? gridY.ticks(
|
|
109
|
+
? (explicitY ?? gridY.ticks(yCount)).map((t) => gridY(t))
|
|
106
110
|
: [];
|
|
107
111
|
// On a calendar axis the verticals are the FULL grain populations —
|
|
108
112
|
// every day in the month, every month in the year, every aligned
|
|
@@ -213,6 +217,7 @@ export function Layers({ children }) {
|
|
|
213
217
|
xTickCount,
|
|
214
218
|
defaultAxisId,
|
|
215
219
|
tickValues,
|
|
220
|
+
tickCounts,
|
|
216
221
|
background,
|
|
217
222
|
gridColor,
|
|
218
223
|
gridDash,
|
|
@@ -347,6 +352,16 @@ export function Layers({ children }) {
|
|
|
347
352
|
const [createPt, setCreatePt] = useState(null);
|
|
348
353
|
const [drawFrom, setDrawFrom] = useState(null);
|
|
349
354
|
const drawFromRef = useRef(null);
|
|
355
|
+
// The region-select drag anchor, mirrored for the gesture handlers (the same
|
|
356
|
+
// ref+state discipline as `drawFromRef`): the container's `regionAnchor`
|
|
357
|
+
// STATE is only how the rows paint the band; the gesture logic must never
|
|
358
|
+
// read it back, because a batched pointer stream (automation, jsdom, a very
|
|
359
|
+
// fast flick under load) delivers down→up before the down's setState commits
|
|
360
|
+
// — the up would see `regionAnchor === null`, silently drop the select, and
|
|
361
|
+
// the late-committing anchor would then stick (#508 item 7). Trusted
|
|
362
|
+
// human-paced input hides this (React flushes trusted discrete events
|
|
363
|
+
// synchronously); the ref is correct under both.
|
|
364
|
+
const regionAnchorRef = useRef(null);
|
|
350
365
|
const handlePointerDown = useCallback((e) => {
|
|
351
366
|
clickStartRef.current = { x: e.clientX, y: e.clientY };
|
|
352
367
|
const c = containerRef.current;
|
|
@@ -380,7 +395,8 @@ export function Layers({ children }) {
|
|
|
380
395
|
const needsShift = c.regionSelectModifier === 'shift' && c.panZoom;
|
|
381
396
|
if (!needsShift || e.shiftKey) {
|
|
382
397
|
const px = Math.max(0, Math.min(c.plotWidth, e.clientX - e.currentTarget.getBoundingClientRect().left));
|
|
383
|
-
|
|
398
|
+
regionAnchorRef.current = +c.xScale.invert(px);
|
|
399
|
+
c.setRegionAnchor(regionAnchorRef.current); // paint-only mirror
|
|
384
400
|
c.setHoverX(px);
|
|
385
401
|
try {
|
|
386
402
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
@@ -423,8 +439,9 @@ export function Layers({ children }) {
|
|
|
423
439
|
return;
|
|
424
440
|
}
|
|
425
441
|
// Region drag in progress: just track the pointer x (the band spans from the
|
|
426
|
-
// anchor bucket to here); no pan, no hover hit-test.
|
|
427
|
-
|
|
442
|
+
// anchor bucket to here); no pan, no hover hit-test. Gesture truth is the
|
|
443
|
+
// ref — the state mirror may not have committed yet (see regionAnchorRef).
|
|
444
|
+
if (regionAnchorRef.current !== null) {
|
|
428
445
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
429
446
|
c.setHoverX(Math.max(0, Math.min(c.plotWidth, e.clientX - rect.left)));
|
|
430
447
|
return;
|
|
@@ -516,9 +533,14 @@ export function Layers({ children }) {
|
|
|
516
533
|
const c = containerRef.current;
|
|
517
534
|
// End a region drag: commit the anchor→pointer span as a one-shot range,
|
|
518
535
|
// then clear the anchor (the cursor reverts to the single-bucket highlight).
|
|
519
|
-
|
|
536
|
+
// The anchor is read from the ref, never the state mirror — under a batched
|
|
537
|
+
// pointer stream the state hasn't committed yet and the select would be
|
|
538
|
+
// silently dropped (and the anchor stuck). See regionAnchorRef.
|
|
539
|
+
if (regionAnchorRef.current !== null) {
|
|
540
|
+
const anchor = regionAnchorRef.current;
|
|
541
|
+
regionAnchorRef.current = null;
|
|
520
542
|
const px = Math.max(0, Math.min(c.plotWidth, e.clientX - e.currentTarget.getBoundingClientRect().left));
|
|
521
|
-
const span = regionSpan(c.cursorBuckets ?? [],
|
|
543
|
+
const span = regionSpan(c.cursorBuckets ?? [], anchor, +c.xScale.invert(px));
|
|
522
544
|
c.setRegionAnchor(null);
|
|
523
545
|
try {
|
|
524
546
|
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
@@ -599,6 +621,7 @@ export function Layers({ children }) {
|
|
|
599
621
|
}
|
|
600
622
|
// Cancel a region-drag on leave (no commit) — a safety net for the rare case
|
|
601
623
|
// where the pointer capture didn't take, so the anchor can't get stuck.
|
|
624
|
+
regionAnchorRef.current = null;
|
|
602
625
|
if (c.regionAnchor !== null)
|
|
603
626
|
c.setRegionAnchor(null);
|
|
604
627
|
c.setHoverX(null);
|
|
@@ -838,7 +861,7 @@ export function Layers({ children }) {
|
|
|
838
861
|
? `${plotWidth - timeX + 4}px`
|
|
839
862
|
: undefined,
|
|
840
863
|
color: cursorColor,
|
|
841
|
-
}, children: formatTime(cursorTime) })), parts.chip === 'inline' &&
|
|
864
|
+
}, children: (container.formatReadout ?? formatTime)(cursorTime) })), parts.chip === 'inline' &&
|
|
842
865
|
trackerSamples.map((s, i) => {
|
|
843
866
|
// Flip the chip left of its dot near the right edge so it stays in-plot.
|
|
844
867
|
const flip = s.px > plotWidth * LABEL_FLIP_FRACTION;
|
package/dist/Legend.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { LegendItemInput } from './swatch.js';
|
|
2
|
+
import { type ChartTheme } from './theme.js';
|
|
3
|
+
/** Where the in-container legend card anchors, relative to the rows block. */
|
|
4
|
+
export type LegendPlacement = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
5
|
+
export interface LegendProps {
|
|
6
|
+
/**
|
|
7
|
+
* Which corner of the **rows block** the card anchors to (8px inset).
|
|
8
|
+
* **Omitted ⇒ `'top-right'`.** Ignored in standalone `items` mode outside a
|
|
9
|
+
* container, where the card renders in normal flow (the consumer places it).
|
|
10
|
+
*/
|
|
11
|
+
placement?: LegendPlacement;
|
|
12
|
+
/**
|
|
13
|
+
* **Escape hatch — explicit rows.** Renders exactly these (in order, no
|
|
14
|
+
* dedup) instead of the container's registry, and works **outside** a
|
|
15
|
+
* `<ChartContainer>` (a dashboard-side key). Each row is a `label` +
|
|
16
|
+
* resolved {@link SwatchSpec} (+ optional `id` for the interactions).
|
|
17
|
+
*/
|
|
18
|
+
items?: readonly LegendItemInput[];
|
|
19
|
+
/**
|
|
20
|
+
* Row click. **Omitted ⇒ the id-gated default:** a row whose layer has an
|
|
21
|
+
* `id` toggles the container selection (the same `select()` path a mark
|
|
22
|
+
* click uses); rows without an `id` are inert. Provide to take over (e.g.
|
|
23
|
+
* a consumer-side show/hide toggle — visibility stays consumer-side by
|
|
24
|
+
* design, a legend mutating composition would be a second styling channel).
|
|
25
|
+
*/
|
|
26
|
+
onRowClick?: (row: LegendItemInput) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Row hover (`null` on leave). **Omitted ⇒ the id-gated default:** hovering
|
|
29
|
+
* a row with an `id` echoes into the container's `hovered` channel — the
|
|
30
|
+
* same one an in-plot hover drives — and clears on leave.
|
|
31
|
+
*/
|
|
32
|
+
onRowHover?: (row: LegendItemInput | null) => void;
|
|
33
|
+
/** Theme for the **standalone** (`items`, outside-a-container) mode, where
|
|
34
|
+
* there is no frame to read one from. **Omitted ⇒ the container's theme,
|
|
35
|
+
* else {@link defaultTheme}.** */
|
|
36
|
+
theme?: ChartTheme;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The **series key** — one row per registered draw layer: a swatch of the
|
|
40
|
+
* layer's *resolved* style (so the key can never drift from the plot) and its
|
|
41
|
+
* readout identity (`as ?? column`, or the layer's `legend="name"` override).
|
|
42
|
+
* A child of {@link ChartContainer} (anywhere — it reads the registry, not its
|
|
43
|
+
* position): renders a small card anchored to a corner of the rows block.
|
|
44
|
+
*
|
|
45
|
+
* - **Rows** follow chart-row order, then declaration order (the z-order
|
|
46
|
+
* convention); two layers sharing an identity (`id ?? label`) collapse to
|
|
47
|
+
* one row, exactly as the tracker readout merges keys.
|
|
48
|
+
* - **A layer opts out** with `legend={false}`, or renames its row with
|
|
49
|
+
* `legend="Display name"`.
|
|
50
|
+
* - **Interactivity is id-gated** (the selection contract): rows whose layer
|
|
51
|
+
* has an `id` echo hover into the container and toggle selection on click;
|
|
52
|
+
* `onRowHover` / `onRowClick` take over when provided. The selected row
|
|
53
|
+
* reads emphasized; the hovered row tints.
|
|
54
|
+
* - **Scope follows placement:** at the container level it lists **all** rows;
|
|
55
|
+
* placed inside a `<Layers>` it **scopes to that `<ChartRow>`** and anchors
|
|
56
|
+
* to that row's plot (like an annotation) — a per-row legend needs no prop.
|
|
57
|
+
* - **Standalone mode:** `<Legend items={…}>` renders explicit rows — inside
|
|
58
|
+
* a container (replacing the registry) or entirely outside one (a
|
|
59
|
+
* dashboard-side key; pass `theme` there).
|
|
60
|
+
*/
|
|
61
|
+
export declare function Legend({ placement, items, onRowClick, onRowHover, theme: themeProp, }: LegendProps): import("react/jsx-runtime").JSX.Element | null;
|
|
62
|
+
//# sourceMappingURL=Legend.d.ts.map
|
package/dist/Legend.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useContext } from 'react';
|
|
3
|
+
import { ContainerContext, RowContext } from './context.js';
|
|
4
|
+
import { buildChartLegend } from './useChartLegend.js';
|
|
5
|
+
import { defaultTheme } from './theme.js';
|
|
6
|
+
/** One 20×12 swatch glyph, drawn from the layer's resolved style. */
|
|
7
|
+
function SwatchGlyph({ spec }) {
|
|
8
|
+
const w = 20;
|
|
9
|
+
const h = 12;
|
|
10
|
+
const mid = h / 2;
|
|
11
|
+
switch (spec.kind) {
|
|
12
|
+
case 'line': {
|
|
13
|
+
const sw = Math.min(spec.width, 4);
|
|
14
|
+
// A dashed series hand-renders as exactly THREE dashes — a canonical
|
|
15
|
+
// "dashed" glyph at swatch scale — rather than scaling the layer's own
|
|
16
|
+
// dash cadence into 20px (which can alias to 1 or 7 dashes and stop
|
|
17
|
+
// reading as dashed at all). Colour/width stay the resolved style.
|
|
18
|
+
if (spec.dash && spec.dash.length > 0) {
|
|
19
|
+
return (_jsxs("g", { stroke: spec.color, strokeWidth: sw, strokeLinecap: "butt", children: [_jsx("line", { x1: 0, y1: mid, x2: 4.6, y2: mid }), _jsx("line", { x1: 7.7, y1: mid, x2: 12.3, y2: mid }), _jsx("line", { x1: 15.4, y1: mid, x2: w, y2: mid })] }));
|
|
20
|
+
}
|
|
21
|
+
return (_jsx("line", { x1: 0, y1: mid, x2: w, y2: mid, stroke: spec.color, strokeWidth: sw }));
|
|
22
|
+
}
|
|
23
|
+
case 'area':
|
|
24
|
+
return (_jsxs("g", { children: [_jsx("rect", { x: 0, y: 3, width: w, height: h - 3, fill: spec.fill, fillOpacity: spec.fillOpacity }), _jsx("line", { x1: 0, y1: 3, x2: w, y2: 3, stroke: spec.line, strokeWidth: 1.5 })] }));
|
|
25
|
+
case 'band':
|
|
26
|
+
return (_jsx("rect", { x: 0, y: 2, width: w, height: h - 4, fill: spec.fill, fillOpacity: spec.opacity }));
|
|
27
|
+
case 'scatter': {
|
|
28
|
+
const r = Math.min(spec.radius, 5);
|
|
29
|
+
return (_jsx("circle", { cx: w / 2, cy: mid, r: r, fill: spec.color, stroke: spec.outline, strokeWidth: spec.outline !== undefined ? 1 : 0 }));
|
|
30
|
+
}
|
|
31
|
+
case 'box': {
|
|
32
|
+
const sw = Math.min(spec.whiskerWidth, 3);
|
|
33
|
+
return (_jsxs("g", { stroke: spec.whisker, strokeWidth: sw, children: [_jsx("line", { x1: w / 2, y1: 1, x2: w / 2, y2: h - 1 }), _jsx("line", { x1: w / 2 - 4, y1: 1, x2: w / 2 + 4, y2: 1 }), _jsx("line", { x1: w / 2 - 4, y1: h - 1, x2: w / 2 + 4, y2: h - 1 })] }));
|
|
34
|
+
}
|
|
35
|
+
case 'bar':
|
|
36
|
+
// A centred rounded square — reads as "a fill" without pretending to be
|
|
37
|
+
// a bar of any particular width (#512 follow-up feedback, second pass:
|
|
38
|
+
// centred in the swatch box like the other glyphs).
|
|
39
|
+
return (_jsx("rect", { x: (w - 10) / 2, y: 1, width: 10, height: 10, rx: 2, fill: spec.fill }));
|
|
40
|
+
case 'candle':
|
|
41
|
+
return (_jsxs("g", { children: [_jsx("line", { x1: 5.5, y1: 0, x2: 5.5, y2: h, stroke: spec.up, strokeWidth: 1 }), _jsx("rect", { x: 3, y: 2, width: 5, height: h - 5, fill: spec.up }), _jsx("line", { x1: 14.5, y1: 0, x2: 14.5, y2: h, stroke: spec.down, strokeWidth: 1 }), _jsx("rect", { x: 12, y: 3, width: 5, height: h - 5, fill: spec.down })] }));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** The card's corner offsets — anchored to the **plot area**, not the rows
|
|
45
|
+
* block: the horizontal inset adds the container's axis gutter on that side,
|
|
46
|
+
* so a left placement never sits over the y-axis labels (#512 follow-up
|
|
47
|
+
* feedback). Vertically the rows block already IS the plot (the x-axis strip
|
|
48
|
+
* renders outside it). */
|
|
49
|
+
function placementStyle(placement, leftGutter, rightGutter) {
|
|
50
|
+
switch (placement) {
|
|
51
|
+
case 'top-left':
|
|
52
|
+
return { top: 8, left: leftGutter + 8 };
|
|
53
|
+
case 'top-right':
|
|
54
|
+
return { top: 8, right: rightGutter + 8 };
|
|
55
|
+
case 'bottom-left':
|
|
56
|
+
return { bottom: 8, left: leftGutter + 8 };
|
|
57
|
+
case 'bottom-right':
|
|
58
|
+
return { bottom: 8, right: rightGutter + 8 };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The **series key** — one row per registered draw layer: a swatch of the
|
|
63
|
+
* layer's *resolved* style (so the key can never drift from the plot) and its
|
|
64
|
+
* readout identity (`as ?? column`, or the layer's `legend="name"` override).
|
|
65
|
+
* A child of {@link ChartContainer} (anywhere — it reads the registry, not its
|
|
66
|
+
* position): renders a small card anchored to a corner of the rows block.
|
|
67
|
+
*
|
|
68
|
+
* - **Rows** follow chart-row order, then declaration order (the z-order
|
|
69
|
+
* convention); two layers sharing an identity (`id ?? label`) collapse to
|
|
70
|
+
* one row, exactly as the tracker readout merges keys.
|
|
71
|
+
* - **A layer opts out** with `legend={false}`, or renames its row with
|
|
72
|
+
* `legend="Display name"`.
|
|
73
|
+
* - **Interactivity is id-gated** (the selection contract): rows whose layer
|
|
74
|
+
* has an `id` echo hover into the container and toggle selection on click;
|
|
75
|
+
* `onRowHover` / `onRowClick` take over when provided. The selected row
|
|
76
|
+
* reads emphasized; the hovered row tints.
|
|
77
|
+
* - **Scope follows placement:** at the container level it lists **all** rows;
|
|
78
|
+
* placed inside a `<Layers>` it **scopes to that `<ChartRow>`** and anchors
|
|
79
|
+
* to that row's plot (like an annotation) — a per-row legend needs no prop.
|
|
80
|
+
* - **Standalone mode:** `<Legend items={…}>` renders explicit rows — inside
|
|
81
|
+
* a container (replacing the registry) or entirely outside one (a
|
|
82
|
+
* dashboard-side key; pass `theme` there).
|
|
83
|
+
*/
|
|
84
|
+
export function Legend({ placement = 'top-right', items, onRowClick, onRowHover, theme: themeProp, }) {
|
|
85
|
+
const container = useContext(ContainerContext);
|
|
86
|
+
if (container === null && items === undefined) {
|
|
87
|
+
throw new Error('<Legend> must be inside a <ChartContainer> (or be given explicit `items`)');
|
|
88
|
+
}
|
|
89
|
+
// A RowContext in scope (the card is inside a <Layers>) narrows the registry
|
|
90
|
+
// to that row's layers AND means we're already inside the row's plot cell —
|
|
91
|
+
// so the corner inset drops the axis gutter (see placementStyle below).
|
|
92
|
+
const row = useContext(RowContext);
|
|
93
|
+
const theme = themeProp ?? container?.theme ?? defaultTheme;
|
|
94
|
+
const slot = theme.legend ?? {
|
|
95
|
+
background: theme.chip?.background ?? '#ffffff',
|
|
96
|
+
border: theme.axis.grid,
|
|
97
|
+
text: theme.axis.label,
|
|
98
|
+
};
|
|
99
|
+
// The shared headless core (also `useChartLegend`'s) — rows + the id-gated
|
|
100
|
+
// hover/select verbs — so the built-in card and a custom-rendered legend
|
|
101
|
+
// can never disagree. `null` in standalone `items` mode (no chart to sync).
|
|
102
|
+
const legend = container !== null ? buildChartLegend(container, row?.rowKey) : null;
|
|
103
|
+
// The card renders a flat item list — `rows` is grouped by chart row, so
|
|
104
|
+
// flatten it (a scoped legend has one group anyway).
|
|
105
|
+
const entries = items ?? legend.rows.flatMap((r) => r.items);
|
|
106
|
+
if (entries.length === 0)
|
|
107
|
+
return null;
|
|
108
|
+
// Selection reads by CONTRAST: the selected item goes bold and every other
|
|
109
|
+
// dulls (the ticker-compare treatment) — quieter and clearer than
|
|
110
|
+
// decorating the selected item itself. Only when the selection points at an
|
|
111
|
+
// item of THIS legend, so selecting an off-legend mark dulls nothing.
|
|
112
|
+
const selectedId = container?.selected?.id;
|
|
113
|
+
const anySelected = selectedId !== undefined && entries.some((it) => it.id === selectedId);
|
|
114
|
+
const enter = (item) => {
|
|
115
|
+
if (onRowHover)
|
|
116
|
+
return onRowHover(item);
|
|
117
|
+
legend?.hover(item);
|
|
118
|
+
};
|
|
119
|
+
const leave = (item) => {
|
|
120
|
+
if (onRowHover)
|
|
121
|
+
return onRowHover(null);
|
|
122
|
+
if (item.id !== undefined)
|
|
123
|
+
legend?.hover(null);
|
|
124
|
+
};
|
|
125
|
+
const click = (item) => {
|
|
126
|
+
if (onRowClick)
|
|
127
|
+
return onRowClick(item);
|
|
128
|
+
legend?.select(item);
|
|
129
|
+
};
|
|
130
|
+
// Standalone (no container): a normal-flow card the consumer places.
|
|
131
|
+
const positioned = container !== null;
|
|
132
|
+
return (_jsx("div", { "data-legend": "", style: {
|
|
133
|
+
...(positioned
|
|
134
|
+
? {
|
|
135
|
+
position: 'absolute',
|
|
136
|
+
// Row-scoped (inside the plot cell): no gutter inset — the cell
|
|
137
|
+
// already starts at the plot edge. Container-level: inset by the
|
|
138
|
+
// axis gutter so the card clears the y-axis labels.
|
|
139
|
+
...placementStyle(placement, row !== null ? 0 : container.leftGutter, row !== null ? 0 : container.rightGutter),
|
|
140
|
+
zIndex: 5,
|
|
141
|
+
}
|
|
142
|
+
: { display: 'inline-block' }),
|
|
143
|
+
background: slot.background,
|
|
144
|
+
border: `1px solid ${slot.border}`,
|
|
145
|
+
borderRadius: 4,
|
|
146
|
+
padding: '4px 8px',
|
|
147
|
+
font: `${theme.font.size}px ${theme.font.family}`,
|
|
148
|
+
color: slot.text,
|
|
149
|
+
lineHeight: 1.6,
|
|
150
|
+
}, children: entries.map((item) => {
|
|
151
|
+
const interactive = onRowClick !== undefined ||
|
|
152
|
+
onRowHover !== undefined ||
|
|
153
|
+
(item.id !== undefined && container !== null);
|
|
154
|
+
const selected = item.id !== undefined && container?.selected?.id === item.id;
|
|
155
|
+
const hovered = item.id !== undefined && container?.hovered?.id === item.id;
|
|
156
|
+
return (_jsxs("div", { onPointerEnter: () => enter(item), onPointerLeave: () => leave(item), onClick: () => click(item), style: {
|
|
157
|
+
display: 'flex',
|
|
158
|
+
alignItems: 'center',
|
|
159
|
+
gap: 6,
|
|
160
|
+
cursor: interactive ? 'pointer' : 'default',
|
|
161
|
+
fontWeight: selected ? 600 : 400,
|
|
162
|
+
opacity: anySelected && !selected ? 0.45 : 1,
|
|
163
|
+
background: hovered ? slot.border : 'transparent',
|
|
164
|
+
borderRadius: 2,
|
|
165
|
+
padding: '0 2px',
|
|
166
|
+
}, children: [_jsx("svg", { width: 20, height: 12, style: { flex: 'none' }, children: _jsx(SwatchGlyph, { spec: item.swatch }) }), _jsx("span", { children: item.label })] }, item.id !== undefined ? `${item.id}${item.label}` : item.label));
|
|
167
|
+
}) }));
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=Legend.js.map
|
package/dist/LineChart.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ValueSeries } from 'pond-ts';
|
|
2
2
|
import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
|
|
3
|
+
import type { DecimateOption } from './decimate.js';
|
|
3
4
|
import { type Curve } from './curve.js';
|
|
4
5
|
import { type GapMode } from './gaps.js';
|
|
5
6
|
export interface LineChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
|
|
@@ -63,6 +64,24 @@ export interface LineChartProps<S extends SeriesSchema = SeriesSchema, VS extend
|
|
|
63
64
|
* no-op on a continuous axis (no provider) or a provider without `boundaries`.
|
|
64
65
|
*/
|
|
65
66
|
sessionBreaks?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* **M4 viewport decimation** (charts decimator wave). **Omitted ⇒ `true`**:
|
|
69
|
+
* once the visible data is denser than ~2 samples per device pixel, the line
|
|
70
|
+
* is drawn from the per-pixel-column min/max/first/last (a pixel-identical
|
|
71
|
+
* polyline of O(plot width) points) instead of every sample — so a 1M-point
|
|
72
|
+
* series pans at interactive rates. It is **visually lossless** (a perf knob,
|
|
73
|
+
* not a style), and applies only to the honest default draw: a solid line with
|
|
74
|
+
* `gaps="empty"`, a linear `curve`, and no `sessionBreaks` (other modes draw
|
|
75
|
+
* full-resolution until later phases wire them). Pass `false` to always draw
|
|
76
|
+
* every point, or `{ threshold }` to tune the samples-per-pixel factor.
|
|
77
|
+
*/
|
|
78
|
+
decimate?: DecimateOption;
|
|
79
|
+
/**
|
|
80
|
+
* This layer's `<Legend>` row: `false` ⇒ no row (opt out), a string ⇒ the
|
|
81
|
+
* row's display name. **Omitted ⇒ a row named by the layer's readout
|
|
82
|
+
* identity** (`as` ?? `column`). The swatch is the resolved line style.
|
|
83
|
+
*/
|
|
84
|
+
legend?: boolean | string;
|
|
66
85
|
/**
|
|
67
86
|
* @internal Declaration position among the `<Layers>` children, injected by
|
|
68
87
|
* `Layers` so z-order follows JSX order. Do not set.
|
|
@@ -75,5 +94,5 @@ export interface LineChartProps<S extends SeriesSchema = SeriesSchema, VS extend
|
|
|
75
94
|
* (scaling against its `axis`), and renders nothing to the DOM — the row draws
|
|
76
95
|
* it. The line breaks at gaps rather than spanning them.
|
|
77
96
|
*/
|
|
78
|
-
export declare function LineChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, as: semantic, axis, curve, gaps, sessionBreaks, index, }: LineChartProps<S, VS>): null;
|
|
97
|
+
export declare function LineChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, as: semantic, axis, curve, gaps, sessionBreaks, decimate, legend, index, }: LineChartProps<S, VS>): null;
|
|
79
98
|
//# sourceMappingURL=LineChart.d.ts.map
|
package/dist/LineChart.js
CHANGED
|
@@ -5,6 +5,7 @@ import { drawLine, yExtent } from './line.js';
|
|
|
5
5
|
import { resolveCurve } from './curve.js';
|
|
6
6
|
import { DEFAULT_GAP_MODE, DEFAULT_GAP_CONNECTOR_OPACITY, } from './gaps.js';
|
|
7
7
|
import { ContainerContext, LayersContext } from './context.js';
|
|
8
|
+
import { legendLabelFor, useLegendItems, } from './swatch.js';
|
|
8
9
|
import { useSlotKey } from './use-slot-key.js';
|
|
9
10
|
/** Stable empty boundary list — so `sessionBreaks={false}` keeps a referentially
|
|
10
11
|
* constant array and the layer entry isn't rebuilt every render. */
|
|
@@ -15,7 +16,7 @@ const NO_BREAKS = [];
|
|
|
15
16
|
* (scaling against its `axis`), and renders nothing to the DOM — the row draws
|
|
16
17
|
* it. The line breaks at gaps rather than spanning them.
|
|
17
18
|
*/
|
|
18
|
-
export function LineChart({ series, column, as: semantic, axis, curve, gaps = DEFAULT_GAP_MODE, sessionBreaks = false, index = 0, }) {
|
|
19
|
+
export function LineChart({ series, column, as: semantic, axis, curve, gaps = DEFAULT_GAP_MODE, sessionBreaks = false, decimate = true, legend, index = 0, }) {
|
|
19
20
|
const container = useContext(ContainerContext);
|
|
20
21
|
if (container === null) {
|
|
21
22
|
throw new Error('<LineChart> must be rendered inside a <ChartContainer>');
|
|
@@ -81,7 +82,7 @@ export function LineChart({ series, column, as: semantic, axis, curve, gaps = DE
|
|
|
81
82
|
? [{ x: e.begin(), value: v, color: style.color, label }]
|
|
82
83
|
: [];
|
|
83
84
|
},
|
|
84
|
-
draw: (ctx, xScale, yScale) => drawLine(ctx, cs, xScale, yScale, style, curveFactory, gaps, gapConnectorOpacity, sessionBreakInstants),
|
|
85
|
+
draw: (ctx, xScale, yScale) => drawLine(ctx, cs, xScale, yScale, style, curveFactory, gaps, gapConnectorOpacity, sessionBreakInstants, decimate),
|
|
85
86
|
},
|
|
86
87
|
axisId: axis,
|
|
87
88
|
index,
|
|
@@ -95,6 +96,7 @@ export function LineChart({ series, column, as: semantic, axis, curve, gaps = DE
|
|
|
95
96
|
gaps,
|
|
96
97
|
gapConnectorOpacity,
|
|
97
98
|
sessionBreakInstants,
|
|
99
|
+
decimate,
|
|
98
100
|
axis,
|
|
99
101
|
index,
|
|
100
102
|
]);
|
|
@@ -115,6 +117,25 @@ export function LineChart({ series, column, as: semantic, axis, curve, gaps = DE
|
|
|
115
117
|
useEffect(() => {
|
|
116
118
|
registerTrackerSource(slot, entry.layer);
|
|
117
119
|
}, [registerTrackerSource, slot, entry.layer]);
|
|
120
|
+
// And a legend row: the readout identity + the resolved line style, so a
|
|
121
|
+
// `<Legend>` swatch can never drift from what the canvas draws.
|
|
122
|
+
const legendRows = useMemo(() => {
|
|
123
|
+
const name = legendLabelFor(legend, label);
|
|
124
|
+
return name === null
|
|
125
|
+
? null
|
|
126
|
+
: [
|
|
127
|
+
{
|
|
128
|
+
label: name,
|
|
129
|
+
swatch: {
|
|
130
|
+
kind: 'line',
|
|
131
|
+
color: style.color,
|
|
132
|
+
width: style.width,
|
|
133
|
+
dash: style.dash,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
}, [legend, label, style]);
|
|
138
|
+
useLegendItems(container, slot, index, legendRows);
|
|
118
139
|
return null;
|
|
119
140
|
}
|
|
120
141
|
//# sourceMappingURL=LineChart.js.map
|
package/dist/ScatterChart.d.ts
CHANGED
|
@@ -85,6 +85,13 @@ export interface ScatterChartProps<S extends SeriesSchema = SeriesSchema, VS ext
|
|
|
85
85
|
* together, so a nudged point still selects.
|
|
86
86
|
*/
|
|
87
87
|
offset?: number;
|
|
88
|
+
/**
|
|
89
|
+
* This layer's `<Legend>` row: `false` ⇒ no row (opt out), a string ⇒ the
|
|
90
|
+
* row's display name. **Omitted ⇒ a row named by the layer's readout
|
|
91
|
+
* identity** (`as` ?? `column`). The swatch is the resolved base dot style
|
|
92
|
+
* (a data-driven `radius`/`color` encoding shows its base, not the range).
|
|
93
|
+
*/
|
|
94
|
+
legend?: boolean | string;
|
|
88
95
|
/**
|
|
89
96
|
* @internal Declaration position among the `<Layers>` children, injected by
|
|
90
97
|
* `Layers` so z-order follows JSX order. Do not set.
|
|
@@ -119,5 +126,5 @@ export interface ScatterChartProps<S extends SeriesSchema = SeriesSchema, VS ext
|
|
|
119
126
|
* </Layers>
|
|
120
127
|
* ```
|
|
121
128
|
*/
|
|
122
|
-
export declare function ScatterChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, as: semantic, id, axis, radius, color, label, offset, index, }: ScatterChartProps<S, VS>): null;
|
|
129
|
+
export declare function ScatterChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, as: semantic, id, axis, radius, color, label, offset, legend, index, }: ScatterChartProps<S, VS>): null;
|
|
123
130
|
//# sourceMappingURL=ScatterChart.d.ts.map
|
package/dist/ScatterChart.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fromTimeSeries, fromValueSeries } from './data.js';
|
|
|
4
4
|
import { drawScatter, hitTestScatter, nearestIndex, scatterExtent, } from './scatter.js';
|
|
5
5
|
import { resolveEncoding, } from './encoding.js';
|
|
6
6
|
import { ContainerContext, LayersContext } from './context.js';
|
|
7
|
+
import { legendLabelFor, useLegendItems, } from './swatch.js';
|
|
7
8
|
import { useSlotKey } from './use-slot-key.js';
|
|
8
9
|
/**
|
|
9
10
|
* A scatter draw layer: one mark per finite point at `(x, column-value)`
|
|
@@ -33,7 +34,7 @@ import { useSlotKey } from './use-slot-key.js';
|
|
|
33
34
|
* </Layers>
|
|
34
35
|
* ```
|
|
35
36
|
*/
|
|
36
|
-
export function ScatterChart({ series, column, as: semantic, id, axis, radius, color, label, offset = 0, index = 0, }) {
|
|
37
|
+
export function ScatterChart({ series, column, as: semantic, id, axis, radius, color, label, offset = 0, legend, index = 0, }) {
|
|
37
38
|
const container = useContext(ContainerContext);
|
|
38
39
|
if (container === null) {
|
|
39
40
|
throw new Error('<ScatterChart> must be rendered inside a <ChartContainer>');
|
|
@@ -163,6 +164,28 @@ export function ScatterChart({ series, column, as: semantic, id, axis, radius, c
|
|
|
163
164
|
useEffect(() => {
|
|
164
165
|
registerTrackerSource(slot, entry.layer);
|
|
165
166
|
}, [registerTrackerSource, slot, entry.layer]);
|
|
167
|
+
// And a legend row: the readout identity + the resolved base dot (a fixed
|
|
168
|
+
// `radius` number shows at size; an encoding shows the style's base radius).
|
|
169
|
+
// Carries the layer's `id` so the legend's default interactions are id-gated
|
|
170
|
+
// exactly like the mark's own.
|
|
171
|
+
const legendRows = useMemo(() => {
|
|
172
|
+
const name = legendLabelFor(legend, seriesLabel);
|
|
173
|
+
return name === null
|
|
174
|
+
? null
|
|
175
|
+
: [
|
|
176
|
+
{
|
|
177
|
+
label: name,
|
|
178
|
+
id,
|
|
179
|
+
swatch: {
|
|
180
|
+
kind: 'scatter',
|
|
181
|
+
color: style.color,
|
|
182
|
+
radius: typeof radius === 'number' ? radius : style.radius,
|
|
183
|
+
outline: style.outline,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
];
|
|
187
|
+
}, [legend, seriesLabel, id, style, radius]);
|
|
188
|
+
useLegendItems(container, slot, index, legendRows);
|
|
166
189
|
// Advertise selectability (only when an `id` was given) so the container can
|
|
167
190
|
// warn if selection is wired but nothing is selectable.
|
|
168
191
|
const { registerSelectable, unregisterSelectable } = container;
|
package/dist/XAxis.js
CHANGED
|
@@ -158,6 +158,13 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
158
158
|
// Split from `fmt` so an axis's terse tick labels never leak into the cursor /
|
|
159
159
|
// marker readouts, which stay full timestamps.
|
|
160
160
|
const tickFmt = flatFmt ?? (stacked ? baseFmt : undefined) ?? fmt;
|
|
161
|
+
// The **readout** formatter — the cursor pill + marker indicator pills. A
|
|
162
|
+
// container `cursorFormat` wins even over an explicit axis `format` (pill
|
|
163
|
+
// precedence `cursorFormat → axis format → container`): the readout is its
|
|
164
|
+
// own channel, so precise-pill-over-terse-ticks works on any axis kind. A
|
|
165
|
+
// `transform`ed axis is exempt — its pill speaks the derived unit, which a
|
|
166
|
+
// data-unit `cursorFormat` can't address.
|
|
167
|
+
const readoutFmt = transform === undefined ? (container.formatReadout ?? fmt) : fmt;
|
|
161
168
|
// Marker annotations that opted into an axis indicator (`<Marker indicator>`)
|
|
162
169
|
// pin their **time** to this shared x-axis — a pill at `at`, in the annotation
|
|
163
170
|
// colour, reading like a tick. An indicator always shows the axis coordinate
|
|
@@ -181,7 +188,7 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
181
188
|
key: a.key,
|
|
182
189
|
id: a.id ?? `marker-at-${at}`,
|
|
183
190
|
x: xScale(at),
|
|
184
|
-
text:
|
|
191
|
+
text: readoutFmt(at),
|
|
185
192
|
};
|
|
186
193
|
})
|
|
187
194
|
.filter((t) => t.x >= 0 && t.x <= plotWidth);
|
|
@@ -411,6 +418,6 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
411
418
|
transform: 'translateX(-50%)',
|
|
412
419
|
[onTop ? 'bottom' : 'top']: `${pillOffset}px`,
|
|
413
420
|
zIndex: 3,
|
|
414
|
-
}, children:
|
|
421
|
+
}, children: readoutFmt(+xScale.invert(cursorX)) })] }))] }));
|
|
415
422
|
}
|
|
416
423
|
//# sourceMappingURL=XAxis.js.map
|
package/dist/YAxis.d.ts
CHANGED
|
@@ -60,6 +60,14 @@ export interface YAxisProps {
|
|
|
60
60
|
readonly at: number;
|
|
61
61
|
readonly label: string;
|
|
62
62
|
}>;
|
|
63
|
+
/**
|
|
64
|
+
* Target number of **auto** ticks — the `count` passed to `scale.ticks()`
|
|
65
|
+
* (d3 returns nice 1-2-5 values near it, not exactly this many). **Omitted ⇒
|
|
66
|
+
* derived from the row height** so a short strip isn't crushed with a tall
|
|
67
|
+
* row's density (mirrors the width-derived x axis). Ignored when explicit
|
|
68
|
+
* {@link ticks} are given (those set both labels and gridlines directly).
|
|
69
|
+
*/
|
|
70
|
+
tickCount?: number;
|
|
63
71
|
/**
|
|
64
72
|
* Render the tick labels at the domain extremes (the top & bottom ticks)?
|
|
65
73
|
* **Default `true`.** `false` drops just those two numbers — the gridlines
|
|
@@ -92,5 +100,5 @@ export interface YAxisProps {
|
|
|
92
100
|
* tick marks + labels from that scale. Charts attach via `<LineChart axis="id">`
|
|
93
101
|
* (default: the first axis).
|
|
94
102
|
*/
|
|
95
|
-
export declare function YAxis({ id, side, label, min, max, format, ticks, pad, boundaryLabels, width, labelPlacement, color, index, }: YAxisProps): import("react/jsx-runtime").JSX.Element;
|
|
103
|
+
export declare function YAxis({ id, side, label, min, max, format, ticks, tickCount, pad, boundaryLabels, width, labelPlacement, color, index, }: YAxisProps): import("react/jsx-runtime").JSX.Element;
|
|
96
104
|
//# sourceMappingURL=YAxis.d.ts.map
|
package/dist/YAxis.js
CHANGED
|
@@ -4,7 +4,10 @@ import { ContainerContext, RowContext } from './context.js';
|
|
|
4
4
|
import { resolveAxisFormat } from './format.js';
|
|
5
5
|
import { useSlotKey } from './use-slot-key.js';
|
|
6
6
|
const DEFAULT_WIDTH = 50;
|
|
7
|
-
|
|
7
|
+
/** Fallback tick count before the row has published its resolved count (the
|
|
8
|
+
* first render, pre-registration). The row's height-derived value takes over
|
|
9
|
+
* immediately after. */
|
|
10
|
+
const DEFAULT_TICK_COUNT = 5;
|
|
8
11
|
/**
|
|
9
12
|
* A y-axis for a {@link ChartRow}, rendered as DOM chrome (not canvas) so the
|
|
10
13
|
* text is crisp, themeable, and accessible. Registers its id / side / width /
|
|
@@ -13,7 +16,7 @@ const TICK_COUNT = 5;
|
|
|
13
16
|
* tick marks + labels from that scale. Charts attach via `<LineChart axis="id">`
|
|
14
17
|
* (default: the first axis).
|
|
15
18
|
*/
|
|
16
|
-
export function YAxis({ id, side = 'left', label, min, max, format, ticks, pad = 0, boundaryLabels = true, width = DEFAULT_WIDTH, labelPlacement = 'rotated', color, index = 0, }) {
|
|
19
|
+
export function YAxis({ id, side = 'left', label, min, max, format, ticks, tickCount, pad = 0, boundaryLabels = true, width = DEFAULT_WIDTH, labelPlacement = 'rotated', color, index = 0, }) {
|
|
17
20
|
const container = useContext(ContainerContext);
|
|
18
21
|
if (container === null) {
|
|
19
22
|
throw new Error('<YAxis> must be rendered inside a <ChartContainer>');
|
|
@@ -32,8 +35,21 @@ export function YAxis({ id, side = 'left', label, min, max, format, ticks, pad =
|
|
|
32
35
|
labelPlacement,
|
|
33
36
|
format,
|
|
34
37
|
tickValues: ticks?.map((t) => t.at),
|
|
38
|
+
tickCount,
|
|
35
39
|
index,
|
|
36
|
-
}), [
|
|
40
|
+
}), [
|
|
41
|
+
id,
|
|
42
|
+
side,
|
|
43
|
+
width,
|
|
44
|
+
min,
|
|
45
|
+
max,
|
|
46
|
+
pad,
|
|
47
|
+
labelPlacement,
|
|
48
|
+
format,
|
|
49
|
+
ticks,
|
|
50
|
+
tickCount,
|
|
51
|
+
index,
|
|
52
|
+
]);
|
|
37
53
|
// A stable per-instance slot (see useSlotKey) keeps this axis in a fixed
|
|
38
54
|
// registry position, so a min/max/side change updates in place rather than
|
|
39
55
|
// re-appending (which would move the first axis behind a later one and
|
|
@@ -48,14 +64,19 @@ export function YAxis({ id, side = 'left', label, min, max, format, ticks, pad =
|
|
|
48
64
|
}, [registerAxis, slot, spec]);
|
|
49
65
|
const { theme } = container;
|
|
50
66
|
const yScale = row.yScales.get(id);
|
|
67
|
+
// The auto-tick count — the row resolves it (explicit `tickCount` else
|
|
68
|
+
// height-derived) and both this axis's labels AND the row's gridlines read
|
|
69
|
+
// the same value, so a label and its gridline can't drift apart.
|
|
70
|
+
const count = row.tickCounts.get(id) ?? DEFAULT_TICK_COUNT;
|
|
51
71
|
// Same formatter the readout uses (resolved per axis on the row), so a tick and
|
|
52
|
-
// a cursor value read identically.
|
|
53
|
-
|
|
72
|
+
// a cursor value read identically. `count` calibrates the default formatter's
|
|
73
|
+
// precision to the tick density, exactly as the axis is.
|
|
74
|
+
const fmt = yScale ? resolveAxisFormat(yScale, count, format) : String;
|
|
54
75
|
// Explicit `{ at, label }` ticks render verbatim (each label at its `at`),
|
|
55
76
|
// overriding the auto-picked d3 ticks; otherwise label the scale's ticks via `fmt`.
|
|
56
77
|
const tickList = ticks
|
|
57
78
|
? ticks.map((t) => ({ value: t.at, label: t.label }))
|
|
58
|
-
: (yScale ? yScale.ticks(
|
|
79
|
+
: (yScale ? yScale.ticks(count) : []).map((t) => ({
|
|
59
80
|
value: t,
|
|
60
81
|
label: fmt(t),
|
|
61
82
|
}));
|