@internetstiftelsen/charts 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -1
- package/dist/area.d.ts +11 -1
- package/dist/area.js +199 -55
- package/dist/bar.d.ts +26 -1
- package/dist/bar.js +425 -306
- package/dist/base-chart.d.ts +5 -0
- package/dist/base-chart.js +91 -67
- package/dist/chart-group.d.ts +16 -0
- package/dist/chart-group.js +201 -143
- package/dist/donut-center-content.d.ts +1 -0
- package/dist/donut-center-content.js +21 -38
- package/dist/donut-chart.js +32 -32
- package/dist/gauge-chart.d.ts +23 -4
- package/dist/gauge-chart.js +235 -185
- package/dist/lazy-mount.d.ts +13 -0
- package/dist/lazy-mount.js +90 -0
- package/dist/legend.js +10 -9
- package/dist/line.d.ts +9 -1
- package/dist/line.js +144 -24
- package/dist/pie-chart.d.ts +3 -0
- package/dist/pie-chart.js +49 -47
- package/dist/radial-chart-base.d.ts +4 -3
- package/dist/radial-chart-base.js +27 -12
- package/dist/scatter.d.ts +5 -1
- package/dist/scatter.js +92 -9
- package/dist/theme.js +17 -0
- package/dist/tooltip.d.ts +55 -3
- package/dist/tooltip.js +968 -159
- package/dist/types.d.ts +23 -1
- package/dist/utils.js +11 -19
- package/dist/x-axis.d.ts +10 -0
- package/dist/x-axis.js +190 -149
- package/dist/xy-animation.d.ts +3 -0
- package/dist/xy-animation.js +2 -0
- package/dist/xy-chart.d.ts +35 -1
- package/dist/xy-chart.js +358 -153
- package/dist/xy-motion/config.d.ts +2 -0
- package/dist/xy-motion/config.js +177 -0
- package/dist/xy-motion/driver.d.ts +9 -0
- package/dist/xy-motion/driver.js +10 -0
- package/dist/xy-motion/helpers.d.ts +17 -0
- package/dist/xy-motion/helpers.js +105 -0
- package/dist/xy-motion/live-state.d.ts +8 -0
- package/dist/xy-motion/live-state.js +240 -0
- package/dist/xy-motion/noop-xy-motion-driver.d.ts +9 -0
- package/dist/xy-motion/noop-xy-motion-driver.js +15 -0
- package/dist/xy-motion/types.d.ts +85 -0
- package/dist/xy-motion/types.js +1 -0
- package/dist/xy-motion/xy-motion-driver.d.ts +19 -0
- package/dist/xy-motion/xy-motion-driver.js +130 -0
- package/dist/y-axis.d.ts +7 -2
- package/dist/y-axis.js +99 -10
- package/docs/components.md +50 -1
- package/docs/getting-started.md +35 -0
- package/docs/theming.md +14 -0
- package/docs/xy-chart.md +88 -7
- package/package.json +5 -4
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const DEFAULT_ROOT_MARGIN = '200px 0px';
|
|
2
|
+
const DEFAULT_THRESHOLD = 0.01;
|
|
3
|
+
function resolveTarget(target) {
|
|
4
|
+
if (target instanceof HTMLElement) {
|
|
5
|
+
return target;
|
|
6
|
+
}
|
|
7
|
+
const container = document.querySelector(target);
|
|
8
|
+
if (!container) {
|
|
9
|
+
throw new Error(`Container "${target}" not found`);
|
|
10
|
+
}
|
|
11
|
+
if (!(container instanceof HTMLElement)) {
|
|
12
|
+
throw new Error(`Container "${target}" is not an HTMLElement`);
|
|
13
|
+
}
|
|
14
|
+
return container;
|
|
15
|
+
}
|
|
16
|
+
export function mountChartWhenVisible(target, factory, options = {}) {
|
|
17
|
+
const container = resolveTarget(target);
|
|
18
|
+
let mountedChart = null;
|
|
19
|
+
let pendingLoad = null;
|
|
20
|
+
let observer = null;
|
|
21
|
+
let destroyed = false;
|
|
22
|
+
const disconnectObserver = () => {
|
|
23
|
+
observer?.disconnect();
|
|
24
|
+
observer = null;
|
|
25
|
+
};
|
|
26
|
+
const handleError = (error) => {
|
|
27
|
+
if (options.onError) {
|
|
28
|
+
options.onError(error);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.error(error);
|
|
32
|
+
};
|
|
33
|
+
const load = async () => {
|
|
34
|
+
if (destroyed) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (mountedChart) {
|
|
38
|
+
return mountedChart;
|
|
39
|
+
}
|
|
40
|
+
if (pendingLoad) {
|
|
41
|
+
return pendingLoad;
|
|
42
|
+
}
|
|
43
|
+
disconnectObserver();
|
|
44
|
+
const currentLoad = Promise.resolve(factory(container))
|
|
45
|
+
.then((chart) => {
|
|
46
|
+
if (destroyed) {
|
|
47
|
+
chart.destroy();
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
mountedChart = chart;
|
|
51
|
+
return chart;
|
|
52
|
+
})
|
|
53
|
+
.catch((error) => {
|
|
54
|
+
handleError(error);
|
|
55
|
+
throw error;
|
|
56
|
+
})
|
|
57
|
+
.finally(() => {
|
|
58
|
+
if (pendingLoad === currentLoad && !mountedChart) {
|
|
59
|
+
pendingLoad = null;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
pendingLoad = currentLoad;
|
|
63
|
+
return currentLoad;
|
|
64
|
+
};
|
|
65
|
+
const destroy = () => {
|
|
66
|
+
destroyed = true;
|
|
67
|
+
disconnectObserver();
|
|
68
|
+
mountedChart?.destroy();
|
|
69
|
+
mountedChart = null;
|
|
70
|
+
};
|
|
71
|
+
observer = new IntersectionObserver((entries) => {
|
|
72
|
+
const isVisible = entries.some((entry) => {
|
|
73
|
+
return entry.isIntersecting || entry.intersectionRatio > 0;
|
|
74
|
+
});
|
|
75
|
+
if (!isVisible) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
void load().catch(() => { });
|
|
79
|
+
}, {
|
|
80
|
+
root: options.root ?? null,
|
|
81
|
+
rootMargin: options.rootMargin ?? DEFAULT_ROOT_MARGIN,
|
|
82
|
+
threshold: options.threshold ?? DEFAULT_THRESHOLD,
|
|
83
|
+
});
|
|
84
|
+
observer.observe(container);
|
|
85
|
+
return {
|
|
86
|
+
load,
|
|
87
|
+
destroy,
|
|
88
|
+
getChart: () => mountedChart,
|
|
89
|
+
};
|
|
90
|
+
}
|
package/dist/legend.js
CHANGED
|
@@ -106,15 +106,16 @@ export class Legend {
|
|
|
106
106
|
writable: true,
|
|
107
107
|
value: null
|
|
108
108
|
});
|
|
109
|
-
|
|
110
|
-
this.
|
|
111
|
-
this.
|
|
112
|
-
this.
|
|
113
|
-
this.
|
|
114
|
-
this.
|
|
115
|
-
this.
|
|
116
|
-
this.
|
|
117
|
-
this.
|
|
109
|
+
const { mode = 'inline', position = 'bottom', disconnectedTarget, marginTop = 20, marginBottom = 10, paddingX, itemSpacingX, itemSpacingY, exportHooks, } = config ?? {};
|
|
110
|
+
this.mode = mode;
|
|
111
|
+
this.position = position;
|
|
112
|
+
this.disconnectedTarget = disconnectedTarget;
|
|
113
|
+
this.marginTop = marginTop;
|
|
114
|
+
this.marginBottom = marginBottom;
|
|
115
|
+
this.paddingX = paddingX;
|
|
116
|
+
this.itemSpacingX = itemSpacingX;
|
|
117
|
+
this.itemSpacingY = itemSpacingY;
|
|
118
|
+
this.exportHooks = exportHooks;
|
|
118
119
|
this.stateController = new LegendStateController();
|
|
119
120
|
this.bindStateController(this.stateController);
|
|
120
121
|
}
|
package/dist/line.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
2
|
import type { LineConfig, DataItem, D3Scale, ScaleType, ChartTheme, LineValueLabelConfig, ExportHooks, LineConfigBase } from './types.js';
|
|
3
3
|
import type { ChartComponent } from './chart-interface.js';
|
|
4
|
+
import type { XYPointAnimationContext, XYPointSnapshot, XYSeriesRenderResult } from './xy-motion/types.js';
|
|
4
5
|
export declare class Line implements ChartComponent<LineConfigBase> {
|
|
5
6
|
readonly type: "line";
|
|
6
7
|
readonly dataKey: string;
|
|
@@ -11,6 +12,13 @@ export declare class Line implements ChartComponent<LineConfigBase> {
|
|
|
11
12
|
constructor(config: LineConfig);
|
|
12
13
|
getExportConfig(): LineConfigBase;
|
|
13
14
|
createExportComponent(override?: Partial<LineConfigBase>): ChartComponent<LineConfigBase>;
|
|
14
|
-
render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: D3Scale, y: D3Scale, parseValue: (value: unknown) => number, xScaleType: ScaleType | undefined, theme: ChartTheme):
|
|
15
|
+
render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: D3Scale, y: D3Scale, parseValue: (value: unknown) => number, xScaleType: ScaleType | undefined, theme: ChartTheme, animation?: XYPointAnimationContext): XYSeriesRenderResult<XYPointSnapshot>;
|
|
16
|
+
private buildLineData;
|
|
17
|
+
private buildAnimatedLineData;
|
|
18
|
+
private createSnapshot;
|
|
19
|
+
private renderLinePath;
|
|
20
|
+
private getInitialPathValue;
|
|
21
|
+
private createRevealTransitions;
|
|
22
|
+
private renderLinePoints;
|
|
15
23
|
private renderValueLabels;
|
|
16
24
|
}
|
package/dist/line.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { line } from 'd3';
|
|
2
2
|
import { sanitizeForCSS, mergeDeep } from './utils.js';
|
|
3
3
|
import { getScalePosition } from './scale-utils.js';
|
|
4
|
+
import { buildXYDatumSnapshotKeys, createTransitionCompletionPromise, createLeftToRightRevealTransition, getEnterStaggerTiming, } from './xy-motion/helpers.js';
|
|
4
5
|
export class Line {
|
|
5
6
|
constructor(config) {
|
|
6
7
|
Object.defineProperty(this, "type", {
|
|
@@ -60,7 +61,7 @@ export class Line {
|
|
|
60
61
|
exportHooks: this.exportHooks,
|
|
61
62
|
});
|
|
62
63
|
}
|
|
63
|
-
render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme) {
|
|
64
|
+
render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme, animation) {
|
|
64
65
|
const getXPosition = (d) => {
|
|
65
66
|
return (getScalePosition(x, d[xKey], xScaleType) +
|
|
66
67
|
(x.bandwidth ? x.bandwidth() / 2 : 0));
|
|
@@ -70,41 +71,158 @@ export class Line {
|
|
|
70
71
|
const value = d[this.dataKey];
|
|
71
72
|
return value !== null && value !== undefined;
|
|
72
73
|
};
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
const snapshotKeys = buildXYDatumSnapshotKeys(data, xKey);
|
|
75
|
+
const lineData = this.buildLineData(data, snapshotKeys, y, parseValue, getXPosition, hasValidValue);
|
|
76
|
+
const animatedLineData = this.buildAnimatedLineData(lineData, animation);
|
|
77
|
+
const validLineData = lineData.filter((entry) => entry.valid);
|
|
78
|
+
const validAnimatedLineData = animatedLineData.filter((entry) => {
|
|
79
|
+
return entry.valid;
|
|
80
|
+
});
|
|
81
|
+
const transitions = [
|
|
82
|
+
...this.renderLinePath(plotGroup, lineData, animatedLineData, theme, sanitizeForCSS(this.dataKey), animation),
|
|
83
|
+
...this.renderLinePoints(plotGroup, validLineData, validAnimatedLineData, theme, animation),
|
|
84
|
+
];
|
|
85
|
+
const snapshot = this.createSnapshot(validLineData);
|
|
86
|
+
// Render value labels if enabled (only for valid values)
|
|
87
|
+
if (this.valueLabel?.show) {
|
|
88
|
+
this.renderValueLabels(plotGroup, validLineData.map((entry) => entry.data), y, parseValue, theme, getXPosition);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
snapshot,
|
|
92
|
+
transitions,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
buildLineData(data, snapshotKeys, y, parseValue, getXPosition, hasValidValue) {
|
|
96
|
+
return data.map((entry, index) => {
|
|
97
|
+
const valid = hasValidValue(entry);
|
|
98
|
+
return {
|
|
99
|
+
data: entry,
|
|
100
|
+
valid,
|
|
101
|
+
snapshotKey: snapshotKeys[index] ?? String(index),
|
|
102
|
+
x: getXPosition(entry),
|
|
103
|
+
y: valid ? (y(parseValue(entry[this.dataKey])) ?? 0) : 0,
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
buildAnimatedLineData(lineData, animation) {
|
|
108
|
+
return lineData.map((entry) => {
|
|
109
|
+
if (!animation || !entry.valid) {
|
|
110
|
+
return entry;
|
|
111
|
+
}
|
|
112
|
+
const previousSnapshot = animation.previousSnapshot?.get(entry.snapshotKey);
|
|
113
|
+
return {
|
|
114
|
+
...entry,
|
|
115
|
+
x: previousSnapshot?.x ?? entry.x,
|
|
116
|
+
y: previousSnapshot?.y ?? animation.baselineY,
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
createSnapshot(lineData) {
|
|
121
|
+
const snapshot = new Map();
|
|
122
|
+
lineData.forEach((entry) => {
|
|
123
|
+
snapshot.set(entry.snapshotKey, {
|
|
124
|
+
x: entry.x,
|
|
125
|
+
y: entry.y,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
return snapshot;
|
|
129
|
+
}
|
|
130
|
+
renderLinePath(plotGroup, lineData, animatedLineData, theme, sanitizedKey, animation) {
|
|
77
131
|
const lineStrokeWidth = this.strokeWidth ?? theme.line.strokeWidth;
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
plotGroup
|
|
132
|
+
const lineGenerator = line()
|
|
133
|
+
.defined((entry) => entry.valid)
|
|
134
|
+
.x((entry) => entry.x)
|
|
135
|
+
.y((entry) => entry.y);
|
|
136
|
+
const finalPath = lineGenerator(lineData);
|
|
137
|
+
const linePath = plotGroup
|
|
84
138
|
.append('path')
|
|
85
|
-
.datum(
|
|
139
|
+
.datum(lineData)
|
|
140
|
+
.attr('class', `line-${sanitizedKey}`)
|
|
86
141
|
.attr('fill', 'none')
|
|
87
142
|
.attr('stroke', this.stroke)
|
|
88
143
|
.attr('stroke-width', lineStrokeWidth)
|
|
89
|
-
.attr('d', lineGenerator);
|
|
90
|
-
|
|
91
|
-
|
|
144
|
+
.attr('d', this.getInitialPathValue(finalPath, animatedLineData, lineGenerator, animation));
|
|
145
|
+
if (!animation || !finalPath) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
const revealTransitions = this.createRevealTransitions(linePath.node(), sanitizedKey, lineStrokeWidth, animation);
|
|
149
|
+
const transition = linePath
|
|
150
|
+
.transition()
|
|
151
|
+
.duration(animation.duration)
|
|
152
|
+
.ease(animation.easing)
|
|
153
|
+
.attr('d', finalPath);
|
|
154
|
+
return [
|
|
155
|
+
...revealTransitions,
|
|
156
|
+
createTransitionCompletionPromise(transition),
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
getInitialPathValue(finalPath, animatedLineData, lineGenerator, animation) {
|
|
160
|
+
if (animation?.mode === 'initial') {
|
|
161
|
+
return finalPath;
|
|
162
|
+
}
|
|
163
|
+
if (!animation) {
|
|
164
|
+
return finalPath;
|
|
165
|
+
}
|
|
166
|
+
return animation.previousPath ?? lineGenerator(animatedLineData);
|
|
167
|
+
}
|
|
168
|
+
createRevealTransitions(linePath, sanitizedKey, lineStrokeWidth, animation) {
|
|
169
|
+
if (animation.mode === 'initial') {
|
|
170
|
+
return createLeftToRightRevealTransition(linePath, animation.duration, animation.easing, `line-${sanitizedKey}-reveal`, lineStrokeWidth);
|
|
171
|
+
}
|
|
172
|
+
if (animation.previousRevealProgress == null) {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
return createLeftToRightRevealTransition(linePath, animation.duration, animation.easing, `line-${sanitizedKey}-reveal`, lineStrokeWidth, animation.previousRevealProgress);
|
|
176
|
+
}
|
|
177
|
+
renderLinePoints(plotGroup, validLineData, validAnimatedLineData, theme, animation) {
|
|
178
|
+
const pointSize = theme.line.point.size;
|
|
179
|
+
const pointStrokeWidth = theme.line.point.strokeWidth;
|
|
180
|
+
const pointStrokeColor = theme.line.point.strokeColor || this.stroke;
|
|
181
|
+
const pointColor = theme.line.point.color || this.stroke;
|
|
92
182
|
const sanitizedKey = sanitizeForCSS(this.dataKey);
|
|
93
|
-
|
|
183
|
+
const isInitialAnimation = animation?.mode === 'initial';
|
|
184
|
+
const circles = plotGroup
|
|
94
185
|
.selectAll(`.circle-${sanitizedKey}`)
|
|
95
|
-
.data(
|
|
186
|
+
.data(validLineData)
|
|
96
187
|
.join('circle')
|
|
97
188
|
.attr('class', `circle-${sanitizedKey}`)
|
|
98
|
-
.attr('cx',
|
|
99
|
-
|
|
100
|
-
|
|
189
|
+
.attr('cx', (_, index) => (isInitialAnimation
|
|
190
|
+
? validLineData[index]?.x
|
|
191
|
+
: animation
|
|
192
|
+
? validAnimatedLineData[index]?.x
|
|
193
|
+
: validLineData[index]?.x) ?? 0)
|
|
194
|
+
.attr('cy', (_, index) => (isInitialAnimation
|
|
195
|
+
? validLineData[index]?.y
|
|
196
|
+
: animation
|
|
197
|
+
? validAnimatedLineData[index]?.y
|
|
198
|
+
: validLineData[index]?.y) ?? 0)
|
|
199
|
+
.attr('r', isInitialAnimation ? 0 : pointSize)
|
|
101
200
|
.attr('fill', pointColor)
|
|
102
201
|
.attr('stroke', pointStrokeColor)
|
|
103
202
|
.attr('stroke-width', pointStrokeWidth);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
203
|
+
if (!animation) {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
if (isInitialAnimation) {
|
|
207
|
+
const transition = circles
|
|
208
|
+
.transition()
|
|
209
|
+
.delay((_, index) => {
|
|
210
|
+
return getEnterStaggerTiming(index, validLineData.length, animation.duration).delay;
|
|
211
|
+
})
|
|
212
|
+
.duration((_, index) => {
|
|
213
|
+
return getEnterStaggerTiming(index, validLineData.length, animation.duration).duration;
|
|
214
|
+
})
|
|
215
|
+
.ease(animation.easing)
|
|
216
|
+
.attr('r', pointSize);
|
|
217
|
+
return [createTransitionCompletionPromise(transition)];
|
|
107
218
|
}
|
|
219
|
+
const transition = circles
|
|
220
|
+
.transition()
|
|
221
|
+
.duration(animation.duration)
|
|
222
|
+
.ease(animation.easing)
|
|
223
|
+
.attr('cx', (_, index) => validLineData[index]?.x ?? 0)
|
|
224
|
+
.attr('cy', (_, index) => validLineData[index]?.y ?? 0);
|
|
225
|
+
return [createTransitionCompletionPromise(transition)];
|
|
108
226
|
}
|
|
109
227
|
renderValueLabels(plotGroup, data, y, parseValue, theme, getXPosition) {
|
|
110
228
|
const config = this.valueLabel;
|
|
@@ -123,7 +241,9 @@ export class Line {
|
|
|
123
241
|
const plotBottom = y.range()[0];
|
|
124
242
|
data.forEach((d) => {
|
|
125
243
|
const value = parseValue(d[this.dataKey]);
|
|
126
|
-
const valueText =
|
|
244
|
+
const valueText = config.formatter
|
|
245
|
+
? config.formatter(this.dataKey, value, d)
|
|
246
|
+
: String(value);
|
|
127
247
|
const xPos = getXPosition(d);
|
|
128
248
|
const yPos = y(value) || 0;
|
|
129
249
|
// Create temporary text to measure dimensions
|
package/dist/pie-chart.d.ts
CHANGED
|
@@ -46,6 +46,9 @@ export declare class PieChart extends RadialChartBase {
|
|
|
46
46
|
private segments;
|
|
47
47
|
constructor(config: PieChartConfig);
|
|
48
48
|
private validatePieData;
|
|
49
|
+
private validatePieConfig;
|
|
50
|
+
private validateValueLabelConfig;
|
|
51
|
+
private validateDataItems;
|
|
49
52
|
private prepareSegments;
|
|
50
53
|
private warnOnTinySlices;
|
|
51
54
|
protected getExportComponents(): ChartComponentBase[];
|
package/dist/pie-chart.js
CHANGED
|
@@ -8,6 +8,17 @@ const FULL_CIRCLE_RADIANS = Math.PI * 2;
|
|
|
8
8
|
const OUTSIDE_LABEL_TEXT_OFFSET_PX = 10;
|
|
9
9
|
const OUTSIDE_LABEL_LINE_INSET_PX = 4;
|
|
10
10
|
const TINY_SLICE_THRESHOLD_RATIO = 0.03;
|
|
11
|
+
const DEFAULT_PIE_VALUE_LABEL = {
|
|
12
|
+
show: false,
|
|
13
|
+
position: 'auto',
|
|
14
|
+
minInsidePercentage: 8,
|
|
15
|
+
outsideOffset: 16,
|
|
16
|
+
insideMargin: 8,
|
|
17
|
+
minVerticalSpacing: 14,
|
|
18
|
+
formatter: (label, value, _data, _percentage) => {
|
|
19
|
+
return `${label}: ${value}`;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
11
22
|
export class PieChart extends RadialChartBase {
|
|
12
23
|
constructor(config) {
|
|
13
24
|
super(config);
|
|
@@ -71,25 +82,31 @@ export class PieChart extends RadialChartBase {
|
|
|
71
82
|
writable: true,
|
|
72
83
|
value: []
|
|
73
84
|
});
|
|
74
|
-
const pieConfig =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
const pieConfig = {
|
|
86
|
+
innerRadius: 0,
|
|
87
|
+
startAngle: 0,
|
|
88
|
+
endAngle: FULL_CIRCLE_RADIANS,
|
|
89
|
+
padAngle: this.theme.donut.padAngle,
|
|
90
|
+
cornerRadius: this.theme.donut.cornerRadius,
|
|
91
|
+
sort: 'none',
|
|
92
|
+
...config.pie,
|
|
93
|
+
};
|
|
94
|
+
const resolvedConfig = {
|
|
95
|
+
valueKey: 'value',
|
|
96
|
+
labelKey: 'name',
|
|
97
|
+
...config,
|
|
98
|
+
};
|
|
99
|
+
this.innerRadiusRatio = pieConfig.innerRadius;
|
|
100
|
+
this.startAngle = pieConfig.startAngle;
|
|
101
|
+
this.endAngle = pieConfig.endAngle;
|
|
102
|
+
this.padAngle = pieConfig.padAngle;
|
|
103
|
+
this.cornerRadius = pieConfig.cornerRadius;
|
|
104
|
+
this.sort = pieConfig.sort;
|
|
105
|
+
this.valueKey = resolvedConfig.valueKey;
|
|
106
|
+
this.labelKey = resolvedConfig.labelKey;
|
|
84
107
|
this.valueLabel = {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
minInsidePercentage: config.valueLabel?.minInsidePercentage ?? 8,
|
|
88
|
-
outsideOffset: config.valueLabel?.outsideOffset ?? 16,
|
|
89
|
-
insideMargin: config.valueLabel?.insideMargin ?? 8,
|
|
90
|
-
minVerticalSpacing: config.valueLabel?.minVerticalSpacing ?? 14,
|
|
91
|
-
formatter: config.valueLabel?.formatter ??
|
|
92
|
-
((label, value, _data, _percentage) => `${label}: ${value}`),
|
|
108
|
+
...DEFAULT_PIE_VALUE_LABEL,
|
|
109
|
+
...config.valueLabel,
|
|
93
110
|
};
|
|
94
111
|
this.initializeDataState();
|
|
95
112
|
}
|
|
@@ -97,6 +114,11 @@ export class PieChart extends RadialChartBase {
|
|
|
97
114
|
ChartValidator.validateDataKey(this.data, this.labelKey, 'PieChart');
|
|
98
115
|
ChartValidator.validateDataKey(this.data, this.valueKey, 'PieChart');
|
|
99
116
|
ChartValidator.validateNumericData(this.data, this.valueKey, 'PieChart');
|
|
117
|
+
this.validatePieConfig();
|
|
118
|
+
this.validateValueLabelConfig();
|
|
119
|
+
this.validateDataItems();
|
|
120
|
+
}
|
|
121
|
+
validatePieConfig() {
|
|
100
122
|
if (this.innerRadiusRatio < 0 || this.innerRadiusRatio > 1) {
|
|
101
123
|
throw new Error(`PieChart: pie.innerRadius must be between 0 and 1, received '${this.innerRadiusRatio}'`);
|
|
102
124
|
}
|
|
@@ -106,6 +128,8 @@ export class PieChart extends RadialChartBase {
|
|
|
106
128
|
if (this.padAngle < 0) {
|
|
107
129
|
throw new Error(`PieChart: pie.padAngle must be >= 0, received '${this.padAngle}'`);
|
|
108
130
|
}
|
|
131
|
+
}
|
|
132
|
+
validateValueLabelConfig() {
|
|
109
133
|
if (this.valueLabel.minInsidePercentage < 0 ||
|
|
110
134
|
this.valueLabel.minInsidePercentage > 100) {
|
|
111
135
|
throw new Error(`PieChart: valueLabel.minInsidePercentage must be between 0 and 100, received '${this.valueLabel.minInsidePercentage}'`);
|
|
@@ -119,6 +143,8 @@ export class PieChart extends RadialChartBase {
|
|
|
119
143
|
if (this.valueLabel.minVerticalSpacing < 0) {
|
|
120
144
|
throw new Error(`PieChart: valueLabel.minVerticalSpacing must be >= 0, received '${this.valueLabel.minVerticalSpacing}'`);
|
|
121
145
|
}
|
|
146
|
+
}
|
|
147
|
+
validateDataItems() {
|
|
122
148
|
for (const [index, item] of this.data.entries()) {
|
|
123
149
|
const label = String(item[this.labelKey] ?? '').trim();
|
|
124
150
|
if (!label) {
|
|
@@ -263,9 +289,6 @@ export class PieChart extends RadialChartBase {
|
|
|
263
289
|
.attr('class', 'pie-segments')
|
|
264
290
|
.attr('transform', `translate(${cx}, ${cy})`);
|
|
265
291
|
const total = segments.reduce((sum, segment) => sum + segment.value, 0);
|
|
266
|
-
const resolveTooltipDiv = () => this.tooltip
|
|
267
|
-
? select(`#${this.tooltip.id}`)
|
|
268
|
-
: null;
|
|
269
292
|
const segmentSelection = segmentGroup
|
|
270
293
|
.selectAll('.pie-segment')
|
|
271
294
|
.data(pieData)
|
|
@@ -287,19 +310,10 @@ export class PieChart extends RadialChartBase {
|
|
|
287
310
|
segmentSelection
|
|
288
311
|
.filter((_, i, nodes) => nodes[i] !== target)
|
|
289
312
|
.style('opacity', 0.5);
|
|
290
|
-
|
|
291
|
-
if (tooltipDiv && !tooltipDiv.empty()) {
|
|
292
|
-
tooltipDiv
|
|
293
|
-
.style('visibility', 'visible')
|
|
294
|
-
.html(this.buildTooltipContent(d, segments));
|
|
295
|
-
this.positionTooltipFromPointer(event, tooltipDiv);
|
|
296
|
-
}
|
|
313
|
+
this.showTooltipFromPointer(event, this.buildTooltipContent(d, segments));
|
|
297
314
|
})
|
|
298
315
|
.on('mousemove', (event) => {
|
|
299
|
-
|
|
300
|
-
if (tooltipDiv && !tooltipDiv.empty()) {
|
|
301
|
-
this.positionTooltipFromPointer(event, tooltipDiv);
|
|
302
|
-
}
|
|
316
|
+
this.positionTooltipFromPointer(event);
|
|
303
317
|
})
|
|
304
318
|
.on('mouseleave', (event, d) => {
|
|
305
319
|
const target = event.currentTarget;
|
|
@@ -308,10 +322,7 @@ export class PieChart extends RadialChartBase {
|
|
|
308
322
|
.duration(ANIMATION_DURATION_MS)
|
|
309
323
|
.attr('d', arcGenerator(d));
|
|
310
324
|
segmentSelection.style('opacity', 1);
|
|
311
|
-
|
|
312
|
-
if (tooltipDiv && !tooltipDiv.empty()) {
|
|
313
|
-
tooltipDiv.style('visibility', 'hidden');
|
|
314
|
-
}
|
|
325
|
+
this.hideTooltip();
|
|
315
326
|
})
|
|
316
327
|
.on('focus', (event, d) => {
|
|
317
328
|
const target = event.currentTarget;
|
|
@@ -322,13 +333,7 @@ export class PieChart extends RadialChartBase {
|
|
|
322
333
|
segmentSelection
|
|
323
334
|
.filter((_, i, nodes) => nodes[i] !== target)
|
|
324
335
|
.style('opacity', 0.5);
|
|
325
|
-
|
|
326
|
-
if (tooltipDiv && !tooltipDiv.empty()) {
|
|
327
|
-
tooltipDiv
|
|
328
|
-
.style('visibility', 'visible')
|
|
329
|
-
.html(this.buildTooltipContent(d, segments));
|
|
330
|
-
this.positionTooltipAtElement(target, tooltipDiv);
|
|
331
|
-
}
|
|
336
|
+
this.showTooltipAtElement(target, this.buildTooltipContent(d, segments));
|
|
332
337
|
})
|
|
333
338
|
.on('blur', (event, d) => {
|
|
334
339
|
const target = event.currentTarget;
|
|
@@ -337,10 +342,7 @@ export class PieChart extends RadialChartBase {
|
|
|
337
342
|
.duration(ANIMATION_DURATION_MS)
|
|
338
343
|
.attr('d', arcGenerator(d));
|
|
339
344
|
segmentSelection.style('opacity', 1);
|
|
340
|
-
|
|
341
|
-
if (tooltipDiv && !tooltipDiv.empty()) {
|
|
342
|
-
tooltipDiv.style('visibility', 'hidden');
|
|
343
|
-
}
|
|
345
|
+
this.hideTooltip();
|
|
344
346
|
})
|
|
345
347
|
.on('keydown', (event) => {
|
|
346
348
|
this.handleSegmentKeyNavigation(event);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Selection } from 'd3';
|
|
2
1
|
import { BaseChart } from './base-chart.js';
|
|
3
2
|
import type { PlotAreaBounds } from './layout-manager.js';
|
|
4
3
|
import type { LegendSeries } from './types.js';
|
|
@@ -17,8 +16,10 @@ export declare abstract class RadialChartBase extends BaseChart {
|
|
|
17
16
|
fontScale: number;
|
|
18
17
|
};
|
|
19
18
|
protected getRadialLegendSeries<T extends RadialLegendItem>(items: T[]): LegendSeries[];
|
|
20
|
-
protected
|
|
21
|
-
protected
|
|
19
|
+
protected showTooltipFromPointer(event: MouseEvent, content: string): void;
|
|
20
|
+
protected positionTooltipFromPointer(event: MouseEvent): void;
|
|
21
|
+
protected showTooltipAtElement(target: Element, content: string): void;
|
|
22
|
+
protected hideTooltip(): void;
|
|
22
23
|
private applyTooltipPosition;
|
|
23
24
|
private resolveRadialFontScale;
|
|
24
25
|
}
|
|
@@ -27,26 +27,38 @@ export class RadialChartBase extends BaseChart {
|
|
|
27
27
|
fill: item.color,
|
|
28
28
|
}));
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
showTooltipFromPointer(event, content) {
|
|
31
|
+
if (!this.tooltip) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
this.tooltip.setContent(content);
|
|
35
|
+
this.positionTooltipFromPointer(event);
|
|
36
|
+
}
|
|
37
|
+
positionTooltipFromPointer(event) {
|
|
38
|
+
if (!this.tooltip) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const rect = this.tooltip.getBounds();
|
|
42
|
+
if (!rect) {
|
|
33
43
|
return;
|
|
34
44
|
}
|
|
35
|
-
const rect = node.getBoundingClientRect();
|
|
36
45
|
let x = event.pageX + TOOLTIP_OFFSET_PX;
|
|
37
46
|
const y = event.pageY - rect.height / 2;
|
|
38
47
|
if (x + rect.width > window.innerWidth - EDGE_MARGIN_PX) {
|
|
39
48
|
x = event.pageX - rect.width - TOOLTIP_OFFSET_PX;
|
|
40
49
|
}
|
|
41
|
-
this.applyTooltipPosition(
|
|
50
|
+
this.applyTooltipPosition(x, y, rect.height, rect.width);
|
|
42
51
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (!node) {
|
|
52
|
+
showTooltipAtElement(target, content) {
|
|
53
|
+
if (!this.tooltip) {
|
|
46
54
|
return;
|
|
47
55
|
}
|
|
56
|
+
this.tooltip.setContent(content);
|
|
48
57
|
const targetRect = target.getBoundingClientRect();
|
|
49
|
-
const tooltipRect =
|
|
58
|
+
const tooltipRect = this.tooltip.getBounds();
|
|
59
|
+
if (!tooltipRect) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
50
62
|
let x = targetRect.left +
|
|
51
63
|
window.scrollX +
|
|
52
64
|
targetRect.width / 2 +
|
|
@@ -62,12 +74,15 @@ export class RadialChartBase extends BaseChart {
|
|
|
62
74
|
tooltipRect.width -
|
|
63
75
|
TOOLTIP_OFFSET_PX;
|
|
64
76
|
}
|
|
65
|
-
this.applyTooltipPosition(
|
|
77
|
+
this.applyTooltipPosition(x, y, tooltipRect.height, tooltipRect.width);
|
|
78
|
+
}
|
|
79
|
+
hideTooltip() {
|
|
80
|
+
this.tooltip?.hide();
|
|
66
81
|
}
|
|
67
|
-
applyTooltipPosition(
|
|
82
|
+
applyTooltipPosition(rawX, rawY, height, width) {
|
|
68
83
|
const x = Math.max(EDGE_MARGIN_PX, Math.min(rawX, window.innerWidth + window.scrollX - width - EDGE_MARGIN_PX));
|
|
69
84
|
const y = Math.max(EDGE_MARGIN_PX, Math.min(rawY, window.innerHeight + window.scrollY - height - EDGE_MARGIN_PX));
|
|
70
|
-
|
|
85
|
+
this.tooltip?.showAt(x, y);
|
|
71
86
|
}
|
|
72
87
|
resolveRadialFontScale(outerRadius, theme) {
|
|
73
88
|
const referenceHeight = this.configuredHeight ?? DEFAULT_CHART_HEIGHT;
|
package/dist/scatter.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ChartTheme, D3Scale, DataItem, ExportHooks, LineValueLabelConfig, ScaleType, ScatterConfig, ScatterConfigBase } from './types.js';
|
|
2
2
|
import type { ChartComponent } from './chart-interface.js';
|
|
3
3
|
import type { Selection } from 'd3';
|
|
4
|
+
import type { XYPointAnimationContext, XYPointSnapshot, XYSeriesRenderResult } from './xy-motion/types.js';
|
|
4
5
|
export declare class Scatter implements ChartComponent<ScatterConfigBase> {
|
|
5
6
|
readonly type: "scatter";
|
|
6
7
|
readonly dataKey: string;
|
|
@@ -11,6 +12,9 @@ export declare class Scatter implements ChartComponent<ScatterConfigBase> {
|
|
|
11
12
|
constructor(config: ScatterConfig);
|
|
12
13
|
getExportConfig(): ScatterConfigBase;
|
|
13
14
|
createExportComponent(override?: Partial<ScatterConfigBase>): ChartComponent<ScatterConfigBase>;
|
|
14
|
-
render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: D3Scale, y: D3Scale, parseValue: (value: unknown) => number, xScaleType: ScaleType | undefined, theme: ChartTheme):
|
|
15
|
+
render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: D3Scale, y: D3Scale, parseValue: (value: unknown) => number, xScaleType: ScaleType | undefined, theme: ChartTheme, animation?: XYPointAnimationContext): XYSeriesRenderResult<XYPointSnapshot>;
|
|
16
|
+
private buildAnimatedData;
|
|
17
|
+
private createSnapshot;
|
|
18
|
+
private renderPoints;
|
|
15
19
|
private renderValueLabels;
|
|
16
20
|
}
|