@internetstiftelsen/charts 0.13.3 → 0.14.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 +4 -1
- package/dist/area.js +2 -1
- package/dist/bar.js +8 -4
- package/dist/base-chart.d.ts +1 -0
- package/dist/base-chart.js +2 -0
- package/dist/donut-chart.d.ts +19 -3
- package/dist/donut-chart.js +129 -25
- package/dist/easing.d.ts +1 -0
- package/dist/easing.js +30 -0
- package/dist/gauge-chart.d.ts +7 -2
- package/dist/gauge-chart.js +43 -18
- package/dist/line.js +2 -1
- package/dist/pie-chart.d.ts +19 -3
- package/dist/pie-chart.js +160 -59
- package/dist/radial-animation.d.ts +69 -0
- package/dist/radial-animation.js +416 -0
- package/dist/radial-chart-base.d.ts +24 -1
- package/dist/radial-chart-base.js +181 -0
- package/dist/scatter.js +2 -1
- package/dist/theme.d.ts +15 -0
- package/dist/theme.js +90 -4
- package/dist/types.d.ts +1 -0
- package/dist/xy-motion/config.js +3 -0
- package/dist/xy-motion/types.d.ts +1 -1
- package/docs/donut-chart.md +57 -14
- package/docs/gauge-chart.md +14 -0
- package/docs/pie-chart.md +58 -16
- package/docs/theming.md +17 -12
- package/docs/xy-chart.md +10 -0
- package/package.json +26 -26
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { BaseChart } from './base-chart.js';
|
|
2
2
|
import { DEFAULT_CHART_HEIGHT } from './theme.js';
|
|
3
|
+
import { measureTextWidth, truncateText, wrapText } from './utils.js';
|
|
3
4
|
const TOOLTIP_OFFSET_PX = 12;
|
|
4
5
|
const EDGE_MARGIN_PX = 10;
|
|
6
|
+
const DEFAULT_LABEL_FONT_SIZE = 14;
|
|
5
7
|
export class RadialChartBase extends BaseChart {
|
|
6
8
|
initializeTooltip() {
|
|
7
9
|
this.tooltip?.initialize(this.renderTheme);
|
|
@@ -27,6 +29,119 @@ export class RadialChartBase extends BaseChart {
|
|
|
27
29
|
fill: item.color,
|
|
28
30
|
}));
|
|
29
31
|
}
|
|
32
|
+
renderRadialLabelText(textElement, text, options, verticalAnchor = 'middle') {
|
|
33
|
+
const layout = this.resolveRadialLabelLayout(text, options);
|
|
34
|
+
textElement.text(null).style('visibility', null);
|
|
35
|
+
if (layout.hidden) {
|
|
36
|
+
textElement.text(text).style('visibility', 'hidden');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (layout.lines.length === 1) {
|
|
40
|
+
textElement.text(layout.lines[0]);
|
|
41
|
+
this.addRadialLabelTitle(textElement, layout.titleText);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const lineHeight = this.resolveRadialLabelLineHeight(options.fontSize);
|
|
45
|
+
const startDy = verticalAnchor === 'middle'
|
|
46
|
+
? -((layout.lines.length - 1) * lineHeight) / 2
|
|
47
|
+
: 0;
|
|
48
|
+
const x = textElement.attr('x') ?? '0';
|
|
49
|
+
layout.lines.forEach((line, index) => {
|
|
50
|
+
textElement
|
|
51
|
+
.append('tspan')
|
|
52
|
+
.attr('x', x)
|
|
53
|
+
.attr('dy', index === 0 ? `${startDy}px` : `${lineHeight}px`)
|
|
54
|
+
.text(line);
|
|
55
|
+
});
|
|
56
|
+
this.addRadialLabelTitle(textElement, layout.titleText);
|
|
57
|
+
}
|
|
58
|
+
renderRadialStructuredLabelText(textElement, labelText, valueText, separator, options, verticalAnchor = 'middle') {
|
|
59
|
+
const layout = this.resolveRadialLabelLayout(labelText, options);
|
|
60
|
+
const fullText = `${labelText}${separator}${valueText}`;
|
|
61
|
+
textElement.text(null).style('visibility', null);
|
|
62
|
+
if (layout.hidden) {
|
|
63
|
+
textElement.text(fullText).style('visibility', 'hidden');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (layout.lines.length === 1) {
|
|
67
|
+
textElement.text(`${layout.lines[0]}${separator}${valueText}`);
|
|
68
|
+
this.addRadialLabelTitle(textElement, layout.titleText ? fullText : null);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const lineHeight = this.resolveRadialLabelLineHeight(options.fontSize);
|
|
72
|
+
const startDy = verticalAnchor === 'middle'
|
|
73
|
+
? -((layout.lines.length - 1) * lineHeight) / 2
|
|
74
|
+
: 0;
|
|
75
|
+
const x = textElement.attr('x') ?? '0';
|
|
76
|
+
const lastLineIndex = layout.lines.length - 1;
|
|
77
|
+
layout.lines.forEach((line, index) => {
|
|
78
|
+
const renderedLine = index === lastLineIndex
|
|
79
|
+
? `${line}${separator}${valueText}`
|
|
80
|
+
: line;
|
|
81
|
+
textElement
|
|
82
|
+
.append('tspan')
|
|
83
|
+
.attr('x', x)
|
|
84
|
+
.attr('dy', index === 0 ? `${startDy}px` : `${lineHeight}px`)
|
|
85
|
+
.text(renderedLine);
|
|
86
|
+
});
|
|
87
|
+
this.addRadialLabelTitle(textElement, layout.titleText ? fullText : null);
|
|
88
|
+
}
|
|
89
|
+
measureRadialLabelDimensions(text, options) {
|
|
90
|
+
const layout = this.resolveRadialLabelLayout(text, options);
|
|
91
|
+
if (layout.hidden) {
|
|
92
|
+
return {
|
|
93
|
+
width: 0,
|
|
94
|
+
height: 0,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const svgNode = this.svg?.node();
|
|
98
|
+
const fontWeight = String(options.fontWeight);
|
|
99
|
+
const width = svgNode
|
|
100
|
+
? layout.lines.reduce((maxWidth, line) => {
|
|
101
|
+
return Math.max(maxWidth, measureTextWidth(line, options.fontSize, options.fontFamily, fontWeight, svgNode));
|
|
102
|
+
}, 0)
|
|
103
|
+
: (options.maxLabelWidth ?? 0);
|
|
104
|
+
return {
|
|
105
|
+
width,
|
|
106
|
+
height: Math.max(layout.lines.length, 1) *
|
|
107
|
+
this.resolveRadialLabelLineHeight(options.fontSize),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
measureRadialStructuredLabelDimensions(labelText, valueText, separator, options) {
|
|
111
|
+
const layout = this.resolveRadialLabelLayout(labelText, options);
|
|
112
|
+
if (layout.hidden) {
|
|
113
|
+
return {
|
|
114
|
+
width: 0,
|
|
115
|
+
height: 0,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const svgNode = this.svg?.node();
|
|
119
|
+
const fontWeight = String(options.fontWeight);
|
|
120
|
+
const lastLineIndex = layout.lines.length - 1;
|
|
121
|
+
const width = svgNode
|
|
122
|
+
? layout.lines.reduce((maxWidth, line, index) => {
|
|
123
|
+
const renderedLine = index === lastLineIndex
|
|
124
|
+
? `${line}${separator}${valueText}`
|
|
125
|
+
: line;
|
|
126
|
+
return Math.max(maxWidth, measureTextWidth(renderedLine, options.fontSize, options.fontFamily, fontWeight, svgNode));
|
|
127
|
+
}, 0)
|
|
128
|
+
: (options.maxLabelWidth ?? 0);
|
|
129
|
+
return {
|
|
130
|
+
width,
|
|
131
|
+
height: Math.max(layout.lines.length, 1) *
|
|
132
|
+
this.resolveRadialLabelLineHeight(options.fontSize),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
resolveRadialLabelLineHeight(fontSize) {
|
|
136
|
+
if (typeof fontSize === 'number' && Number.isFinite(fontSize)) {
|
|
137
|
+
return fontSize * 1.2;
|
|
138
|
+
}
|
|
139
|
+
const parsedFontSize = Number.parseFloat(String(fontSize));
|
|
140
|
+
if (Number.isFinite(parsedFontSize)) {
|
|
141
|
+
return parsedFontSize * 1.2;
|
|
142
|
+
}
|
|
143
|
+
return DEFAULT_LABEL_FONT_SIZE * 1.2;
|
|
144
|
+
}
|
|
30
145
|
showTooltipFromPointer(event, content) {
|
|
31
146
|
if (!this.tooltip) {
|
|
32
147
|
return;
|
|
@@ -84,6 +199,72 @@ export class RadialChartBase extends BaseChart {
|
|
|
84
199
|
const y = Math.max(EDGE_MARGIN_PX, Math.min(rawY, window.innerHeight + window.scrollY - height - EDGE_MARGIN_PX));
|
|
85
200
|
this.tooltip?.showAt(x, y);
|
|
86
201
|
}
|
|
202
|
+
resolveRadialLabelLayout(text, options) {
|
|
203
|
+
const overflowContext = this.resolveRadialLabelOverflowContext(options.maxLabelWidth);
|
|
204
|
+
if (!overflowContext) {
|
|
205
|
+
return this.createVisibleRadialLabelLayout(text);
|
|
206
|
+
}
|
|
207
|
+
const { maxLabelWidth, svgNode } = overflowContext;
|
|
208
|
+
const fontWeight = String(options.fontWeight);
|
|
209
|
+
const textWidth = measureTextWidth(text, options.fontSize, options.fontFamily, fontWeight, svgNode);
|
|
210
|
+
if (textWidth <= maxLabelWidth) {
|
|
211
|
+
return this.createVisibleRadialLabelLayout(text);
|
|
212
|
+
}
|
|
213
|
+
return this.resolveOverflowingRadialLabelLayout(text, options, maxLabelWidth, fontWeight, svgNode);
|
|
214
|
+
}
|
|
215
|
+
resolveRadialLabelOverflowContext(maxLabelWidth) {
|
|
216
|
+
const svgNode = this.svg?.node();
|
|
217
|
+
if (maxLabelWidth === undefined ||
|
|
218
|
+
!Number.isFinite(maxLabelWidth) ||
|
|
219
|
+
maxLabelWidth <= 0 ||
|
|
220
|
+
!svgNode) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
return { maxLabelWidth, svgNode };
|
|
224
|
+
}
|
|
225
|
+
createVisibleRadialLabelLayout(text) {
|
|
226
|
+
return {
|
|
227
|
+
lines: [text],
|
|
228
|
+
hidden: false,
|
|
229
|
+
titleText: null,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
resolveOverflowingRadialLabelLayout(text, options, maxLabelWidth, fontWeight, svgNode) {
|
|
233
|
+
if (options.oversizedBehavior === 'hide') {
|
|
234
|
+
if (options.forceVisible) {
|
|
235
|
+
return {
|
|
236
|
+
lines: [text],
|
|
237
|
+
hidden: false,
|
|
238
|
+
titleText: null,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
lines: [text],
|
|
243
|
+
hidden: true,
|
|
244
|
+
titleText: null,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (options.oversizedBehavior === 'wrap') {
|
|
248
|
+
const lines = wrapText(text, maxLabelWidth, options.fontSize, options.fontFamily, fontWeight, svgNode);
|
|
249
|
+
return {
|
|
250
|
+
lines,
|
|
251
|
+
hidden: false,
|
|
252
|
+
titleText: lines.length > 1 ? text : null,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
const result = truncateText(text, maxLabelWidth, options.fontSize, options.fontFamily, fontWeight, svgNode);
|
|
256
|
+
return {
|
|
257
|
+
lines: [result.text],
|
|
258
|
+
hidden: false,
|
|
259
|
+
titleText: result.truncated ? text : null,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
addRadialLabelTitle(textElement, text) {
|
|
263
|
+
if (!text) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
textElement.append('title').text(text);
|
|
267
|
+
}
|
|
87
268
|
resolveRadialFontScale(outerRadius, theme) {
|
|
88
269
|
const referenceHeight = this.configuredHeight ?? DEFAULT_CHART_HEIGHT;
|
|
89
270
|
const plotHeight = Math.max(1, referenceHeight - theme.margins.top - theme.margins.bottom);
|
package/dist/scatter.js
CHANGED
|
@@ -183,6 +183,7 @@ export class Scatter {
|
|
|
183
183
|
const border = config.border ?? theme.valueLabel.border;
|
|
184
184
|
const borderRadius = config.borderRadius ?? theme.valueLabel.borderRadius;
|
|
185
185
|
const padding = config.padding ?? theme.valueLabel.padding;
|
|
186
|
+
const forceVisible = config.forceVisible === true;
|
|
186
187
|
const labelGroup = plotGroup
|
|
187
188
|
.append('g')
|
|
188
189
|
.attr('class', `scatter-value-labels-${sanitizeForCSS(this.dataKey)}`);
|
|
@@ -211,7 +212,7 @@ export class Scatter {
|
|
|
211
212
|
if (labelY - boxHeight / 2 < plotTop + 4) {
|
|
212
213
|
labelY = yPos + boxHeight / 2 + pointSize + 4;
|
|
213
214
|
if (labelY + boxHeight / 2 > plotBottom - 4) {
|
|
214
|
-
shouldRender =
|
|
215
|
+
shouldRender = forceVisible;
|
|
215
216
|
}
|
|
216
217
|
}
|
|
217
218
|
tempText.remove();
|
package/dist/theme.d.ts
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { type ChartTheme, type ResponsiveConfig } from './types.js';
|
|
2
|
+
export declare const RUBY_COLOR_PALETTE: string[];
|
|
3
|
+
export declare const PEACOCK_COLOR_PALETTE: string[];
|
|
4
|
+
export declare const JADE_COLOR_PALETTE: string[];
|
|
5
|
+
export declare const LEMON_COLOR_PALETTE: string[];
|
|
6
|
+
export declare const OCEAN_COLOR_PALETTE: string[];
|
|
2
7
|
export declare const DEFAULT_COLOR_PALETTE: string[];
|
|
3
8
|
export declare const DEFAULT_CHART_WIDTH = 928;
|
|
4
9
|
export declare const DEFAULT_CHART_HEIGHT = 600;
|
|
5
10
|
export declare const defaultTheme: ChartTheme;
|
|
11
|
+
export declare const rubyTheme: ChartTheme;
|
|
12
|
+
export declare const peacockTheme: ChartTheme;
|
|
13
|
+
export declare const jadeTheme: ChartTheme;
|
|
14
|
+
export declare const lemonTheme: ChartTheme;
|
|
15
|
+
export declare const oceanTheme: ChartTheme;
|
|
6
16
|
export declare const newspaperTheme: ChartTheme;
|
|
7
17
|
export declare const defaultResponsiveConfig: ResponsiveConfig;
|
|
8
18
|
export declare const themes: {
|
|
9
19
|
default: ChartTheme;
|
|
20
|
+
ruby: ChartTheme;
|
|
21
|
+
peacock: ChartTheme;
|
|
22
|
+
jade: ChartTheme;
|
|
23
|
+
lemon: ChartTheme;
|
|
24
|
+
ocean: ChartTheme;
|
|
10
25
|
newspaper: ChartTheme;
|
|
11
26
|
};
|
package/dist/theme.js
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
const SYSTEM_FONT = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"';
|
|
2
|
+
export const RUBY_COLOR_PALETTE = [
|
|
3
|
+
'#ff4069',
|
|
4
|
+
'#c51f46',
|
|
5
|
+
'#ff7f99',
|
|
6
|
+
'#ffb3c2',
|
|
7
|
+
'#7f102d',
|
|
8
|
+
];
|
|
9
|
+
export const PEACOCK_COLOR_PALETTE = [
|
|
10
|
+
'#c27fec',
|
|
11
|
+
'#934bc5',
|
|
12
|
+
'#d5a4f3',
|
|
13
|
+
'#ead2fa',
|
|
14
|
+
'#5f287f',
|
|
15
|
+
];
|
|
16
|
+
export const JADE_COLOR_PALETTE = [
|
|
17
|
+
'#55c7b4',
|
|
18
|
+
'#2f8f80',
|
|
19
|
+
'#88dacd',
|
|
20
|
+
'#c4eee8',
|
|
21
|
+
'#1b5f55',
|
|
22
|
+
];
|
|
23
|
+
export const LEMON_COLOR_PALETTE = [
|
|
24
|
+
'#ffce2e',
|
|
25
|
+
'#c89200',
|
|
26
|
+
'#ffe07a',
|
|
27
|
+
'#fff0b8',
|
|
28
|
+
'#7a5900',
|
|
29
|
+
];
|
|
30
|
+
export const OCEAN_COLOR_PALETTE = [
|
|
31
|
+
'#50b2fc',
|
|
32
|
+
'#147eca',
|
|
33
|
+
'#8bcbfd',
|
|
34
|
+
'#c6e6fe',
|
|
35
|
+
'#0f4f7f',
|
|
36
|
+
];
|
|
1
37
|
export const DEFAULT_COLOR_PALETTE = [
|
|
2
38
|
'#50b2fc', // ocean
|
|
3
39
|
'#ff4069', // ruby
|
|
@@ -11,7 +47,7 @@ export const DEFAULT_COLOR_PALETTE = [
|
|
|
11
47
|
export const DEFAULT_CHART_WIDTH = 928;
|
|
12
48
|
export const DEFAULT_CHART_HEIGHT = 600;
|
|
13
49
|
export const defaultTheme = {
|
|
14
|
-
fontFamily:
|
|
50
|
+
fontFamily: SYSTEM_FONT,
|
|
15
51
|
margins: {
|
|
16
52
|
top: 20,
|
|
17
53
|
right: 20,
|
|
@@ -24,7 +60,7 @@ export const defaultTheme = {
|
|
|
24
60
|
},
|
|
25
61
|
colorPalette: [...DEFAULT_COLOR_PALETTE],
|
|
26
62
|
axis: {
|
|
27
|
-
fontFamily:
|
|
63
|
+
fontFamily: SYSTEM_FONT,
|
|
28
64
|
fontSize: 14,
|
|
29
65
|
fontWeight: 'normal',
|
|
30
66
|
groupLabel: {
|
|
@@ -72,7 +108,7 @@ export const defaultTheme = {
|
|
|
72
108
|
background: '#ffffff',
|
|
73
109
|
border: '#dddddd',
|
|
74
110
|
color: '#1f2a36',
|
|
75
|
-
fontFamily:
|
|
111
|
+
fontFamily: SYSTEM_FONT,
|
|
76
112
|
fontSize: 12,
|
|
77
113
|
fontWeight: 'normal',
|
|
78
114
|
},
|
|
@@ -86,7 +122,7 @@ export const defaultTheme = {
|
|
|
86
122
|
},
|
|
87
123
|
valueLabel: {
|
|
88
124
|
fontSize: 12,
|
|
89
|
-
fontFamily:
|
|
125
|
+
fontFamily: SYSTEM_FONT,
|
|
90
126
|
fontWeight: '600',
|
|
91
127
|
color: '#1f2a36',
|
|
92
128
|
background: '#ffffff',
|
|
@@ -117,6 +153,51 @@ export const defaultTheme = {
|
|
|
117
153
|
},
|
|
118
154
|
},
|
|
119
155
|
};
|
|
156
|
+
function cloneTheme(theme) {
|
|
157
|
+
return {
|
|
158
|
+
...theme,
|
|
159
|
+
margins: { ...theme.margins },
|
|
160
|
+
grid: { ...theme.grid },
|
|
161
|
+
colorPalette: [...theme.colorPalette],
|
|
162
|
+
axis: {
|
|
163
|
+
...theme.axis,
|
|
164
|
+
groupLabel: theme.axis.groupLabel
|
|
165
|
+
? { ...theme.axis.groupLabel }
|
|
166
|
+
: undefined,
|
|
167
|
+
},
|
|
168
|
+
legend: { ...theme.legend },
|
|
169
|
+
text: {
|
|
170
|
+
variants: Object.fromEntries(Object.entries(theme.text.variants).map(([name, style]) => [
|
|
171
|
+
name,
|
|
172
|
+
{ ...style },
|
|
173
|
+
])),
|
|
174
|
+
},
|
|
175
|
+
tooltip: { ...theme.tooltip },
|
|
176
|
+
line: {
|
|
177
|
+
...theme.line,
|
|
178
|
+
point: { ...theme.line.point },
|
|
179
|
+
},
|
|
180
|
+
valueLabel: { ...theme.valueLabel },
|
|
181
|
+
donut: {
|
|
182
|
+
...theme.donut,
|
|
183
|
+
centerContent: {
|
|
184
|
+
mainValue: { ...theme.donut.centerContent.mainValue },
|
|
185
|
+
title: { ...theme.donut.centerContent.title },
|
|
186
|
+
subtitle: { ...theme.donut.centerContent.subtitle },
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function createAccentTheme(colorPalette) {
|
|
192
|
+
const theme = cloneTheme(defaultTheme);
|
|
193
|
+
theme.colorPalette = [...colorPalette];
|
|
194
|
+
return theme;
|
|
195
|
+
}
|
|
196
|
+
export const rubyTheme = createAccentTheme(RUBY_COLOR_PALETTE);
|
|
197
|
+
export const peacockTheme = createAccentTheme(PEACOCK_COLOR_PALETTE);
|
|
198
|
+
export const jadeTheme = createAccentTheme(JADE_COLOR_PALETTE);
|
|
199
|
+
export const lemonTheme = createAccentTheme(LEMON_COLOR_PALETTE);
|
|
200
|
+
export const oceanTheme = createAccentTheme(OCEAN_COLOR_PALETTE);
|
|
120
201
|
export const newspaperTheme = {
|
|
121
202
|
fontFamily: 'Georgia, "Times New Roman", Times, serif',
|
|
122
203
|
margins: {
|
|
@@ -261,5 +342,10 @@ export const defaultResponsiveConfig = {
|
|
|
261
342
|
};
|
|
262
343
|
export const themes = {
|
|
263
344
|
default: defaultTheme,
|
|
345
|
+
ruby: rubyTheme,
|
|
346
|
+
peacock: peacockTheme,
|
|
347
|
+
jade: jadeTheme,
|
|
348
|
+
lemon: lemonTheme,
|
|
349
|
+
ocean: oceanTheme,
|
|
264
350
|
newspaper: newspaperTheme,
|
|
265
351
|
};
|
package/dist/types.d.ts
CHANGED
package/dist/xy-motion/config.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { ChartValidationError, ChartValidator } from '../validation.js';
|
|
2
2
|
import { easeBounceOut, easeCubicIn, easeCubicInOut, easeCubicOut, easeElasticOut, easeLinear, } from 'd3';
|
|
3
|
+
import { createCubicBezierEasing } from '../easing.js';
|
|
3
4
|
const DEFAULT_ANIMATE = false;
|
|
4
5
|
const DEFAULT_ANIMATION_DURATION_MS = 700;
|
|
5
6
|
const DEFAULT_ANIMATION_EASING_PRESET = 'ease-in-out';
|
|
7
|
+
const easeSpringOut = createCubicBezierEasing(0.85, 0, 0.15, 1);
|
|
6
8
|
const XY_ANIMATION_EASING_PRESETS = {
|
|
7
9
|
linear: easeLinear,
|
|
8
10
|
'ease-in': easeCubicIn,
|
|
@@ -10,6 +12,7 @@ const XY_ANIMATION_EASING_PRESETS = {
|
|
|
10
12
|
'ease-in-out': easeCubicInOut,
|
|
11
13
|
'bounce-out': easeBounceOut,
|
|
12
14
|
'elastic-out': easeElasticOut,
|
|
15
|
+
'spring-out': easeSpringOut,
|
|
13
16
|
};
|
|
14
17
|
export function normalizeXYAnimationConfig(config) {
|
|
15
18
|
if (config === undefined) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Selection } from 'd3';
|
|
2
|
-
export type XYAnimationEasingPreset = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce-out' | 'elastic-out';
|
|
2
|
+
export type XYAnimationEasingPreset = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce-out' | 'elastic-out' | 'spring-out';
|
|
3
3
|
export type XYAnimationConfig = {
|
|
4
4
|
show?: boolean;
|
|
5
5
|
duration?: number;
|
package/docs/donut-chart.md
CHANGED
|
@@ -10,17 +10,18 @@ new DonutChart(config: DonutChartConfig)
|
|
|
10
10
|
|
|
11
11
|
### Config Options
|
|
12
12
|
|
|
13
|
-
| Option | Type
|
|
14
|
-
| ------------ |
|
|
15
|
-
| `data` | `DataItem[]`
|
|
16
|
-
| `width` | `number`
|
|
17
|
-
| `height` | `number`
|
|
18
|
-
| `valueKey` | `string`
|
|
19
|
-
| `labelKey` | `string`
|
|
20
|
-
| `donut` | `DonutConfig`
|
|
21
|
-
| `valueLabel` | `DonutValueLabelConfig`
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
13
|
+
| Option | Type | Default | Description |
|
|
14
|
+
| ------------ | --------------------------------- | --------- | --------------------------------------------------------------------- |
|
|
15
|
+
| `data` | `DataItem[]` | required | Array of data objects |
|
|
16
|
+
| `width` | `number` | - | Explicit chart width in pixels |
|
|
17
|
+
| `height` | `number` | - | Explicit chart height in pixels |
|
|
18
|
+
| `valueKey` | `string` | `'value'` | Key for numeric values in data |
|
|
19
|
+
| `labelKey` | `string` | `'name'` | Key for segment labels in data |
|
|
20
|
+
| `donut` | `DonutConfig` | - | Donut-specific configuration |
|
|
21
|
+
| `valueLabel` | `DonutValueLabelConfig` | - | On-chart outside label/value rendering configuration |
|
|
22
|
+
| `animate` | `boolean \| DonutAnimationConfig` | `false` | Opt-in segment animation for initial render and `update()` |
|
|
23
|
+
| `theme` | `DeepPartial<ChartTheme>` | - | Theme customization |
|
|
24
|
+
| `responsive` | `ResponsiveConfig` | - | Declarative container-query responsive overrides (theme + components) |
|
|
24
25
|
|
|
25
26
|
### Donut Config
|
|
26
27
|
|
|
@@ -40,13 +41,50 @@ valueLabel: {
|
|
|
40
41
|
position?: 'outside' | 'auto', // default: 'auto'
|
|
41
42
|
outsideOffset?: number, // default: 16
|
|
42
43
|
minVerticalSpacing?: number, // default: 14
|
|
43
|
-
|
|
44
|
+
maxLabelWidth?: number,
|
|
45
|
+
oversizedBehavior?: 'truncate' | 'wrap' | 'hide', // default: 'truncate'
|
|
46
|
+
forceVisible?: boolean, // default: false
|
|
47
|
+
labelFormatter?: (label, value, data, percentage) => string,
|
|
48
|
+
valueFormatter?: (label, value, data, percentage) => string,
|
|
49
|
+
separator?: string, // default: ': '
|
|
50
|
+
formatter?: (label, value, data, percentage) => string,
|
|
44
51
|
}
|
|
45
52
|
```
|
|
46
53
|
|
|
47
54
|
Donut value labels are rendered outside the ring with leader lines. `auto`
|
|
48
55
|
currently resolves to the same outside placement as `outside`.
|
|
49
56
|
|
|
57
|
+
By default, donut value labels render as `{label}: {value}`. Use
|
|
58
|
+
`labelFormatter`, `valueFormatter`, and `separator` to customize those parts
|
|
59
|
+
while keeping the label and value structurally separate. When `maxLabelWidth` is
|
|
60
|
+
set, `oversizedBehavior` applies to the label part only, so long labels can be
|
|
61
|
+
truncated, wrapped, or hidden without truncating the value. The `percentage`
|
|
62
|
+
argument is the computed segment share from `0` to `100`.
|
|
63
|
+
|
|
64
|
+
Use `formatter` for full custom label text. When `formatter` is provided, the
|
|
65
|
+
returned string is treated as a single label and overflow behavior applies to
|
|
66
|
+
that whole string.
|
|
67
|
+
|
|
68
|
+
Set `forceVisible: true` to keep value labels rendered when
|
|
69
|
+
`oversizedBehavior: 'hide'` would normally hide them.
|
|
70
|
+
|
|
71
|
+
### Animation Config
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
animate: boolean | {
|
|
75
|
+
show?: boolean, // default: true when object is provided
|
|
76
|
+
duration?: number, // default: 700
|
|
77
|
+
easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' |
|
|
78
|
+
'bounce-out' | 'elastic-out' | 'spring-out' |
|
|
79
|
+
`linear(${string})` | ((progress: number) => number),
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Animation is off by default. When enabled, segments grow from zero length on the
|
|
84
|
+
first render and animate from their previous geometry on `chart.update(...)` and
|
|
85
|
+
legend visibility changes. Use `chart.whenReady()` when surrounding UI or tests
|
|
86
|
+
need to wait until the current animation has finished.
|
|
87
|
+
|
|
50
88
|
## Example
|
|
51
89
|
|
|
52
90
|
```javascript
|
|
@@ -73,8 +111,13 @@ const chart = new DonutChart({
|
|
|
73
111
|
},
|
|
74
112
|
valueLabel: {
|
|
75
113
|
show: true,
|
|
76
|
-
formatter: (label, _value, _data, percentage) =>
|
|
77
|
-
`${label}: ${percentage.toFixed(1)}
|
|
114
|
+
formatter: (label, _value, _data, percentage) => {
|
|
115
|
+
return `${label}: ${percentage.toFixed(1)}%`;
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
animate: {
|
|
119
|
+
duration: 700,
|
|
120
|
+
easing: 'ease-in-out',
|
|
78
121
|
},
|
|
79
122
|
});
|
|
80
123
|
|
package/docs/gauge-chart.md
CHANGED
|
@@ -40,6 +40,7 @@ gauge: {
|
|
|
40
40
|
| 'ease-in-out'
|
|
41
41
|
| 'bounce-out'
|
|
42
42
|
| 'elastic-out'
|
|
43
|
+
| 'spring-out'
|
|
43
44
|
| `linear(...)` // CSS-like piecewise linear easing
|
|
44
45
|
| ((t: number) => number),
|
|
45
46
|
},
|
|
@@ -60,6 +61,9 @@ gauge: {
|
|
|
60
61
|
fontFamily?: string, // default: theme.axis.fontFamily
|
|
61
62
|
fontWeight?: number | string, // default: 700
|
|
62
63
|
color?: string, // default: #111827
|
|
64
|
+
maxLabelWidth?: number,
|
|
65
|
+
oversizedBehavior?: 'truncate' | 'wrap' | 'hide', // default: 'truncate'
|
|
66
|
+
forceVisible?: boolean, // default: false
|
|
63
67
|
},
|
|
64
68
|
needle?: boolean | {
|
|
65
69
|
show?: boolean, // default: true
|
|
@@ -86,6 +90,9 @@ gauge: {
|
|
|
86
90
|
fontFamily?: string, // default: theme.axis.fontFamily
|
|
87
91
|
fontWeight?: number | string, // default: theme.axis.fontWeight
|
|
88
92
|
color?: string, // default: #4b5563
|
|
93
|
+
maxLabelWidth?: number,
|
|
94
|
+
oversizedBehavior?: 'truncate' | 'wrap' | 'hide', // default: 'truncate'
|
|
95
|
+
forceVisible?: boolean, // default: false
|
|
89
96
|
},
|
|
90
97
|
},
|
|
91
98
|
segments?: [
|
|
@@ -99,6 +106,11 @@ gauge: {
|
|
|
99
106
|
}
|
|
100
107
|
```
|
|
101
108
|
|
|
109
|
+
Set `maxLabelWidth` on `valueLabelStyle` or `ticks.labelStyle` to cap rendered
|
|
110
|
+
label width. `oversizedBehavior` controls whether labels over that cap are
|
|
111
|
+
truncated, wrapped, or hidden. Set `forceVisible: true` to keep labels rendered
|
|
112
|
+
when `oversizedBehavior: 'hide'` would normally hide them.
|
|
113
|
+
|
|
102
114
|
## Example
|
|
103
115
|
|
|
104
116
|
```javascript
|
|
@@ -151,6 +163,8 @@ chart.render('#gauge-container');
|
|
|
151
163
|
|
|
152
164
|
## Animation Easing Example
|
|
153
165
|
|
|
166
|
+
`spring-out` is a duration-based cubic-bezier preset.
|
|
167
|
+
|
|
154
168
|
```typescript
|
|
155
169
|
const chart = new GaugeChart({
|
|
156
170
|
data: [{ value: 72 }],
|
package/docs/pie-chart.md
CHANGED
|
@@ -10,17 +10,18 @@ new PieChart(config: PieChartConfig)
|
|
|
10
10
|
|
|
11
11
|
### Config Options
|
|
12
12
|
|
|
13
|
-
| Option | Type
|
|
14
|
-
| ------------ |
|
|
15
|
-
| `data` | `DataItem[]`
|
|
16
|
-
| `width` | `number`
|
|
17
|
-
| `height` | `number`
|
|
18
|
-
| `valueKey` | `string`
|
|
19
|
-
| `labelKey` | `string`
|
|
20
|
-
| `pie` | `PieConfig`
|
|
21
|
-
| `valueLabel` | `PieValueLabelConfig`
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
13
|
+
| Option | Type | Default | Description |
|
|
14
|
+
| ------------ | ------------------------------- | --------- | --------------------------------------------------------------------- |
|
|
15
|
+
| `data` | `DataItem[]` | required | Array of data objects |
|
|
16
|
+
| `width` | `number` | - | Explicit chart width in pixels |
|
|
17
|
+
| `height` | `number` | - | Explicit chart height in pixels |
|
|
18
|
+
| `valueKey` | `string` | `'value'` | Key for numeric values in data |
|
|
19
|
+
| `labelKey` | `string` | `'name'` | Key for segment labels in data |
|
|
20
|
+
| `pie` | `PieConfig` | - | Pie-specific configuration |
|
|
21
|
+
| `valueLabel` | `PieValueLabelConfig` | - | On-chart slice label/value rendering configuration |
|
|
22
|
+
| `animate` | `boolean \| PieAnimationConfig` | `false` | Opt-in slice animation for initial render and `update()` |
|
|
23
|
+
| `theme` | `DeepPartial<ChartTheme>` | - | Theme customization |
|
|
24
|
+
| `responsive` | `ResponsiveConfig` | - | Declarative container-query responsive overrides (theme + components) |
|
|
24
25
|
|
|
25
26
|
### Pie Config
|
|
26
27
|
|
|
@@ -45,11 +46,46 @@ valueLabel: {
|
|
|
45
46
|
outsideOffset?: number, // default: 16
|
|
46
47
|
insideMargin?: number, // default: 8
|
|
47
48
|
minVerticalSpacing?: number, // default: 14
|
|
48
|
-
|
|
49
|
+
maxLabelWidth?: number,
|
|
50
|
+
oversizedBehavior?: 'truncate' | 'wrap' | 'hide', // default: 'truncate'
|
|
51
|
+
forceVisible?: boolean, // default: false
|
|
52
|
+
labelFormatter?: (label, value, data, percentage) => string,
|
|
53
|
+
valueFormatter?: (label, value, data, percentage) => string,
|
|
54
|
+
separator?: string, // default: ': '
|
|
55
|
+
formatter?: (label, value, data, percentage) => string,
|
|
49
56
|
}
|
|
50
57
|
```
|
|
51
58
|
|
|
52
|
-
Rendered pie value-label text defaults to `{label}: {value}
|
|
59
|
+
Rendered pie value-label text defaults to `{label}: {value}`. Use
|
|
60
|
+
`labelFormatter`, `valueFormatter`, and `separator` to customize those parts
|
|
61
|
+
while keeping the label and value structurally separate. When `maxLabelWidth` is
|
|
62
|
+
set, `oversizedBehavior` applies to the label part only, so long labels can be
|
|
63
|
+
truncated, wrapped, or hidden without truncating the value. The `percentage`
|
|
64
|
+
argument is the computed slice share from `0` to `100`.
|
|
65
|
+
|
|
66
|
+
Use `formatter` for full custom label text. When `formatter` is provided, the
|
|
67
|
+
returned string is treated as a single label and overflow behavior applies to
|
|
68
|
+
that whole string.
|
|
69
|
+
|
|
70
|
+
Set `forceVisible: true` to keep value labels rendered when inside-fit checks or
|
|
71
|
+
`oversizedBehavior: 'hide'` would normally hide them.
|
|
72
|
+
|
|
73
|
+
### Animation Config
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
animate: boolean | {
|
|
77
|
+
show?: boolean, // default: true when object is provided
|
|
78
|
+
duration?: number, // default: 700
|
|
79
|
+
easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' |
|
|
80
|
+
'bounce-out' | 'elastic-out' | 'spring-out' |
|
|
81
|
+
`linear(${string})` | ((progress: number) => number),
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Animation is off by default. When enabled, slices grow from zero length on the
|
|
86
|
+
first render and animate from their previous geometry on `chart.update(...)` and
|
|
87
|
+
legend visibility changes. Use `chart.whenReady()` when surrounding UI or tests
|
|
88
|
+
need to wait until the current animation has finished.
|
|
53
89
|
|
|
54
90
|
## Example
|
|
55
91
|
|
|
@@ -76,8 +112,13 @@ const chart = new PieChart({
|
|
|
76
112
|
valueLabel: {
|
|
77
113
|
show: true,
|
|
78
114
|
position: 'auto',
|
|
79
|
-
formatter: (label, _value, _data, percentage) =>
|
|
80
|
-
`${label}: ${percentage.toFixed(1)}
|
|
115
|
+
formatter: (label, _value, _data, percentage) => {
|
|
116
|
+
return `${label}: ${percentage.toFixed(1)}%`;
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
animate: {
|
|
120
|
+
duration: 700,
|
|
121
|
+
easing: 'ease-in-out',
|
|
81
122
|
},
|
|
82
123
|
});
|
|
83
124
|
|
|
@@ -102,7 +143,8 @@ are negative, the chart throws an error because there is nothing to render.
|
|
|
102
143
|
|
|
103
144
|
When `valueLabel.position` is `inside`, the formatted value-label text that does not
|
|
104
145
|
fit inside its slice is hidden. In `auto` mode, labels that are too tight (based on
|
|
105
|
-
`insideMargin`) move outside instead.
|
|
146
|
+
`insideMargin`) move outside instead. Set `valueLabel.forceVisible: true` to
|
|
147
|
+
keep inside labels rendered even when they do not fit.
|
|
106
148
|
|
|
107
149
|
## Supported Components
|
|
108
150
|
|