@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,224 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useTweenedNumbers } from '../../core/animation/useTweenedNumbers';
|
|
3
|
+
import { normalizePoints } from '../../core/data/points';
|
|
4
|
+
import { applyValueFormat } from '../../core/format/time';
|
|
5
|
+
import { DEFAULT_SURFACE_COLOR } from '../../core/model/palette';
|
|
6
|
+
import { brighten } from '../shared/color';
|
|
7
|
+
const RADIUS_FRACTION = 0.85;
|
|
8
|
+
const LABEL_OFFSET = 12;
|
|
9
|
+
const HOVER_BRIGHTNESS = 0.12;
|
|
10
|
+
const FULL_TURN = Math.PI * 2;
|
|
11
|
+
const positive = point => {
|
|
12
|
+
const raw = point.value ?? point.y;
|
|
13
|
+
return raw !== null && Number.isFinite(raw) && raw > 0 ? raw : 0;
|
|
14
|
+
};
|
|
15
|
+
function geometryOf(ctx, radiusScale = 1) {
|
|
16
|
+
const {
|
|
17
|
+
plot
|
|
18
|
+
} = ctx;
|
|
19
|
+
const outer = Math.min(plot.width, plot.height) / 2 * RADIUS_FRACTION * radiusScale;
|
|
20
|
+
const requested = ctx.series.innerSize;
|
|
21
|
+
const fraction = typeof requested === 'string' ? Number.parseFloat(requested) / 100 : requested ?? 0;
|
|
22
|
+
return {
|
|
23
|
+
cx: plot.x + plot.width / 2,
|
|
24
|
+
cy: plot.y + plot.height / 2,
|
|
25
|
+
outer,
|
|
26
|
+
inner: outer * Math.min(0.95, Math.max(0, Number.isFinite(fraction) ? fraction : 0))
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function toSlices(ctx) {
|
|
30
|
+
const shown = point => !ctx.hiddenPoints.has(point.index);
|
|
31
|
+
const visibleTotal = ctx.points.reduce((sum, point) => sum + (shown(point) ? positive(point) : 0), 0);
|
|
32
|
+
// Nothing left on show: keep the angles the circle had and let the radius carry it
|
|
33
|
+
// away, so the last slice pulls into the centre rather than snapping shut.
|
|
34
|
+
const total = visibleTotal > 0 ? visibleTotal : ctx.points.reduce((sum, point) => sum + positive(point), 0);
|
|
35
|
+
if (total <= 0) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const slices = [];
|
|
39
|
+
let cursor = -Math.PI / 2;
|
|
40
|
+
ctx.points.forEach((point, index) => {
|
|
41
|
+
const value = positive(point);
|
|
42
|
+
if (value <= 0) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// A switched-off slice takes no angle: it closes like a fan while the slices
|
|
46
|
+
// after it slide into the room it leaves, and the shape of the path is the same
|
|
47
|
+
// throughout, which is what lets the browser interpolate it.
|
|
48
|
+
const sweep = shown(point) || visibleTotal <= 0 ? value / total * FULL_TURN : 0;
|
|
49
|
+
slices.push({
|
|
50
|
+
point,
|
|
51
|
+
from: cursor,
|
|
52
|
+
to: cursor + sweep,
|
|
53
|
+
color: point.color ?? ctx.colors[index % ctx.colors.length] ?? ctx.color,
|
|
54
|
+
hidden: !shown(point)
|
|
55
|
+
});
|
|
56
|
+
cursor += sweep;
|
|
57
|
+
});
|
|
58
|
+
return slices;
|
|
59
|
+
}
|
|
60
|
+
const polar = (cx, cy, radius, angle) => ({
|
|
61
|
+
x: cx + radius * Math.cos(angle),
|
|
62
|
+
y: cy + radius * Math.sin(angle)
|
|
63
|
+
});
|
|
64
|
+
function slicePath(slice, geometry) {
|
|
65
|
+
const {
|
|
66
|
+
cx,
|
|
67
|
+
cy,
|
|
68
|
+
outer,
|
|
69
|
+
inner
|
|
70
|
+
} = geometry;
|
|
71
|
+
const sweep = slice.to - slice.from;
|
|
72
|
+
const large = sweep > Math.PI ? 1 : 0;
|
|
73
|
+
const outerStart = polar(cx, cy, outer, slice.from);
|
|
74
|
+
const outerEnd = polar(cx, cy, outer, slice.to);
|
|
75
|
+
if (sweep >= FULL_TURN - 1e-9) {
|
|
76
|
+
const half = polar(cx, cy, outer, slice.from + Math.PI);
|
|
77
|
+
const ring = `M${outerStart.x} ${outerStart.y}` + `A${outer} ${outer} 0 1 1 ${half.x} ${half.y}` + `A${outer} ${outer} 0 1 1 ${outerStart.x} ${outerStart.y}Z`;
|
|
78
|
+
if (inner <= 0) {
|
|
79
|
+
return ring;
|
|
80
|
+
}
|
|
81
|
+
const innerStart = polar(cx, cy, inner, slice.from);
|
|
82
|
+
const innerHalf = polar(cx, cy, inner, slice.from + Math.PI);
|
|
83
|
+
return `${ring}M${innerStart.x} ${innerStart.y}` + `A${inner} ${inner} 0 1 0 ${innerHalf.x} ${innerHalf.y}` + `A${inner} ${inner} 0 1 0 ${innerStart.x} ${innerStart.y}Z`;
|
|
84
|
+
}
|
|
85
|
+
if (inner <= 0) {
|
|
86
|
+
return `M${cx} ${cy}L${outerStart.x} ${outerStart.y}` + `A${outer} ${outer} 0 ${large} 1 ${outerEnd.x} ${outerEnd.y}Z`;
|
|
87
|
+
}
|
|
88
|
+
const innerEnd = polar(cx, cy, inner, slice.to);
|
|
89
|
+
const innerStart = polar(cx, cy, inner, slice.from);
|
|
90
|
+
return `M${outerStart.x} ${outerStart.y}` + `A${outer} ${outer} 0 ${large} 1 ${outerEnd.x} ${outerEnd.y}` + `L${innerEnd.x} ${innerEnd.y}` + `A${inner} ${inner} 0 ${large} 0 ${innerStart.x} ${innerStart.y}Z`;
|
|
91
|
+
}
|
|
92
|
+
function Labels({
|
|
93
|
+
ctx,
|
|
94
|
+
slices,
|
|
95
|
+
geometry
|
|
96
|
+
}) {
|
|
97
|
+
const labels = ctx.series.dataLabels;
|
|
98
|
+
if (!(labels !== null && labels !== void 0 && labels.enabled)) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return slices.filter(slice => !slice.hidden).map(slice => {
|
|
102
|
+
var _labels$style, _labels$style2, _labels$style3, _labels$style4;
|
|
103
|
+
const mid = (slice.from + slice.to) / 2;
|
|
104
|
+
const at = polar(geometry.cx, geometry.cy, geometry.outer + LABEL_OFFSET, mid);
|
|
105
|
+
const text = labels.formatter ? labels.formatter({
|
|
106
|
+
x: slice.point.x,
|
|
107
|
+
y: slice.point.y,
|
|
108
|
+
value: slice.point.value,
|
|
109
|
+
pointIndex: slice.point.index,
|
|
110
|
+
seriesIndex: ctx.seriesIndex,
|
|
111
|
+
seriesName: ctx.series.name ?? '',
|
|
112
|
+
meta: ctx.series.meta
|
|
113
|
+
}) : applyValueFormat(labels.format ?? '{value}', slice.point.y ?? '');
|
|
114
|
+
return _jsx("text", {
|
|
115
|
+
x: at.x,
|
|
116
|
+
y: at.y,
|
|
117
|
+
textAnchor: Math.cos(mid) < 0 ? 'end' : 'start',
|
|
118
|
+
dominantBaseline: "middle",
|
|
119
|
+
fill: labels.color ?? ((_labels$style = labels.style) === null || _labels$style === void 0 ? void 0 : _labels$style.color),
|
|
120
|
+
fontFamily: (_labels$style2 = labels.style) === null || _labels$style2 === void 0 ? void 0 : _labels$style2.fontFamily,
|
|
121
|
+
fontSize: ((_labels$style3 = labels.style) === null || _labels$style3 === void 0 ? void 0 : _labels$style3.fontSize) ?? '11px',
|
|
122
|
+
fontWeight: (_labels$style4 = labels.style) === null || _labels$style4 === void 0 ? void 0 : _labels$style4.fontWeight,
|
|
123
|
+
children: text
|
|
124
|
+
}, `label-${slice.point.index}`);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The circle is drawn from tweened numbers: two angles per slice plus the radius of
|
|
129
|
+
* the whole pie. A slice that is switched off loses its angle and closes like a fan;
|
|
130
|
+
* a series that is switched off loses its radius and pulls into the centre. Because
|
|
131
|
+
* every frame regenerates the arc from those numbers, the shape is always a true
|
|
132
|
+
* arc — interpolating the path itself would bend it.
|
|
133
|
+
*/
|
|
134
|
+
function Slices({
|
|
135
|
+
ctx
|
|
136
|
+
}) {
|
|
137
|
+
var _ctx$hovered;
|
|
138
|
+
const slices = toSlices(ctx);
|
|
139
|
+
const alive = ctx.points.some(point => !ctx.hiddenPoints.has(point.index));
|
|
140
|
+
const targets = [...slices.flatMap(slice => [slice.from, slice.to]), ctx.hidden || !alive ? 0 : 1];
|
|
141
|
+
const tweened = useTweenedNumbers(targets);
|
|
142
|
+
if (!slices.length) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const radiusScale = tweened[tweened.length - 1] ?? 1;
|
|
146
|
+
const geometry = geometryOf(ctx, radiusScale);
|
|
147
|
+
const hovered = (_ctx$hovered = ctx.hovered) === null || _ctx$hovered === void 0 ? void 0 : _ctx$hovered.pointIndex;
|
|
148
|
+
const drawn = slices.map((slice, index) => ({
|
|
149
|
+
...slice,
|
|
150
|
+
from: tweened[index * 2] ?? slice.from,
|
|
151
|
+
to: tweened[index * 2 + 1] ?? slice.to
|
|
152
|
+
}));
|
|
153
|
+
return _jsxs("g", {
|
|
154
|
+
children: [drawn.map(slice => _jsx("path", {
|
|
155
|
+
d: slicePath(slice, geometry),
|
|
156
|
+
opacity: slice.hidden ? 0 : 1,
|
|
157
|
+
fill: slice.point.index === hovered ? brighten(slice.color, HOVER_BRIGHTNESS) : slice.color,
|
|
158
|
+
stroke: ctx.series.borderColor ?? DEFAULT_SURFACE_COLOR,
|
|
159
|
+
strokeWidth: ctx.series.borderWidth ?? 1
|
|
160
|
+
}, `slice-${slice.point.index}`)), _jsx(Labels, {
|
|
161
|
+
ctx: ctx,
|
|
162
|
+
slices: drawn,
|
|
163
|
+
geometry: geometry
|
|
164
|
+
})]
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
export const pie = {
|
|
168
|
+
type: 'pie',
|
|
169
|
+
scales: {
|
|
170
|
+
x: 'none',
|
|
171
|
+
y: 'none'
|
|
172
|
+
},
|
|
173
|
+
// The renderer tweens its own geometry; CSS must not transition it as well.
|
|
174
|
+
exit: 'collapse',
|
|
175
|
+
animatesGeometry: true,
|
|
176
|
+
extendDomain: (_series, acc) => acc,
|
|
177
|
+
render: ctx => _jsx(Slices, {
|
|
178
|
+
ctx: ctx
|
|
179
|
+
}),
|
|
180
|
+
hitTest: (ctx, pointer) => {
|
|
181
|
+
const slices = toSlices(ctx).filter(slice => !slice.hidden);
|
|
182
|
+
if (!slices.length) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const geometry = geometryOf(ctx);
|
|
186
|
+
const dx = pointer.x - geometry.cx;
|
|
187
|
+
const dy = pointer.y - geometry.cy;
|
|
188
|
+
const radius = Math.sqrt(dx * dx + dy * dy);
|
|
189
|
+
if (radius > geometry.outer || radius < geometry.inner) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const raw = Math.atan2(dy, dx);
|
|
193
|
+
const angle = raw < -Math.PI / 2 ? raw + FULL_TURN : raw;
|
|
194
|
+
const hit = slices.find(slice => angle >= slice.from && angle < slice.to);
|
|
195
|
+
if (!hit) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const mid = (hit.from + hit.to) / 2;
|
|
199
|
+
return [{
|
|
200
|
+
seriesIndex: ctx.seriesIndex,
|
|
201
|
+
pointIndex: hit.point.index,
|
|
202
|
+
x: hit.point.name ?? hit.point.x,
|
|
203
|
+
y: hit.point.y,
|
|
204
|
+
value: hit.point.value,
|
|
205
|
+
anchor: polar(geometry.cx, geometry.cy, (geometry.outer + geometry.inner) / 2, mid),
|
|
206
|
+
distance: 0
|
|
207
|
+
}];
|
|
208
|
+
},
|
|
209
|
+
legendItems: (series, seriesIndex, colors) => {
|
|
210
|
+
if (series.showInLegend === false) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
// One item per slice: a pie's legend keys the points, so each item toggles its
|
|
214
|
+
// own slice and the remaining ones share the circle out again.
|
|
215
|
+
return normalizePoints(series.data, series.name).map((point, index) => ({
|
|
216
|
+
seriesIndex,
|
|
217
|
+
pointIndex: point.index,
|
|
218
|
+
label: point.name ?? String(point.y ?? index + 1),
|
|
219
|
+
color: point.color ?? colors[index % colors.length] ?? '',
|
|
220
|
+
symbol: 'circle',
|
|
221
|
+
visible: true
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type Projected } from '../../core/geometry/path';
|
|
3
|
+
import { type DataLabelsOptions } from '../../core/types/options';
|
|
4
|
+
import { type RenderContext } from '../../core/types/renderer';
|
|
5
|
+
export declare const labelAnchor: (align: DataLabelsOptions["align"]) => "start" | "middle" | "end";
|
|
6
|
+
export interface DataLabelsProps {
|
|
7
|
+
ctx: RenderContext;
|
|
8
|
+
projected: readonly Projected[];
|
|
9
|
+
positionOf: (projected: Projected) => {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export declare function DataLabels({ ctx, projected, positionOf }: DataLabelsProps): ReactNode;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { applyValueFormat } from '../../core/format/time';
|
|
3
|
+
import { measureText, textHeight } from '../../core/layout/measure-text';
|
|
4
|
+
import { CHART_EDGE_GAP } from '../../core/layout/metrics';
|
|
5
|
+
import { DEFAULT_TEXT_COLOR } from '../../core/model/palette';
|
|
6
|
+
export const labelAnchor = align => {
|
|
7
|
+
if (align === 'left') {
|
|
8
|
+
return 'start';
|
|
9
|
+
}
|
|
10
|
+
if (align === 'right') {
|
|
11
|
+
return 'end';
|
|
12
|
+
}
|
|
13
|
+
return 'middle';
|
|
14
|
+
};
|
|
15
|
+
const textProps = style => ({
|
|
16
|
+
fontFamily: style === null || style === void 0 ? void 0 : style.fontFamily,
|
|
17
|
+
fontSize: style === null || style === void 0 ? void 0 : style.fontSize,
|
|
18
|
+
fontWeight: style === null || style === void 0 ? void 0 : style.fontWeight,
|
|
19
|
+
fontStyle: style === null || style === void 0 ? void 0 : style.fontStyle,
|
|
20
|
+
paintOrder: (style === null || style === void 0 ? void 0 : style.textOutline) === 'none' ? undefined : 'stroke'
|
|
21
|
+
});
|
|
22
|
+
/** No formatter and no format string: the chart's own number rule prints it. */
|
|
23
|
+
const defaultLabel = (ctx, format, y) => {
|
|
24
|
+
if (format) {
|
|
25
|
+
return applyValueFormat(format, y ?? '');
|
|
26
|
+
}
|
|
27
|
+
return y === null ? '' : ctx.numberFormat(y);
|
|
28
|
+
};
|
|
29
|
+
const boxOf = (lines, at, anchor, styles) => {
|
|
30
|
+
const width = lines.reduce((widest, text, index) => Math.max(widest, measureText(text, styles[Math.min(index, 1)])), 0);
|
|
31
|
+
const height = lines.reduce((total, _text, index) => total + textHeight(styles[Math.min(index, 1)]), 0);
|
|
32
|
+
const left = anchor === 'start' ? at.x : at.x - (anchor === 'middle' ? width / 2 : width);
|
|
33
|
+
return {
|
|
34
|
+
left,
|
|
35
|
+
right: left + width,
|
|
36
|
+
top: at.y - textHeight(styles[0]),
|
|
37
|
+
bottom: at.y - textHeight(styles[0]) + height
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
const overlaps = (a, b) => a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
|
|
41
|
+
export function DataLabels({
|
|
42
|
+
ctx,
|
|
43
|
+
projected,
|
|
44
|
+
positionOf
|
|
45
|
+
}) {
|
|
46
|
+
const options = ctx.series.dataLabels;
|
|
47
|
+
if (!(options !== null && options !== void 0 && options.enabled)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const turbo = ctx.series.turboThreshold;
|
|
51
|
+
if (turbo !== undefined && ctx.points.length > turbo) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const style = options.style;
|
|
55
|
+
const secondary = options.secondaryStyle;
|
|
56
|
+
const styles = [style, secondary ?? style];
|
|
57
|
+
const anchor = labelAnchor(options.align);
|
|
58
|
+
// A newline splits the label into stacked lines: a value with its share under
|
|
59
|
+
// it is one label, not two, so it moves and hides as one.
|
|
60
|
+
const linesOf = content => {
|
|
61
|
+
if (typeof content !== 'string' && typeof content !== 'number') {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return String(content).split('\n');
|
|
65
|
+
};
|
|
66
|
+
// Highcharts hides a label rather than let it collide. Only text can be
|
|
67
|
+
// measured, so a formatter returning an element is always drawn.
|
|
68
|
+
const placed = [];
|
|
69
|
+
const collides = (lines, at) => {
|
|
70
|
+
if (options.allowOverlap === true || !lines) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const box = boxOf(lines, at, anchor, styles);
|
|
74
|
+
if (placed.some(other => overlaps(box, other))) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
placed.push(box);
|
|
78
|
+
return false;
|
|
79
|
+
};
|
|
80
|
+
return projected.map(item => {
|
|
81
|
+
const {
|
|
82
|
+
point
|
|
83
|
+
} = item;
|
|
84
|
+
const content = options.formatter ? options.formatter({
|
|
85
|
+
x: point.x,
|
|
86
|
+
y: point.y,
|
|
87
|
+
value: point.value,
|
|
88
|
+
pointIndex: point.index,
|
|
89
|
+
seriesIndex: ctx.seriesIndex,
|
|
90
|
+
seriesName: ctx.series.name ?? `Series ${ctx.seriesIndex + 1}`,
|
|
91
|
+
meta: ctx.series.meta
|
|
92
|
+
}) : defaultLabel(ctx, options.format, point.y);
|
|
93
|
+
if (content === null || content === undefined || content === '') {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const lines = linesOf(content);
|
|
97
|
+
const at = positionOf(item);
|
|
98
|
+
// A label may sit above the plot area — that is what the headroom is for — but
|
|
99
|
+
// never against the chart edge, where it reads as glued to the border. Extra
|
|
100
|
+
// lines grow downwards, so only the first one has to clear the edge.
|
|
101
|
+
const stack = lines ? lines.length - 1 : 0;
|
|
102
|
+
const y = Math.max(at.y - stack * textHeight(secondary ?? style), textHeight(style) + CHART_EDGE_GAP);
|
|
103
|
+
if (collides(lines, {
|
|
104
|
+
x: at.x,
|
|
105
|
+
y
|
|
106
|
+
})) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return _jsx("text", {
|
|
110
|
+
x: at.x,
|
|
111
|
+
y: y,
|
|
112
|
+
textAnchor: anchor,
|
|
113
|
+
fill: options.color ?? (style === null || style === void 0 ? void 0 : style.color) ?? DEFAULT_TEXT_COLOR,
|
|
114
|
+
...textProps(style),
|
|
115
|
+
children: lines && lines.length > 1 ? lines.map((text, index) => _jsx("tspan", {
|
|
116
|
+
x: at.x,
|
|
117
|
+
dy: index === 0 ? 0 : textHeight(secondary ?? style),
|
|
118
|
+
fill: index === 0 ? undefined : secondary === null || secondary === void 0 ? void 0 : secondary.color,
|
|
119
|
+
...(index === 0 ? {} : textProps(secondary)),
|
|
120
|
+
children: text
|
|
121
|
+
}, `line-${index}`)) : content
|
|
122
|
+
}, `dl-${point.index}`);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type Projected } from '../../core/geometry/path';
|
|
3
|
+
import { type MarkerOptions } from '../../core/types/options';
|
|
4
|
+
export interface MarkersProps {
|
|
5
|
+
marker: MarkerOptions | undefined;
|
|
6
|
+
projected: readonly Projected[];
|
|
7
|
+
color: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function Markers({ marker, projected, color }: MarkersProps): ReactNode;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
const markerRadius = marker => (marker === null || marker === void 0 ? void 0 : marker.radius) ?? (marker !== null && marker !== void 0 && marker.width ? marker.width / 2 : 3);
|
|
3
|
+
export function Markers({
|
|
4
|
+
marker,
|
|
5
|
+
projected,
|
|
6
|
+
color
|
|
7
|
+
}) {
|
|
8
|
+
if ((marker === null || marker === void 0 ? void 0 : marker.enabled) === false || (marker === null || marker === void 0 ? void 0 : marker.symbol) === 'none') {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const radius = markerRadius(marker);
|
|
12
|
+
const fill = (marker === null || marker === void 0 ? void 0 : marker.fillColor) ?? color;
|
|
13
|
+
const square = (marker === null || marker === void 0 ? void 0 : marker.symbol) === 'square';
|
|
14
|
+
return projected.map(({
|
|
15
|
+
point,
|
|
16
|
+
px,
|
|
17
|
+
py
|
|
18
|
+
}) => square ? _jsx("rect", {
|
|
19
|
+
x: px - radius,
|
|
20
|
+
y: py - radius,
|
|
21
|
+
width: radius * 2,
|
|
22
|
+
height: radius * 2,
|
|
23
|
+
fill: fill,
|
|
24
|
+
stroke: marker === null || marker === void 0 ? void 0 : marker.lineColor,
|
|
25
|
+
strokeWidth: marker === null || marker === void 0 ? void 0 : marker.lineWidth
|
|
26
|
+
}, `m-${point.index}`) : _jsx("circle", {
|
|
27
|
+
cx: px,
|
|
28
|
+
cy: py,
|
|
29
|
+
r: radius,
|
|
30
|
+
fill: fill,
|
|
31
|
+
stroke: marker === null || marker === void 0 ? void 0 : marker.lineColor,
|
|
32
|
+
strokeWidth: marker === null || marker === void 0 ? void 0 : marker.lineWidth
|
|
33
|
+
}, `m-${point.index}`));
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function brighten(color: string, amount: number): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const clamp = value => Math.min(255, Math.max(0, Math.round(value)));
|
|
2
|
+
const parseHex = color => {
|
|
3
|
+
const match = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color.trim());
|
|
4
|
+
if (!match) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const hex = match[1].length === 3 ? match[1].split('').map(char => char + char).join('') : match[1];
|
|
8
|
+
return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
|
|
9
|
+
};
|
|
10
|
+
export function brighten(color, amount) {
|
|
11
|
+
const rgb = parseHex(color);
|
|
12
|
+
if (!rgb || !amount) {
|
|
13
|
+
return color;
|
|
14
|
+
}
|
|
15
|
+
const target = amount > 0 ? 255 : 0;
|
|
16
|
+
const ratio = Math.abs(amount);
|
|
17
|
+
const [r, g, b] = rgb.map(channel => clamp(channel + (target - channel) * ratio));
|
|
18
|
+
return `#${[r, g, b].map(channel => channel.toString(16).padStart(2, '0')).join('')}`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function includeZero(series, acc) {
|
|
2
|
+
const index = Math.min(series.yAxis ?? 0, acc.y.length - 1);
|
|
3
|
+
const target = acc.y[index];
|
|
4
|
+
if (!target) {
|
|
5
|
+
return acc;
|
|
6
|
+
}
|
|
7
|
+
return {
|
|
8
|
+
...acc,
|
|
9
|
+
y: acc.y.map((range, current) => current === index ? {
|
|
10
|
+
min: Math.min(range.min, 0),
|
|
11
|
+
max: Math.max(range.max, 0)
|
|
12
|
+
} : range)
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { pointKey } from '../../core/data/points';
|
|
2
|
+
import { drawable, projectSeries } from '../../core/geometry/path';
|
|
3
|
+
const HIT_RADIUS = 40;
|
|
4
|
+
export function nearestByX(ctx, pointer, radius = HIT_RADIUS) {
|
|
5
|
+
const projected = drawable(projectSeries(ctx));
|
|
6
|
+
if (!projected.length) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
let best;
|
|
10
|
+
let bestDistance = Infinity;
|
|
11
|
+
projected.forEach(candidate => {
|
|
12
|
+
const distance = Math.abs(candidate.px - pointer.x);
|
|
13
|
+
if (distance < bestDistance) {
|
|
14
|
+
best = candidate;
|
|
15
|
+
bestDistance = distance;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
if (!best || bestDistance > radius) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return [{
|
|
22
|
+
seriesIndex: ctx.seriesIndex,
|
|
23
|
+
pointIndex: best.point.index,
|
|
24
|
+
x: pointKey(best.point),
|
|
25
|
+
y: best.point.y,
|
|
26
|
+
value: best.point.value,
|
|
27
|
+
anchor: {
|
|
28
|
+
x: best.px,
|
|
29
|
+
y: best.py
|
|
30
|
+
},
|
|
31
|
+
distance: bestDistance
|
|
32
|
+
}];
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function seriesLegend(series, seriesIndex, symbol) {
|
|
2
|
+
if (series.showInLegend === false) {
|
|
3
|
+
return [];
|
|
4
|
+
}
|
|
5
|
+
return [{
|
|
6
|
+
seriesIndex,
|
|
7
|
+
label: series.name ?? `Series ${seriesIndex + 1}`,
|
|
8
|
+
color: series.color ?? '',
|
|
9
|
+
symbol: series.legendSymbol ?? symbol,
|
|
10
|
+
visible: series.visible !== false
|
|
11
|
+
}];
|
|
12
|
+
}
|