@pond-ts/charts 0.54.0 → 0.56.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +451 -1
- package/dist/AreaChart.d.ts +47 -22
- package/dist/AreaChart.js +24 -1
- package/dist/BandChart.d.ts +29 -17
- package/dist/BarChart.d.ts +92 -49
- package/dist/BarChart.js +65 -8
- package/dist/BarList.d.ts +127 -0
- package/dist/BarList.js +84 -0
- package/dist/BoxList.d.ts +108 -0
- package/dist/BoxList.js +125 -0
- package/dist/BoxPlot.d.ts +63 -30
- package/dist/Candlestick.d.ts +5 -4
- package/dist/ChartRow.js +57 -6
- package/dist/Layers.js +22 -2
- package/dist/LineChart.d.ts +29 -26
- package/dist/ListTable.d.ts +51 -0
- package/dist/ListTable.js +143 -0
- package/dist/ScatterChart.d.ts +27 -17
- package/dist/YAxis.d.ts +29 -1
- package/dist/YAxis.js +19 -5
- package/dist/area.js +46 -15
- package/dist/band.js +13 -0
- package/dist/bars.d.ts +89 -18
- package/dist/bars.js +141 -30
- package/dist/column-names.d.ts +74 -0
- package/dist/column-names.js +2 -0
- package/dist/context.d.ts +40 -2
- package/dist/data.d.ts +19 -0
- package/dist/data.js +46 -0
- package/dist/dev.d.ts +2 -0
- package/dist/dev.js +2 -0
- package/dist/domain.d.ts +54 -1
- package/dist/domain.js +195 -2
- package/dist/format.d.ts +20 -0
- package/dist/format.js +23 -10
- package/dist/gaps.d.ts +33 -0
- package/dist/gaps.js +49 -0
- package/dist/index.d.ts +19 -13
- package/dist/index.js +21 -13
- package/dist/line.js +10 -1
- package/dist/list-source.d.ts +61 -0
- package/dist/list-source.js +5 -0
- package/dist/list.d.ts +205 -0
- package/dist/list.js +165 -0
- package/dist/theme.d.ts +42 -0
- package/dist/theme.js +24 -0
- package/dist/viewport.d.ts +9 -1
- package/dist/viewport.js +40 -4
- package/dist/yticks.d.ts +44 -0
- package/dist/yticks.js +55 -0
- package/package.json +3 -3
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @internal The shared row-table shell behind {@link BarList} / {@link BoxList}.
|
|
4
|
+
*
|
|
5
|
+
* Renders a real `<table>` — the point of the list family is table semantics
|
|
6
|
+
* (label cells that can be links, aligned data cells, a `colSpan` detail row
|
|
7
|
+
* for the expander, screen-reader-legible rows), which a canvas plot can't
|
|
8
|
+
* carry and which hand-rolled flex rows re-implement badly (per-row cell
|
|
9
|
+
* alignment is exactly what table layout solves). The glyph cell takes
|
|
10
|
+
* `width: 100%` so it absorbs the free width; every text cell shrinks to fit.
|
|
11
|
+
*
|
|
12
|
+
* Not exported from the package: the public surface is the two sisters, so the
|
|
13
|
+
* shared shell can evolve without a compatibility contract.
|
|
14
|
+
*/
|
|
15
|
+
import { Fragment, useState } from 'react';
|
|
16
|
+
/** The turquoise the selected-row edge falls back to when the theme has no
|
|
17
|
+
* annotation register — the same built-in the annotation layer uses. */
|
|
18
|
+
const FALLBACK_ACCENT = '#0d9488';
|
|
19
|
+
/** The shared text ink: the band-label tone when the theme has one (stronger
|
|
20
|
+
* than tick labels — these cells are primary content), else the tick ink. */
|
|
21
|
+
export function listInk(theme) {
|
|
22
|
+
return theme.axis.band?.label ?? theme.axis.label;
|
|
23
|
+
}
|
|
24
|
+
export function ListTable({ rows, kind, renderGlyphs, before = [], after = [], renderExpanded, defaultExpanded, onExpandToggle, selected, onRowClick, divided = true, baseline = false, markers = [], theme, }) {
|
|
25
|
+
// Uncontrolled expansion, keyed on row identity so it survives a re-sort.
|
|
26
|
+
const [expanded, setExpanded] = useState(() => new Set(defaultExpanded ?? []));
|
|
27
|
+
const [hovered, setHovered] = useState(null);
|
|
28
|
+
const interactive = onRowClick !== undefined;
|
|
29
|
+
const toggle = (key) => {
|
|
30
|
+
const open = !expanded.has(key);
|
|
31
|
+
setExpanded((prev) => {
|
|
32
|
+
const next = new Set(prev);
|
|
33
|
+
if (open)
|
|
34
|
+
next.add(key);
|
|
35
|
+
else
|
|
36
|
+
next.delete(key);
|
|
37
|
+
return next;
|
|
38
|
+
});
|
|
39
|
+
onExpandToggle?.(key, open);
|
|
40
|
+
};
|
|
41
|
+
const ink = listInk(theme);
|
|
42
|
+
const accent = theme.annotation?.color ?? FALLBACK_ACCENT;
|
|
43
|
+
const divider = divided ? `1px solid ${theme.axis.grid}` : undefined;
|
|
44
|
+
// Label + before + glyph + after (+ expander) — the detail row spans them all.
|
|
45
|
+
const span = 2 + before.length + after.length + (renderExpanded ? 1 : 0);
|
|
46
|
+
const textCell = (align) => ({
|
|
47
|
+
padding: '6px 12px',
|
|
48
|
+
whiteSpace: 'nowrap',
|
|
49
|
+
textAlign: align ?? 'left',
|
|
50
|
+
verticalAlign: 'middle',
|
|
51
|
+
});
|
|
52
|
+
// The glyph cell's shared horizontal geometry — the label strip must use
|
|
53
|
+
// the SAME left/right padding (and baseline border) as the data rows, or
|
|
54
|
+
// its percentages would resolve against a different content width and the
|
|
55
|
+
// labels would sit off their rules.
|
|
56
|
+
const glyphCellStyle = (vertical) => ({
|
|
57
|
+
width: '100%',
|
|
58
|
+
padding: baseline ? `${vertical} 8px ${vertical} 5px` : `${vertical} 8px`,
|
|
59
|
+
verticalAlign: 'middle',
|
|
60
|
+
borderLeft: baseline ? `1px solid ${theme.axis.grid}` : undefined,
|
|
61
|
+
});
|
|
62
|
+
const drawnMarkers = markers.filter((m) => m.frac !== null);
|
|
63
|
+
return (_jsx("table", { "data-list": kind, style: {
|
|
64
|
+
width: '100%',
|
|
65
|
+
borderCollapse: 'collapse',
|
|
66
|
+
font: `${theme.font.size}px/${1.5} ${theme.font.family}`,
|
|
67
|
+
color: ink,
|
|
68
|
+
background: theme.background,
|
|
69
|
+
}, children: _jsxs("tbody", { children: [drawnMarkers.some((m) => m.label !== undefined) && (
|
|
70
|
+
// The marker label strip: one synthetic row above the data, its
|
|
71
|
+
// glyph cell sharing the data rows' horizontal geometry so each
|
|
72
|
+
// label centres exactly on its rule below.
|
|
73
|
+
_jsxs("tr", { "data-list-marker-labels": "", children: [_jsx("td", { style: textCell() }), before.map((cell) => (_jsx("td", { style: textCell(cell.align) }, cell.key))), _jsx("td", { style: glyphCellStyle('0px'), children: _jsx("div", { style: {
|
|
74
|
+
position: 'relative',
|
|
75
|
+
height: theme.font.size + 6,
|
|
76
|
+
}, children: drawnMarkers.map((m, mi) => m.label !== undefined && (_jsx("span", { "data-list-marker-label": "", style: {
|
|
77
|
+
position: 'absolute',
|
|
78
|
+
left: `${m.frac * 100}%`,
|
|
79
|
+
bottom: 0,
|
|
80
|
+
transform: 'translateX(-50%)',
|
|
81
|
+
whiteSpace: 'nowrap',
|
|
82
|
+
color: accent,
|
|
83
|
+
}, children: m.label }, mi))) }) }), after.map((cell) => (_jsx("td", { style: textCell(cell.align) }, cell.key))), renderExpanded !== undefined && _jsx("td", {})] })), rows.map((row, i) => {
|
|
84
|
+
const isSelected = selected != null && selected === row.key;
|
|
85
|
+
const isOpen = renderExpanded !== undefined && expanded.has(row.key);
|
|
86
|
+
return (_jsxs(Fragment, { children: [_jsxs("tr", { "data-list-row": row.key, ...(isSelected ? { 'data-selected': '' } : {}), onClick: onRowClick === undefined ? undefined : () => onRowClick(row),
|
|
87
|
+
// A clickable row is keyboard-reachable too: focusable, and
|
|
88
|
+
// Enter / Space activate it (Space's default scroll is eaten).
|
|
89
|
+
tabIndex: interactive ? 0 : undefined, onKeyDown: onRowClick === undefined
|
|
90
|
+
? undefined
|
|
91
|
+
: (e) => {
|
|
92
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
onRowClick(row);
|
|
95
|
+
}
|
|
96
|
+
}, onPointerEnter: interactive ? () => setHovered(row.key) : undefined, onPointerLeave: interactive ? () => setHovered(null) : undefined, style: {
|
|
97
|
+
borderTop: i > 0 ? divider : undefined,
|
|
98
|
+
cursor: interactive ? 'pointer' : undefined,
|
|
99
|
+
background: interactive && hovered === row.key
|
|
100
|
+
? (theme.legend?.border ?? theme.axis.grid)
|
|
101
|
+
: undefined,
|
|
102
|
+
// The selection accent: an inset edge in the annotation
|
|
103
|
+
// register (a *user's* mark, so it takes the marks colour,
|
|
104
|
+
// not a data hue) — reads on any ground, moves no layout.
|
|
105
|
+
boxShadow: isSelected ? `inset 3px 0 0 ${accent}` : undefined,
|
|
106
|
+
}, children: [_jsx("td", { "data-list-cell": "label", style: textCell(), children: row.label ?? row.key }), before.map((cell) => (_jsx("td", { "data-list-cell": cell.key, style: textCell(cell.align), children: cell.render(row) }, cell.key))), _jsx("td", { "data-list-cell": "glyphs",
|
|
107
|
+
// 100% absorbs the table's free width; every text cell
|
|
108
|
+
// shrinks to its content, staying aligned down the list.
|
|
109
|
+
// The baseline rule marks the scale origin: left padding
|
|
110
|
+
// narrows to a 5px breath so the glyphs start just off the
|
|
111
|
+
// rule, and border-collapse joins the rows' rules into one
|
|
112
|
+
// continuous vertical — the same thin `axis.grid` ink as
|
|
113
|
+
// the row dividers, so the two read as one quiet grid.
|
|
114
|
+
style: glyphCellStyle('6px'), children: _jsxs("div", { style: { position: 'relative' }, children: [renderGlyphs(row), drawnMarkers.map((m, mi) => (
|
|
115
|
+
// One dotted segment per row, bleeding through the
|
|
116
|
+
// row's vertical padding (+ divider) so adjacent rows'
|
|
117
|
+
// segments join into one continuous rule. Annotation
|
|
118
|
+
// register — a reference is a user's mark, not data.
|
|
119
|
+
_jsx("div", { "data-list-marker": "", style: {
|
|
120
|
+
position: 'absolute',
|
|
121
|
+
top: -7,
|
|
122
|
+
bottom: -7,
|
|
123
|
+
left: `calc(${m.frac * 100}% - 0.5px)`,
|
|
124
|
+
width: 0,
|
|
125
|
+
borderLeft: `1px dotted ${accent}`,
|
|
126
|
+
pointerEvents: 'none',
|
|
127
|
+
} }, mi)))] }) }), after.map((cell) => (_jsx("td", { "data-list-cell": cell.key, style: textCell(cell.align), children: cell.render(row) }, cell.key))), renderExpanded !== undefined && (_jsx("td", { style: { padding: '0 4px', verticalAlign: 'middle' }, children: _jsx("button", { type: "button", "data-list-expander": "", "aria-expanded": isOpen, "aria-label": isOpen ? 'Collapse row' : 'Expand row', onClick: (e) => {
|
|
128
|
+
// The chevron toggles; it must not double as a row click.
|
|
129
|
+
e.stopPropagation();
|
|
130
|
+
toggle(row.key);
|
|
131
|
+
}, style: {
|
|
132
|
+
background: 'none',
|
|
133
|
+
border: 'none',
|
|
134
|
+
cursor: 'pointer',
|
|
135
|
+
color: theme.axis.label,
|
|
136
|
+
font: 'inherit',
|
|
137
|
+
padding: '2px 6px',
|
|
138
|
+
transform: isOpen ? 'rotate(90deg)' : undefined,
|
|
139
|
+
transition: 'transform 120ms',
|
|
140
|
+
}, children: "\u25B8" }) }))] }), isOpen && (_jsx("tr", { "data-list-detail": row.key, children: _jsx("td", { colSpan: span, style: { padding: '2px 12px 12px' }, children: renderExpanded(row) }) }))] }, row.key));
|
|
141
|
+
})] }) }));
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=ListTable.js.map
|
package/dist/ScatterChart.d.ts
CHANGED
|
@@ -1,24 +1,9 @@
|
|
|
1
1
|
import { ValueSeries } from 'pond-ts';
|
|
2
2
|
import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
|
|
3
|
+
import type { NumericColumn, ValueNumericColumn } from './column-names.js';
|
|
3
4
|
import { type ColorEncoding, type RadiusEncoding } from './encoding.js';
|
|
4
5
|
import type { DecimateOption } from './decimate.js';
|
|
5
|
-
export interface
|
|
6
|
-
/**
|
|
7
|
-
* The source series. A `TimeSeries` scatters against the time axis; a
|
|
8
|
-
* `ValueSeries` (`series.byValue('cumDist')`, or `ValueSeries.fromColumns`
|
|
9
|
-
* for natively value-keyed data — IV marks keyed by strike) against its
|
|
10
|
-
* value axis — the container infers which from the data, no axis-type prop
|
|
11
|
-
* (mirrors `<LineChart>`). Either way the key / axis column supplies each
|
|
12
|
-
* point's x and `column` supplies y.
|
|
13
|
-
*
|
|
14
|
-
* **Live charts:** `series.byValue(…)` mints a *fresh* projection each call,
|
|
15
|
-
* so passing `series={s.byValue('dist')}` inline re-registers this layer
|
|
16
|
-
* every render — memoize the projection (`useMemo`) on a frequently
|
|
17
|
-
* re-rendering chart.
|
|
18
|
-
*/
|
|
19
|
-
series: TimeSeries<S> | ValueSeries<VS>;
|
|
20
|
-
/** Name of the numeric value column — each point's y. */
|
|
21
|
-
column: string;
|
|
6
|
+
export interface ScatterChartCommon<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
|
|
22
7
|
/**
|
|
23
8
|
* The scatter's semantic identifier — what the marks _are_ / how they should
|
|
24
9
|
* read. The theme maps it to a {@link ScatterStyle} (`theme.scatter[as] ??
|
|
@@ -109,6 +94,30 @@ export interface ScatterChartProps<S extends SeriesSchema = SeriesSchema, VS ext
|
|
|
109
94
|
*/
|
|
110
95
|
index?: number;
|
|
111
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* ScatterChart's source + column props, a **union over the series kind** so the
|
|
99
|
+
* column names are checked against the schema that was actually passed
|
|
100
|
+
* ([PND-CHARTAPI]). A single member carrying `NumericColumn<S> |
|
|
101
|
+
* ValueNumericColumn<VS>` would silently widen to `string`: only one of the
|
|
102
|
+
* two generics is ever inferred, and the other falls back (measured in
|
|
103
|
+
* `spikes/charts-type-seam/`). Loosely-typed series still accept any name.
|
|
104
|
+
*/
|
|
105
|
+
type ScatterChartSource<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = {
|
|
106
|
+
/**
|
|
107
|
+
* The source series. **Live charts:** `series.byValue(…)` mints a
|
|
108
|
+
* *fresh* projection each call, so an inline `series={s.byValue('d')}`
|
|
109
|
+
* re-registers this layer every render — on a frequently re-rendering
|
|
110
|
+
* (e.g. scrub-driven) chart, memoize the projection (`useMemo`) so the
|
|
111
|
+
* layer isn't rebuilt each frame.
|
|
112
|
+
*/
|
|
113
|
+
series: TimeSeries<S>;
|
|
114
|
+
column: NumericColumn<S>;
|
|
115
|
+
} | {
|
|
116
|
+
series: ValueSeries<VS>;
|
|
117
|
+
column: ValueNumericColumn<VS>;
|
|
118
|
+
};
|
|
119
|
+
/** `<ScatterChart>`'s props: the shared knobs plus one series-kind source shape. */
|
|
120
|
+
export type ScatterChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = ScatterChartCommon<S, VS> & ScatterChartSource<S, VS>;
|
|
112
121
|
/**
|
|
113
122
|
* A scatter draw layer: one mark per finite point at `(x, column-value)`
|
|
114
123
|
* — x from the series' key / axis column (time or value axis) —
|
|
@@ -138,4 +147,5 @@ export interface ScatterChartProps<S extends SeriesSchema = SeriesSchema, VS ext
|
|
|
138
147
|
* ```
|
|
139
148
|
*/
|
|
140
149
|
export declare function ScatterChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, as: semantic, id, axis, radius, color, label, offset, decimate, legend, index, }: ScatterChartProps<S, VS>): null;
|
|
150
|
+
export {};
|
|
141
151
|
//# sourceMappingURL=ScatterChart.d.ts.map
|
package/dist/YAxis.d.ts
CHANGED
|
@@ -20,6 +20,34 @@ export interface YAxisProps {
|
|
|
20
20
|
* that has headroom (auto-fit / padded) so it doesn't crowd the top tick.
|
|
21
21
|
*/
|
|
22
22
|
labelPlacement?: 'rotated' | 'top';
|
|
23
|
+
/**
|
|
24
|
+
* Which scale the axis maps its domain through. **Default `'linear'`.**
|
|
25
|
+
*
|
|
26
|
+
* `'log'` gives a base-10 logarithmic axis — for data spanning orders of
|
|
27
|
+
* magnitude, where a linear axis flattens everything below the top decade
|
|
28
|
+
* onto the baseline. Ticks land on the decades, and `format` still formats
|
|
29
|
+
* the **value**, so a readout says `1.2 PB`, not its logarithm.
|
|
30
|
+
*
|
|
31
|
+
* A log domain cannot contain zero or negative numbers — d3 maps them to
|
|
32
|
+
* `NaN`, which has no position on the plot. So:
|
|
33
|
+
*
|
|
34
|
+
* - **Auto-fit ignores non-positive extents** when picking the low end (a
|
|
35
|
+
* `BarChart`, whose extent always reaches zero so its bars can meet their
|
|
36
|
+
* baseline, can therefore share the axis), and rounds the domain out to
|
|
37
|
+
* whole powers of ten.
|
|
38
|
+
* - **An explicit `min`/`max` that is not positive is refused**, and that
|
|
39
|
+
* side auto-fits instead. A positive bound is always honoured exactly; when
|
|
40
|
+
* only one side is given and the domain would invert, the *auto* side moves
|
|
41
|
+
* — the same policy a linear axis follows.
|
|
42
|
+
* - **Layers that fill to a baseline** (`AreaChart`, `BarChart`, a stacked
|
|
43
|
+
* histogram) rest it on the bottom of the domain rather than on zero.
|
|
44
|
+
* - **A value with no position gaps the line**, rather than its neighbours
|
|
45
|
+
* being joined straight across it.
|
|
46
|
+
*
|
|
47
|
+
* A dev-mode warning fires for the cases that are unambiguously a mistake: a
|
|
48
|
+
* refused bound, negative data, or an axis with no positive data at all.
|
|
49
|
+
*/
|
|
50
|
+
scale?: 'linear' | 'log';
|
|
23
51
|
/** Explicit domain bounds; omit to auto-fit the charts linked to this axis. */
|
|
24
52
|
min?: number;
|
|
25
53
|
max?: number;
|
|
@@ -100,5 +128,5 @@ export interface YAxisProps {
|
|
|
100
128
|
* tick marks + labels from that scale. Charts attach via `<LineChart axis="id">`
|
|
101
129
|
* (default: the first axis).
|
|
102
130
|
*/
|
|
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;
|
|
131
|
+
export declare function YAxis({ id, side, label, scale, min, max, format, ticks, tickCount, pad, boundaryLabels, width, labelPlacement, color, index, }: YAxisProps): import("react/jsx-runtime").JSX.Element;
|
|
104
132
|
//# sourceMappingURL=YAxis.d.ts.map
|
package/dist/YAxis.js
CHANGED
|
@@ -3,6 +3,7 @@ import { useContext, useEffect, useMemo } from 'react';
|
|
|
3
3
|
import { ContainerContext, RowContext } from './context.js';
|
|
4
4
|
import { resolveAxisFormat } from './format.js';
|
|
5
5
|
import { useSlotKey } from './use-slot-key.js';
|
|
6
|
+
import { yTickValues } from './yticks.js';
|
|
6
7
|
const DEFAULT_WIDTH = 50;
|
|
7
8
|
/** Fallback tick count before the row has published its resolved count (the
|
|
8
9
|
* first render, pre-registration). The row's height-derived value takes over
|
|
@@ -16,7 +17,7 @@ const DEFAULT_TICK_COUNT = 5;
|
|
|
16
17
|
* tick marks + labels from that scale. Charts attach via `<LineChart axis="id">`
|
|
17
18
|
* (default: the first axis).
|
|
18
19
|
*/
|
|
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, }) {
|
|
20
|
+
export function YAxis({ id, side = 'left', label, scale = 'linear', min, max, format, ticks, tickCount, pad = 0, boundaryLabels = true, width = DEFAULT_WIDTH, labelPlacement = 'rotated', color, index = 0, }) {
|
|
20
21
|
const container = useContext(ContainerContext);
|
|
21
22
|
if (container === null) {
|
|
22
23
|
throw new Error('<YAxis> must be rendered inside a <ChartContainer>');
|
|
@@ -29,6 +30,7 @@ export function YAxis({ id, side = 'left', label, min, max, format, ticks, tickC
|
|
|
29
30
|
id,
|
|
30
31
|
side,
|
|
31
32
|
width,
|
|
33
|
+
scale,
|
|
32
34
|
min,
|
|
33
35
|
max,
|
|
34
36
|
pad,
|
|
@@ -41,6 +43,7 @@ export function YAxis({ id, side = 'left', label, min, max, format, ticks, tickC
|
|
|
41
43
|
id,
|
|
42
44
|
side,
|
|
43
45
|
width,
|
|
46
|
+
scale,
|
|
44
47
|
min,
|
|
45
48
|
max,
|
|
46
49
|
pad,
|
|
@@ -72,14 +75,25 @@ export function YAxis({ id, side = 'left', label, min, max, format, ticks, tickC
|
|
|
72
75
|
// a cursor value read identically. `count` calibrates the default formatter's
|
|
73
76
|
// precision to the tick density, exactly as the axis is.
|
|
74
77
|
const fmt = yScale ? resolveAxisFormat(yScale, count, format) : String;
|
|
78
|
+
// A **horizontal categorical** layer on this axis supplies its category
|
|
79
|
+
// names ([PND-HCAT]); with no explicit `ticks`, label one per unit slot at
|
|
80
|
+
// its centre (`i + 0.5`) instead of the scale's numeric ticks — a slot index
|
|
81
|
+
// is not a number anyone wants to read. Explicit `ticks` still win, and a
|
|
82
|
+
// non-categorical row is unaffected (no layer answers, so this is `null`).
|
|
83
|
+
const layerCategories = row.layers
|
|
84
|
+
.filter((e) => (e.axisId ?? row.defaultAxisId) === id)
|
|
85
|
+
.map((e) => e.layer.binCategories?.() ?? null)
|
|
86
|
+
.find((c) => c !== null) ?? null;
|
|
75
87
|
// Explicit `{ at, label }` ticks render verbatim (each label at its `at`),
|
|
76
88
|
// overriding the auto-picked d3 ticks; otherwise label the scale's ticks via `fmt`.
|
|
77
89
|
const tickList = ticks
|
|
78
90
|
? ticks.map((t) => ({ value: t.at, label: t.label }))
|
|
79
|
-
:
|
|
80
|
-
value:
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
: layerCategories !== null
|
|
92
|
+
? layerCategories.map((label, i) => ({ value: i + 0.5, label }))
|
|
93
|
+
: (yScale ? yTickValues(yScale, count) : []).map((t) => ({
|
|
94
|
+
value: t,
|
|
95
|
+
label: fmt(t),
|
|
96
|
+
}));
|
|
83
97
|
// The row reserves a slot per axis column (the widest in that column across
|
|
84
98
|
// rows). Size the box to the slot and align this axis's own (narrower)
|
|
85
99
|
// content toward the plot — left axes flush right, right axes flush left — so
|
package/dist/area.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { area as d3area, curveLinear } from 'd3-shape';
|
|
2
2
|
import { strokeAffinePolyline } from './line.js';
|
|
3
|
-
import { bridgeGaps, collectGapEdges, drawGapBridges, drawGapFades, drawGapSteps, withAlpha, DEFAULT_GAP_MODE, DEFAULT_GAP_CONNECTOR_OPACITY, } from './gaps.js';
|
|
3
|
+
import { bridgeGaps, collectGapEdges, drawGapBridges, drawGapFades, drawGapSteps, gapUnscalable, withAlpha, DEFAULT_GAP_MODE, DEFAULT_GAP_CONNECTOR_OPACITY, } from './gaps.js';
|
|
4
4
|
import { cullChartSeries } from './culling.js';
|
|
5
5
|
import { decimateM4Cached } from './decimate.js';
|
|
6
6
|
import { affineOf } from './affine.js';
|
|
@@ -191,6 +191,15 @@ export function drawArea(ctx, cs, xScale, yScale, style, baselineValue, curve =
|
|
|
191
191
|
else {
|
|
192
192
|
cs = cullChartSeries(source, xScale);
|
|
193
193
|
}
|
|
194
|
+
// Values with no position on the y scale (zero / negative on a log axis)
|
|
195
|
+
// become ordinary NaN gaps, so the fill and outline break at them rather than
|
|
196
|
+
// bridging over a dropped `lineTo(x, NaN)`. Deliberately **after** the
|
|
197
|
+
// gradient above: that reads the pre-cull buffer, whose finite extent is
|
|
198
|
+
// memoized per `Float64Array` ([PND-GRADX]), and a fresh array here would miss
|
|
199
|
+
// that cache on every frame. A no-op on an affine (linear) y scale.
|
|
200
|
+
const scaledY = gapUnscalable(cs.y, cs.length, yScale);
|
|
201
|
+
if (scaledY !== cs.y)
|
|
202
|
+
cs = { ...cs, y: scaledY };
|
|
194
203
|
// `none` interpolates interior gaps so the fill + outline bridge them; every
|
|
195
204
|
// other mode keeps NaN so d3 breaks both (the inferred line bridge, if any, is
|
|
196
205
|
// a separate overlay pass below).
|
|
@@ -277,20 +286,42 @@ export function drawArea(ctx, cs, xScale, yScale, style, baselineValue, curve =
|
|
|
277
286
|
function buildGradient(ctx, valueExtent, yScale, baselinePx, style) {
|
|
278
287
|
if (valueExtent === null)
|
|
279
288
|
return style.fill; // no finite values (caller no-ops)
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
289
|
+
// Stacked areas opt out of the grade entirely: a band that fades to
|
|
290
|
+
// transparent at the baseline shows every band beneath it (see AreaStyle).
|
|
291
|
+
if (style.flatFill === true)
|
|
292
|
+
return style.fill;
|
|
293
|
+
// The pixel extent is the two value extremes mapped through the (monotonic)
|
|
294
|
+
// y scale; min/max them so the result is flip-agnostic, exactly as the former
|
|
295
|
+
// per-point pixel scan produced. [PND-GRADX] moved the O(N) walk into the
|
|
296
|
+
// memoized {@link columnFiniteExtent}.
|
|
297
|
+
//
|
|
298
|
+
// **An extreme with no position on the scale is dropped**, not min/maxed in.
|
|
299
|
+
// `valueExtent` is the data's own `[min, max]`, and on a **log** axis a
|
|
300
|
+
// non-positive extreme — a series that touches zero, which is the ordinary
|
|
301
|
+
// shape of traffic or storage data — maps to `NaN`. `Math.min(NaN, pb)` is
|
|
302
|
+
// `NaN`, `NaN` propagates to the height, and `NaN < 1e-6` is **false**, so the
|
|
303
|
+
// degenerate guard below waved it through to `createLinearGradient(0, NaN, 0,
|
|
304
|
+
// NaN)` — which throws `IndexSizeError` on a real canvas and takes the whole
|
|
305
|
+
// chart down. The region is seeded from the baseline pixel (always in-domain,
|
|
306
|
+
// via `resolveAreaBaseline`) and widened only by extremes that have a
|
|
307
|
+
// position, so the grade still spans the part of the series that draws.
|
|
308
|
+
let regionTop = baselinePx;
|
|
309
|
+
let regionBottom = baselinePx;
|
|
310
|
+
const widen = (px) => {
|
|
311
|
+
if (!Number.isFinite(px))
|
|
312
|
+
return;
|
|
313
|
+
if (px < regionTop)
|
|
314
|
+
regionTop = px;
|
|
315
|
+
if (px > regionBottom)
|
|
316
|
+
regionBottom = px;
|
|
317
|
+
};
|
|
318
|
+
widen(yScale(valueExtent[0]));
|
|
319
|
+
widen(yScale(valueExtent[1]));
|
|
320
|
+
// `!(… >= 1e-6)` rather than `< 1e-6`, so a non-finite height — a baseline
|
|
321
|
+
// that somehow has no position either, leaving nothing finite to anchor on —
|
|
322
|
+
// falls back to the flat fill instead of reaching the gradient calls.
|
|
323
|
+
if (!(regionBottom - regionTop >= 1e-6))
|
|
324
|
+
return style.fill; // degenerate
|
|
294
325
|
const opaque = style.fill;
|
|
295
326
|
const transparent = withAlpha(style.fill, 0);
|
|
296
327
|
const grad = ctx.createLinearGradient(0, regionTop, 0, regionBottom);
|
package/dist/band.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { area as d3area, curveLinear } from 'd3-shape';
|
|
2
2
|
import { cullBandSeries } from './culling.js';
|
|
3
3
|
import { decimateBand } from './decimate.js';
|
|
4
|
+
import { gapUnscalable } from './gaps.js';
|
|
4
5
|
/**
|
|
5
6
|
* The `[min, max]` vertical extent of the **drawn** band — the lowest `lower`
|
|
6
7
|
* and highest `upper` over samples where both edges are finite — or `null` if
|
|
@@ -60,6 +61,18 @@ export function drawBand(ctx, band, xScale, yScale, style, curve = curveLinear,
|
|
|
60
61
|
band = decimateBand(band, xScale, ctx, k);
|
|
61
62
|
decimated = band !== before;
|
|
62
63
|
}
|
|
64
|
+
// An edge with no position on the y scale becomes an ordinary NaN gap, so the
|
|
65
|
+
// envelope breaks there rather than emitting dropped path ops that stitch the
|
|
66
|
+
// neighbouring samples together. A `lower` of `0` is the common shape — a band
|
|
67
|
+
// measured from nothing — and on a log axis zero has no position, so without
|
|
68
|
+
// this the fill silently spanned the samples it could not draw. Gapping either
|
|
69
|
+
// edge gaps the sample, which is already the band's contract: a sample counts
|
|
70
|
+
// only where **both** edges do. A no-op on an affine (linear) y scale.
|
|
71
|
+
const gapLower = gapUnscalable(band.lower, band.length, yScale);
|
|
72
|
+
const gapUpper = gapUnscalable(band.upper, band.length, yScale);
|
|
73
|
+
if (gapLower !== band.lower || gapUpper !== band.upper) {
|
|
74
|
+
band = { ...band, lower: gapLower, upper: gapUpper };
|
|
75
|
+
}
|
|
63
76
|
const gen = d3area()
|
|
64
77
|
.defined((_, i) => Number.isFinite(band.lower[i]) && Number.isFinite(band.upper[i]))
|
|
65
78
|
.x((_, i) => xScale(band.x[i]))
|
package/dist/bars.d.ts
CHANGED
|
@@ -50,8 +50,10 @@ export declare function resolveBarBaseline(yScale: Scale): number;
|
|
|
50
50
|
* from {@link barSpanPx} (the key's `[begin, end]`, inset by `gapPx`, floored at
|
|
51
51
|
* `minWidthPx`); the y-span runs between the value and the `baseline` pixel,
|
|
52
52
|
* normalized so a value above *or* below the baseline both yield an ascending
|
|
53
|
-
* rect.
|
|
54
|
-
*
|
|
53
|
+
* rect. This is the **ink** — what {@link drawBars} paints. Hit-testing uses
|
|
54
|
+
* {@link barSlotRect} instead (the bar's whole slot), so the drawn rect and the
|
|
55
|
+
* hit region are deliberately *not* the same geometry: the `gapPx` inset
|
|
56
|
+
* separates columns visually without carving a dead channel out of the target.
|
|
55
57
|
*/
|
|
56
58
|
export declare function barRect(cs: BarSeries, i: number, xScale: Scale, yScale: Scale, baseline: number, gapPx: number, minWidthPx: number): [x0: number, x1: number, yTop: number, yBottom: number] | null;
|
|
57
59
|
/**
|
|
@@ -75,9 +77,12 @@ export interface BarMark {
|
|
|
75
77
|
* no-id layer passes `undefined` and never matches — plus the bar's identity,
|
|
76
78
|
* see {@link barMatches}) draws in the style's `highlight` colour **and
|
|
77
79
|
* outlined**, so a click reads back on the canvas; a bar matching `hovered`
|
|
78
|
-
* draws
|
|
79
|
-
*
|
|
80
|
-
*
|
|
80
|
+
* draws **without** the outline (a lighter "this bar is live" on pointer-over)
|
|
81
|
+
* in the style's optional `hover` colour, or in `highlight` when the theme
|
|
82
|
+
* doesn't set one; all others use the flat `fill`. Either live state fills at
|
|
83
|
+
* **full opacity** — the resting `opacity` applies to resting bars only, and is
|
|
84
|
+
* restored so it doesn't leak into later layers. A bar that is both selected
|
|
85
|
+
* and hovered reads as **selected**.
|
|
81
86
|
*
|
|
82
87
|
* **Which identity.** A selection carrying a `mark` matches against the series'
|
|
83
88
|
* stable per-bar name ({@link BarSeries.marks} — the sample's own axis key,
|
|
@@ -94,11 +99,12 @@ export interface BarMark {
|
|
|
94
99
|
* (an `undefined` entry falls back to the flat `fill`). This is the
|
|
95
100
|
* direction-coloured financial volume row (rising / falling) and the
|
|
96
101
|
* value-band case on a time axis. Highlight follows {@link drawStacks}'s
|
|
97
|
-
* binFills convention
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
+
* binFills convention: the bar **keeps its own colour** under hover /
|
|
103
|
+
* selection — the highlight pops `globalAlpha` to 1 (and outlines the
|
|
104
|
+
* selection in the bar's own fill) — so a red / green bar stays red / green
|
|
105
|
+
* while live, instead of swapping to the single `highlight` colour and losing
|
|
106
|
+
* its meaning. (Both paths now pop to 1; what still differs is the *colour* —
|
|
107
|
+
* the flat path swaps to `highlight`, this one keeps `binFills[i]`.)
|
|
102
108
|
*
|
|
103
109
|
* **M4 column decimation ([PND-MARKDEC]):** once the *visible* bars are denser
|
|
104
110
|
* than ~2 per device pixel, they overplot into a solid silhouette, so
|
|
@@ -129,19 +135,68 @@ export declare function drawBars(ctx: CanvasRenderingContext2D, cs: BarSeries, x
|
|
|
129
135
|
* cheap and allocation-free).
|
|
130
136
|
*/
|
|
131
137
|
export declare function barIndexAtTime(cs: BarSeries, time: number): number;
|
|
138
|
+
/**
|
|
139
|
+
* The pixel rect of bar `i`'s **slot** — the region that *belongs* to the bar,
|
|
140
|
+
* as opposed to the ink {@link barRect} puts on the canvas. It spans the key's
|
|
141
|
+
* full `[begin, end]` in x (**no `gapPx` inset**) and the **whole plot height**
|
|
142
|
+
* in y. `null` for a gap (non-finite value), which owns no slot to select.
|
|
143
|
+
*
|
|
144
|
+
* The distinction is the point: a bar *is* the full width of its interval, and
|
|
145
|
+
* the drawing gap is a display affordance so adjacent columns read as discrete.
|
|
146
|
+
* Hit-testing the drawn rect made that affordance interactive — the gap became
|
|
147
|
+
* a dead channel you could point at and select nothing, and the empty plot
|
|
148
|
+
* space above a short bar likewise. Slots tile the axis, so every x inside the
|
|
149
|
+
* data range belongs to exactly one bar, which is what a column chart's hover
|
|
150
|
+
* should feel like and what {@link barIndexAtTime} (the x-scrub cursor) has
|
|
151
|
+
* always done.
|
|
152
|
+
*
|
|
153
|
+
* The plot's y extent is read from the `yScale`'s own domain, the same
|
|
154
|
+
* localized shape {@link resolveBarBaseline} uses. When it isn't readable (a
|
|
155
|
+
* bare test stub with no `.domain()`), this falls back to {@link barRect}'s
|
|
156
|
+
* value→baseline span, so a scale-less caller keeps the old behaviour rather
|
|
157
|
+
* than getting an unbounded hit region.
|
|
158
|
+
*
|
|
159
|
+
* `minWidthPx` still floors the span, so a lone point-keyed bar (zero-width
|
|
160
|
+
* key) stays selectable.
|
|
161
|
+
*
|
|
162
|
+
* **Two consequences worth knowing before you compose with it.**
|
|
163
|
+
*
|
|
164
|
+
* 1. **It reaches across the whole plot height, so it can shadow layers below
|
|
165
|
+
* it.** `resolveSelection` returns the topmost hit, so a `<BarChart>`
|
|
166
|
+
* declared *after* a `<ScatterChart>` / `<BoxPlot>` / another `<BarChart>`
|
|
167
|
+
* in the same row now claims every hit inside its x-range, at any y — where
|
|
168
|
+
* the drawn-rect target only claimed the bar's own ink. Declare a bar layer
|
|
169
|
+
* **below** the marks you want to stay clickable (which is also the usual
|
|
170
|
+
* z-order for bars-as-context). No shipped story composes that way, so this
|
|
171
|
+
* is latent rather than a live regression.
|
|
172
|
+
* 2. **Only the single-series vertical path uses it.** A stacked, `bins`,
|
|
173
|
+
* `categories` or horizontal `<BarChart>` hit-tests through
|
|
174
|
+
* {@link stackAt}, which still targets the drawn segment — a stack has to,
|
|
175
|
+
* since segments share a bin's x-range and only y tells them apart. So
|
|
176
|
+
* `<BarChart>` has two hit models; this is the one for a plain bar.
|
|
177
|
+
*/
|
|
178
|
+
export declare function barSlotRect(cs: BarSeries, i: number, xScale: Scale, yScale: Scale, baseline: number, minWidthPx: number): [x0: number, x1: number, yTop: number, yBottom: number] | null;
|
|
132
179
|
/**
|
|
133
180
|
* Hit-test plot-pixel `(px, py)` against `cs`'s bars — the **first** bar whose
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
181
|
+
* **slot** contains the point, or `null`. The geometry is {@link barSlotRect}:
|
|
182
|
+
* the bar's full interval width and the full plot height, *not* the drawn rect.
|
|
183
|
+
* Pointing at the gap between two columns, or above a short one, selects the
|
|
184
|
+
* bar whose slot you are in. The returned tuple is `[index, begin, value]` for
|
|
185
|
+
* the chart to assemble a `SelectInfo` (it owns the colour + label); keeping
|
|
186
|
+
* this layer free of the theme keeps it unit-testable without a `ChartTheme`.
|
|
187
|
+
*
|
|
188
|
+
* **Shared edges.** Contiguous bars meet exactly (`end[i] === begin[i+1]`) once
|
|
189
|
+
* the gap is gone, and both ends are inclusive, so a point landing precisely on
|
|
190
|
+
* the boundary matches **the left bar** — first match wins, the same rule
|
|
191
|
+
* {@link barIndexAtTime} documents, so hover and the x-scrub cursor agree.
|
|
192
|
+
*
|
|
193
|
+
* A **gap** bar (non-finite value) owns no slot and is skipped, so hovering
|
|
194
|
+
* where the data is missing selects nothing rather than a `NaN`.
|
|
139
195
|
*
|
|
140
196
|
* O(N) over the events (no spatial index — bar counts are view-scale, hundreds
|
|
141
|
-
* not millions; click is a rare event).
|
|
142
|
-
* series, so "first match" is unambiguous in practice.
|
|
197
|
+
* not millions; click is a rare event).
|
|
143
198
|
*/
|
|
144
|
-
export declare function barAt(cs: BarSeries, px: number, py: number, xScale: Scale, yScale: Scale, baseline: number,
|
|
199
|
+
export declare function barAt(cs: BarSeries, px: number, py: number, xScale: Scale, yScale: Scale, baseline: number, minWidthPx: number): [index: number, begin: number, value: number] | null;
|
|
145
200
|
/**
|
|
146
201
|
* A resolved per-group stack style: `fills` aligned index-for-index to
|
|
147
202
|
* {@link StackedBarSeries.groups} (segment `g` uses `fills[g]`), plus the shared
|
|
@@ -196,6 +251,22 @@ export declare function stackValueExtent(ss: StackedBarSeries): [number, number]
|
|
|
196
251
|
* the x auto-fit for a vertical histogram, the y auto-fit for a horizontal one.
|
|
197
252
|
*/
|
|
198
253
|
export declare function stackBinExtent(ss: StackedBarSeries): [number, number] | null;
|
|
254
|
+
/**
|
|
255
|
+
* The value a stack's **first** segment rests on, in data units — the same
|
|
256
|
+
* `0`-clamped-into-the-domain rule {@link resolveBarBaseline} applies to a plain
|
|
257
|
+
* bar, read off whichever scale carries the stacked value (`yScale` when the
|
|
258
|
+
* bars grow up, `xScale` when they grow right).
|
|
259
|
+
*
|
|
260
|
+
* Both stack walks used to start at a literal `0`, which is right only while the
|
|
261
|
+
* domain contains zero — and a **log** domain never can. `yScale(0)` on a log
|
|
262
|
+
* scale is `NaN`, `fillRect` with a `NaN` argument is a silent canvas no-op, and
|
|
263
|
+
* the same rect feeds {@link stackAt} — so the bottom segment of every stack
|
|
264
|
+
* both vanished *and* became unhittable, with nothing to see but a stack that
|
|
265
|
+
* starts one segment up. The linear case is unaffected: the value extents pull
|
|
266
|
+
* `0` into the domain, so this returns exactly `0` and the geometry is
|
|
267
|
+
* unchanged.
|
|
268
|
+
*/
|
|
269
|
+
export declare function stackBase(orientation: Orientation, xScale: Scale, yScale: Scale): number;
|
|
199
270
|
/**
|
|
200
271
|
* The pixel rect `[x0, x1, yTop, yBottom]` (ascending on both axes) of bin `b`'s
|
|
201
272
|
* segment `g`, stacked so it sits atop `cumBefore` (the summed value of the
|