@pushwoosh/kit-charts 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/ai-documentation.md +378 -0
- package/core/Chart/Axes.d.ts +19 -0
- package/core/Chart/Axes.js +204 -0
- package/core/Chart/Chart.d.ts +33 -0
- package/core/Chart/Chart.js +616 -0
- package/core/Chart/Chart.styles.d.ts +109 -0
- package/core/Chart/Chart.styles.js +227 -0
- package/core/Chart/ChartErrorBoundary.d.ts +15 -0
- package/core/Chart/ChartErrorBoundary.js +27 -0
- package/core/Chart/Hinted.d.ts +11 -0
- package/core/Chart/Hinted.js +63 -0
- package/core/Chart/Legend.d.ts +15 -0
- package/core/Chart/Legend.js +92 -0
- package/core/Chart/PlotDecorations.d.ts +17 -0
- package/core/Chart/PlotDecorations.js +172 -0
- package/core/Chart/StackLabels.d.ts +6 -0
- package/core/Chart/StackLabels.js +85 -0
- package/core/Chart/Titles.d.ts +6 -0
- package/core/Chart/Titles.js +67 -0
- package/core/Chart/Tooltip.d.ts +13 -0
- package/core/Chart/Tooltip.js +87 -0
- package/core/Chart/useSize.d.ts +5 -0
- package/core/Chart/useSize.js +45 -0
- package/core/animation/motion.d.ts +20 -0
- package/core/animation/motion.js +20 -0
- package/core/animation/useTweenedNumbers.d.ts +1 -0
- package/core/animation/useTweenedNumbers.js +51 -0
- package/core/data/points.d.ts +14 -0
- package/core/data/points.js +73 -0
- package/core/format/time.d.ts +2 -0
- package/core/format/time.js +38 -0
- package/core/geometry/path.d.ts +19 -0
- package/core/geometry/path.js +76 -0
- package/core/layout/legend-layout.d.ts +21 -0
- package/core/layout/legend-layout.js +103 -0
- package/core/layout/measure-text.d.ts +9 -0
- package/core/layout/measure-text.js +57 -0
- package/core/layout/metrics.d.ts +63 -0
- package/core/layout/metrics.js +66 -0
- package/core/layout/point-width.d.ts +11 -0
- package/core/layout/point-width.js +86 -0
- package/core/layout/title-layout.d.ts +30 -0
- package/core/layout/title-layout.js +99 -0
- package/core/model/build-model.d.ts +69 -0
- package/core/model/build-model.js +556 -0
- package/core/model/palette.d.ts +15 -0
- package/core/model/palette.js +17 -0
- package/core/options/normalize.d.ts +8 -0
- package/core/options/normalize.js +44 -0
- package/core/registry/registry.d.ts +5 -0
- package/core/registry/registry.js +32 -0
- package/core/scale/band.d.ts +10 -0
- package/core/scale/band.js +33 -0
- package/core/scale/color.d.ts +9 -0
- package/core/scale/color.js +69 -0
- package/core/scale/linear.d.ts +17 -0
- package/core/scale/linear.js +64 -0
- package/core/scale/time.d.ts +19 -0
- package/core/scale/time.js +187 -0
- package/core/stack/stack.d.ts +9 -0
- package/core/stack/stack.js +76 -0
- package/core/types/formatter.d.ts +59 -0
- package/core/types/formatter.js +1 -0
- package/core/types/geometry.d.ts +16 -0
- package/core/types/geometry.js +1 -0
- package/core/types/options.d.ts +331 -0
- package/core/types/options.js +2 -0
- package/core/types/renderer.d.ts +101 -0
- package/core/types/renderer.js +1 -0
- package/core/types/scale.d.ts +34 -0
- package/core/types/scale.js +1 -0
- package/index.d.ts +16 -0
- package/index.js +10 -0
- package/package.json +64 -0
- package/presets/AreaChart.d.ts +3 -0
- package/presets/AreaChart.js +13 -0
- package/presets/ColumnChart.d.ts +3 -0
- package/presets/ColumnChart.js +13 -0
- package/presets/LineChart.d.ts +3 -0
- package/presets/LineChart.js +13 -0
- package/presets/PieChart.d.ts +3 -0
- package/presets/PieChart.js +13 -0
- package/presets/StackedColumnChart.d.ts +7 -0
- package/presets/StackedColumnChart.js +31 -0
- package/presets/index.d.ts +5 -0
- package/presets/index.js +5 -0
- package/series/area/area.d.ts +2 -0
- package/series/area/area.js +57 -0
- package/series/area/index.d.ts +1 -0
- package/series/area/index.js +1 -0
- package/series/column/column.d.ts +2 -0
- package/series/column/column.js +163 -0
- package/series/column/index.d.ts +1 -0
- package/series/column/index.js +1 -0
- package/series/heatmap/heatmap.d.ts +2 -0
- package/series/heatmap/heatmap.js +172 -0
- package/series/heatmap/index.d.ts +1 -0
- package/series/heatmap/index.js +1 -0
- package/series/line/index.d.ts +1 -0
- package/series/line/index.js +1 -0
- package/series/line/line.d.ts +2 -0
- package/series/line/line.js +49 -0
- package/series/pie/index.d.ts +1 -0
- package/series/pie/index.js +1 -0
- package/series/pie/pie.d.ts +2 -0
- package/series/pie/pie.js +224 -0
- package/series/shared/DataLabels.d.ts +14 -0
- package/series/shared/DataLabels.js +124 -0
- package/series/shared/Markers.d.ts +9 -0
- package/series/shared/Markers.js +34 -0
- package/series/shared/color.d.ts +1 -0
- package/series/shared/color.js +19 -0
- package/series/shared/domain.d.ts +3 -0
- package/series/shared/domain.js +14 -0
- package/series/shared/hit.d.ts +3 -0
- package/series/shared/hit.js +33 -0
- package/series/shared/legend.d.ts +3 -0
- package/series/shared/legend.js +12 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { DEFAULT_FONT_SIZE, textHeight } from '../layout/measure-text';
|
|
3
|
+
import { crisp, PLOT_LABEL_GAP } from '../layout/metrics';
|
|
4
|
+
import { DEFAULT_AXIS_COLOR, DEFAULT_MUTED_TEXT_COLOR, DEFAULT_SURFACE_COLOR } from '../model/palette';
|
|
5
|
+
import { ABOVE_SERIES_Z_INDEX } from '../types/options';
|
|
6
|
+
const DASH = {
|
|
7
|
+
solid: undefined,
|
|
8
|
+
dash: '4 4',
|
|
9
|
+
dot: '1 3'
|
|
10
|
+
};
|
|
11
|
+
const inLayer = (zIndex, layer) => (zIndex ?? 0) >= ABOVE_SERIES_Z_INDEX ? layer === 'over' : layer === 'under';
|
|
12
|
+
/**
|
|
13
|
+
* A band scale has no position for a value between two categories, so a
|
|
14
|
+
* decoration on one addresses a category by name and covers its whole slot.
|
|
15
|
+
*/
|
|
16
|
+
function project(scale, value, edge) {
|
|
17
|
+
if (scale.kind === 'band') {
|
|
18
|
+
const at = scale.map(String(value));
|
|
19
|
+
if (!Number.isFinite(at)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return edge === 'start' ? at : at + scale.bandwidth();
|
|
23
|
+
}
|
|
24
|
+
const numeric = Number(value);
|
|
25
|
+
if (!Number.isFinite(numeric)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const at = scale.map(numeric);
|
|
29
|
+
return typeof at === 'number' ? at : null;
|
|
30
|
+
}
|
|
31
|
+
const textProps = style => ({
|
|
32
|
+
fill: (style === null || style === void 0 ? void 0 : style.color) ?? DEFAULT_MUTED_TEXT_COLOR,
|
|
33
|
+
fontFamily: style === null || style === void 0 ? void 0 : style.fontFamily,
|
|
34
|
+
fontSize: (style === null || style === void 0 ? void 0 : style.fontSize) ?? DEFAULT_FONT_SIZE,
|
|
35
|
+
fontWeight: style === null || style === void 0 ? void 0 : style.fontWeight
|
|
36
|
+
});
|
|
37
|
+
const ANCHOR = {
|
|
38
|
+
left: 'start',
|
|
39
|
+
center: 'middle',
|
|
40
|
+
right: 'end'
|
|
41
|
+
};
|
|
42
|
+
function PlotLabel({
|
|
43
|
+
label,
|
|
44
|
+
plot,
|
|
45
|
+
span,
|
|
46
|
+
along
|
|
47
|
+
}) {
|
|
48
|
+
if (!(label !== null && label !== void 0 && label.text)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const [low, high] = span[0] <= span[1] ? span : [span[1], span[0]];
|
|
52
|
+
const align = label.align ?? (along === 'x' ? 'center' : 'left');
|
|
53
|
+
const verticalAlign = label.verticalAlign ?? (along === 'x' ? 'top' : 'middle');
|
|
54
|
+
const acrossLow = along === 'x' ? plot.y : plot.x;
|
|
55
|
+
const acrossHigh = along === 'x' ? plot.y + plot.height : plot.x + plot.width;
|
|
56
|
+
const alongAt = {
|
|
57
|
+
left: low,
|
|
58
|
+
center: (low + high) / 2,
|
|
59
|
+
right: high
|
|
60
|
+
}[align];
|
|
61
|
+
const acrossAt = {
|
|
62
|
+
top: acrossLow + PLOT_LABEL_GAP + textHeight(label.style),
|
|
63
|
+
middle: (acrossLow + acrossHigh) / 2,
|
|
64
|
+
bottom: acrossHigh - PLOT_LABEL_GAP
|
|
65
|
+
}[verticalAlign];
|
|
66
|
+
const x = (along === 'x' ? alongAt : acrossAt) + (label.x ?? 0);
|
|
67
|
+
const y = (along === 'x' ? acrossAt : alongAt) + (label.y ?? 0);
|
|
68
|
+
const rotation = label.rotation ?? 0;
|
|
69
|
+
return _jsx("text", {
|
|
70
|
+
x: x,
|
|
71
|
+
y: y,
|
|
72
|
+
textAnchor: along === 'x' ? ANCHOR[align] : 'start',
|
|
73
|
+
transform: rotation === 0 ? undefined : `rotate(${rotation} ${x} ${y})`,
|
|
74
|
+
...textProps(label.style),
|
|
75
|
+
children: label.text
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Plot bands and plot lines for one axis. They are drawn outside the axis
|
|
80
|
+
* component because a `zIndex` may put them over the series, which the axis
|
|
81
|
+
* itself never is.
|
|
82
|
+
*/
|
|
83
|
+
export function PlotDecorations({
|
|
84
|
+
plot,
|
|
85
|
+
axis,
|
|
86
|
+
along,
|
|
87
|
+
layer
|
|
88
|
+
}) {
|
|
89
|
+
const {
|
|
90
|
+
options,
|
|
91
|
+
scale
|
|
92
|
+
} = axis;
|
|
93
|
+
if (options.visible === false) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const bands = (options.plotBands ?? []).filter(band => inLayer(band.zIndex, layer));
|
|
97
|
+
const lines = (options.plotLines ?? []).filter(line => inLayer(line.zIndex, layer));
|
|
98
|
+
if (!bands.length && !lines.length) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const band = (options_, index) => {
|
|
102
|
+
const from = project(scale, options_.from, 'start');
|
|
103
|
+
const to = project(scale, options_.to, 'end');
|
|
104
|
+
if (from === null || to === null) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const [low, high] = from <= to ? [from, to] : [to, from];
|
|
108
|
+
const box = along === 'x' ? {
|
|
109
|
+
x: low,
|
|
110
|
+
y: plot.y,
|
|
111
|
+
width: high - low,
|
|
112
|
+
height: plot.height
|
|
113
|
+
} : {
|
|
114
|
+
x: plot.x,
|
|
115
|
+
y: low,
|
|
116
|
+
width: plot.width,
|
|
117
|
+
height: high - low
|
|
118
|
+
};
|
|
119
|
+
return _jsxs("g", {
|
|
120
|
+
children: [_jsx("rect", {
|
|
121
|
+
...box,
|
|
122
|
+
fill: options_.color ?? DEFAULT_SURFACE_COLOR,
|
|
123
|
+
stroke: options_.borderWidth ? options_.borderColor ?? DEFAULT_AXIS_COLOR : undefined,
|
|
124
|
+
strokeWidth: options_.borderWidth ?? 0
|
|
125
|
+
}), _jsx(PlotLabel, {
|
|
126
|
+
label: options_.label,
|
|
127
|
+
plot: plot,
|
|
128
|
+
span: [low, high],
|
|
129
|
+
along: along
|
|
130
|
+
})]
|
|
131
|
+
}, `band-${index}`);
|
|
132
|
+
};
|
|
133
|
+
const rule = (options_, index) => {
|
|
134
|
+
// A line on a category sits in the middle of its slot, not on its edge.
|
|
135
|
+
const start = project(scale, options_.value, 'start');
|
|
136
|
+
const end = project(scale, options_.value, 'end');
|
|
137
|
+
if (start === null || end === null) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const at = (start + end) / 2;
|
|
141
|
+
const width = options_.width ?? 1;
|
|
142
|
+
const on = crisp(at, width);
|
|
143
|
+
const ends = along === 'x' ? {
|
|
144
|
+
x1: on,
|
|
145
|
+
x2: on,
|
|
146
|
+
y1: plot.y,
|
|
147
|
+
y2: plot.y + plot.height
|
|
148
|
+
} : {
|
|
149
|
+
x1: plot.x,
|
|
150
|
+
x2: plot.x + plot.width,
|
|
151
|
+
y1: on,
|
|
152
|
+
y2: on
|
|
153
|
+
};
|
|
154
|
+
return _jsxs("g", {
|
|
155
|
+
children: [_jsx("line", {
|
|
156
|
+
...ends,
|
|
157
|
+
stroke: options_.color ?? DEFAULT_AXIS_COLOR,
|
|
158
|
+
strokeWidth: width,
|
|
159
|
+
strokeDasharray: DASH[options_.dashStyle ?? 'solid']
|
|
160
|
+
}), _jsx(PlotLabel, {
|
|
161
|
+
label: options_.label,
|
|
162
|
+
plot: plot,
|
|
163
|
+
span: [at, at],
|
|
164
|
+
along: along
|
|
165
|
+
})]
|
|
166
|
+
}, `line-${index}`);
|
|
167
|
+
};
|
|
168
|
+
return _jsxs("g", {
|
|
169
|
+
"aria-hidden": true,
|
|
170
|
+
children: [bands.map(band), lines.map(rule)]
|
|
171
|
+
});
|
|
172
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { projectX } from '../geometry/path';
|
|
3
|
+
import { DEFAULT_FONT_SIZE } from '../layout/measure-text';
|
|
4
|
+
import { VALUE_LABEL_GAP } from '../layout/metrics';
|
|
5
|
+
import { DEFAULT_TEXT_COLOR } from '../model/palette';
|
|
6
|
+
function collectTotals(model, axisIndex) {
|
|
7
|
+
const totals = new Map();
|
|
8
|
+
model.series.forEach(series => {
|
|
9
|
+
var _model$yAxes$axisInde;
|
|
10
|
+
if ((series.options.yAxis ?? 0) !== axisIndex) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const stacked = model.stacks.get(series.index);
|
|
14
|
+
if (!stacked) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const yScale = (_model$yAxes$axisInde = model.yAxes[axisIndex]) === null || _model$yAxes$axisInde === void 0 ? void 0 : _model$yAxes$axisInde.scale;
|
|
18
|
+
if (!yScale || yScale.kind === 'band' || yScale.kind === 'color') {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
series.points.forEach((point, pointIndex) => {
|
|
22
|
+
const cumulative = stacked[pointIndex];
|
|
23
|
+
if (!cumulative) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const key = String(point.x);
|
|
27
|
+
const px = projectX(model.xAxis.scale, point);
|
|
28
|
+
const py = yScale.map(cumulative.to);
|
|
29
|
+
const existing = totals.get(key);
|
|
30
|
+
if (!Number.isFinite(px) || !Number.isFinite(py)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!existing || py < existing.py) {
|
|
34
|
+
totals.set(key, {
|
|
35
|
+
key,
|
|
36
|
+
px,
|
|
37
|
+
py,
|
|
38
|
+
value: cumulative.total
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
return [...totals.values()];
|
|
44
|
+
}
|
|
45
|
+
export function StackLabels({
|
|
46
|
+
model
|
|
47
|
+
}) {
|
|
48
|
+
const labels = model.yAxes.flatMap((axis, axisIndex) => {
|
|
49
|
+
const options = axis.options.stackLabels;
|
|
50
|
+
if (!(options !== null && options !== void 0 && options.enabled)) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const style = options.style;
|
|
54
|
+
return collectTotals(model, axisIndex).map(total => {
|
|
55
|
+
const content = options.formatter ? options.formatter({
|
|
56
|
+
x: total.key,
|
|
57
|
+
y: total.value,
|
|
58
|
+
value: total.value,
|
|
59
|
+
pointIndex: -1,
|
|
60
|
+
seriesIndex: -1,
|
|
61
|
+
seriesName: '',
|
|
62
|
+
meta: undefined
|
|
63
|
+
}) : model.numberFormat(total.value);
|
|
64
|
+
if (content === null || content === undefined || content === '') {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return _jsx("text", {
|
|
68
|
+
x: total.px,
|
|
69
|
+
y: total.py - VALUE_LABEL_GAP,
|
|
70
|
+
textAnchor: "middle",
|
|
71
|
+
fill: (style === null || style === void 0 ? void 0 : style.color) ?? DEFAULT_TEXT_COLOR,
|
|
72
|
+
fontFamily: style === null || style === void 0 ? void 0 : style.fontFamily,
|
|
73
|
+
fontSize: (style === null || style === void 0 ? void 0 : style.fontSize) ?? DEFAULT_FONT_SIZE,
|
|
74
|
+
fontWeight: (style === null || style === void 0 ? void 0 : style.fontWeight) ?? '600',
|
|
75
|
+
children: content
|
|
76
|
+
}, `sl-${axis.id}-${total.key}`);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
if (!labels.length) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return _jsx("g", {
|
|
83
|
+
children: labels
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { HintIcon } from './Chart.styles';
|
|
3
|
+
import { Hinted } from './Hinted';
|
|
4
|
+
import { DEFAULT_FONT_SIZE } from '../layout/measure-text';
|
|
5
|
+
import { HINT_ICON_RADIUS, HINT_ICON_SIZE } from '../layout/metrics';
|
|
6
|
+
import { DEFAULT_MUTED_TEXT_COLOR, DEFAULT_TEXT_COLOR } from '../model/palette';
|
|
7
|
+
const WEIGHT = {
|
|
8
|
+
title: '600',
|
|
9
|
+
subtitle: '400',
|
|
10
|
+
caption: '400'
|
|
11
|
+
};
|
|
12
|
+
const COLOR = {
|
|
13
|
+
title: DEFAULT_TEXT_COLOR,
|
|
14
|
+
subtitle: DEFAULT_MUTED_TEXT_COLOR,
|
|
15
|
+
caption: DEFAULT_MUTED_TEXT_COLOR
|
|
16
|
+
};
|
|
17
|
+
const HINT_FONT_SIZE = '11px';
|
|
18
|
+
const HINT_BASELINE_SHIFT = 4;
|
|
19
|
+
/** The `i` beside a title: its explanation opens in the same callout as a point's. */
|
|
20
|
+
function Hint({
|
|
21
|
+
hint
|
|
22
|
+
}) {
|
|
23
|
+
return _jsx(Hinted, {
|
|
24
|
+
text: hint.text,
|
|
25
|
+
children: _jsxs(HintIcon, {
|
|
26
|
+
children: [_jsx("rect", {
|
|
27
|
+
x: hint.x - HINT_ICON_SIZE / 2,
|
|
28
|
+
y: hint.y - HINT_ICON_SIZE / 2,
|
|
29
|
+
width: HINT_ICON_SIZE,
|
|
30
|
+
height: HINT_ICON_SIZE,
|
|
31
|
+
rx: HINT_ICON_RADIUS
|
|
32
|
+
}), _jsx("text", {
|
|
33
|
+
x: hint.x,
|
|
34
|
+
y: hint.y + HINT_BASELINE_SHIFT,
|
|
35
|
+
textAnchor: "middle",
|
|
36
|
+
fontSize: HINT_FONT_SIZE,
|
|
37
|
+
children: "i"
|
|
38
|
+
})]
|
|
39
|
+
})
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function Titles({
|
|
43
|
+
entries
|
|
44
|
+
}) {
|
|
45
|
+
if (!entries.length) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return _jsx("g", {
|
|
49
|
+
children: entries.map(entry => {
|
|
50
|
+
var _entry$style, _entry$style2, _entry$style3;
|
|
51
|
+
return _jsxs("g", {
|
|
52
|
+
children: [_jsx("text", {
|
|
53
|
+
x: entry.x,
|
|
54
|
+
y: entry.y,
|
|
55
|
+
textAnchor: entry.anchor,
|
|
56
|
+
fill: ((_entry$style = entry.style) === null || _entry$style === void 0 ? void 0 : _entry$style.color) ?? COLOR[entry.key],
|
|
57
|
+
fontFamily: (_entry$style2 = entry.style) === null || _entry$style2 === void 0 ? void 0 : _entry$style2.fontFamily,
|
|
58
|
+
fontSize: entry.fontSize ?? DEFAULT_FONT_SIZE,
|
|
59
|
+
fontWeight: ((_entry$style3 = entry.style) === null || _entry$style3 === void 0 ? void 0 : _entry$style3.fontWeight) ?? WEIGHT[entry.key],
|
|
60
|
+
children: entry.text
|
|
61
|
+
}), entry.hint ? _jsx(Hint, {
|
|
62
|
+
hint: entry.hint
|
|
63
|
+
}) : null]
|
|
64
|
+
}, entry.key);
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from 'react';
|
|
2
|
+
import { type Point } from '../types/geometry';
|
|
3
|
+
import { type TooltipOptions } from '../types/options';
|
|
4
|
+
interface TooltipProps {
|
|
5
|
+
/** Anchor the callout points at, in viewport coordinates. */
|
|
6
|
+
at: Point;
|
|
7
|
+
/** Pointer position, used when the tooltip follows the cursor instead. */
|
|
8
|
+
pointer: Point;
|
|
9
|
+
options: TooltipOptions | undefined;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
export declare function Tooltip({ at, options, pointer, children, }: TooltipProps): ReactElement | null;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import { DEFAULTS, Tail, TooltipBox } from './Chart.styles';
|
|
5
|
+
import { TOOLTIP_CURSOR_OFFSET, TOOLTIP_PADDING, TOOLTIP_RADIUS, TOOLTIP_TAIL, VIEWPORT_MARGIN } from '../layout/metrics';
|
|
6
|
+
const SHADOW_COLOR = 'rgba(0, 0, 0, 0.12)';
|
|
7
|
+
const DEFAULT_SHADOW = `0 2px 8px ${SHADOW_COLOR}`;
|
|
8
|
+
export function Tooltip({
|
|
9
|
+
at,
|
|
10
|
+
options,
|
|
11
|
+
pointer,
|
|
12
|
+
children
|
|
13
|
+
}) {
|
|
14
|
+
var _options$style, _options$style2, _options$style3, _options$style4;
|
|
15
|
+
const ref = useRef(null);
|
|
16
|
+
const callout = ((options === null || options === void 0 ? void 0 : options.shape) ?? 'callout') === 'callout' && !(options !== null && options !== void 0 && options.followPointer);
|
|
17
|
+
const target = options !== null && options !== void 0 && options.followPointer ? pointer : at;
|
|
18
|
+
const [placement, setPlacement] = useState({
|
|
19
|
+
x: target.x + TOOLTIP_CURSOR_OFFSET,
|
|
20
|
+
y: target.y + TOOLTIP_CURSOR_OFFSET,
|
|
21
|
+
tailX: null,
|
|
22
|
+
below: true
|
|
23
|
+
});
|
|
24
|
+
useLayoutEffect(() => {
|
|
25
|
+
const node = ref.current;
|
|
26
|
+
if (!node) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const {
|
|
30
|
+
width,
|
|
31
|
+
height
|
|
32
|
+
} = node.getBoundingClientRect();
|
|
33
|
+
const limitX = value => Math.min(Math.max(VIEWPORT_MARGIN, value), Math.max(VIEWPORT_MARGIN, window.innerWidth - width - VIEWPORT_MARGIN));
|
|
34
|
+
if (!callout) {
|
|
35
|
+
const flipX = target.x + TOOLTIP_CURSOR_OFFSET + width > window.innerWidth - VIEWPORT_MARGIN;
|
|
36
|
+
const flipY = target.y + TOOLTIP_CURSOR_OFFSET + height > window.innerHeight - VIEWPORT_MARGIN;
|
|
37
|
+
setPlacement({
|
|
38
|
+
x: limitX(flipX ? target.x - TOOLTIP_CURSOR_OFFSET - width : target.x + TOOLTIP_CURSOR_OFFSET),
|
|
39
|
+
y: Math.max(VIEWPORT_MARGIN, flipY ? target.y - TOOLTIP_CURSOR_OFFSET - height : target.y + TOOLTIP_CURSOR_OFFSET),
|
|
40
|
+
tailX: null,
|
|
41
|
+
below: !flipY
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Sit above the point with the tail pointing down at it, and flip under the
|
|
46
|
+
// point when there is no room. The box may be nudged sideways to stay on
|
|
47
|
+
// screen, so the tail keeps its own offset and still points at the anchor.
|
|
48
|
+
const below = target.y - height - TOOLTIP_TAIL < VIEWPORT_MARGIN;
|
|
49
|
+
const x = limitX(target.x - width / 2);
|
|
50
|
+
setPlacement({
|
|
51
|
+
x,
|
|
52
|
+
y: below ? target.y + TOOLTIP_TAIL : target.y - height - TOOLTIP_TAIL,
|
|
53
|
+
tailX: Math.min(Math.max(TOOLTIP_TAIL * 2, target.x - x), Math.max(TOOLTIP_TAIL * 2, width - TOOLTIP_TAIL * 2)),
|
|
54
|
+
below
|
|
55
|
+
});
|
|
56
|
+
}, [target.x, target.y, callout, children]);
|
|
57
|
+
if (typeof document === 'undefined') {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const shadow = options === null || options === void 0 ? void 0 : options.shadow;
|
|
61
|
+
// A table needs room and columns; the dark hint chrome reads badly at that size,
|
|
62
|
+
// so the tabular tooltip is a light card unless the caller says otherwise.
|
|
63
|
+
const tabular = Boolean(options === null || options === void 0 ? void 0 : options.table);
|
|
64
|
+
const background = (options === null || options === void 0 ? void 0 : options.backgroundColor) ?? (tabular ? DEFAULTS.surface : DEFAULTS.tooltipSurface);
|
|
65
|
+
const color = (options === null || options === void 0 || (_options$style = options.style) === null || _options$style === void 0 ? void 0 : _options$style.color) ?? (tabular ? DEFAULTS.text : DEFAULTS.tooltipText);
|
|
66
|
+
return createPortal(_jsxs(TooltipBox, {
|
|
67
|
+
ref: ref,
|
|
68
|
+
role: "tooltip",
|
|
69
|
+
"$x": placement.x,
|
|
70
|
+
"$y": placement.y,
|
|
71
|
+
"$padding": (options === null || options === void 0 ? void 0 : options.padding) ?? TOOLTIP_PADDING,
|
|
72
|
+
"$radius": (options === null || options === void 0 ? void 0 : options.borderRadius) ?? TOOLTIP_RADIUS,
|
|
73
|
+
"$borderColor": (options === null || options === void 0 ? void 0 : options.borderColor) ?? (tabular ? DEFAULTS.border : 'transparent'),
|
|
74
|
+
"$background": background,
|
|
75
|
+
"$color": color,
|
|
76
|
+
"$fontFamily": (options === null || options === void 0 || (_options$style2 = options.style) === null || _options$style2 === void 0 ? void 0 : _options$style2.fontFamily) ?? DEFAULTS.fontFamily,
|
|
77
|
+
"$fontSize": (options === null || options === void 0 || (_options$style3 = options.style) === null || _options$style3 === void 0 ? void 0 : _options$style3.fontSize) ?? DEFAULTS.fontSize,
|
|
78
|
+
"$whiteSpace": (options === null || options === void 0 || (_options$style4 = options.style) === null || _options$style4 === void 0 ? void 0 : _options$style4.whiteSpace) ?? 'nowrap',
|
|
79
|
+
"$shadow": shadow ? `${shadow.offsetX ?? 0}px ${shadow.offsetY ?? 2}px ${shadow.width ?? 8}px ` + `${shadow.color ?? SHADOW_COLOR}` : DEFAULT_SHADOW,
|
|
80
|
+
children: [children, placement.tailX === null ? null : _jsx(Tail, {
|
|
81
|
+
"$left": placement.tailX,
|
|
82
|
+
"$size": TOOLTIP_TAIL,
|
|
83
|
+
"$color": background,
|
|
84
|
+
"$below": placement.below
|
|
85
|
+
})]
|
|
86
|
+
}), document.body);
|
|
87
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
const ZERO = {
|
|
3
|
+
width: 0,
|
|
4
|
+
height: 0
|
|
5
|
+
};
|
|
6
|
+
export function useSize() {
|
|
7
|
+
const ref = useRef(null);
|
|
8
|
+
const [size, setSize] = useState(ZERO);
|
|
9
|
+
const frame = useRef(0);
|
|
10
|
+
useLayoutEffect(() => {
|
|
11
|
+
const node = ref.current;
|
|
12
|
+
if (!node) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
// Whole pixels only. Browser zoom sizes a box in fractions of a css pixel, and
|
|
16
|
+
// comparing rounded sizes while storing the fraction left the canvas at its old
|
|
17
|
+
// width whenever the box moved by less than half a pixel — drawn narrower than
|
|
18
|
+
// its box, with the right edge shaved off by the container's overflow.
|
|
19
|
+
const apply = () => {
|
|
20
|
+
const {
|
|
21
|
+
width,
|
|
22
|
+
height
|
|
23
|
+
} = node.getBoundingClientRect();
|
|
24
|
+
const next = {
|
|
25
|
+
width: Math.floor(width),
|
|
26
|
+
height: Math.floor(height)
|
|
27
|
+
};
|
|
28
|
+
setSize(current => current.width === next.width && current.height === next.height ? current : next);
|
|
29
|
+
};
|
|
30
|
+
apply();
|
|
31
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const observer = new ResizeObserver(() => {
|
|
35
|
+
cancelAnimationFrame(frame.current);
|
|
36
|
+
frame.current = requestAnimationFrame(apply);
|
|
37
|
+
});
|
|
38
|
+
observer.observe(node);
|
|
39
|
+
return () => {
|
|
40
|
+
cancelAnimationFrame(frame.current);
|
|
41
|
+
observer.disconnect();
|
|
42
|
+
};
|
|
43
|
+
}, []);
|
|
44
|
+
return [ref, size];
|
|
45
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One clock for the whole chart.
|
|
3
|
+
*
|
|
4
|
+
* Anything that answers the same gesture has to take the same time and the same
|
|
5
|
+
* curve, whatever draws it: a column shrinking through a CSS transition, a pie
|
|
6
|
+
* easing its angles frame by frame, a legend entry dimming. Two mechanisms are
|
|
7
|
+
* unavoidable — a browser cannot interpolate an arc correctly — but the numbers
|
|
8
|
+
* behind them must come from here, or the chart answers a single click at three
|
|
9
|
+
* different speeds.
|
|
10
|
+
*/
|
|
11
|
+
/** Everything that moves or leaves in response to a click on the chart. */
|
|
12
|
+
export declare const MOTION_MS = 320;
|
|
13
|
+
/** Hover feedback is an affordance, not chart motion: it may be quicker. */
|
|
14
|
+
export declare const HOVER_MS = 140;
|
|
15
|
+
/**
|
|
16
|
+
* Fast out, gentle in. The CSS curve and the function below are the same easing
|
|
17
|
+
* written twice — keep them in step.
|
|
18
|
+
*/
|
|
19
|
+
export declare const MOTION_EASING = "cubic-bezier(0.33, 1, 0.68, 1)";
|
|
20
|
+
export declare const easeOutCubic: (t: number) => number;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One clock for the whole chart.
|
|
3
|
+
*
|
|
4
|
+
* Anything that answers the same gesture has to take the same time and the same
|
|
5
|
+
* curve, whatever draws it: a column shrinking through a CSS transition, a pie
|
|
6
|
+
* easing its angles frame by frame, a legend entry dimming. Two mechanisms are
|
|
7
|
+
* unavoidable — a browser cannot interpolate an arc correctly — but the numbers
|
|
8
|
+
* behind them must come from here, or the chart answers a single click at three
|
|
9
|
+
* different speeds.
|
|
10
|
+
*/
|
|
11
|
+
/** Everything that moves or leaves in response to a click on the chart. */
|
|
12
|
+
export const MOTION_MS = 320;
|
|
13
|
+
/** Hover feedback is an affordance, not chart motion: it may be quicker. */
|
|
14
|
+
export const HOVER_MS = 140;
|
|
15
|
+
/**
|
|
16
|
+
* Fast out, gentle in. The CSS curve and the function below are the same easing
|
|
17
|
+
* written twice — keep them in step.
|
|
18
|
+
*/
|
|
19
|
+
export const MOTION_EASING = 'cubic-bezier(0.33, 1, 0.68, 1)';
|
|
20
|
+
export const easeOutCubic = t => 1 - (1 - t) ** 3;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useTweenedNumbers(target: readonly number[], duration?: number): readonly number[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { easeOutCubic, MOTION_MS } from './motion';
|
|
3
|
+
/**
|
|
4
|
+
* Eases a vector of numbers towards its target over time, a frame at a time.
|
|
5
|
+
*
|
|
6
|
+
* Shapes that are generated from numbers — the angles and radii of a pie, say —
|
|
7
|
+
* must be animated through those numbers, never by interpolating the path they
|
|
8
|
+
* produce: a browser blends a path command by command, which bends an arc out of
|
|
9
|
+
* shape and flips its large-arc flag halfway through. Tweening the inputs and
|
|
10
|
+
* regenerating the shape each frame is what D3's `arcTween` and Highcharts'
|
|
11
|
+
* animator both do, and it is the only way the result stays a real arc.
|
|
12
|
+
*/
|
|
13
|
+
const prefersReducedMotion = () => typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
14
|
+
const same = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
|
|
15
|
+
export function useTweenedNumbers(target, duration = MOTION_MS) {
|
|
16
|
+
const [values, setValues] = useState(target);
|
|
17
|
+
const current = useRef(target);
|
|
18
|
+
const key = target.join(',');
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const from = current.current;
|
|
21
|
+
// A different number of shapes has nothing to tween between, and neither has a
|
|
22
|
+
// browser that cannot schedule frames or a viewer who asked for less motion.
|
|
23
|
+
const jump = from.length !== target.length || duration <= 0 || typeof requestAnimationFrame !== 'function' || prefersReducedMotion();
|
|
24
|
+
if (jump) {
|
|
25
|
+
current.current = target;
|
|
26
|
+
setValues(target);
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
if (same(from, target)) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
let frame = 0;
|
|
33
|
+
let started = 0;
|
|
34
|
+
const step = now => {
|
|
35
|
+
started = started || now;
|
|
36
|
+
const progress = Math.min(1, (now - started) / duration);
|
|
37
|
+
const eased = easeOutCubic(progress);
|
|
38
|
+
const next = from.map((value, index) => value + (target[index] - value) * eased);
|
|
39
|
+
current.current = progress < 1 ? next : target;
|
|
40
|
+
setValues(current.current);
|
|
41
|
+
if (progress < 1) {
|
|
42
|
+
frame = requestAnimationFrame(step);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
frame = requestAnimationFrame(step);
|
|
46
|
+
return () => cancelAnimationFrame(frame);
|
|
47
|
+
// `key` stands in for the target vector; the values themselves are read above.
|
|
48
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
49
|
+
}, [key, duration]);
|
|
50
|
+
return values;
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type SeriesData } from '../types/options';
|
|
2
|
+
export interface NormalizedPoint {
|
|
3
|
+
x: number | string;
|
|
4
|
+
band?: string;
|
|
5
|
+
y: number | null;
|
|
6
|
+
value: number | null;
|
|
7
|
+
name: string | undefined;
|
|
8
|
+
color: string | undefined;
|
|
9
|
+
meta: unknown;
|
|
10
|
+
index: number;
|
|
11
|
+
}
|
|
12
|
+
/** What a hit or a tooltip should show for the point's x: its category, else its x. */
|
|
13
|
+
export declare const pointKey: (point: NormalizedPoint) => number | string;
|
|
14
|
+
export declare function normalizePoints(data: SeriesData | undefined, seriesName?: string): NormalizedPoint[];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
function toValue(raw, context) {
|
|
2
|
+
if (raw === null || raw === undefined) {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
const value = Number(raw);
|
|
6
|
+
if (!Number.isFinite(value)) {
|
|
7
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
8
|
+
console.warn(`[kit-charts] Non-finite value in ${context}: ${String(raw)} — treated as a gap.`);
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
const isPointObject = point => typeof point === 'object' && point !== null && !Array.isArray(point);
|
|
15
|
+
/** What a hit or a tooltip should show for the point's x: its category, else its x. */
|
|
16
|
+
export const pointKey = point => point.band ?? point.name ?? point.x;
|
|
17
|
+
export function normalizePoints(data, seriesName = 'series') {
|
|
18
|
+
if (!(data !== null && data !== void 0 && data.length)) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
return data.map((point, index) => {
|
|
22
|
+
const context = `${seriesName}[${index}]`;
|
|
23
|
+
if (point === null || typeof point === 'number') {
|
|
24
|
+
const y = toValue(point, context);
|
|
25
|
+
return {
|
|
26
|
+
x: index,
|
|
27
|
+
y,
|
|
28
|
+
value: y,
|
|
29
|
+
name: undefined,
|
|
30
|
+
color: undefined,
|
|
31
|
+
meta: undefined,
|
|
32
|
+
index
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(point)) {
|
|
36
|
+
const [x, second, third] = point;
|
|
37
|
+
const y = toValue(second, context);
|
|
38
|
+
const value = third === undefined ? y : toValue(third, context);
|
|
39
|
+
return {
|
|
40
|
+
x: typeof x === 'string' ? x : toValue(x, context) ?? index,
|
|
41
|
+
y,
|
|
42
|
+
value,
|
|
43
|
+
name: typeof x === 'string' ? x : undefined,
|
|
44
|
+
color: undefined,
|
|
45
|
+
meta: undefined,
|
|
46
|
+
index
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (isPointObject(point)) {
|
|
50
|
+
const y = toValue(point.y, context);
|
|
51
|
+
const value = point.value === undefined ? y : toValue(point.value, context);
|
|
52
|
+
const x = point.x ?? point.name ?? index;
|
|
53
|
+
return {
|
|
54
|
+
x: typeof x === 'string' ? x : toValue(x, context) ?? index,
|
|
55
|
+
y,
|
|
56
|
+
value,
|
|
57
|
+
name: point.name,
|
|
58
|
+
color: point.color,
|
|
59
|
+
meta: point.meta,
|
|
60
|
+
index
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
x: index,
|
|
65
|
+
y: null,
|
|
66
|
+
value: null,
|
|
67
|
+
name: undefined,
|
|
68
|
+
color: undefined,
|
|
69
|
+
meta: undefined,
|
|
70
|
+
index
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|