@pond-ts/charts 0.57.0 → 0.59.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.
Files changed (85) hide show
  1. package/API.md +576 -0
  2. package/CHANGELOG.md +1213 -1
  3. package/dist/AreaChart.d.ts +12 -1
  4. package/dist/AreaChart.js +131 -13
  5. package/dist/BarChart.d.ts +56 -7
  6. package/dist/BarChart.js +263 -39
  7. package/dist/BarList.d.ts +85 -5
  8. package/dist/BarList.js +25 -4
  9. package/dist/BoxList.d.ts +70 -3
  10. package/dist/BoxList.js +21 -7
  11. package/dist/BoxPlot.d.ts +2 -1
  12. package/dist/BoxPlot.js +101 -9
  13. package/dist/Candlestick.d.ts +13 -1
  14. package/dist/Candlestick.js +89 -3
  15. package/dist/ChartContainer.d.ts +36 -48
  16. package/dist/ChartContainer.js +465 -59
  17. package/dist/ChartRow.d.ts +9 -2
  18. package/dist/ChartRow.js +176 -14
  19. package/dist/HeatMap.d.ts +176 -0
  20. package/dist/HeatMap.js +344 -0
  21. package/dist/Layers.d.ts +5 -1
  22. package/dist/Layers.js +1014 -253
  23. package/dist/Legend.js +8 -4
  24. package/dist/LineChart.d.ts +18 -1
  25. package/dist/LineChart.js +165 -4
  26. package/dist/ListTable.d.ts +30 -3
  27. package/dist/ListTable.js +381 -23
  28. package/dist/ScatterChart.d.ts +3 -2
  29. package/dist/ScatterChart.js +68 -4
  30. package/dist/XAxis.js +40 -22
  31. package/dist/YAxis.d.ts +58 -2
  32. package/dist/YAxis.js +3 -1
  33. package/dist/area.d.ts +34 -1
  34. package/dist/area.js +88 -1
  35. package/dist/bars.d.ts +67 -6
  36. package/dist/bars.js +250 -35
  37. package/dist/box.d.ts +2 -2
  38. package/dist/box.js +158 -40
  39. package/dist/brush.d.ts +142 -0
  40. package/dist/brush.js +179 -0
  41. package/dist/child-index.d.ts +27 -0
  42. package/dist/child-index.js +57 -0
  43. package/dist/context.d.ts +870 -39
  44. package/dist/cursors.d.ts +161 -0
  45. package/dist/cursors.js +503 -0
  46. package/dist/data.d.ts +38 -0
  47. package/dist/data.js +43 -0
  48. package/dist/decimate.d.ts +78 -1
  49. package/dist/decimate.js +157 -0
  50. package/dist/format.d.ts +15 -0
  51. package/dist/format.js +16 -1
  52. package/dist/heat.d.ts +163 -0
  53. package/dist/heat.js +659 -0
  54. package/dist/index.d.ts +13 -4
  55. package/dist/index.js +27 -0
  56. package/dist/line.d.ts +137 -0
  57. package/dist/line.js +328 -0
  58. package/dist/ohlc.d.ts +16 -1
  59. package/dist/ohlc.js +93 -4
  60. package/dist/range.d.ts +14 -1
  61. package/dist/range.js +24 -3
  62. package/dist/scatter.d.ts +17 -9
  63. package/dist/scatter.js +221 -33
  64. package/dist/select.d.ts +13 -5
  65. package/dist/select.js +14 -6
  66. package/dist/selection-fixtures.d.ts +174 -0
  67. package/dist/selection-fixtures.js +569 -0
  68. package/dist/selection-stories.d.ts +73 -0
  69. package/dist/selection-stories.js +301 -0
  70. package/dist/selectors.d.ts +316 -0
  71. package/dist/selectors.js +391 -0
  72. package/dist/span.d.ts +122 -0
  73. package/dist/span.js +203 -0
  74. package/dist/sweep.d.ts +154 -0
  75. package/dist/sweep.js +282 -0
  76. package/dist/theme.d.ts +510 -5
  77. package/dist/theme.js +217 -41
  78. package/dist/tracker.d.ts +6 -0
  79. package/dist/tracker.js +6 -0
  80. package/dist/tradingAxis.fixture.d.ts +78 -0
  81. package/dist/tradingAxis.fixture.js +215 -0
  82. package/dist/useChartLegend.js +18 -3
  83. package/dist/yticks.d.ts +3 -0
  84. package/dist/yticks.js +104 -0
  85. package/package.json +6 -5
package/dist/theme.d.ts CHANGED
@@ -89,6 +89,24 @@ export interface ChartTheme {
89
89
  readonly default: CandleStyle;
90
90
  readonly [semantic: string]: CandleStyle;
91
91
  };
