@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +451 -1
  2. package/dist/AreaChart.d.ts +47 -22
  3. package/dist/AreaChart.js +24 -1
  4. package/dist/BandChart.d.ts +29 -17
  5. package/dist/BarChart.d.ts +92 -49
  6. package/dist/BarChart.js +65 -8
  7. package/dist/BarList.d.ts +127 -0
  8. package/dist/BarList.js +84 -0
  9. package/dist/BoxList.d.ts +108 -0
  10. package/dist/BoxList.js +125 -0
  11. package/dist/BoxPlot.d.ts +63 -30
  12. package/dist/Candlestick.d.ts +5 -4
  13. package/dist/ChartRow.js +57 -6
  14. package/dist/Layers.js +22 -2
  15. package/dist/LineChart.d.ts +29 -26
  16. package/dist/ListTable.d.ts +51 -0
  17. package/dist/ListTable.js +143 -0
  18. package/dist/ScatterChart.d.ts +27 -17
  19. package/dist/YAxis.d.ts +29 -1
  20. package/dist/YAxis.js +19 -5
  21. package/dist/area.js +46 -15
  22. package/dist/band.js +13 -0
  23. package/dist/bars.d.ts +89 -18
  24. package/dist/bars.js +141 -30
  25. package/dist/column-names.d.ts +74 -0
  26. package/dist/column-names.js +2 -0
  27. package/dist/context.d.ts +40 -2
  28. package/dist/data.d.ts +19 -0
  29. package/dist/data.js +46 -0
  30. package/dist/dev.d.ts +2 -0
  31. package/dist/dev.js +2 -0
  32. package/dist/domain.d.ts +54 -1
  33. package/dist/domain.js +195 -2
  34. package/dist/format.d.ts +20 -0
  35. package/dist/format.js +23 -10
  36. package/dist/gaps.d.ts +33 -0
  37. package/dist/gaps.js +49 -0
  38. package/dist/index.d.ts +19 -13
  39. package/dist/index.js +21 -13
  40. package/dist/line.js +10 -1
  41. package/dist/list-source.d.ts +61 -0
  42. package/dist/list-source.js +5 -0
  43. package/dist/list.d.ts +205 -0
  44. package/dist/list.js +165 -0
  45. package/dist/theme.d.ts +42 -0
  46. package/dist/theme.js +24 -0
  47. package/dist/viewport.d.ts +9 -1
  48. package/dist/viewport.js +40 -4
  49. package/dist/yticks.d.ts +44 -0
  50. package/dist/yticks.js +55 -0
  51. 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
- ? scale.tickFormat(count, format)
24
- : scale.tickFormat(count);
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. The architecture (hard
6
- * layers: adapter typed-array store decimatorcanvas renderer → React
7
- * shell) is documented in the charts RFC at `docs/rfcs/charts.md`; the
8
- * milestone plan lives in `PLAN.md`. (A Path2D path cache was explored and
9
- * deferredit doesn't help the pan case, which re-decimates every frame; see
10
- * the [PND-DECIM] floor decision.)
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
- * **M1 rendering spine.** The layout shell + the first draw layer:
13
- * `<ChartContainer>` (time axis) `<ChartRow>` (y-axis + canvas) →
14
- * `<LineChart>` (a gap-aware line), fed from a pond `TimeSeries` via
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. The architecture (hard
6
- * layers: adapter typed-array store decimatorcanvas renderer → React
7
- * shell) is documented in the charts RFC at `docs/rfcs/charts.md`; the
8
- * milestone plan lives in `PLAN.md`. (A Path2D path cache was explored and
9
- * deferredit doesn't help the pan case, which re-decimates every frame; see
10
- * the [PND-DECIM] floor decision.)
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
- * **M1 rendering spine.** The layout shell + the first draw layer:
13
- * `<ChartContainer>` (time axis) `<ChartRow>` (y-axis + canvas) →
14
- * `<LineChart>` (a gap-aware line), fed from a pond `TimeSeries` via
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
@@ -0,0 +1,5 @@
1
+ /** Narrowing helper for the components' runtime read of the union. */
2
+ export function isSeriesSource(src) {
3
+ return src.series !== undefined;
4
+ }
5
+ //# sourceMappingURL=list-source.js.map