@eifi1/ui-kit 0.15.0 → 0.15.1

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/dist/chart.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  export { ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartSeriesConfig, ChartTooltip, ChartTooltipContent, useChart } from './components/chart.js';
2
2
  export { CHART_COLORS, HEATMAP_HEX, HeatStops, PALETTE_HEX, lerpHex, paletteFor, textOn } from './theme/chart-palette.js';
3
3
  export { Treemap, TreemapCell, TreemapCellProps, TreemapNode, TreemapProps, fitLabel } from './components/treemap.js';
4
- export { AXIS_TICK_WIDTH, AXIS_TITLE_STRIP, SeriesChart, SeriesChartAxis, SeriesChartHit, SeriesChartMarker, SeriesChartPoint, SeriesChartProps, SeriesChartReference, SeriesChartRow, SeriesChartSeries, SeriesChartSpan, SeriesChartTickValues, SeriesChartTone, SeriesChartTooltip, SeriesChartType, SeriesChartX, SeriesChartXTick, SeriesChartXValue, SeriesSource, StaticSeriesChart, anchoredBand, axisBandWidth, mergeSeries, oneAxis, padBand, paddedDomain, seriesKey, seriesLegendEntries, soleSeriesColor, visibleSeries } from './components/series-chart.js';
4
+ export { AXIS_TICK_WIDTH, AXIS_TITLE_STRIP, SeriesChart, SeriesChartAxis, SeriesChartHit, SeriesChartMarker, SeriesChartPoint, SeriesChartProps, SeriesChartReference, SeriesChartRow, SeriesChartSeries, SeriesChartSpan, SeriesChartTickValues, SeriesChartTone, SeriesChartTooltip, SeriesChartType, SeriesChartX, SeriesChartXTick, SeriesChartXValue, SeriesLegendAxes, SeriesSource, StaticSeriesChart, anchoredBand, axisBandWidth, mergeSeries, oneAxis, padBand, paddedDomain, seriesKey, seriesLegendEntries, soleSeriesColor, visibleSeries } from './components/series-chart.js';
5
5
  export { DEFAULT_Y_AXIS, NO_ZOOM, SharedXZoom, ZOOM_MIN_DRAG, ZOOM_SQUARE_ENOUGH, ZoomAxes, ZoomAxesSetting, ZoomBinding, ZoomDrag, ZoomFitSeries, ZoomFitSource, ZoomRow, ZoomSelection, ZoomState, ZoomTarget, axisExtent, defaultZoomAxes, fitXToY, fitYToX, selectionFromDrag, withChartZoom, zoomAfter, zoomAxesFor, zoomDomains } from './components/chart-zoom.js';
6
6
  export { LegendColumn, LegendEntry, LegendGroup, STEP_DASH, STROKE_PATTERNS, StaticLegend, StaticLegendProps, ToggleLegend, ToggleLegendProps, strokeDash, toggleHidden } from './components/toggle-legend.js';
7
7
  export { FACING_SIDES, FacingSide, facingAxes, facingBand, facingHeadingPad } from './components/facing-pair.js';
8
8
  export { DEFAULT_SERIES_CHART_LABELS, SeriesChartLabels } from './components/series-chart-labels.js';
9
9
  export { PieChart, PieChartProps, PieChartSlice } from './components/pie-chart.js';
10
10
  export { DEFAULT_PIE_CHART_LABELS, PieChartLabels } from './components/pie-chart-labels.js';
11
+ export { SeriesChartAxisBudget } from './components/series-chart-budget.js';
11
12
  export { TimeTickUnit } from './components/series-chart-ticks.js';
12
13
  import 'recharts';
13
14
  import 'react';
@@ -5,6 +5,7 @@ import 'recharts';
5
5
  import './series-chart-labels.js';
6
6
  import './toggle-legend.js';
7
7
  import './series-chart-ticks.js';
8
+ import './series-chart-budget.js';
8
9
 
9
10
  /** Which chart of a facing pair. Physical, like the axes: charts do not mirror in RTL. */
10
11
  type FacingSide = "left" | "right";
