@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/dist/BarChart.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useContext, useEffect, useMemo } from 'react';
|
|
2
2
|
import { Interval, ValueSeries } from 'pond-ts';
|
|
3
|
-
import { barsFromTimeSeries, barsFromBins, barsFromValueSeries, categoryStack, stacksFromBins, stacksFromColumns, stacksFromGroups, } from './data.js';
|
|
4
|
-
import { barAt, barExtent, barIndexAtTime, drawBars, drawStacks,
|
|
3
|
+
import { barsFromTimeSeries, barsFromBins, barsFromValueSeries, categoryStack, categoryStacks, stacksFromBins, stacksFromColumns, stacksFromGroups, } from './data.js';
|
|
4
|
+
import { barAt, barExtent, barIndexAtTime, drawBars, drawStacks, resolveBarBaseline, stackAt, stackBinExtent, stackValueExtent, } from './bars.js';
|
|
5
5
|
import { spansForLayer } from './span.js';
|
|
6
|
+
import { useBandLadder } from './use-band-ladder.js';
|
|
6
7
|
import { isDev } from './dev.js';
|
|
7
8
|
import { ContainerContext, LayersContext, } from './context.js';
|
|
8
9
|
import { sweep1D } from './sweep.js';
|
|
@@ -60,7 +61,7 @@ const EMPTY_MARKS = [];
|
|
|
60
61
|
* </Layers>
|
|
61
62
|
* ```
|
|
62
63
|
*/
|
|
63
|
-
export function BarChart({ series, bins, categories, column, columns, as: semantic, colors, binColors, thresholds, bandColors, orientation = 'vertical', ordinal = false, id, axis, gap, decimate = true, legend, index = 0, }) {
|
|
64
|
+
export function BarChart({ series, bins, categories, column, columns, as: semantic, colors, binColors, thresholds, bandColors, orientation = 'vertical', ordinal = false, id, axis, gap, maxBarWidth, decimate = true, legend, index = 0, }) {
|
|
64
65
|
const container = useContext(ContainerContext);
|
|
65
66
|
if (container === null) {
|
|
66
67
|
throw new Error('<BarChart> must be rendered inside a <ChartContainer>');
|
|
@@ -78,8 +79,11 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
|
|
|
78
79
|
throw new Error('<BarChart> needs exactly one of `series`, `bins`, or `categories`');
|
|
79
80
|
}
|
|
80
81
|
if (categories !== undefined) {
|
|
81
|
-
if (column !== undefined
|
|
82
|
-
throw new Error('<BarChart categories> takes no `column
|
|
82
|
+
if (column !== undefined) {
|
|
83
|
+
throw new Error('<BarChart categories> takes no `column` — a single-value datum carries its own `value`, and a stacked one names its groups with `columns`');
|
|
84
|
+
}
|
|
85
|
+
if (columns !== undefined && columns.length === 0) {
|
|
86
|
+
throw new Error('<BarChart categories> with `columns` needs at least one group name');
|
|
83
87
|
}
|
|
84
88
|
}
|
|
85
89
|
const isMap = series instanceof Map;
|
|
@@ -105,10 +109,21 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
|
|
|
105
109
|
// horizontal bar) so one oriented draw path covers it.
|
|
106
110
|
const shape = useMemo(() => {
|
|
107
111
|
if (categories !== undefined) {
|
|
108
|
-
// Categorical row-read: one unit-slot bar per category
|
|
109
|
-
//
|
|
112
|
+
// Categorical row-read: one unit-slot bar per category, drawn on the
|
|
113
|
+
// container's band scale. The reused stacked geometry — only the axis
|
|
110
114
|
// (band scale + labels) is new.
|
|
111
|
-
|
|
115
|
+
//
|
|
116
|
+
// With `columns` it is a real multi-group stack ([PND-CATSTACK]): same
|
|
117
|
+
// slots, same `marks`, `G > 1`. Because `marks` is indexed by BIN, one
|
|
118
|
+
// selection entry naming `(id, mark)` matches every segment of a bar —
|
|
119
|
+
// which is the property that made the hand-composed workaround's
|
|
120
|
+
// "recedes from the waist up" bug inexpressible here.
|
|
121
|
+
return {
|
|
122
|
+
kind: 'stacked',
|
|
123
|
+
ss: columns !== undefined
|
|
124
|
+
? categoryStacks(categories, columns)
|
|
125
|
+
: categoryStack(categories),
|
|
126
|
+
};
|
|
112
127
|
}
|
|
113
128
|
if (bins !== undefined) {
|
|
114
129
|
const cols = columns ?? (column !== undefined ? [column] : undefined);
|
|
@@ -250,67 +265,24 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
|
|
|
250
265
|
// The stacked path's bar-thickness floor comes from `bar.default` (not the `as`
|
|
251
266
|
// role — `as` is single-series only), matching how `gapPx` sources its default.
|
|
252
267
|
const stackMinWidth = bar.default.minWidth;
|
|
268
|
+
// Same sourcing as `gapPx`: the prop wins, else the theme token, else
|
|
269
|
+
// uncapped ([PND-BARWIDTH]). `bar.default` rather than the `as` role for the
|
|
270
|
+
// stacked ceiling, for the reason above.
|
|
271
|
+
const maxWidthPx = maxBarWidth ?? bar.default.maxWidth;
|
|
272
|
+
// The single-series draw takes a `BarStyle` straight from the theme, so the
|
|
273
|
+
// prop override is applied by shadowing the token — same relationship `gap`
|
|
274
|
+
// has, and with the prop absent the role's own `maxWidth` (if any) already
|
|
275
|
+
// rides along untouched. Only the INK path gets this: `barSlotRect` (the hit
|
|
276
|
+
// region) stays the whole slot, so a narrow capped bar keeps a full-width
|
|
277
|
+
// target — the deliberate ink/hit split `gapPx` already relies on.
|
|
278
|
+
const singleDrawStyle = useMemo(() => maxBarWidth !== undefined
|
|
279
|
+
? { ...singleStyle, maxWidth: maxBarWidth }
|
|
280
|
+
: singleStyle, [singleStyle, maxBarWidth]);
|
|
253
281
|
// ── Threshold ladder ([PND-BANDBAR2]) ────────────────────────────────────
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
// because a quietly-unbanded bar was the workaround's failure mode.
|
|
259
|
-
// Value-compare the two array props rather than relying on their identity.
|
|
260
|
-
// `thresholds={[1, 2]}` inline is the documented usage and the shape every
|
|
261
|
-
// story and doc example uses — and a fresh array each render would rebuild
|
|
262
|
-
// the ladder, hence the layer `entry` below, hence a `registerLayer` call
|
|
263
|
-
// **every render**. That is a repaint treadmill, not just a noisy warning.
|
|
264
|
-
// The same value-compare-on-registration reasoning `<YAxis ticks>` already
|
|
265
|
-
// applies.
|
|
266
|
-
const thresholdKey = thresholds === undefined ? '' : thresholds.join(',');
|
|
267
|
-
const bandColorKey = bandColors === undefined ? '' : bandColors.join(',');
|
|
268
|
-
const bandLadder = useMemo(() => {
|
|
269
|
-
const steps = normalizeThresholds(thresholds);
|
|
270
|
-
if (steps === null) {
|
|
271
|
-
if (isDev && thresholds !== undefined && thresholds.length > 0) {
|
|
272
|
-
console.warn('<BarChart thresholds>: no usable breakpoints, so no banding was ' +
|
|
273
|
-
'applied — each must be finite and greater than zero. Bars draw ' +
|
|
274
|
-
'in the flat fill.');
|
|
275
|
-
}
|
|
276
|
-
return undefined;
|
|
277
|
-
}
|
|
278
|
-
// Some, but not all, entries dropped. Silently banding on a subset of what
|
|
279
|
-
// the caller wrote is exactly the class of quiet wrongness this feature is
|
|
280
|
-
// meant to remove, so say so.
|
|
281
|
-
if (isDev && thresholds !== undefined && steps.length < thresholds.length) {
|
|
282
|
-
console.warn(`<BarChart thresholds>: dropped ${thresholds.length - steps.length} ` +
|
|
283
|
-
'breakpoint(s) that were not finite and greater than zero. The ' +
|
|
284
|
-
'ladder is walked on the magnitude and mirrored onto whichever side ' +
|
|
285
|
-
'of zero a bar is on, so a negative breakpoint has no meaning; ' +
|
|
286
|
-
`banding on [${steps.join(', ')}].`);
|
|
287
|
-
}
|
|
288
|
-
const want = steps.length + 1;
|
|
289
|
-
const supplied = bandColors ?? singleStyle.bands;
|
|
290
|
-
if (supplied === undefined || supplied.length === 0) {
|
|
291
|
-
if (isDev) {
|
|
292
|
-
console.warn(`<BarChart thresholds>: ${steps.length} breakpoint(s) need ${want} ` +
|
|
293
|
-
'band colours, but neither `bandColors` nor the theme role’s ' +
|
|
294
|
-
'`BarStyle.bands` supplies any. Bars draw in the flat fill.');
|
|
295
|
-
}
|
|
296
|
-
return undefined;
|
|
297
|
-
}
|
|
298
|
-
if (supplied.length < want && isDev) {
|
|
299
|
-
console.warn(`<BarChart thresholds>: ${steps.length} breakpoint(s) need ${want} ` +
|
|
300
|
-
`band colours but only ${supplied.length} were supplied; bands ` +
|
|
301
|
-
'above the last colour fall back to the flat fill.');
|
|
302
|
-
}
|
|
303
|
-
// Pad a short ladder with the flat fill so the draw path can index freely.
|
|
304
|
-
const resolved = supplied.length >= want
|
|
305
|
-
? supplied.slice(0, want)
|
|
306
|
-
: [
|
|
307
|
-
...supplied,
|
|
308
|
-
...Array.from({ length: want - supplied.length }, () => singleStyle.fill),
|
|
309
|
-
];
|
|
310
|
-
return { thresholds: steps, colors: resolved };
|
|
311
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- `thresholdKey` /
|
|
312
|
-
// `bandColorKey` are the value-compared stand-ins for the array props.
|
|
313
|
-
}, [thresholdKey, bandColorKey, singleStyle]);
|
|
282
|
+
// Breakpoints normalized + paired with `bandColors` → the role's
|
|
283
|
+
// `BarStyle.bands` in the shared {@link useBandLadder} (one contract with
|
|
284
|
+
// `<AreaChart thresholds>`, including every dev warning).
|
|
285
|
+
const bandLadder = useBandLadder('BarChart', thresholds, bandColors, singleStyle.bands, singleStyle.fill);
|
|
314
286
|
// Conflicts between the ladder and the shapes it can't apply to. In an effect
|
|
315
287
|
// so a re-render doesn't re-log; each fires once per genuinely new pairing.
|
|
316
288
|
const multiGroup = shape.kind === 'stacked' && shape.ss.groups.length > 1;
|
|
@@ -351,11 +323,38 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
|
|
|
351
323
|
(ramp !== undefined ? at(ramp, i) : base.fill));
|
|
352
324
|
// A ramp entry the call site overrode is no longer the ramp's colour, so
|
|
353
325
|
// its receded counterpart would be wrong — the whole ramp only means
|
|
354
|
-
// anything when it is the ramp that painted it.
|
|
326
|
+
// anything when it is the ramp that painted it. This gates the *derived*
|
|
327
|
+
// companions (`dimmedFills` / `hoverFills`), which need a counterpart per
|
|
328
|
+
// group and cannot invent one for an arbitrary call-site colour.
|
|
355
329
|
const ramped = ramp !== undefined && colors === undefined;
|
|
330
|
+
// `groupColored` derives nothing — it only says "a selected segment keeps
|
|
331
|
+
// its own fill rather than taking the flat `highlight`", because the colour
|
|
332
|
+
// is meaning-carrying.
|
|
333
|
+
//
|
|
334
|
+
// So the condition is exactly **"do the fills actually differ"**, read off
|
|
335
|
+
// the resolved `fills` rather than inferred from anything upstream of them.
|
|
336
|
+
// Both cheaper inferences are wrong, and each was shipped in turn:
|
|
337
|
+
//
|
|
338
|
+
// - Gating on `ramped` (the ramp painted it) meant a stack with `colors`
|
|
339
|
+
// collapsed BOTH segments of a selected bar to one `highlight` blue —
|
|
340
|
+
// losing the segment distinction exactly where the reader is looking. A
|
|
341
|
+
// call site naming its groups' colours is *more* deliberate than a
|
|
342
|
+
// fallback ramp, not less. Found building [PND-CATSTACK].
|
|
343
|
+
// - Gating on `groups.length > 1` (my fix for that) is wrong in the other
|
|
344
|
+
// direction: a multi-group stack under a theme with **no** group ramp, no
|
|
345
|
+
// `colors` and no per-group roles resolves every fill to `base.fill`, so
|
|
346
|
+
// claiming the colour carries meaning suppresses the highlight and leaves
|
|
347
|
+
// *nothing* — selection becomes invisible. `estelaTheme` is exactly that
|
|
348
|
+
// theme, and it ships. Found by Layer-2 review, which is the only way it
|
|
349
|
+
// could have been: every story and test renders `defaultTheme`, whose ramp
|
|
350
|
+
// makes the two gates indistinguishable.
|
|
351
|
+
//
|
|
352
|
+
// Reading `fills` also handles the degenerate `colors` map that assigns one
|
|
353
|
+
// colour to every group: nothing is distinguished, so the highlight applies.
|
|
354
|
+
const groupColoured = new Set(fills).size > 1;
|
|
356
355
|
return {
|
|
357
356
|
fills,
|
|
358
|
-
...(
|
|
357
|
+
...(groupColoured ? { groupColored: true } : {}),
|
|
359
358
|
...(ramped && rampDim !== undefined
|
|
360
359
|
? {
|
|
361
360
|
dimmedFills: (groups ?? []).map((_g, i) => at(rampDim, i)),
|
|
@@ -380,9 +379,10 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
|
|
|
380
379
|
? { emphasisOpacity: base.emphasisOpacity }
|
|
381
380
|
: {}),
|
|
382
381
|
...(base.dimmed !== undefined ? { dimmed: base.dimmed } : {}),
|
|
382
|
+
...(maxWidthPx !== undefined ? { maxWidth: maxWidthPx } : {}),
|
|
383
383
|
...(binColors !== undefined ? { binFills: binColors } : {}),
|
|
384
384
|
};
|
|
385
|
-
}, [bar, groups, colors, binColors]);
|
|
385
|
+
}, [bar, groups, colors, binColors, maxWidthPx]);
|
|
386
386
|
// The current selection / hover, narrowed to the identity the highlight match
|
|
387
387
|
// needs. For a stack that's (id, key, label = group); the single path uses just
|
|
388
388
|
// (id, key). Read here so a change re-registers the layer → the canvas repaints.
|
|
@@ -525,7 +525,7 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
|
|
|
525
525
|
},
|
|
526
526
|
}),
|
|
527
527
|
}),
|
|
528
|
-
draw: (ctx, xScale, yScale) => drawBars(ctx, bs, xScale, yScale,
|
|
528
|
+
draw: (ctx, xScale, yScale) => drawBars(ctx, bs, xScale, yScale, singleDrawStyle, resolveBarBaseline(yScale), gapPx, id, selection, hover, decimate, binColors, bandLadder, layerSpans),
|
|
529
529
|
},
|
|
530
530
|
axisId: axis,
|
|
531
531
|
index,
|
|
@@ -563,7 +563,20 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
|
|
|
563
563
|
? {}
|
|
564
564
|
: {
|
|
565
565
|
hitTest: (px, py, xScale, yScale) => {
|
|
566
|
-
|
|
566
|
+
// The cap reaches the hit rect ONLY for a real stack, where the
|
|
567
|
+
// rect is what resolves *which segment* was hit and so must be
|
|
568
|
+
// the drawn one. A single-series chart has one segment per slot
|
|
569
|
+
// and nothing to disambiguate, so narrowing its target buys
|
|
570
|
+
// nothing and costs clickability.
|
|
571
|
+
//
|
|
572
|
+
// This path serves single-series **horizontal** charts as well as
|
|
573
|
+
// stacks (see the branch comment above), which is how the prop's
|
|
574
|
+
// documented guarantee — "a single-series bar hit-tests its whole
|
|
575
|
+
// slot" — was true only of vertical ones. Found by Layer-2 review;
|
|
576
|
+
// the fix is to make the guarantee orientation-independent rather
|
|
577
|
+
// than to narrow the claim, since the reason for the split is
|
|
578
|
+
// segment disambiguation and that is a property of the *stack*.
|
|
579
|
+
const hit = stackAt(ss, px, py, orientation, xScale, yScale, gapPx, stackMinWidth, ss.groups.length > 1 ? maxWidthPx : undefined);
|
|
567
580
|
if (hit === null)
|
|
568
581
|
return null;
|
|
569
582
|
const [bi, g, begin, name, value] = hit;
|
|
@@ -656,6 +669,7 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
|
|
|
656
669
|
categoryLabels,
|
|
657
670
|
orientation,
|
|
658
671
|
singleStyle,
|
|
672
|
+
singleDrawStyle,
|
|
659
673
|
stackStyle,
|
|
660
674
|
binColors,
|
|
661
675
|
bandLadder,
|
|
@@ -664,6 +678,7 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
|
|
|
664
678
|
gapPx,
|
|
665
679
|
decimate,
|
|
666
680
|
stackMinWidth,
|
|
681
|
+
maxWidthPx,
|
|
667
682
|
selection,
|
|
668
683
|
hover,
|
|
669
684
|
layerSpans,
|
package/dist/BarList.d.ts
CHANGED
|
@@ -69,6 +69,28 @@ export interface BarListCommon<R extends ListRow = ListRow> {
|
|
|
69
69
|
selected?: string | readonly string[] | null;
|
|
70
70
|
/** Row click (rows show the pointer affordance only when provided). */
|
|
71
71
|
onRowClick?: (row: R) => void;
|
|
72
|
+
/**
|
|
73
|
+
* Per-**row** bar colour, `barColors[i]` aligned to the rows in order — the
|
|
74
|
+
* list's `<BarChart binColors>` ([#650]). An `undefined` or short entry falls
|
|
75
|
+
* back to the column's `as` / theme fill, so a partial list is legal.
|
|
76
|
+
*
|
|
77
|
+
* For the case `binColors` was built for and a list could not do: a zone
|
|
78
|
+
* table where each row's bar carries its own step of a ramp (Z1 deep → Z7
|
|
79
|
+
* pale). Without it a `BarListColumn`'s single `as` paints every row the same
|
|
80
|
+
* colour, and the ramp has to move onto the label — putting it on the wrong
|
|
81
|
+
* element, since the bar is the natural carrier of a magnitude.
|
|
82
|
+
*
|
|
83
|
+
* **The fill becomes load-bearing, so its state treatment stands down.** A
|
|
84
|
+
* per-row-coloured bar keeps its own colour while selected or hovered, and
|
|
85
|
+
* the live state is carried by the band and rail that already say it. This is
|
|
86
|
+
* the same rule the multi-metric case follows for the same reason — see the
|
|
87
|
+
* comment beside the fill resolution — and the same one `binColors` follows
|
|
88
|
+
* on the canvas: recolouring a bar that means something would trade a
|
|
89
|
+
* distinction the reader needs for one they already have.
|
|
90
|
+
*
|
|
91
|
+
* [#650]: https://github.com/pond-ts/pond/issues/650
|
|
92
|
+
*/
|
|
93
|
+
barColors?: readonly (string | undefined)[];
|
|
72
94
|
/**
|
|
73
95
|
* **Plural select** — the list's answer to `<MultiSelector>`, and how a user
|
|
74
96
|
* produces a multi-row {@link selected}.
|
package/dist/BarList.js
CHANGED
|
@@ -36,7 +36,7 @@ export function BarList(props) {
|
|
|
36
36
|
// One normalized view of the union — `isSeriesSource` is the runtime
|
|
37
37
|
// narrowing; the doors are mutually exclusive by construction now.
|
|
38
38
|
const source = props;
|
|
39
|
-
const { rows, series, label, columns, domain, sortBy, sortDirection = 'desc', sort, before, after, renderExpanded, defaultExpanded, onExpandToggle, selected, onRowClick, onRowSelect, hovered, onHover, markers, barHeight = 8, divided, baseline, theme = defaultTheme, } = source;
|
|
39
|
+
const { rows, series, label, columns, domain, sortBy, sortDirection = 'desc', sort, before, after, renderExpanded, defaultExpanded, onExpandToggle, selected, onRowClick, onRowSelect, barColors, hovered, onHover, markers, barHeight = 8, divided, baseline, theme = defaultTheme, } = source;
|
|
40
40
|
// A runtime guard for JS consumers and `any`-typed call sites — the
|
|
41
41
|
// props union makes both branches unreachable from typed TS, but a
|
|
42
42
|
// silently-ignored source prop is a worse failure than a throw.
|
|
@@ -49,6 +49,21 @@ export function BarList(props) {
|
|
|
49
49
|
(series instanceof ValueSeries
|
|
50
50
|
? listRowsFromValueSeries(series, label !== undefined ? { label } : {})
|
|
51
51
|
: listRowsFromTimeSeries(series, label !== undefined ? { label } : {})), [rows, series, label]);
|
|
52
|
+
// Keyed by row, not indexed by render position. `barColors` aligns to the
|
|
53
|
+
// rows the caller passed — but the table renders `sorted`, so an index would
|
|
54
|
+
// silently repaint the ramp onto the wrong rows the moment `sortBy` is set.
|
|
55
|
+
// The key survives any reordering.
|
|
56
|
+
const barColorOf = useMemo(() => {
|
|
57
|
+
if (barColors === undefined)
|
|
58
|
+
return null;
|
|
59
|
+
const m = new Map();
|
|
60
|
+
allRows.forEach((r, i) => {
|
|
61
|
+
const c = barColors[i];
|
|
62
|
+
if (c !== undefined)
|
|
63
|
+
m.set(r.key, c);
|
|
64
|
+
});
|
|
65
|
+
return m;
|
|
66
|
+
}, [barColors, allRows]);
|
|
52
67
|
const sorted = useMemo(() => sortListRows(allRows, sortBy, sortDirection, sort), [allRows, sortBy, sortDirection, sort]);
|
|
53
68
|
const scale = useMemo(() => resolveListDomain(allRows, columns.map((c) => c.column), domain, markers?.map((m) => m.value)), [allRows, columns, domain, markers]);
|
|
54
69
|
const resolvedMarkers = useMemo(() => markers?.map((m) => ({
|
|
@@ -68,17 +83,35 @@ export function BarList(props) {
|
|
|
68
83
|
// Which is also why nothing below may be the *only* signal: strip
|
|
69
84
|
// this block and a selected row still reads as selected.
|
|
70
85
|
const soleMetric = columns.length === 1;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
// A per-row colour makes the fill mean something, which puts it in
|
|
87
|
+
// exactly the position a multi-metric row's fill is already in: the
|
|
88
|
+
// state treatment stands down rather than trading a distinction the
|
|
89
|
+
// reader needs for one the band and rail already give them. Same
|
|
90
|
+
// rule `binColors` follows on the canvas.
|
|
91
|
+
const own = barColorOf?.get(row.key);
|
|
92
|
+
const fill = own !== undefined
|
|
93
|
+
? own
|
|
94
|
+
: state.selected && soleMetric
|
|
95
|
+
? style.highlight
|
|
96
|
+
: state.dimmed
|
|
97
|
+
? (style.dimmed ?? style.fill)
|
|
98
|
+
: style.fill;
|
|
76
99
|
// A `dimmed` token carries its own alpha (`rgba(…,0.32)`), so
|
|
77
100
|
// multiplying `opacity` on top of it would dim twice. Fall back to
|
|
78
101
|
// the raw 0.32 only when the theme names no dimmed colour.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
102
|
+
// Opacity is where a coloured bar shows its state, since its hue
|
|
103
|
+
// is spoken for. A `dimmed` THEME token carries its own alpha so
|
|
104
|
+
// multiplying on top would dim twice — but that token is not in
|
|
105
|
+
// play for a per-row colour, so the raw 0.32 applies there too.
|
|
106
|
+
const fillOpacity = own !== undefined
|
|
107
|
+
? state.dimmed
|
|
108
|
+
? style.opacity * 0.32
|
|
109
|
+
: state.selected
|
|
110
|
+
? 1
|
|
111
|
+
: style.opacity
|
|
112
|
+
: state.dimmed && style.dimmed === undefined
|
|
113
|
+
? style.opacity * 0.32
|
|
114
|
+
: style.opacity;
|
|
82
115
|
return (_jsxs("div", { "data-list-track": col.column, style: {
|
|
83
116
|
position: 'relative',
|
|
84
117
|
height: barHeight,
|
package/dist/ChartContainer.d.ts
CHANGED
|
@@ -14,6 +14,108 @@ export interface ChartContainerProps {
|
|
|
14
14
|
* the data — so a tuple stays a time domain on a time chart.
|
|
15
15
|
*/
|
|
16
16
|
range?: readonly [number, number] | TimeRange;
|
|
17
|
+
/**
|
|
18
|
+
* **Make the x axis ordinal at the container level** — one equal-width slot
|
|
19
|
+
* per name, in this order ([PND-IGNITECAT]).
|
|
20
|
+
*
|
|
21
|
+
* Note this is a list of **names** (`string[]`), unlike `<BarChart
|
|
22
|
+
* categories>`, which takes `{ label, value }` data. The container names the
|
|
23
|
+
* slots; the bar layer fills them.
|
|
24
|
+
*
|
|
25
|
+
* Until this prop, the band scale was reachable only *through a layer*:
|
|
26
|
+
* `<BarChart categories>` (and a **horizontal** heat map) reported
|
|
27
|
+
* `xKind: 'category'`,
|
|
28
|
+
* every other layer reported `'time'` or `'value'`, and the container throws
|
|
29
|
+
* on a mix — so **a line, a point or an envelope over categorical bars was
|
|
30
|
+
* not expressible at all**. The workaround was to key every layer to a
|
|
31
|
+
* synthetic integer index and hand-supply the tick labels, which forfeits two
|
|
32
|
+
* features the ordinal axis already implements: `<XAxis>` label thinning
|
|
33
|
+
* (gated on a category axis with no custom ticks) and the
|
|
34
|
+
* {@link maxBandWidth} / {@link bandAlign} slot packing.
|
|
35
|
+
*
|
|
36
|
+
* Declaring the categories here inverts that. The container owns the ordinal
|
|
37
|
+
* domain, so **any value-keyed layer can live on it** and both of those
|
|
38
|
+
* features keep working.
|
|
39
|
+
*
|
|
40
|
+
* ## Keying a layer to the slots
|
|
41
|
+
*
|
|
42
|
+
* The band scale's domain is **numeric** — slot `i` occupies `[i, i+1]`, so
|
|
43
|
+
* its **centre is `i + 0.5`**. Key a `ValueSeries` there and the mark lands
|
|
44
|
+
* on the slot centre, which is also where `<XAxis>` puts the tick:
|
|
45
|
+
*
|
|
46
|
+
* ```tsx
|
|
47
|
+
* const line = ValueSeries.from(
|
|
48
|
+
* tickers.map((t, i) => ({ x: i + 0.5, target: t.target })),
|
|
49
|
+
* { key: 'x' },
|
|
50
|
+
* );
|
|
51
|
+
*
|
|
52
|
+
* <ChartContainer categories={tickers.map((t) => t.label)} width="auto">
|
|
53
|
+
* <ChartRow height={220}>
|
|
54
|
+
* <YAxis id="v" />
|
|
55
|
+
* <Layers>
|
|
56
|
+
* <BarChart categories={bars} />
|
|
57
|
+
* <LineChart series={line} column="target" axis="v" />
|
|
58
|
+
* </Layers>
|
|
59
|
+
* </ChartRow>
|
|
60
|
+
* </ChartContainer>;
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* ## What still errors
|
|
64
|
+
*
|
|
65
|
+
* - **A time-keyed layer.** A `TimeSeries` has no slot to sit in; mixing one
|
|
66
|
+
* into an ordinal container is a hard error, as a mixed x-kind always was.
|
|
67
|
+
* - **A category layer that disagrees.** `<BarChart categories>` in an
|
|
68
|
+
* ordinal container must name the same list in the same order — this prop
|
|
69
|
+
* is authoritative, and a silent mismatch would draw bars under the wrong
|
|
70
|
+
* labels.
|
|
71
|
+
* ## What declaring it costs
|
|
72
|
+
*
|
|
73
|
+
* Setting this makes the x axis ordinal, and two container capabilities are
|
|
74
|
+
* defined only on a continuous x. Both were already true of an *inferred*
|
|
75
|
+
* category axis; they are stated here because this prop lets you opt a
|
|
76
|
+
* previously-continuous container into them:
|
|
77
|
+
*
|
|
78
|
+
* - **x pan and zoom stop.** `panZoom` keeps working on **y** (`panY` /
|
|
79
|
+
* `zoomY`), but the x half is gated off — sliding between named slots is
|
|
80
|
+
* not a gesture the axis has a meaning for.
|
|
81
|
+
* - **{@link range} stops applying to x.** The domain is `[0, n]`, derived
|
|
82
|
+
* from the slot count, so an x range is a no-op rather than an error.
|
|
83
|
+
* Show a subset by passing fewer categories.
|
|
84
|
+
* - **{@link xScale} stops applying.** `'log'` / `'symlog'` describe how a
|
|
85
|
+
* *continuous* x spaces its values; ordinal slots are evenly spaced by
|
|
86
|
+
* definition, so the kind is ignored (as it already is on a time axis).
|
|
87
|
+
*
|
|
88
|
+
* ## The hazard this cannot catch
|
|
89
|
+
*
|
|
90
|
+
* **A value-keyed layer is taken at its word.** Anything reporting `'value'`
|
|
91
|
+
* is read as slot coordinates, so a layer whose x means something *else*
|
|
92
|
+
* will draw — in the wrong place, silently. The sharpest instance is a
|
|
93
|
+
* **horizontal categorical `<BarChart>`**: its x is bar *length*, not a
|
|
94
|
+
* coordinate, so on an ordinal x it plots magnitudes as slot positions.
|
|
95
|
+
* Don't mix one into an ordinal container.
|
|
96
|
+
*
|
|
97
|
+
* This is documented rather than enforced, and the reason is worth keeping:
|
|
98
|
+
* a guard was written for it, testing `binCategories`. That is the generic
|
|
99
|
+
* "my **y** is ordinal" channel, and a *vertical* heat map sets it too — so
|
|
100
|
+
* the guard rejected a `ValueSeries` grid with named columns on x, which is
|
|
101
|
+
* a wanted layout (ordinal rows plus ordinal columns is just a 2-D grid),
|
|
102
|
+
* with an error naming a `<BarChart>` that wasn't in the tree. Nothing on a
|
|
103
|
+
* layer source distinguishes "my x is a coordinate" from "my x is a
|
|
104
|
+
* magnitude", so there is no contradiction to detect — and a flag invented
|
|
105
|
+
* to carry it would buy a false sense of coverage while every other misuse
|
|
106
|
+
* stayed silent.
|
|
107
|
+
*
|
|
108
|
+
* ## One more edge
|
|
109
|
+
*
|
|
110
|
+
* **`categories={[]}` is an ordinal axis with no slots yet**, not a fallback
|
|
111
|
+
* to time. That is the useful reading for a loading state: the kind stays
|
|
112
|
+
* put when the data arrives, instead of flipping and rebuilding every scale
|
|
113
|
+
* mid-session.
|
|
114
|
+
*
|
|
115
|
+
* Omit for the inferred behaviour: a container with only category layers
|
|
116
|
+
* still resolves its slots from them, exactly as before.
|
|
117
|
+
*/
|
|
118
|
+
categories?: readonly string[];
|
|
17
119
|
/**
|
|
18
120
|
* **Cap the slot pitch** on a **category** x axis, in CSS pixels
|
|
19
121
|
* ([PND-BANDPACK]). A band scale otherwise spreads its categories across the
|
|
@@ -99,6 +201,38 @@ export interface ChartContainerProps {
|
|
|
99
201
|
* carries its own metric).
|
|
100
202
|
*/
|
|
101
203
|
spacing?: 'proportional' | 'uniform';
|
|
204
|
+
/**
|
|
205
|
+
* How the **value** x axis maps data to pixels. **Omitted ⇒ `'linear'`.**
|
|
206
|
+
*
|
|
207
|
+
* `'log'` for a quantity spanning orders of magnitude — a power–duration
|
|
208
|
+
* curve is watts against 1s · 5s · 1m · 20m · 3h, which is unreadable on a
|
|
209
|
+
* linear x. `'symlog'` is the same but linear through zero, for data that
|
|
210
|
+
* crosses it.
|
|
211
|
+
*
|
|
212
|
+
* **Ignored on a time or category axis**, which have their own spacing rules.
|
|
213
|
+
*
|
|
214
|
+
* **Why this lives on the container and not on `<XAxis scale>`,** which is
|
|
215
|
+
* where `<YAxis scale>`'s mirror would put it: **the rows are stacked
|
|
216
|
+
* vertically, so a given pixel column has to mean the same x in every one of
|
|
217
|
+
* them** — otherwise the stack doesn't line up and a cursor at one pixel
|
|
218
|
+
* reads a different value per row. The x scale and its domain are therefore
|
|
219
|
+
* *shared by requirement*, not by convention, and a shared thing is declared
|
|
220
|
+
* once by the thing that contains them. `<YAxis>` is the opposite for the
|
|
221
|
+
* same reason: each row carries its own quantity, so its scale **must** be
|
|
222
|
+
* per-row, which is why `min` / `max` / `pad` / `scale` belong to the axis.
|
|
223
|
+
*
|
|
224
|
+
* That gives the test for what belongs here rather than on `<XAxis>`: **does
|
|
225
|
+
* it define the mapping or the domain?** `origin`, `spacing`, `calendar` and
|
|
226
|
+
* the viewport props all do, and sit here for the same reason. Every
|
|
227
|
+
* `<XAxis>` prop (`format`, `label`, `side`, `ticks`, `align`, …) does not —
|
|
228
|
+
* they style a scale the axis only draws, and putting a scale-defining prop
|
|
229
|
+
* among them would mean a registration round-trip to the component that
|
|
230
|
+
* already owns it.
|
|
231
|
+
*
|
|
232
|
+
* (Had `<XAxis>` been mandatory in the declaration, the props would more
|
|
233
|
+
* naturally have lived there and x would mirror y — see [PND-XLOG].)
|
|
234
|
+
*/
|
|
235
|
+
xScale?: 'linear' | 'log' | 'symlog';
|
|
102
236
|
/**
|
|
103
237
|
* Draw the reference gridlines behind the data. On a calendar (time) axis
|
|
104
238
|
* the verticals are the **full grain populations** — every day / month /
|
|
@@ -124,8 +258,42 @@ export interface ChartContainerProps {
|
|
|
124
258
|
* separators-on-a-clean-plot look.
|
|
125
259
|
*/
|
|
126
260
|
sessionDividers?: 'labeled' | 'all' | 'none';
|
|
127
|
-
/**
|
|
128
|
-
|
|
261
|
+
/**
|
|
262
|
+
* Total width in CSS pixels (plot + axis gutters), or **`'auto'` to fill the
|
|
263
|
+
* available width** — which is also what an omitted `width` means.
|
|
264
|
+
*
|
|
265
|
+
* The canvas renderer needs real pixels to lay out ticks and slots before it
|
|
266
|
+
* draws, so `'auto'` does not hand the canvas a percentage: the container
|
|
267
|
+
* renders a plain full-width box, measures it with a `ResizeObserver`, and
|
|
268
|
+
* mounts the chart at that pixel width, re-rendering as the box resizes.
|
|
269
|
+
* **Nothing paints until a real width exists** — a zero-width chart is
|
|
270
|
+
* degenerate, not empty — so an auto container renders an empty box for the
|
|
271
|
+
* first layout pass.
|
|
272
|
+
*
|
|
273
|
+
* This is the [responsive-width recipe](https://pond-ts.github.io/pond/docs/recipes/responsive-width)
|
|
274
|
+
* moved inside the library, and it closes that recipe's sharpest edge by
|
|
275
|
+
* construction: the measured box is one the library owns, so it can never be
|
|
276
|
+
* the caller's padded or bordered box (whose border-box width overflows the
|
|
277
|
+
* chart by exactly the padding). Style your own wrapper *around* the
|
|
278
|
+
* container as freely as you like.
|
|
279
|
+
*
|
|
280
|
+
* **The parent needs a definite width.** `'auto'` measures a `width: 100%`
|
|
281
|
+
* box, so a parent whose own width comes from its *content* — a float, an
|
|
282
|
+
* `inline-block`, a grid `auto` track, a flex child without `min-width: 0` —
|
|
283
|
+
* measures 0, and the chart is the content that would have given it a width.
|
|
284
|
+
* That is a standing deadlock, not a slow start: the chart stays blank with
|
|
285
|
+
* no error. Give the parent a width, a `flex` basis, or `min-width: 0`, or
|
|
286
|
+
* pass a number.
|
|
287
|
+
*
|
|
288
|
+
* A container hidden by an ancestor's `display: none` is fine — it keeps the
|
|
289
|
+
* last width it measured and stays mounted, so a tab switch does not discard
|
|
290
|
+
* pan/zoom position, selection or hover.
|
|
291
|
+
*
|
|
292
|
+
* Pass a number whenever the width is already known — a fixed-size panel, a
|
|
293
|
+
* print layout, a test. It skips the measure pass and paints on the first
|
|
294
|
+
* render.
|
|
295
|
+
*/
|
|
296
|
+
width?: number | 'auto';
|
|
129
297
|
/** Vertical space between rows in CSS pixels (not under the axis). Default 0. */
|
|
130
298
|
rowGap?: number;
|
|
131
299
|
/**
|
|
@@ -449,6 +617,10 @@ export interface ChartContainerProps {
|
|
|
449
617
|
* the shared time `xScale`. It renders its rows (separated by `rowGap`) then one
|
|
450
618
|
* {@link TimeAxis} at the bottom, aligned under the plots. Y axes are per-row
|
|
451
619
|
* (`<YAxis>`).
|
|
620
|
+
*
|
|
621
|
+
* A `width` in pixels renders straight through; `'auto'` (or an omitted
|
|
622
|
+
* `width`) measures the available width first — see {@link
|
|
623
|
+
* ChartContainerProps.width} and {@link AutoWidthContainer}.
|
|
452
624
|
*/
|
|
453
|
-
export declare function ChartContainer(
|
|
625
|
+
export declare function ChartContainer(props: ChartContainerProps): import("react/jsx-runtime").JSX.Element;
|
|
454
626
|
//# sourceMappingURL=ChartContainer.d.ts.map
|