@internetstiftelsen/charts 0.13.2 → 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/tooltip.d.ts +10 -1
- package/dist/tooltip.js +116 -59
- 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/tooltip.d.ts
CHANGED
|
@@ -81,6 +81,10 @@ export declare class Tooltip implements ChartComponent<TooltipConfigBase> {
|
|
|
81
81
|
private countPlacedLayoutsOnEdge;
|
|
82
82
|
private doSplitTooltipLayoutsOverlap;
|
|
83
83
|
private resolveSplitTooltipPositions;
|
|
84
|
+
private resolveHorizontalChartSplitTooltipPositions;
|
|
85
|
+
private resolveVerticalChartSplitTooltipPositions;
|
|
86
|
+
private getSplitTooltipViewportBounds;
|
|
87
|
+
private groupSplitTooltipLayoutsByEdge;
|
|
84
88
|
private resolveSideSplitTooltipPositions;
|
|
85
89
|
private resolveHorizontalSideSplitTooltipPositions;
|
|
86
90
|
private resolveHorizontalSideSplitTooltipCollisions;
|
|
@@ -91,6 +95,11 @@ export declare class Tooltip implements ChartComponent<TooltipConfigBase> {
|
|
|
91
95
|
private assignLayoutToHorizontalTooltipLane;
|
|
92
96
|
private doHorizontalTooltipLanesOverlap;
|
|
93
97
|
private doSplitTooltipLayoutsOverlapHorizontally;
|
|
94
|
-
private
|
|
98
|
+
private resolveHorizontalChartAboveBelowSplitTooltipPositions;
|
|
99
|
+
private resolvePackedAboveBelowTooltipRow;
|
|
100
|
+
private resolveVerticalChartAboveBelowSplitTooltipPositions;
|
|
101
|
+
private resolveCollisionAwareAboveBelowTooltipPositions;
|
|
102
|
+
private resolveNonOverlappingAboveBelowTooltipLeft;
|
|
103
|
+
private doesSplitTooltipOverlapPlacedLayouts;
|
|
95
104
|
}
|
|
96
105
|
export {};
|
package/dist/tooltip.js
CHANGED
|
@@ -466,10 +466,6 @@ export class Tooltip {
|
|
|
466
466
|
if (layouts.length === 0) {
|
|
467
467
|
return;
|
|
468
468
|
}
|
|
469
|
-
if (!isHorizontal) {
|
|
470
|
-
this.resolveSplitTooltipCollisions(layouts, 'vertical', this.getOppositeVerticalArrowEdge);
|
|
471
|
-
this.resolveSplitTooltipCollisions(layouts, 'side', this.getOppositeSideArrowEdge);
|
|
472
|
-
}
|
|
473
469
|
this.resolveSplitTooltipPositions(layouts, isHorizontal);
|
|
474
470
|
layouts.forEach((layout) => {
|
|
475
471
|
this.renderTooltipWithConnector(layout.div, layout.arrowEdge, layout.left, layout.top, layout.width, layout.height, layout.targetX, layout.targetY, layout.anchor);
|
|
@@ -1303,19 +1299,42 @@ export class Tooltip {
|
|
|
1303
1299
|
a.top + a.height + SPLIT_TOOLTIP_GAP_PX > b.top);
|
|
1304
1300
|
}
|
|
1305
1301
|
resolveSplitTooltipPositions(layouts, isHorizontal) {
|
|
1306
|
-
if (isHorizontal
|
|
1307
|
-
this.
|
|
1302
|
+
if (isHorizontal) {
|
|
1303
|
+
this.resolveHorizontalChartSplitTooltipPositions(layouts);
|
|
1308
1304
|
return;
|
|
1309
1305
|
}
|
|
1306
|
+
this.resolveVerticalChartSplitTooltipPositions(layouts);
|
|
1307
|
+
}
|
|
1308
|
+
resolveHorizontalChartSplitTooltipPositions(layouts) {
|
|
1310
1309
|
if (this.position === 'vertical') {
|
|
1311
|
-
this.
|
|
1310
|
+
this.resolveSplitTooltipCollisions(layouts, 'vertical', this.getOppositeVerticalArrowEdge);
|
|
1311
|
+
this.resolveHorizontalChartAboveBelowSplitTooltipPositions(layouts);
|
|
1312
1312
|
return;
|
|
1313
1313
|
}
|
|
1314
|
+
this.resolveHorizontalSideSplitTooltipPositions(layouts);
|
|
1315
|
+
}
|
|
1316
|
+
resolveVerticalChartSplitTooltipPositions(layouts) {
|
|
1317
|
+
if (this.position === 'vertical') {
|
|
1318
|
+
this.resolveSplitTooltipCollisions(layouts, 'vertical', this.getOppositeVerticalArrowEdge);
|
|
1319
|
+
this.resolveVerticalChartAboveBelowSplitTooltipPositions(layouts);
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
this.resolveSplitTooltipCollisions(layouts, 'side', this.getOppositeSideArrowEdge);
|
|
1314
1323
|
this.resolveSideSplitTooltipPositions(layouts);
|
|
1315
1324
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1325
|
+
getSplitTooltipViewportBounds() {
|
|
1326
|
+
return {
|
|
1327
|
+
minLeft: window.scrollX + TOOLTIP_VIEWPORT_PADDING_PX,
|
|
1328
|
+
maxRight: window.scrollX +
|
|
1329
|
+
window.innerWidth -
|
|
1330
|
+
TOOLTIP_VIEWPORT_PADDING_PX,
|
|
1331
|
+
minTop: window.scrollY + TOOLTIP_VIEWPORT_PADDING_PX,
|
|
1332
|
+
maxBottom: window.scrollY +
|
|
1333
|
+
window.innerHeight -
|
|
1334
|
+
TOOLTIP_VIEWPORT_PADDING_PX,
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
groupSplitTooltipLayoutsByEdge(layouts) {
|
|
1319
1338
|
const tooltipsByEdge = {
|
|
1320
1339
|
left: [],
|
|
1321
1340
|
right: [],
|
|
@@ -1325,6 +1344,11 @@ export class Tooltip {
|
|
|
1325
1344
|
layouts.forEach((layout) => {
|
|
1326
1345
|
tooltipsByEdge[layout.arrowEdge].push(layout);
|
|
1327
1346
|
});
|
|
1347
|
+
return tooltipsByEdge;
|
|
1348
|
+
}
|
|
1349
|
+
resolveSideSplitTooltipPositions(layouts) {
|
|
1350
|
+
const { minTop, maxBottom } = this.getSplitTooltipViewportBounds();
|
|
1351
|
+
const tooltipsByEdge = this.groupSplitTooltipLayoutsByEdge(layouts);
|
|
1328
1352
|
Object.values(tooltipsByEdge).forEach((edgeLayouts) => {
|
|
1329
1353
|
if (edgeLayouts.length === 0) {
|
|
1330
1354
|
return;
|
|
@@ -1463,65 +1487,98 @@ export class Tooltip {
|
|
|
1463
1487
|
return (a.left < b.left + b.width + SPLIT_TOOLTIP_GAP_PX &&
|
|
1464
1488
|
a.left + a.width + SPLIT_TOOLTIP_GAP_PX > b.left);
|
|
1465
1489
|
}
|
|
1466
|
-
|
|
1467
|
-
const
|
|
1468
|
-
const
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
left
|
|
1473
|
-
right
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
layouts.
|
|
1478
|
-
|
|
1490
|
+
resolveHorizontalChartAboveBelowSplitTooltipPositions(layouts) {
|
|
1491
|
+
const bounds = this.getSplitTooltipViewportBounds();
|
|
1492
|
+
const tooltipsByEdge = this.groupSplitTooltipLayoutsByEdge(layouts);
|
|
1493
|
+
this.resolvePackedAboveBelowTooltipRow(tooltipsByEdge.top, bounds);
|
|
1494
|
+
this.resolvePackedAboveBelowTooltipRow(tooltipsByEdge.bottom, bounds);
|
|
1495
|
+
this.resolveSideSplitTooltipPositions([
|
|
1496
|
+
...tooltipsByEdge.left,
|
|
1497
|
+
...tooltipsByEdge.right,
|
|
1498
|
+
]);
|
|
1499
|
+
}
|
|
1500
|
+
resolvePackedAboveBelowTooltipRow(layouts, bounds) {
|
|
1501
|
+
if (layouts.length === 0) {
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
const orderedLayouts = [...layouts].sort((a, b) => a.left - b.left || a.targetX - b.targetX);
|
|
1505
|
+
orderedLayouts.forEach((layout) => {
|
|
1506
|
+
const maxTop = Math.max(bounds.minTop, bounds.maxBottom - layout.height);
|
|
1507
|
+
layout.top = Math.max(bounds.minTop, Math.min(layout.top, maxTop));
|
|
1479
1508
|
});
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1509
|
+
const firstLayout = orderedLayouts[0];
|
|
1510
|
+
const firstMaxLeft = Math.max(bounds.minLeft, bounds.maxRight - firstLayout.width);
|
|
1511
|
+
firstLayout.left = Math.max(bounds.minLeft, Math.min(firstLayout.left, firstMaxLeft));
|
|
1512
|
+
for (let i = 1; i < orderedLayouts.length; i++) {
|
|
1513
|
+
const previousLayout = orderedLayouts[i - 1];
|
|
1514
|
+
const currentLayout = orderedLayouts[i];
|
|
1515
|
+
const minAllowedLeft = previousLayout.left +
|
|
1516
|
+
previousLayout.width +
|
|
1517
|
+
SPLIT_TOOLTIP_GAP_PX;
|
|
1518
|
+
currentLayout.left = Math.max(currentLayout.left, minAllowedLeft);
|
|
1519
|
+
}
|
|
1520
|
+
const lastLayout = orderedLayouts[orderedLayouts.length - 1];
|
|
1521
|
+
const overflow = lastLayout.left + lastLayout.width - bounds.maxRight;
|
|
1522
|
+
if (overflow > 0) {
|
|
1523
|
+
lastLayout.left -= overflow;
|
|
1524
|
+
for (let i = orderedLayouts.length - 2; i >= 0; i--) {
|
|
1525
|
+
const currentLayout = orderedLayouts[i];
|
|
1526
|
+
const nextLayout = orderedLayouts[i + 1];
|
|
1527
|
+
const maxAllowedLeft = nextLayout.left -
|
|
1528
|
+
currentLayout.width -
|
|
1491
1529
|
SPLIT_TOOLTIP_GAP_PX;
|
|
1492
|
-
currentLayout.left = Math.
|
|
1530
|
+
currentLayout.left = Math.min(currentLayout.left, maxAllowedLeft);
|
|
1493
1531
|
}
|
|
1494
|
-
const
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
const currentLayout = edgeLayouts[i];
|
|
1500
|
-
const nextLayout = edgeLayouts[i + 1];
|
|
1501
|
-
const maxAllowedLeft = nextLayout.left -
|
|
1502
|
-
currentLayout.width -
|
|
1503
|
-
SPLIT_TOOLTIP_GAP_PX;
|
|
1504
|
-
currentLayout.left = Math.min(currentLayout.left, maxAllowedLeft);
|
|
1505
|
-
}
|
|
1506
|
-
const underflow = minLeft - edgeLayouts[0].left;
|
|
1507
|
-
if (underflow > 0) {
|
|
1508
|
-
edgeLayouts.forEach((layout) => {
|
|
1509
|
-
layout.left += underflow;
|
|
1510
|
-
});
|
|
1511
|
-
}
|
|
1532
|
+
const underflow = bounds.minLeft - orderedLayouts[0].left;
|
|
1533
|
+
if (underflow > 0) {
|
|
1534
|
+
orderedLayouts.forEach((layout) => {
|
|
1535
|
+
layout.left += underflow;
|
|
1536
|
+
});
|
|
1512
1537
|
}
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
layout.top = Math.max(minTop, Math.min(layout.top, maxTop));
|
|
1518
|
-
});
|
|
1538
|
+
}
|
|
1539
|
+
orderedLayouts.forEach((layout) => {
|
|
1540
|
+
const maxLeft = Math.max(bounds.minLeft, bounds.maxRight - layout.width);
|
|
1541
|
+
layout.left = Math.max(bounds.minLeft, Math.min(layout.left, maxLeft));
|
|
1519
1542
|
});
|
|
1543
|
+
}
|
|
1544
|
+
resolveVerticalChartAboveBelowSplitTooltipPositions(layouts) {
|
|
1545
|
+
const bounds = this.getSplitTooltipViewportBounds();
|
|
1546
|
+
const tooltipsByEdge = this.groupSplitTooltipLayoutsByEdge(layouts);
|
|
1547
|
+
this.resolveCollisionAwareAboveBelowTooltipPositions(tooltipsByEdge.top, bounds);
|
|
1548
|
+
this.resolveCollisionAwareAboveBelowTooltipPositions(tooltipsByEdge.bottom, bounds);
|
|
1520
1549
|
this.resolveSideSplitTooltipPositions([
|
|
1521
1550
|
...tooltipsByEdge.left,
|
|
1522
1551
|
...tooltipsByEdge.right,
|
|
1523
1552
|
]);
|
|
1524
1553
|
}
|
|
1554
|
+
resolveCollisionAwareAboveBelowTooltipPositions(layouts, bounds) {
|
|
1555
|
+
const placedLayouts = [];
|
|
1556
|
+
const orderedLayouts = [...layouts].sort((a, b) => a.left - b.left || a.top - b.top);
|
|
1557
|
+
orderedLayouts.forEach((layout) => {
|
|
1558
|
+
const maxLeft = Math.max(bounds.minLeft, bounds.maxRight - layout.width);
|
|
1559
|
+
const maxTop = Math.max(bounds.minTop, bounds.maxBottom - layout.height);
|
|
1560
|
+
layout.top = Math.max(bounds.minTop, Math.min(layout.top, maxTop));
|
|
1561
|
+
layout.left = this.resolveNonOverlappingAboveBelowTooltipLeft(layout, placedLayouts, bounds.minLeft, maxLeft);
|
|
1562
|
+
placedLayouts.push(layout);
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
resolveNonOverlappingAboveBelowTooltipLeft(layout, placedLayouts, minLeft, maxLeft) {
|
|
1566
|
+
const preferredLeft = Math.max(minLeft, Math.min(layout.left, maxLeft));
|
|
1567
|
+
if (!this.doesSplitTooltipOverlapPlacedLayouts({ ...layout, left: preferredLeft }, placedLayouts)) {
|
|
1568
|
+
return preferredLeft;
|
|
1569
|
+
}
|
|
1570
|
+
const candidates = placedLayouts.flatMap((placedLayout) => [
|
|
1571
|
+
placedLayout.left + placedLayout.width + SPLIT_TOOLTIP_GAP_PX,
|
|
1572
|
+
placedLayout.left - layout.width - SPLIT_TOOLTIP_GAP_PX,
|
|
1573
|
+
]);
|
|
1574
|
+
return (Array.from(new Set(candidates.map((candidate) => Math.round(Math.max(minLeft, Math.min(candidate, maxLeft))))))
|
|
1575
|
+
.sort((a, b) => Math.abs(a - preferredLeft) -
|
|
1576
|
+
Math.abs(b - preferredLeft))
|
|
1577
|
+
.find((candidate) => !this.doesSplitTooltipOverlapPlacedLayouts({ ...layout, left: candidate }, placedLayouts)) ?? preferredLeft);
|
|
1578
|
+
}
|
|
1579
|
+
doesSplitTooltipOverlapPlacedLayouts(layout, placedLayouts) {
|
|
1580
|
+
return placedLayouts.some((placedLayout) => this.doSplitTooltipLayoutsOverlap(layout, placedLayout));
|
|
1581
|
+
}
|
|
1525
1582
|
}
|
|
1526
1583
|
Object.defineProperty(Tooltip, "nextTooltipId", {
|
|
1527
1584
|
enumerable: true,
|
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;
|