@@ -0,0 +1,60 @@
1
+ /**
2
+ * How many y axes a chart draws. A number caps the TOTAL; `{ left, right }` caps each
3
+ * side (a side left out is not capped). Axes over the cap are hidden in declaration
4
+ * order — the first of each side is the last to go, see {@link budgetedAxes}.
5
+ */
6
+ type SeriesChartAxisBudget = number | {
7
+ left?: number;
8
+ right?: number;
9
+ };
10
+ /** The part of an axis the budget reads. */
11
+ interface BudgetAxis {
12
+ id: string;
13
+ orientation?: "left" | "right";
14
+ hide?: boolean;
15
+ title?: string;
16
+ width?: number;
17
+ }
18
+ /**
19
+ * The narrowest the plot may get before the automatic budget steps in, in px.
20
+ *
21
+ * About a phone's plot with one axis a side (390 px viewport, card padding, two bands
22
+ * of 48 + 16), where a curve's shape still reads. It is the point below which the chart
23
+ * is already broken today — a plot the width of a thumb between four columns of numbers
24
+ * — so the rule changes nothing that was working.
25
+ */
26
+ declare const MIN_PLOT_WIDTH = 160;
27
+ /** The budget the automatic rule applies: one axis a side. */
28
+ declare const NARROW_AXIS_BUDGET: SeriesChartAxisBudget;
29
+ /**
30
+ * The ids of the VISIBLE axes a budget hides — axes already `hide: true` are neither
31
+ * counted nor returned. Empty for no budget.
32
+ *
33
+ * Kept, in order of priority: the first visible axis of each side (in declaration order,
34
+ * so the axis a caller lists first — the one the grid hangs its rules off — always
35
+ * stays), then the rest in declaration order, until the cap. Per side, that is simply the
36
+ * first `left` left-hand axes and the first `right` right-hand ones.
37
+ */
38
+ declare function budgetedAxes(axes: readonly BudgetAxis[], budget: SeriesChartAxisBudget | undefined): string[];
39
+ /**
40
+ * The automatic budget for a chart `width` px wide: {@link NARROW_AXIS_BUDGET} when the
41
+ * visible axes' bands would leave the plot under {@link MIN_PLOT_WIDTH} AND some side
42
+ * draws more than one axis — the only case one-a-side gives anything back. `undefined`
43
+ * otherwise, and for a width that is not known (no layout, no `ResizeObserver`).
44
+ *
45
+ * Decided on the CHART's width and the axes as declared, never on the plot the budget
46
+ * produced, so hiding an axis cannot widen the plot back over the line and bring it
47
+ * back: the rule has no feedback to oscillate on.
48
+ */
49
+ declare function autoAxisBudget(axes: readonly BudgetAxis[], width: number | undefined, bandWidth: (axis: BudgetAxis) => number): SeriesChartAxisBudget | undefined;
50
+ /**
51
+ * The unit an axis' series say when the axis is not drawn: its `unit`, or else the
52
+ * parenthesised tail of its title — "Load (N)" says "N", which is how the apps write
53
+ * every axis title. `undefined` when neither says one.
54
+ */
55
+ declare function axisUnit(axis: {
56
+ unit?: string;
57
+ title?: string;
58
+ }): string | undefined;
59
+
60
+ export { MIN_PLOT_WIDTH, NARROW_AXIS_BUDGET, type SeriesChartAxisBudget, autoAxisBudget, axisUnit, budgetedAxes };
@@ -0,0 +1,56 @@
1
+ "use client";
2
+ const MIN_PLOT_WIDTH = 160;
3
+ const NARROW_AXIS_BUDGET = { left: 1, right: 1 };
4
+ const BARE_SIDE_MARGIN = 10;
5
+ const sideOf = (axis) => axis.orientation ?? "left";
6
+ function budgetedAxes(axes, budget) {
7
+ if (budget === void 0) return [];
8
+ const visible = axes.filter((axis) => !axis.hide);
9
+ if (typeof budget === "number") {
10
+ const cap = Math.max(0, Math.floor(budget));
11
+ const firsts = /* @__PURE__ */ new Set();
12
+ const seen = /* @__PURE__ */ new Set();
13
+ for (const axis of visible) {
14
+ if (!seen.has(sideOf(axis))) firsts.add(axis.id);
15
+ seen.add(sideOf(axis));
16
+ }
17
+ const priority = [
18
+ ...visible.filter((axis) => firsts.has(axis.id)),
19
+ ...visible.filter((axis) => !firsts.has(axis.id))
20
+ ];
21
+ const kept = new Set(priority.slice(0, cap).map((axis) => axis.id));
22
+ return visible.filter((axis) => !kept.has(axis.id)).map((axis) => axis.id);
23
+ }
24
+ const used = { left: 0, right: 0 };
25
+ const out = [];
26
+ for (const axis of visible) {
27
+ const side = sideOf(axis);
28
+ const cap = budget[side];
29
+ if (cap !== void 0 && used[side] >= Math.max(0, Math.floor(cap))) out.push(axis.id);
30
+ else used[side] += 1;
31
+ }
32
+ return out;
33
+ }
34
+ function autoAxisBudget(axes, width, bandWidth) {
35
+ if (width === void 0 || !(width > 0)) return void 0;
36
+ const visible = axes.filter((axis) => !axis.hide);
37
+ const left = visible.filter((axis) => sideOf(axis) === "left");
38
+ const right = visible.filter((axis) => sideOf(axis) === "right");
39
+ if (left.length <= 1 && right.length <= 1) return void 0;
40
+ const bands = visible.reduce((sum, axis) => sum + bandWidth(axis), 0);
41
+ const margins = (left.length ? 0 : BARE_SIDE_MARGIN) + (right.length ? 0 : BARE_SIDE_MARGIN);
42
+ return width - bands - margins < MIN_PLOT_WIDTH ? NARROW_AXIS_BUDGET : void 0;
43
+ }
44
+ function axisUnit(axis) {
45
+ if (axis.unit !== void 0) return axis.unit || void 0;
46
+ const tail = /\(([^()]+)\)\s*$/.exec(axis.title ?? "");
47
+ return tail?.[1].trim() || void 0;
48
+ }
49
+ export {
50
+ MIN_PLOT_WIDTH,
51
+ NARROW_AXIS_BUDGET,
52
+ autoAxisBudget,
53
+ axisUnit,
54
+ budgetedAxes
55
+ };
56
+ //# sourceMappingURL=series-chart-budget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/series-chart-budget.ts"],"sourcesContent":["// The axis budget of a series chart: which of its y axes are drawn, when there are more\n// than the width can carry. Pure, and apart from the chart, so the rule is tested on\n// numbers rather than on a jsdom that has no layout to measure.\n//\n// lenkbank's curve plot declares four visible axes — velocity, position and acceleration\n// on the left, loads on the right — and every one reserves its band of ticks and title.\n// At 1280 px that is a quarter of the card; at 390 px it is all of it, and the plot the\n// axes are for is ten pixels wide. An axis over budget is drawn the way a `hide: true`\n// one always was: it still scales its lines (and the zoom still refits it), but it draws\n// no ticks, no title, and reserves no width.\n\n/**\n * How many y axes a chart draws. A number caps the TOTAL; `{ left, right }` caps each\n * side (a side left out is not capped). Axes over the cap are hidden in declaration\n * order — the first of each side is the last to go, see {@link budgetedAxes}.\n */\nexport type SeriesChartAxisBudget = number | { left?: number; right?: number };\n\n/** The part of an axis the budget reads. */\ninterface BudgetAxis {\n id: string;\n orientation?: \"left\" | \"right\";\n hide?: boolean;\n title?: string;\n width?: number;\n}\n\n/**\n * The narrowest the plot may get before the automatic budget steps in, in px.\n *\n * About a phone's plot with one axis a side (390 px viewport, card padding, two bands\n * of 48 + 16), where a curve's shape still reads. It is the point below which the chart\n * is already broken today — a plot the width of a thumb between four columns of numbers\n * — so the rule changes nothing that was working.\n */\nexport const MIN_PLOT_WIDTH = 160;\n\n/** The budget the automatic rule applies: one axis a side. */\nexport const NARROW_AXIS_BUDGET: SeriesChartAxisBudget = { left: 1, right: 1 };\n\n/** The plot's margin on a side no axis band covers — `SeriesPlot`'s own. */\nconst BARE_SIDE_MARGIN = 10;\n\nconst sideOf = (axis: BudgetAxis) => axis.orientation ?? \"left\";\n\n/**\n * The ids of the VISIBLE axes a budget hides — axes already `hide: true` are neither\n * counted nor returned. Empty for no budget.\n *\n * Kept, in order of priority: the first visible axis of each side (in declaration order,\n * so the axis a caller lists first — the one the grid hangs its rules off — always\n * stays), then the rest in declaration order, until the cap. Per side, that is simply the\n * first `left` left-hand axes and the first `right` right-hand ones.\n */\nexport function budgetedAxes(\n axes: readonly BudgetAxis[],\n budget: SeriesChartAxisBudget | undefined,\n): string[] {\n if (budget === undefined) return [];\n const visible = axes.filter((axis) => !axis.hide);\n if (typeof budget === \"number\") {\n const cap = Math.max(0, Math.floor(budget));\n const firsts = new Set<string>();\n const seen = new Set<string>();\n for (const axis of visible) {\n if (!seen.has(sideOf(axis))) firsts.add(axis.id);\n seen.add(sideOf(axis));\n }\n const priority = [\n ...visible.filter((axis) => firsts.has(axis.id)),\n ...visible.filter((axis) => !firsts.has(axis.id)),\n ];\n const kept = new Set(priority.slice(0, cap).map((axis) => axis.id));\n return visible.filter((axis) => !kept.has(axis.id)).map((axis) => axis.id);\n }\n const used = { left: 0, right: 0 };\n const out: string[] = [];\n for (const axis of visible) {\n const side = sideOf(axis);\n const cap = budget[side];\n if (cap !== undefined && used[side] >= Math.max(0, Math.floor(cap))) out.push(axis.id);\n else used[side] += 1;\n }\n return out;\n}\n\n/**\n * The automatic budget for a chart `width` px wide: {@link NARROW_AXIS_BUDGET} when the\n * visible axes' bands would leave the plot under {@link MIN_PLOT_WIDTH} AND some side\n * draws more than one axis — the only case one-a-side gives anything back. `undefined`\n * otherwise, and for a width that is not known (no layout, no `ResizeObserver`).\n *\n * Decided on the CHART's width and the axes as declared, never on the plot the budget\n * produced, so hiding an axis cannot widen the plot back over the line and bring it\n * back: the rule has no feedback to oscillate on.\n */\nexport function autoAxisBudget(\n axes: readonly BudgetAxis[],\n width: number | undefined,\n bandWidth: (axis: BudgetAxis) => number,\n): SeriesChartAxisBudget | undefined {\n if (width === undefined || !(width > 0)) return undefined;\n const visible = axes.filter((axis) => !axis.hide);\n const left = visible.filter((axis) => sideOf(axis) === \"left\");\n const right = visible.filter((axis) => sideOf(axis) === \"right\");\n if (left.length <= 1 && right.length <= 1) return undefined;\n const bands = visible.reduce((sum, axis) => sum + bandWidth(axis), 0);\n const margins = (left.length ? 0 : BARE_SIDE_MARGIN) + (right.length ? 0 : BARE_SIDE_MARGIN);\n return width - bands - margins < MIN_PLOT_WIDTH ? NARROW_AXIS_BUDGET : undefined;\n}\n\n/**\n * The unit an axis' series say when the axis is not drawn: its `unit`, or else the\n * parenthesised tail of its title — \"Load (N)\" says \"N\", which is how the apps write\n * every axis title. `undefined` when neither says one.\n */\nexport function axisUnit(axis: { unit?: string; title?: string }): string | undefined {\n if (axis.unit !== undefined) return axis.unit || undefined;\n const tail = /\\(([^()]+)\\)\\s*$/.exec(axis.title ?? \"\");\n return tail?.[1].trim() || undefined;\n}\n"],"mappings":";AAmCO,MAAM,iBAAiB;AAGvB,MAAM,qBAA4C,EAAE,MAAM,GAAG,OAAO,EAAE;AAG7E,MAAM,mBAAmB;AAEzB,MAAM,SAAS,CAAC,SAAqB,KAAK,eAAe;AAWlD,SAAS,aACd,MACA,QACU;AACV,MAAI,WAAW,OAAW,QAAO,CAAC;AAClC,QAAM,UAAU,KAAK,OAAO,CAAC,SAAS,CAAC,KAAK,IAAI;AAChD,MAAI,OAAO,WAAW,UAAU;AAC9B,UAAM,MAAM,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,CAAC;AAC1C,UAAM,SAAS,oBAAI,IAAY;AAC/B,UAAM,OAAO,oBAAI,IAAY;AAC7B,eAAW,QAAQ,SAAS;AAC1B,UAAI,CAAC,KAAK,IAAI,OAAO,IAAI,CAAC,EAAG,QAAO,IAAI,KAAK,EAAE;AAC/C,WAAK,IAAI,OAAO,IAAI,CAAC;AAAA,IACvB;AACA,UAAM,WAAW;AAAA,MACf,GAAG,QAAQ,OAAO,CAAC,SAAS,OAAO,IAAI,KAAK,EAAE,CAAC;AAAA,MAC/C,GAAG,QAAQ,OAAO,CAAC,SAAS,CAAC,OAAO,IAAI,KAAK,EAAE,CAAC;AAAA,IAClD;AACA,UAAM,OAAO,IAAI,IAAI,SAAS,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,SAAS,KAAK,EAAE,CAAC;AAClE,WAAO,QAAQ,OAAO,CAAC,SAAS,CAAC,KAAK,IAAI,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,SAAS,KAAK,EAAE;AAAA,EAC3E;AACA,QAAM,OAAO,EAAE,MAAM,GAAG,OAAO,EAAE;AACjC,QAAM,MAAgB,CAAC;AACvB,aAAW,QAAQ,SAAS;AAC1B,UAAM,OAAO,OAAO,IAAI;AACxB,UAAM,MAAM,OAAO,IAAI;AACvB,QAAI,QAAQ,UAAa,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,KAAK,MAAM,GAAG,CAAC,EAAG,KAAI,KAAK,KAAK,EAAE;AAAA,QAChF,MAAK,IAAI,KAAK;AAAA,EACrB;AACA,SAAO;AACT;AAYO,SAAS,eACd,MACA,OACA,WACmC;AACnC,MAAI,UAAU,UAAa,EAAE,QAAQ,GAAI,QAAO;AAChD,QAAM,UAAU,KAAK,OAAO,CAAC,SAAS,CAAC,KAAK,IAAI;AAChD,QAAM,OAAO,QAAQ,OAAO,CAAC,SAAS,OAAO,IAAI,MAAM,MAAM;AAC7D,QAAM,QAAQ,QAAQ,OAAO,CAAC,SAAS,OAAO,IAAI,MAAM,OAAO;AAC/D,MAAI,KAAK,UAAU,KAAK,MAAM,UAAU,EAAG,QAAO;AAClD,QAAM,QAAQ,QAAQ,OAAO,CAAC,KAAK,SAAS,MAAM,UAAU,IAAI,GAAG,CAAC;AACpE,QAAM,WAAW,KAAK,SAAS,IAAI,qBAAqB,MAAM,SAAS,IAAI;AAC3E,SAAO,QAAQ,QAAQ,UAAU,iBAAiB,qBAAqB;AACzE;AAOO,SAAS,SAAS,MAA6D;AACpF,MAAI,KAAK,SAAS,OAAW,QAAO,KAAK,QAAQ;AACjD,QAAM,OAAO,mBAAmB,KAAK,KAAK,SAAS,EAAE;AACrD,SAAO,OAAO,CAAC,EAAE,KAAK,KAAK;AAC7B;","names":[]}
@@ -4,6 +4,7 @@ import { ZoomAxesSetting, ZoomBinding } from './chart-zoom.js';
4
4
  import { LegendEntry } from './toggle-legend.js';
