@pond-ts/charts 0.58.0 → 0.60.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/API.md +580 -0
- package/CHANGELOG.md +339 -1
- package/dist/AreaChart.d.ts +53 -1
- package/dist/AreaChart.js +16 -3
- package/dist/BarChart.d.ts +56 -7
- package/dist/BarChart.js +88 -73
- package/dist/BarList.d.ts +22 -0
- package/dist/BarList.js +42 -9
- package/dist/ChartContainer.d.ts +175 -3
- package/dist/ChartContainer.js +190 -11
- package/dist/ChartRow.js +92 -2
- package/dist/Layers.js +14 -4
- package/dist/XAxis.js +19 -14
- package/dist/YAxis.d.ts +58 -2
- package/dist/YAxis.js +5 -3
- package/dist/area.d.ts +43 -1
- package/dist/area.js +122 -5
- package/dist/bars.d.ts +10 -3
- package/dist/bars.js +13 -9
- package/dist/context.d.ts +46 -8
- package/dist/data.d.ts +38 -0
- package/dist/data.js +43 -0
- package/dist/format.d.ts +16 -1
- package/dist/format.js +17 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +11 -0
- package/dist/range.d.ts +14 -1
- package/dist/range.js +24 -3
- package/dist/theme.d.ts +80 -4
- package/dist/theme.js +3 -0
- package/dist/use-band-ladder.d.ts +30 -0
- package/dist/use-band-ladder.js +81 -0
- package/dist/useChartFrame.d.ts +122 -0
- package/dist/useChartFrame.js +155 -0
- package/dist/useChartLegend.d.ts +8 -0
- package/dist/viewport.d.ts +35 -2
- package/dist/viewport.js +53 -6
- package/dist/yticks.d.ts +8 -1
- package/dist/yticks.js +109 -1
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -8,7 +8,9 @@ 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.
|
|
11
|
+
[Unreleased]: https://github.com/pond-ts/pond/compare/v0.60.0...HEAD
|
|
12
|
+
[0.60.0]: https://github.com/pond-ts/pond/compare/v0.59.0...v0.60.0
|
|
13
|
+
[0.59.0]: https://github.com/pond-ts/pond/compare/v0.58.0...v0.59.0
|
|
12
14
|
[0.58.0]: https://github.com/pond-ts/pond/compare/v0.57.0...v0.58.0
|
|
13
15
|
[0.57.0]: https://github.com/pond-ts/pond/compare/v0.56.2...v0.57.0
|
|
14
16
|
[0.56.2]: https://github.com/pond-ts/pond/compare/v0.56.1...v0.56.2
|
|
@@ -61,6 +63,342 @@ include new features and type-level changes; patch bumps are strictly additive.
|
|
|
61
63
|
|
|
62
64
|
## [Unreleased]
|
|
63
65
|
|
|
66
|
+
## [0.60.0] — 2026-08-13
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
|
|
70
|
+
- `@pond-ts/charts`: `<AreaChart thresholds>` + `bandColors` — threshold
|
|
71
|
+
banding along the area's height ([PND-BANDAREA]), the area counterpart of
|
|
72
|
+
`<BarChart thresholds>`. `n` breakpoints (absolute data values, magnitude-
|
|
73
|
+
mirrored below zero) make `n + 1` bands; fills resolve `bandColors` → the
|
|
74
|
+
new `AreaStyle.bands` theme token (the default theme ships the same
|
|
75
|
+
teal/amber/red ladder as the bar role). One hard-stop pixel-space gradient
|
|
76
|
+
carries the ladder for the fill **and** the outline, so the value line
|
|
77
|
+
switches hue exactly at each crossing and the banded area keeps one hit
|
|
78
|
+
region, one legend row and one readout identity. Composes with `curve`,
|
|
79
|
+
`gaps` and M4 decimation unchanged; a swept window keeps the band colours
|
|
80
|
+
(no `spanColor` swap). Ladder resolution + dev warnings shared with
|
|
81
|
+
`<BarChart>` via one internal hook.
|
|
82
|
+
- **charts: `<ChartContainer xScale="log" | "symlog">` — a logarithmic value x
|
|
83
|
+
axis** ([#649]). The x counterpart of `<YAxis scale>`, for a quantity spanning
|
|
84
|
+
orders of magnitude — a power–duration curve is watts against 1s · 5s · 1m ·
|
|
85
|
+
20m · 3h, which is unreadable on a linear x.
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
<ChartContainer range={[1, 10800]} xScale="log">
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Why the container and not `<XAxis scale>`**, where the `<YAxis>` mirror would
|
|
92
|
+
put it: there is one x scale shared by every row and the container builds it,
|
|
93
|
+
while every `<XAxis>` prop is presentational (`format`, `label`, `side`,
|
|
94
|
+
`ticks`, `align`, …). `<YAxis>` is the opposite — one scale per axis per row,
|
|
95
|
+
declared by the axis, which is why `min`/`max`/`pad`/`scale` live there. It
|
|
96
|
+
sits beside `origin`, `spacing` and `calendar`, which shape the same scale.
|
|
97
|
+
|
|
98
|
+
Ignored on a time or category axis. A `'log'` domain reaching zero **falls back
|
|
99
|
+
to linear and warns** rather than silently clamping — `log(0)` is undefined;
|
|
100
|
+
use `'symlog'` for data that crosses zero.
|
|
101
|
+
|
|
102
|
+
Nothing downstream branches: d3's log scales share the continuous-scale
|
|
103
|
+
surface, so draw layers are untouched. The tick ladder is the one the y axis
|
|
104
|
+
already built (`tickValues`, renamed from `yTickValues` now that both axes use
|
|
105
|
+
it) — d3's raw `scaleLog.ticks()` is nearly a step function.
|
|
106
|
+
|
|
107
|
+
[#649]: https://github.com/pond-ts/pond/issues/649
|
|
108
|
+
|
|
109
|
+
- **charts: `<BarList barColors>` — per-row bar colour** ([#650]). The list's
|
|
110
|
+
counterpart to `<BarChart binColors>`: `barColors[i]` aligned to the rows you
|
|
111
|
+
passed, an `undefined` or short entry falling back to the column's `as` /
|
|
112
|
+
theme fill.
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
<BarList rows={zones} columns={[{ column: 'frac' }]} barColors={ZONE_RAMP} />
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
For the case `binColors` was built for and a list could not do — a zone table
|
|
119
|
+
where each row's bar carries its own step of a ramp. A `BarListColumn`'s
|
|
120
|
+
single `as` paints every row the same colour, so the ramp had to move onto the
|
|
121
|
+
row label, putting it on the wrong element: the bar is the natural carrier of
|
|
122
|
+
a magnitude.
|
|
123
|
+
|
|
124
|
+
**Colours key on the row, not its render position**, so they follow the data
|
|
125
|
+
through a `sortBy` rather than repainting the ramp onto whichever rows now sit
|
|
126
|
+
in those slots.
|
|
127
|
+
|
|
128
|
+
**A per-row colour makes the fill load-bearing, so its state treatment stands
|
|
129
|
+
down** — a coloured bar keeps its own colour while selected, and shows the
|
|
130
|
+
state in opacity instead. That is the rule a multi-metric row already
|
|
131
|
+
followed, and the one `binColors` follows on the canvas: recolouring a bar
|
|
132
|
+
that means something trades a distinction the reader needs for one the band
|
|
133
|
+
and rail already give them.
|
|
134
|
+
|
|
135
|
+
[#650]: https://github.com/pond-ts/pond/issues/650
|
|
136
|
+
|
|
137
|
+
- **charts: `<ChartContainer categories>` — the ordinal axis as a
|
|
138
|
+
container-level choice** ([PND-IGNITECAT]). Declare the slot names on the
|
|
139
|
+
container and **any value-keyed layer can live on them** — a target line, a
|
|
140
|
+
point mark or a filled envelope over categorical bars, which was previously
|
|
141
|
+
not expressible at all.
|
|
142
|
+
|
|
143
|
+
The band scale used to be reachable only _through a layer_: `<BarChart
|
|
144
|
+
categories>` and a **horizontal** heat map reported `xKind: 'category'`, every other layer
|
|
145
|
+
reported `'time'` or `'value'`, and a container throws on a mixed kind. The
|
|
146
|
+
workaround — key every layer to a synthetic integer index and hand-supply the
|
|
147
|
+
tick labels — forfeits two features the ordinal axis already implements, and
|
|
148
|
+
both came back as their own friction entries: `<XAxis>` label thinning (gated
|
|
149
|
+
on a category axis with no custom ticks) and the `maxBandWidth` / `bandAlign`
|
|
150
|
+
slot packing. Declaring the categories on the container keeps both.
|
|
151
|
+
|
|
152
|
+
The change is small because the scale was already built for it: `scaleBand`'s
|
|
153
|
+
domain is **numeric** (`[0, n]`, slot `i` at `[i, i+1]`) with a linear pixel
|
|
154
|
+
mapping, so a `ValueSeries` keyed on slot coordinates already lands where the
|
|
155
|
+
bars do. **Slot `i`'s centre is `i + 0.5`** — the same number
|
|
156
|
+
`ScaleBand.ticks()` returns and where `<XAxis>` puts the tick.
|
|
157
|
+
|
|
158
|
+
Two things error, deliberately: a **time-keyed layer** (a timestamp has no
|
|
159
|
+
slot), and a **category layer that disagrees** with the prop in content or
|
|
160
|
+
order — the prop is authoritative, and a silent mismatch would draw bars
|
|
161
|
+
under the wrong labels. The pre-existing mixed-kind error now names the prop
|
|
162
|
+
as the fix.
|
|
163
|
+
|
|
164
|
+
Declaring it has two costs, both already true of an inferred category axis
|
|
165
|
+
and now reachable from a previously-continuous container: **x pan and zoom
|
|
166
|
+
stop** (`panZoom` keeps working on y), and **`range` stops applying to x**
|
|
167
|
+
(the domain is `[0, n]` from the slot count, so an x range is a no-op).
|
|
168
|
+
|
|
169
|
+
One hazard is documented rather than enforced: **a value-keyed layer is taken
|
|
170
|
+
at its word**, so a layer whose x means something other than a slot
|
|
171
|
+
coordinate — a horizontal categorical `<BarChart>`, whose x is bar _length_ —
|
|
172
|
+
will draw in the wrong place. A guard for that case was written and removed
|
|
173
|
+
after review: it tested `binCategories`, which is the generic "my _y_ is
|
|
174
|
+
ordinal" channel that a **vertical heat map** sets for its rows, so it
|
|
175
|
+
rejected a slot-keyed grid with named columns on x — a wanted layout, since
|
|
176
|
+
ordinal rows plus ordinal columns is just a 2-D grid. Nothing distinguishes
|
|
177
|
+
"my x is a coordinate" from "my x is a magnitude", so there is no
|
|
178
|
+
contradiction to detect.
|
|
179
|
+
|
|
180
|
+
`categories={[]}` is an ordinal axis with **no slots yet**, not a fallback to
|
|
181
|
+
time — so the kind doesn't flip and rebuild every scale when data arrives.
|
|
182
|
+
|
|
183
|
+
Omitting `categories` leaves the inferred behaviour exactly as it was.
|
|
184
|
+
|
|
185
|
+
- **charts: `useChartFrame()` — the resolved plot geometry, published**
|
|
186
|
+
([PND-IGNITEFRAME]). A hook returning what the container already worked out:
|
|
187
|
+
the plot rect (`plot.x` / `plot.width`), the reserved axis `gutters`, the
|
|
188
|
+
shared `xScale` and its `xKind`, a row's `yScales` and top inset, and — on a
|
|
189
|
+
category axis — the ordinal slot edges (`bands.at(i)`, `pitch`, `labels`).
|
|
190
|
+
|
|
191
|
+
Consumers aligning DOM chrome to the plot (per-slot header tables, column
|
|
192
|
+
summary strips, cards pinned over a band, a colour ramp keyed to the plot's
|
|
193
|
+
own scale) previously had to re-derive all of it: pin every axis gutter to a
|
|
194
|
+
fixed width so it stops depending on label content, measure the outer box,
|
|
195
|
+
subtract, and re-implement the band packing. **That duplicate is not merely
|
|
196
|
+
verbose — it is wrong over time.** It holds only until the library changes
|
|
197
|
+
how a gutter is sized or how bands are packed, at which point the chrome
|
|
198
|
+
slides out of alignment with the plot it labels, with no type error and no
|
|
199
|
+
failing test.
|
|
200
|
+
|
|
201
|
+
Two shape notes. **The x/y split is the library's own** — the container owns
|
|
202
|
+
one shared x scale, rows own their y scales — so `plot` carries x and `row`
|
|
203
|
+
carries y, and `row` is `null` when the hook is called outside a
|
|
204
|
+
`<ChartRow>`. That `null` is deliberate: the common case (a header strip
|
|
205
|
+
beside the rows) genuinely has no y geometry, and reporting `height: 0`
|
|
206
|
+
instead would be the same silent misalignment the hook exists to remove.
|
|
207
|
+
**Scope follows placement**, exactly as `useChartLegend` already does.
|
|
208
|
+
|
|
209
|
+
`useChartLegend`'s `gutters` is unchanged and still the right call for a
|
|
210
|
+
legend; it now documents `useChartFrame()` as the fuller surface. It was the
|
|
211
|
+
only geometry the library published, for one consumer, on a hook named for
|
|
212
|
+
something else — which is why this exists.
|
|
213
|
+
|
|
214
|
+
- **charts: `<ChartContainer width="auto">` — fill the available width**
|
|
215
|
+
([PND-WIDTH]). `width` now accepts `'auto'`, and an omitted `width` means the
|
|
216
|
+
same; a number still skips the measure pass and paints on the first render.
|
|
217
|
+
The container renders a plain full-width box, measures it with a
|
|
218
|
+
`ResizeObserver`, and mounts the chart at that pixel width — the canvas
|
|
219
|
+
renderer needs real pixels to lay out ticks and slots, so this is measurement
|
|
220
|
+
moved inside the library rather than a percentage handed to a canvas. Nothing
|
|
221
|
+
paints until a real width exists.
|
|
222
|
+
|
|
223
|
+
This is the shipped
|
|
224
|
+
[responsive-width recipe](https://pond-ts.github.io/pond/docs/recipes/responsive-width)
|
|
225
|
+
become the implementation, and it closes that recipe's sharpest edge by
|
|
226
|
+
construction: the measured box is one the library owns, so it can never be
|
|
227
|
+
the caller's padded or bordered box (whose border-box width overflowed the
|
|
228
|
+
chart by exactly the padding, silently clipped when the box also hid
|
|
229
|
+
overflow). Style your own wrapper freely.
|
|
230
|
+
|
|
231
|
+
Two behaviours worth knowing. A container **hidden** by an ancestor's
|
|
232
|
+
`display: none` keeps the last width it measured and stays mounted, so a tab
|
|
233
|
+
switch does not discard pan/zoom position, selection or hover — writing the
|
|
234
|
+
zero measurement through would unmount and rebuild all of it. And `'auto'`
|
|
235
|
+
needs a parent with a **definite** width: a parent sized by its own content
|
|
236
|
+
(a float, an `inline-block`, a grid `auto` track, a flex child without
|
|
237
|
+
`min-width: 0`) measures 0, and the chart is the content that would have
|
|
238
|
+
given it a width, so the chart stays blank with no error.
|
|
239
|
+
|
|
240
|
+
Three independent consumers reported the explicit-pixel requirement; the
|
|
241
|
+
third was multiplying the same ~25-line measure-and-gate hook across seven
|
|
242
|
+
panes.
|
|
243
|
+
|
|
244
|
+
### Changed
|
|
245
|
+
|
|
246
|
+
- **charts: `ChartContainerProps.width` is now `number | 'auto'` and optional**
|
|
247
|
+
(was a required `number`). Strictly additive for callers passing a number.
|
|
248
|
+
|
|
249
|
+
### Fixed
|
|
250
|
+
|
|
251
|
+
- **charts: value-axis pan and zoom no longer snap to whole integers**
|
|
252
|
+
(shipped inside [#653]). `panRange`/`zoomRange` were written for a
|
|
253
|
+
millisecond axis and silently assumed every axis was one, so a value domain
|
|
254
|
+
of `[0.5, 10800]` snapped its floor to `0` and `[0.001, 1]` collapsed to
|
|
255
|
+
`[0, 1]` — fatal under a log axis, where `log(0)` is undefined. Gestures now
|
|
256
|
+
snap only on a **time** axis (`ViewportOptions.snap`), and a log x axis pans
|
|
257
|
+
by ratio and zooms in log space, keeping the value under the cursor fixed.
|
|
258
|
+
Time-axis behaviour is unchanged.
|
|
259
|
+
|
|
260
|
+
## [0.59.0] — 2026-08-11
|
|
261
|
+
|
|
262
|
+
### Added
|
|
263
|
+
|
|
264
|
+
- **charts: `<YAxis scale="symlog">` — linear through zero, logarithmic beyond**
|
|
265
|
+
([PND-SYMLOG]). The third `scale` kind, for a **diverging** measure spanning
|
|
266
|
+
orders of magnitude on both sides of zero. `scale="log"` cannot express that
|
|
267
|
+
domain at all (no zero, no negatives) and `scale="linear"` flattens everything
|
|
268
|
+
outside the top decade onto the axis line — so the small and mid-range values,
|
|
269
|
+
usually the finding, become unreadable.
|
|
270
|
+
|
|
271
|
+
The knee is set by the new **`linearWindow`** prop as a _fraction of the
|
|
272
|
+
domain's largest magnitude_ (default `0.02`): on a ±1M domain the axis is
|
|
273
|
+
linear through ±20k and logarithmic beyond. Relative rather than absolute so
|
|
274
|
+
it survives a domain change with no arithmetic at the call site. Values are
|
|
275
|
+
strictly monotonic across the knee, and zero has a real position. A fraction
|
|
276
|
+
outside `(0, 1]` is unusable as a knee, so the axis draws with the default and
|
|
277
|
+
dev-warns which window is in force.
|
|
278
|
+
|
|
279
|
+
**The tick ladder is pond's, not d3's.** `scaleSymlog` supplies the transform
|
|
280
|
+
but ticks it _linearly_, which puts every label in the top decade and none in
|
|
281
|
+
the linear window the scale exists to open up. `<YAxis scale="symlog">` grids
|
|
282
|
+
zero, ±the knee, and mirrored decades beyond it, thinned to the tick budget
|
|
283
|
+
the same way the log path thins its decades, clipped to the domain. When
|
|
284
|
+
`linearWindow` swallows the domain there is nothing left to grid
|
|
285
|
+
logarithmically, and the axis defers to the linear ticks — which is correct,
|
|
286
|
+
not a fallback: inside the knee, symlog _is_ linear.
|
|
287
|
+
|
|
288
|
+
It removes a workaround whose cost was **silence**: pre-transforming values
|
|
289
|
+
into a ±1 plot space with a linear axis pinned to `[-1, 1]` leaves tick
|
|
290
|
+
positions in plot space while their labels must read in real units, so
|
|
291
|
+
computing the two by different routes yields a chart that confidently labels
|
|
292
|
+
positions it does not occupy — no exception, no visual artifact.
|
|
293
|
+
|
|
294
|
+
**If you are replacing a hand-rolled curve, the shape will shift.** `symlog` is
|
|
295
|
+
the single smooth `sign(x) · log1p(|x / knee|)`, not two joined segments; a
|
|
296
|
+
hand-rolled curve that is exactly linear below the knee and `log10` above is the
|
|
297
|
+
same family with a different shape. Migrating one, a consumer measured small
|
|
298
|
+
values at **roughly half** their former height (on a ±9M domain, 283k moved from
|
|
299
|
+
0.44 to 0.24 of the half-plot above the zero line) with order, tail dominance and
|
|
300
|
+
the several-fold lift over a linear axis all preserved. No `linearWindow` recovers the piecewise shape — the
|
|
301
|
+
difference is the curve, not the knee.
|
|
302
|
+
|
|
303
|
+
- **charts: `<BarChart maxBarWidth>` — cap a bar's ink independently of its slot**
|
|
304
|
+
([PND-BARWIDTH]). Applied after the `gap` inset and centred in the slot, with
|
|
305
|
+
`theme.bar[as].maxWidth` as the fallback (the same relationship `gap` has) and
|
|
306
|
+
uncapped when neither is set.
|
|
307
|
+
|
|
308
|
+
It is the **absolute** half of the width vocabulary. `gap` is _relative_, so
|
|
309
|
+
with it alone bar width is always `slot - gap` and fattens as the plot widens
|
|
310
|
+
— and a fixed ink width is what makes a measure comparable **between** panes,
|
|
311
|
+
since bars that widen with their pane read as different weights of the same
|
|
312
|
+
thing. Neither existing spelling expresses "spread the slots, pin the bar":
|
|
313
|
+
`maxBandWidth = barWidth + gap` pins the bar but stops the slots spreading,
|
|
314
|
+
and `maxBandWidth = slotCap` spreads them but lets the bar grow. The
|
|
315
|
+
workaround was to compute `gap` from the band width you predicted the library
|
|
316
|
+
would pick — a re-derivation of pond's layout arithmetic in consumer code,
|
|
317
|
+
which goes silently wrong the moment that rule changes on either side.
|
|
318
|
+
|
|
319
|
+
Pairs with `<ChartContainer maxBandWidth>` (which caps the **slot**) and
|
|
320
|
+
`minWidth` still wins if the two bounds would invert. **A single-series bar's
|
|
321
|
+
hit target stays its whole slot**, so narrow ink costs nothing in clickability;
|
|
322
|
+
on a **stacked** chart the cap does narrow the target, because a stack must
|
|
323
|
+
hit-test its drawn segment rect to resolve which segment.
|
|
324
|
+
|
|
325
|
+
- **charts: `<BarChart categories columns>` — a first-class stacked category
|
|
326
|
+
chart** ([PND-CATSTACK]). Each datum is `{ label, values }` and `columns`
|
|
327
|
+
names the groups to stack bottom → top, the same relationship
|
|
328
|
+
`series` + `columns` already has. New `categoryStacks` reader and
|
|
329
|
+
`CategoryStackDatum` type; geometry, `marks` and the categorical axis are
|
|
330
|
+
unchanged from the single-value case, so this reaches the shipped
|
|
331
|
+
`drawStacks` path with no new draw code. A missing or non-finite group reads
|
|
332
|
+
as a **gap**, not a zero.
|
|
333
|
+
|
|
334
|
+
**It removes a workaround with three costs**, the third only visible since
|
|
335
|
+
0.58.0: composing the picture from one `categories` layer per _cumulative
|
|
336
|
+
total_ (drawn outermost-first so each overpaints the one beneath) meant a
|
|
337
|
+
hand-assembled legend, label thinning blind to the sibling layers, and — because
|
|
338
|
+
a selection entry keys on `(layer id, mark)` — a controlled set replicated
|
|
339
|
+
across every segment layer, where missing one made a selected bar recede
|
|
340
|
+
**from the waist up**. Because `marks` is indexed by **bin**, one entry naming
|
|
341
|
+
`(id, mark)` now matches every segment of a bar, so that failure is not
|
|
342
|
+
expressible rather than merely fixed.
|
|
343
|
+
|
|
344
|
+
### Fixed
|
|
345
|
+
|
|
346
|
+
- **charts: a selected segment of a stack with `colors` no longer collapses to
|
|
347
|
+
the flat `highlight`.** `StackStyle.groupColored` — the "a selected segment
|
|
348
|
+
keeps its own fill" exclusion — was gated on the _theme ramp_ having painted
|
|
349
|
+
the stack, so a call site passing `colors` lost it and both segments of a
|
|
350
|
+
selected bar went one `highlight` blue, losing the segment distinction exactly
|
|
351
|
+
where the reader is looking. The gate's stated reason ("a ramp entry the call
|
|
352
|
+
site overrode is no longer the ramp's colour, so its receded counterpart would
|
|
353
|
+
be wrong") applies to the _derived_ companions `dimmedFills` / `hoverFills`,
|
|
354
|
+
which must invent a per-group colour; `groupColored` derives nothing. It now
|
|
355
|
+
gates on **whether the resolved fills actually differ**, so a `colors` map keeps
|
|
356
|
+
its colours under selection, while a multi-group stack under a theme that gives
|
|
357
|
+
its groups no distinct colours at all (no ramp, no roles, no `colors` — e.g.
|
|
358
|
+
`estelaTheme`) still takes the themed `highlight`, because there is no
|
|
359
|
+
meaning-carrying colour there to preserve and suppressing the highlight would
|
|
360
|
+
leave selection invisible. Found building [PND-CATSTACK], where the old gate made
|
|
361
|
+
the first-class stack render _worse_ under selection than the workaround it
|
|
362
|
+
replaces; the second half was found in review, since every story and test renders
|
|
363
|
+
`defaultTheme`, whose ramp hides the difference.
|
|
364
|
+
|
|
365
|
+
- **all packages: `API.md` now ships inside the npm tarball.** The agent-facing
|
|
366
|
+
map of every public export across the six packages — one line per export with
|
|
367
|
+
its purpose and source path — was repo-only, so an agent working in a
|
|
368
|
+
_consuming_ repo had to crawl `node_modules/*/dist/*.d.ts` or go to the
|
|
369
|
+
network to learn the surface. It is now copied in by each package's existing
|
|
370
|
+
`prepack` (the same mechanism that already ships `README`, `LICENSE` and
|
|
371
|
+
`CHANGELOG`) and listed in `files`. ~69kB per tarball.
|
|
372
|
+
|
|
373
|
+
Every package carries the same **monorepo-wide** copy rather than a
|
|
374
|
+
per-package slice, deliberately: the packages compose, and knowing what is
|
|
375
|
+
next door is most of the value. The header now names its audience and
|
|
376
|
+
resolves repo-relative source paths against GitHub, since inside
|
|
377
|
+
`node_modules` a bare `packages/core/src/…` points nowhere.
|
|
378
|
+
|
|
379
|
+
- **charts: `BarStyle.dimmed`'s precedence over per-bar and per-band colour is
|
|
380
|
+
documented, and pinned.** A consumer migrating onto 0.58.0 read
|
|
381
|
+
`BarStyle.hover`'s documented `binColors` exclusion ("pops each bar's _own_
|
|
382
|
+
fill"), reasonably generalized it to `dimmed`, concluded their `binColors` and
|
|
383
|
+
`thresholds` charts would get no de-emphasis, and was about to hand-dim inside
|
|
384
|
+
their own colour arrays. The opposite is true: **an unselected bar takes
|
|
385
|
+
`dimmed`, discarding its per-bar colour, and a banded bar draws flat rather
|
|
386
|
+
than dimming each band.** The asymmetry is deliberate — emphasis preserves a
|
|
387
|
+
per-bar colour because that colour is what the value means, while a receded
|
|
388
|
+
bar's job is to stop competing over meaning — but only `hover` said anything,
|
|
389
|
+
so generalizing was the natural read. `dimmed` now spells out all three paths
|
|
390
|
+
(`binColors`/`binFills`, bands/thresholds, and the per-group stack fallback
|
|
391
|
+
through `StackStyle.dimmedFills`), `hover` scopes its exclusion to the live
|
|
392
|
+
states, and three tests pin the behaviour.
|
|
393
|
+
|
|
394
|
+
- **charts: the `theme.list` register no longer reads as if it carries
|
|
395
|
+
`dimmed`.** Its doc mentioned `highlight`/`dimmed` while explaining that a
|
|
396
|
+
list resolves glyph state through the bar tokens — accurate, but sitting in a
|
|
397
|
+
sentence about per-metric resolution it read as a field list, and cost the
|
|
398
|
+
same consumer a couple of passes to rule out a missing `list.dimmed`. Now
|
|
399
|
+
states explicitly that those are `BarStyle` tokens resolved via
|
|
400
|
+
`theme.bar[as]`, and that this register carries exactly its five values.
|
|
401
|
+
|
|
64
402
|
## [0.58.0] — 2026-08-10
|
|
65
403
|
|
|
66
404
|
### Added
|
package/dist/AreaChart.d.ts
CHANGED
|
@@ -75,6 +75,58 @@ export interface AreaChartCommon<S extends SeriesSchema = SeriesSchema, VS exten
|
|
|
75
75
|
* `DecimateOption`.
|
|
76
76
|
*/
|
|
77
77
|
decimate?: DecimateOption;
|
|
78
|
+
/**
|
|
79
|
+
* **Threshold breakpoints** ([PND-BANDAREA]) — colour the area *along its
|
|
80
|
+
* height* against a ladder, so the fill (and the value line on its top edge)
|
|
81
|
+
* shows which zone each stretch of the series sits in: low / medium / high
|
|
82
|
+
* as bands that switch exactly at the breakpoints. Breakpoints are
|
|
83
|
+
* **absolute data values** in the axis's own units — `[1, 2]` means
|
|
84
|
+
* "warning above 1, alarm above 2", not offsets from the baseline — and `n`
|
|
85
|
+
* of them make `n + 1` bands. Each must be finite and greater than zero;
|
|
86
|
+
* anything else is dropped with a dev warning (the ladder is walked on the
|
|
87
|
+
* magnitude, so a negative breakpoint is not expressible).
|
|
88
|
+
*
|
|
89
|
+
* ```tsx
|
|
90
|
+
* // neutral to 1, warning 1–2, alarm above 2
|
|
91
|
+
* <AreaChart series={s} column="value" thresholds={[1, 2]} />
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* Band fills come from {@link AreaStyle.bands} on the resolved role
|
|
95
|
+
* (`theme.area[as] ?? theme.area.default`), overridden by {@link bandColors}.
|
|
96
|
+
* Breakpoints are data and live here; colour stays in the theme — the same
|
|
97
|
+
* split `<BarChart thresholds>` uses ([PND-BANDBAR2]), and the same
|
|
98
|
+
* magnitude-mirrored reading: values below zero (an above/below-axis area
|
|
99
|
+
* with `baseline={0}`) walk the same ±ladder without negative breakpoints.
|
|
100
|
+
*
|
|
101
|
+
* **A banded area is still one area.** One hit region, one legend row, one
|
|
102
|
+
* readout identity; hover / selection strengthen the fill and keep the band
|
|
103
|
+
* colours, and a swept window's `spanColor` single-hue swap is skipped for
|
|
104
|
+
* the same reason the banded bar keeps its own colours — one hue would
|
|
105
|
+
* erase the very thing the bands encode. The banded fill is **flat** (the
|
|
106
|
+
* grade to transparent at the baseline is dropped): the fade encoded
|
|
107
|
+
* distance-from-baseline, which the ladder now states discretely. The
|
|
108
|
+
* outline strokes in the band hues too, switching exactly at each crossing.
|
|
109
|
+
*
|
|
110
|
+
* Composes with `curve`, `gaps` and `decimate` unchanged — banding rides
|
|
111
|
+
* the paint, not the geometry, so M4 decimation stays on (unlike the bar
|
|
112
|
+
* envelope, which collapses many bars into one rect and so cannot carry
|
|
113
|
+
* per-band colour). The inferred gap connectors (`'dashed'` / `'step'` /
|
|
114
|
+
* `'fade'`) keep the role's line colour: they are guesses about absent
|
|
115
|
+
* data, and a zone colour would over-claim exactly where nothing was
|
|
116
|
+
* measured.
|
|
117
|
+
*/
|
|
118
|
+
thresholds?: readonly number[];
|
|
119
|
+
/**
|
|
120
|
+
* Call-site override for the {@link thresholds} band fills — `bandColors[k]`
|
|
121
|
+
* paints the band above `thresholds[k - 1]`, so a ladder of `n` thresholds
|
|
122
|
+
* reads `n + 1` entries. Omitted ⇒ {@link AreaStyle.bands} from the theme.
|
|
123
|
+
*
|
|
124
|
+
* Prefer the theme for anything a design system owns; this is the escape
|
|
125
|
+
* hatch for a one-off ladder that shouldn't mint a theme role. If neither
|
|
126
|
+
* source supplies enough entries, the shortfall falls back to the flat fill
|
|
127
|
+
* and dev-warns rather than silently drawing an unbanded area.
|
|
128
|
+
*/
|
|
129
|
+
bandColors?: readonly string[];
|
|
78
130
|
/**
|
|
79
131
|
* This layer's `<Legend>` row: `false` ⇒ no row (opt out), a string ⇒ the
|
|
80
132
|
* row's display name. **Omitted ⇒ a row named by the layer's readout
|
|
@@ -151,6 +203,6 @@ export declare function resolveAreaBaseline(baseline: number | undefined, yScale
|
|
|
151
203
|
* </Layers>
|
|
152
204
|
* ```
|
|
153
205
|
*/
|
|
154
|
-
export declare function AreaChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, readout, as: semantic, axis, id, baseline, curve, gaps, decimate, legend, index, }: AreaChartProps<S, VS>): null;
|
|
206
|
+
export declare function AreaChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, readout, as: semantic, axis, id, baseline, curve, gaps, decimate, thresholds, bandColors, legend, index, }: AreaChartProps<S, VS>): null;
|
|
155
207
|
export {};
|
|
156
208
|
//# sourceMappingURL=AreaChart.d.ts.map
|
package/dist/AreaChart.js
CHANGED
|
@@ -9,6 +9,7 @@ import { DEFAULT_GAP_MODE, DEFAULT_GAP_CONNECTOR_OPACITY, } from './gaps.js';
|
|
|
9
9
|
import { ContainerContext, LayersContext, } from './context.js';
|
|
10
10
|
import { legendLabelFor, useLegendItems, } from './swatch.js';
|
|
11
11
|
import { useSlotKey } from './use-slot-key.js';
|
|
12
|
+
import { useBandLadder } from './use-band-ladder.js';
|
|
12
13
|
/** Read a d3 linear scale's domain lower bound (the axis floor) from the plain
|
|
13
14
|
* `(value) => pixel` function the row hands to `draw`. The runtime object is a
|
|
14
15
|
* d3 `ScaleLinear` (it carries `.domain()`); the {@link RowLayer} type narrows
|
|
@@ -56,7 +57,7 @@ function domainFloor(yScale) {
|
|
|
56
57
|
* </Layers>
|
|
57
58
|
* ```
|
|
58
59
|
*/
|
|
59
|
-
export function AreaChart({ series, column, readout, as: semantic, axis, id, baseline, curve, gaps = DEFAULT_GAP_MODE, decimate = true, legend, index = 0, }) {
|
|
60
|
+
export function AreaChart({ series, column, readout, as: semantic, axis, id, baseline, curve, gaps = DEFAULT_GAP_MODE, decimate = true, thresholds, bandColors, legend, index = 0, }) {
|
|
60
61
|
const container = useContext(ContainerContext);
|
|
61
62
|
if (container === null) {
|
|
62
63
|
throw new Error('<AreaChart> must be rendered inside a <ChartContainer>');
|
|
@@ -87,6 +88,11 @@ export function AreaChart({ series, column, readout, as: semantic, axis, id, bas
|
|
|
87
88
|
// Styling: semantic identifier → theme area style. The single styling channel.
|
|
88
89
|
const { area } = container.theme;
|
|
89
90
|
const style = (semantic !== undefined ? area[semantic] : undefined) ?? area.default;
|
|
91
|
+
// ── Threshold ladder ([PND-BANDAREA]) — breakpoints normalized + paired
|
|
92
|
+
// with `bandColors` → the role's `AreaStyle.bands` in the shared
|
|
93
|
+
// {@link useBandLadder} (one contract with `<BarChart thresholds>`,
|
|
94
|
+
// including every dev warning).
|
|
95
|
+
const bandLadder = useBandLadder('AreaChart', thresholds, bandColors, style.bands, style.fill);
|
|
90
96
|
// Series identity for the readout (the `as` role, else the column name).
|
|
91
97
|
const label = semantic ?? column;
|
|
92
98
|
const curveFactory = resolveCurve(curve);
|
|
@@ -249,7 +255,13 @@ export function AreaChart({ series, column, readout, as: semantic, axis, id, bas
|
|
|
249
255
|
strokeSpanEdges(ctx, [xScale(spanX[0]), xScale(spanX[1])], ctx.canvas.height, container.theme.annotation?.spanEdge ?? '#f0b26b');
|
|
250
256
|
const [outStyle, outAlpha] = areaStateStyle(style, 'dimmed');
|
|
251
257
|
const [inStyle] = areaStateStyle(style, 'selected');
|
|
252
|
-
return drawPartitioned(ctx, [xScale(spanX[0]), xScale(spanX[1])], plotExtentOf(ctx, xScale, yScale).height, fill(outStyle, outAlpha), fill(
|
|
258
|
+
return drawPartitioned(ctx, [xScale(spanX[0]), xScale(spanX[1])], plotExtentOf(ctx, xScale, yScale).height, fill(outStyle, outAlpha), fill(
|
|
259
|
+
// A banded area skips the window's single-hue swap: one hue
|
|
260
|
+
// would erase the very thing the bands encode (the banded bar's
|
|
261
|
+
// rule), so the window strengthens and keeps the ladder.
|
|
262
|
+
style.spanColor === undefined ||
|
|
263
|
+
!soleSpannedTrace ||
|
|
264
|
+
bandLadder !== undefined
|
|
253
265
|
? inStyle
|
|
254
266
|
: { ...inStyle, color: style.spanColor, fill: style.spanColor }, 1), true, 0, plotExtentOf(ctx, xScale, yScale).width);
|
|
255
267
|
function drawAreaWith(st) {
|
|
@@ -262,7 +274,7 @@ export function AreaChart({ series, column, readout, as: semantic, axis, id, bas
|
|
|
262
274
|
// `NaN` and poison every coordinate in the fill path. Fall back
|
|
263
275
|
// to the axis floor, which is exactly what an omitted baseline
|
|
264
276
|
// already resolves to.
|
|
265
|
-
resolveAreaBaseline(baseline, yScale), curveFactory, gaps, gapConnectorOpacity, decimate);
|
|
277
|
+
resolveAreaBaseline(baseline, yScale), curveFactory, gaps, gapConnectorOpacity, decimate, bandLadder);
|
|
266
278
|
}
|
|
267
279
|
},
|
|
268
280
|
},
|
|
@@ -281,6 +293,7 @@ export function AreaChart({ series, column, readout, as: semantic, axis, id, bas
|
|
|
281
293
|
gaps,
|
|
282
294
|
gapConnectorOpacity,
|
|
283
295
|
decimate,
|
|
296
|
+
bandLadder,
|
|
284
297
|
axis,
|
|
285
298
|
id,
|
|
286
299
|
traceState,
|
package/dist/BarChart.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ValueSeries } from 'pond-ts';
|
|
2
2
|
import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
|
|
3
|
-
import { type BinRecord, type CategoryDatum } from './data.js';
|
|
3
|
+
import { type BinRecord, type CategoryDatum, type CategoryStackDatum } from './data.js';
|
|
4
4
|
import { type Orientation } from './bars.js';
|
|
5
5
|
import type { NumericColumn, ValueNumericColumn } from './column-names.js';
|
|
6
6
|
import type { DecimateOption } from './decimate.js';
|
|
@@ -43,11 +43,17 @@ import type { DecimateOption } from './decimate.js';
|
|
|
43
43
|
* `<ChartContainer origin>` does not rescue it: it relabels a value axis but
|
|
44
44
|
* does not re-ladder it.
|
|
45
45
|
* - **`categories`** — an ordered `{ label, value }[]`, one bar per category.
|
|
46
|
-
* Takes
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
46
|
+
* Takes no `column`. Vertical puts the categories on the ordinal **x** axis
|
|
47
|
+
* (the container's band scale); `orientation="horizontal"` puts them on **y**
|
|
48
|
+
* as unit slots and the value on x, and a `<YAxis>` with no explicit `ticks`
|
|
49
|
+
* labels one per category automatically ([PND-HCAT]).
|
|
50
|
+
* - **`categories` + `columns`** — a **stacked** category chart
|
|
51
|
+
* ([PND-CATSTACK]): each datum is `{ label, values }` and `columns` names the
|
|
52
|
+
* groups to stack, bottom → top. The same relationship `series` + `columns`
|
|
53
|
+
* already has, so a category stack is now a first-class shape rather than one
|
|
54
|
+
* `categories` layer per cumulative total. One layer means one `mark` per
|
|
55
|
+
* bar, so **one selection entry lights the whole bar** and the composed
|
|
56
|
+
* workaround's per-layer replication is unnecessary.
|
|
51
57
|
*
|
|
52
58
|
* **Live charts:** `series.byValue(…)` / `.toMap()` mint fresh objects each
|
|
53
59
|
* call, so an inline `series={…}` re-registers this layer every render — on a
|
|
@@ -95,6 +101,12 @@ type BarChartSource<S extends SeriesSchema = SeriesSchema, VS extends ValueSerie
|
|
|
95
101
|
bins?: never;
|
|
96
102
|
column?: never;
|
|
97
103
|
columns?: never;
|
|
104
|
+
} | {
|
|
105
|
+
categories: readonly CategoryStackDatum[];
|
|
106
|
+
columns: readonly string[];
|
|
107
|
+
series?: never;
|
|
108
|
+
bins?: never;
|
|
109
|
+
column?: never;
|
|
98
110
|
};
|
|
99
111
|
/** The props every {@link BarChartSource} mode shares. */
|
|
100
112
|
export interface BarChartCommon<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
|
|
@@ -230,6 +242,43 @@ export interface BarChartCommon<S extends SeriesSchema = SeriesSchema, VS extend
|
|
|
230
242
|
* would invert collapses to the style's `minWidth`.
|
|
231
243
|
*/
|
|
232
244
|
gap?: number;
|
|
245
|
+
/**
|
|
246
|
+
* Cap on a bar's **ink** width in px — applied after the `gap` inset and
|
|
247
|
+
* centred in its slot ([PND-BARWIDTH]). **Omitted ⇒ the theme's `bar`
|
|
248
|
+
* `maxWidth`, and uncapped if that is unset too** (a bar is `slot - gap`
|
|
249
|
+
* wide, as it always was).
|
|
250
|
+
*
|
|
251
|
+
* This is the *absolute* half of the width vocabulary. `gap` is **relative**,
|
|
252
|
+
* so with it alone bar width is always `slot - gap` and fattens as the plot
|
|
253
|
+
* widens; a fixed ink width is what makes a measure comparable **between**
|
|
254
|
+
* panes, since bars that widen with their pane read as different weights of
|
|
255
|
+
* the same thing.
|
|
256
|
+
*
|
|
257
|
+
* Pairs with `<ChartContainer maxBandWidth>`: that caps the **slot** (how far
|
|
258
|
+
* the bars spread), this caps the **ink** inside whatever slot results. The
|
|
259
|
+
* two are independent, which is the point — neither spelling alone expresses
|
|
260
|
+
* "spread the slots, pin the bar":
|
|
261
|
+
*
|
|
262
|
+
* - `maxBandWidth = barWidth + gap` pins the bar but stops the slots
|
|
263
|
+
* spreading;
|
|
264
|
+
* - `maxBandWidth = slotCap` spreads them but lets the bar grow.
|
|
265
|
+
*
|
|
266
|
+
* `theme.bar[as].minWidth` still wins if the two bounds would invert, so the
|
|
267
|
+
* rect can never flip.
|
|
268
|
+
*
|
|
269
|
+
* **On a stacked chart the cap narrows the hit target too**, because a stack
|
|
270
|
+
* hit-tests its drawn segment rect (it must, to resolve *which* segment).
|
|
271
|
+
* A single-series bar is unaffected: it hit-tests its whole slot, so the ink
|
|
272
|
+
* can be narrow while the target stays full width.
|
|
273
|
+
*
|
|
274
|
+
* That holds in **both orientations**, and costs a deliberate guard to keep:
|
|
275
|
+
* a single-series *horizontal* chart shares the oriented draw/hit path with
|
|
276
|
+
* stacks, so the cap is withheld from its hit rect explicitly (`groups.length >
|
|
277
|
+
* 1`). Vertical charts get it for free — `barSlotRect` takes no cap. Don't
|
|
278
|
+
* "simplify" that guard away: the rule follows from segment disambiguation,
|
|
279
|
+
* which is a property of a stack, not of an axis.
|
|
280
|
+
*/
|
|
281
|
+
maxBarWidth?: number;
|
|
233
282
|
/**
|
|
234
283
|
* **M4 column decimation** (charts decimator wave). **Omitted ⇒ `true`**: once
|
|
235
284
|
* the visible bars are denser than ~2 per device pixel (each slot < ~1px), the
|
|
@@ -319,6 +368,6 @@ export type BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends Valu
|
|
|
319
368
|
* </Layers>
|
|
320
369
|
* ```
|
|
321
370
|
*/
|
|
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;
|
|
371
|
+
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, maxBarWidth, decimate, legend, index, }: BarChartProps<S, VS>): null;
|
|
323
372
|
export {};
|
|
324
373
|
//# sourceMappingURL=BarChart.d.ts.map
|