@scalewing/react 1.38.0 → 1.39.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 +9 -0
- package/README.md +53 -1
- 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/LineChart.d.ts +83 -0
- package/dist/components/LineChart.js +77 -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/line-chart/LineChartDrawing.d.ts +17 -0
- package/dist/components/line-chart/LineChartDrawing.js +16 -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/{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-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-components.js +14 -2
- package/dist/css/css-data-bar.d.ts +4 -9
- package/dist/css/css-data-bar.js +15 -31
- package/dist/css/css-line-chart.d.ts +2 -0
- package/dist/css/css-line-chart.js +31 -0
- package/dist/css/stylesheet.js +10 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- 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/styles.css +219 -0
- package/package.json +1 -1
|
@@ -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>>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import { cx } from '../class-names.js';
|
|
4
|
+
import { chartFillTones, chartStrokeTones, } from '../css/chart-tones.js';
|
|
5
|
+
import { chartLegendSwatches, } from '../css/css-chart-legend.js';
|
|
6
|
+
import { isOneOf } from '../is-one-of.js';
|
|
7
|
+
export { chartFillTones, chartLegendSwatches, chartStrokeTones };
|
|
8
|
+
/** Refuses empty words, and a swatch or a tone the item cannot draw. */
|
|
9
|
+
function assertLegendItem(item) {
|
|
10
|
+
if (typeof item.label !== 'string' || item.label.trim() === '')
|
|
11
|
+
throw new RangeError('ChartLegend item label must not be empty');
|
|
12
|
+
if (!isOneOf(chartLegendSwatches, item.swatch))
|
|
13
|
+
throw new RangeError(`ChartLegend swatch must be one of ${chartLegendSwatches.join(', ')}; received ${String(item.swatch)}`);
|
|
14
|
+
const tones = item.swatch === 'bar' ? chartFillTones : chartStrokeTones;
|
|
15
|
+
if (!isOneOf(tones, item.tone))
|
|
16
|
+
throw new RangeError(`ChartLegend ${item.swatch} tone must be one of ${tones.join(', ')}; received ${String(item.tone)}`);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A chart's key: a list of its marks, each a swatch and its words, in a
|
|
20
|
+
* wrapping row in the caption type. Put it above the chart it names, or
|
|
21
|
+
* above a statement's range bars. The swatches are decorative
|
|
22
|
+
* (`aria-hidden`), so each item reads as its words; the list keeps its
|
|
23
|
+
* list role where a style would drop it. Empty words, or a swatch or tone the
|
|
24
|
+
* item cannot draw, throw a `RangeError`.
|
|
25
|
+
*/
|
|
26
|
+
export const ChartLegend = forwardRef(function ChartLegend({ className, items, ...rest }, ref) {
|
|
27
|
+
for (const item of items)
|
|
28
|
+
assertLegendItem(item);
|
|
29
|
+
return (_jsx("ul", { ref: ref, className: cx('sw-chart-legend', className),
|
|
30
|
+
// A list styled without markers loses its role in Safari.
|
|
31
|
+
role: "list", ...rest, children: items.map((item, index) => (_jsxs("li", { className: cx('sw-chart-legend-item', `sw-chart-tone-${item.tone}`), children: [_jsx("span", { "aria-hidden": "true", className: cx('sw-chart-legend-swatch', `sw-chart-legend-${item.swatch}`) }), item.label] }, `${item.label}-${index}`))) }));
|
|
32
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
import { type ColumnChartMode, columnChartModes } from '../column-chart-bars.js';
|
|
3
|
+
import { type ColumnChartCallout, type ColumnChartLine, type ColumnChartSeries } from '../column-chart-input.js';
|
|
4
|
+
export type { ColumnChartCallout, ColumnChartLine, ColumnChartMode, ColumnChartSeries, };
|
|
5
|
+
export { columnChartModes };
|
|
6
|
+
export type ColumnChartProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
7
|
+
/** Names the chart: the caption of the table that carries its figures. */
|
|
8
|
+
label: string;
|
|
9
|
+
/** The columns' places, such as weeks, in order; each is a row's header. */
|
|
10
|
+
categories: readonly string[];
|
|
11
|
+
/** Each series: a value per category, drawn in a categorical hue. */
|
|
12
|
+
series: readonly ColumnChartSeries[];
|
|
13
|
+
/**
|
|
14
|
+
* `stacked` (the default) puts a category's columns on one another,
|
|
15
|
+
* positive values up from zero and negative ones down from it;
|
|
16
|
+
* `grouped` sets them side by side, each from zero.
|
|
17
|
+
*/
|
|
18
|
+
mode?: ColumnChartMode;
|
|
19
|
+
/** A line over the columns, such as a running total, on its own scale. */
|
|
20
|
+
line?: ColumnChartLine;
|
|
21
|
+
/** The index of the category to box, such as the current week. */
|
|
22
|
+
highlight?: number;
|
|
23
|
+
/** Words pinned above a category's column, with a leader down to it. */
|
|
24
|
+
callouts?: readonly ColumnChartCallout[];
|
|
25
|
+
/**
|
|
26
|
+
* Writes each figure in the table and the line's last value, such as a
|
|
27
|
+
* currency. Defaults to a plain number with at most one decimal.
|
|
28
|
+
*/
|
|
29
|
+
formatValue?: (value: number) => string;
|
|
30
|
+
/** Writes the axes' values, shorter if need be; defaults to `formatValue`. */
|
|
31
|
+
formatTick?: (value: number) => string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Columns for each category (a week, a month), stacked or grouped by
|
|
35
|
+
* series, with an optional line on its own right-hand scale whose zero
|
|
36
|
+
* and grid lines are level with the columns', the current period boxed
|
|
37
|
+
* and callouts pinned above their columns. It fills its container's width
|
|
38
|
+
* and is laid out for it: a narrow chart labels fewer categories and lists
|
|
39
|
+
* its callouts under the plot, never scrolling sideways. The drawing is
|
|
40
|
+
* hidden from assistive technology; a visually hidden table named by
|
|
41
|
+
* `label` carries every figure, and the callouts stay a list. Put it on
|
|
42
|
+
* the surface (a `Card`) with a `ChartLegend` above it naming each series
|
|
43
|
+
* (`bar` in its tone), the line (`line`, `accent`) and the box (`box`,
|
|
44
|
+
* `accent`). Input it cannot draw throws a `RangeError`.
|
|
45
|
+
*/
|
|
46
|
+
export declare const ColumnChart: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLDivElement>, "children"> & {
|
|
47
|
+
/** Names the chart: the caption of the table that carries its figures. */
|
|
48
|
+
label: string;
|
|
49
|
+
/** The columns' places, such as weeks, in order; each is a row's header. */
|
|
50
|
+
categories: readonly string[];
|
|
51
|
+
/** Each series: a value per category, drawn in a categorical hue. */
|
|
52
|
+
series: readonly ColumnChartSeries[];
|
|
53
|
+
/**
|
|
54
|
+
* `stacked` (the default) puts a category's columns on one another,
|
|
55
|
+
* positive values up from zero and negative ones down from it;
|
|
56
|
+
* `grouped` sets them side by side, each from zero.
|
|
57
|
+
*/
|
|
58
|
+
mode?: ColumnChartMode;
|
|
59
|
+
/** A line over the columns, such as a running total, on its own scale. */
|
|
60
|
+
line?: ColumnChartLine;
|
|
61
|
+
/** The index of the category to box, such as the current week. */
|
|
62
|
+
highlight?: number;
|
|
63
|
+
/** Words pinned above a category's column, with a leader down to it. */
|
|
64
|
+
callouts?: readonly ColumnChartCallout[];
|
|
65
|
+
/**
|
|
66
|
+
* Writes each figure in the table and the line's last value, such as a
|
|
67
|
+
* currency. Defaults to a plain number with at most one decimal.
|
|
68
|
+
*/
|
|
69
|
+
formatValue?: (value: number) => string;
|
|
70
|
+
/** Writes the axes' values, shorter if need be; defaults to `formatValue`. */
|
|
71
|
+
formatTick?: (value: number) => string;
|
|
72
|
+
} & import("react").RefAttributes<HTMLDivElement>>;
|