@galaxy-io/dls 1.3.0 → 1.3.2

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 (43) hide show
  1. package/dist/charts/BarChart.d.ts +3 -3
  2. package/dist/charts/BarChart.js +68 -104
  3. package/dist/charts/ChartCategoryTargets.d.ts +27 -0
  4. package/dist/charts/ChartCategoryTargets.js +75 -0
  5. package/dist/charts/ChartFrame.d.ts +34 -6
  6. package/dist/charts/ChartFrame.js +72 -15
  7. package/dist/charts/ChartPrimitives.d.ts +35 -0
  8. package/dist/charts/ChartPrimitives.js +75 -38
  9. package/dist/charts/ChartTooltip.d.ts +3 -3
  10. package/dist/charts/ChartTooltip.js +2 -2
  11. package/dist/charts/LineChart.js +47 -62
  12. package/dist/charts/PieChart.js +48 -26
  13. package/dist/charts/barChartGeometry.d.ts +66 -33
  14. package/dist/charts/barChartGeometry.js +7 -15
  15. package/dist/charts/barChartLegend.d.ts +25 -0
  16. package/dist/charts/barChartLegend.js +23 -0
  17. package/dist/charts/chartFormat.d.ts +9 -1
  18. package/dist/charts/chartFormat.js +13 -1
  19. package/dist/charts/chartScales.d.ts +58 -22
  20. package/dist/charts/chartScales.js +92 -38
  21. package/dist/charts/constants.d.ts +22 -6
  22. package/dist/charts/constants.js +22 -1
  23. package/dist/charts/lineChartGeometry.d.ts +53 -15
  24. package/dist/charts/lineChartGeometry.js +6 -15
  25. package/dist/charts/pieChartGeometry.d.ts +20 -8
  26. package/dist/charts/pieChartGeometry.js +3 -13
  27. package/dist/charts/types.d.ts +51 -145
  28. package/dist/charts/types.js +22 -3
  29. package/dist/charts/useCartesianChartLayout.d.ts +70 -0
  30. package/dist/charts/useCartesianChartLayout.js +56 -0
  31. package/dist/charts/useChartDimensions.d.ts +10 -3
  32. package/dist/charts/useChartDimensions.js +14 -4
  33. package/dist/charts/useChartInteraction.d.ts +13 -8
  34. package/dist/charts/useChartInteraction.js +7 -11
  35. package/dist/charts/useSeriesColorResolver.d.ts +37 -0
  36. package/dist/charts/useSeriesColorResolver.js +27 -0
  37. package/dist/inputs/DateInput.js +3 -3
  38. package/dist/inputs/MultiSelectInput.d.ts +3 -2
  39. package/dist/inputs/MultiSelectInput.js +8 -8
  40. package/dist/inputs/SelectInput.d.ts +4 -8
  41. package/dist/inputs/SelectInput.js +14 -20
  42. package/dist/styles.css +13 -11
  43. package/package.json +1 -1
@@ -1,29 +1,6 @@
1
1
  import { buildValueScale } from "./chartScales";
2
2
  import { BarChartNormalization, BarCorners } from "./types";
3
- import type { BarChartGroupBand, BarChartGroupDatum, BarChartRect, ChartPalette, ChartValueDomain } from "./types";
4
- /**
5
- * Bar chart geometry.
6
- *
7
- * Flattens the group → bar → component hierarchy into a flat array of
8
- * rectangles plus one hit band per group, so the component body is a `.map()`
9
- * with no nested loops or running accumulators.
10
- *
11
- * Pure: no React, no theme access. Colors arrive through a resolver callback so
12
- * this module never has to know how the palette maps to the theme.
13
- */
14
- interface BarSpacing {
15
- /** Px between neighbouring groups. */
16
- groupGap: number;
17
- /** Px between bars inside a group. */
18
- barGap: number;
19
- }
20
- /**
21
- * Pick gap sizes from how crowded the chart is.
22
- *
23
- * Sparse charts stay roomy regardless of width; once there are enough bars to
24
- * matter, the gaps tighten as the plot narrows.
25
- */
26
- export declare function getBarSpacing(plotWidth: number, totalBars: number): BarSpacing;
3
+ import type { BarChartGroupDatum, ChartPalette } from "./types";
27
4
  /**
28
5
  * Rectangle with one pair of corners rounded.
29
6
  *
@@ -37,7 +14,60 @@ export declare function roundedRectPath(x: number, y: number, width: number, hei
37
14
  * Percent-normalized charts always span 0–1, so the axis is stable no matter
38
15
  * what the underlying totals are.
39
16
  */
