@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,616 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { Grid, XAxis, YAxis } from './Axes';
|
|
4
|
+
import { Canvas, Centred, Container, PinnedAxes, ResetZoom, ScrollBox, SeriesLayer, TooltipCell, TooltipDivider, TooltipHeadCell, TooltipName, TooltipRow, TooltipSeriesCell, TooltipSeriesInner, TooltipSwatch, TooltipTable, TooltipTitle, TooltipValue, ZoomBand } from './Chart.styles';
|
|
5
|
+
import { ChartErrorBoundary } from './ChartErrorBoundary';
|
|
6
|
+
import { Legend } from './Legend';
|
|
7
|
+
import { PlotDecorations } from './PlotDecorations';
|
|
8
|
+
import { StackLabels } from './StackLabels';
|
|
9
|
+
import { Titles } from './Titles';
|
|
10
|
+
import { Tooltip } from './Tooltip';
|
|
11
|
+
import { useSize } from './useSize';
|
|
12
|
+
import { MOTION_MS } from '../animation/motion';
|
|
13
|
+
import { formatTime } from '../format/time';
|
|
14
|
+
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE } from '../layout/measure-text';
|
|
15
|
+
import { buildChartModel, hiddenKey, isContinuous } from '../model/build-model';
|
|
16
|
+
import { DEFAULT_GRID_COLOR, DEFAULT_SURFACE_COLOR, DEFAULT_TEXT_COLOR } from '../model/palette';
|
|
17
|
+
import { resolveRenderers } from '../registry/registry';
|
|
18
|
+
/** Series switched off through their options; the legend takes over from there. */
|
|
19
|
+
const hiddenByOptions = options => {
|
|
20
|
+
const off = new Set();
|
|
21
|
+
(options.series ?? []).forEach((series, index) => {
|
|
22
|
+
if (series.visible === false) {
|
|
23
|
+
off.add(hiddenKey(index));
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
return off;
|
|
27
|
+
};
|
|
28
|
+
const seriesIdentity = options => (options.series ?? []).map(series => `${series.type}:${series.name ?? ''}`).join('|');
|
|
29
|
+
const toCssSize = (value, fallback) => {
|
|
30
|
+
if (value === null || value === undefined) {
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
return typeof value === 'number' ? `${value}px` : value;
|
|
34
|
+
};
|
|
35
|
+
function defaultTooltip(ctx, formatX, formatValue) {
|
|
36
|
+
return _jsxs("div", {
|
|
37
|
+
children: [_jsx(TooltipTitle, {
|
|
38
|
+
children: formatX(ctx.x)
|
|
39
|
+
}), ctx.points.map(point => _jsxs(TooltipRow, {
|
|
40
|
+
children: [_jsx(TooltipSwatch, {
|
|
41
|
+
"$color": point.color
|
|
42
|
+
}), _jsx(TooltipName, {
|
|
43
|
+
children: point.seriesName
|
|
44
|
+
}), _jsx(TooltipValue, {
|
|
45
|
+
children: formatValue(point.value ?? point.y)
|
|
46
|
+
})]
|
|
47
|
+
}, `${point.seriesIndex}-${point.index}`))]
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function tableTooltip(ctx, table, formatX) {
|
|
51
|
+
const heading = table.header ? table.header(ctx) : formatX(ctx.x);
|
|
52
|
+
const columns = table.columns.map(column => ({
|
|
53
|
+
column,
|
|
54
|
+
title: typeof column.title === 'function' ? column.title(ctx) : column.title
|
|
55
|
+
})).filter(({
|
|
56
|
+
title
|
|
57
|
+
}) => title !== null);
|
|
58
|
+
const headed = table.seriesTitle !== undefined || columns.some(({
|
|
59
|
+
title
|
|
60
|
+
}) => title !== undefined);
|
|
61
|
+
return _jsxs("div", {
|
|
62
|
+
children: [heading === null || heading === undefined || heading === '' ? null : _jsxs(_Fragment, {
|
|
63
|
+
children: [_jsx(TooltipTitle, {
|
|
64
|
+
children: heading
|
|
65
|
+
}), _jsx(TooltipDivider, {
|
|
66
|
+
"$color": DEFAULT_GRID_COLOR
|
|
67
|
+
})]
|
|
68
|
+
}), _jsxs(TooltipTable, {
|
|
69
|
+
children: [headed ? _jsx("thead", {
|
|
70
|
+
children: _jsxs("tr", {
|
|
71
|
+
children: [_jsx(TooltipHeadCell, {
|
|
72
|
+
"$align": "left",
|
|
73
|
+
children: table.seriesTitle ?? ''
|
|
74
|
+
}), columns.map(({
|
|
75
|
+
column,
|
|
76
|
+
title
|
|
77
|
+
}, index) => _jsx(TooltipHeadCell
|
|
78
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
79
|
+
, {
|
|
80
|
+
"$align": column.align ?? 'right',
|
|
81
|
+
children: title ?? ''
|
|
82
|
+
}, `head-${index}`))]
|
|
83
|
+
})
|
|
84
|
+
}) : null, _jsx("tbody", {
|
|
85
|
+
children: ctx.points.map(point => _jsxs("tr", {
|
|
86
|
+
children: [_jsx(TooltipSeriesCell, {
|
|
87
|
+
children: _jsxs(TooltipSeriesInner, {
|
|
88
|
+
children: [_jsx(TooltipSwatch, {
|
|
89
|
+
"$color": point.color,
|
|
90
|
+
"$round": true
|
|
91
|
+
}), point.seriesName]
|
|
92
|
+
})
|
|
93
|
+
}), columns.map(({
|
|
94
|
+
column
|
|
95
|
+
}, index) => _jsx(TooltipCell
|
|
96
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
97
|
+
, {
|
|
98
|
+
"$align": column.align ?? 'right',
|
|
99
|
+
"$muted": index > 0,
|
|
100
|
+
children: column.cell(point)
|
|
101
|
+
}, `cell-${index}`))]
|
|
102
|
+
}, `${point.seriesIndex}-${point.index}`))
|
|
103
|
+
})]
|
|
104
|
+
})]
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** Shorter than this and the drag was a click, not a selection. */
|
|
108
|
+
const ZOOM_MIN_DRAG = 6;
|
|
109
|
+
function ChartBody({
|
|
110
|
+
options,
|
|
111
|
+
renderers,
|
|
112
|
+
isLoading = false,
|
|
113
|
+
empty,
|
|
114
|
+
loading,
|
|
115
|
+
className,
|
|
116
|
+
containerProps,
|
|
117
|
+
toggleSeries = true,
|
|
118
|
+
onSeriesToggle,
|
|
119
|
+
onPointClick
|
|
120
|
+
}) {
|
|
121
|
+
var _options$chart, _options$chart2, _options$tooltip4, _options$tooltip7, _options$tooltip8, _options$title2, _options$tooltip1, _options$tooltip10, _options$chart3, _options$chart4, _options$chart5, _options$chart6, _options$chart7, _options$chart8, _options$chart9, _options$title3, _options$chart0, _options$chart1, _options$chart10;
|
|
122
|
+
const [container, size] = useSize();
|
|
123
|
+
const [hover, setHover] = useState(null);
|
|
124
|
+
const [zoom, setZoom] = useState(null);
|
|
125
|
+
const [drag, setDrag] = useState(null);
|
|
126
|
+
const [hidden, setHidden] = useState(() => hiddenByOptions(options));
|
|
127
|
+
const identity = seriesIdentity(options);
|
|
128
|
+
// A different set of series is a different chart: start from its own options
|
|
129
|
+
// again rather than carrying the previous chart's switched-off indices over.
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
setHidden(hiddenByOptions(options));
|
|
132
|
+
setZoom(null);
|
|
133
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
134
|
+
}, [identity]);
|
|
135
|
+
const toggle = useCallback(item => {
|
|
136
|
+
// A tooltip captured before the toggle would keep quoting what just went away.
|
|
137
|
+
setHover(null);
|
|
138
|
+
const key = hiddenKey(item.seriesIndex, item.pointIndex);
|
|
139
|
+
setHidden(current => {
|
|
140
|
+
const next = new Set(current);
|
|
141
|
+
if (next.has(key)) {
|
|
142
|
+
next.delete(key);
|
|
143
|
+
} else {
|
|
144
|
+
next.add(key);
|
|
145
|
+
}
|
|
146
|
+
onSeriesToggle === null || onSeriesToggle === void 0 || onSeriesToggle(item.seriesIndex, !next.has(key), item.pointIndex);
|
|
147
|
+
return next;
|
|
148
|
+
});
|
|
149
|
+
}, [onSeriesToggle]);
|
|
150
|
+
const registry = useMemo(() => resolveRenderers(renderers), [renderers]);
|
|
151
|
+
// A scrollable plot area draws at its minimum size and scrolls inside the box.
|
|
152
|
+
const scrollable = (_options$chart = options.chart) === null || _options$chart === void 0 ? void 0 : _options$chart.scrollablePlotArea;
|
|
153
|
+
const canvas = useMemo(() => ({
|
|
154
|
+
width: Math.max(size.width, (scrollable === null || scrollable === void 0 ? void 0 : scrollable.minWidth) ?? 0),
|
|
155
|
+
height: Math.max(size.height, (scrollable === null || scrollable === void 0 ? void 0 : scrollable.minHeight) ?? 0)
|
|
156
|
+
}), [size, scrollable === null || scrollable === void 0 ? void 0 : scrollable.minWidth, scrollable === null || scrollable === void 0 ? void 0 : scrollable.minHeight]);
|
|
157
|
+
// A zoom is an override on the first x axis, not a second source of truth: the
|
|
158
|
+
// model is still built from the options, so everything downstream is unaware.
|
|
159
|
+
const zoomed = useMemo(() => {
|
|
160
|
+
if (!zoom) {
|
|
161
|
+
return options;
|
|
162
|
+
}
|
|
163
|
+
const axes = options.xAxis;
|
|
164
|
+
const [first, ...rest] = Array.isArray(axes) ? axes : [axes ?? {}];
|
|
165
|
+
return {
|
|
166
|
+
...options,
|
|
167
|
+
xAxis: [{
|
|
168
|
+
...first,
|
|
169
|
+
min: zoom[0],
|
|
170
|
+
max: zoom[1]
|
|
171
|
+
}, ...rest]
|
|
172
|
+
};
|
|
173
|
+
}, [options, zoom]);
|
|
174
|
+
const built = useMemo(() => canvas.width && canvas.height ? buildChartModel({
|
|
175
|
+
options: zoomed,
|
|
176
|
+
size: canvas,
|
|
177
|
+
renderers: registry,
|
|
178
|
+
hidden
|
|
179
|
+
}) : null, [zoomed, canvas, registry, hidden]);
|
|
180
|
+
const anyShown = (built === null || built === void 0 ? void 0 : built.series.some(series => !series.hidden)) ?? false;
|
|
181
|
+
const lastShown = useRef(null);
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (built && anyShown) {
|
|
184
|
+
lastShown.current = built;
|
|
185
|
+
}
|
|
186
|
+
}, [built, anyShown]);
|
|
187
|
+
// Switching off the last series must not reshape the axes under it: the plot
|
|
188
|
+
// holds the geometry of the last state that had something on show, so the series
|
|
189
|
+
// fades out where it stood instead of jumping to a rescaled axis first.
|
|
190
|
+
const model = useMemo(() => {
|
|
191
|
+
if (anyShown || !built) {
|
|
192
|
+
return built;
|
|
193
|
+
}
|
|
194
|
+
const frozen = lastShown.current;
|
|
195
|
+
if (!frozen) {
|
|
196
|
+
return built;
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
...frozen,
|
|
200
|
+
series: frozen.series.map(series => ({
|
|
201
|
+
...series,
|
|
202
|
+
hidden: true
|
|
203
|
+
})),
|
|
204
|
+
legend: built.legend,
|
|
205
|
+
titles: built.titles
|
|
206
|
+
};
|
|
207
|
+
}, [built, anyShown]);
|
|
208
|
+
const contextFor = useCallback(series => {
|
|
209
|
+
if (!model) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const yModel = model.yAxes[Math.min(series.options.yAxis ?? 0, model.yAxes.length - 1)];
|
|
213
|
+
if (!yModel || yModel.scale.kind === 'color') {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const hit = hover === null || hover === void 0 ? void 0 : hover.hits.find(candidate => candidate.seriesIndex === series.index);
|
|
217
|
+
return {
|
|
218
|
+
series: series.options,
|
|
219
|
+
seriesIndex: series.index,
|
|
220
|
+
points: series.points,
|
|
221
|
+
plot: model.plot,
|
|
222
|
+
scales: {
|
|
223
|
+
x: model.xAxis.scale,
|
|
224
|
+
y: yModel.scale,
|
|
225
|
+
color: model.colorScale
|
|
226
|
+
},
|
|
227
|
+
color: series.color,
|
|
228
|
+
colors: model.colors,
|
|
229
|
+
stacked: model.stacks.get(series.index),
|
|
230
|
+
slot: model.slots.get(series.index),
|
|
231
|
+
hovered: hit ? {
|
|
232
|
+
seriesIndex: series.index,
|
|
233
|
+
pointIndex: hit.pointIndex
|
|
234
|
+
} : undefined,
|
|
235
|
+
hidden: series.hidden,
|
|
236
|
+
hiddenPoints: series.hiddenPoints,
|
|
237
|
+
animate: true,
|
|
238
|
+
numberFormat: model.numberFormat
|
|
239
|
+
};
|
|
240
|
+
}, [model, hover]);
|
|
241
|
+
const hitsAt = useCallback(event => {
|
|
242
|
+
if (!model) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
const bounds = event.currentTarget.getBoundingClientRect();
|
|
246
|
+
const local = {
|
|
247
|
+
x: event.clientX - bounds.left,
|
|
248
|
+
y: event.clientY - bounds.top
|
|
249
|
+
};
|
|
250
|
+
const hits = model.series.flatMap(series => {
|
|
251
|
+
if (series.hidden) {
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
const ctx = contextFor(series);
|
|
255
|
+
return ctx ? series.renderer.hitTest(ctx, local) ?? [] : [];
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
hits,
|
|
259
|
+
bounds
|
|
260
|
+
};
|
|
261
|
+
}, [model, contextFor]);
|
|
262
|
+
const handlePointerMove = useCallback(event => {
|
|
263
|
+
var _options$tooltip, _options$tooltip2;
|
|
264
|
+
if (!model || ((_options$tooltip = options.tooltip) === null || _options$tooltip === void 0 ? void 0 : _options$tooltip.enabled) === false) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
// While a zoom is being dragged out, a tooltip is in the way of the thing
|
|
268
|
+
// the reader is actually doing.
|
|
269
|
+
if (drag) {
|
|
270
|
+
setHover(null);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const found = hitsAt(event);
|
|
274
|
+
if (!found) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const {
|
|
278
|
+
hits,
|
|
279
|
+
bounds
|
|
280
|
+
} = found;
|
|
281
|
+
if (!hits.length) {
|
|
282
|
+
setHover(null);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const shared = ((_options$tooltip2 = options.tooltip) === null || _options$tooltip2 === void 0 ? void 0 : _options$tooltip2.shared) ?? false;
|
|
286
|
+
const nearest = hits.reduce((best, hit) => hit.distance < best.distance ? hit : best);
|
|
287
|
+
const selected = shared ? hits.filter(hit => String(hit.x) === String(nearest.x)) : [nearest];
|
|
288
|
+
const highest = selected.reduce((best, hit) => hit.anchor.y < best.anchor.y ? hit : best, selected[0]);
|
|
289
|
+
setHover({
|
|
290
|
+
hits: selected,
|
|
291
|
+
pointer: {
|
|
292
|
+
x: event.clientX,
|
|
293
|
+
y: event.clientY
|
|
294
|
+
},
|
|
295
|
+
anchor: {
|
|
296
|
+
x: bounds.left + highest.anchor.x,
|
|
297
|
+
y: bounds.top + highest.anchor.y
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}, [model, hitsAt, options.tooltip, drag]);
|
|
301
|
+
const handlePointerLeave = useCallback(() => setHover(null), []);
|
|
302
|
+
const zooms = ((_options$chart2 = options.chart) === null || _options$chart2 === void 0 ? void 0 : _options$chart2.zoomType) === 'x' && model !== null && isContinuous(model.xAxis.scale);
|
|
303
|
+
const localX = event => event.clientX - event.currentTarget.getBoundingClientRect().left;
|
|
304
|
+
const startDrag = useCallback(event => {
|
|
305
|
+
if (!zooms || !model) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const x = localX(event);
|
|
309
|
+
if (x < model.plot.x || x > model.plot.x + model.plot.width) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
setDrag({
|
|
313
|
+
from: x,
|
|
314
|
+
to: x
|
|
315
|
+
});
|
|
316
|
+
}, [zooms, model]);
|
|
317
|
+
const extendDrag = useCallback(event => {
|
|
318
|
+
if (!drag || !model) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const x = Math.min(Math.max(localX(event), model.plot.x), model.plot.x + model.plot.width);
|
|
322
|
+
setDrag({
|
|
323
|
+
from: drag.from,
|
|
324
|
+
to: x
|
|
325
|
+
});
|
|
326
|
+
}, [drag, model]);
|
|
327
|
+
const endDrag = useCallback(() => {
|
|
328
|
+
if (!drag || !model || !isContinuous(model.xAxis.scale)) {
|
|
329
|
+
setDrag(null);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const [low, high] = drag.from <= drag.to ? [drag.from, drag.to] : [drag.to, drag.from];
|
|
333
|
+
setDrag(null);
|
|
334
|
+
// A click is a drag of nothing; zooming to a point would empty the chart.
|
|
335
|
+
if (high - low < ZOOM_MIN_DRAG) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const scale = model.xAxis.scale;
|
|
339
|
+
setZoom([scale.invert(low), scale.invert(high)]);
|
|
340
|
+
}, [drag, model]);
|
|
341
|
+
// Released outside the chart, the drag still ends: bound to the window rather
|
|
342
|
+
// than the canvas, or letting go past the edge leaves the band stuck on screen.
|
|
343
|
+
useEffect(() => {
|
|
344
|
+
if (!drag) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
window.addEventListener('pointerup', endDrag);
|
|
348
|
+
return () => window.removeEventListener('pointerup', endDrag);
|
|
349
|
+
}, [drag, endDrag]);
|
|
350
|
+
const resetZoom = useCallback(() => {
|
|
351
|
+
setZoom(null);
|
|
352
|
+
setHover(null);
|
|
353
|
+
}, []);
|
|
354
|
+
const handleClick = useCallback(event => {
|
|
355
|
+
var _hitsAt, _series$points$neares;
|
|
356
|
+
if (!onPointClick || !model) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const hits = ((_hitsAt = hitsAt(event)) === null || _hitsAt === void 0 ? void 0 : _hitsAt.hits) ?? [];
|
|
360
|
+
if (!hits.length) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const nearest = hits.reduce((best, hit) => hit.distance < best.distance ? hit : best);
|
|
364
|
+
const series = model.series.find(item => item.index === nearest.seriesIndex);
|
|
365
|
+
onPointClick({
|
|
366
|
+
seriesIndex: nearest.seriesIndex,
|
|
367
|
+
seriesName: (series === null || series === void 0 ? void 0 : series.options.name) ?? `Series ${nearest.seriesIndex + 1}`,
|
|
368
|
+
pointIndex: nearest.pointIndex,
|
|
369
|
+
x: nearest.x,
|
|
370
|
+
y: nearest.y,
|
|
371
|
+
value: nearest.value,
|
|
372
|
+
color: (series === null || series === void 0 ? void 0 : series.color) ?? DEFAULT_TEXT_COLOR,
|
|
373
|
+
meta: series === null || series === void 0 || (_series$points$neares = series.points[nearest.pointIndex]) === null || _series$points$neares === void 0 ? void 0 : _series$points$neares.meta
|
|
374
|
+
}, event);
|
|
375
|
+
}, [onPointClick, model, hitsAt]);
|
|
376
|
+
const formatX = useCallback(value => {
|
|
377
|
+
var _options$tooltip3;
|
|
378
|
+
if (typeof value !== 'number') {
|
|
379
|
+
return String(value);
|
|
380
|
+
}
|
|
381
|
+
const pattern = ((_options$tooltip3 = options.tooltip) === null || _options$tooltip3 === void 0 ? void 0 : _options$tooltip3.dateFormat) ?? (model && isContinuous(model.xAxis.scale) && model.xAxis.scale.kind === 'time' ? '%b %d, %Y' : undefined);
|
|
382
|
+
return pattern ? formatTime(pattern, value) : String(value);
|
|
383
|
+
}, [(_options$tooltip4 = options.tooltip) === null || _options$tooltip4 === void 0 ? void 0 : _options$tooltip4.dateFormat, model]);
|
|
384
|
+
const formatValue = useCallback(value => {
|
|
385
|
+
var _options$tooltip5, _options$tooltip6;
|
|
386
|
+
if (value === null) {
|
|
387
|
+
return '—';
|
|
388
|
+
}
|
|
389
|
+
const text = model ? model.numberFormat(value) : String(value);
|
|
390
|
+
return `${((_options$tooltip5 = options.tooltip) === null || _options$tooltip5 === void 0 ? void 0 : _options$tooltip5.valuePrefix) ?? ''}${text}${((_options$tooltip6 = options.tooltip) === null || _options$tooltip6 === void 0 ? void 0 : _options$tooltip6.valueSuffix) ?? ''}`;
|
|
391
|
+
}, [(_options$tooltip7 = options.tooltip) === null || _options$tooltip7 === void 0 ? void 0 : _options$tooltip7.valuePrefix, (_options$tooltip8 = options.tooltip) === null || _options$tooltip8 === void 0 ? void 0 : _options$tooltip8.valueSuffix, model]);
|
|
392
|
+
const tooltipContent = useMemo(() => {
|
|
393
|
+
var _points$, _options$title, _options$tooltip9, _options$tooltip0;
|
|
394
|
+
if (!hover || !model) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
const shown = hover.hits.filter(hit => {
|
|
398
|
+
const series = model.series.find(item => item.index === hit.seriesIndex);
|
|
399
|
+
return series ? !series.hidden : false;
|
|
400
|
+
});
|
|
401
|
+
if (!shown.length) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
const points = shown.map(hit => {
|
|
405
|
+
const series = model.series.find(item => item.index === hit.seriesIndex);
|
|
406
|
+
const neighbours = (series === null || series === void 0 ? void 0 : series.points) ?? [];
|
|
407
|
+
const previous = neighbours[hit.pointIndex - 1];
|
|
408
|
+
const next = neighbours[hit.pointIndex + 1];
|
|
409
|
+
return {
|
|
410
|
+
seriesIndex: hit.seriesIndex,
|
|
411
|
+
seriesName: (series === null || series === void 0 ? void 0 : series.options.name) ?? `Series ${hit.seriesIndex + 1}`,
|
|
412
|
+
color: (series === null || series === void 0 ? void 0 : series.color) ?? DEFAULT_TEXT_COLOR,
|
|
413
|
+
x: hit.x,
|
|
414
|
+
y: hit.y,
|
|
415
|
+
value: hit.value,
|
|
416
|
+
index: hit.pointIndex,
|
|
417
|
+
meta: series === null || series === void 0 ? void 0 : series.options.meta,
|
|
418
|
+
prev: previous ? {
|
|
419
|
+
x: previous.x,
|
|
420
|
+
y: previous.y,
|
|
421
|
+
index: previous.index
|
|
422
|
+
} : undefined,
|
|
423
|
+
next: next ? {
|
|
424
|
+
x: next.x,
|
|
425
|
+
y: next.y,
|
|
426
|
+
index: next.index
|
|
427
|
+
} : undefined
|
|
428
|
+
};
|
|
429
|
+
});
|
|
430
|
+
const ctx = {
|
|
431
|
+
x: ((_points$ = points[0]) === null || _points$ === void 0 ? void 0 : _points$.x) ?? 0,
|
|
432
|
+
points,
|
|
433
|
+
chart: {
|
|
434
|
+
title: ((_options$title = options.title) === null || _options$title === void 0 ? void 0 : _options$title.text) ?? undefined,
|
|
435
|
+
meta: options.meta
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
const formatter = (_options$tooltip9 = options.tooltip) === null || _options$tooltip9 === void 0 ? void 0 : _options$tooltip9.formatter;
|
|
439
|
+
if (formatter) {
|
|
440
|
+
return formatter(ctx);
|
|
441
|
+
}
|
|
442
|
+
const table = (_options$tooltip0 = options.tooltip) === null || _options$tooltip0 === void 0 ? void 0 : _options$tooltip0.table;
|
|
443
|
+
return table ? tableTooltip(ctx, table, formatX) : defaultTooltip(ctx, formatX, formatValue);
|
|
444
|
+
}, [hover, model, (_options$title2 = options.title) === null || _options$title2 === void 0 ? void 0 : _options$title2.text, options.meta, (_options$tooltip1 = options.tooltip) === null || _options$tooltip1 === void 0 ? void 0 : _options$tooltip1.formatter, (_options$tooltip10 = options.tooltip) === null || _options$tooltip10 === void 0 ? void 0 : _options$tooltip10.table, formatX, formatValue]);
|
|
445
|
+
const hasData = (model === null || model === void 0 ? void 0 : model.series.some(series => series.points.some(point => point.y !== null))) ?? false;
|
|
446
|
+
const plotBorderWidth = ((_options$chart3 = options.chart) === null || _options$chart3 === void 0 ? void 0 : _options$chart3.plotBorderWidth) ?? 0;
|
|
447
|
+
const scrolls = canvas.width > size.width || canvas.height > size.height;
|
|
448
|
+
// Only horizontal scrolling pins anything, and only a left-hand y axis: that is
|
|
449
|
+
// the one whose labels the plot would otherwise slide out from under. A right-hand
|
|
450
|
+
// axis and a vertically scrolling chart both stay in the scrolling canvas.
|
|
451
|
+
const pinsAxes = canvas.width > size.width && !((model === null || model === void 0 ? void 0 : model.axisless) ?? true);
|
|
452
|
+
const isPinned = axis => pinsAxes && axis.options.opposite !== true;
|
|
453
|
+
// Highcharts dims the series a hover is not on. Ours only does it when asked:
|
|
454
|
+
// switching it on by default would restyle every chart already drawn.
|
|
455
|
+
const inactiveOpacity = series => {
|
|
456
|
+
var _series$options$state;
|
|
457
|
+
const opacity = (_series$options$state = series.options.states) === null || _series$options$state === void 0 || (_series$options$state = _series$options$state.inactive) === null || _series$options$state === void 0 ? void 0 : _series$options$state.opacity;
|
|
458
|
+
if (opacity === undefined || !(hover !== null && hover !== void 0 && hover.hits.length)) {
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
return hover.hits.some(hit => hit.seriesIndex === series.index) ? undefined : opacity;
|
|
462
|
+
};
|
|
463
|
+
// `animation: false` is a zero-length transition rather than a separate code
|
|
464
|
+
// path, so nothing downstream has to know a series opted out.
|
|
465
|
+
const motionOf = series => {
|
|
466
|
+
const animation = series.options.animation;
|
|
467
|
+
if (animation === false) {
|
|
468
|
+
return 0;
|
|
469
|
+
}
|
|
470
|
+
return (animation === null || animation === void 0 ? void 0 : animation.duration) ?? MOTION_MS;
|
|
471
|
+
};
|
|
472
|
+
const decorations = layer => {
|
|
473
|
+
if (!model || model.axisless) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
return _jsxs(_Fragment, {
|
|
477
|
+
children: [_jsx(PlotDecorations, {
|
|
478
|
+
plot: model.plot,
|
|
479
|
+
axis: model.xAxis,
|
|
480
|
+
along: "x",
|
|
481
|
+
layer: layer
|
|
482
|
+
}), model.yAxes.map(axis => _jsx(PlotDecorations, {
|
|
483
|
+
plot: model.plot,
|
|
484
|
+
axis: axis,
|
|
485
|
+
along: "y",
|
|
486
|
+
layer: layer
|
|
487
|
+
}, `${axis.id}-${layer}`))]
|
|
488
|
+
});
|
|
489
|
+
};
|
|
490
|
+
return _jsxs(Container, {
|
|
491
|
+
...containerProps,
|
|
492
|
+
ref: container,
|
|
493
|
+
className: className,
|
|
494
|
+
"$width": toCssSize((_options$chart4 = options.chart) === null || _options$chart4 === void 0 ? void 0 : _options$chart4.width, '100%'),
|
|
495
|
+
"$height": toCssSize((_options$chart5 = options.chart) === null || _options$chart5 === void 0 ? void 0 : _options$chart5.height, '100%'),
|
|
496
|
+
"$autoHeight": ((_options$chart6 = options.chart) === null || _options$chart6 === void 0 ? void 0 : _options$chart6.height) === undefined,
|
|
497
|
+
"$background": (_options$chart7 = options.chart) === null || _options$chart7 === void 0 ? void 0 : _options$chart7.backgroundColor,
|
|
498
|
+
"$fontFamily": ((_options$chart8 = options.chart) === null || _options$chart8 === void 0 || (_options$chart8 = _options$chart8.style) === null || _options$chart8 === void 0 ? void 0 : _options$chart8.fontFamily) ?? DEFAULT_FONT_FAMILY,
|
|
499
|
+
"$fontSize": ((_options$chart9 = options.chart) === null || _options$chart9 === void 0 || (_options$chart9 = _options$chart9.style) === null || _options$chart9 === void 0 ? void 0 : _options$chart9.fontSize) ?? DEFAULT_FONT_SIZE,
|
|
500
|
+
children: [isLoading ? _jsx(Centred, {
|
|
501
|
+
children: loading ?? 'Loading…'
|
|
502
|
+
}) : null, !isLoading && model && !hasData ? _jsx(Centred, {
|
|
503
|
+
children: empty ?? 'No data'
|
|
504
|
+
}) : null, !isLoading && model && hasData ? _jsx(ScrollBox, {
|
|
505
|
+
"$scrolls": scrolls,
|
|
506
|
+
children: _jsxs(Canvas, {
|
|
507
|
+
width: canvas.width,
|
|
508
|
+
height: canvas.height,
|
|
509
|
+
role: "img",
|
|
510
|
+
"aria-label": ((_options$title3 = options.title) === null || _options$title3 === void 0 ? void 0 : _options$title3.text) ?? 'Chart',
|
|
511
|
+
onPointerMove: event => {
|
|
512
|
+
extendDrag(event);
|
|
513
|
+
handlePointerMove(event);
|
|
514
|
+
},
|
|
515
|
+
onPointerLeave: handlePointerLeave,
|
|
516
|
+
onPointerDown: zooms ? startDrag : undefined,
|
|
517
|
+
onClick: onPointClick ? handleClick : undefined,
|
|
518
|
+
"$clickable": Boolean(onPointClick),
|
|
519
|
+
children: [_jsx(Titles, {
|
|
520
|
+
entries: model.titles
|
|
521
|
+
}), model.axisless ? null : _jsx(Grid, {
|
|
522
|
+
plot: model.plot,
|
|
523
|
+
yAxis: model.yAxes[0]
|
|
524
|
+
}), model.axisless ? null : model.yAxes.filter(axis => !isPinned(axis)).map(axis => _jsx(YAxis, {
|
|
525
|
+
plot: model.plot,
|
|
526
|
+
axis: axis
|
|
527
|
+
}, axis.id)), model.axisless ? null : _jsx(XAxis, {
|
|
528
|
+
plot: model.plot,
|
|
529
|
+
axis: model.xAxis
|
|
530
|
+
}), decorations('under'), plotBorderWidth > 0 ? _jsx("rect", {
|
|
531
|
+
x: model.plot.x,
|
|
532
|
+
y: model.plot.y,
|
|
533
|
+
width: model.plot.width,
|
|
534
|
+
height: model.plot.height,
|
|
535
|
+
fill: "none",
|
|
536
|
+
stroke: ((_options$chart0 = options.chart) === null || _options$chart0 === void 0 ? void 0 : _options$chart0.plotBorderColor) ?? DEFAULT_GRID_COLOR,
|
|
537
|
+
strokeWidth: plotBorderWidth
|
|
538
|
+
}) : null, model.series.map(series => {
|
|
539
|
+
const ctx = contextFor(series);
|
|
540
|
+
return ctx ? _jsx(SeriesLayer, {
|
|
541
|
+
"data-series-index": series.index,
|
|
542
|
+
"$hidden": series.hidden,
|
|
543
|
+
"$collapse": series.renderer.exit === 'collapse',
|
|
544
|
+
"$selfAnimated": series.renderer.animatesGeometry === true,
|
|
545
|
+
"$inactive": inactiveOpacity(series),
|
|
546
|
+
"$motionMs": motionOf(series),
|
|
547
|
+
"$cursor": series.options.cursor ?? 'default',
|
|
548
|
+
"aria-hidden": series.hidden,
|
|
549
|
+
children: series.renderer.render(ctx)
|
|
550
|
+
}, `series-${series.index}`) : null;
|
|
551
|
+
}), decorations('over'), drag ? _jsx(ZoomBand, {
|
|
552
|
+
x: Math.min(drag.from, drag.to),
|
|
553
|
+
y: model.plot.y,
|
|
554
|
+
width: Math.abs(drag.to - drag.from),
|
|
555
|
+
height: model.plot.height
|
|
556
|
+
}) : null, _jsx(StackLabels, {
|
|
557
|
+
model: model
|
|
558
|
+
}), _jsx(Legend, {
|
|
559
|
+
items: model.legend,
|
|
560
|
+
options: options.legend,
|
|
561
|
+
plot: model.plot,
|
|
562
|
+
size: canvas,
|
|
563
|
+
spacing: model.spacing,
|
|
564
|
+
onToggle: toggleSeries ? toggle : undefined,
|
|
565
|
+
renderLabel: item => {
|
|
566
|
+
var _options$legend, _model$series$find;
|
|
567
|
+
const formatter = (_options$legend = options.legend) === null || _options$legend === void 0 ? void 0 : _options$legend.labelFormatter;
|
|
568
|
+
if (!formatter) {
|
|
569
|
+
return item.label;
|
|
570
|
+
}
|
|
571
|
+
return formatter({
|
|
572
|
+
seriesIndex: item.seriesIndex,
|
|
573
|
+
name: item.label,
|
|
574
|
+
color: item.color,
|
|
575
|
+
visible: item.visible,
|
|
576
|
+
meta: (_model$series$find = model.series.find(s => s.index === item.seriesIndex)) === null || _model$series$find === void 0 ? void 0 : _model$series$find.options.meta
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
})]
|
|
580
|
+
})
|
|
581
|
+
}) : null, !isLoading && model && hasData && pinsAxes ? _jsxs(PinnedAxes, {
|
|
582
|
+
width: model.plot.x + 1,
|
|
583
|
+
height: canvas.height,
|
|
584
|
+
"aria-hidden": true,
|
|
585
|
+
children: [_jsx("rect", {
|
|
586
|
+
x: 0,
|
|
587
|
+
y: 0,
|
|
588
|
+
width: model.plot.x,
|
|
589
|
+
height: canvas.height,
|
|
590
|
+
fill: ((_options$chart1 = options.chart) === null || _options$chart1 === void 0 ? void 0 : _options$chart1.backgroundColor) ?? DEFAULT_SURFACE_COLOR
|
|
591
|
+
}), model.yAxes.filter(isPinned).map(axis => _jsx(YAxis, {
|
|
592
|
+
plot: model.plot,
|
|
593
|
+
axis: axis
|
|
594
|
+
}, axis.id))]
|
|
595
|
+
}) : null, zoom ? _jsx(ResetZoom, {
|
|
596
|
+
type: "button",
|
|
597
|
+
onClick: resetZoom,
|
|
598
|
+
children: ((_options$chart10 = options.chart) === null || _options$chart10 === void 0 ? void 0 : _options$chart10.resetZoomText) ?? 'Reset zoom'
|
|
599
|
+
}) : null, hover && tooltipContent ? _jsx(Tooltip, {
|
|
600
|
+
at: hover.anchor,
|
|
601
|
+
pointer: hover.pointer,
|
|
602
|
+
options: options.tooltip,
|
|
603
|
+
children: tooltipContent
|
|
604
|
+
}) : null]
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
export function Chart(props) {
|
|
608
|
+
return _jsx(ChartErrorBoundary, {
|
|
609
|
+
fallback: _jsx(Centred, {
|
|
610
|
+
children: "Chart failed to render"
|
|
611
|
+
}),
|
|
612
|
+
children: _jsx(ChartBody, {
|
|
613
|
+
...props
|
|
614
|
+
})
|
|
615
|
+
});
|
|
616
|
+
}
|