@scalewing/react 1.38.0 → 1.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +155 -1
- package/dist/calendar-month.d.ts +6 -0
- package/dist/calendar-month.js +15 -0
- package/dist/chart-axis.d.ts +61 -0
- package/dist/chart-axis.js +83 -0
- package/dist/chart-dates.d.ts +6 -0
- package/dist/chart-dates.js +15 -0
- package/dist/chart-metrics.d.ts +40 -0
- package/dist/chart-metrics.js +48 -0
- package/dist/chart-scale.d.ts +48 -0
- package/dist/chart-scale.js +116 -0
- package/dist/chart-table-rows.d.ts +8 -0
- package/dist/chart-table-rows.js +1 -0
- package/dist/column-chart-bars.d.ts +38 -0
- package/dist/column-chart-bars.js +82 -0
- package/dist/column-chart-callouts.d.ts +48 -0
- package/dist/column-chart-callouts.js +54 -0
- package/dist/column-chart-input.d.ts +40 -0
- package/dist/column-chart-input.js +60 -0
- package/dist/column-chart-layout.d.ts +63 -0
- package/dist/column-chart-layout.js +97 -0
- package/dist/components/BarChart.d.ts +1 -1
- package/dist/components/BarChart.js +1 -1
- package/dist/components/ChartLegend.d.ts +38 -0
- package/dist/components/ChartLegend.js +32 -0
- package/dist/components/ColumnChart.d.ts +72 -0
- package/dist/components/ColumnChart.js +71 -0
- package/dist/components/DataBar.d.ts +6 -6
- package/dist/components/DateRangeButton.d.ts +65 -0
- package/dist/components/DateRangeButton.js +54 -0
- package/dist/components/LineChart.d.ts +83 -0
- package/dist/components/LineChart.js +77 -0
- package/dist/components/ListGroup.d.ts +33 -0
- package/dist/components/ListGroup.js +19 -0
- package/dist/components/ListRow.d.ts +51 -0
- package/dist/components/ListRow.js +59 -0
- package/dist/components/NavCount.d.ts +21 -0
- package/dist/components/NavCount.js +23 -0
- package/dist/components/SectionNav.js +1 -8
- package/dist/components/StatTile.d.ts +11 -4
- package/dist/components/StatTile.js +11 -4
- package/dist/components/{date-field → calendar}/CalendarGrid.d.ts +17 -2
- package/dist/components/{date-field → calendar}/CalendarGrid.js +11 -3
- package/dist/components/calendar/CalendarPopover.d.ts +25 -0
- package/dist/components/calendar/CalendarPopover.js +22 -0
- package/dist/components/calendar/MonthStep.d.ts +16 -0
- package/dist/components/calendar/MonthStep.js +17 -0
- package/dist/components/calendar/use-grid-focus.d.ts +18 -0
- package/dist/components/calendar/use-grid-focus.js +35 -0
- package/dist/components/calendar/wrap-tab.d.ts +7 -0
- package/dist/components/calendar/wrap-tab.js +29 -0
- package/dist/components/chart/ChartTable.d.ts +16 -0
- package/dist/components/chart/ChartTable.js +12 -0
- package/dist/components/chart/ChartValueAxis.d.ts +14 -0
- package/dist/components/chart/ChartValueAxis.js +11 -0
- package/dist/components/chart/use-chart-width.d.ts +8 -0
- package/dist/components/chart/use-chart-width.js +34 -0
- package/dist/components/column-chart/ChartCalloutList.d.ts +16 -0
- package/dist/components/column-chart/ChartCalloutList.js +13 -0
- package/dist/components/column-chart/ColumnChartDrawing.d.ts +17 -0
- package/dist/components/column-chart/ColumnChartDrawing.js +18 -0
- package/dist/components/date-field/CalendarDialog.js +9 -67
- package/dist/components/date-field/CalendarHeader.js +2 -11
- package/dist/components/date-range/DateRangePopover.d.ts +7 -0
- package/dist/components/date-range/DateRangePopover.js +17 -0
- package/dist/components/date-range/DateRangeSheet.d.ts +7 -0
- package/dist/components/date-range/DateRangeSheet.js +15 -0
- package/dist/components/date-range/RangeMonths.d.ts +28 -0
- package/dist/components/date-range/RangeMonths.js +50 -0
- package/dist/components/date-range/RangePresets.d.ts +16 -0
- package/dist/components/date-range/RangePresets.js +10 -0
- package/dist/components/date-range/RangeSummary.d.ts +15 -0
- package/dist/components/date-range/RangeSummary.js +11 -0
- package/dist/components/date-range/range-panel-props.d.ts +31 -0
- package/dist/components/date-range/range-panel-props.js +1 -0
- package/dist/components/date-range/use-range-popup.d.ts +21 -0
- package/dist/components/date-range/use-range-popup.js +53 -0
- package/dist/components/line-chart/LineChartDrawing.d.ts +17 -0
- package/dist/components/line-chart/LineChartDrawing.js +16 -0
- package/dist/components/link-press.d.ts +7 -0
- package/dist/components/link-press.js +13 -0
- package/dist/components/list/ListRowAccessoryMark.d.ts +5 -0
- package/dist/components/list/ListRowAccessoryMark.js +7 -0
- package/dist/components/list/ListRowContent.d.ts +20 -0
- package/dist/components/list/ListRowContent.js +10 -0
- package/dist/components/list/list-row-container.d.ts +5 -0
- package/dist/components/list/list-row-container.js +6 -0
- package/dist/components/use-media-query.d.ts +5 -0
- package/dist/components/use-media-query.js +18 -0
- package/dist/css/chart-marks.d.ts +10 -0
- package/dist/css/chart-marks.js +15 -0
- package/dist/css/chart-tones.d.ts +68 -0
- package/dist/css/chart-tones.js +130 -0
- package/dist/css/chevron.d.ts +6 -0
- package/dist/css/chevron.js +14 -0
- package/dist/css/{css-chart.d.ts → css-bar-chart.d.ts} +2 -2
- package/dist/css/{css-chart.js → css-bar-chart.js} +2 -2
- package/dist/css/css-button-group-attached.js +8 -4
- package/dist/css/css-button-group-track.js +1 -1
- package/dist/css/css-chart-legend.d.ts +5 -0
- package/dist/css/css-chart-legend.js +102 -0
- package/dist/css/css-chart-plot.d.ts +2 -0
- package/dist/css/css-chart-plot.js +170 -0
- package/dist/css/css-checkbox.js +2 -2
- package/dist/css/css-components.js +23 -2
- package/dist/css/css-count-pill.d.ts +2 -0
- package/dist/css/css-count-pill.js +54 -0
- package/dist/css/css-data-bar.d.ts +4 -9
- package/dist/css/css-data-bar.js +15 -31
- package/dist/css/css-date-field.js +3 -2
- package/dist/css/css-date-range-presets.d.ts +2 -0
- package/dist/css/css-date-range-presets.js +109 -0
- package/dist/css/css-date-range.d.ts +2 -0
- package/dist/css/css-date-range.js +197 -0
- package/dist/css/css-line-chart.d.ts +2 -0
- package/dist/css/css-line-chart.js +31 -0
- package/dist/css/css-list-row.d.ts +2 -0
- package/dist/css/css-list-row.js +193 -0
- package/dist/css/css-list.d.ts +3 -0
- package/dist/css/css-list.js +85 -0
- package/dist/css/css-scroll-region.d.ts +18 -0
- package/dist/css/css-scroll-region.js +41 -0
- package/dist/css/css-section-nav-filled.js +2 -25
- package/dist/css/css-stat-tile.d.ts +7 -0
- package/dist/css/css-stat-tile.js +16 -0
- package/dist/css/css-steps.js +2 -2
- package/dist/css/css-table-bounded-edges.js +4 -27
- package/dist/css/range-band.d.ts +2 -0
- package/dist/css/range-band.js +7 -0
- package/dist/css/stacking.js +2 -2
- package/dist/css/stylesheet.js +16 -2
- package/dist/date-range-draft.d.ts +26 -0
- package/dist/date-range-draft.js +36 -0
- package/dist/date-range-input.d.ts +26 -0
- package/dist/date-range-input.js +58 -0
- package/dist/date-range-labels.d.ts +22 -0
- package/dist/date-range-labels.js +26 -0
- package/dist/date-range-view.d.ts +12 -0
- package/dist/date-range-view.js +29 -0
- package/dist/date-range-words.d.ts +11 -0
- package/dist/date-range-words.js +40 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +9 -1
- package/dist/is-one-of.d.ts +5 -0
- package/dist/is-one-of.js +7 -0
- package/dist/line-chart-input.d.ts +39 -0
- package/dist/line-chart-input.js +70 -0
- package/dist/line-chart-layout.d.ts +68 -0
- package/dist/line-chart-layout.js +99 -0
- package/dist/line-chart-markers.d.ts +29 -0
- package/dist/line-chart-markers.js +57 -0
- package/dist/line-chart-path.d.ts +18 -0
- package/dist/line-chart-path.js +33 -0
- package/dist/line-chart-points.d.ts +6 -0
- package/dist/line-chart-points.js +8 -0
- package/dist/line-chart-table.d.ts +15 -0
- package/dist/line-chart-table.js +29 -0
- package/dist/list-options.d.ts +70 -0
- package/dist/list-options.js +77 -0
- package/dist/styles.css +726 -26
- package/package.json +1 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** How many steps a value axis aims for between its ends. */
|
|
2
|
+
export const chartScaleSteps = 4;
|
|
3
|
+
/** The multipliers a step takes within each power of ten. */
|
|
4
|
+
const niceMultipliers = [1, 2, 2.5, 5, 10];
|
|
5
|
+
/**
|
|
6
|
+
* The smallest round step (1, 2, 2.5 or 5 times a power of ten) that
|
|
7
|
+
* covers `span` in at most `steps` steps.
|
|
8
|
+
*/
|
|
9
|
+
export function niceStep(span, steps) {
|
|
10
|
+
if (!(span > 0))
|
|
11
|
+
return 1;
|
|
12
|
+
const raw = span / steps;
|
|
13
|
+
const power = 10 ** Math.floor(Math.log10(raw));
|
|
14
|
+
for (const multiplier of niceMultipliers) {
|
|
15
|
+
const step = multiplier * power;
|
|
16
|
+
if (step >= raw * (1 - 1e-9))
|
|
17
|
+
return step;
|
|
18
|
+
}
|
|
19
|
+
return 10 * power;
|
|
20
|
+
}
|
|
21
|
+
/** The least and greatest of some values, both stretched to reach zero. */
|
|
22
|
+
export function extentThroughZero(values) {
|
|
23
|
+
let min = 0;
|
|
24
|
+
let max = 0;
|
|
25
|
+
for (const value of values) {
|
|
26
|
+
if (value < min)
|
|
27
|
+
min = value;
|
|
28
|
+
if (value > max)
|
|
29
|
+
max = value;
|
|
30
|
+
}
|
|
31
|
+
return { min, max };
|
|
32
|
+
}
|
|
33
|
+
/** Rounds away the floating-point dust of a step multiple. */
|
|
34
|
+
function tidy(value, step) {
|
|
35
|
+
// toFixed takes at most 100 digits.
|
|
36
|
+
const decimals = Math.min(100, Math.max(0, -Math.floor(Math.log10(step)) + 2));
|
|
37
|
+
return Number(value.toFixed(decimals)) + 0;
|
|
38
|
+
}
|
|
39
|
+
function scaleFrom(step, below, above) {
|
|
40
|
+
const ticks = [];
|
|
41
|
+
for (let index = -below; index <= above; index += 1)
|
|
42
|
+
ticks.push(tidy(index * step, step));
|
|
43
|
+
return {
|
|
44
|
+
min: tidy(-below * step, step),
|
|
45
|
+
max: tidy(above * step, step),
|
|
46
|
+
step,
|
|
47
|
+
ticks,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Whole steps below and above zero for an extent at one step. */
|
|
51
|
+
function stepsAround(extent, step) {
|
|
52
|
+
return {
|
|
53
|
+
below: Math.ceil(-extent.min / step - 1e-9),
|
|
54
|
+
above: Math.ceil(extent.max / step - 1e-9),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** A scale for an extent aiming at a number of steps. */
|
|
58
|
+
function scaleAiming(extent, steps) {
|
|
59
|
+
const span = extent.max - extent.min;
|
|
60
|
+
if (span === 0)
|
|
61
|
+
return scaleFrom(1, 0, 1);
|
|
62
|
+
let step = niceStep(span, steps);
|
|
63
|
+
let { below, above } = stepsAround(extent, step);
|
|
64
|
+
// Rounding each side up can add a step; widen the step once if so.
|
|
65
|
+
if (below + above > steps + 1 && steps > 1) {
|
|
66
|
+
step = niceStep(span, steps - 1);
|
|
67
|
+
({ below, above } = stepsAround(extent, step));
|
|
68
|
+
}
|
|
69
|
+
return scaleFrom(step, Math.max(below, 0), Math.max(above, 0));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A value axis for an extent through zero: a round step, and as many whole
|
|
73
|
+
* steps either side of zero as the extent needs. An extent of only zero
|
|
74
|
+
* reads 0 to 1.
|
|
75
|
+
*/
|
|
76
|
+
export function chartScale(extent) {
|
|
77
|
+
return scaleAiming(extent, chartScaleSteps);
|
|
78
|
+
}
|
|
79
|
+
/** Whole steps a scale has below and above zero. */
|
|
80
|
+
function sides(scale) {
|
|
81
|
+
return {
|
|
82
|
+
below: Math.round(-scale.min / scale.step),
|
|
83
|
+
above: Math.round(scale.max / scale.step),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Two value axes drawn on one plot, such as columns on the left and a
|
|
88
|
+
* running total on the right: each keeps its own round step, and both have
|
|
89
|
+
* the same number of steps below and above zero, so zero and every grid
|
|
90
|
+
* line fall at the same height on both. When the two lean different ways
|
|
91
|
+
* (one mostly above zero, the other below), both take fewer, longer steps
|
|
92
|
+
* so the plot keeps about as many grid lines as one scale would.
|
|
93
|
+
*/
|
|
94
|
+
export function alignedChartScales(first, second) {
|
|
95
|
+
let aligned = [
|
|
96
|
+
chartScale(first),
|
|
97
|
+
chartScale(second),
|
|
98
|
+
];
|
|
99
|
+
for (let steps = chartScaleSteps; steps >= 1; steps -= 1) {
|
|
100
|
+
const a = scaleAiming(first, steps);
|
|
101
|
+
const b = scaleAiming(second, steps);
|
|
102
|
+
const below = Math.max(sides(a).below, sides(b).below);
|
|
103
|
+
const above = Math.max(sides(a).above, sides(b).above);
|
|
104
|
+
aligned = [
|
|
105
|
+
scaleFrom(a.step, below, above),
|
|
106
|
+
scaleFrom(b.step, below, above),
|
|
107
|
+
];
|
|
108
|
+
if (below + above <= chartScaleSteps + 1)
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
return aligned;
|
|
112
|
+
}
|
|
113
|
+
/** Where a value falls on a scale, as a share from its bottom (0) to top (1). */
|
|
114
|
+
export function scaleShare(value, scale) {
|
|
115
|
+
return (value - scale.min) / (scale.max - scale.min);
|
|
116
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type ChartPlot } from './chart-axis.js';
|
|
2
|
+
import { type ChartScale } from './chart-scale.js';
|
|
3
|
+
/** How a category's columns sit: one on another, or side by side. */
|
|
4
|
+
export declare const columnChartModes: readonly ["stacked", "grouped"];
|
|
5
|
+
export type ColumnChartMode = (typeof columnChartModes)[number];
|
|
6
|
+
/**
|
|
7
|
+
* One series' column in one category, in values: from `low` to `high`,
|
|
8
|
+
* and which of the category's slots it takes (always 0 when stacked).
|
|
9
|
+
*/
|
|
10
|
+
export type ColumnSpan = {
|
|
11
|
+
series: number;
|
|
12
|
+
category: number;
|
|
13
|
+
slot: number;
|
|
14
|
+
low: number;
|
|
15
|
+
high: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Each series' column in each category, in values. Stacked, the positive
|
|
19
|
+
* values stack up from zero and the negative ones down from it, each in
|
|
20
|
+
* series order; grouped, every value runs from zero in its own slot. A
|
|
21
|
+
* zero value has no column.
|
|
22
|
+
*/
|
|
23
|
+
export declare function columnSpans(series: readonly (readonly number[])[], categories: number, mode: ColumnChartMode): ColumnSpan[];
|
|
24
|
+
/** A drawn column: which series and category, and its box. */
|
|
25
|
+
export type PlacedColumn = {
|
|
26
|
+
series: number;
|
|
27
|
+
category: number;
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
};
|
|
33
|
+
/** Each span's box: a column, or its slot in the category's group. */
|
|
34
|
+
export declare function placeColumns(input: {
|
|
35
|
+
series: readonly (readonly number[])[];
|
|
36
|
+
categories: number;
|
|
37
|
+
mode: ColumnChartMode;
|
|
38
|
+
}, scale: ChartScale, plot: ChartPlot, band: number): PlacedColumn[];
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { valueY } from './chart-axis.js';
|
|
2
|
+
import { chartColumnGap } from './css/chart-marks.js';
|
|
3
|
+
/** How a category's columns sit: one on another, or side by side. */
|
|
4
|
+
export const columnChartModes = ['stacked', 'grouped'];
|
|
5
|
+
/**
|
|
6
|
+
* Each series' column in each category, in values. Stacked, the positive
|
|
7
|
+
* values stack up from zero and the negative ones down from it, each in
|
|
8
|
+
* series order; grouped, every value runs from zero in its own slot. A
|
|
9
|
+
* zero value has no column.
|
|
10
|
+
*/
|
|
11
|
+
export function columnSpans(series, categories, mode) {
|
|
12
|
+
const spans = [];
|
|
13
|
+
for (let category = 0; category < categories; category += 1) {
|
|
14
|
+
let up = 0;
|
|
15
|
+
let down = 0;
|
|
16
|
+
series.forEach((values, index) => {
|
|
17
|
+
const value = values[category];
|
|
18
|
+
if (value === 0)
|
|
19
|
+
return;
|
|
20
|
+
if (mode === 'grouped') {
|
|
21
|
+
spans.push({
|
|
22
|
+
series: index,
|
|
23
|
+
category,
|
|
24
|
+
slot: index,
|
|
25
|
+
low: Math.min(0, value),
|
|
26
|
+
high: Math.max(0, value),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
else if (value > 0) {
|
|
30
|
+
spans.push({
|
|
31
|
+
series: index,
|
|
32
|
+
category,
|
|
33
|
+
slot: 0,
|
|
34
|
+
low: up,
|
|
35
|
+
high: up + value,
|
|
36
|
+
});
|
|
37
|
+
up += value;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
spans.push({
|
|
41
|
+
series: index,
|
|
42
|
+
category,
|
|
43
|
+
slot: 0,
|
|
44
|
+
low: down + value,
|
|
45
|
+
high: down,
|
|
46
|
+
});
|
|
47
|
+
down += value;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return spans;
|
|
52
|
+
}
|
|
53
|
+
/** How much of a category's width its stacked column, or its group, takes. */
|
|
54
|
+
const stackedShare = 0.6;
|
|
55
|
+
const groupedShare = 0.72;
|
|
56
|
+
/** Each span's box: a column, or its slot in the category's group. */
|
|
57
|
+
export function placeColumns(input, scale, plot, band) {
|
|
58
|
+
const slots = input.mode === 'grouped' ? input.series.length : 1;
|
|
59
|
+
const group = band * (input.mode === 'grouped' ? groupedShare : stackedShare);
|
|
60
|
+
const slot = group / slots;
|
|
61
|
+
const gap = slots > 1 ? Math.min(chartColumnGap, slot / 4) : 0;
|
|
62
|
+
return columnSpans(input.series, input.categories, input.mode).map((span) => {
|
|
63
|
+
const top = valueY(span.high, scale, plot);
|
|
64
|
+
const bottom = valueY(span.low, scale, plot);
|
|
65
|
+
// A value too small to see still shows: a column at least 1 px tall,
|
|
66
|
+
// standing on zero (or hanging from it).
|
|
67
|
+
const height = Math.max(bottom - top, 1);
|
|
68
|
+
const y = span.low < 0 && span.high <= 0 ? top : bottom - height;
|
|
69
|
+
return {
|
|
70
|
+
series: span.series,
|
|
71
|
+
category: span.category,
|
|
72
|
+
x: plot.left +
|
|
73
|
+
band * span.category +
|
|
74
|
+
(band - group) / 2 +
|
|
75
|
+
slot * span.slot +
|
|
76
|
+
gap / 2,
|
|
77
|
+
y,
|
|
78
|
+
width: slot - gap,
|
|
79
|
+
height,
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A callout pinned above the plot: its words in a row at the top, beside a
|
|
3
|
+
* dashed leader that runs down from the row to the column it is about.
|
|
4
|
+
*/
|
|
5
|
+
export type PlacedCallout = {
|
|
6
|
+
/** Its place in the chart's `callouts`. */
|
|
7
|
+
order: number;
|
|
8
|
+
/** The category it is about. */
|
|
9
|
+
index: number;
|
|
10
|
+
text: string;
|
|
11
|
+
/** The leader's x, the category's centre. */
|
|
12
|
+
x: number;
|
|
13
|
+
/** The words' start (or end) and baseline. */
|
|
14
|
+
textX: number;
|
|
15
|
+
textY: number;
|
|
16
|
+
anchor: 'start' | 'end';
|
|
17
|
+
/** The leader runs from the row (`leaderStart`) down to `leaderEnd`. */
|
|
18
|
+
leaderStart: number;
|
|
19
|
+
leaderEnd: number;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Where a callout's words sit: after its leader in the first half of the
|
|
23
|
+
* chart and before it in the second, the side with more room. Words
|
|
24
|
+
* longer than that room are moved in so they stay inside the chart,
|
|
25
|
+
* crossing their leader.
|
|
26
|
+
*/
|
|
27
|
+
export declare function besideLeader(x: number, width: number, bounds: {
|
|
28
|
+
left: number;
|
|
29
|
+
right: number;
|
|
30
|
+
}): {
|
|
31
|
+
anchor: 'start' | 'end';
|
|
32
|
+
textX: number;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Sets each callout's words beside its leader (`besideLeader`) and gives it the first row from
|
|
36
|
+
* the top where it meets no other callout's words. Returns how many rows
|
|
37
|
+
* the callouts take.
|
|
38
|
+
*/
|
|
39
|
+
export declare function calloutRows(callouts: readonly {
|
|
40
|
+
index: number;
|
|
41
|
+
text: string;
|
|
42
|
+
}[], centre: (index: number) => number, bounds: {
|
|
43
|
+
left: number;
|
|
44
|
+
right: number;
|
|
45
|
+
}): {
|
|
46
|
+
rows: number;
|
|
47
|
+
callouts: PlacedCallout[];
|
|
48
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { chartGap, chartInset, chartTextWidth, chartType, } from './chart-metrics.js';
|
|
2
|
+
function overlaps(a, b) {
|
|
3
|
+
return a.from < b.to + chartGap && b.from < a.to + chartGap;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Where a callout's words sit: after its leader in the first half of the
|
|
7
|
+
* chart and before it in the second, the side with more room. Words
|
|
8
|
+
* longer than that room are moved in so they stay inside the chart,
|
|
9
|
+
* crossing their leader.
|
|
10
|
+
*/
|
|
11
|
+
export function besideLeader(x, width, bounds) {
|
|
12
|
+
if (x <= (bounds.left + bounds.right) / 2)
|
|
13
|
+
return {
|
|
14
|
+
anchor: 'start',
|
|
15
|
+
textX: Math.max(bounds.left, Math.min(x + chartInset, bounds.right - width)),
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
anchor: 'end',
|
|
19
|
+
textX: Math.min(bounds.right, Math.max(x - chartInset, bounds.left + width)),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Sets each callout's words beside its leader (`besideLeader`) and gives it the first row from
|
|
24
|
+
* the top where it meets no other callout's words. Returns how many rows
|
|
25
|
+
* the callouts take.
|
|
26
|
+
*/
|
|
27
|
+
export function calloutRows(callouts, centre, bounds) {
|
|
28
|
+
const rows = [];
|
|
29
|
+
const placed = callouts.map((callout, order) => {
|
|
30
|
+
const x = centre(callout.index);
|
|
31
|
+
const width = chartTextWidth(callout.text);
|
|
32
|
+
const { anchor, textX } = besideLeader(x, width, bounds);
|
|
33
|
+
const span = anchor === 'start'
|
|
34
|
+
? { from: Math.min(x, textX), to: textX + width }
|
|
35
|
+
: { from: textX - width, to: Math.max(x, textX) };
|
|
36
|
+
let row = rows.findIndex((taken) => !taken.some((t) => overlaps(t, span)));
|
|
37
|
+
if (row === -1)
|
|
38
|
+
row = rows.push([]) - 1;
|
|
39
|
+
rows[row].push(span);
|
|
40
|
+
const rowTop = row * chartType.lineHeight;
|
|
41
|
+
return {
|
|
42
|
+
order,
|
|
43
|
+
index: callout.index,
|
|
44
|
+
text: callout.text,
|
|
45
|
+
x,
|
|
46
|
+
textX,
|
|
47
|
+
textY: rowTop + chartType.fontSize,
|
|
48
|
+
anchor,
|
|
49
|
+
leaderStart: rowTop + chartType.lineHeight / 2,
|
|
50
|
+
leaderEnd: rowTop + chartType.lineHeight / 2,
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
return { rows: rows.length, callouts: placed };
|
|
54
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type CategoricalColor } from '@scalewing/tokens';
|
|
2
|
+
import { type ColumnChartMode } from './column-chart-bars.js';
|
|
3
|
+
/** One series of a column chart: a value per category, in one hue. */
|
|
4
|
+
export type ColumnChartSeries = {
|
|
5
|
+
/** Stable and unique within the chart. */
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
values: readonly number[];
|
|
9
|
+
tone: CategoricalColor;
|
|
10
|
+
};
|
|
11
|
+
/** A line over the columns, on its own scale: a value per category. */
|
|
12
|
+
export type ColumnChartLine = {
|
|
13
|
+
label: string;
|
|
14
|
+
values: readonly number[];
|
|
15
|
+
/** Writes the line's last value beside its axis (default `true`). */
|
|
16
|
+
endLabel?: boolean;
|
|
17
|
+
};
|
|
18
|
+
/** Words pinned above one category's column, such as why it stands out. */
|
|
19
|
+
export type ColumnChartCallout = {
|
|
20
|
+
index: number;
|
|
21
|
+
text: string;
|
|
22
|
+
/** The hue of its words and dot; the text colour when left out. */
|
|
23
|
+
tone?: CategoricalColor;
|
|
24
|
+
};
|
|
25
|
+
export type ColumnChartInput = {
|
|
26
|
+
label: string;
|
|
27
|
+
categories: readonly string[];
|
|
28
|
+
series: readonly ColumnChartSeries[];
|
|
29
|
+
mode: ColumnChartMode;
|
|
30
|
+
line?: ColumnChartLine;
|
|
31
|
+
highlight?: number;
|
|
32
|
+
callouts: readonly ColumnChartCallout[];
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Refuses what a column chart cannot draw, with a `RangeError`: empty
|
|
36
|
+
* words, a series or line without one finite value per category, a
|
|
37
|
+
* repeated series id, a tone off the categorical scale, an unknown mode,
|
|
38
|
+
* and a highlight or callout that is not a category's index.
|
|
39
|
+
*/
|
|
40
|
+
export declare function assertColumnChartInput(input: ColumnChartInput): void;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { isCategoricalColor } from '@scalewing/tokens';
|
|
2
|
+
import { columnChartModes } from './column-chart-bars.js';
|
|
3
|
+
import { isOneOf } from './is-one-of.js';
|
|
4
|
+
function refuse(message) {
|
|
5
|
+
throw new RangeError(`ColumnChart ${message}`);
|
|
6
|
+
}
|
|
7
|
+
function assertWords(what, value) {
|
|
8
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
9
|
+
refuse(`${what} must not be empty`);
|
|
10
|
+
}
|
|
11
|
+
function assertValues(what, values, count) {
|
|
12
|
+
if (values.length !== count)
|
|
13
|
+
refuse(`${what} must have one value per category (${count}); received ${values.length}`);
|
|
14
|
+
for (const value of values)
|
|
15
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
16
|
+
refuse(`${what} values must be finite numbers: ${String(value)}`);
|
|
17
|
+
}
|
|
18
|
+
function assertIndex(what, index, count) {
|
|
19
|
+
if (!Number.isInteger(index) ||
|
|
20
|
+
index < 0 ||
|
|
21
|
+
index >= count)
|
|
22
|
+
refuse(`${what} must be a category's index, 0 to ${count - 1}: ${String(index)}`);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Refuses what a column chart cannot draw, with a `RangeError`: empty
|
|
26
|
+
* words, a series or line without one finite value per category, a
|
|
27
|
+
* repeated series id, a tone off the categorical scale, an unknown mode,
|
|
28
|
+
* and a highlight or callout that is not a category's index.
|
|
29
|
+
*/
|
|
30
|
+
export function assertColumnChartInput(input) {
|
|
31
|
+
const count = input.categories.length;
|
|
32
|
+
assertWords('label', input.label);
|
|
33
|
+
for (const category of input.categories)
|
|
34
|
+
assertWords('category', category);
|
|
35
|
+
if (!isOneOf(columnChartModes, input.mode))
|
|
36
|
+
refuse(`mode must be one of ${columnChartModes.join(', ')}; received ${String(input.mode)}`);
|
|
37
|
+
const ids = new Set();
|
|
38
|
+
for (const series of input.series) {
|
|
39
|
+
assertWords('series id', series.id);
|
|
40
|
+
if (ids.has(series.id))
|
|
41
|
+
refuse(`series id must be unique: ${series.id}`);
|
|
42
|
+
ids.add(series.id);
|
|
43
|
+
assertWords('series label', series.label);
|
|
44
|
+
assertValues(`series ${series.id}`, series.values, count);
|
|
45
|
+
if (!isCategoricalColor(series.tone))
|
|
46
|
+
refuse(`series tone must be a categorical colour: ${String(series.tone)}`);
|
|
47
|
+
}
|
|
48
|
+
if (input.line) {
|
|
49
|
+
assertWords('line label', input.line.label);
|
|
50
|
+
assertValues('line', input.line.values, count);
|
|
51
|
+
}
|
|
52
|
+
if (input.highlight !== undefined)
|
|
53
|
+
assertIndex('highlight', input.highlight, count);
|
|
54
|
+
for (const callout of input.callouts) {
|
|
55
|
+
assertIndex('callout index', callout.index, count);
|
|
56
|
+
assertWords('callout text', callout.text);
|
|
57
|
+
if (callout.tone !== undefined && !isCategoricalColor(callout.tone))
|
|
58
|
+
refuse(`callout tone must be a categorical colour: ${String(callout.tone)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type ChartPlot, type ChartTick } from './chart-axis.js';
|
|
2
|
+
import { type ColumnChartMode, type PlacedColumn } from './column-chart-bars.js';
|
|
3
|
+
import { type PlacedCallout } from './column-chart-callouts.js';
|
|
4
|
+
export type ColumnChartLayoutInput = {
|
|
5
|
+
width: number;
|
|
6
|
+
categories: readonly string[];
|
|
7
|
+
series: readonly (readonly number[])[];
|
|
8
|
+
mode: ColumnChartMode;
|
|
9
|
+
/** The line's values, and the words at its last value (or none). */
|
|
10
|
+
line: {
|
|
11
|
+
values: readonly number[];
|
|
12
|
+
endLabel: string | null;
|
|
13
|
+
} | null;
|
|
14
|
+
highlight: number | null;
|
|
15
|
+
callouts: readonly {
|
|
16
|
+
index: number;
|
|
17
|
+
text: string;
|
|
18
|
+
}[];
|
|
19
|
+
formatTick: (value: number) => string;
|
|
20
|
+
};
|
|
21
|
+
export type ColumnChartLayout = {
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
compact: boolean;
|
|
25
|
+
plot: ChartPlot;
|
|
26
|
+
ticks: ChartTick[];
|
|
27
|
+
lineTicks: ChartTick[];
|
|
28
|
+
columns: PlacedColumn[];
|
|
29
|
+
categoryLabels: {
|
|
30
|
+
index: number;
|
|
31
|
+
x: number;
|
|
32
|
+
y: number;
|
|
33
|
+
text: string;
|
|
34
|
+
}[];
|
|
35
|
+
highlight: {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
width: number;
|
|
39
|
+
height: number;
|
|
40
|
+
} | null;
|
|
41
|
+
line: {
|
|
42
|
+
points: {
|
|
43
|
+
x: number;
|
|
44
|
+
y: number;
|
|
45
|
+
}[];
|
|
46
|
+
end: {
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
text: string;
|
|
50
|
+
} | null;
|
|
51
|
+
} | null;
|
|
52
|
+
callouts: PlacedCallout[];
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Lays a column chart out at a width: its value axis on the left (and a
|
|
56
|
+
* line's own scale on the right, its zero and grid lines level with the
|
|
57
|
+
* columns'), the columns, the category labels (fewer when they would
|
|
58
|
+
* crowd), the boxed period, the line with its last value beside its axis,
|
|
59
|
+
* and the callouts pinned in rows above the plot. A compact chart (under
|
|
60
|
+
* `chartCompactWidth`) has a shorter plot and draws no callouts: they are
|
|
61
|
+
* listed under it.
|
|
62
|
+
*/
|
|
63
|
+
export declare function columnChartLayout(input: ColumnChartLayoutInput): ColumnChartLayout;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { endGutter, labelGutter, labelInside, periodBox, labelledPlaces, ticksClearOf, valueTicks, valueY, } from './chart-axis.js';
|
|
2
|
+
import { chartCompactWidth, chartGap, chartInset, chartPlotHeight, chartType, } from './chart-metrics.js';
|
|
3
|
+
import { alignedChartScales, chartScale, extentThroughZero, } from './chart-scale.js';
|
|
4
|
+
import { columnSpans, placeColumns, } from './column-chart-bars.js';
|
|
5
|
+
import { calloutRows } from './column-chart-callouts.js';
|
|
6
|
+
function scalesFor(input) {
|
|
7
|
+
const spans = columnSpans(input.series, input.categories.length, input.mode);
|
|
8
|
+
const columns = extentThroughZero(spans.flatMap((s) => [s.low, s.high]));
|
|
9
|
+
if (!input.line)
|
|
10
|
+
return { values: chartScale(columns), line: null };
|
|
11
|
+
const [values, line] = alignedChartScales(columns, extentThroughZero(input.line.values));
|
|
12
|
+
return { values, line };
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Lays a column chart out at a width: its value axis on the left (and a
|
|
16
|
+
* line's own scale on the right, its zero and grid lines level with the
|
|
17
|
+
* columns'), the columns, the category labels (fewer when they would
|
|
18
|
+
* crowd), the boxed period, the line with its last value beside its axis,
|
|
19
|
+
* and the callouts pinned in rows above the plot. A compact chart (under
|
|
20
|
+
* `chartCompactWidth`) has a shorter plot and draws no callouts: they are
|
|
21
|
+
* listed under it.
|
|
22
|
+
*/
|
|
23
|
+
export function columnChartLayout(input) {
|
|
24
|
+
const { width, categories } = input;
|
|
25
|
+
const compact = width < chartCompactWidth;
|
|
26
|
+
const scales = scalesFor(input);
|
|
27
|
+
const tickWords = scales.values.ticks.map(input.formatTick);
|
|
28
|
+
const lineWords = scales.line ? scales.line.ticks.map(input.formatTick) : [];
|
|
29
|
+
const endLabel = input.line?.endLabel ?? null;
|
|
30
|
+
const left = labelGutter(tickWords);
|
|
31
|
+
const end = endGutter({
|
|
32
|
+
width,
|
|
33
|
+
left,
|
|
34
|
+
axisWords: scales.line ? lineWords : [],
|
|
35
|
+
endLabel: scales.line ? endLabel : null,
|
|
36
|
+
});
|
|
37
|
+
const lineWordsShown = end.axisWordsShown;
|
|
38
|
+
const rightGutter = end.gutter;
|
|
39
|
+
const right = Math.max(left + 1, width - rightGutter);
|
|
40
|
+
const band = categories.length > 0 ? (right - left) / categories.length : 0;
|
|
41
|
+
const centre = (index) => left + band * (index + 0.5);
|
|
42
|
+
const placed = compact
|
|
43
|
+
? { rows: 0, callouts: [] }
|
|
44
|
+
: calloutRows(input.callouts, centre, { left: 0, right: width });
|
|
45
|
+
const top = placed.rows * chartType.lineHeight + chartGap + chartType.lineHeight / 2;
|
|
46
|
+
const plot = {
|
|
47
|
+
left,
|
|
48
|
+
right,
|
|
49
|
+
top,
|
|
50
|
+
bottom: top + chartPlotHeight(compact),
|
|
51
|
+
};
|
|
52
|
+
const columns = placeColumns({ series: input.series, categories: categories.length, mode: input.mode }, scales.values, plot, band);
|
|
53
|
+
const line = input.line && scales.line
|
|
54
|
+
? input.line.values.map((value, index) => ({
|
|
55
|
+
x: centre(index),
|
|
56
|
+
y: valueY(value, scales.line, plot),
|
|
57
|
+
}))
|
|
58
|
+
: null;
|
|
59
|
+
const last = line?.at(-1);
|
|
60
|
+
const endMark = last && endLabel
|
|
61
|
+
? { x: right + chartGap, y: last.y, text: endLabel }
|
|
62
|
+
: null;
|
|
63
|
+
const lineTicks = scales.line && lineWordsShown
|
|
64
|
+
? valueTicks(scales.line, plot, input.formatTick)
|
|
65
|
+
: [];
|
|
66
|
+
return {
|
|
67
|
+
width,
|
|
68
|
+
height: plot.bottom + chartGap + chartType.lineHeight,
|
|
69
|
+
compact,
|
|
70
|
+
plot,
|
|
71
|
+
ticks: valueTicks(scales.values, plot, input.formatTick),
|
|
72
|
+
lineTicks: endMark ? ticksClearOf(lineTicks, endMark.y) : lineTicks,
|
|
73
|
+
columns,
|
|
74
|
+
categoryLabels: labelledPlaces(categories, band).map((index) => ({
|
|
75
|
+
index,
|
|
76
|
+
x: labelInside(centre(index), categories[index], width),
|
|
77
|
+
y: plot.bottom + chartGap + chartType.fontSize,
|
|
78
|
+
text: categories[index],
|
|
79
|
+
})),
|
|
80
|
+
highlight: input.highlight === null || categories.length === 0
|
|
81
|
+
? null
|
|
82
|
+
: periodBox(left + band * input.highlight, band, plot),
|
|
83
|
+
line: line ? { points: line, end: endMark } : null,
|
|
84
|
+
callouts: placed.callouts.map((callout) => leaderFrom(callout, columns, plot, scales.values)),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** A callout's leader starts just above its category's tallest column. */
|
|
88
|
+
function leaderFrom(callout, columns, plot, scale) {
|
|
89
|
+
let top = valueY(0, scale, plot);
|
|
90
|
+
for (const column of columns)
|
|
91
|
+
if (column.category === callout.index)
|
|
92
|
+
top = Math.min(top, column.y);
|
|
93
|
+
return {
|
|
94
|
+
...callout,
|
|
95
|
+
leaderEnd: Math.max(top - chartInset, callout.leaderStart),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type HTMLAttributes } from 'react';
|
|
2
|
-
import { type BarChartTone, barChartTones } from '../css/css-chart.js';
|
|
2
|
+
import { type BarChartTone, barChartTones } from '../css/css-bar-chart.js';
|
|
3
3
|
export type { BarChartTone };
|
|
4
4
|
export { barChartTones };
|
|
5
5
|
export type BarChartItem = {
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { barChartFillRatio, barChartScaleMax, formatBarChartValue, } from '@scalewing/tokens';
|
|
3
3
|
import { forwardRef } from 'react';
|
|
4
4
|
import { cx } from '../class-names.js';
|
|
5
|
-
import { barChartTones } from '../css/css-chart.js';
|
|
5
|
+
import { barChartTones } from '../css/css-bar-chart.js';
|
|
6
6
|
export { barChartTones };
|
|
7
7
|
/** The fill's classes: the sign's (which also shapes a diverging bar) and the tone's. */
|
|
8
8
|
function barChartFillClassName(item) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
import { type ChartFillTone, chartFillTones, type ChartStrokeTone, chartStrokeTones, type ChartTone } from '../css/chart-tones.js';
|
|
3
|
+
import { type ChartLegendSwatch, chartLegendSwatches } from '../css/css-chart-legend.js';
|
|
4
|
+
export type { ChartFillTone, ChartLegendSwatch, ChartStrokeTone, ChartTone };
|
|
5
|
+
export { chartFillTones, chartLegendSwatches, chartStrokeTones };
|
|
6
|
+
/**
|
|
7
|
+
* One item of a legend: its words and the mark they name. A `bar` is a
|
|
8
|
+
* filled mark (a column, a range bar) and takes a fill tone: a categorical
|
|
9
|
+
* colour for a chart's series, or a range bar's statement tone (`danger`
|
|
10
|
+
* is DataBar's light shortage fill: for a shortage only, never for cash
|
|
11
|
+
* leaving). A `line`,
|
|
12
|
+
* a `marker` (a downward triangle) and a `box` (the boxed current period)
|
|
13
|
+
* are drawn strokes and take a stroke tone. Each swatch is the colour of
|
|
14
|
+
* the mark it names in the same tone.
|
|
15
|
+
*/
|
|
16
|
+
export type ChartLegendItem = {
|
|
17
|
+
label: string;
|
|
18
|
+
swatch: 'bar';
|
|
19
|
+
tone: ChartFillTone;
|
|
20
|
+
} | {
|
|
21
|
+
label: string;
|
|
22
|
+
swatch: 'line' | 'marker' | 'box';
|
|
23
|
+
tone: ChartStrokeTone;
|
|
24
|
+
};
|
|
25
|
+
export type ChartLegendProps = Omit<HTMLAttributes<HTMLUListElement>, 'children'> & {
|
|
26
|
+
items: readonly ChartLegendItem[];
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* A chart's key: a list of its marks, each a swatch and its words, in a
|
|
30
|
+
* wrapping row in the caption type. Put it above the chart it names, or
|
|
31
|
+
* above a statement's range bars. The swatches are decorative
|
|
32
|
+
* (`aria-hidden`), so each item reads as its words; the list keeps its
|
|
33
|
+
* list role where a style would drop it. Empty words, or a swatch or tone the
|
|
34
|
+
* item cannot draw, throw a `RangeError`.
|
|
35
|
+
*/
|
|
36
|
+
export declare const ChartLegend: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLUListElement>, "children"> & {
|
|
37
|
+
items: readonly ChartLegendItem[];
|
|
38
|
+
} & import("react").RefAttributes<HTMLUListElement>>;
|