@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
package/dist/domain.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { scaleLinear } from 'd3-scale';
|
|
1
|
+
import { scaleLinear, scaleLog } from 'd3-scale';
|
|
2
2
|
/**
|
|
3
3
|
* Resolve a y-axis `[lo, hi]` domain from its explicit bounds and the extents of
|
|
4
4
|
* the layers linked to it. An `undefined` bound auto-fits the data: with no
|
|
@@ -20,8 +20,17 @@ import { scaleLinear } from 'd3-scale';
|
|
|
20
20
|
* `pad × span` on each side — headroom without hand-computing bounds, useful to
|
|
21
21
|
* lift a tight **explicit** domain off the plot edges. Applied last, to whatever
|
|
22
22
|
* domain was resolved (explicit or auto); `0` is a no-op.
|
|
23
|
+
*
|
|
24
|
+
* `scale` selects the spacing. `'log'` delegates to {@link resolveLogDomain},
|
|
25
|
+
* which applies **every policy above** — verbatim explicit bounds, an auto-fit
|
|
26
|
+
* side that moves rather than a caller's bound being discarded, `.nice()` on a
|
|
27
|
+
* fully auto-fit domain — and differs only where a log axis forces it to: a
|
|
28
|
+
* non-positive bound has no position and is refused, and `pad` is a fraction of
|
|
29
|
+
* the *decades* spanned rather than of the difference.
|
|
23
30
|
*/
|
|
24
|
-
export function resolveYDomain(min, max, extents, pad = 0) {
|
|
31
|
+
export function resolveYDomain(min, max, extents, pad = 0, scale = 'linear') {
|
|
32
|
+
if (scale === 'log')
|
|
33
|
+
return resolveLogDomain(min, max, extents, pad);
|
|
25
34
|
const result = resolveBase(min, max, extents);
|
|
26
35
|
if (pad) {
|
|
27
36
|
const [lo, hi] = result;
|
|
@@ -30,6 +39,190 @@ export function resolveYDomain(min, max, extents, pad = 0) {
|
|
|
30
39
|
}
|
|
31
40
|
return result;
|
|
32
41
|
}
|
|
42
|
+
/** Smallest positive value a log domain will fall back to when the data offers
|
|
43
|
+
* nothing positive at all. Arbitrary but finite — a log scale has no natural
|
|
44
|
+
* zero to anchor on, and `[0, 1]` (the linear empty-data domain) has no
|
|
45
|
+
* position for its own low end: `scaleLog().domain([0, 1])(x)` is `NaN` for
|
|
46
|
+
* every `x`, which poisons every coordinate drawn against it. */
|
|
47
|
+
const LOG_EMPTY_LO = 1;
|
|
48
|
+
const LOG_EMPTY_HI = 10;
|
|
49
|
+
/**
|
|
50
|
+
* The log analog of {@link resolveBase} + padding.
|
|
51
|
+
*
|
|
52
|
+
* The **policy** is deliberately identical to linear's, so `scale="log"` changes
|
|
53
|
+
* how a domain is spaced and not what the props mean:
|
|
54
|
+
*
|
|
55
|
+
* - **Two explicit bounds are verbatim** (an inverted pair is a deliberate axis
|
|
56
|
+
* flip; we don't second-guess it).
|
|
57
|
+
* - **A partial explicit bound is never discarded.** When one side is explicit
|
|
58
|
+
* and the resolved domain would invert, the **auto-fit** side moves — exactly
|
|
59
|
+
* as {@link resolveBase} does. (This inverted the caller's policy until the
|
|
60
|
+
* log-axis review: `resolveLogDomain(undefined, 100, [[1000, 2000]])` returned
|
|
61
|
+
* `[1000, 10000]`, silently throwing away the requested `max` and putting the
|
|
62
|
+
* axis three decades from where it was asked to be.)
|
|
63
|
+
* - **A fully auto-fit domain is `.nice()`d**, the promise {@link resolveYDomain}
|
|
64
|
+
* already documents. `scaleLog().nice()` extends to whole powers of ten, so
|
|
65
|
+
* the extremes get headroom instead of sitting clipped against the plot edge
|
|
66
|
+
* and the decade ticks land on the domain bounds.
|
|
67
|
+
*
|
|
68
|
+
* Two things differ from linear, and both are consequences of the same fact —
|
|
69
|
+
* that a log axis has no position for zero:
|
|
70
|
+
*
|
|
71
|
+
* - **Non-positive bounds are unusable.** Auto-fit takes the smallest
|
|
72
|
+
* *positive* extent rather than the smallest, so one zero sample doesn't
|
|
73
|
+
* collapse the axis; an explicit non-positive `min`/`max` is ignored in
|
|
74
|
+
* favour of the data (a caller asking for `min={0}` on a log axis has asked
|
|
75
|
+
* for `NaN` — see {@link logAxisWarning} — which we will not hand to a scale).
|
|
76
|
+
* A bound refused this way is treated as *absent* from here on, so the side
|
|
77
|
+
* that survives is still honoured as an explicit bound.
|
|
78
|
+
* - **Padding is multiplicative.** `pad` is a *fraction of the domain*, and on
|
|
79
|
+
* a log axis the domain's span is a ratio, not a difference. Padding
|
|
80
|
+
* additively would add a constant number of bytes to a decade — invisible at
|
|
81
|
+
* the top, enormous at the bottom. Applying it in log space adds the same
|
|
82
|
+
* *fraction of a decade* at both ends, which is what the linear behaviour
|
|
83
|
+
* looks like to the eye. (The expression is sign-correct on a flipped domain
|
|
84
|
+
* for the same reason linear's is: `log10(hi/lo)` goes negative, so both ends
|
|
85
|
+
* still move outward.)
|
|
86
|
+
*/
|
|
87
|
+
function resolveLogDomain(min, max, extents, pad) {
|
|
88
|
+
// A non-positive explicit bound is refused, and from here on is simply absent
|
|
89
|
+
// — so `max={1e6}` with a refused `min={0}` still behaves as "explicit top,
|
|
90
|
+
// auto-fit floor" rather than falling into the both-explicit branch.
|
|
91
|
+
const explicitLo = min !== undefined && min > 0 ? min : undefined;
|
|
92
|
+
const explicitHi = max !== undefined && max > 0 ? max : undefined;
|
|
93
|
+
const [lo, hi] = resolveLogBase(explicitLo, explicitHi, extents);
|
|
94
|
+
if (!pad)
|
|
95
|
+
return [lo, hi];
|
|
96
|
+
// A fraction of the *decades* spanned, added at each end.
|
|
97
|
+
const decades = Math.log10(hi / lo) * pad;
|
|
98
|
+
return [lo / 10 ** decades, hi * 10 ** decades];
|
|
99
|
+
}
|
|
100
|
+
/** {@link resolveLogDomain} minus the padding — the domain itself. */
|
|
101
|
+
function resolveLogBase(explicitLo, explicitHi, extents) {
|
|
102
|
+
// Both bounds explicit (and positive): trust them verbatim, matching
|
|
103
|
+
// `resolveBase` — including an intentional flip.
|
|
104
|
+
if (explicitLo !== undefined && explicitHi !== undefined) {
|
|
105
|
+
return [explicitLo, explicitHi];
|
|
106
|
+
}
|
|
107
|
+
let dataLo = Infinity;
|
|
108
|
+
let dataHi = -Infinity;
|
|
109
|
+
for (const e of extents) {
|
|
110
|
+
if (!e)
|
|
111
|
+
continue;
|
|
112
|
+
// The low end walks both ends of the extent: a series whose min is 0 or
|
|
113
|
+
// negative can still have a positive max, and that max is the only
|
|
114
|
+
// positive floor it can offer.
|
|
115
|
+
if (e[0] > 0 && e[0] < dataLo)
|
|
116
|
+
dataLo = e[0];
|
|
117
|
+
else if (e[1] > 0 && e[1] < dataLo)
|
|
118
|
+
dataLo = e[1];
|
|
119
|
+
if (e[1] > dataHi)
|
|
120
|
+
dataHi = e[1];
|
|
121
|
+
}
|
|
122
|
+
if (dataLo === Infinity || !(dataHi > 0)) {
|
|
123
|
+
// Nothing positive to fit — the log counterpart of linear's `[0, 1]`.
|
|
124
|
+
dataLo = LOG_EMPTY_LO;
|
|
125
|
+
dataHi = LOG_EMPTY_HI;
|
|
126
|
+
}
|
|
127
|
+
else if (dataLo === dataHi) {
|
|
128
|
+
// Flat — give it room, so a constant line sits mid-row rather than on an
|
|
129
|
+
// edge (linear's ±1, expressed as a ratio: half a decade each way).
|
|
130
|
+
dataLo /= 10 ** 0.5;
|
|
131
|
+
dataHi *= 10 ** 0.5;
|
|
132
|
+
}
|
|
133
|
+
let lo = explicitLo ?? dataLo;
|
|
134
|
+
let hi = explicitHi ?? dataHi;
|
|
135
|
+
// A partial explicit bound can sit at or past the auto-fit other side. Keep
|
|
136
|
+
// the axis ascending by moving the *auto-fit* side — never discard the
|
|
137
|
+
// caller's explicit bound. Exactly one side is explicit here: both-explicit
|
|
138
|
+
// returned early, and a both-auto domain can't invert after the guards above.
|
|
139
|
+
if (lo >= hi) {
|
|
140
|
+
if (explicitLo === undefined)
|
|
141
|
+
lo = hi / 10; // hi is explicit → preserve it
|
|
142
|
+
else
|
|
143
|
+
hi = lo * 10; // lo is explicit → preserve it
|
|
144
|
+
}
|
|
145
|
+
// Fully auto-fit → round out to whole powers of ten, for headroom and so the
|
|
146
|
+
// decade ticks reach the domain bounds. A partial/full explicit bound is left
|
|
147
|
+
// exact, exactly as `resolveBase` leaves it.
|
|
148
|
+
if (explicitLo === undefined && explicitHi === undefined) {
|
|
149
|
+
return scaleLog().domain([lo, hi]).nice().domain();
|
|
150
|
+
}
|
|
151
|
+
return [lo, hi];
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Does resolving this axis's domain need its layers' extents walked?
|
|
155
|
+
* `yExtent()` is O(points) per layer, so the caller only pays it when a side
|
|
156
|
+
* actually auto-fits.
|
|
157
|
+
*
|
|
158
|
+
* A log axis **refuses a non-positive bound** ({@link resolveLogDomain}), which
|
|
159
|
+
* means such a bound is not a bound: that side auto-fits and needs the data. The
|
|
160
|
+
* naive `min === undefined || max === undefined` test misses this, and the miss
|
|
161
|
+
* is silent — `<YAxis scale="log" min={0} max={1e6}>` looked fully explicit, so
|
|
162
|
+
* no extents were gathered, so the refused floor fell back to the empty-data
|
|
163
|
+
* placeholder instead of the data's own floor. (`resolveLogDomain`'s unit tests
|
|
164
|
+
* passed throughout: they hand it the extents directly, which is precisely what
|
|
165
|
+
* the component was not doing.)
|
|
166
|
+
*/
|
|
167
|
+
export function needsExtents(axis) {
|
|
168
|
+
if (axis.scale === 'log') {
|
|
169
|
+
return (!(axis.min !== undefined && axis.min > 0) ||
|
|
170
|
+
!(axis.max !== undefined && axis.max > 0));
|
|
171
|
+
}
|
|
172
|
+
return axis.min === undefined || axis.max === undefined;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The dev-mode complaint a `scale="log"` axis has about its own bounds and the
|
|
176
|
+
* data linked to it, or `null` when it has none. Pure, so the policy is unit
|
|
177
|
+
* tested directly rather than through a rendered console spy.
|
|
178
|
+
*
|
|
179
|
+
* **Every case here is unambiguous**, which is the whole design constraint. The
|
|
180
|
+
* previous version warned whenever a linked extent reached zero, and that fires
|
|
181
|
+
* on *every* `BarChart` — `barExtent` always widens its low end to `0` so a bar
|
|
182
|
+
* can reach its baseline, whether or not the data goes anywhere near it. So the
|
|
183
|
+
* `WithBars` story warned, on strictly positive data, with text asserting
|
|
184
|
+
* something false about it. A dev warning that cries wolf gets muted, and then
|
|
185
|
+
* the real ones are lost too.
|
|
186
|
+
*
|
|
187
|
+
* The cost of that precision is the one genuinely ambiguous shape: an extent of
|
|
188
|
+
* exactly `[0, hi]`, which is what a line touching zero *and* a bar layer on
|
|
189
|
+
* positive data both report. It is not warned about. That case is no longer
|
|
190
|
+
* silent, though — a sample with no position on the axis now renders as a
|
|
191
|
+
* **gap** rather than being bridged straight over, so the picture itself says
|
|
192
|
+
* the data is missing there.
|
|
193
|
+
*/
|
|
194
|
+
export function logAxisWarning(axis, extents) {
|
|
195
|
+
if (axis.scale !== 'log')
|
|
196
|
+
return null;
|
|
197
|
+
const reasons = [];
|
|
198
|
+
// `!(x > 0)` rather than `x <= 0` so a NaN bound is caught too — it is refused
|
|
199
|
+
// by the same rule and is just as invisible.
|
|
200
|
+
if (axis.min !== undefined && !(axis.min > 0)) {
|
|
201
|
+
reasons.push(`min={${axis.min}} was ignored (it is not a positive number)`);
|
|
202
|
+
}
|
|
203
|
+
if (axis.max !== undefined && !(axis.max > 0)) {
|
|
204
|
+
reasons.push(`max={${axis.max}} was ignored (it is not a positive number)`);
|
|
205
|
+
}
|
|
206
|
+
const present = extents.filter((e) => e !== null);
|
|
207
|
+
if (present.some((e) => e[0] < 0)) {
|
|
208
|
+
reasons.push('data linked to this axis includes negative values');
|
|
209
|
+
}
|
|
210
|
+
// Independent of the above, not an `else`: an axis whose data is *entirely*
|
|
211
|
+
// non-positive is both negative-valued and undrawable, and the second fact is
|
|
212
|
+
// the one that explains the empty plot.
|
|
213
|
+
if (present.length > 0 && !present.some((e) => e[1] > 0)) {
|
|
214
|
+
// There is data, and none of it is positive — the whole axis is a fallback
|
|
215
|
+
// domain and nothing will be drawn against it.
|
|
216
|
+
reasons.push(`no data linked to this axis is positive, so the domain fell back to ` +
|
|
217
|
+
`[${LOG_EMPTY_LO}, ${LOG_EMPTY_HI}]`);
|
|
218
|
+
}
|
|
219
|
+
if (reasons.length === 0)
|
|
220
|
+
return null;
|
|
221
|
+
return (`<YAxis id="${axis.id}" scale="log">: ${reasons.join('; ')}. ` +
|
|
222
|
+
'A log scale has no position for zero or negative numbers (d3 maps them ' +
|
|
223
|
+
'to NaN), so such values are refused as bounds and are not drawn. Filter ' +
|
|
224
|
+
'them out, or use the default linear scale.');
|
|
225
|
+
}
|
|
33
226
|
function resolveBase(min, max, extents) {
|
|
34
227
|
// Both bounds explicit: trust them verbatim (allows an intentional flip).
|
|
35
228
|
if (min !== undefined && max !== undefined)
|
package/dist/format.d.ts
CHANGED
|
@@ -45,6 +45,9 @@ export type CursorFormat = string | ((value: number, ctx: {
|
|
|
45
45
|
* optional specifier. A d3 `ScaleLinear` / `ScaleTime` satisfies it. */
|
|
46
46
|
interface Tickable {
|
|
47
47
|
tickFormat(count: number, specifier?: string): (value: number) => string;
|
|
48
|
+
/** Present on d3's `scaleLog` and on no other continuous scale. */
|
|
49
|
+
base?: () => number;
|
|
50
|
+
domain?: () => number[];
|
|
48
51
|
}
|
|
49
52
|
/**
|
|
50
53
|
* Resolve an {@link AxisFormat} (or `undefined`) to a `(value) => string`
|
|
@@ -55,6 +58,23 @@ interface Tickable {
|
|
|
55
58
|
* - a **specifier string** → `scale.tickFormat(count, specifier)` — d3 applies
|
|
56
59
|
* the specifier, so the readout matches ticks formatted the same way;
|
|
57
60
|
* - **`undefined`** → `scale.tickFormat(count)` — the scale's default.
|
|
61
|
+
*
|
|
62
|
+
* **A log scale is formatted through a linear one over the same domain.**
|
|
63
|
+
* `scaleLog.tickFormat` is not a value formatter — it is a *tick* formatter,
|
|
64
|
+
* and it deliberately returns `''` for anything it doesn't consider a
|
|
65
|
+
* significant tick:
|
|
66
|
+
*
|
|
67
|
+
* ```
|
|
68
|
+
* scaleLog().domain([1.9e10, 2.6e17]).tickFormat(6, '.3s')(1.97e17) === ''
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* That is correct for axis labels (it is how a log axis thins them) and
|
|
72
|
+
* catastrophic for the cursor readout, `YAxisIndicator` and `Baseline` chips,
|
|
73
|
+
* which share this formatter and ask it about arbitrary values — they would
|
|
74
|
+
* render blank for almost every real number. A linear scale's `tickFormat`
|
|
75
|
+
* applies the specifier to whatever it is handed, which is what every consumer
|
|
76
|
+
* of this function actually wants; the axis's own tick *thinning* is handled by
|
|
77
|
+
* `yTickValues`, not here.
|
|
58
78
|
*/
|
|
59
79
|
export declare function resolveAxisFormat(scale: Tickable, count: number, format: AxisFormat | undefined): (value: number) => string;
|
|
60
80
|
/** The slice of a d3 **time** scale {@link resolveTimeFormat} needs. A d3
|
package/dist/format.js
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Axis value formatting — the single formatter shared by an axis's tick labels
|
|
3
|
-
* and the cursor readout, so a value reads the same in both places (the readout
|
|
4
|
-
* "matches the axes"). d3-style: a format **specifier string** (e.g. `'.0%'`,
|
|
5
|
-
* `',.2f'`) is applied through the scale's own `tickFormat` (the same path d3
|
|
6
|
-
* uses for the ticks), a **function** is used verbatim, and `undefined` falls
|
|
7
|
-
* back to the scale's default `tickFormat`.
|
|
8
|
-
*/
|
|
1
|
+
import { scaleLinear } from 'd3-scale';
|
|
9
2
|
/**
|
|
10
3
|
* Resolve an {@link AxisFormat} (or `undefined`) to a `(value) => string`
|
|
11
4
|
* formatter, given the `scale` it formats against and the axis `count` (so the
|
|
@@ -15,13 +8,33 @@
|
|
|
15
8
|
* - a **specifier string** → `scale.tickFormat(count, specifier)` — d3 applies
|
|
16
9
|
* the specifier, so the readout matches ticks formatted the same way;
|
|
17
10
|
* - **`undefined`** → `scale.tickFormat(count)` — the scale's default.
|
|
11
|
+
*
|
|
12
|
+
* **A log scale is formatted through a linear one over the same domain.**
|
|
13
|
+
* `scaleLog.tickFormat` is not a value formatter — it is a *tick* formatter,
|
|
14
|
+
* and it deliberately returns `''` for anything it doesn't consider a
|
|
15
|
+
* significant tick:
|
|
16
|
+
*
|
|
17
|
+
* ```
|
|
18
|
+
* scaleLog().domain([1.9e10, 2.6e17]).tickFormat(6, '.3s')(1.97e17) === ''
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* That is correct for axis labels (it is how a log axis thins them) and
|
|
22
|
+
* catastrophic for the cursor readout, `YAxisIndicator` and `Baseline` chips,
|
|
23
|
+
* which share this formatter and ask it about arbitrary values — they would
|
|
24
|
+
* render blank for almost every real number. A linear scale's `tickFormat`
|
|
25
|
+
* applies the specifier to whatever it is handed, which is what every consumer
|
|
26
|
+
* of this function actually wants; the axis's own tick *thinning* is handled by
|
|
27
|
+
* `yTickValues`, not here.
|
|
18
28
|
*/
|
|
19
29
|
export function resolveAxisFormat(scale, count, format) {
|
|
20
30
|
if (typeof format === 'function')
|
|
21
31
|
return format;
|
|
32
|
+
const source = typeof scale.base === 'function' && typeof scale.domain === 'function'
|
|
33
|
+
? scaleLinear().domain(scale.domain())
|
|
34
|
+
: scale;
|
|
22
35
|
return format !== undefined
|
|
23
|
-
?
|
|
24
|
-
:
|
|
36
|
+
? source.tickFormat(count, format)
|
|
37
|
+
: source.tickFormat(count);
|
|
25
38
|
}
|
|
26
39
|
/**
|
|
27
40
|
* The time analog of {@link resolveAxisFormat} — resolve an {@link AxisFormat}
|
package/dist/gaps.d.ts
CHANGED
|
@@ -72,6 +72,39 @@ export interface GapEdge {
|
|
|
72
72
|
/** Pixel y of the next-good sample (`toIndex`). */
|
|
73
73
|
readonly toY: number;
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Return `values` with every entry that has **no position on `scale`** replaced
|
|
77
|
+
* by `NaN` — i.e. converted into the gap signal the rest of this module, the
|
|
78
|
+
* `.defined` predicates, and the affine draw loops already speak. Returns the
|
|
79
|
+
* input array itself (no copy) when nothing needs gapping, which is the
|
|
80
|
+
* overwhelmingly common case.
|
|
81
|
+
*
|
|
82
|
+
* **Why the raw value is not the right test.** Every gap check in this package
|
|
83
|
+
* asks `Number.isFinite(value)`, and `0` is finite — so on a **log** axis a zero
|
|
84
|
+
* sample sailed through as real data, d3 emitted `lineTo(x, NaN)` for it, and
|
|
85
|
+
* the canvas spec says a path op with a non-finite coordinate is *dropped*. Not
|
|
86
|
+
* drawn as a break: dropped. The pen stays where it was, the next point draws
|
|
87
|
+
* from there, and the two neighbours of the missing sample join with a straight
|
|
88
|
+
* line — a bridge over absent data, which is the one thing
|
|
89
|
+
* `docs/rfcs/charts.md` trap #2 exists to prevent, arrived at silently by a
|
|
90
|
+
* chart that had asked for the honest `'empty'` mode.
|
|
91
|
+
*
|
|
92
|
+
* Testing the **scaled** coordinate instead is both the honest answer and the
|
|
93
|
+
* general one: it needs no knowledge of which scale kind is in play, and any
|
|
94
|
+
* future scale with a restricted domain (`sqrt`, `pow` with a fractional
|
|
95
|
+
* exponent) inherits it. Normalizing to `NaN` here rather than special-casing
|
|
96
|
+
* three `.defined` predicates means the gap *modes* keep working too — a
|
|
97
|
+
* `'dashed'` connector spans it, `'none'` interpolates across it, `'fade'`
|
|
98
|
+
* drops to the floor at it — instead of the value being invisible to
|
|
99
|
+
* {@link collectGapEdges} and {@link bridgeGaps}, which read the values directly.
|
|
100
|
+
*
|
|
101
|
+
* **Costs nothing on a linear axis.** An affine scale ({@link affineOf}) maps
|
|
102
|
+
* every finite value to a finite pixel by construction, so there is nothing to
|
|
103
|
+
* find and the walk is skipped outright. That is also exactly the condition
|
|
104
|
+
* under which the callers take their affine fast path, so the O(N) probe only
|
|
105
|
+
* ever runs where the draw was already on the d3-scale path.
|
|
106
|
+
*/
|
|
107
|
+
export declare function gapUnscalable(values: Float64Array, length: number, scale: Scale): Float64Array;
|
|
75
108
|
/**
|
|
76
109
|
* Return a copy of `values` with **interior** gaps (NaN runs that have a finite
|
|
77
110
|
* sample on each side) linearly interpolated across, for the `none` mode — so d3
|
package/dist/gaps.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { affineOf } from './affine.js';
|
|
1
2
|
/** The default gap mode — break at the gap, leave a hole (today's behavior). */
|
|
2
3
|
export const DEFAULT_GAP_MODE = 'empty';
|
|
3
4
|
/**
|
|
@@ -6,6 +7,54 @@ export const DEFAULT_GAP_MODE = 'empty';
|
|
|
6
7
|
* inferred bridge reads as secondary to measured data.
|
|
7
8
|
*/
|
|
8
9
|
export const DEFAULT_GAP_CONNECTOR_OPACITY = 0.5;
|
|
10
|
+
/**
|
|
11
|
+
* Return `values` with every entry that has **no position on `scale`** replaced
|
|
12
|
+
* by `NaN` — i.e. converted into the gap signal the rest of this module, the
|
|
13
|
+
* `.defined` predicates, and the affine draw loops already speak. Returns the
|
|
14
|
+
* input array itself (no copy) when nothing needs gapping, which is the
|
|
15
|
+
* overwhelmingly common case.
|
|
16
|
+
*
|
|
17
|
+
* **Why the raw value is not the right test.** Every gap check in this package
|
|
18
|
+
* asks `Number.isFinite(value)`, and `0` is finite — so on a **log** axis a zero
|
|
19
|
+
* sample sailed through as real data, d3 emitted `lineTo(x, NaN)` for it, and
|
|
20
|
+
* the canvas spec says a path op with a non-finite coordinate is *dropped*. Not
|
|
21
|
+
* drawn as a break: dropped. The pen stays where it was, the next point draws
|
|
22
|
+
* from there, and the two neighbours of the missing sample join with a straight
|
|
23
|
+
* line — a bridge over absent data, which is the one thing
|
|
24
|
+
* `docs/rfcs/charts.md` trap #2 exists to prevent, arrived at silently by a
|
|
25
|
+
* chart that had asked for the honest `'empty'` mode.
|
|
26
|
+
*
|
|
27
|
+
* Testing the **scaled** coordinate instead is both the honest answer and the
|
|
28
|
+
* general one: it needs no knowledge of which scale kind is in play, and any
|
|
29
|
+
* future scale with a restricted domain (`sqrt`, `pow` with a fractional
|
|
30
|
+
* exponent) inherits it. Normalizing to `NaN` here rather than special-casing
|
|
31
|
+
* three `.defined` predicates means the gap *modes* keep working too — a
|
|
32
|
+
* `'dashed'` connector spans it, `'none'` interpolates across it, `'fade'`
|
|
33
|
+
* drops to the floor at it — instead of the value being invisible to
|
|
34
|
+
* {@link collectGapEdges} and {@link bridgeGaps}, which read the values directly.
|
|
35
|
+
*
|
|
36
|
+
* **Costs nothing on a linear axis.** An affine scale ({@link affineOf}) maps
|
|
37
|
+
* every finite value to a finite pixel by construction, so there is nothing to
|
|
38
|
+
* find and the walk is skipped outright. That is also exactly the condition
|
|
39
|
+
* under which the callers take their affine fast path, so the O(N) probe only
|
|
40
|
+
* ever runs where the draw was already on the d3-scale path.
|
|
41
|
+
*/
|
|
42
|
+
export function gapUnscalable(values, length, scale) {
|
|
43
|
+
if (affineOf(scale) !== null)
|
|
44
|
+
return values;
|
|
45
|
+
let out = null;
|
|
46
|
+
for (let i = 0; i < length; i += 1) {
|
|
47
|
+
const v = values[i];
|
|
48
|
+
if (!Number.isFinite(v) || Number.isFinite(scale(v)))
|
|
49
|
+
continue;
|
|
50
|
+
// Copy lazily — a log axis whose data never touches zero (the normal case)
|
|
51
|
+
// pays a scan and no allocation.
|
|
52
|
+
if (out === null)
|
|
53
|
+
out = values.slice();
|
|
54
|
+
out[i] = NaN;
|
|
55
|
+
}
|
|
56
|
+
return out ?? values;
|
|
57
|
+
}
|
|
9
58
|
/**
|
|
10
59
|
* Return a copy of `values` with **interior** gaps (NaN runs that have a finite
|
|
11
60
|
* sample on each side) linearly interpolated across, for the `none` mode — so d3
|
package/dist/index.d.ts
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
* `@pond-ts/charts` — the visualization end of pond.
|
|
3
3
|
*
|
|
4
4
|
* Canvas-rendered, streaming-first time-series charts with a
|
|
5
|
-
* react-timeseries-charts-style declarative layout
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* react-timeseries-charts-style declarative layout: compose
|
|
6
|
+
* `<ChartContainer>` → `<ChartRow>` → `<Layers>` → draw layers
|
|
7
|
+
* (line/area/band/scatter/bar/box/candle), plus the standalone DOM row lists
|
|
8
|
+
* ({@link BarList} / {@link BoxList}). **The data contract is the pond series
|
|
9
|
+
* itself** — every layer takes a `TimeSeries` / `ValueSeries` (or a
|
|
10
|
+
* partition `Map`, `byColumn` bins, category records) directly and shapes
|
|
11
|
+
* internally; an adapter you must call when starting from a series is an API
|
|
12
|
+
* failure (`docs/notes/charts-api-review-2026-08.md`). The exported `from*`
|
|
13
|
+
* view builders are **interop escape hatches** for non-pond data only.
|
|
11
14
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* {@link fromTimeSeries}. Axes, themes, the variance band, and interactions
|
|
16
|
-
* land in M2–M4. {@link Canvas} is the low-level DPR-aware primitive the rows
|
|
17
|
-
* sit on.
|
|
15
|
+
* Architecture (typed-array store → decimator → canvas renderer → React
|
|
16
|
+
* shell): `docs/rfcs/charts.md`; roadmap: `PLAN.md`. {@link Canvas} is the
|
|
17
|
+
* low-level DPR-aware primitive the rows sit on.
|
|
18
18
|
*
|
|
19
19
|
* @packageDocumentation
|
|
20
20
|
*/
|
|
@@ -49,6 +49,12 @@ export type { BarChartProps } from './BarChart.js';
|
|
|
49
49
|
export { Candlestick } from './Candlestick.js';
|
|
50
50
|
export type { CandlestickProps } from './Candlestick.js';
|
|
51
51
|
export type { CandleVariant, ColorBy } from './ohlc.js';
|
|
52
|
+
export { BarList } from './BarList.js';
|
|
53
|
+
export type { BarListProps } from './BarList.js';
|
|
54
|
+
export { BoxList } from './BoxList.js';
|
|
55
|
+
export type { BoxListProps } from './BoxList.js';
|
|
56
|
+
export { listRowsFromTimeSeries, listRowsFromValueSeries } from './list.js';
|
|
57
|
+
export type { ListRow, ListValue, ListCellSpec, ListMarker, ListSortDirection, ListRowsOptions, BarListColumn, BoxListColumn, } from './list.js';
|
|
52
58
|
export { Legend } from './Legend.js';
|
|
53
59
|
export type { LegendProps, LegendPlacement } from './Legend.js';
|
|
54
60
|
export type { SwatchSpec, LegendItemInput } from './swatch.js';
|
|
@@ -63,7 +69,7 @@ export type { RegionProps, BaselineProps, MarkerProps } from './annotations.js';
|
|
|
63
69
|
export type { AnnotationKind, CreateSpec } from './context.js';
|
|
64
70
|
export { YAxisIndicator, createLiveValue } from './indicators.js';
|
|
65
71
|
export type { YAxisIndicatorProps, LiveValue } from './indicators.js';
|
|
66
|
-
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, ohlcFromTimeSeries, stacksFromGroups, stacksFromColumns, stacksFromBins, categoryStack, transposeRow, } from './data.js';
|
|
72
|
+
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, barsFromBins, ohlcFromTimeSeries, stacksFromGroups, stacksFromColumns, stacksFromBins, categoryStack, transposeRow, } from './data.js';
|
|
67
73
|
export type { ChartSeries, BandSeries, BoxSeries, BoxColumns, BarSeries, OhlcSeries, OhlcColumns, StackedBarSeries, BinRecord, StacksFromBinsOptions, CategoryDatum, RowAt, TransposeRowOptions, } from './data.js';
|
|
68
74
|
export type { Orientation } from './bars.js';
|
|
69
75
|
export type { RadiusEncoding, ColorEncoding } from './encoding.js';
|
package/dist/index.js
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
* `@pond-ts/charts` — the visualization end of pond.
|
|
3
3
|
*
|
|
4
4
|
* Canvas-rendered, streaming-first time-series charts with a
|
|
5
|
-
* react-timeseries-charts-style declarative layout
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* react-timeseries-charts-style declarative layout: compose
|
|
6
|
+
* `<ChartContainer>` → `<ChartRow>` → `<Layers>` → draw layers
|
|
7
|
+
* (line/area/band/scatter/bar/box/candle), plus the standalone DOM row lists
|
|
8
|
+
* ({@link BarList} / {@link BoxList}). **The data contract is the pond series
|
|
9
|
+
* itself** — every layer takes a `TimeSeries` / `ValueSeries` (or a
|
|
10
|
+
* partition `Map`, `byColumn` bins, category records) directly and shapes
|
|
11
|
+
* internally; an adapter you must call when starting from a series is an API
|
|
12
|
+
* failure (`docs/notes/charts-api-review-2026-08.md`). The exported `from*`
|
|
13
|
+
* view builders are **interop escape hatches** for non-pond data only.
|
|
11
14
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* {@link fromTimeSeries}. Axes, themes, the variance band, and interactions
|
|
16
|
-
* land in M2–M4. {@link Canvas} is the low-level DPR-aware primitive the rows
|
|
17
|
-
* sit on.
|
|
15
|
+
* Architecture (typed-array store → decimator → canvas renderer → React
|
|
16
|
+
* shell): `docs/rfcs/charts.md`; roadmap: `PLAN.md`. {@link Canvas} is the
|
|
17
|
+
* low-level DPR-aware primitive the rows sit on.
|
|
18
18
|
*
|
|
19
19
|
* @packageDocumentation
|
|
20
20
|
*/
|
|
@@ -33,6 +33,14 @@ export { ScatterChart } from './ScatterChart.js';
|
|
|
33
33
|
export { BoxPlot } from './BoxPlot.js';
|
|
34
34
|
export { BarChart } from './BarChart.js';
|
|
35
35
|
export { Candlestick } from './Candlestick.js';
|
|
36
|
+
// The list family — DOM-rendered *ranked row lists* (the react-timeseries-charts
|
|
37
|
+
// `HorizontalBarChart` shape, reconceived as a table): one row per entity, a
|
|
38
|
+
// proportional bar / five-number box line per configured column, data cells,
|
|
39
|
+
// custom sort, per-row expander. Standalone — no <ChartContainer> (the in-plot
|
|
40
|
+
// horizontal bars remain `<BarChart orientation="horizontal">`).
|
|
41
|
+
export { BarList } from './BarList.js';
|
|
42
|
+
export { BoxList } from './BoxList.js';
|
|
43
|
+
export { listRowsFromTimeSeries, listRowsFromValueSeries } from './list.js';
|
|
36
44
|
// The series key: rows enumerate the registered layers' resolved styles.
|
|
37
45
|
export { Legend } from './Legend.js';
|
|
38
46
|
// The headless legend — the same rows + hover/select sync as data, for
|
|
@@ -48,7 +56,7 @@ export { Region, Baseline, Marker } from './annotations.js';
|
|
|
48
56
|
// Axis indicators — a value pill pinned to an axis edge (the ChartIQ live tag).
|
|
49
57
|
// `createLiveValue` is the high-frequency, isolated-repaint update path.
|
|
50
58
|
export { YAxisIndicator, createLiveValue } from './indicators.js';
|
|
51
|
-
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, ohlcFromTimeSeries,
|
|
59
|
+
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, barsFromBins, ohlcFromTimeSeries,
|
|
52
60
|
// Stacked / histogram readers — assemble a StackedBarSeries from pond's own
|
|
53
61
|
// aggregation output: a Map of grouped series, a wide series, or byColumn bins.
|
|
54
62
|
stacksFromGroups, stacksFromColumns, stacksFromBins,
|
package/dist/line.js
CHANGED
|
@@ -2,7 +2,7 @@ import { line as d3line, curveLinear } from 'd3-shape';
|
|
|
2
2
|
import { cullChartSeries } from './culling.js';
|
|
3
3
|
import { decimateM4Cached } from './decimate.js';
|
|
4
4
|
import { affineOf } from './affine.js';
|
|
5
|
-
import { bridgeGaps, collectGapEdges, drawGapBridges, drawGapFades, drawGapSteps, DEFAULT_GAP_MODE, DEFAULT_GAP_CONNECTOR_OPACITY, } from './gaps.js';
|
|
5
|
+
import { bridgeGaps, collectGapEdges, drawGapBridges, drawGapFades, drawGapSteps, gapUnscalable, DEFAULT_GAP_MODE, DEFAULT_GAP_CONNECTOR_OPACITY, } from './gaps.js';
|
|
6
6
|
/** Shared empty boundary list — passed to `sessionRuns` when a decimated series
|
|
7
7
|
* already carries its session breaks as baked-in `NaN` points. */
|
|
8
8
|
const EMPTY_BOUNDARIES = [];
|
|
@@ -131,6 +131,15 @@ export function drawLine(ctx, cs, xScale, yScale, style, curve = curveLinear, ga
|
|
|
131
131
|
else {
|
|
132
132
|
cs = cullChartSeries(source, xScale);
|
|
133
133
|
}
|
|
134
|
+
// Normalize any value with **no position on the y scale** (a zero or negative
|
|
135
|
+
// sample on a log axis) into the ordinary NaN gap signal, so every consumer
|
|
136
|
+
// below — the `.defined` break, `bridgeGaps`, `collectGapEdges` — treats it as
|
|
137
|
+
// the absence it is instead of emitting a dropped `lineTo(x, NaN)` that
|
|
138
|
+
// silently bridges its neighbours. A no-op on an affine (linear) y scale; see
|
|
139
|
+
// {@link gapUnscalable}.
|
|
140
|
+
const scaledY = gapUnscalable(cs.y, cs.length, yScale);
|
|
141
|
+
if (scaledY !== cs.y)
|
|
142
|
+
cs = { ...cs, y: scaledY };
|
|
134
143
|
// Split into independent index runs at each boundary; no boundary inside the
|
|
135
144
|
// data ⇒ one run over the whole series (the hot path — no slicing, so the draw
|
|
136
145
|
// is byte-identical to the pre-boundary single pass). When the series was
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The list family's **source union** — `rows` XOR `series`, shared by
|
|
3
|
+
* {@link BarList} and {@link BoxList} ([PND-CHARTAPI]).
|
|
4
|
+
*
|
|
5
|
+
* Two things this fixes, both flagged on #590's review and deferred to here
|
|
6
|
+
* so one union pattern serves the layers and the lists alike:
|
|
7
|
+
*
|
|
8
|
+
* 1. **The doors were both optional**, so `<BarList columns={…} />` with
|
|
9
|
+
* neither source, and `rows` + `series` together, type-checked and threw at
|
|
10
|
+
* render. Now each is a compile error.
|
|
11
|
+
* 2. **The generic row type could lie.** `R` is inferred from the *callbacks*
|
|
12
|
+
* (`sort`, cell `render`, `renderExpanded`, `onRowClick`), so merely
|
|
13
|
+
* annotating one of them while passing `series` inferred a custom `R` that
|
|
14
|
+
* the series door cannot honour — it produces plain {@link ListRow}s. The
|
|
15
|
+
* series member pins `R` to `ListRow`, so the annotation now fails to
|
|
16
|
+
* compile instead of lying at runtime.
|
|
17
|
+
*/
|
|
18
|
+
import type { ReactNode } from 'react';
|
|
19
|
+
import type { SeriesSchema, TimeSeries, ValueSeries, ValueSeriesSchema } from 'pond-ts';
|
|
20
|
+
import type { ListRow, ListRowsOptions } from './list.js';
|
|
21
|
+
/**
|
|
22
|
+
* The **record door**: entity rows built by hand or from partition facts.
|
|
23
|
+
* `R` may extend {@link ListRow} with extra fields, which then flow into
|
|
24
|
+
* every callback fully typed.
|
|
25
|
+
*/
|
|
26
|
+
export interface ListRowsSource<R extends ListRow> {
|
|
27
|
+
readonly rows: readonly R[];
|
|
28
|
+
readonly series?: never;
|
|
29
|
+
readonly label?: never;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The **series door**: one row per event, read internally — starting from a
|
|
33
|
+
* pond series there is no shaping step. Rows are plain {@link ListRow}s (the
|
|
34
|
+
* reader cannot know a caller's custom row shape), so this member does not
|
|
35
|
+
* carry `R`.
|
|
36
|
+
*/
|
|
37
|
+
export interface ListSeriesSource<S extends SeriesSchema, VS extends ValueSeriesSchema> {
|
|
38
|
+
readonly series: TimeSeries<S> | ValueSeries<VS>;
|
|
39
|
+
readonly rows?: never;
|
|
40
|
+
/**
|
|
41
|
+
* The built-in label cell per row — from the row's ordinal and its axis key
|
|
42
|
+
* (epoch ms / axis value). **Omitted ⇒ the stringified key renders.**
|
|
43
|
+
*/
|
|
44
|
+
readonly label?: ListRowsOptions['label'];
|
|
45
|
+
}
|
|
46
|
+
/** `rows` XOR `series` — exactly one door, enforced at compile time. */
|
|
47
|
+
export type ListSource<R extends ListRow, S extends SeriesSchema, VS extends ValueSeriesSchema> = ListRowsSource<R> | ListSeriesSource<S, VS>;
|
|
48
|
+
/**
|
|
49
|
+
* The row type a given source yields: a caller's `R` through the record door,
|
|
50
|
+
* plain {@link ListRow} through the series door. Callback props resolve
|
|
51
|
+
* against this, which is what stops the series door from claiming a custom
|
|
52
|
+
* row shape it cannot produce.
|
|
53
|
+
*/
|
|
54
|
+
export type RowOf<Src> = Src extends {
|
|
55
|
+
rows: readonly (infer T)[];
|
|
56
|
+
} ? T extends ListRow ? T : ListRow : ListRow;
|
|
57
|
+
/** Narrowing helper for the components' runtime read of the union. */
|
|
58
|
+
export declare function isSeriesSource<R extends ListRow, S extends SeriesSchema, VS extends ValueSeriesSchema>(src: ListSource<R, S, VS>): src is ListSeriesSource<S, VS>;
|
|
59
|
+
/** Re-exported for the components' prop docs. */
|
|
60
|
+
export type ListLabel = (i: number, key: number) => ReactNode;
|
|
61
|
+
//# sourceMappingURL=list-source.d.ts.map
|