5
5
  import { SeriesChartLabels } from './series-chart-labels.js';
6
6
  import { TimeTickUnit } from './series-chart-ticks.js';
7
+ import { SeriesChartAxisBudget } from './series-chart-budget.js';
7
8
  import 'recharts';
8
9
 
9
10
  interface SeriesChartSeries {
@@ -131,6 +132,15 @@ interface SeriesChartAxisShape {
131
132
  color?: string;
132
133
  /** What the TICKS need, in px. The title's strip is added on top. Default 48. */
133
134
  width?: number;
135
+ /**
136
+ * The unit, bare — `"mm/s"`. The title says it while the axis is drawn; when it is not
137
+ * (`hide`, or over the chart's axis budget — see {@link SeriesChartProps.maxVisibleAxes})
138
+ * the series on it say it instead, as "Velocity (mm/s)": in the tooltip of a chart
139
+ * whose budget hid the axis, and in the legend {@link seriesLegendEntries} builds when
140
+ * handed the axes. Default: the parenthesised tail of `title` ("Load (N)" → "N").
141
+ * `""` for a quantity with no unit whose title merely ends in brackets.
142
+ */
143
+ unit?: string;
134
144
  /**
135
145
  * Pin the scale instead of fitting it to this chart's data. For a ROW of charts
136
146
  * read against each other: auto-fitted, a 200 N loop and a 2000 N one draw the
@@ -426,6 +436,37 @@ interface SeriesChartProps {
426
436
  /** Where the tooltip may go: for a chart inside a scroll wrapper. See
427
437
  * {@link SeriesChartTooltip}. */
428
438
  tooltip?: SeriesChartTooltip;
439
+ /**
440
+ * The most y axes the chart DRAWS: a total, or `{ left, right }` per side. Axes over it
441
+ * are drawn as `hide: true` ones are — they still scale their lines and the zoom still
442
+ * refits them, but they draw no ticks or title and reserve no width — in declaration
443
+ * order, so the first axis of each side is the last to go (`series-chart-budget.ts`).
444
+ * The series on a hidden axis say its unit instead (see {@link SeriesChartAxis.unit}).
445
+ * Default: no cap — only the automatic one of `axisBudget`.
446
+ */
447
+ maxVisibleAxes?: SeriesChartAxisBudget;
448
+ /**
449
+ * `"auto"` (the default): a chart so narrow that its axes' bands would leave the plot
450
+ * under 160 px (`MIN_PLOT_WIDTH`) draws ONE axis a side — lenkbank's four-axis curve
451
+ * plot on a phone, whose plot was 10 px wide. Measured on the chart's own box
452
+ * (`ResizeObserver`), and applied on top of `maxVisibleAxes`.
453
+ *
454
+ * On by default because it cannot touch a layout that works: it needs a side with two
455
+ * or more axes AND a plot that would otherwise be under 160 px — a four-axis chart
456
+ * below about 440 px, a two-left-axis one below about 300. A chart with at most one
457
+ * axis a side (every facing pair, every single-axis chart) is never budgeted, at any
458
+ * width. `"off"` keeps every declared axis whatever the width: for a stack of
459
+ * multi-axis charts that must keep identical bands, or a caller budgeting itself.
460
+ * Without layout (jsdom, SSR) nothing is measured and nothing is hidden.
461
+ */
462
+ axisBudget?: "auto" | "off";
463
+ /**
464
+ * The ids of the axes the budget hid (not those declared `hide`), every time that set
465
+ * changes — for a legend that says their units: pass it to {@link seriesLegendEntries}
466
+ * as `budgeted`. Called after the render that hid them; empty once the chart is wide
467
+ * again.
468
+ */
469
+ onAxisBudget?: (hidden: readonly string[]) => void;
429
470
  /** Supplied by `withChartZoom` and by nothing else. */
430
471
  zoom?: ZoomBinding;
431
472
  /** Per-chart strings over `<UiKitProvider labels={{ seriesChart }}>`. */
@@ -510,13 +551,24 @@ declare function anchoredBand(extent: readonly [number, number] | undefined): [n
510
551
  * and filtering after keeps each series in the colour its legend entry shows.
511
552
  */
512
553
  declare function visibleSeries(series: readonly SeriesChartSeries[], hidden: ReadonlySet<string>): SeriesChartSeries[];
554
+ /** Where a legend learns which axes are not drawn — see {@link seriesLegendEntries}. */
555
+ interface SeriesLegendAxes {
556
+ /** The chart's axes. A series on one that is `hide`, or `budgeted`, says its unit. */
557
+ axes?: readonly SeriesChartAxis[];
558
+ /** What the chart's `onAxisBudget` last said. */
559
+ budgeted?: readonly string[];
560
+ }
513
561
  /**
514
562
  * The `ToggleLegend` entries for a chart's FULL series list — colours resolved the way
515
563
  * the chart resolves them, and a stroke mark for a line drawn in a pattern (dashed,
516
564
  * step) so the key promises the stroke the plot draws. A bar or an area is a swatch.
517
565
  * Pair with {@link visibleSeries} for the chart itself.
566
+ *
567
+ * Handed the chart's `axes` (and the `budgeted` ids its `onAxisBudget` reports), a
568
+ * series whose axis is not drawn says the unit that axis would have: "Velocity (mm/s)"
569
+ * — see {@link SeriesChartAxis.unit}. Without them, the labels are as given.
518
570
  */
519
- declare function seriesLegendEntries(series: readonly SeriesChartSeries[]): LegendEntry[];
571
+ declare function seriesLegendEntries(series: readonly SeriesChartSeries[], { axes, budgeted }?: SeriesLegendAxes): LegendEntry[];
520
572
  /**
521
573
  * {@link SeriesChart} without the zoom: the same picture, no drag layer, no reset
522
574
  * button. For a thumbnail, a print view, or a chart the consumer wraps in its own
@@ -531,4 +583,4 @@ declare function StaticSeriesChart(props: SeriesChartProps): react.JSX.Element;
531
583
  */
532
584
  declare function SeriesChart(props: SeriesChartProps): react.JSX.Element;
533
585
 
534
- export { AXIS_TICK_WIDTH, AXIS_TITLE_STRIP, SeriesChart, type SeriesChartAxis, type SeriesChartHit, type SeriesChartMarker, type SeriesChartPoint, type SeriesChartProps, type SeriesChartReference, type SeriesChartRow, type SeriesChartSeries, type SeriesChartSpan, type SeriesChartTickValues, type SeriesChartTone, type SeriesChartTooltip, type SeriesChartType, type SeriesChartX, type SeriesChartXTick, type SeriesChartXValue, type SeriesSource, StaticSeriesChart, TimeTickUnit, anchoredBand, axisBandWidth, mergeSeries, oneAxis, padBand, paddedDomain, seriesKey, seriesLegendEntries, soleSeriesColor, visibleSeries };
586
+ export { AXIS_TICK_WIDTH, AXIS_TITLE_STRIP, SeriesChart, type SeriesChartAxis, SeriesChartAxisBudget, type SeriesChartHit, type SeriesChartMarker, type SeriesChartPoint, type SeriesChartProps, type SeriesChartReference, type SeriesChartRow, type SeriesChartSeries, type SeriesChartSpan, type SeriesChartTickValues, type SeriesChartTone, type SeriesChartTooltip, type SeriesChartType, type SeriesChartX, type SeriesChartXTick, type SeriesChartXValue, type SeriesLegendAxes, type SeriesSource, StaticSeriesChart, TimeTickUnit, anchoredBand, axisBandWidth, mergeSeries, oneAxis, padBand, paddedDomain, seriesKey, seriesLegendEntries, soleSeriesColor, visibleSeries };
@@ -1,6 +1,6 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
4
4
  import {
5
5
  Area,
6
6
  Bar,
@@ -25,6 +25,7 @@ import {
25
25
  import { STEP_DASH, strokeDash } from "./toggle-legend.js";
26
26
  import { DEFAULT_SERIES_CHART_LABELS } from "./series-chart-labels.js";
27
27
  import { categoryTicks, integerTicks, niceTicks, timeTicksWithUnit } from "./series-chart-ticks.js";
28
+ import { autoAxisBudget, axisUnit, budgetedAxes } from "./series-chart-budget.js";
28
29
  import { paletteFor } from "../theme/chart-palette.js";
29
30
  import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
30
31
  import { cn } from "../lib/cn.js";
@@ -104,14 +105,28 @@ function anchoredBand(extent) {
104
105
  function visibleSeries(series, hidden) {
105
106
  return series.map((entry, index) => ({ ...entry, color: entry.color ?? paletteFor(index) })).filter((entry) => !hidden.has(entry.key));
106
107
  }
107
- function seriesLegendEntries(series) {
108
+ function withUnit(label, unit) {
109
+ if (!unit) return label;
110
+ if (typeof label === "string" || typeof label === "number") return `${label} (${unit})`;
111
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
112
+ label,
113
+ " (",
114
+ unit,
115
+ ")"
116
+ ] });
117
+ }
118
+ function seriesLegendEntries(series, { axes, budgeted } = {}) {
119
+ const undrawn = /* @__PURE__ */ new Map();
120
+ for (const axis of axes ?? []) {
121
+ if (axis.hide || budgeted?.includes(axis.id)) undrawn.set(axis.id, axisUnit(axis));
122
+ }
108
123
  return series.map((entry, index) => {
109
124
  const own = entry.dash ?? (entry.dashed ? 1 : 0);
110
125
  const dash = entry.step ? STEP_DASH : own;
111
126
  const line = (entry.type ?? "line") === "line";
112
127
  return {
113
128
  key: entry.key,
114
- label: entry.label,
129
+ label: withUnit(entry.label, undrawn.get(entry.axis ?? DEFAULT_Y_AXIS)),
115
130
  color: entry.color ?? paletteFor(index),
116
131
  ...line && dash !== 0 ? { marker: "stroke", dash } : {}
117
132
  };
@@ -323,13 +338,44 @@ function SeriesPlot({
323
338
  labels: labelsProp,
324
339
  locale: localeProp,
325
340
  className,
326
- minBarLength
341
+ minBarLength,
342
+ maxVisibleAxes,
343
+ axisBudget = "auto",
344
+ onAxisBudget
327
345
  }) {
328
346
  const labels = useKitLabels("seriesChart", DEFAULT_SERIES_CHART_LABELS, labelsProp);
329
347
  const locale = useKitLocale(localeProp);
330
348
  const number = useDefaultFormat(localeProp);
331
349
  const [keyStop, setKeyStop] = useState(0);
332
350
  const [keyFocus, setKeyFocus] = useState(void 0);
351
+ const [box, setBox] = useState(null);
352
+ const [width, setWidth] = useState(void 0);
353
+ useLayoutEffect(() => {
354
+ if (!box || axisBudget === "off" || typeof ResizeObserver === "undefined") return;
355
+ const measure = () => setWidth(box.getBoundingClientRect().width || void 0);
356
+ measure();
357
+ const observer = new ResizeObserver(measure);
358
+ observer.observe(box);
359
+ return () => observer.disconnect();
360
+ }, [box, axisBudget]);
361
+ const capped = budgetedAxes(axes, maxVisibleAxes);
362
+ const afterCap = capped.length ? axes.map((axis) => capped.includes(axis.id) ? { ...axis, hide: true } : axis) : axes;
363
+ const auto = axisBudget === "off" ? [] : budgetedAxes(
364
+ afterCap,
365
+ autoAxisBudget(
366
+ afterCap,
367
+ width,
368
+ (axis) => axisBandWidth(axis.width, Boolean(axis.title))
369
+ )
370
+ );
371
+ const budgeted = [...capped, ...auto];
372
+ const budgetKey = budgeted.join("\n");
373
+ const reported = useRef("");
374
+ useEffect(() => {
375
+ if (budgetKey === reported.current) return;
376
+ reported.current = budgetKey;
377
+ onAxisBudget?.(budgetKey ? budgetKey.split("\n") : []);
378
+ }, [budgetKey, onAxisBudget]);
333
379
  const heightClass = typeof height === "string" ? height : void 0;
334
380
  const heightStyle = typeof height === "number" ? { height } : void 0;
335
381
  if (!model.source.length || !series.length) {
@@ -346,14 +392,21 @@ function SeriesPlot({
346
392
  const xKey = model.plotKey;
347
393
  const plotted = model.rows;
348
394
  const rows = model.source;
395
+ const drawnAxes = budgeted.length ? axes.map((axis) => budgeted.includes(axis.id) ? { ...axis, hide: true } : axis) : axes;
396
+ const lostUnit = new Map(
397
+ axes.filter((axis) => budgeted.includes(axis.id)).map((axis) => [axis.id, axisUnit(axis)])
398
+ );
349
399
  const config = Object.fromEntries(
350
400
  series.map((entry, index) => [
351
401
  entry.key,
352
- { label: entry.label, color: entry.color ?? paletteFor(index) }
402
+ {
403
+ label: withUnit(entry.label, lostUnit.get(entry.axis ?? DEFAULT_Y_AXIS)),
404
+ color: entry.color ?? paletteFor(index)
405
+ }
353
406
  ])
354
407
  );
355
- const visible = axes.filter((axis) => !axis.hide);
356
- const gridAxis = (visible[0] ?? axes[0])?.id ?? DEFAULT_Y_AXIS;
408
+ const visible = drawnAxes.filter((axis) => !axis.hide);
409
+ const gridAxis = (visible[0] ?? drawnAxes[0])?.id ?? DEFAULT_Y_AXIS;
357
410
  const onLeft = visible.some((axis) => (axis.orientation ?? "left") === "left");
358
411
  const onRight = visible.some((axis) => axis.orientation === "right");
359
412
  const margin = {
@@ -511,6 +564,8 @@ function SeriesPlot({
511
564
  return /* @__PURE__ */ jsx(
512
565
  ChartContainer,
513
566
  {
567
+ ref: setBox,
568
+ "data-axis-budget": budgeted.length ? budgetKey.replace(/\n/g, " ") : void 0,
514
569
  config,
515
570
  className: cn("w-full", heightClass, className, onPointClick && "cursor-pointer"),
516
571
  style: heightStyle,
@@ -555,7 +610,7 @@ function SeriesPlot({
555
610
  )
556
611
  }
557
612
  ),
558
- axes.map((axis) => {
613
+ drawnAxes.map((axis) => {
559
614
  const domain = fittedY(axis);
560
615
  const zoomed = anchored(axis) ? zoom?.xDomain !== void 0 : zoom?.yDomains[axis.id] !== void 0;
561
616
  return /* @__PURE__ */ jsx(
@@ -567,7 +622,7 @@ function SeriesPlot({
567
622
  ticks: resolveTicks(axis.tickValues, domain, finite) ?? (wholeOnly(axis) ? integerTicks(domain) ?? niceTicks(domain) : niceTicks(domain)),
568
623
  allowDataOverflow: zoomed,
569
624
  orientation: axis.orientation ?? "left",
570
- width: axisBandWidth(axis.width, Boolean(axis.title) && !axis.hide),
625
+ width: axis.hide ? 0 : axisBandWidth(axis.width, Boolean(axis.title)),
571
626
  tickLine: false,
572
627
  axisLine: false,
573
628
  tickFormatter: axis.format ?? number,
@@ -610,7 +665,7 @@ function SeriesPlot({
610
665
  const own = entry.dash ?? (entry.dashed ? 1 : 0);
611
666
  const dash = entry.step ? strokeDash(STEP_DASH) : typeof own === "string" ? own : strokeDash(own);
612
667
  const curve = entry.step ? "stepAfter" : entry.curve ?? "monotone";
613
- const width = entry.strokeWidth ?? (entry.step ? STEP_WIDTH : LINE_WIDTH);
668
+ const width2 = entry.strokeWidth ?? (entry.step ? STEP_WIDTH : LINE_WIDTH);
614
669
  if (type === "bar") {
615
670
  const stub = entry.minBarLength ?? minBarLength;
616
671
  return /* @__PURE__ */ jsx(
@@ -643,7 +698,7 @@ function SeriesPlot({
643
698
  dataKey: entry.key,
644
699
  stackId: entry.stack,
645
700
  stroke: color,
646
- strokeWidth: width,
701
+ strokeWidth: width2,
647
702
  strokeDasharray: dash,
648
703
  fill: color,
649
704
  fillOpacity: entry.fillOpacity ?? (entry.stack !== void 0 ? 0.55 : 0.2),
@@ -662,7 +717,7 @@ function SeriesPlot({
662
717
  type: curve,
663
718
  dataKey: entry.key,
664
719
  stroke: color,
665
- strokeWidth: width,
720
+ strokeWidth: width2,
666
721
  strokeDasharray: dash,
667
722
  dot: dotFor(entry),
668
723
  ...entry.activeDot !== void 0 ? { activeDot: entry.activeDot } : {},