@pond-ts/charts 0.55.0 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,7 +8,11 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`,
8
8
  under a single `v*` tag, so this file covers them all. Pre-1.0: minor bumps may
9
9
  include new features and type-level changes; patch bumps are strictly additive.
10
10
 
11
- [Unreleased]: https://github.com/pond-ts/pond/compare/v0.55.0...HEAD
11
+ [Unreleased]: https://github.com/pond-ts/pond/compare/v0.57.0...HEAD
12
+ [0.57.0]: https://github.com/pond-ts/pond/compare/v0.56.2...v0.57.0
13
+ [0.56.2]: https://github.com/pond-ts/pond/compare/v0.56.1...v0.56.2
14
+ [0.56.1]: https://github.com/pond-ts/pond/compare/v0.56.0...v0.56.1
15
+ [0.56.0]: https://github.com/pond-ts/pond/compare/v0.55.0...v0.56.0
12
16
  [0.55.0]: https://github.com/pond-ts/pond/compare/v0.54.0...v0.55.0
13
17
  [0.54.0]: https://github.com/pond-ts/pond/compare/v0.53.1...v0.54.0
14
18
  [0.53.1]: https://github.com/pond-ts/pond/compare/v0.53.0...v0.53.1
@@ -56,6 +60,289 @@ include new features and type-level changes; patch bumps are strictly additive.
56
60
 
57
61
  ## [Unreleased]
58
62
 
63
+ ## [0.57.0] — 2026-08-07
64
+
65
+ ### Added
66
+
67
+ - **charts: `<Zone>` — a shaded y-span annotation.** The fourth annotation
68
+ mark, and the value-axis counterpart of `<Region>`: a band between two y
69
+ values, spanning the full plot width. The mark for a **classification of the
70
+ value axis** — US EPA AQI categories, heart-rate / power zones, SLO bands,
71
+ control-chart spec limits — where a reading only means something read against
72
+ a scale.
73
+
74
+ ```tsx
75
+ {
76
+ AQI_CATEGORIES.map((c) => (
77
+ <Zone key={c.role} from={c.from} to={c.to} axis="aqi" role={c.role} />
78
+ ));
79
+ }
80
+ ```
81
+
82
+ Colour comes from `theme.annotation.roles[role]`, so a zone _set_ styles as
83
+ one palette in the theme rather than N colours at the call site. Bounds are
84
+ order-free, clamped to the plot (a band past the axis domain is cut, one
85
+ fully outside culls), and accept `±Infinity` for genuinely open-ended bands
86
+ (`to={Infinity}` — AQI's "Hazardous", a `ZoneTime.openEnded` zone), so a
87
+ whole category table can be rendered and the axis decides what shows.
88
+
89
+ Three defaults deliberately **invert** the rest of the annotation family,
90
+ because a zone spans the full width and a set tiles the row:
91
+ `selectable={false}` (else every mousemove lights a band and its hit area
92
+ eats the plot's clicks), `edges={false}` (contiguous sets share every
93
+ interior boundary — edges-on draws each twice), and no auto-label (the bounds
94
+ are already legible on the y axis; the useful label is a name). `<Zone>` has
95
+ no `onChange` — drag-to-edit zones await a consumer.
96
+
97
+ Guide: [From a CSV to a banded chart](https://pond-ts.org/docs/how-to-guides/air-quality-bands).
98
+
99
+ - **charts: `annotation.dash` — an optional dash pattern for the annotation
100
+ register**, per-register or per-role (`{ color, fillOpacity?, dash? }`), same
101
+ shape as `LineStyle.dash`. Applies to marker / baseline lines and region /
102
+ zone boundaries; fills are never dashed. A dashed reference line reads as
103
+ _placed_ rather than _measured_ — the job the annotation register exists to
104
+ do, and one colour alone can't always carry.
105
+
106
+ - **charts: threshold-banded bars** — `<BarChart thresholds={[1, 2]}>` colours
107
+ one bar **along its length** against a ladder (neutral → warning → alarm), so
108
+ a long bar shows how far through the ladder it travelled rather than only
109
+ which band it ended in. Band fills come from the new
110
+ **`BarStyle.bands`** on the resolved theme role, overridable per chart with
111
+ **`<BarChart bandColors>`** — breakpoints are data, colour stays in the theme,
112
+ the same split `colors` already applies to a stack's group fills.
113
+
114
+ The shape was previously expressible as N `<BarChart>` layers drawn
115
+ outermost-first, each clipped to a band, compositing the gradient by
116
+ overpainting. That produces the same pixels and loses what matters: N layers
117
+ means N hit targets, N `SelectInfo.mark` identities and N legend rows for
118
+ something the reader sees as one bar. Banding is **draw-only** — the hit rect
119
+ is untouched, so a banded bar stays one bar. It also measures **27–49%
120
+ cheaper** than the layered workaround at 8–400 categories
121
+ (`scripts/perf-bandbar.mjs`, which interleaves the arms and prints the
122
+ ratios), and costs nothing when unused.
123
+
124
+ Applies to any single-value bar — `series`, `bins`, `categories`, both
125
+ orientations. Negatives band symmetrically on the magnitude, so a ± diverging
126
+ scale needs no negative breakpoints. Ignored (with a dev warning) on a
127
+ multi-group stack, and yields to `binColors` when both are set. Suppresses
128
+ envelope decimation for the same reason `binColors` does.
129
+
130
+ - **charts: `<YAxis hide>`** — keep the scale, draw no gutter, reserve no
131
+ width. A `<YAxis>` does two jobs — it _holds the scale_ (`min`/`max`/`scale`/
132
+ `pad`) and it _renders a gutter_ — and there was no way to ask for the first
133
+ without the second. A caller could express "auto domain, no gutter" (omit the
134
+ axis) or "explicit domain, with a gutter", but not the pairing a fixed-domain
135
+ chart needs. Omitting the axis is not equivalent: the row supplies an implicit
136
+ auto-domain axis, which is exactly what must not be given up. `width={0}`
137
+ isn't either — the labels still draw, over the plot.
138
+
139
+ Gridlines are unaffected: they belong to the plot, not the gutter, and
140
+ `<ChartContainer grid>` already governs them.
141
+
142
+ - **charts: `<ChartContainer maxBandWidth>` + `bandAlign`** — cap the **slot
143
+ pitch** on a category x axis and place the resulting block
144
+ (`'start'` (default) / `'center'` / `'end'`). A band scale otherwise spreads
145
+ its categories across the full plot width, so three categories in a 900px
146
+ panel become three 300px bars and thirty become thirty 30px ones — the same
147
+ chart in the same panel reading as two different charts depending on how many
148
+ categories the data returned. Fine for a fixed domain; wrong for a **live**
149
+ one, where bar width becomes a variable that moves on its own and a reader
150
+ can't compare the chart to itself a minute ago.
151
+
152
+ `maxBandWidth` caps the **slot**; `<BarChart gap>` still insets the bar
153
+ within it — one knob for pitch, one for ink. Omitting `maxBandWidth` is the
154
+ previous fill behaviour exactly, and a cap too loose to bind degrades back to
155
+ it rather than clipping. There is deliberately no `bandAlign: 'fill'`: "fill"
156
+ is what omitting the cap means, and a `fill` alongside a pitch cap would be a
157
+ contradiction rather than a choice.
158
+
159
+ **Vertical / x-axis categories only** — a `orientation="horizontal"`
160
+ categorical chart puts its categories on the y axis as unit slots, a
161
+ different mechanism this does not cap.
162
+
163
+ - **charts: themed emphasis on the category path** — `BarStyle.hover` /
164
+ `.highlight` now apply to `categories` and horizontal bars, which routed
165
+ through the transposed stacked draw path and read neither. New
166
+ `BarStyle.selectedOutline` (the selected bar's stroke, where the default is
167
+ its own fill) and `BarStyle.emphasisOpacity` (the alpha a live bar pops to,
168
+ previously hard-coded `1`) make the emphasis tunable rather than fixed.
169
+
170
+ The _behaviour_ was defensible; the problem was that the theme accepted
171
+ values it would not use. `bar.hover` / `.highlight` were typed, settable and
172
+ documented as the emphasis channel, and silently did nothing on the most
173
+ common categorical chart, so a theme author set them, saw no change, and
174
+ could not tell whether they were wrong about the colour or the mechanism.
175
+ The one genuine exclusion stays and is now the only one: a `binColors` bar
176
+ keeps its own colour under hover/selection, because swapping a
177
+ zone-coloured or direction-coloured bar to a single highlight hue erases
178
+ what the colour encodes.
179
+
180
+ ### Fixed
181
+
182
+ - **charts: a negative segment in a multi-group stack is no longer silently
183
+ dropped** ([PND-SIGNSTACK]). Positives stack **up** from the baseline and
184
+ negatives stack **down** from it — two running totals per bin — so the
185
+ **signed stacked histogram** (net flow by category, inflow/outflow, buy/sell
186
+ pressure by venue) renders correctly. `stackValueExtent` grew the matching
187
+ negative half; both had to move together, since an extent stopping at `0`
188
+ would clip the segments the draw path now emits.
189
+
190
+ **This is a visible behaviour change** for any existing chart feeding
191
+ negative values into a multi-group stack — but such a chart was previously
192
+ rendering _wrongly_: the dropped segments did not clamp, warn or throw, every
193
+ remaining segment stacked up as though they had never been in the data, and a
194
+ mixed-sign series came out as a confident, wrong, all-positive chart. An
195
+ all-positive stack is bit-identical to before. Splitting into two layers was
196
+ not a workaround either — the negative layer was still `G > 1`, so it was
197
+ dropped too.
198
+
199
+ ### Changed
200
+
201
+ - **charts (docs): `<BarChart bins>` now states that it selects a _value_
202
+ axis** ([PND-TICKUNIT]), so a time-bucketed histogram fed through `bins` gets
203
+ the decimal 1-2-5 tick ladder rather than the duration ladder a clock
204
+ subdivides by — labelling e.g. 11:40 and 13:20 at a ~100-minute step. The
205
+ natural reading ("I have pre-binned buckets, so I'll pass `bins`") is exactly
206
+ what forecloses the time axis, and nothing at the call site said so. The prop
207
+ docs now point a time-keyed caller at `<BarChart series columns>`, where the
208
+ clock ticks are native. No behaviour change.
209
+
210
+ ## [0.56.2] — 2026-08-05
211
+
212
+ ### Fixed
213
+
214
+ - **charts (tests only, no shipped change):** the log-axis rendered-label test
215
+ is no longer an exact-set assertion over every digit-bearing node in the
216
+ render tree. It passed on Node 22 and failed on CI's Node 18 with one extra
217
+ element, blocking the publish twice. The discrepancy is **unreproduced and
218
+ still open** — recorded as `[PND-LOGTICK-N18]` in `PND_CHARTS_PLAN.md` with
219
+ everything measured about it. The assertion now checks that every chosen tick
220
+ renders in order, which is the wiring this test exists to cover; the tick
221
+ _selection_ it was really about is pinned deterministically by the
222
+ `yTickValues` unit tests.
223
+
224
+ ## [0.56.1] — 2026-08-05
225
+
226
+ ### Fixed
227
+
228
+ - **charts (tests only, no shipped change):** a log-axis test asserted on
229
+ rendered label _text_, parsing numbers back out of the DOM to infer scale
230
+ behaviour. It passed locally and failed in CI on a value it could not have
231
+ produced there, which blocked the v0.56.0 publish. The root cause was never
232
+ reproduced; rather than guess at it, the assertion now compares the rendered
233
+ labels against the ticks the axis is specified to draw, formatted through the
234
+ same formatter — deterministic regardless of locale, formatting or DOM
235
+ differences, and the numeric guarantee itself was already pinned directly by
236
+ the `yTickValues` unit tests. The published artifact is identical to what
237
+ v0.56.0 would have been.
238
+
239
+ ## [0.56.0] — 2026-08-05
240
+
241
+ ### Added
242
+
243
+ - **charts:** **`<YAxis scale="log">` — a base-10 logarithmic y axis.** Every y
244
+ scale was `scaleLinear`, so data spanning orders of magnitude was
245
+ undrawable: on a linear axis everything below the top decade collapses onto
246
+ the baseline. Set `scale="log"` and the axis maps by ratio, ticking the
247
+ decades. `format` still formats the **value**, so a readout says `1.2 PB`
248
+ rather than its logarithm — the transform is in the scale, not in the data,
249
+ which is what keeps it transparent to every draw layer, annotation and
250
+ cursor readout.
251
+
252
+ A log domain cannot contain zero, and d3 maps a non-positive value to
253
+ **`NaN`** — a coordinate the canvas silently _drops_, which is why every
254
+ consequence below is about something failing invisibly rather than throwing.
255
+ So the axis is deliberate about it:
256
+ - **Domain policy matches the linear axis exactly.** Auto-fit takes the
257
+ smallest **positive** extent (one zero sample can't collapse the axis, and
258
+ a `BarChart` — whose extent always widens to include zero — can still share
259
+ it); a positive explicit `min`/`max` is honoured verbatim and never
260
+ discarded, with the _auto-fit_ side moving if the domain would otherwise
261
+ invert; a fully auto-fit domain is `.nice()`d out to whole powers of ten, so
262
+ the extremes get headroom instead of sitting clipped on the plot edge. A
263
+ non-positive bound has no position and is refused in favour of the data.
264
+ `pad` is applied multiplicatively, adding the same fraction of a decade at
265
+ both ends.
266
+ - **A value with no position on the axis renders as a gap.** Previously the
267
+ gap test was `Number.isFinite(value)`, and `0` is finite — so the coordinate
268
+ became `NaN`, the canvas dropped the path op without breaking the path, and
269
+ the two neighbours were joined by a straight line _over_ the missing data.
270
+ Lines, area fills and outlines, and band envelopes now all break there.
271
+ - **Layers that reach for a baseline rest on the axis floor.** `AreaChart`
272
+ resolves an out-of-domain `baseline` there (writing `baseline={0}` is
273
+ natural and correct on a linear axis), and a **stacked** bar layer starts
274
+ its first segment there — starting at zero made the bottom segment of every
275
+ stack both invisible and unhittable. Unchanged on a linear axis, where zero
276
+ clamped into the domain _is_ zero.
277
+ - **The dev-mode warning names only unambiguous mistakes**: a refused
278
+ `min`/`max`, negative data, or an axis with no positive data at all. It
279
+ deliberately says nothing about an extent of exactly `[0, hi]`, which a
280
+ line touching zero and a bar layer on strictly positive data both report
281
+ identically — warning there fired on _every_ bar chart on a log axis. It
282
+ warns once per distinct complaint rather than on every repaint.
283
+
284
+ - **charts:** **pan and zoom now yield whole-millisecond view ranges.** A
285
+ wheel-zoom derives its range from pixel positions through `xScale.invert()`,
286
+ so the result was fractional by construction — an ordinary scroll produced
287
+ `1.7e12 + 0.37`. The epoch millisecond is this model's atomic unit and
288
+ consumers are entitled to assume it; one did, and a calendar `cursorSequence`
289
+ threw on a plain scroll. `zoomRange` / `panRange` round both ends, and never
290
+ collapse a positive span to zero width in doing so. (Core's fractional-instant
291
+ fix covers the same crash from the other side; this closes the class.)
292
+
293
+ - **charts:** **`AreaStyle.flatFill` — stacked areas that read as slabs.** An
294
+ area's fill has always graded to transparent at the baseline, which is right
295
+ for the elevation form and wrong for a stack: every band showed the one
296
+ beneath it through the fade, so a stacked area was not really drawable. Set
297
+ `flatFill` and the fill is flat; omitted, the gradient is unchanged, so no
298
+ existing theme shifts. The docs theme's `seq1…seq8` area roles set it, since
299
+ stacking is what they exist for.
300
+
301
+ - **docs theme:** **a sequential ramp — `seq1…seq8` — for charts with more
302
+ series than the categorical set has hues.** `--pond-viz-1…5` were, and
303
+ remain, the categorical set; a chart needing more slots (an eight-source
304
+ stack, a wall of climate stripes) now steps **tonally** through the brand
305
+ teal instead of introducing competing hues. Eight steps, evenly spaced
306
+ (~ΔL\* 9 in CIELAB), defined for light and dark, each mode's ramp containing
307
+ that mode's `--pond-viz-1` exactly. Exposed as `line` / `area` / `bar` theme
308
+ roles on `docsTheme` (Storybook) and the docs site's `useSiteChartTheme`,
309
+ and as an array from the site's `useSequentialRamp()`. Dev-only: the ramp
310
+ lives in the `docs-theme.fixture.ts` Storybook fixture and the website's
311
+ CSS, both excluded from the published `@pond-ts/charts` build — the library
312
+ still ships no palette.
313
+
314
+ ### Fixed
315
+
316
+ - **core:** **a fractional epoch millisecond no longer crashes calendar
317
+ math.** `Temporal.Instant` refuses a non-integer epoch ms outright
318
+ (`epoch milliseconds must be an integer`), and `toPlainDateStart` passed
319
+ whatever it was given straight through — so realizing a `Sequence.calendar`
320
+ over a fractional range threw, and in a React app the exception unmounted the
321
+ page. A fraction is not a caller error: a chart's wheel-zoom derives its view
322
+ range from pixel positions via `xScale.invert()`, so an ordinary scroll
323
+ produces `1.7e12 + 0.37`. The instant is now floored to the millisecond
324
+ containing it — the epoch millisecond is this model's atomic unit and
325
+ calendar boundaries are themselves whole milliseconds, so the bucket
326
+ containing `t` and the one containing `t + 0.37` are necessarily the same,
327
+ and integer inputs are untouched. (`Math.floor`, not `Math.trunc`: pre-1970
328
+ they disagree, and `-5.5` lies inside the millisecond spanning `[-6, -5)`.)
329
+
330
+ - **charts:** toggling **`<ChartContainer grid>`** now repaints immediately.
331
+ `Layers`' draw callback read `container.grid` but didn't depend on it, so
332
+ switching gridlines off changed nothing until an unrelated dependency moved —
333
+ in practice you had to pan or zoom a little to force the update. The same
334
+ omission covered `sessionDividers` and `xKind`.
335
+
336
+ - **charts:** the log axis's dev-mode warning no longer requires **node's
337
+ ambient types**. It was guarded by a bare `process.env.NODE_ENV`, which
338
+ typechecks only when a tool happens to resolve `@types/node` from a parent
339
+ `node_modules` — so `tsc` inside the package passed while running the _same_
340
+ tsconfig from a consumer's directory failed with `TS2591: Cannot find name
341
+ 'process'`. That took out the docs site's TypeDoc step, and would equally hit
342
+ any consumer typechecking the package's sources. The guard now lives in
343
+ `src/dev.ts` behind a local declaration and a `typeof` check, so a browser
344
+ bundle with no `process` global doesn't throw at import either.
345
+
59
346
  ## [0.55.0] — 2026-08-04
60
347
 
61
348
  ### Added
@@ -102,6 +102,24 @@ type AreaChartSource<S extends SeriesSchema = SeriesSchema, VS extends ValueSeri
102
102
  };
103
103
  /** `<AreaChart>`'s props: the shared knobs plus one series-kind source shape. */
104
104
  export type AreaChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> = AreaChartCommon<S, VS> & AreaChartSource<S, VS>;
105
+ /** Read a d3 linear scale's domain lower bound (the axis floor) from the plain
106
+ * `(value) => pixel` function the row hands to `draw`. The runtime object is a
107
+ * d3 `ScaleLinear` (it carries `.domain()`); the {@link RowLayer} type narrows
108
+ * it to the call signature, so this reads the bound through a localized,
109
+ * documented shape rather than widening `drawArea`'s contract to d3-scale. */
110
+ /**
111
+ * The area's baseline in **data** units: the caller's `baseline` when it has a
112
+ * finite position on this axis, else the axis floor.
113
+ *
114
+ * The fallback is not defensive padding — it's the log case. `baseline={0}` is
115
+ * the natural thing to write and is correct on a linear axis; on a log axis
116
+ * zero has no position at all — `scaleLog()(0)` is **`NaN`** (the `-Infinity`
117
+ * the log *transform* produces is then interpolated into the range, and
118
+ * `∞ − ∞` is what comes out) — and a single non-finite coordinate turns the
119
+ * whole filled path into nothing drawn at all. Resolving to the floor keeps the
120
+ * layer's meaning ("fill from the bottom") on both scale kinds.
121
+ */
122
+ export declare function resolveAreaBaseline(baseline: number | undefined, yScale: (value: number) => number): number;
105
123
  /**
106
124
  * An area draw layer: fills between a value `column` and a `baseline`, with a
107
125
  * graded (gradient) shade — opaque at the line, transparent at the baseline —
package/dist/AreaChart.js CHANGED
@@ -12,6 +12,24 @@ import { useSlotKey } from './use-slot-key.js';
12
12
  * d3 `ScaleLinear` (it carries `.domain()`); the {@link RowLayer} type narrows
13
13
  * it to the call signature, so this reads the bound through a localized,
14
14
  * documented shape rather than widening `drawArea`'s contract to d3-scale. */
15
+ /**
16
+ * The area's baseline in **data** units: the caller's `baseline` when it has a
17
+ * finite position on this axis, else the axis floor.
18
+ *
19
+ * The fallback is not defensive padding — it's the log case. `baseline={0}` is
20
+ * the natural thing to write and is correct on a linear axis; on a log axis
21
+ * zero has no position at all — `scaleLog()(0)` is **`NaN`** (the `-Infinity`
22
+ * the log *transform* produces is then interpolated into the range, and
23
+ * `∞ − ∞` is what comes out) — and a single non-finite coordinate turns the
24
+ * whole filled path into nothing drawn at all. Resolving to the floor keeps the
25
+ * layer's meaning ("fill from the bottom") on both scale kinds.
26
+ */
27
+ export function resolveAreaBaseline(baseline, yScale) {
28
+ const floor = domainFloor(yScale);
29
+ if (baseline === undefined)
30
+ return floor;
31
+ return Number.isFinite(yScale(baseline)) ? baseline : floor;
32
+ }
15
33
  function domainFloor(yScale) {
16
34
  const d = yScale.domain?.();
17
35
  return d && d.length > 0 ? d[0] : 0;
@@ -137,7 +155,12 @@ export function AreaChart({ series, column, readout, as: semantic, axis, baselin
137
155
  // Omitted baseline rests on the axis floor (resolved late from the
138
156
  // scale, so it tracks the auto-fit domain); a fixed baseline is used
139
157
  // verbatim.
140
- baseline ?? domainFloor(yScale), curveFactory, gaps, gapConnectorOpacity, decimate),
158
+ // A log axis has no position for zero — or anything at or below
159
+ // it — so an explicit out-of-domain `baseline` would scale to
160
+ // `NaN` and poison every coordinate in the fill path. Fall back
161
+ // to the axis floor, which is exactly what an omitted baseline
162
+ // already resolves to.
163
+ resolveAreaBaseline(baseline, yScale), curveFactory, gaps, gapConnectorOpacity, decimate),
141
164
  },
142
165
  axisId: axis,
143
166
  index,
@@ -26,6 +26,22 @@ import type { DecimateOption } from './decimate.js';
26
26
  * (`Array<{ start, end, …aggregates }>`); the names are **aggregate
27
27
  * fields** of the record, not schema columns, so they stay `string`. Pair
28
28
  * with `ordinal` for a band axis.
29
+ *
30
+ * **`bins` selects a *value* axis, so a time-bucketed histogram fed this way
31
+ * gets decimal ticks** ([PND-TICKUNIT]). A `TimeSeries`/`Map` bins on time; a
32
+ * `ValueSeries`/`bins`-array bins on a value axis — which means the tick
33
+ * ladder is the plain 1-2-5 walk, not the **duration** ladder a clock
34
+ * subdivides by (15s and 30s are round durations where 20s and 50s are not).
35
+ * A minute-of-day histogram passed as `bins` therefore labels something like
36
+ * 11:40 and 13:20 — real times at a ~100-minute step — and nothing at the
37
+ * call site says why.
38
+ *
39
+ * The natural reading ("I have pre-binned buckets, so I'll pass `bins`") is
40
+ * exactly what forecloses the time axis. For a **time-keyed** histogram use
41
+ * the series door instead — `<BarChart series columns>` on a time-keyed wide
42
+ * series — and the clock ticks are native with no workaround.
43
+ * `<ChartContainer origin>` does not rescue it: it relabels a value axis but
44
+ * does not re-ladder it.
29
45
  * - **`categories`** — an ordered `{ label, value }[]`, one bar per category.
30
46
  * Takes **no** `column`/`columns` (each datum carries its own value).
31
47
  * Vertical puts the categories on the ordinal **x** axis (the container's
@@ -116,6 +132,58 @@ export interface BarChartCommon<S extends SeriesSchema = SeriesSchema, VS extend
116
132
  * carry many bars' colours, so every visible bar draws.
117
133
  */
118
134
  binColors?: readonly (string | undefined)[];
135
+ /**
136
+ * **Threshold breakpoints** — colour each bar *along its length* against a
137
+ * ladder, so a long bar shows how far through the ladder it travelled rather
138
+ * than only which band it ended in. Breakpoints are **absolute data values**
139
+ * in the axis's own units — `[1, 2]` means "warning above 1, alarm above 2",
140
+ * not offsets from wherever the bar happens to rest — and `n` of them make
141
+ * `n + 1` bands. Each must be finite and greater than zero; anything else is
142
+ * dropped with a dev warning (the ladder is walked on the magnitude, so a
143
+ * negative breakpoint is not expressible).
144
+ *
145
+ * ```tsx
146
+ * // neutral to 1, warning 1–2, alarm above 2
147
+ * <BarChart categories={cats} thresholds={[1, 2]} />
148
+ * ```
149
+ *
150
+ * Band fills come from {@link BarStyle.bands} on the resolved role
151
+ * (`theme.bar[as] ?? theme.bar.default`), overridden by {@link bandColors}.
152
+ * Breakpoints are data and live here; colour stays in the theme — the same
153
+ * split as `colors` over the stack's group fills.
154
+ *
155
+ * **A banded bar is still one bar.** It keeps one hit region, one stable
156
+ * `SelectInfo.mark` and one legend row — which is the whole difference from
157
+ * the N-overlaid-layers recipe this replaces, where each band was separately
158
+ * hittable and separately listed. Selection and hover pop the opacity and
159
+ * keep the band colours (as `binColors` does), outlining in the colour of the
160
+ * band the value actually reached.
161
+ *
162
+ * **Negatives band symmetrically**: the ladder is walked on the magnitude and
163
+ * re-signed, so a bar hanging below the baseline reads the same ±ladder
164
+ * without negative breakpoints. Out-of-order entries are sorted (the bands
165
+ * are defined by their boundaries, so there is no second reading) and
166
+ * non-finite ones dropped.
167
+ *
168
+ * Applies to any **single-value** bar — a `series`/`bins` chart, `categories`,
169
+ * and both orientations. On a genuine **multi-group stack** it is ignored
170
+ * with a dev warning: a segment that is already one slice of a total has no
171
+ * defined banding. Set with `binColors` it also yields (per-bar colour is the
172
+ * more specific answer) and warns. **Disables envelope decimation** for the
173
+ * same reason `binColors` does.
174
+ */
175
+ thresholds?: readonly number[];
176
+ /**
177
+ * Call-site override for the {@link thresholds} band fills — `bandColors[k]`
178
+ * paints the band above `thresholds[k - 1]`, so a ladder of `n` thresholds
179
+ * reads `n + 1` entries. Omitted ⇒ {@link BarStyle.bands} from the theme.
180
+ *
181
+ * Prefer the theme for anything a design system owns; this is the escape
182
+ * hatch for a one-off ladder that shouldn't mint a theme role. If neither
183
+ * source supplies enough entries, the shortfall falls back to the flat fill
184
+ * and dev-warns rather than silently drawing an unbanded bar.
185
+ */
186
+ bandColors?: readonly string[];
119
187
  /**
120
188
  * Bar growth direction (the histogram orientation). **Default `'vertical'`.**
121
189
  *
@@ -220,14 +288,21 @@ export type BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends Valu
220
288
  * domain spans zero, or on the axis floor when an explicit `<YAxis min>` sits
221
289
  * above zero (see {@link resolveBarBaseline}).
222
290
  *
223
- * **Baseline (stacked).** A stack is **cumulative from value 0** — the segments
224
- * sum upward from the zero line, so its value axis **must include 0**. The
225
- * auto-fit guarantees this: {@link stackValueExtent} always returns `[0, maxTotal]`.
226
- * An explicit `<YAxis min>` **above** 0 is therefore unsupported for a stack — it
227
- * would hide the bottom of the cumulative column; only the portion above the floor
228
- * draws (clipped cleanly at the plot floor, as any bar below an explicit floor is).
229
- * Segment values are assumed **non-negative** (a negative or zero segment is
230
- * skipped — diverging stacks are out of scope).
291
+ * **Baseline (stacked).** A stack is **cumulative from value 0** — so its value
292
+ * axis **must include 0**. The auto-fit guarantees this:
293
+ * {@link stackValueExtent} returns `[minNegativeTotal, maxPositiveTotal]`, both
294
+ * seeded at `0`. An explicit `<YAxis min>` **above** 0 is therefore unsupported
295
+ * for a stack — it would hide the bottom of the cumulative column; only the
296
+ * portion above the floor draws (clipped cleanly at the plot floor, as any bar
297
+ * below an explicit floor is).
298
+ *
299
+ * **Signed stacks are supported** ([PND-SIGNSTACK]): each bin keeps two running
300
+ * totals, so positive segments stack **up** from the zero line and negative
301
+ * ones stack **down** from it — the signed histogram (net flow by category,
302
+ * inflow/outflow, buy/sell pressure by venue). A **zero** segment is still
303
+ * skipped, having no extent to draw or hit-test. This changed in the
304
+ * threshold-banding wave: negative segments were previously dropped outright
305
+ * and silently, so a mixed-sign series rendered as an all-positive chart.
231
306
  *
232
307
  * **Interaction (opt-in via `id`).** Hover lights the bar / segment under the
233
308
  * cursor (hit-tested by pixel rect, so it works in both orientations); click
@@ -244,6 +319,6 @@ export type BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends Valu
244
319
  * </Layers>
245
320
  * ```
246
321
  */
247
- export declare function BarChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, bins, categories, column, columns, as: semantic, colors, binColors, orientation, ordinal, id, axis, gap, decimate, legend, index, }: BarChartProps<S, VS>): null;
322
+ export declare function BarChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, bins, categories, column, columns, as: semantic, colors, binColors, thresholds, bandColors, orientation, ordinal, id, axis, gap, decimate, legend, index, }: BarChartProps<S, VS>): null;
248
323
  export {};
249
324
  //# sourceMappingURL=BarChart.d.ts.map