92
+ /**
93
+ * Map from a heat map's semantic identifier to its **interaction states**
94
+ * ({@link HeatMap}). Optional throughout; with no `heat` slot a live cell
95
+ * takes the pre-states treatment (one outline per cell, in
96
+ * `bar.highlight`).
97
+ *
98
+ * **Only the states, and deliberately so.** A heat map's *geometry* is
99
+ * bar-family — the slot gap, the minimum bin width, the outline weight —
100
+ * and it reads all of it from `bar[semantic] ?? bar.default`, which is
101
+ * right: a cell is a bar's slot with colour instead of height. What it
102
+ * cannot share is state styling, because a bar's fill is free and a cell's
103
+ * fill **is the datum** (see {@link HeatStates}). So the split is between
104
+ * the two things, not an accident of where the tokens landed.
105
+ */
106
+ readonly heat?: {
107
+ readonly default: HeatStates;
108
+ readonly [semantic: string]: HeatStates;
109
+ };
92
110
  /**
93
111
  * Map from a bar's semantic identifier to its style — the fill, the
94
112
  * selected-bar highlight, and the slot gap / minimum width ({@link BarChart}).
@@ -143,6 +161,29 @@ export interface ChartTheme {
143
161
  };
144
162
  /** Crosshair / tracker stroke colour. Falls back to {@link axis.label} if unset. */
145
163
  readonly cursor?: string;
164
+ /**
165
+ * The **drag band** — the live region the shared brush paints while a drag
166
+ * is in flight (`<RangeCursor>`'s band and `<MultiSelector>`'s sweep are the
167
+ * same pixels; see `renderBrushBand`). `fill` washes the covered span and
168
+ * `edge` hairlines its two boundaries at 1px, so the band has a readable
169
+ * start/end while the gesture is still live.
170
+ *
171
+ * **Optional, and back-compatible when omitted:** with no `brush` the band
172
+ * falls back to the cursor ink at 0.12 with no edges — byte-for-byte what
173
+ * every theme drew before this token existed. `defaultTheme` opts in with
174
+ * the *selection* blue at 7%: the band is about to become a selection, so it
175
+ * should be the same hue as one (whereas the resting bars are teal).
176
+ */
177
+ readonly brush?: {
178
+ /**
179
+ * The band's wash. Drawn at **full element opacity**, so carry the alpha
180
+ * in the colour (`rgba(…, 0.07)`) — a solid hex here paints over the marks
181
+ * the band is supposed to be previewing.
182
+ */
183
+ readonly fill: string;
184
+ /** Stroke for the band's 1px start/end edges. Omit for a fill-only band. */
185
+ readonly edge?: string;
186
+ };
146
187
  /**
147
188
  * Readout chip background (the `flag` / `inline` tracker modes). The value text
148
189
  * is the series colour; this is the panel behind it. Falls back to the plot
@@ -177,6 +218,18 @@ export interface ChartTheme {
177
218
  */