40
- export declare function getBarValueDomain<TMetric extends string, TPivot extends string, TComponentKey extends string>(groups: BarChartGroupDatum<TMetric, TPivot, TComponentKey>[], normalization: BarChartNormalization): [number, number];
17
+ export declare function getBarValueDomain<TMetric extends string, TComponentKey extends string>(groups: BarChartGroupDatum<TMetric, TComponentKey>[], normalization: BarChartNormalization): [number, number];
18
+ /**
19
+ * A single rendered rectangle.
20
+ *
21
+ * The geometry layer flattens the group/bar/component hierarchy into an array
22
+ * of these, so rendering is a plain `.map()` with no nested loops or
23
+ * accumulators in the component body.
24
+ */
25
+ export interface BarChartRect<TMetric extends string, TComponentKey extends string = string> {
26
+ /**
27
+ * React key: `${groupIndex}:${barIndex}:${componentKey}`.
28
+ *
29
+ * Deliberately index-based rather than label-based — group labels are free to
30
+ * repeat, and duplicated keys would silently drop rectangles.
31
+ */
32
+ id: string;
33
+ groupIndex: number;
34
+ metric: TMetric;
35
+ componentKey: TComponentKey;
36
+ /** The component's label, carried through for the tooltip. */
37
+ label: string;
38
+ /** The raw value, before any normalization. */
39
+ value: number;
40
+ x: number;
41
+ y: number;
42
+ width: number;
43
+ height: number;
44
+ /** Resolved from the theme — never a raw hex from consumer data. */
45
+ color: string;
46
+ /** Interior stack segments get {@link BarCorners.NONE}. */
47
+ corners: BarCorners;
48
+ /** Corner radius in px, reduced for narrow bars so the curve can't dominate. */
49
+ radius: number;
50
+ }
51
+ /**
52
+ * A category's invisible hit target and tooltip anchor.
53
+ *
54
+ * Hit testing happens per group rather than per rectangle: a two-pixel bar is
55
+ * then exactly as hoverable as a two-hundred-pixel one, and moving the pointer
56
+ * between segments inside one group fires nothing, so the tooltip stays put
57
+ * instead of jumping between segment tops.
58
+ */
59
+ export interface BarChartGroupBand {
60
+ groupIndex: number;
61
+ label: string;
62
+ /** Tiles edge to edge with its neighbours, so there are no dead gutters. */
63
+ x: number;
64
+ y: number;
65
+ width: number;
66
+ height: number;
67
+ /** Group centre, at the top of its tallest bar. Stable while hovering. */
68
+ anchorX: number;
69
+ anchorY: number;
70
+ }
41
71
  export interface BarChartGeometry<TMetric extends string, TComponentKey extends string = string> {
42
72
  /** Every rendered rectangle, in draw order. */
43
73
  rects: BarChartRect<TMetric, TComponentKey>[];
@@ -45,11 +75,9 @@ export interface BarChartGeometry<TMetric extends string, TComponentKey extends
45
75
  bands: BarChartGroupBand[];
46
76
  /** The resolved value scale, shared with the axis. */
47
77
  valueScale: ReturnType<typeof buildValueScale>;
48
- /** Pixel y of value zero — where bars meet the baseline. */
49
- baselineY: number;
50
78
  }
