@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.
- package/dist/charts/BarChart.d.ts +3 -3
- package/dist/charts/BarChart.js +68 -104
- package/dist/charts/ChartCategoryTargets.d.ts +27 -0
- package/dist/charts/ChartCategoryTargets.js +75 -0
- package/dist/charts/ChartFrame.d.ts +34 -6
- package/dist/charts/ChartFrame.js +72 -15
- package/dist/charts/ChartPrimitives.d.ts +35 -0
- package/dist/charts/ChartPrimitives.js +75 -38
- package/dist/charts/ChartTooltip.d.ts +3 -3
- package/dist/charts/ChartTooltip.js +2 -2
- package/dist/charts/LineChart.js +47 -62
- package/dist/charts/PieChart.js +48 -26
- package/dist/charts/barChartGeometry.d.ts +66 -33
- package/dist/charts/barChartGeometry.js +7 -15
- package/dist/charts/barChartLegend.d.ts +25 -0
- package/dist/charts/barChartLegend.js +23 -0
- package/dist/charts/chartFormat.d.ts +9 -1
- package/dist/charts/chartFormat.js +13 -1
- package/dist/charts/chartScales.d.ts +58 -22
- package/dist/charts/chartScales.js +92 -38
- package/dist/charts/constants.d.ts +22 -6
- package/dist/charts/constants.js +22 -1
- package/dist/charts/lineChartGeometry.d.ts +53 -15
- package/dist/charts/lineChartGeometry.js +6 -15
- package/dist/charts/pieChartGeometry.d.ts +20 -8
- package/dist/charts/pieChartGeometry.js +3 -13
- package/dist/charts/types.d.ts +51 -145
- package/dist/charts/types.js +22 -3
- package/dist/charts/useCartesianChartLayout.d.ts +70 -0
- package/dist/charts/useCartesianChartLayout.js +56 -0
- package/dist/charts/useChartDimensions.d.ts +10 -3
- package/dist/charts/useChartDimensions.js +14 -4
- package/dist/charts/useChartInteraction.d.ts +13 -8
- package/dist/charts/useChartInteraction.js +7 -11
- package/dist/charts/useSeriesColorResolver.d.ts +37 -0
- package/dist/charts/useSeriesColorResolver.js +27 -0
- package/dist/inputs/DateInput.js +3 -3
- package/dist/inputs/MultiSelectInput.d.ts +3 -2
- package/dist/inputs/MultiSelectInput.js +8 -8
- package/dist/inputs/SelectInput.d.ts +4 -8
- package/dist/inputs/SelectInput.js +14 -20
- package/dist/styles.css +13 -11
- 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 {
|
|
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,
|
|
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,
|
|
52
|
-
groups: BarChartGroupDatum<TMetric,
|
|
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
|
-
|
|
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,
|
|
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 {
|
|
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,
|
|
115
|
-
const valueScale = buildValueScale(
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
30
|
+
export declare function resolveMargin(partial?: Partial<ChartMargin>, defaults?: ChartMargin): ChartMargin;
|
|
25
31
|
/** Inner plot dimensions, never negative. */
|
|
26
|
-
export declare
|
|
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
|
|
49
|
+
export declare function addAxisTitleSpace(partial: Partial<ChartMargin> | undefined, { hasValueTitle, hasCategoryTitle }: {
|
|
44
50
|
hasValueTitle: boolean;
|
|
45
51
|
hasCategoryTitle: boolean;
|
|
46
|
-
})
|
|
52
|
+
}): Partial<ChartMargin> | undefined;
|
|
47
53
|
/**
|
|
48
|
-
*
|
|
54
|
+
* Turn a raw data extent into the domain the axis and the marks both use.
|
|
49
55
|
*
|
|
50
|
-
*
|
|
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
|
|
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
|
|
86
|
+
export declare function buildCategoryPointScale(labels: string[], range: [number, number], padding?: number): import("d3-scale").ScalePoint<string>;
|
|
67
87
|
/**
|
|
68
|
-
*
|
|
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
|
-
*
|
|
73
|
-
*
|
|
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
|
|
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
|
|
83
|
-
/**
|
|
84
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
*
|
|
73
|
+
* Turn a raw data extent into the domain the axis and the marks both use.
|
|
60
74
|
*
|
|
61
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
114
|
+
function buildCategoryPointScale(labels, range, padding = 0) {
|
|
115
|
+
return scalePoint().domain(labels).range(range).padding(padding);
|
|
116
|
+
}
|
|
82
117
|
/**
|
|
83
|
-
*
|
|
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
|
-
*
|
|
88
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
109
|
-
|
|
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,
|
|
165
|
+
export { addAxisTitleSpace, buildCategoryPointScale, buildValueScale, clamp, estimateValueAxisMargin, getCategoryLabelStride, getPlotDimensions, getValueTicks, resolveMargin, resolveValueDomain, sanitizeValue };
|