178
219
  readonly annotation?: {
179
220
  readonly color: string;
221
+ /**
222
+ * **EXPERIMENTAL ([PND-ANNSNAP]).** Ink for the vertical rules at a swept
223
+ * window's edges — a preview of what promoting the sweep to an annotation
224
+ * would look like. Lighter than {@link color} because these mark a
225
+ * *candidate* range rather than a committed mark. **Omitted ⇒ a light
226
+ * orange fallback.**
227
+ *
228
+ * Not settled: the rules are drawn canvas-side, under the trace ink,
229
+ * whereas a real annotation renders in the SVG overlay above it. If the
230
+ * look is kept, that mismatch has to be resolved rather than papered over.
231
+ */
232
+ readonly spanEdge?: string;
180
233
  readonly fillOpacity: number;
181
234
  readonly depth: readonly [number, number, number];
182
235
  /**
@@ -223,6 +276,62 @@ export interface ChartTheme {
223
276
  readonly border: string;
224
277
  readonly text: string;
225
278
  };
279
+ /**
280
+ * The **row-chart register** — `<BarList>` / `<BoxList>`, whose row states
281
+ * live on chrome the canvas has no equivalent of.
282
+ *
283
+ * A row chart cannot signal state the way a column chart does. Two reasons,
284
+ * and the second is the load-bearing one:
285
+ *
286
+ * - **The row is the target, not the bar.** A vertical bar can be its own
287
+ * hit area because every mark spans the full column width; a row's mark
288
+ * is as short as its value, so a 4% row would be a 30px sliver. The
289
+ * label gutter, the track and the trailing value are one target, and the
290
+ * thing that lights has to be the whole **band**.
291
+ * - **The band carries selection alone.** In a multi-metric row the fill
292
+ * *is* the identity of the metric, so it cannot also carry state — the
293
+ * same channel rule the canvas marks follow. Band + rail must read as
294
+ * selected with no help from the fill, and designing the single-metric
295
+ * case that way too means one treatment covers every row chart.
296
+ *
297
+ * So the two values here are the ones with no canvas counterpart: the row
298
+ * **band** tints. Everything else resolves from tokens that already exist
299
+ * and are per-metric where they should be — a selected glyph fill takes
300
+ * `bar[as].highlight` and a dimmed one `bar[as].dimmed` — so a consumer who
301
+ * themes their bars gets a coherent list without theming it twice.
302
+ *
303
+ * **To be unambiguous, because this reads as a list of fields on this
304
+ * block:** `highlight` / `dimmed` are **`BarStyle`** tokens, resolved through
305
+ * `theme.bar[as]`. They are not fields of `theme.list` and there is no
306
+ * `list.dimmed` — this register carries exactly the five values below.
307
+ *
308
+ * **The rail is deliberately NOT per-metric.** There is one rail per row
309
+ * and a row may carry several metrics, so it cannot resolve through
310
+ * `bar[as]` the way a fill does; it lives here with the bands.
311
+ *
312
+ * **Optional, and back-compatible when omitted:** with no `list` the rows
313
+ * keep exactly the pre-token look — a hover band from `legend.border`, a
314
+ * selection rail from the annotation register, and no dimmed state at all.
315
+ */
316
+ readonly list?: {
317
+ /** Row stripe behind the hovered row — the whole band, gutter to value. */
318
+ readonly hoverBand: string;
319
+ /** The hovered row's 3px inset left edge. Never the selection hue. */
320
+ readonly hoverRail: string;
321
+ /** Row stripe behind a selected row. */
322
+ readonly selectedBand: string;
323
+ /** The selected row's 3px inset left edge. */
324
+ readonly selectedRail: string;
325
+ /**
326
+ * Reference ink — per-row target markers, thresholds, reference ticks.
327
+ *
328
+ * **Reserved away from the selection hue on purpose.** On a bullet row
329
+ * the marker sits *inside* the mark that selection recolors, so a tick
330
+ * in the selection blue is the one collision the rest of the language
331
+ * cannot absorb: you could not tell a target from a selected bar.
332
+ */
333
+ readonly markerInk: string;
334
+ };
226
335
  }
227
336
  /** A resolved line style: stroke colour + width (px). */
