@internetstiftelsen/charts 0.13.3 → 0.14.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/README.md +7 -1
- package/dist/area.js +2 -1
- package/dist/bar.js +8 -4
- package/dist/base-chart.d.ts +8 -0
- package/dist/base-chart.js +79 -1
- package/dist/chart-group.d.ts +1 -0
- package/dist/chart-group.js +45 -4
- 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/word-cloud-chart.d.ts +30 -0
- package/dist/word-cloud-chart.js +207 -7
- 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/getting-started.md +3 -0
- package/docs/pie-chart.md +58 -16
- package/docs/theming.md +17 -12
- package/docs/word-cloud-chart.md +43 -8
- package/docs/xy-chart.md +10 -0
- package/package.json +26 -26
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
|
@@ -2,6 +2,12 @@ import { BaseChart, type BaseChartConfig, type BaseRenderContext } from './base-
|
|
|
2
2
|
import type { ChartData } from './types.js';
|
|
3
3
|
export type WordCloudRotationMode = 'none' | 'right-angle';
|
|
4
4
|
export type WordCloudSpiral = 'archimedean' | 'rectangular';
|
|
5
|
+
export type WordCloudAnimationEasingPreset = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce-out' | 'elastic-out' | 'spring-out';
|
|
6
|
+
export type WordCloudAnimationConfig = {
|
|
7
|
+
show?: boolean;
|
|
8
|
+
duration?: number;
|
|
9
|
+
easing?: WordCloudAnimationEasingPreset | `linear(${string})` | ((progress: number) => number);
|
|
10
|
+
};
|
|
5
11
|
export type WordCloudConfig = {
|
|
6
12
|
maxWords?: number;
|
|
7
13
|
minWordLength?: number;
|
|
@@ -14,19 +20,43 @@ export type WordCloudConfig = {
|
|
|
14
20
|
};
|
|
15
21
|
export type WordCloudChartConfig = Pick<BaseChartConfig, 'data' | 'width' | 'height' | 'theme' | 'responsive'> & {
|
|
16
22
|
wordCloud?: WordCloudConfig;
|
|
23
|
+
animate?: boolean | WordCloudAnimationConfig;
|
|
17
24
|
};
|
|
18
25
|
export declare class WordCloudChart extends BaseChart {
|
|
19
26
|
private readonly options;
|
|
27
|
+
private readonly animation;
|
|
20
28
|
private layout;
|
|
21
29
|
private layoutRunId;
|
|
22
30
|
private resolvePendingReady;
|
|
31
|
+
private hasRenderedLive;
|
|
32
|
+
private nextRenderShouldAnimate;
|
|
33
|
+
private previousWordSnapshot;
|
|
23
34
|
constructor(config: WordCloudChartConfig);
|
|
35
|
+
update(data: ChartData): void;
|
|
24
36
|
destroy(): void;
|
|
25
37
|
protected validateSourceData(data: ChartData): void;
|
|
26
38
|
protected renderChart({ svg, plotArea }: BaseRenderContext): void;
|
|
27
39
|
protected createExportChart(): BaseChart;
|
|
40
|
+
private prepareAnimationForUpdate;
|
|
28
41
|
private startLayout;
|
|
29
42
|
private renderWords;
|
|
43
|
+
private shouldAnimateWords;
|
|
44
|
+
private createInitialWordTransform;
|
|
45
|
+
private createInitialWordTransformState;
|
|
46
|
+
private createWordTransform;
|
|
47
|
+
private createWordTransformState;
|
|
48
|
+
private createWordPositionTransform;
|
|
49
|
+
private createInitialScaleTransform;
|
|
50
|
+
private createScaleTransform;
|
|
51
|
+
private createInitialWordOpacity;
|
|
52
|
+
private animateWords;
|
|
53
|
+
private createWordGroupAnimationEntries;
|
|
54
|
+
private createWordTextAnimationEntries;
|
|
55
|
+
private applyWordAnimationFrame;
|
|
56
|
+
private requestAnimationFrame;
|
|
57
|
+
private now;
|
|
58
|
+
private completeRender;
|
|
59
|
+
private createWordSnapshot;
|
|
30
60
|
private stopLayout;
|
|
31
61
|
private finishReady;
|
|
32
62
|
}
|
package/dist/word-cloud-chart.js
CHANGED
|
@@ -2,6 +2,7 @@ import cloud from 'd3-cloud';
|
|
|
2
2
|
import { scaleSqrt } from 'd3';
|
|
3
3
|
import { BaseChart, } from './base-chart.js';
|
|
4
4
|
import { isGroupedData } from './grouped-data.js';
|
|
5
|
+
import { normalizeRadialAnimationConfig, } from './radial-animation.js';
|
|
5
6
|
const DEFAULT_OPTIONS = {
|
|
6
7
|
maxWords: 75,
|
|
7
8
|
minWordLength: 1,
|
|
@@ -12,6 +13,7 @@ const DEFAULT_OPTIONS = {
|
|
|
12
13
|
rotation: undefined,
|
|
13
14
|
spiral: 'archimedean',
|
|
14
15
|
};
|
|
16
|
+
const INITIAL_WORD_SCALE = 0.2;
|
|
15
17
|
const GROUPED_DATA_ERROR = 'WordCloudChart: grouped datasets are not supported; provide a flat array of rows instead';
|
|
16
18
|
function createPreparedWords(data, plotArea, options, colors) {
|
|
17
19
|
const counts = new Map();
|
|
@@ -61,6 +63,12 @@ export class WordCloudChart extends BaseChart {
|
|
|
61
63
|
writable: true,
|
|
62
64
|
value: void 0
|
|
63
65
|
});
|
|
66
|
+
Object.defineProperty(this, "animation", {
|
|
67
|
+
enumerable: true,
|
|
68
|
+
configurable: true,
|
|
69
|
+
writable: true,
|
|
70
|
+
value: void 0
|
|
71
|
+
});
|
|
64
72
|
Object.defineProperty(this, "layout", {
|
|
65
73
|
enumerable: true,
|
|
66
74
|
configurable: true,
|
|
@@ -79,6 +87,24 @@ export class WordCloudChart extends BaseChart {
|
|
|
79
87
|
writable: true,
|
|
80
88
|
value: null
|
|
81
89
|
});
|
|
90
|
+
Object.defineProperty(this, "hasRenderedLive", {
|
|
91
|
+
enumerable: true,
|
|
92
|
+
configurable: true,
|
|
93
|
+
writable: true,
|
|
94
|
+
value: false
|
|
95
|
+
});
|
|
96
|
+
Object.defineProperty(this, "nextRenderShouldAnimate", {
|
|
97
|
+
enumerable: true,
|
|
98
|
+
configurable: true,
|
|
99
|
+
writable: true,
|
|
100
|
+
value: false
|
|
101
|
+
});
|
|
102
|
+
Object.defineProperty(this, "previousWordSnapshot", {
|
|
103
|
+
enumerable: true,
|
|
104
|
+
configurable: true,
|
|
105
|
+
writable: true,
|
|
106
|
+
value: new Map()
|
|
107
|
+
});
|
|
82
108
|
const wordCloud = config.wordCloud ?? {};
|
|
83
109
|
this.options = {
|
|
84
110
|
maxWords: wordCloud.maxWords ?? DEFAULT_OPTIONS.maxWords,
|
|
@@ -90,8 +116,16 @@ export class WordCloudChart extends BaseChart {
|
|
|
90
116
|
rotation: wordCloud.rotation,
|
|
91
117
|
spiral: wordCloud.spiral ?? DEFAULT_OPTIONS.spiral,
|
|
92
118
|
};
|
|
119
|
+
this.animation = normalizeRadialAnimationConfig(config.animate, 'WordCloudChart');
|
|
120
|
+
this.nextRenderShouldAnimate = this.animation.show;
|
|
93
121
|
this.initializeDataState();
|
|
94
122
|
}
|
|
123
|
+
update(data) {
|
|
124
|
+
if (this.container) {
|
|
125
|
+
this.prepareAnimationForUpdate();
|
|
126
|
+
}
|
|
127
|
+
super.update(data);
|
|
128
|
+
}
|
|
95
129
|
destroy() {
|
|
96
130
|
this.layoutRunId += 1;
|
|
97
131
|
this.stopLayout();
|
|
@@ -120,8 +154,15 @@ export class WordCloudChart extends BaseChart {
|
|
|
120
154
|
theme: this.theme,
|
|
121
155
|
responsive: this.responsiveConfig,
|
|
122
156
|
wordCloud: this.options,
|
|
157
|
+
animate: false,
|
|
123
158
|
});
|
|
124
159
|
}
|
|
160
|
+
prepareAnimationForUpdate() {
|
|
161
|
+
this.nextRenderShouldAnimate =
|
|
162
|
+
this.animation.show &&
|
|
163
|
+
this.animation.duration > 0 &&
|
|
164
|
+
this.hasRenderedLive;
|
|
165
|
+
}
|
|
125
166
|
startLayout(words, plotArea, runId, resolve) {
|
|
126
167
|
const layout = cloud()
|
|
127
168
|
.words(words.map((word) => ({ ...word })))
|
|
@@ -146,8 +187,10 @@ export class WordCloudChart extends BaseChart {
|
|
|
146
187
|
if (placedWords.length < words.length) {
|
|
147
188
|
console.warn(`[Chart Warning] WordCloudChart: rendered ${placedWords.length} of ${words.length} words within the available area; reduce maxWords or font sizes to fit more words`);
|
|
148
189
|
}
|
|
149
|
-
this.renderWords(this.plotGroup, this.plotArea, placedWords);
|
|
150
|
-
this.
|
|
190
|
+
const transitions = this.renderWords(this.plotGroup, this.plotArea, placedWords);
|
|
191
|
+
this.completeRender(placedWords, transitions).then(() => {
|
|
192
|
+
this.finishReady(resolve);
|
|
193
|
+
});
|
|
151
194
|
});
|
|
152
195
|
if (this.options.rotation === 'none') {
|
|
153
196
|
layout.rotate(0);
|
|
@@ -170,27 +213,184 @@ export class WordCloudChart extends BaseChart {
|
|
|
170
213
|
.attr('fill', 'transparent')
|
|
171
214
|
.attr('stroke', 'none')
|
|
172
215
|
.attr('pointer-events', 'none');
|
|
173
|
-
plotGroup
|
|
216
|
+
const cloudGroup = plotGroup
|
|
174
217
|
.append('g')
|
|
175
218
|
.attr('class', 'word-cloud')
|
|
176
|
-
.attr('transform', `translate(${plotArea.width / 2}, ${plotArea.height / 2})`)
|
|
177
|
-
|
|
219
|
+
.attr('transform', `translate(${plotArea.width / 2}, ${plotArea.height / 2})`);
|
|
220
|
+
const wordGroupSelection = cloudGroup
|
|
221
|
+
.selectAll('g.word-cloud-word-wrapper')
|
|
178
222
|
.data(words)
|
|
179
|
-
.join('
|
|
223
|
+
.join('g')
|
|
224
|
+
.attr('class', 'word-cloud-word-wrapper')
|
|
225
|
+
.attr('transform', (word) => this.createWordTransform(word));
|
|
226
|
+
const wordSelection = wordGroupSelection
|
|
227
|
+
.append('text')
|
|
180
228
|
.attr('class', 'word-cloud-word')
|
|
181
229
|
.attr('text-anchor', 'middle')
|
|
182
230
|
.style('font-family', this.renderTheme.fontFamily)
|
|
183
231
|
.style('font-weight', String(this.renderTheme.valueLabel.fontWeight))
|
|
184
232
|
.style('font-size', (word) => `${word.size}px`)
|
|
185
233
|
.style('fill', (word) => word.color)
|
|
186
|
-
.attr('transform', (word) => `translate(${word.x ?? 0}, ${word.y ?? 0}) rotate(${word.rotate ?? 0})`)
|
|
187
234
|
.text((word) => word.text);
|
|
235
|
+
if (!this.shouldAnimateWords()) {
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
wordGroupSelection.attr('transform', (word) => this.createInitialWordTransform(word));
|
|
239
|
+
wordSelection.attr('transform', (word) => this.createInitialScaleTransform(word));
|
|
240
|
+
wordSelection.attr('opacity', (word) => String(this.createInitialWordOpacity(word)));
|
|
241
|
+
return [this.animateWords(wordGroupSelection, wordSelection)];
|
|
242
|
+
}
|
|
243
|
+
shouldAnimateWords() {
|
|
244
|
+
return (this.nextRenderShouldAnimate &&
|
|
245
|
+
this.animation.show &&
|
|
246
|
+
this.animation.duration > 0);
|
|
247
|
+
}
|
|
248
|
+
createInitialWordTransform(word) {
|
|
249
|
+
return this.createWordPositionTransform(this.createInitialWordTransformState(word));
|
|
250
|
+
}
|
|
251
|
+
createInitialWordTransformState(word) {
|
|
252
|
+
const previous = this.previousWordSnapshot.get(word.text);
|
|
253
|
+
if (previous) {
|
|
254
|
+
return {
|
|
255
|
+
x: previous.x,
|
|
256
|
+
y: previous.y,
|
|
257
|
+
rotate: previous.rotate,
|
|
258
|
+
scale: word.size > 0 ? previous.size / word.size : 1,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
x: word.x ?? 0,
|
|
263
|
+
y: word.y ?? 0,
|
|
264
|
+
rotate: word.rotate ?? 0,
|
|
265
|
+
scale: INITIAL_WORD_SCALE,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
createWordTransform(word) {
|
|
269
|
+
return this.createWordPositionTransform({
|
|
270
|
+
x: word.x ?? 0,
|
|
271
|
+
y: word.y ?? 0,
|
|
272
|
+
rotate: word.rotate ?? 0,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
createWordTransformState(word) {
|
|
276
|
+
return {
|
|
277
|
+
x: word.x ?? 0,
|
|
278
|
+
y: word.y ?? 0,
|
|
279
|
+
rotate: word.rotate ?? 0,
|
|
280
|
+
scale: 1,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
createWordPositionTransform(state) {
|
|
284
|
+
return `translate(${state.x}, ${state.y}) rotate(${state.rotate})`;
|
|
285
|
+
}
|
|
286
|
+
createInitialScaleTransform(word) {
|
|
287
|
+
return this.createScaleTransform(this.createInitialWordTransformState(word).scale);
|
|
288
|
+
}
|
|
289
|
+
createScaleTransform(scale) {
|
|
290
|
+
return `scale(${scale})`;
|
|
291
|
+
}
|
|
292
|
+
createInitialWordOpacity(word) {
|
|
293
|
+
return this.previousWordSnapshot.has(word.text) ? 1 : 0;
|
|
294
|
+
}
|
|
295
|
+
animateWords(wordGroupSelection, wordSelection) {
|
|
296
|
+
const groupEntries = this.createWordGroupAnimationEntries(wordGroupSelection);
|
|
297
|
+
const textEntries = this.createWordTextAnimationEntries(wordSelection);
|
|
298
|
+
const startTime = this.now();
|
|
299
|
+
return new Promise((resolve) => {
|
|
300
|
+
const tick = (currentTime) => {
|
|
301
|
+
const progress = Math.min(1, (currentTime - startTime) / this.animation.duration);
|
|
302
|
+
const easedProgress = this.animation.easing(progress);
|
|
303
|
+
this.applyWordAnimationFrame(groupEntries, textEntries, easedProgress);
|
|
304
|
+
if (progress >= 1) {
|
|
305
|
+
resolve();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
this.requestAnimationFrame(tick);
|
|
309
|
+
};
|
|
310
|
+
this.requestAnimationFrame(tick);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
createWordGroupAnimationEntries(wordGroupSelection) {
|
|
314
|
+
const entries = [];
|
|
315
|
+
wordGroupSelection.each((word, _index, nodes) => {
|
|
316
|
+
const node = nodes[_index];
|
|
317
|
+
entries.push({
|
|
318
|
+
node,
|
|
319
|
+
start: this.createInitialWordTransformState(word),
|
|
320
|
+
end: this.createWordTransformState(word),
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
return entries;
|
|
324
|
+
}
|
|
325
|
+
createWordTextAnimationEntries(wordSelection) {
|
|
326
|
+
const entries = [];
|
|
327
|
+
wordSelection.each((word, _index, nodes) => {
|
|
328
|
+
entries.push({
|
|
329
|
+
node: nodes[_index],
|
|
330
|
+
startScale: this.createInitialWordTransformState(word).scale,
|
|
331
|
+
startOpacity: this.createInitialWordOpacity(word),
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
return entries;
|
|
335
|
+
}
|
|
336
|
+
applyWordAnimationFrame(groupEntries, textEntries, progress) {
|
|
337
|
+
groupEntries.forEach((entry) => {
|
|
338
|
+
entry.node.setAttribute('transform', this.createWordPositionTransform({
|
|
339
|
+
x: entry.start.x + (entry.end.x - entry.start.x) * progress,
|
|
340
|
+
y: entry.start.y + (entry.end.y - entry.start.y) * progress,
|
|
341
|
+
rotate: entry.start.rotate +
|
|
342
|
+
(entry.end.rotate - entry.start.rotate) * progress,
|
|
343
|
+
}));
|
|
344
|
+
});
|
|
345
|
+
textEntries.forEach((entry) => {
|
|
346
|
+
entry.node.setAttribute('opacity', String(entry.startOpacity + (1 - entry.startOpacity) * progress));
|
|
347
|
+
entry.node.setAttribute('transform', this.createScaleTransform(entry.startScale + (1 - entry.startScale) * progress));
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
requestAnimationFrame(callback) {
|
|
351
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
352
|
+
requestAnimationFrame(callback);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
setTimeout(() => {
|
|
356
|
+
callback(this.now());
|
|
357
|
+
}, 16);
|
|
358
|
+
}
|
|
359
|
+
now() {
|
|
360
|
+
return typeof performance === 'undefined'
|
|
361
|
+
? Date.now()
|
|
362
|
+
: performance.now();
|
|
363
|
+
}
|
|
364
|
+
completeRender(words, transitions) {
|
|
365
|
+
this.previousWordSnapshot = this.createWordSnapshot(words);
|
|
366
|
+
this.hasRenderedLive = true;
|
|
367
|
+
this.nextRenderShouldAnimate = false;
|
|
368
|
+
if (transitions.length === 0) {
|
|
369
|
+
return Promise.resolve();
|
|
370
|
+
}
|
|
371
|
+
return Promise.allSettled(transitions).then(() => undefined);
|
|
372
|
+
}
|
|
373
|
+
createWordSnapshot(words) {
|
|
374
|
+
return new Map(words.map((word) => {
|
|
375
|
+
return [
|
|
376
|
+
word.text,
|
|
377
|
+
{
|
|
378
|
+
x: word.x ?? 0,
|
|
379
|
+
y: word.y ?? 0,
|
|
380
|
+
rotate: word.rotate ?? 0,
|
|
381
|
+
size: word.size,
|
|
382
|
+
},
|
|
383
|
+
];
|
|
384
|
+
}));
|
|
188
385
|
}
|
|
189
386
|
stopLayout() {
|
|
190
387
|
if (this.layout) {
|
|
191
388
|
this.layout.stop();
|
|
192
389
|
this.layout = null;
|
|
193
390
|
}
|
|
391
|
+
this.plotGroup
|
|
392
|
+
?.selectAll('.word-cloud-word-wrapper, .word-cloud-word')
|
|
393
|
+
.interrupt();
|
|
194
394
|
this.resolvePendingReady?.();
|
|
195
395
|
this.resolvePendingReady = null;
|
|
196
396
|
}
|
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
|
|