51
- interface BuildBarChartGeometryArgs<TMetric extends string, TPivot extends string, TComponentKey extends string> {
52
- groups: BarChartGroupDatum<TMetric, TPivot, TComponentKey>[];
79
+ interface BuildBarChartGeometryArgs<TMetric extends string, TComponentKey extends string> {
80
+ groups: BarChartGroupDatum<TMetric, TComponentKey>[];
53
81
  plotWidth: number;
54
82
  plotHeight: number;
55
83
  normalization: BarChartNormalization;
@@ -61,7 +89,14 @@ interface BuildBarChartGeometryArgs<TMetric extends string, TPivot extends strin
61
89
  * single-component bar by metric while the legend named components.
62
90
  */
63
91
  isStacked: boolean;
64
- valueDomain?: Partial<ChartValueDomain>;
92
+ /**
93
+ * The final value domain, already overridden, zeroed and niced.
94
+ *
95
+ * Resolved by the caller rather than here, because the left gutter is sized
96
+ * from the tick labels and therefore needs the domain before the plot — and
97
+ * hence this builder — exists.
98
+ */
99
+ domain: [number, number];
65
100
  /**
66
101
  * Maps one segment to a concrete color.
67
102
  *
@@ -74,8 +109,6 @@ interface BuildBarChartGeometryArgs<TMetric extends string, TPivot extends strin
74
109
  }
75
110
  export interface ResolveBarColorArgs<TMetric extends string> {
76
111
  metric: TMetric;
77
- /** Index of the bar within its group. */
78
- barIndex: number;
79
112
  /** Index of the component within its bar. */
80
113
  componentIndex: number;
81
114
  /** True when the *chart* is stacked — see the option of the same name. */
@@ -83,5 +116,5 @@ export interface ResolveBarColorArgs<TMetric extends string> {
83
116
  /** The component's own palette override, if it declared one. */
84
117
  explicit?: ChartPalette;
85
118
  }
86
- export declare function buildBarChartGeometry<TMetric extends string, TPivot extends string = string, TComponentKey extends string = string>({ groups, plotWidth, plotHeight, normalization, isStacked, valueDomain, resolveColor, }: BuildBarChartGeometryArgs<TMetric, TPivot, TComponentKey>): BarChartGeometry<TMetric, TComponentKey>;
119
+ export declare function buildBarChartGeometry<TMetric extends string, TComponentKey extends string = string>({ groups, plotWidth, plotHeight, normalization, isStacked, domain, resolveColor, }: BuildBarChartGeometryArgs<TMetric, TComponentKey>): BarChartGeometry<TMetric, TComponentKey>;
87
120
  export {};
@@ -1,6 +1,6 @@
1
1
  import { BarChartNormalization, BarCorners } from "./types.js";
2
2
  import { BAR_WIDTH_RATIO, MAX_GAP_SHARE } from "./constants.js";
3
- import { applyDomainOverride, buildValueScale, sanitizeValue } from "./chartScales.js";
3
+ import { buildValueScale, sanitizeValue } from "./chartScales.js";
4
4
  //#region src/charts/barChartGeometry.ts
5
5
  /**
6
6
  * Pick gap sizes from how crowded the chart is.
@@ -111,16 +111,15 @@ function getBarValueDomain(groups, normalization) {
111
111
  }
112
112
  return [min, max];
113
113
  }
114
- function buildBarChartGeometry({ groups, plotWidth, plotHeight, normalization, isStacked, valueDomain, resolveColor }) {
115
- const valueScale = buildValueScale(applyDomainOverride(getBarValueDomain(groups, normalization), valueDomain), [plotHeight, 0]);
114
+ function buildBarChartGeometry({ groups, plotWidth, plotHeight, normalization, isStacked, domain, resolveColor }) {
115
+ const valueScale = buildValueScale(domain, [plotHeight, 0]);
116
116
  const baselineY = valueScale(0);
117
117
  const rects = [];
118
118
  const bands = [];
119
119
  if (groups.length === 0 || plotWidth <= 0 || plotHeight <= 0) return {
120
120
  rects,
121
121
  bands,
122
- valueScale,
123
- baselineY
122
+ valueScale
124
123
  };
125
124
  const { groupGap, barGap } = getBarSpacing(plotWidth, groups.reduce((sum, group) => sum + group.bars.length, 0));
126
125
  const step = plotWidth / groups.length;
@@ -136,7 +135,6 @@ function buildBarChartGeometry({ groups, plotWidth, plotHeight, normalization, i
136
135
  const radius = barWidth < 40 ? 2 : 4;
137
136
  const barsStartX = groupX + (groupWidth - (barCount * barWidth + (barCount - 1) * effectiveBarGap)) / 2;
138
137
  let groupTopY = baselineY;
139
- let hasValue = false;
140
138
  group.bars.forEach((bar, barIndex) => {
141
139
  const barX = barsStartX + barIndex * (barWidth + effectiveBarGap);
142
140
  const values = bar.components.map((component) => sanitizeValue(component.value));
@@ -158,7 +156,6 @@ function buildBarChartGeometry({ groups, plotWidth, plotHeight, normalization, i
158
156
  bar.components.forEach((component, componentIndex) => {
159
157
  const { value, from, to } = intervals[componentIndex];
160
158
  if (value === 0) return;
161
- hasValue = true;
162
159
  const isNegative = value < 0;
163
160
  const isCap = componentIndex === (isNegative ? lastNegative : lastPositive);
164
161
  const fromY = Math.round(valueScale(from));
@@ -173,19 +170,16 @@ function buildBarChartGeometry({ groups, plotWidth, plotHeight, normalization, i
173
170
  rects.push({
174
171
  id: `${groupIndex}:${barIndex}:${component.key}`,
175
172
  groupIndex,
176
- barIndex,
177
173
  metric: bar.metric,
178
174
  componentKey: component.key,
179
175
  label: component.label,
180
176
  value,
181
- valueRange: [from, to],
182
177
  x: barX,
183
178
  y,
184
179
  width: barWidth,
185
180
  height,
186
181
  color: resolveColor({
187
182
  metric: bar.metric,
188
- barIndex,
189
183
  componentIndex,
190
184
  isStacked,
191
185
  explicit: component.color
@@ -203,16 +197,14 @@ function buildBarChartGeometry({ groups, plotWidth, plotHeight, normalization, i
203
197
  width: step,
204
198
  height: plotHeight,
205
199
  anchorX: bandX + step / 2,
206
- anchorY: groupTopY,
207
- isEmpty: !hasValue
200
+ anchorY: groupTopY
208
201
  });
209
202
  });
210
203
  return {
211
204
  rects,
212
205
  bands,
213
- valueScale,
214
- baselineY
206
+ valueScale
215
207
  };
216
208
  }
217
209
  //#endregion
218
- export { buildBarChartGeometry, getBarSpacing, getBarValueDomain, roundedRectPath };
210
+ export { buildBarChartGeometry, getBarValueDomain, roundedRectPath };
@@ -0,0 +1,25 @@
1
+ import type { BarChartGroupDatum, ChartLegendItem, ChartSeriesStyles } from "./types";
2
+ import type { SeriesColorResolver } from "./useSeriesColorResolver";
3
+ /**
4
+ * Legend entries for a bar chart.
5
+ *
6
+ * A pure walk over the data, kept out of the component because it is the one
7
+ * legend in the DLS that cannot be a `.map()` over the `series` record: a bar's
8
+ * *components* are a second dimension with no record of their own, so a stacked
9
+ * chart's legend has to be recovered from the data itself.
10
+ */
11
+ interface GetBarLegendItemsArgs<TMetric extends string, TComponentKey extends string> {
12
+ /**
13
+ * Whether the chart as a whole is stacked.
14
+ *
15
+ * Decides which dimension the legend names, and must match the flag the
16
+ * geometry coloured with — a legend naming metrics beside bars coloured by
17
+ * component describes a chart that is not on screen.
18
+ */
19
+ isStacked: boolean;
20
+ series: ChartSeriesStyles<TMetric>;
21
+ groups: BarChartGroupDatum<TMetric, TComponentKey>[];
22
+ colors: SeriesColorResolver<TMetric>;
23
+ }
24
+ export declare function getBarLegendItems<TMetric extends string, TComponentKey extends string>({ isStacked, series, groups, colors, }: GetBarLegendItemsArgs<TMetric, TComponentKey>): ChartLegendItem[];
25
+ export {};
@@ -0,0 +1,23 @@
1
+ //#region src/charts/barChartLegend.ts
2
+ function getBarLegendItems({ isStacked, series, groups, colors }) {
3
+ if (!isStacked) return colors.keys.map((metric) => ({
4
+ key: metric,
5
+ label: series[metric].label,
6
+ color: colors.resolve(metric)
7
+ }));
8
+ const seen = /* @__PURE__ */ new Map();
9
+ for (const group of groups) for (const bar of group.bars) bar.components.forEach((component, componentIndex) => {
10
+ if (!seen.has(component.key)) seen.set(component.key, {
11
+ label: component.label,
12
+ index: componentIndex,
13
+ explicit: component.color
14
+ });
15
+ });
16
+ return [...seen.entries()].map(([key, { label, index, explicit }]) => ({
17
+ key,
18
+ label,
19
+ color: colors.resolveByIndex(index, explicit)
20
+ }));
21
+ }
22
+ //#endregion
23
+ export { getBarLegendItems };
@@ -1,5 +1,5 @@
1
1
  import { ChartValueUnit } from "./types";
2
- import type { ChartLabelFormatter, ChartValueFormatter } from "./types";
2
+ import type { ChartLabelFormatter, ChartPlotTooltipModel, ChartValueFormatter } from "./types";
3
3
  /**
4
4
  * Format a value with a built-in unit.
5
5
  *
@@ -17,3 +17,11 @@ export declare function formatChartValue(value: number, unit: ChartValueUnit): s
17
17
  export declare function resolveValueFormatter(valueFormatter?: ChartValueFormatter, unit?: ChartValueUnit): ChartValueFormatter;
18
18
  /** Pick the effective label formatter, defaulting to identity. */
19
19
  export declare function resolveLabelFormatter(labelFormatter?: ChartLabelFormatter): ChartLabelFormatter;
20
+ /**
21
+ * Flatten a tooltip model into one sentence, for a screen reader.
22
+ *
23
+ * Derived from the tooltip rather than written separately so the two cannot
24
+ * drift: whatever a sighted reader sees on hover is what a keyboard reader
25
+ * hears on focus, by construction.
26
+ */
27
+ export declare function describeTooltipModel(model: ChartPlotTooltipModel): string;
@@ -55,5 +55,17 @@ function resolveValueFormatter(valueFormatter, unit) {
55
55
  function resolveLabelFormatter(labelFormatter) {
56
56
  return labelFormatter ?? ((label) => label);
57
57
  }
58
+ /**
59
+ * Flatten a tooltip model into one sentence, for a screen reader.
60
+ *
61
+ * Derived from the tooltip rather than written separately so the two cannot
62
+ * drift: whatever a sighted reader sees on hover is what a keyboard reader
63
+ * hears on focus, by construction.
64
+ */
65
+ function describeTooltipModel(model) {
66
+ if (model.rows.length === 0) return model.label;
67
+ const rows = model.rows.map((row) => `${row.label} ${row.formattedValue}`).join(", ");
68
+ return `${model.label}: ${rows}`;
69
+ }
58
70
  //#endregion
59
- export { formatChartValue, resolveLabelFormatter, resolveValueFormatter };
71
+ export { describeTooltipModel, formatChartValue, resolveLabelFormatter, resolveValueFormatter };
@@ -5,14 +5,14 @@ import type { ChartMargin, ChartValueDomain, ChartValueFormatter } from "./types
5
5
  * Pure arithmetic with no React and no theme access, so it can be unit-reasoned
6
6
  * about and stays safe for Linaria's build-time evaluation.
7
7
  */
8
- export declare const clamp: (value: number, min: number, max: number) => number;
8
+ export declare function clamp(value: number, min: number, max: number): number;
9
9
  /**
10
10
  * Coerce a datum value into something safe to feed a scale.
11
11
  *
12
12
  * A single `NaN` or `Infinity` in consumer data otherwise propagates into every
13
13
  * path's `d` attribute and blanks the entire chart rather than one mark.
14
14
  */
15
- export declare const sanitizeValue: (value: number | undefined | null) => number;
15
+ export declare function sanitizeValue(value: number | undefined | null): number;
16
16
  /**
17
17
  * Merge a partial margin over the defaults.
18
18
  *
@@ -20,10 +20,16 @@ export declare const sanitizeValue: (value: number | undefined | null) => number
20
20
  * an explicit `undefined` overwrites the default with `undefined` rather than
21
21
  * falling back to it, which is easy to trip over when the partial is assembled
22
22
  * from separate optional values.
23
+ *
24
+ * `defaults` exists because the cartesian default reserves axis gutters a
25
+ * radial chart has no use for. Taking it as a parameter keeps the merge
26
+ * per-field: a chart that substituted its own defaults with `margin ?? OWN`
27
+ * would silently fall back to the *cartesian* defaults the moment a caller
28
+ * passed a partial, handing an axis-less chart a 52px left gutter.
23
29
  */
24
- export declare const resolveMargin: (partial?: Partial<ChartMargin>) => ChartMargin;
30
+ export declare function resolveMargin(partial?: Partial<ChartMargin>, defaults?: ChartMargin): ChartMargin;
25
31
  /** Inner plot dimensions, never negative. */
26
- export declare const getPlotDimensions: (width: number, height: number, margin: ChartMargin) => {
32
+ export declare function getPlotDimensions(width: number, height: number, margin: ChartMargin): {
27
33
  plotWidth: number;
28
34
  plotHeight: number;
29
35
  };
@@ -40,14 +46,14 @@ export declare const getPlotDimensions: (width: number, height: number, margin:
40
46
  * title band is added on top of it rather than eating into it. Returns the input
41
47
  * untouched when there are no titles, so the common case allocates nothing.
42
48
  */
43
- export declare const addAxisTitleSpace: (partial: Partial<ChartMargin> | undefined, { hasValueTitle, hasCategoryTitle }: {
49
+ export declare function addAxisTitleSpace(partial: Partial<ChartMargin> | undefined, { hasValueTitle, hasCategoryTitle }: {
44
50
  hasValueTitle: boolean;
45
51
  hasCategoryTitle: boolean;
46
- }) => Partial<ChartMargin> | undefined;
52
+ }): Partial<ChartMargin> | undefined;
47
53
  /**
48
- * Build the value scale from a signed domain.
54
+ * Turn a raw data extent into the domain the axis and the marks both use.
49
55
  *
50
- * Two behaviours worth knowing, both fixing defects in the V1 charts:
56
+ * Applies the caller's override, then three behaviours worth knowing:
51
57
  *
52
58
  * - **Zero is always in the domain.** Bars need a baseline to grow from, and
53
59
  * without this a negative value maps outside the range and renders as a
@@ -56,29 +62,59 @@ export declare const addAxisTitleSpace: (partial: Partial<ChartMargin> | undefin
56
62
  * constant `0.5` when the domain has no extent, so empty or all-zero data
57
63
  * would otherwise map every value to the middle of the plot and strand the
58
64
  * baseline halfway up.
65
+ * - **`nice()` rounds the ends outward** to values a reader would choose.
66
+ *
67
+ * Split out of {@link buildValueScale} so the domain is computed exactly once
68
+ * per chart and then handed to both the tick list and the geometry. Building it
69
+ * twice — once at a throwaway range to get ticks, once at the real range to
70
+ * place marks — leaves two `nice()` calls that must agree forever.
71
+ *
72
+ * `nice()` deliberately takes **no argument**. The default count is 10, not the
73
+ * five ticks {@link DEFAULT_VALUE_TICK_COUNT} asks for, and nicing to the tick
74
+ * count instead would change the axis extent on every existing chart.
75
+ */
76
+ export declare function resolveValueDomain(raw: [number, number], override?: Partial<ChartValueDomain>): [number, number];
77
+ /**
78
+ * Linear scale over an already-resolved domain.
79
+ *
80
+ * Does no nicing and no zero-forcing — {@link resolveValueDomain} has done
81
+ * both. Takes the domain rather than raw data so the same numbers drive the
82
+ * axis, the ticks and every mark.
59
83
  */
60
- export declare const buildValueScale: ([min, max]: [number, number], range: [number, number]) => import("d3-scale").ScaleLinear<number, number, never>;
61
- /** Apply an explicit domain override on top of a computed domain. */
62
- export declare const applyDomainOverride: (computed: [number, number], override?: Partial<ChartValueDomain>) => [number, number];
63
- /** Band scale for categories, used by the bar chart. */
64
- export declare const buildCategoryBandScale: (labels: string[], range: [number, number], paddingInner: number, paddingOuter: number) => import("d3-scale").ScaleBand<string>;
84
+ export declare function buildValueScale(domain: [number, number], range: [number, number]): import("d3-scale").ScaleLinear<number, number, never>;
65
85
  /** Point scale for categories, used by the line chart. */
66
- export declare const buildCategoryPointScale: (labels: string[], range: [number, number], padding?: number) => import("d3-scale").ScalePoint<string>;
86
+ export declare function buildCategoryPointScale(labels: string[], range: [number, number], padding?: number): import("d3-scale").ScalePoint<string>;
67
87
  /**
68
- * Estimate the left margin needed to fit the widest formatted tick label.
88
+ * Left margin needed to fit the widest formatted tick label.
69
89
  *
70
90
  * Estimated from character count rather than measured from the DOM, mirroring
71
- * `estimateColumnWidth` in `table/utils.ts`, so it works during server
72
- * rendering. Clamped to at least the default margin, so it can only widen a
73
- * chart, never shrink one.
91
+ * `estimateColumnWidth` in `table/utils.ts`, so it works during server rendering
92
+ * and build-time evaluation. The estimate only has to be close: tick labels
93
+ * right-align against the plot and ellipsize, so an underestimate truncates one
94
+ * label rather than breaking the layout, and {@link MAX_AUTO_MARGIN_LEFT} caps
95
+ * what a pathological formatter can take from the plot.
96
+ *
97
+ * Rounded up to {@link AUTO_MARGIN_LEFT_STEP} rather than used raw — see that
98
+ * constant for why an exact fit is the wrong target.
99
+ *
100
+ * Safe to call before the chart has been measured: `nice()` and `ticks()` depend
101
+ * on the domain and the requested count, never on the scale's range, so the tick
102
+ * values here are the same ones the axis renders later.
74
103
  */
75
- export declare const estimateValueAxisMargin: (ticks: number[], formatter: ChartValueFormatter) => number;
104
+ export declare function estimateValueAxisMargin(ticks: number[], formatter: ChartValueFormatter): number;
76
105
  /**
77
106
  * How many categories to skip between rendered axis labels.
78
107
  *
79
108
  * Returns 1 when every label fits, so a chart that is not crowded renders
80
109
  * exactly as it would with no skipping at all.
81
110
  */
82
- export declare const getCategoryLabelStride: (labels: string[], step: number) => number;
83
- /** Tick values for the value axis, honoring an explicit tick count. */
84
- export declare const getValueTicks: (scale: ReturnType<typeof buildValueScale>, override?: Partial<ChartValueDomain>) => number[];
111
+ export declare function getCategoryLabelStride(labels: string[], step: number): number;
112
+ /**
113
+ * Tick values for a resolved domain, honoring an explicit tick count.
114
+ *
115
+ * Needs no dimensions, which is what breaks the apparent circularity in the
116
+ * layout: the left margin must fit the tick labels, the margin decides the plot
117
+ * size, and the plot size builds the scale. Tick *values* fall out of the domain
118
+ * alone, so they can be known before anything is measured.
119
+ */
120
+ export declare function getValueTicks(domain: [number, number], override?: Partial<ChartValueDomain>): number[];
@@ -1,5 +1,5 @@
1
1
  import { CAPTION_CHAR_WIDTH_PX, DEFAULT_CHART_MARGIN } from "./constants.js";
2
- import { scaleBand, scaleLinear, scalePoint } from "d3-scale";
2
+ import { scaleLinear, scalePoint } from "d3-scale";
3
3
  //#region src/charts/chartScales.ts
4
4
  /**
5
5
  * Scale construction and axis sizing for the chart components.
@@ -7,14 +7,18 @@ import { scaleBand, scaleLinear, scalePoint } from "d3-scale";
7
7
  * Pure arithmetic with no React and no theme access, so it can be unit-reasoned
8
8
  * about and stays safe for Linaria's build-time evaluation.
9
9
  */
10
- var clamp = (value, min, max) => Math.min(Math.max(value, min), max);
10
+ function clamp(value, min, max) {
11
+ return Math.min(Math.max(value, min), max);
12
+ }
11
13
  /**
12
14
  * Coerce a datum value into something safe to feed a scale.
13
15
  *
14
16
  * A single `NaN` or `Infinity` in consumer data otherwise propagates into every
15
17
  * path's `d` attribute and blanks the entire chart rather than one mark.
16
18
  */
17
- var sanitizeValue = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
19
+ function sanitizeValue(value) {
20
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
21
+ }
18
22
  /**
19
23
  * Merge a partial margin over the defaults.
20
24
  *
@@ -22,18 +26,28 @@ var sanitizeValue = (value) => typeof value === "number" && Number.isFinite(valu
22
26
  * an explicit `undefined` overwrites the default with `undefined` rather than
23
27
  * falling back to it, which is easy to trip over when the partial is assembled
24
28
  * from separate optional values.
29
+ *
30
+ * `defaults` exists because the cartesian default reserves axis gutters a
31
+ * radial chart has no use for. Taking it as a parameter keeps the merge
32
+ * per-field: a chart that substituted its own defaults with `margin ?? OWN`
33
+ * would silently fall back to the *cartesian* defaults the moment a caller
34
+ * passed a partial, handing an axis-less chart a 52px left gutter.
25
35
  */
26
- var resolveMargin = (partial) => ({
27
- top: partial?.top ?? DEFAULT_CHART_MARGIN.top,
28
- right: partial?.right ?? DEFAULT_CHART_MARGIN.right,
29
- bottom: partial?.bottom ?? DEFAULT_CHART_MARGIN.bottom,
30
- left: partial?.left ?? DEFAULT_CHART_MARGIN.left
31
- });
36
+ function resolveMargin(partial, defaults = DEFAULT_CHART_MARGIN) {
37
+ return {
38
+ top: partial?.top ?? defaults.top,
39
+ right: partial?.right ?? defaults.right,
40
+ bottom: partial?.bottom ?? defaults.bottom,
41
+ left: partial?.left ?? defaults.left
42
+ };
43
+ }
32
44
  /** Inner plot dimensions, never negative. */
33
- var getPlotDimensions = (width, height, margin) => ({
34
- plotWidth: Math.max(0, width - margin.left - margin.right),
35
- plotHeight: Math.max(0, height - margin.top - margin.bottom)
36
- });
45
+ function getPlotDimensions(width, height, margin) {
46
+ return {
47
+ plotWidth: Math.max(0, width - margin.left - margin.right),
48
+ plotHeight: Math.max(0, height - margin.top - margin.bottom)
49
+ };
50
+ }
37
51
  /**
38
52
  * Widen a margin to leave room for axis titles.
39
53
  *
@@ -47,18 +61,18 @@ var getPlotDimensions = (width, height, margin) => ({
47
61
  * title band is added on top of it rather than eating into it. Returns the input
48
62
  * untouched when there are no titles, so the common case allocates nothing.
49
63
  */
50
- var addAxisTitleSpace = (partial, { hasValueTitle, hasCategoryTitle }) => {
64
+ function addAxisTitleSpace(partial, { hasValueTitle, hasCategoryTitle }) {
51
65
  if (!hasValueTitle && !hasCategoryTitle) return partial;
52
66
  return {
53
67
  ...partial,
54
- left: (partial?.left ?? DEFAULT_CHART_MARGIN.left) + (hasValueTitle ? 18 : 0),
68
+ left: (partial?.left ?? DEFAULT_CHART_MARGIN.left) + (hasValueTitle ? 26 : 0),
55
69
  bottom: (partial?.bottom ?? DEFAULT_CHART_MARGIN.bottom) + (hasCategoryTitle ? 18 : 0)
56
70
  };
57
- };
71
+ }
58
72
  /**
59
- * Build the value scale from a signed domain.
73
+ * Turn a raw data extent into the domain the axis and the marks both use.
60
74
  *
61
- * Two behaviours worth knowing, both fixing defects in the V1 charts:
75
+ * Applies the caller's override, then three behaviours worth knowing:
62
76
  *
63
77
  * - **Zero is always in the domain.** Bars need a baseline to grow from, and
64
78
  * without this a negative value maps outside the range and renders as a
@@ -67,45 +81,85 @@ var addAxisTitleSpace = (partial, { hasValueTitle, hasCategoryTitle }) => {
67
81
  * constant `0.5` when the domain has no extent, so empty or all-zero data
68
82
  * would otherwise map every value to the middle of the plot and strand the
69
83
  * baseline halfway up.
84
+ * - **`nice()` rounds the ends outward** to values a reader would choose.
85
+ *
86
+ * Split out of {@link buildValueScale} so the domain is computed exactly once
87
+ * per chart and then handed to both the tick list and the geometry. Building it
88
+ * twice — once at a throwaway range to get ticks, once at the real range to
89
+ * place marks — leaves two `nice()` calls that must agree forever.
90
+ *
91
+ * `nice()` deliberately takes **no argument**. The default count is 10, not the
92
+ * five ticks {@link DEFAULT_VALUE_TICK_COUNT} asks for, and nicing to the tick
93
+ * count instead would change the axis extent on every existing chart.
70
94
  */
71
- var buildValueScale = ([min, max], range) => {
95
+ function resolveValueDomain(raw, override) {
96
+ const min = override?.min ?? raw[0];
97
+ const max = override?.max ?? raw[1];
72
98
  const lo = Math.min(0, min);
73
99
  const hi = Math.max(0, max);
74
- return scaleLinear().domain([lo, lo === hi ? hi + 1 : hi]).nice().range(range);
75
- };
76
- /** Apply an explicit domain override on top of a computed domain. */
77
- var applyDomainOverride = (computed, override) => [override?.min ?? computed[0], override?.max ?? computed[1]];
78
- /** Band scale for categories, used by the bar chart. */
79
- var buildCategoryBandScale = (labels, range, paddingInner, paddingOuter) => scaleBand().domain(labels).range(range).paddingInner(paddingInner).paddingOuter(paddingOuter);
100
+ const [niceLo, niceHi] = scaleLinear().domain([lo, lo === hi ? hi + 1 : hi]).nice().domain();
101
+ return [niceLo, niceHi];
102
+ }
103
+ /**
104
+ * Linear scale over an already-resolved domain.
105
+ *
106
+ * Does no nicing and no zero-forcing — {@link resolveValueDomain} has done
107
+ * both. Takes the domain rather than raw data so the same numbers drive the
108
+ * axis, the ticks and every mark.
109
+ */
110
+ function buildValueScale(domain, range) {
111
+ return scaleLinear().domain(domain).range(range);
112
+ }
80
113
  /** Point scale for categories, used by the line chart. */
81
- var buildCategoryPointScale = (labels, range, padding = 0) => scalePoint().domain(labels).range(range).padding(padding);
114
+ function buildCategoryPointScale(labels, range, padding = 0) {
115
+ return scalePoint().domain(labels).range(range).padding(padding);
116
+ }
82
117
  /**
83
- * Estimate the left margin needed to fit the widest formatted tick label.
118
+ * Left margin needed to fit the widest formatted tick label.
84
119
  *
85
120
  * Estimated from character count rather than measured from the DOM, mirroring
86
- * `estimateColumnWidth` in `table/utils.ts`, so it works during server
87
- * rendering. Clamped to at least the default margin, so it can only widen a
88
- * chart, never shrink one.
121
+ * `estimateColumnWidth` in `table/utils.ts`, so it works during server rendering
122
+ * and build-time evaluation. The estimate only has to be close: tick labels
123
+ * right-align against the plot and ellipsize, so an underestimate truncates one
124
+ * label rather than breaking the layout, and {@link MAX_AUTO_MARGIN_LEFT} caps
125
+ * what a pathological formatter can take from the plot.
126
+ *
127
+ * Rounded up to {@link AUTO_MARGIN_LEFT_STEP} rather than used raw — see that
128
+ * constant for why an exact fit is the wrong target.
129
+ *
130
+ * Safe to call before the chart has been measured: `nice()` and `ticks()` depend
131
+ * on the domain and the requested count, never on the scale's range, so the tick
132
+ * values here are the same ones the axis renders later.
89
133
  */
90
- var estimateValueAxisMargin = (ticks, formatter) => {
134
+ function estimateValueAxisMargin(ticks, formatter) {
91
135
  let widest = 0;
92
136
  for (const tick of ticks) widest = Math.max(widest, formatter(tick).length * CAPTION_CHAR_WIDTH_PX);
93
- return clamp(Math.ceil(widest) + 12, DEFAULT_CHART_MARGIN.left, 96);
94
- };
137
+ const needed = Math.ceil(widest) + 12;
138
+ return clamp(Math.ceil(needed / 8) * 8, 32, 96);
139
+ }
95
140
  /**
96
141
  * How many categories to skip between rendered axis labels.
97
142
  *
98
143
  * Returns 1 when every label fits, so a chart that is not crowded renders
99
144
  * exactly as it would with no skipping at all.
100
145
  */
101
- var getCategoryLabelStride = (labels, step) => {
146
+ function getCategoryLabelStride(labels, step) {
102
147
  if (labels.length === 0 || step <= 0) return 1;
103
148
  let widest = 0;
104
149
  for (const label of labels) widest = Math.max(widest, label.length * CAPTION_CHAR_WIDTH_PX);
105
150
  const required = widest + 8;
106
151
  return required <= step ? 1 : Math.ceil(required / step);
107
- };
108
- /** Tick values for the value axis, honoring an explicit tick count. */
109
- var getValueTicks = (scale, override) => scale.ticks(override?.tickCount ?? 5);
152
+ }
153
+ /**
154
+ * Tick values for a resolved domain, honoring an explicit tick count.
155
+ *
156
+ * Needs no dimensions, which is what breaks the apparent circularity in the
157
+ * layout: the left margin must fit the tick labels, the margin decides the plot
158
+ * size, and the plot size builds the scale. Tick *values* fall out of the domain
159
+ * alone, so they can be known before anything is measured.
160
+ */
161
+ function getValueTicks(domain, override) {
162
+ return scaleLinear().domain(domain).ticks(override?.tickCount ?? 5);
163
+ }
110
164
  //#endregion
111
- export { addAxisTitleSpace, applyDomainOverride, buildCategoryBandScale, buildCategoryPointScale, buildValueScale, clamp, estimateValueAxisMargin, getCategoryLabelStride, getPlotDimensions, getValueTicks, resolveMargin, sanitizeValue };
165
+ export { addAxisTitleSpace, buildCategoryPointScale, buildValueScale, clamp, estimateValueAxisMargin, getCategoryLabelStride, getPlotDimensions, getValueTicks, resolveMargin, resolveValueDomain, sanitizeValue };