228
337
  export interface LineStyle {
@@ -237,6 +346,41 @@ export interface LineStyle {
237
346
  * observed one at a glance.
238
347
  */
239
348
  readonly dash?: readonly number[];
349
+ /**
350
+ * Stroke width when this trace is the **selected series**, and for the
351
+ * emphasised portion of a swept window. **Omitted ⇒ `width * 2`.**
352
+ *
353
+ * **State on a trace is WEIGHT, not hue** — the channel rule, and the same
354
+ * answer `<Candlestick>` got. A line's **colour is its identity**: it is how
355
+ * a reader tells one series from another in a multi-series plot, so a
356
+ * selected line that turned blue would trade the distinction they need for
357
+ * one they already have from the chrome. Thickening says "this one" without
358
+ * spending the channel that says "which one".
359
+ */
360
+ readonly selectedWidth?: number;
361
+ /** Stroke width on **hover** — the transient echo of `selectedWidth`.
362
+ * **Omitted ⇒ `selectedWidth`**, so hover previews the committed weight. */
363
+ readonly hoverWidth?: number;
364
+ /**
365
+ * Alpha for a trace that is **outside** the active selection — another
366
+ * series is selected, or this trace is outside a swept window.
367
+ * **Omitted ⇒ `0.32`**, the value the rest of the palette recedes to.
368
+ *
369
+ * Alpha rather than a colour, deliberately: a muted line must still read as
370
+ * *which* series it is, so its hue has to survive being pushed back.
371
+ */
372
+ readonly dimmedOpacity?: number;
373
+ /**
374
+ * Ink for the **swept window's** emphasised portion. **Omitted ⇒ the trace
375
+ * keeps its own colour and only thickens.**
376
+ *
377
+ * This is the one place a trace's state may take a hue, and the reason is
378
+ * that a window is a **region of one series** — identity is not in question
379
+ * there, because you can see which line you swept. So the region can read as
380
+ * the same act as the brush band that made it (the selection blue), where a
381
+ * whole-series selection cannot.
382
+ */
383
+ readonly spanColor?: string;
240
384
  }
241
385
  /** A resolved band style: fill colour + opacity (0–1) for the variance envelope. */
242
386
  export interface BandStyle {
@@ -265,6 +409,123 @@ export interface ScatterStyle {
265
409
  readonly selectedWidth: number;
266
410
  /** Colour of the optional per-point text label. */
267
411
  readonly label: string;
412
+ /**
413
+ * The **interaction states** — fill and size per state, and the whole
414
+ * styling channel for a live point when set. Unset ⇒ the pre-states
415
+ * behaviour exactly: every point keeps its resting fill and radius, and a
416
+ * live one is merely re-ringed in `selectedOutline`/`selectedWidth`.
417
+ *
418
+ * A scatter can afford what a candle cannot. A candle's hue *is* its
419
+ * meaning (rising vs falling), so it carries state in weight and alpha
420
+ * alone; a heat cell's colour is its value, so it carries state in chrome.
421
+ * A point's colour encodes nothing by default, so it is free to recolour —
422
+ * and it also has a channel none of the column marks have: **size**.
423
+ *
424
+ * That is why hover and selection split the channels rather than sharing
425
+ * them. See {@link ScatterStates}.
426
+ */
427
+ readonly states?: ScatterStates;
428
+ }
429
+ /**
430
+ * A scatter point's per-state fill and size ({@link ScatterStyle.states}).
431
+ *
432
+ * The radii are given in px against {@link ScatterStyle.radius}, and applied
433
+ * as the **ratio** between them — so a data-driven `radius` encoding still
434
+ * grows and shrinks by the same proportion instead of being flattened to one
435
+ * size the moment a point goes live.
436
+ */
437
+ export interface ScatterStates {
438
+ /**
439
+ * Fill for a **hovered** point — or one under a live drag rect, which is
440
+ * the same state (the sweep lights its covered marks through the plural
441
+ * `hovered`). A brightened form of the resting colour, not a new hue: the
442
+ * preview says "these are the ones", and saying it in the committed colour
443
+ * would make the preview and the commit read alike.
444
+ */
445
+ readonly hover: string;
446
+ /** Hovered radius (px). Hover is the state that spends **size** — it is the
447
+ * channel a lone pointer-over can afford, and a hover does not have to
448
+ * survive being read against a whole field of committed marks. */
449
+ readonly hoverRadius: number;
450
+ /**
451
+ * Fill for a **selected** point. Its radius is deliberately left at
452
+ * {@link ScatterStyle.radius}: selection spends **hue** instead, so
453
+ * committing a sweep does not reflow the cloud under the pointer.
454
+ */
455
+ readonly selected: string;
456
+ /** The ring around a live (hovered or selected) point — what keeps
457
+ * overlapping points countable once a whole swept region shares one fill. */
458
+ readonly halo: string;
459
+ /** Halo width (px); `0` draws none. */
460
+ readonly haloWidth: number;
461
+ /**
462
+ * Radius (px) of a point **outside a non-empty selection**. It shrinks as
463
+ * well as fading because alpha alone at these levels thins a cloud to
464
+ * nearly nothing, and the shape of the unselected field is the thing a
465
+ * scatter's background is *for*.
466
+ */
467
+ readonly dimmedRadius: number;
468
+ /** Alpha of a point outside a non-empty selection. */
469
+ readonly dimmedOpacity: number;
470
+ }
471
+ /**
472
+ * A heat map's **interaction states** ({@link ChartTheme.heat}).
473
+ *
474
+ * A cell has no spare channel at all. A bar's fill is free, so a bar swaps it;
475
+ * a point's colour encodes nothing by default, so a point recolours *and*
476
+ * resizes. A cell's colour **is** its value, and its rect is the grid — so
477
+ * every state here is **chrome added around the cell** or a transform applied
478
+ * uniformly to all of them, and none of them repaints a cell in a colour the
479
+ * ramp could also have produced.
480
+ */
481
+ export interface HeatStates {
482
+ /**
483
+ * The flat overlay painted over a cell **outside a non-empty selection** —
484
+ * carry the alpha in the colour (`rgba(255,255,255,0.62)`), because it is
485
+ * composited over the cell, not applied as one.
486
+ *
487
+ * **A flat overlay, not `globalAlpha`, and the difference is the whole
488
+ * point.** Alpha and value are the same channel on a ramp, so fading a cell
489
+ * slides it along the scale — a dimmed dark cell becomes a resting mid one.
490
+ * An overlay is *uniform and monotonic*: every cell moves by the same
491
+ * transform, so the ramp's **order survives inside the veiled set** and only
492
+ * the cross-set comparison is ambiguous — which is exactly what the
493
+ * {@link perimeter} is there to disambiguate.
494
+ *
495
+ * It is also why this is a colour and not a number: opacity composites with
496
+ * whatever is *behind* the cell (a gridline, a non-white background, another
497
+ * layer), so the same value would veil to different colours in different
498
+ * charts. An overlay is a property of the cell.
499
+ */
500
+ readonly veil: string;
501
+ /**
502
+ * The hovered cell's **double ring**, outer colour first — two concentric
503
+ * rings of {@link ringWidth}, both inside the cell.
504
+ *
505
+ * A single ring cannot work against a ramp: a light ring vanishes at the
506
+ * pale end and a dark one at the dark end, and a cell can be anywhere on the
507
+ * scale. The pair guarantees one of the two reads wherever the cell happens
508
+ * to sit. (The same problem `bar.binFills` has, and a better answer than
509
+ * picking one colour and hoping.)
510
+ */
511
+ readonly hoverRing: readonly [string, string];
512
+ /** Width of each of the two hover rings, in px. */
513
+ readonly ringWidth: number;
514
+ /**
515
+ * The selected region's **perimeter** — one outline around the union of
516
+ * selected cells, not one per cell.
517
+ *
518
+ * Per-cell outlines are what this replaces, and the reason is legible in any
519
+ * screenshot of them: a bordered grid is mostly border, and every interior
520
+ * line says nothing, because it is interior to the selection. Drawn by
521
+ * suppressing each cell edge whose neighbour is also selected, so a
522
+ * selection in several disconnected pieces gets one outline **per piece**,
523
+ * and a hole in the middle of one gets its own — no connectivity pass, and
524
+ * no assumption that a selection is a single rectangle.
525
+ */
526
+ readonly perimeter: string;
527
+ /** Perimeter stroke width, in px. */
528
+ readonly perimeterWidth: number;
268
529
  }
269
530
  /**
270
531
  * A resolved box-and-whisker style ({@link BoxPlot}). The q1→q3 box is a filled
@@ -282,6 +543,68 @@ export interface BoxStyle {
282
543
  readonly medianWidth: number;
283
544
  readonly whisker: string;
284
545
  readonly whiskerWidth: number;
546
+ /**
547
+ * The **tint ladder** — one four-step ladder per interaction state, and the
548
+ * whole styling channel for a box when set. Unset ⇒ the flat
549
+ * `fill`/`stroke`/`median`/`whisker` tokens above, unchanged.
550
+ *
551
+ * Every mark of a box reads its step from the *same* ladder
552
+ * ({@link BoxLadder} documents which step is which), so a state change is a
553
+ * **single palette swap** rather than four independent colour decisions.
554
+ * That is what keeps the quantile read intact across states: the ladder
555
+ * carries its meaning in *lightness*, so moving the whole ladder — brighter
556
+ * teal on hover, blue when committed — leaves every relationship between the
557
+ * marks untouched.
558
+ *
559
+ * Two consequences worth stating, because both are the opposite of what the
560
+ * multi-hue `bar` palette needs:
561
+ *
562
+ * - **Shift the ladder, not one step.** Recolouring only the median, or only
563
+ * the body, breaks the read. All four steps move together and keep their
564
+ * relative lightness spacing.
565
+ * - **Dim without desaturating.** A single-hue ladder has nothing to muddy
566
+ * into, so {@link dimmedOpacity} alone is the receded state — no
567
+ * desaturated companion ladder of the kind `bar.groupsDimmed` needs.
568
+ */
569
+ readonly states?: BoxStates;
570
+ /**
571
+ * Stroke width for a **selected** box's hairlines — the body outline and the
572
+ * whiskers both. Unset ⇒ they keep {@link strokeWidth} / {@link whiskerWidth}.
573
+ *
574
+ * A hairline cannot carry a state in hue alone: at 1px a colour change is
575
+ * nearly invisible, and the whisker is the mark that reaches furthest. A
576
+ * weight change is legible at any width, so selection bumps it (1 → 1.5 on
577
+ * `defaultTheme`) alongside the ladder swap.
578
+ */
579
+ readonly selectedStrokeWidth?: number;
580
+ }
581
+ /**
582
+ * A box's four tint steps, lightest → darkest. Which mark reads which step:
583
+ *
584
+ * | step | box plot | quantile bands |
585
+ * | ---- | --------------- | -------------- |
586
+ * | `0` | body fill / the solid shape's outer bar | outer band |
587
+ * | `1` | the solid shape's inner q1→q3 bar | inner band |
588
+ * | `2` | body stroke + whiskers | — |
589
+ * | `3` | the median rule | median rule |
590
+ *
591
+ * Steps are *positions on one hue's lightness ramp*, not four palette entries —
592
+ * a ladder whose steps don't descend in lightness stops encoding anything.
593
+ */
594
+ export type BoxLadder = readonly [string, string, string, string];
595
+ /** The per-state ladders (see {@link BoxStyle.states}). */
596
+ export interface BoxStates {
597
+ /** Nothing selected anywhere. */
598
+ readonly rest: BoxLadder;
599
+ /** Pointer over this box — a preview of what a click would commit, so it
600
+ * sits between {@link rest} and {@link selected} in strength. */
601
+ readonly hover: BoxLadder;
602
+ /** Committed. Pairs with {@link BoxStyle.selectedStrokeWidth}. */
603
+ readonly selected: BoxLadder;
604
+ /** How far a box **outside** a non-empty selection recedes — the
605
+ * {@link rest} ladder at this alpha. No separate ladder, and deliberately
606
+ * no desaturation (see {@link BoxStyle.states}). */
607
+ readonly dimmedOpacity: number;
285
608
  }
286
609
  /**
287
610
  * A resolved candlestick style ({@link Candlestick}). A candle is unreadable in
@@ -309,6 +632,41 @@ export interface CandleStyle {
309
632
  readonly body: string;
310
633
  readonly wick: string;
311
634
  };
635
+ /**
636
+ * The **interaction state** cues — and note what is *missing* from them.
637
+ *
638
+ * A `bar` swaps its fill and a `box` rotates its whole tint ladder, because
639
+ * on those marks hue is free: the meaning lives in position and in lightness
640
+ * ordering. **A candle's hue is its meaning** — rising vs falling is the
641
+ * first thing anyone reads off it — so a candle introduces *no state colour
642
+ * at all*, not even for its outline. Every cue here is a change of weight or
643
+ * of alpha:
644
+ *
645
+ * - **Live** (hovered *or* selected) — the candle **grows**: its body is
646
+ * stroked in its own colour and its lines thicken to
647
+ * {@link liveWickWidth}, so the mark gets a little heavier and nothing
648
+ * else about it changes. Nothing is recoloured, and nothing is added that
649
+ * the chart doesn't otherwise draw.
650
+ * - **Selected** — the same, plus the rest of the field recedes to
651
+ * {@link dimmedOpacity}. The dimming is what separates a committed
652
+ * selection from a passing hover, since the lit mark looks identical
653
+ * either way.
654
+ *
655
+ * That last point is deliberate but worth knowing: hover and selection are
656
+ * distinguished by what happens to the *other* candles, not by this one.
657
+ *
658
+ * Both optional; unset ⇒ a display-only candle exactly as before.
659
+ */
660
+ readonly dimmedOpacity?: number;
661
+ /**
662
+ * Line weight for a **live** (hovered or selected) candle — its wick, and
663
+ * the stroke around its body that makes the mark grow.
664
+ *
665
+ * Unlike {@link BoxStyle.selectedStrokeWidth}, which only selection triggers,
666
+ * this fires on hover too: a box announces hover by moving its tint ladder,
667
+ * and a candle has no ladder to move.
668
+ */
669
+ readonly liveWickWidth?: number;
312
670
  /** Doji (open === close) — body + wick colours; falls back to `rising` if unset. */
313
671
  readonly neutral?: {
314
672
  readonly body: string;
@@ -343,6 +701,25 @@ export interface AreaStyle {
343
701
  * a stack can be uniformly translucent — just not *graded*.)
344
702
  */
345
703
  readonly flatFill?: boolean;
704
+ /**
705
+ * Interaction state, the area's counterpart of `LineStyle`'s
706
+ * ([PND-TRACESEL]) — and the channels differ because what carries the mark
707
+ * differs. An area's mark is its **fill**, so state is the fill's *strength*
708
+ * plus the edge's weight; a line's mark is a stroke, so state is weight alone.
709
+ *
710
+ * `selectedWidth` thickens the edge, `selectedFillOpacity` strengthens the
711
+ * fill, `dimmedOpacity` recedes the whole shape, and `spanColor` is the one
712
+ * hue a swept **window** may take (identity is not in question inside a
713
+ * single series — see `LineStyle.spanColor`).
714
+ */
715
+ readonly selectedWidth?: number;
716
+ /** Fill opacity when selected. **Omitted ⇒ `min(fillOpacity * 2, 1)`.** */
717
+ readonly selectedFillOpacity?: number;
718
+ /** Alpha for an area outside the active selection. **Omitted ⇒ `0.32`.** */
719
+ readonly dimmedOpacity?: number;
720
+ /** Ink for a swept window's emphasised portion (edge + fill).
721
+ * **Omitted ⇒ the area keeps its own colours and only strengthens.** */
722
+ readonly spanColor?: string;
346
723
  }
347
724
  /**
348
725
  * A resolved bar style: the flat `fill` (scaled by `opacity`, 0–1) plus the
@@ -357,6 +734,28 @@ export interface BarStyle {
357
734
  readonly highlight: string;
358
735
  readonly gap: number;
359
736
  readonly minWidth: number;
737
+ /**
738
+ * Cap on a bar's **ink** width in px, applied after the `gap` inset and
739
+ * centred in the slot ([PND-BARWIDTH]). **Omitted ⇒ uncapped** — a bar is
740
+ * `slot - gap` wide, as it always was.
741
+ *
742
+ * It is the missing half of the width vocabulary, and the reason it cannot be
743
+ * spelled with `gap` alone: `gap` is a *relative* inset, so bar width tracks
744
+ * the slot and fattens as the plot widens. A **fixed** ink width is what makes
745
+ * a measure comparable *between* panes — bars that widen with their pane read
746
+ * as different weights of the same thing. Wanting both (slots spreading to
747
+ * fill, ink pinned) needs two independent knobs; with one, a consumer has to
748
+ * predict the slot width and back-solve the gap, re-deriving pond's own layout
749
+ * arithmetic in their code.
750
+ *
751
+ * Pairs with `<ChartContainer maxBandWidth>`, which caps the **slot**: that
752
+ * one decides how far the bars spread, this one how wide the ink is inside
753
+ * whatever slot results. `minWidth` still wins if the two would invert.
754
+ *
755
+ * `<BarChart maxBarWidth>` overrides this per layer, the same relationship
756
+ * `gap` has.
757
+ */
758
+ readonly maxWidth?: number;
360
759
  readonly outlineWidth: number;
361
760
  /**
362
761
  * Optional distinct **hover** fill, so a bar can read a three-step emphasis —
@@ -384,6 +783,11 @@ export interface BarStyle {
384
783
  * both states so a red/green volume bar keeps its meaning while live —
385
784
  * the one *design* exclusion rather than a path consequence.
386
785
  *
786
+ * **Scope note: that `binColors` exclusion is about the LIVE states only.**
787
+ * It does not carry over to {@link dimmed}, which *replaces* a per-bar fill
788
+ * on an unselected bar — see that token, which spells out the asymmetry and
789
+ * why emphasis preserves a per-bar colour while recession suppresses it.
790
+ *
387
791
  * The **decimated** dense-bar pass also draws the flat fill only, as it
388
792
  * already did for `highlight`.
389
793
  */
@@ -412,6 +816,50 @@ export interface BarStyle {
412
816
  * already one slice of a total has no defined meaning.
413
817
  */
414
818
  readonly bands?: readonly string[];
819
+ /**
820
+ * The **stack group ramp** — ordered fills for a *multi-group* stack's
821
+ * segments, `groups[0]` for the first (bottom / left) group. Cycles when the
822
+ * stack has more groups than the ramp has entries.
823
+ *
824
+ * Sibling of {@link bands}, and here for the same reason (a `theme.bar`
825
+ * top-level key would collide with a role of that name) — but a different
826
+ * axis: `bands` colours one bar *along its length* against a threshold
827
+ * ladder, this colours *across the groups* of one bin.
828
+ *
829
+ * **Multi-group only.** A ramp exists to tell groups apart, so with one group
830
+ * there is nothing to tell apart and the bar keeps its {@link fill} — which
831
+ * is what keeps every categorical and single-series chart (both of which run
832
+ * the stacked draw path with `G === 1`) exactly as it was.
833
+ *
834
+ * Resolution order per group: `<BarChart colors>` → a theme role named after
835
+ * the group (`bar.web`) → this ramp → {@link fill}. So a named role still
836
+ * wins, and the ramp is the fallback that makes an *unthemed* stack legible
837
+ * instead of painting every segment one colour.
838
+ */
839
+ readonly groups?: readonly string[];
840
+ /**
841
+ * The receded counterpart of {@link groups}, same order and cycling — what a
842
+ * segment fades to when a selection exists elsewhere.
843
+ *
844
+ * Per-group rather than the flat {@link dimmed}, because a stack dimmed to a
845
+ * single colour stops being a stack: the segment boundaries vanish and the
846
+ * unselected columns read as solid blocks. Each entry is its ramp colour
847
+ * desaturated and lightened, so the bin keeps its structure while clearly
848
+ * receding.
849
+ */
850
+ readonly groupsDimmed?: readonly string[];
851
+ /**
852
+ * The **hovered** counterpart of {@link groups}, same order and cycling.
853
+ *
854
+ * Per-group for the reason the flat {@link hover} cannot be: one hover
855
+ * colour repaints whichever segment the pointer is over in a hue belonging
856
+ * to a different group, so pointing at a stack momentarily *erases the
857
+ * ramp* — and under a `<MultiSelector>`, where hover is block-scoped, it
858
+ * erases the whole bin at once. Each entry is its ramp colour brightened
859
+ * (same hue, lighter), the same relationship {@link fill} has to
860
+ * {@link hover}.
861
+ */
862
+ readonly groupsHover?: readonly string[];
415
863
  /**
416
864
  * Stroke for a **selected** bar's outline, where the default is the bar's own
417
865
  * resolved fill. The one selection cue that still works when the fill cannot
@@ -427,13 +875,70 @@ export interface BarStyle {
427
875
  * live — which is the part that reads as emphasis.
428
876
  */
429
877
  readonly emphasisOpacity?: number;
878
+ /**
879
+ * The fill for a bar that is **not** in a non-empty selection set — the
880
+ * "everything else recedes" state a chart used as a filter control needs
881
+ * ([PND-MULTISEL]).
882
+ *
883
+ * **Opt-in by construction:** a theme that sets no `dimmed` dims nothing, so
884
+ * existing charts are untouched (RFC `selection.md` A2.3 — the library never
885
+ * auto-dims; the theme carries the selection-state styling and the library
886
+ * references it by state). Nothing dims while the set is empty either: with
887
+ * no selection there is nothing to recede *from*.
888
+ *
889
+ * It exists because "not in the selection" was otherwise re-invented per
890
+ * component, and drifted immediately — one consumer had three charts using
891
+ * `color-mix` at 22%, 28% and 30% for the same concept, in the same week, for
892
+ * no reason. One theme value fixes that permanently.
893
+ *
894
+ * **It OVERRIDES a per-bar fill, unlike the live states.** This is the one
895
+ * place `dimmed` and {@link hover} deliberately disagree, and the asymmetry
896
+ * is easy to read the wrong way round:
897
+ *
898
+ * - **{@link binColors} / {@link binFills}:** an unselected bar paints
899
+ * `dimmed`, discarding its own colour. (Hover and selection do the
900
+ * opposite — they keep the per-bar colour and pop the alpha, so a
901
+ * red/green volume bar stays red/green while live.)
902
+ * - **{@link bands} / thresholds:** an unselected banded bar draws **flat**
903
+ * in `dimmed`, discarding the ladder entirely rather than dimming each
904
+ * band.
905
+ * - **A multi-group stack** resolves per group through
906
+ * {@link StackStyle.dimmedFills} first, falling back to this flat value —
907
+ * a stack dimmed to one colour stops reading as a stack.
908
+ *
909
+ * The rule behind all three: a per-bar or per-band colour encodes *what the
910
+ * value is*, and a receded bar's whole job is to stop competing over that.
911
+ * Emphasis preserves meaning; recession suppresses it. So a chart that keeps
912
+ * `binColors` or `thresholds` for reasons unrelated to selection still gets
913
+ * a visible de-emphasis for free, and does **not** need to dim inside its own
914
+ * colour arrays. (Asked by a consumer who reasonably generalized `hover`'s
915
+ * `binColors` exclusion to this token; the exclusion is live-states-only.)
916
+ */
917
+ readonly dimmed?: string;
430
918
  }
431
919
  /**
432
- * The neutral default theme. `default` / `primary` match the M1 `LineChart`
433
- * colour (`#2563eb`) so adopting the theme channel doesn't shift existing
434
- * renders. `primary` / `secondary` / `context` are a built-in generic role
435
- * vocabulary; an unrecognised (e.g. domain-specific) identifier falls back to
436
- * `default`.
920
+ * The neutral default theme. The shared data hue is a cerulean (`#0284c7`)
921
+ * across `line` / `band` / `area` / `scatter` / `box` / candle-rising, chosen
922
+ * to clear the bar palette's *selection* blue (`#3F5BE0`) the original M1
923
+ * royal blue (`#2563eb`) sat ~ΔE 5 from it, so a line drawn over bars read as
924
+ * nearly the selection colour. `primary` / `secondary` / `context` are a
925
+ * built-in generic role vocabulary; an unrecognised (e.g. domain-specific)
926
+ * identifier falls back to `default`.
927
+ *
928
+ * **Spreading this inherits every slot you don't override** — including colours
929
+ * for layers you haven't added yet, so a `{ ...defaultTheme, bar: … }` theme
930
+ * paints this blue the first time someone drops in a `<LineChart>`. That is the
931
+ * intended workflow, not a trap: take the defaults, change the one or two things
932
+ * that are yours. A design system that must own *every* colour should assert on
933
+ * that in its own test rather than catch it in review — walk the resolved theme
934
+ * for values outside your palette ([PND-THEMEBASE]).
935
+ *
936
+ * **The `bar` slot is the exception to "one blue".** Bars carry an interaction
937
+ * state (rest / hover / selected / dimmed), and encoding four states as four
938
+ * shades of one hue is unreadable — so `bar.default` runs its own
939
+ * **interaction-state palette**: teal at rest, blue when selected, brighter
940
+ * teal on hover. See the comment on that slot, and `brush` for the matching
941
+ * drag band.
437
942
  */
438
943
  export declare const defaultTheme: ChartTheme;
439
944
  /**