@internetstiftelsen/charts 0.10.1 → 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 +64 -0
- package/dist/area.d.ts +9 -1
- package/dist/area.js +174 -38
- package/dist/bar.d.ts +9 -1
- package/dist/bar.js +130 -47
- package/dist/base-chart.js +11 -1
- package/dist/donut-chart.js +3 -18
- package/dist/gauge-chart.d.ts +3 -4
- package/dist/gauge-chart.js +7 -53
- package/dist/lazy-mount.d.ts +13 -0
- package/dist/lazy-mount.js +90 -0
- package/dist/line.d.ts +9 -1
- package/dist/line.js +141 -23
- package/dist/pie-chart.js +5 -29
- 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 +89 -8
- package/dist/theme.js +17 -0
- package/dist/tooltip.d.ts +55 -3
- package/dist/tooltip.js +950 -137
- package/dist/types.d.ts +20 -0
- package/dist/xy-animation.d.ts +3 -0
- package/dist/xy-animation.js +2 -0
- package/dist/xy-chart.d.ts +11 -1
- package/dist/xy-chart.js +107 -10
- 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/docs/components.md +36 -0
- package/docs/getting-started.md +35 -0
- package/docs/theming.md +14 -0
- package/docs/xy-chart.md +67 -1
- package/package.json +1 -1
package/dist/tooltip.js
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
import { pointer, select } from 'd3';
|
|
2
2
|
import { getSeriesColor } from './types.js';
|
|
3
3
|
import { sanitizeForCSS, mergeDeep } from './utils.js';
|
|
4
|
+
const TOOLTIP_OFFSET_PX = 12;
|
|
5
|
+
const TOOLTIP_VIEWPORT_PADDING_PX = 10;
|
|
6
|
+
const TOOLTIP_CONNECTOR_INSET_PX = 14;
|
|
7
|
+
const TOOLTIP_CONNECTOR_PADDING_PX = 4;
|
|
8
|
+
const TOOLTIP_CONNECTOR_ELBOW_RATIO = 0.45;
|
|
9
|
+
const TOOLTIP_BOX_ARROW_LENGTH_PX = 10;
|
|
10
|
+
const TOOLTIP_BOX_ARROW_HALF_HEIGHT_PX = 6;
|
|
11
|
+
const TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX = 1;
|
|
12
|
+
const SPLIT_TOOLTIP_GAP_PX = 8;
|
|
13
|
+
const DEFAULT_TOOLTIP_TRANSITION = {
|
|
14
|
+
show: false,
|
|
15
|
+
duration: 120,
|
|
16
|
+
easing: 'ease-out',
|
|
17
|
+
};
|
|
18
|
+
const TOOLTIP_HIDDEN_TRANSFORM = 'translateY(2px)';
|
|
19
|
+
const TOOLTIP_VISIBLE_TRANSFORM = 'translateY(0)';
|
|
4
20
|
export class Tooltip {
|
|
5
|
-
constructor(config) {
|
|
21
|
+
constructor(config = {}) {
|
|
6
22
|
Object.defineProperty(this, "id", {
|
|
7
23
|
enumerable: true,
|
|
8
24
|
configurable: true,
|
|
9
25
|
writable: true,
|
|
10
|
-
value:
|
|
26
|
+
value: void 0
|
|
11
27
|
});
|
|
12
28
|
Object.defineProperty(this, "type", {
|
|
13
29
|
enumerable: true,
|
|
@@ -15,6 +31,30 @@ export class Tooltip {
|
|
|
15
31
|
writable: true,
|
|
16
32
|
value: 'tooltip'
|
|
17
33
|
});
|
|
34
|
+
Object.defineProperty(this, "mode", {
|
|
35
|
+
enumerable: true,
|
|
36
|
+
configurable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
value: void 0
|
|
39
|
+
});
|
|
40
|
+
Object.defineProperty(this, "position", {
|
|
41
|
+
enumerable: true,
|
|
42
|
+
configurable: true,
|
|
43
|
+
writable: true,
|
|
44
|
+
value: void 0
|
|
45
|
+
});
|
|
46
|
+
Object.defineProperty(this, "barAnchorPosition", {
|
|
47
|
+
enumerable: true,
|
|
48
|
+
configurable: true,
|
|
49
|
+
writable: true,
|
|
50
|
+
value: void 0
|
|
51
|
+
});
|
|
52
|
+
Object.defineProperty(this, "transition", {
|
|
53
|
+
enumerable: true,
|
|
54
|
+
configurable: true,
|
|
55
|
+
writable: true,
|
|
56
|
+
value: void 0
|
|
57
|
+
});
|
|
18
58
|
Object.defineProperty(this, "formatter", {
|
|
19
59
|
enumerable: true,
|
|
20
60
|
configurable: true,
|
|
@@ -39,19 +79,52 @@ export class Tooltip {
|
|
|
39
79
|
writable: true,
|
|
40
80
|
value: void 0
|
|
41
81
|
});
|
|
82
|
+
Object.defineProperty(this, "splitTooltipOwner", {
|
|
83
|
+
enumerable: true,
|
|
84
|
+
configurable: true,
|
|
85
|
+
writable: true,
|
|
86
|
+
value: void 0
|
|
87
|
+
});
|
|
88
|
+
Object.defineProperty(this, "tooltipStyleKeys", {
|
|
89
|
+
enumerable: true,
|
|
90
|
+
configurable: true,
|
|
91
|
+
writable: true,
|
|
92
|
+
value: new WeakMap()
|
|
93
|
+
});
|
|
42
94
|
Object.defineProperty(this, "tooltipDiv", {
|
|
43
95
|
enumerable: true,
|
|
44
96
|
configurable: true,
|
|
45
97
|
writable: true,
|
|
46
98
|
value: null
|
|
47
99
|
});
|
|
48
|
-
this
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
100
|
+
Object.defineProperty(this, "tooltipTheme", {
|
|
101
|
+
enumerable: true,
|
|
102
|
+
configurable: true,
|
|
103
|
+
writable: true,
|
|
104
|
+
value: null
|
|
105
|
+
});
|
|
106
|
+
const { mode = 'split', position = 'side', barAnchorPosition = 'middle', transition, formatter, labelFormatter, customFormatter, exportHooks, } = config;
|
|
107
|
+
const tooltipId = Tooltip.nextTooltipId++;
|
|
108
|
+
this.id = `iisChartTooltip-${tooltipId}`;
|
|
109
|
+
this.splitTooltipOwner = `${this.id}-split`;
|
|
110
|
+
this.mode = mode;
|
|
111
|
+
this.position = position;
|
|
112
|
+
this.barAnchorPosition = barAnchorPosition;
|
|
113
|
+
this.transition = {
|
|
114
|
+
...DEFAULT_TOOLTIP_TRANSITION,
|
|
115
|
+
...transition,
|
|
116
|
+
};
|
|
117
|
+
this.formatter = formatter;
|
|
118
|
+
this.labelFormatter = labelFormatter;
|
|
119
|
+
this.customFormatter = customFormatter;
|
|
120
|
+
this.exportHooks = exportHooks;
|
|
52
121
|
}
|
|
53
122
|
getExportConfig() {
|
|
54
123
|
return {
|
|
124
|
+
mode: this.mode,
|
|
125
|
+
position: this.position,
|
|
126
|
+
barAnchorPosition: this.barAnchorPosition,
|
|
127
|
+
transition: this.transition,
|
|
55
128
|
formatter: this.formatter,
|
|
56
129
|
labelFormatter: this.labelFormatter,
|
|
57
130
|
customFormatter: this.customFormatter,
|
|
@@ -72,19 +145,10 @@ export class Tooltip {
|
|
|
72
145
|
.attr('class', 'chart-tooltip')
|
|
73
146
|
.attr('id', this.id)
|
|
74
147
|
: existingTooltip;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.style('visibility', 'hidden')
|
|
78
|
-
.style('background-color', 'white')
|
|
79
|
-
.style('border', '1px solid #ddd')
|
|
80
|
-
.style('border-radius', '4px')
|
|
81
|
-
.style('padding', '8px')
|
|
82
|
-
.style('box-shadow', '0 2px 4px rgba(0,0,0,0.1)')
|
|
83
|
-
.style('font-family', theme.axis.fontFamily)
|
|
84
|
-
.style('font-size', '12px')
|
|
85
|
-
.style('pointer-events', 'none')
|
|
86
|
-
.style('z-index', '1000');
|
|
148
|
+
this.removeSplitTooltips();
|
|
149
|
+
this.applyTooltipStylesIfNeeded(tooltip, theme);
|
|
87
150
|
this.tooltipDiv = tooltip;
|
|
151
|
+
this.hideTooltipSelection(tooltip);
|
|
88
152
|
}
|
|
89
153
|
attachToArea(svg, data, series, xKey, x, y, theme, plotArea, parseValue, isHorizontal = false, categoryScaleType = 'band', resolveSeriesValue = (targetSeries, dataPoint) => {
|
|
90
154
|
const rawValue = dataPoint[targetSeries.dataKey];
|
|
@@ -93,9 +157,14 @@ export class Tooltip {
|
|
|
93
157
|
}
|
|
94
158
|
return parseValue(rawValue);
|
|
95
159
|
}) {
|
|
96
|
-
if (!this.tooltipDiv) {
|
|
160
|
+
if (!this.tooltipDiv || data.length === 0) {
|
|
97
161
|
return;
|
|
98
162
|
}
|
|
163
|
+
const tooltip = this.tooltipDiv;
|
|
164
|
+
const formatter = this.formatter;
|
|
165
|
+
const labelFormatter = this.labelFormatter;
|
|
166
|
+
const customFormatter = this.customFormatter;
|
|
167
|
+
const tooltipMode = this.mode;
|
|
99
168
|
const normalizeFormatterValue = (value) => {
|
|
100
169
|
if (value === null ||
|
|
101
170
|
value === undefined ||
|
|
@@ -107,13 +176,6 @@ export class Tooltip {
|
|
|
107
176
|
}
|
|
108
177
|
return String(value);
|
|
109
178
|
};
|
|
110
|
-
if (data.length === 0) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const tooltip = this.tooltipDiv;
|
|
114
|
-
const formatter = this.formatter;
|
|
115
|
-
const labelFormatter = this.labelFormatter;
|
|
116
|
-
const customFormatter = this.customFormatter;
|
|
117
179
|
const getCategoryScaleValue = (value, scaleType) => {
|
|
118
180
|
switch (scaleType) {
|
|
119
181
|
case 'band':
|
|
@@ -128,13 +190,11 @@ export class Tooltip {
|
|
|
128
190
|
}
|
|
129
191
|
};
|
|
130
192
|
const getXPosition = (dataPoint) => {
|
|
131
|
-
const
|
|
132
|
-
const scaled = x(getCategoryScaleValue(xValue, categoryScaleType));
|
|
193
|
+
const scaled = x(getCategoryScaleValue(dataPoint[xKey], categoryScaleType));
|
|
133
194
|
return (scaled || 0) + (x.bandwidth ? x.bandwidth() / 2 : 0);
|
|
134
195
|
};
|
|
135
196
|
const getYPosition = (dataPoint) => {
|
|
136
|
-
const
|
|
137
|
-
const scaled = y(getCategoryScaleValue(yValue, categoryScaleType));
|
|
197
|
+
const scaled = y(getCategoryScaleValue(dataPoint[xKey], categoryScaleType));
|
|
138
198
|
return (scaled || 0) + (y.bandwidth ? y.bandwidth() / 2 : 0);
|
|
139
199
|
};
|
|
140
200
|
const stripHtml = (content) => {
|
|
@@ -144,46 +204,36 @@ export class Tooltip {
|
|
|
144
204
|
.replace(/\s+/g, ' ')
|
|
145
205
|
.trim();
|
|
146
206
|
};
|
|
147
|
-
const
|
|
148
|
-
if (customFormatter) {
|
|
149
|
-
return customFormatter(dataPoint, series);
|
|
150
|
-
}
|
|
207
|
+
const buildTooltipLabel = (dataPoint) => {
|
|
151
208
|
const labelValue = dataPoint[xKey];
|
|
152
|
-
|
|
209
|
+
return labelFormatter
|
|
153
210
|
? labelFormatter(String(labelValue), dataPoint)
|
|
154
211
|
: String(labelValue);
|
|
155
|
-
let content = `<strong>${label}</strong><br/>`;
|
|
156
|
-
series.forEach((s) => {
|
|
157
|
-
const value = dataPoint[s.dataKey];
|
|
158
|
-
if (formatter) {
|
|
159
|
-
content +=
|
|
160
|
-
formatter(s.dataKey, normalizeFormatterValue(value), dataPoint) + '<br/>';
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
content += `${s.dataKey}: ${value}<br/>`;
|
|
164
|
-
});
|
|
165
|
-
return content;
|
|
166
212
|
};
|
|
167
|
-
const
|
|
213
|
+
const buildTooltipRow = (dataPoint, currentSeries) => {
|
|
214
|
+
const value = dataPoint[currentSeries.dataKey];
|
|
215
|
+
if (formatter) {
|
|
216
|
+
return formatter(currentSeries.dataKey, normalizeFormatterValue(value), dataPoint);
|
|
217
|
+
}
|
|
218
|
+
return `${currentSeries.dataKey}: ${value}`;
|
|
219
|
+
};
|
|
220
|
+
const buildSharedTooltipContent = (dataPoint) => {
|
|
168
221
|
if (customFormatter) {
|
|
169
|
-
return
|
|
222
|
+
return customFormatter(dataPoint, series);
|
|
170
223
|
}
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
: value}`);
|
|
185
|
-
});
|
|
186
|
-
return parts.join('. ');
|
|
224
|
+
const label = buildTooltipLabel(dataPoint);
|
|
225
|
+
const rows = series.map((currentSeries) => buildTooltipRow(dataPoint, currentSeries));
|
|
226
|
+
return `<strong>${label}</strong><br/>${rows.join('<br/>')}`;
|
|
227
|
+
};
|
|
228
|
+
const buildSplitTooltipContent = (dataPoint, currentSeries) => {
|
|
229
|
+
if (customFormatter) {
|
|
230
|
+
return customFormatter(dataPoint, [currentSeries]);
|
|
231
|
+
}
|
|
232
|
+
const label = buildTooltipLabel(dataPoint);
|
|
233
|
+
return `<strong>${label}</strong><br/>${buildTooltipRow(dataPoint, currentSeries)}`;
|
|
234
|
+
};
|
|
235
|
+
const buildAccessibleLabel = (dataPoint) => {
|
|
236
|
+
return stripHtml(buildSharedTooltipContent(dataPoint));
|
|
187
237
|
};
|
|
188
238
|
const isTooltipFocusTarget = (element) => {
|
|
189
239
|
return (element instanceof SVGElement &&
|
|
@@ -203,7 +253,6 @@ export class Tooltip {
|
|
|
203
253
|
}
|
|
204
254
|
return closestIndex;
|
|
205
255
|
};
|
|
206
|
-
// Create overlay rect for mouse tracking using plot area bounds
|
|
207
256
|
const overlay = svg
|
|
208
257
|
.append('rect')
|
|
209
258
|
.attr('class', 'tooltip-overlay')
|
|
@@ -214,14 +263,20 @@ export class Tooltip {
|
|
|
214
263
|
.attr('aria-hidden', 'true')
|
|
215
264
|
.style('fill', 'none')
|
|
216
265
|
.style('pointer-events', 'all');
|
|
217
|
-
const pointSeries = series.filter((
|
|
218
|
-
|
|
266
|
+
const pointSeries = series.filter((currentSeries) => {
|
|
267
|
+
return (currentSeries.type === 'line' ||
|
|
268
|
+
currentSeries.type === 'area' ||
|
|
269
|
+
currentSeries.type === 'scatter');
|
|
270
|
+
});
|
|
271
|
+
const barSeries = series.filter((currentSeries) => {
|
|
272
|
+
return currentSeries.type === 'bar';
|
|
273
|
+
});
|
|
219
274
|
const hasBarSeries = barSeries.length > 0;
|
|
220
|
-
const focusCircles = pointSeries.map((
|
|
221
|
-
const seriesColor = getSeriesColor(
|
|
275
|
+
const focusCircles = pointSeries.map((currentSeries) => {
|
|
276
|
+
const seriesColor = getSeriesColor(currentSeries);
|
|
222
277
|
return svg
|
|
223
278
|
.append('circle')
|
|
224
|
-
.attr('class', `focus-circle-${sanitizeForCSS(
|
|
279
|
+
.attr('class', `focus-circle-${sanitizeForCSS(currentSeries.dataKey)}`)
|
|
225
280
|
.attr('r', theme.line.point.size + 1)
|
|
226
281
|
.attr('fill', theme.line.point.color || seriesColor)
|
|
227
282
|
.attr('stroke', theme.line.point.strokeColor || seriesColor)
|
|
@@ -235,48 +290,51 @@ export class Tooltip {
|
|
|
235
290
|
if (!hasBarSeries) {
|
|
236
291
|
return;
|
|
237
292
|
}
|
|
238
|
-
barSeries.forEach((
|
|
239
|
-
|
|
240
|
-
svg.selectAll(`.bar-${sanitizedKey}`).style('opacity', 1);
|
|
293
|
+
barSeries.forEach((currentSeries) => {
|
|
294
|
+
svg.selectAll(`.bar-${sanitizeForCSS(currentSeries.dataKey)}`).style('opacity', 1);
|
|
241
295
|
});
|
|
242
296
|
};
|
|
243
|
-
const
|
|
297
|
+
const updateVisualStateAtIndex = (closestIndex) => {
|
|
244
298
|
const dataPoint = data[closestIndex];
|
|
245
299
|
const dataPointPosition = dataPointPositions[closestIndex];
|
|
246
|
-
pointSeries.forEach((
|
|
247
|
-
const value = resolveSeriesValue(
|
|
300
|
+
pointSeries.forEach((currentSeries, seriesIndex) => {
|
|
301
|
+
const value = resolveSeriesValue(currentSeries, dataPoint, closestIndex);
|
|
248
302
|
if (!Number.isFinite(value)) {
|
|
249
|
-
focusCircles[
|
|
303
|
+
focusCircles[seriesIndex].style('opacity', 0);
|
|
250
304
|
return;
|
|
251
305
|
}
|
|
252
306
|
if (isHorizontal) {
|
|
253
|
-
focusCircles[
|
|
307
|
+
focusCircles[seriesIndex]
|
|
254
308
|
.attr('cx', x(value) ?? 0)
|
|
255
309
|
.attr('cy', dataPointPosition)
|
|
256
310
|
.style('opacity', 1);
|
|
257
311
|
return;
|
|
258
312
|
}
|
|
259
|
-
focusCircles[
|
|
313
|
+
focusCircles[seriesIndex]
|
|
260
314
|
.attr('cx', dataPointPosition)
|
|
261
315
|
.attr('cy', y(value) ?? 0)
|
|
262
316
|
.style('opacity', 1);
|
|
263
317
|
});
|
|
264
|
-
if (hasBarSeries) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
});
|
|
318
|
+
if (!hasBarSeries) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
barSeries.forEach((currentSeries) => {
|
|
322
|
+
svg.selectAll(`.bar-${sanitizeForCSS(currentSeries.dataKey)}`).style('opacity', (_, index) => index === closestIndex ? 1 : 0.5);
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
const showSharedTooltipAtIndex = (closestIndex) => {
|
|
326
|
+
const dataPoint = data[closestIndex];
|
|
327
|
+
const dataPointPosition = dataPointPositions[closestIndex];
|
|
328
|
+
updateVisualStateAtIndex(closestIndex);
|
|
329
|
+
this.hideSplitTooltips();
|
|
330
|
+
const content = buildSharedTooltipContent(dataPoint);
|
|
331
|
+
const measuredTooltip = this.measureTooltip(tooltip, content);
|
|
332
|
+
if (!measuredTooltip) {
|
|
333
|
+
this.hideTooltipSelection(tooltip);
|
|
334
|
+
return;
|
|
269
335
|
}
|
|
270
|
-
tooltip
|
|
271
|
-
.style('visibility', 'visible')
|
|
272
|
-
.html(buildTooltipContent(dataPoint));
|
|
273
|
-
const tooltipNode = tooltip.node();
|
|
274
|
-
const tooltipRect = tooltipNode.getBoundingClientRect();
|
|
275
|
-
const tooltipWidth = tooltipRect.width;
|
|
276
|
-
const tooltipHeight = tooltipRect.height;
|
|
277
336
|
const svgRect = svg.node().getBoundingClientRect();
|
|
278
|
-
const
|
|
279
|
-
const values = series.map((s) => resolveSeriesValue(s, dataPoint, closestIndex));
|
|
337
|
+
const values = series.map((currentSeries) => resolveSeriesValue(currentSeries, dataPoint, closestIndex));
|
|
280
338
|
const finiteValues = values.filter((value) => Number.isFinite(value));
|
|
281
339
|
const minValue = finiteValues.length
|
|
282
340
|
? Math.min(...finiteValues)
|
|
@@ -284,58 +342,171 @@ export class Tooltip {
|
|
|
284
342
|
const maxValue = finiteValues.length
|
|
285
343
|
? Math.max(...finiteValues)
|
|
286
344
|
: 0;
|
|
287
|
-
|
|
288
|
-
|
|
345
|
+
const sharedAnchor = {
|
|
346
|
+
left: 0,
|
|
347
|
+
right: 0,
|
|
348
|
+
top: 0,
|
|
349
|
+
bottom: 0,
|
|
350
|
+
centerX: 0,
|
|
351
|
+
centerY: 0,
|
|
352
|
+
};
|
|
289
353
|
if (isHorizontal) {
|
|
290
354
|
const minX = x(minValue);
|
|
291
355
|
const maxX = x(maxValue);
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
356
|
+
const centerX = svgRect.left + window.scrollX + (minX + maxX) / 2;
|
|
357
|
+
const centerY = svgRect.top + window.scrollY + dataPointPosition;
|
|
358
|
+
sharedAnchor.left = centerX;
|
|
359
|
+
sharedAnchor.right = centerX;
|
|
360
|
+
sharedAnchor.top = centerY;
|
|
361
|
+
sharedAnchor.bottom = centerY;
|
|
362
|
+
sharedAnchor.centerX = centerX;
|
|
363
|
+
sharedAnchor.centerY = centerY;
|
|
299
364
|
}
|
|
300
365
|
else {
|
|
301
366
|
const minY = y(maxValue);
|
|
302
367
|
const maxY = y(minValue);
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
368
|
+
const centerX = svgRect.left + window.scrollX + dataPointPosition;
|
|
369
|
+
const topY = svgRect.top + window.scrollY + minY;
|
|
370
|
+
const bottomY = svgRect.top + window.scrollY + maxY;
|
|
371
|
+
sharedAnchor.left = centerX;
|
|
372
|
+
sharedAnchor.right = centerX;
|
|
373
|
+
sharedAnchor.top = topY;
|
|
374
|
+
sharedAnchor.bottom = bottomY;
|
|
375
|
+
sharedAnchor.centerX = centerX;
|
|
376
|
+
sharedAnchor.centerY = (topY + bottomY) / 2;
|
|
377
|
+
}
|
|
378
|
+
const target = this.resolveSharedTooltipTarget(sharedAnchor);
|
|
379
|
+
const arrowEdge = this.resolveTooltipArrowEdge(sharedAnchor, target, measuredTooltip.width, measuredTooltip.height);
|
|
380
|
+
const position = this.getAnchoredTooltipPosition(sharedAnchor, target, measuredTooltip.width, measuredTooltip.height, arrowEdge);
|
|
381
|
+
if (!position) {
|
|
382
|
+
this.hideTooltipSelection(tooltip);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
this.renderTooltipWithoutConnector(tooltip, position.left, position.top);
|
|
386
|
+
};
|
|
387
|
+
const getSeriesTooltipAnchor = (currentSeries, dataPoint, index) => {
|
|
388
|
+
const value = resolveSeriesValue(currentSeries, dataPoint, index);
|
|
389
|
+
if (!Number.isFinite(value)) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
const svgNode = svg.node();
|
|
393
|
+
if (!svgNode) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
if (currentSeries.type === 'bar') {
|
|
397
|
+
return this.resolveBarTooltipAnchor(svgNode, currentSeries.dataKey, index);
|
|
398
|
+
}
|
|
399
|
+
const categoryPosition = dataPointPositions[index];
|
|
400
|
+
const valuePosition = isHorizontal
|
|
401
|
+
? (x(value) ?? 0)
|
|
402
|
+
: (y(value) ?? 0);
|
|
403
|
+
return this.resolvePointTooltipAnchor(svgNode, isHorizontal, categoryPosition, valuePosition);
|
|
404
|
+
};
|
|
405
|
+
const showSplitTooltipAtIndex = (closestIndex) => {
|
|
406
|
+
const dataPoint = data[closestIndex];
|
|
407
|
+
updateVisualStateAtIndex(closestIndex);
|
|
408
|
+
this.hideTooltipSelection(tooltip);
|
|
409
|
+
this.hideSplitTooltips();
|
|
410
|
+
const layouts = [];
|
|
411
|
+
series.forEach((currentSeries, seriesIndex) => {
|
|
412
|
+
const rawValue = dataPoint[currentSeries.dataKey];
|
|
413
|
+
if (rawValue === null || rawValue === undefined) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const anchor = getSeriesTooltipAnchor(currentSeries, dataPoint, closestIndex);
|
|
417
|
+
if (!anchor) {
|
|
418
|
+
return;
|
|
319
419
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
tooltipWidth -
|
|
326
|
-
offsetX;
|
|
420
|
+
const splitTooltip = this.getSplitTooltip(seriesIndex, theme);
|
|
421
|
+
const content = buildSplitTooltipContent(dataPoint, currentSeries);
|
|
422
|
+
const measuredTooltip = this.measureTooltip(splitTooltip, content);
|
|
423
|
+
if (!measuredTooltip) {
|
|
424
|
+
return;
|
|
327
425
|
}
|
|
426
|
+
const target = this.resolveSplitTooltipTarget(currentSeries, anchor);
|
|
427
|
+
const arrowEdge = this.resolveTooltipArrowEdge(anchor, target, measuredTooltip.width, measuredTooltip.height);
|
|
428
|
+
const position = this.getAnchoredTooltipPosition(anchor, target, measuredTooltip.width, measuredTooltip.height, arrowEdge);
|
|
429
|
+
if (!position) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
layouts.push({
|
|
433
|
+
div: splitTooltip,
|
|
434
|
+
anchor,
|
|
435
|
+
width: measuredTooltip.width,
|
|
436
|
+
height: measuredTooltip.height,
|
|
437
|
+
left: position.left,
|
|
438
|
+
top: position.top,
|
|
439
|
+
arrowEdge,
|
|
440
|
+
targetX: target.x,
|
|
441
|
+
targetY: target.y,
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
if (layouts.length === 0) {
|
|
445
|
+
return;
|
|
328
446
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
.
|
|
447
|
+
this.resolveSplitTooltipCollisions(layouts, 'vertical', this.getOppositeVerticalArrowEdge);
|
|
448
|
+
this.resolveSplitTooltipCollisions(layouts, 'side', this.getOppositeSideArrowEdge);
|
|
449
|
+
this.resolveSplitTooltipPositions(layouts);
|
|
450
|
+
layouts.forEach((layout) => {
|
|
451
|
+
this.renderTooltipWithConnector(layout.div, layout.arrowEdge, layout.left, layout.top, layout.width, layout.height, layout.targetX, layout.targetY);
|
|
452
|
+
});
|
|
334
453
|
};
|
|
335
454
|
const hideTooltip = () => {
|
|
336
|
-
|
|
455
|
+
this.hideTooltipSelection(tooltip);
|
|
456
|
+
this.hideSplitTooltips();
|
|
337
457
|
clearVisualState();
|
|
338
458
|
};
|
|
459
|
+
let pendingTooltipFrame = null;
|
|
460
|
+
let pendingTooltipIndex = null;
|
|
461
|
+
const tooltipSvgNode = svg.node();
|
|
462
|
+
const requestFrame = (callback) => {
|
|
463
|
+
if (typeof window.requestAnimationFrame === 'function') {
|
|
464
|
+
return window.requestAnimationFrame(callback);
|
|
465
|
+
}
|
|
466
|
+
return window.setTimeout(() => {
|
|
467
|
+
callback(window.performance.now());
|
|
468
|
+
}, 16);
|
|
469
|
+
};
|
|
470
|
+
const cancelFrame = (frameId) => {
|
|
471
|
+
if (typeof window.cancelAnimationFrame === 'function') {
|
|
472
|
+
window.cancelAnimationFrame(frameId);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
window.clearTimeout(frameId);
|
|
476
|
+
};
|
|
477
|
+
const renderTooltipAtIndex = (index) => {
|
|
478
|
+
if (tooltipMode === 'split') {
|
|
479
|
+
showSplitTooltipAtIndex(index);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
showSharedTooltipAtIndex(index);
|
|
483
|
+
};
|
|
484
|
+
const cancelPendingTooltipRender = () => {
|
|
485
|
+
if (pendingTooltipFrame === null) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
cancelFrame(pendingTooltipFrame);
|
|
489
|
+
pendingTooltipFrame = null;
|
|
490
|
+
pendingTooltipIndex = null;
|
|
491
|
+
};
|
|
492
|
+
const requestTooltipRenderAtIndex = (index) => {
|
|
493
|
+
pendingTooltipIndex = index;
|
|
494
|
+
if (pendingTooltipFrame !== null) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
pendingTooltipFrame = requestFrame(() => {
|
|
498
|
+
const nextIndex = pendingTooltipIndex;
|
|
499
|
+
pendingTooltipFrame = null;
|
|
500
|
+
pendingTooltipIndex = null;
|
|
501
|
+
if (nextIndex === null) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (!tooltipSvgNode?.isConnected) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
renderTooltipAtIndex(nextIndex);
|
|
508
|
+
});
|
|
509
|
+
};
|
|
339
510
|
const getFocusTargetBounds = (index) => {
|
|
340
511
|
if (isHorizontal) {
|
|
341
512
|
if (y.bandwidth) {
|
|
@@ -393,12 +564,14 @@ export class Tooltip {
|
|
|
393
564
|
overlay
|
|
394
565
|
.on('mousemove', (event) => {
|
|
395
566
|
const [mouseX, mouseY] = pointer(event, svg.node());
|
|
396
|
-
|
|
567
|
+
const closestIndex = getClosestIndexFromPointer(mouseX, mouseY);
|
|
568
|
+
requestTooltipRenderAtIndex(closestIndex);
|
|
397
569
|
})
|
|
398
570
|
.on('mouseout', () => {
|
|
399
571
|
if (isTooltipFocusTarget(document.activeElement)) {
|
|
400
572
|
return;
|
|
401
573
|
}
|
|
574
|
+
cancelPendingTooltipRender();
|
|
402
575
|
hideTooltip();
|
|
403
576
|
});
|
|
404
577
|
const focusTargets = svg
|
|
@@ -442,14 +615,16 @@ export class Tooltip {
|
|
|
442
615
|
if (currentIndex === -1) {
|
|
443
616
|
return;
|
|
444
617
|
}
|
|
618
|
+
cancelPendingTooltipRender();
|
|
445
619
|
select(this).attr('stroke', '#111827').attr('stroke-width', 2);
|
|
446
|
-
|
|
620
|
+
renderTooltipAtIndex(currentIndex);
|
|
447
621
|
})
|
|
448
622
|
.on('blur', function (event) {
|
|
449
623
|
select(this).attr('stroke', 'none').attr('stroke-width', 0);
|
|
450
624
|
if (isTooltipFocusTarget(event.relatedTarget)) {
|
|
451
625
|
return;
|
|
452
626
|
}
|
|
627
|
+
cancelPendingTooltipRender();
|
|
453
628
|
hideTooltip();
|
|
454
629
|
})
|
|
455
630
|
.on('keydown', function (event) {
|
|
@@ -465,13 +640,651 @@ export class Tooltip {
|
|
|
465
640
|
focusTargetNodes[nextIndex].focus();
|
|
466
641
|
});
|
|
467
642
|
}
|
|
643
|
+
setContent(content) {
|
|
644
|
+
if (!this.tooltipDiv) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
this.setTooltipMarkup(this.tooltipDiv, content);
|
|
648
|
+
}
|
|
649
|
+
getBounds() {
|
|
650
|
+
const node = this.tooltipDiv?.node();
|
|
651
|
+
if (!node) {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
return node.getBoundingClientRect();
|
|
655
|
+
}
|
|
656
|
+
showAt(left, top) {
|
|
657
|
+
if (!this.tooltipDiv) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (!Number.isFinite(left) || !Number.isFinite(top)) {
|
|
661
|
+
this.hide();
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
this.showTooltipAt(this.tooltipDiv, left, top);
|
|
665
|
+
}
|
|
666
|
+
hide() {
|
|
667
|
+
const tooltip = this.tooltipDiv ?? select(`#${this.id}`);
|
|
668
|
+
if (!tooltip.empty()) {
|
|
669
|
+
this.hideTooltipSelection(tooltip);
|
|
670
|
+
}
|
|
671
|
+
this.hideSplitTooltips();
|
|
672
|
+
}
|
|
468
673
|
cleanup() {
|
|
674
|
+
this.removeRootTooltip();
|
|
675
|
+
this.removeSplitTooltips();
|
|
676
|
+
this.tooltipDiv = null;
|
|
677
|
+
}
|
|
678
|
+
applyTooltipStylesIfNeeded(tooltip, theme) {
|
|
679
|
+
const node = tooltip.node();
|
|
680
|
+
if (!node) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const styleKey = this.getTooltipStyleKey(theme);
|
|
684
|
+
if (this.tooltipStyleKeys.get(node) === styleKey) {
|
|
685
|
+
this.tooltipTheme = theme.tooltip;
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
this.tooltipStyleKeys.set(node, styleKey);
|
|
689
|
+
this.writeTooltipStyles(tooltip, theme);
|
|
690
|
+
}
|
|
691
|
+
getTooltipStyleKey(theme) {
|
|
692
|
+
return [
|
|
693
|
+
theme.tooltip.background,
|
|
694
|
+
theme.tooltip.border,
|
|
695
|
+
theme.tooltip.color,
|
|
696
|
+
theme.tooltip.fontFamily,
|
|
697
|
+
theme.tooltip.fontSize,
|
|
698
|
+
theme.tooltip.fontWeight,
|
|
699
|
+
this.transition.show,
|
|
700
|
+
this.transition.duration,
|
|
701
|
+
this.transition.easing,
|
|
702
|
+
].join('|');
|
|
703
|
+
}
|
|
704
|
+
writeTooltipStyles(tooltip, theme) {
|
|
705
|
+
this.tooltipTheme = theme.tooltip;
|
|
706
|
+
tooltip
|
|
707
|
+
.style('position', 'absolute')
|
|
708
|
+
.style('background-color', theme.tooltip.background)
|
|
709
|
+
.style('border', `1px solid ${theme.tooltip.border}`)
|
|
710
|
+
.style('border-radius', '4px')
|
|
711
|
+
.style('padding', '8px')
|
|
712
|
+
.style('box-shadow', '0 2px 4px rgba(0,0,0,0.1)')
|
|
713
|
+
.style('color', theme.tooltip.color)
|
|
714
|
+
.style('font-family', theme.tooltip.fontFamily)
|
|
715
|
+
.style('font-size', `${theme.tooltip.fontSize}px`)
|
|
716
|
+
.style('font-weight', theme.tooltip.fontWeight)
|
|
717
|
+
.style('overflow', 'visible')
|
|
718
|
+
.style('isolation', 'isolate')
|
|
719
|
+
.style('pointer-events', 'none')
|
|
720
|
+
.style('z-index', '1000');
|
|
721
|
+
if (this.transition.show) {
|
|
722
|
+
tooltip
|
|
723
|
+
.style('transition', `opacity ${this.transition.duration}ms ${this.transition.easing}, transform ${this.transition.duration}ms ${this.transition.easing}`)
|
|
724
|
+
.style('will-change', 'opacity, transform');
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
tooltip
|
|
728
|
+
.style('opacity', null)
|
|
729
|
+
.style('transform', null)
|
|
730
|
+
.style('transition', null)
|
|
731
|
+
.style('will-change', null);
|
|
732
|
+
}
|
|
733
|
+
measureTooltip(tooltip, content) {
|
|
734
|
+
this.setTooltipMarkup(tooltip, content);
|
|
735
|
+
tooltip.style('left', '-9999px').style('top', '-9999px');
|
|
736
|
+
this.hideTooltipSelection(tooltip);
|
|
737
|
+
const tooltipNode = tooltip.node();
|
|
738
|
+
if (!tooltipNode) {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
const tooltipRect = tooltipNode.getBoundingClientRect();
|
|
742
|
+
if (!Number.isFinite(tooltipRect.width) ||
|
|
743
|
+
!Number.isFinite(tooltipRect.height)) {
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
return {
|
|
747
|
+
width: tooltipRect.width,
|
|
748
|
+
height: tooltipRect.height,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
renderTooltipWithConnector(tooltip, arrowEdge, left, top, tooltipWidth, tooltipHeight, targetX, targetY) {
|
|
752
|
+
if (!Number.isFinite(left) ||
|
|
753
|
+
!Number.isFinite(top) ||
|
|
754
|
+
!Number.isFinite(targetX) ||
|
|
755
|
+
!Number.isFinite(targetY)) {
|
|
756
|
+
this.hideTooltipSelection(tooltip);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const connectorLayout = this.resolveTooltipConnectorLayout(arrowEdge, left, top, tooltipWidth, tooltipHeight, targetX, targetY);
|
|
760
|
+
if (!connectorLayout) {
|
|
761
|
+
this.hideTooltipSelection(tooltip);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
this.appendTooltipConnector(tooltip, connectorLayout);
|
|
765
|
+
this.showTooltipAt(tooltip, left, top);
|
|
766
|
+
}
|
|
767
|
+
renderTooltipWithoutConnector(tooltip, left, top) {
|
|
768
|
+
if (!Number.isFinite(left) || !Number.isFinite(top)) {
|
|
769
|
+
this.hideTooltipSelection(tooltip);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
this.showTooltipAt(tooltip, left, top);
|
|
773
|
+
}
|
|
774
|
+
showTooltipAt(tooltip, left, top) {
|
|
775
|
+
tooltip.style('left', `${left}px`).style('top', `${top}px`);
|
|
776
|
+
this.showTooltipSelection(tooltip);
|
|
777
|
+
}
|
|
778
|
+
showTooltipSelection(tooltip) {
|
|
779
|
+
tooltip.style('visibility', 'visible');
|
|
780
|
+
if (!this.transition.show) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
tooltip
|
|
784
|
+
.style('opacity', '1')
|
|
785
|
+
.style('transform', TOOLTIP_VISIBLE_TRANSFORM);
|
|
786
|
+
}
|
|
787
|
+
hideTooltipSelection(tooltip) {
|
|
788
|
+
const node = tooltip.node();
|
|
789
|
+
if (!node) {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
this.hideTooltipElement(node);
|
|
793
|
+
}
|
|
794
|
+
hideTooltipElement(node) {
|
|
795
|
+
if (!this.transition.show) {
|
|
796
|
+
node.style.visibility = 'hidden';
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
node.style.visibility = 'visible';
|
|
800
|
+
node.style.opacity = '0';
|
|
801
|
+
node.style.transform = TOOLTIP_HIDDEN_TRANSFORM;
|
|
802
|
+
}
|
|
803
|
+
setTooltipMarkup(tooltip, content) {
|
|
804
|
+
tooltip.html(`<div data-chart-tooltip-body="true">${content}</div>`);
|
|
805
|
+
const body = tooltip.select('[data-chart-tooltip-body]');
|
|
806
|
+
if (body.empty()) {
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
body.style('position', 'relative').style('z-index', '1');
|
|
810
|
+
}
|
|
811
|
+
appendTooltipConnector(tooltip, connectorLayout) {
|
|
812
|
+
const tooltipBackground = this.tooltipTheme?.background ?? '#ffffff';
|
|
813
|
+
const tooltipBorder = this.tooltipTheme?.border ?? '#dddddd';
|
|
814
|
+
const connector = tooltip
|
|
815
|
+
.append('svg')
|
|
816
|
+
.attr('data-chart-tooltip-connector', 'true')
|
|
817
|
+
.attr('data-chart-tooltip-arrow-edge', connectorLayout.arrowEdge)
|
|
818
|
+
.attr('aria-hidden', 'true')
|
|
819
|
+
.attr('width', connectorLayout.width)
|
|
820
|
+
.attr('height', connectorLayout.height)
|
|
821
|
+
.attr('viewBox', `0 0 ${connectorLayout.width} ${connectorLayout.height}`)
|
|
822
|
+
.style('position', 'absolute')
|
|
823
|
+
.style('left', `${connectorLayout.left}px`)
|
|
824
|
+
.style('top', `${connectorLayout.top}px`)
|
|
825
|
+
.style('pointer-events', 'none')
|
|
826
|
+
.style('overflow', 'visible')
|
|
827
|
+
.style('z-index', '0');
|
|
828
|
+
connector
|
|
829
|
+
.append('path')
|
|
830
|
+
.attr('data-chart-tooltip-connector-path', 'true')
|
|
831
|
+
.attr('d', connectorLayout.path)
|
|
832
|
+
.attr('fill', 'none')
|
|
833
|
+
.attr('stroke', tooltipBorder)
|
|
834
|
+
.attr('stroke-width', 1.25)
|
|
835
|
+
.attr('stroke-linecap', 'round')
|
|
836
|
+
.attr('stroke-linejoin', 'round');
|
|
837
|
+
connector
|
|
838
|
+
.append('path')
|
|
839
|
+
.attr('data-chart-tooltip-arrow', 'true')
|
|
840
|
+
.attr('d', connectorLayout.arrowPath)
|
|
841
|
+
.attr('fill', tooltipBackground)
|
|
842
|
+
.attr('stroke', 'none');
|
|
843
|
+
connector
|
|
844
|
+
.append('path')
|
|
845
|
+
.attr('data-chart-tooltip-arrow-base-mask', 'true')
|
|
846
|
+
.attr('d', connectorLayout.arrowBaseMaskPath)
|
|
847
|
+
.attr('fill', 'none')
|
|
848
|
+
.attr('stroke', tooltipBackground)
|
|
849
|
+
.attr('stroke-width', 2)
|
|
850
|
+
.attr('stroke-linecap', 'butt');
|
|
851
|
+
connector
|
|
852
|
+
.append('path')
|
|
853
|
+
.attr('data-chart-tooltip-arrow-border', 'true')
|
|
854
|
+
.attr('d', connectorLayout.arrowBorderPath)
|
|
855
|
+
.attr('fill', 'none')
|
|
856
|
+
.attr('stroke', tooltipBorder)
|
|
857
|
+
.attr('stroke-width', 1)
|
|
858
|
+
.attr('stroke-linecap', 'round')
|
|
859
|
+
.attr('stroke-linejoin', 'round');
|
|
860
|
+
}
|
|
861
|
+
resolveBarTooltipAnchor(svgNode, dataKey, index) {
|
|
862
|
+
const barNode = svgNode.querySelector(`.bar-${sanitizeForCSS(dataKey)}[data-index="${index}"]`);
|
|
863
|
+
if (!barNode) {
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
const rect = barNode.getBoundingClientRect();
|
|
867
|
+
if (rect.width > 0 || rect.height > 0) {
|
|
868
|
+
return {
|
|
869
|
+
left: rect.left + window.scrollX,
|
|
870
|
+
right: rect.right + window.scrollX,
|
|
871
|
+
top: rect.top + window.scrollY,
|
|
872
|
+
bottom: rect.bottom + window.scrollY,
|
|
873
|
+
centerX: rect.left + window.scrollX + rect.width / 2,
|
|
874
|
+
centerY: rect.top + window.scrollY + rect.height / 2,
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
const svgRect = svgNode.getBoundingClientRect();
|
|
878
|
+
const xValue = Number(barNode.getAttribute('x') ?? '0');
|
|
879
|
+
const yValue = Number(barNode.getAttribute('y') ?? '0');
|
|
880
|
+
const widthValue = Number(barNode.getAttribute('width') ?? '0');
|
|
881
|
+
const heightValue = Number(barNode.getAttribute('height') ?? '0');
|
|
882
|
+
const left = svgRect.left + window.scrollX + xValue;
|
|
883
|
+
const top = svgRect.top + window.scrollY + yValue;
|
|
884
|
+
const right = left + widthValue;
|
|
885
|
+
const bottom = top + heightValue;
|
|
886
|
+
return {
|
|
887
|
+
left,
|
|
888
|
+
right,
|
|
889
|
+
top,
|
|
890
|
+
bottom,
|
|
891
|
+
centerX: left + widthValue / 2,
|
|
892
|
+
centerY: top + heightValue / 2,
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
resolvePointTooltipAnchor(svgNode, isHorizontal, categoryPosition, valuePosition) {
|
|
896
|
+
if (!Number.isFinite(categoryPosition) ||
|
|
897
|
+
!Number.isFinite(valuePosition)) {
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
const svgRect = svgNode.getBoundingClientRect();
|
|
901
|
+
const anchorX = svgRect.left +
|
|
902
|
+
window.scrollX +
|
|
903
|
+
(isHorizontal ? valuePosition : categoryPosition);
|
|
904
|
+
const anchorY = svgRect.top +
|
|
905
|
+
window.scrollY +
|
|
906
|
+
(isHorizontal ? categoryPosition : valuePosition);
|
|
907
|
+
if (!Number.isFinite(anchorX) || !Number.isFinite(anchorY)) {
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
return {
|
|
911
|
+
left: anchorX,
|
|
912
|
+
right: anchorX,
|
|
913
|
+
top: anchorY,
|
|
914
|
+
bottom: anchorY,
|
|
915
|
+
centerX: anchorX,
|
|
916
|
+
centerY: anchorY,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
getSplitTooltip(index, theme) {
|
|
920
|
+
const tooltipId = `${this.splitTooltipOwner}-${index}`;
|
|
921
|
+
const existingTooltip = select(`#${tooltipId}`);
|
|
922
|
+
const tooltip = existingTooltip.empty()
|
|
923
|
+
? select('body')
|
|
924
|
+
.append('div')
|
|
925
|
+
.attr('class', 'chart-tooltip chart-tooltip--split')
|
|
926
|
+
.attr('id', tooltipId)
|
|
927
|
+
.attr('data-chart-tooltip-owner', this.splitTooltipOwner)
|
|
928
|
+
.attr('data-chart-tooltip-index', String(index))
|
|
929
|
+
: existingTooltip;
|
|
930
|
+
this.applyTooltipStylesIfNeeded(tooltip, theme);
|
|
931
|
+
return tooltip;
|
|
932
|
+
}
|
|
933
|
+
hideSplitTooltips() {
|
|
934
|
+
document
|
|
935
|
+
.querySelectorAll(`[data-chart-tooltip-owner="${this.splitTooltipOwner}"]`)
|
|
936
|
+
.forEach((node) => {
|
|
937
|
+
this.hideTooltipElement(node);
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
removeSplitTooltips() {
|
|
941
|
+
document
|
|
942
|
+
.querySelectorAll(`[data-chart-tooltip-owner="${this.splitTooltipOwner}"]`)
|
|
943
|
+
.forEach((node) => {
|
|
944
|
+
node.remove();
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
removeRootTooltip() {
|
|
469
948
|
const tooltip = this.tooltipDiv ?? select(`#${this.id}`);
|
|
470
|
-
if (tooltip.empty()) {
|
|
471
|
-
|
|
949
|
+
if (!tooltip.empty()) {
|
|
950
|
+
tooltip.remove();
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
resolveTooltipArrowEdge(anchor, target, tooltipWidth, tooltipHeight) {
|
|
954
|
+
if (this.position === 'vertical') {
|
|
955
|
+
return this.resolveVerticalPlacementArrowEdge(target, tooltipHeight);
|
|
956
|
+
}
|
|
957
|
+
return this.resolveSidePlacementArrowEdge(anchor, tooltipWidth);
|
|
958
|
+
}
|
|
959
|
+
resolveSidePlacementArrowEdge(anchor, tooltipWidth) {
|
|
960
|
+
const viewportLeft = window.scrollX + TOOLTIP_VIEWPORT_PADDING_PX + TOOLTIP_OFFSET_PX;
|
|
961
|
+
const viewportRight = window.scrollX +
|
|
962
|
+
window.innerWidth -
|
|
963
|
+
TOOLTIP_VIEWPORT_PADDING_PX -
|
|
964
|
+
TOOLTIP_OFFSET_PX;
|
|
965
|
+
const availableRightSpace = viewportRight - anchor.right;
|
|
966
|
+
const availableLeftSpace = anchor.left - viewportLeft;
|
|
967
|
+
if (availableRightSpace >= tooltipWidth ||
|
|
968
|
+
availableRightSpace >= availableLeftSpace) {
|
|
969
|
+
return 'left';
|
|
970
|
+
}
|
|
971
|
+
return 'right';
|
|
972
|
+
}
|
|
973
|
+
resolveVerticalPlacementArrowEdge(target, tooltipHeight) {
|
|
974
|
+
const viewportTop = window.scrollY + TOOLTIP_VIEWPORT_PADDING_PX + TOOLTIP_OFFSET_PX;
|
|
975
|
+
const viewportBottom = window.scrollY +
|
|
976
|
+
window.innerHeight -
|
|
977
|
+
TOOLTIP_VIEWPORT_PADDING_PX -
|
|
978
|
+
TOOLTIP_OFFSET_PX;
|
|
979
|
+
const availableTopSpace = target.y - viewportTop;
|
|
980
|
+
const availableBottomSpace = viewportBottom - target.y;
|
|
981
|
+
if (availableTopSpace >= tooltipHeight ||
|
|
982
|
+
availableTopSpace >= availableBottomSpace) {
|
|
983
|
+
return 'bottom';
|
|
984
|
+
}
|
|
985
|
+
return 'top';
|
|
986
|
+
}
|
|
987
|
+
resolveSharedTooltipTarget(anchor) {
|
|
988
|
+
return {
|
|
989
|
+
x: anchor.centerX,
|
|
990
|
+
y: anchor.centerY,
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
resolveSplitTooltipTarget(currentSeries, anchor) {
|
|
994
|
+
if (currentSeries.type === 'bar') {
|
|
995
|
+
return {
|
|
996
|
+
x: anchor.centerX,
|
|
997
|
+
y: this.barAnchorPosition === 'top'
|
|
998
|
+
? anchor.top
|
|
999
|
+
: anchor.centerY,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
return {
|
|
1003
|
+
x: anchor.centerX,
|
|
1004
|
+
y: anchor.centerY,
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
getTooltipConnectorOffset(start, size, target) {
|
|
1008
|
+
const minOffset = TOOLTIP_CONNECTOR_INSET_PX;
|
|
1009
|
+
const maxOffset = Math.max(minOffset, size - minOffset);
|
|
1010
|
+
const preferredOffset = target - start;
|
|
1011
|
+
return Math.max(minOffset, Math.min(preferredOffset, maxOffset));
|
|
1012
|
+
}
|
|
1013
|
+
getAnchoredTooltipPosition(anchor, target, tooltipWidth, tooltipHeight, arrowEdge) {
|
|
1014
|
+
const minLeft = window.scrollX + TOOLTIP_VIEWPORT_PADDING_PX;
|
|
1015
|
+
const maxLeft = window.scrollX +
|
|
1016
|
+
window.innerWidth -
|
|
1017
|
+
tooltipWidth -
|
|
1018
|
+
TOOLTIP_VIEWPORT_PADDING_PX;
|
|
1019
|
+
const minTop = window.scrollY + TOOLTIP_VIEWPORT_PADDING_PX;
|
|
1020
|
+
const maxTop = window.scrollY +
|
|
1021
|
+
window.innerHeight -
|
|
1022
|
+
tooltipHeight -
|
|
1023
|
+
TOOLTIP_VIEWPORT_PADDING_PX;
|
|
1024
|
+
let left = target.x - tooltipWidth / 2;
|
|
1025
|
+
let top = target.y - tooltipHeight / 2;
|
|
1026
|
+
if (arrowEdge === 'left') {
|
|
1027
|
+
left = anchor.right + TOOLTIP_OFFSET_PX;
|
|
1028
|
+
}
|
|
1029
|
+
else if (arrowEdge === 'right') {
|
|
1030
|
+
left = anchor.left - tooltipWidth - TOOLTIP_OFFSET_PX;
|
|
1031
|
+
}
|
|
1032
|
+
else if (arrowEdge === 'bottom') {
|
|
1033
|
+
top = target.y - tooltipHeight - TOOLTIP_OFFSET_PX;
|
|
1034
|
+
}
|
|
1035
|
+
else {
|
|
1036
|
+
top = target.y + TOOLTIP_OFFSET_PX;
|
|
1037
|
+
}
|
|
1038
|
+
if (!Number.isFinite(left) || !Number.isFinite(top)) {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
return {
|
|
1042
|
+
left: Math.max(minLeft, Math.min(left, maxLeft)),
|
|
1043
|
+
top: Math.max(minTop, Math.min(top, maxTop)),
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
resolveTooltipConnectorLayout(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY) {
|
|
1047
|
+
const localTargetX = targetX - tooltipLeft;
|
|
1048
|
+
const localTargetY = targetY - tooltipTop;
|
|
1049
|
+
if (!Number.isFinite(localTargetX) || !Number.isFinite(localTargetY)) {
|
|
1050
|
+
return null;
|
|
1051
|
+
}
|
|
1052
|
+
const boxArrowPosition = this.resolveTooltipBoxArrowPosition(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY);
|
|
1053
|
+
const arrow = this.resolveTooltipBoxArrow(arrowEdge, boxArrowPosition.x, boxArrowPosition.y);
|
|
1054
|
+
const minX = Math.min(arrow.tipX, arrow.baseStartX, arrow.baseEndX, localTargetX) - TOOLTIP_CONNECTOR_PADDING_PX;
|
|
1055
|
+
const maxX = Math.max(arrow.tipX, arrow.baseStartX, arrow.baseEndX, localTargetX) + TOOLTIP_CONNECTOR_PADDING_PX;
|
|
1056
|
+
const minY = Math.min(arrow.tipY, arrow.baseStartY, arrow.baseEndY, localTargetY) - TOOLTIP_CONNECTOR_PADDING_PX;
|
|
1057
|
+
const maxY = Math.max(arrow.tipY, arrow.baseStartY, arrow.baseEndY, localTargetY) + TOOLTIP_CONNECTOR_PADDING_PX;
|
|
1058
|
+
const width = Math.max(1, maxX - minX);
|
|
1059
|
+
const height = Math.max(1, maxY - minY);
|
|
1060
|
+
const boxX = boxArrowPosition.x - minX;
|
|
1061
|
+
const boxY = boxArrowPosition.y - minY;
|
|
1062
|
+
const startX = arrow.tipX - minX;
|
|
1063
|
+
const startY = arrow.tipY - minY;
|
|
1064
|
+
const arrowBaseStartX = arrow.baseStartX - minX;
|
|
1065
|
+
const arrowBaseStartY = arrow.baseStartY - minY;
|
|
1066
|
+
const arrowBaseEndX = arrow.baseEndX - minX;
|
|
1067
|
+
const arrowBaseEndY = arrow.baseEndY - minY;
|
|
1068
|
+
const endX = localTargetX - minX;
|
|
1069
|
+
const endY = localTargetY - minY;
|
|
1070
|
+
const connectorPath = this.resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY);
|
|
1071
|
+
if (!this.hasFiniteNumbers(width, height, boxX, boxY, startX, startY, arrowBaseStartX, arrowBaseStartY, arrowBaseEndX, arrowBaseEndY, endX, endY)) {
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
return {
|
|
1075
|
+
left: minX,
|
|
1076
|
+
top: minY,
|
|
1077
|
+
arrowEdge,
|
|
1078
|
+
width,
|
|
1079
|
+
height,
|
|
1080
|
+
path: connectorPath,
|
|
1081
|
+
arrowPath: `M ${startX},${startY} L ${arrowBaseStartX},${arrowBaseStartY} L ${arrowBaseEndX},${arrowBaseEndY} Z`,
|
|
1082
|
+
arrowBaseMaskPath: this.resolveTooltipArrowBaseMaskPath(arrowEdge, boxX, boxY, arrowBaseStartX, arrowBaseStartY, arrowBaseEndX, arrowBaseEndY),
|
|
1083
|
+
arrowBorderPath: `M ${arrowBaseStartX},${arrowBaseStartY} L ${startX},${startY} L ${arrowBaseEndX},${arrowBaseEndY}`,
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
resolveTooltipBoxArrowPosition(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY) {
|
|
1087
|
+
switch (arrowEdge) {
|
|
1088
|
+
case 'left':
|
|
1089
|
+
return {
|
|
1090
|
+
x: 0,
|
|
1091
|
+
y: this.getTooltipConnectorOffset(tooltipTop, tooltipHeight, targetY),
|
|
1092
|
+
};
|
|
1093
|
+
case 'right':
|
|
1094
|
+
return {
|
|
1095
|
+
x: tooltipWidth,
|
|
1096
|
+
y: this.getTooltipConnectorOffset(tooltipTop, tooltipHeight, targetY),
|
|
1097
|
+
};
|
|
1098
|
+
case 'top':
|
|
1099
|
+
return {
|
|
1100
|
+
x: this.getTooltipConnectorOffset(tooltipLeft, tooltipWidth, targetX),
|
|
1101
|
+
y: 0,
|
|
1102
|
+
};
|
|
1103
|
+
case 'bottom':
|
|
1104
|
+
return {
|
|
1105
|
+
x: this.getTooltipConnectorOffset(tooltipLeft, tooltipWidth, targetX),
|
|
1106
|
+
y: tooltipHeight,
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
resolveTooltipArrowBaseMaskPath(arrowEdge, boxX, boxY, startX, startY, endX, endY) {
|
|
1111
|
+
if (arrowEdge === 'top') {
|
|
1112
|
+
return `M ${startX - 1},${boxY + 1} L ${endX + 1},${boxY + 1}`;
|
|
1113
|
+
}
|
|
1114
|
+
if (arrowEdge === 'bottom') {
|
|
1115
|
+
return `M ${startX - 1},${boxY - 1} L ${endX + 1},${boxY - 1}`;
|
|
1116
|
+
}
|
|
1117
|
+
if (arrowEdge === 'left') {
|
|
1118
|
+
return `M ${boxX + 1},${startY - 1} L ${boxX + 1},${endY + 1}`;
|
|
1119
|
+
}
|
|
1120
|
+
return `M ${boxX - 1},${startY - 1} L ${boxX - 1},${endY + 1}`;
|
|
1121
|
+
}
|
|
1122
|
+
resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY) {
|
|
1123
|
+
if (arrowEdge === 'left' || arrowEdge === 'right') {
|
|
1124
|
+
if (Math.abs(endY - startY) <=
|
|
1125
|
+
TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX) {
|
|
1126
|
+
return '';
|
|
1127
|
+
}
|
|
1128
|
+
const elbowX = startX + (endX - startX) * TOOLTIP_CONNECTOR_ELBOW_RATIO;
|
|
1129
|
+
return `M ${startX},${startY} L ${elbowX},${startY} L ${endX},${endY}`;
|
|
1130
|
+
}
|
|
1131
|
+
if (Math.abs(endX - startX) <= TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX) {
|
|
1132
|
+
return '';
|
|
1133
|
+
}
|
|
1134
|
+
const elbowY = startY + (endY - startY) * TOOLTIP_CONNECTOR_ELBOW_RATIO;
|
|
1135
|
+
return `M ${startX},${startY} L ${startX},${elbowY} L ${endX},${endY}`;
|
|
1136
|
+
}
|
|
1137
|
+
resolveTooltipBoxArrow(arrowEdge, boxX, boxY) {
|
|
1138
|
+
if (arrowEdge === 'left' || arrowEdge === 'right') {
|
|
1139
|
+
const baseX = boxX;
|
|
1140
|
+
const tipX = arrowEdge === 'left'
|
|
1141
|
+
? baseX - TOOLTIP_BOX_ARROW_LENGTH_PX
|
|
1142
|
+
: baseX + TOOLTIP_BOX_ARROW_LENGTH_PX;
|
|
1143
|
+
return {
|
|
1144
|
+
tipX,
|
|
1145
|
+
tipY: boxY,
|
|
1146
|
+
baseStartX: baseX,
|
|
1147
|
+
baseStartY: boxY - TOOLTIP_BOX_ARROW_HALF_HEIGHT_PX,
|
|
1148
|
+
baseEndX: baseX,
|
|
1149
|
+
baseEndY: boxY + TOOLTIP_BOX_ARROW_HALF_HEIGHT_PX,
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
const baseY = boxY;
|
|
1153
|
+
const tipY = arrowEdge === 'top'
|
|
1154
|
+
? baseY - TOOLTIP_BOX_ARROW_LENGTH_PX
|
|
1155
|
+
: baseY + TOOLTIP_BOX_ARROW_LENGTH_PX;
|
|
1156
|
+
return {
|
|
1157
|
+
tipX: boxX,
|
|
1158
|
+
tipY,
|
|
1159
|
+
baseStartX: boxX - TOOLTIP_BOX_ARROW_HALF_HEIGHT_PX,
|
|
1160
|
+
baseStartY: baseY,
|
|
1161
|
+
baseEndX: boxX + TOOLTIP_BOX_ARROW_HALF_HEIGHT_PX,
|
|
1162
|
+
baseEndY: baseY,
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
hasFiniteNumbers(...values) {
|
|
1166
|
+
return values.every((value) => Number.isFinite(value));
|
|
1167
|
+
}
|
|
1168
|
+
resolveSplitTooltipCollisions(layouts, position, getOppositeArrowEdge) {
|
|
1169
|
+
if (this.position !== position) {
|
|
472
1170
|
return;
|
|
473
1171
|
}
|
|
474
|
-
|
|
475
|
-
|
|
1172
|
+
const placedLayouts = [];
|
|
1173
|
+
const orderedLayouts = [...layouts].sort((a, b) => a.targetY - b.targetY);
|
|
1174
|
+
orderedLayouts.forEach((layout) => {
|
|
1175
|
+
this.flipTooltipIfItReducesCollisions(layout, placedLayouts, getOppositeArrowEdge);
|
|
1176
|
+
placedLayouts.push(layout);
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
getOppositeSideArrowEdge(arrowEdge) {
|
|
1180
|
+
if (arrowEdge === 'left') {
|
|
1181
|
+
return 'right';
|
|
1182
|
+
}
|
|
1183
|
+
if (arrowEdge === 'right') {
|
|
1184
|
+
return 'left';
|
|
1185
|
+
}
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
getOppositeVerticalArrowEdge(arrowEdge) {
|
|
1189
|
+
if (arrowEdge === 'top') {
|
|
1190
|
+
return 'bottom';
|
|
1191
|
+
}
|
|
1192
|
+
if (arrowEdge === 'bottom') {
|
|
1193
|
+
return 'top';
|
|
1194
|
+
}
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1197
|
+
flipTooltipIfItReducesCollisions(layout, placedLayouts, getOppositeArrowEdge) {
|
|
1198
|
+
const currentCollisions = this.countSplitTooltipCollisions(layout, placedLayouts);
|
|
1199
|
+
if (currentCollisions === 0) {
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
const flippedArrowEdge = getOppositeArrowEdge(layout.arrowEdge);
|
|
1203
|
+
if (!flippedArrowEdge) {
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
const flippedPosition = this.getAnchoredTooltipPosition(layout.anchor, { x: layout.targetX, y: layout.targetY }, layout.width, layout.height, flippedArrowEdge);
|
|
1207
|
+
if (!flippedPosition) {
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
const flippedLayout = {
|
|
1211
|
+
...layout,
|
|
1212
|
+
arrowEdge: flippedArrowEdge,
|
|
1213
|
+
left: flippedPosition.left,
|
|
1214
|
+
top: flippedPosition.top,
|
|
1215
|
+
};
|
|
1216
|
+
const flippedCollisions = this.countSplitTooltipCollisions(flippedLayout, placedLayouts);
|
|
1217
|
+
if (flippedCollisions >= currentCollisions) {
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
layout.arrowEdge = flippedArrowEdge;
|
|
1221
|
+
layout.left = flippedPosition.left;
|
|
1222
|
+
layout.top = flippedPosition.top;
|
|
1223
|
+
}
|
|
1224
|
+
countSplitTooltipCollisions(layout, placedLayouts) {
|
|
1225
|
+
return placedLayouts.filter((placedLayout) => this.doSplitTooltipLayoutsOverlap(layout, placedLayout)).length;
|
|
1226
|
+
}
|
|
1227
|
+
doSplitTooltipLayoutsOverlap(a, b) {
|
|
1228
|
+
return (a.left < b.left + b.width + SPLIT_TOOLTIP_GAP_PX &&
|
|
1229
|
+
a.left + a.width + SPLIT_TOOLTIP_GAP_PX > b.left &&
|
|
1230
|
+
a.top < b.top + b.height + SPLIT_TOOLTIP_GAP_PX &&
|
|
1231
|
+
a.top + a.height + SPLIT_TOOLTIP_GAP_PX > b.top);
|
|
1232
|
+
}
|
|
1233
|
+
resolveSplitTooltipPositions(layouts) {
|
|
1234
|
+
const minTop = window.scrollY + TOOLTIP_VIEWPORT_PADDING_PX;
|
|
1235
|
+
const maxBottom = window.scrollY + window.innerHeight - TOOLTIP_VIEWPORT_PADDING_PX;
|
|
1236
|
+
const tooltipsByEdge = {
|
|
1237
|
+
left: [],
|
|
1238
|
+
right: [],
|
|
1239
|
+
top: [],
|
|
1240
|
+
bottom: [],
|
|
1241
|
+
};
|
|
1242
|
+
layouts.forEach((layout) => {
|
|
1243
|
+
tooltipsByEdge[layout.arrowEdge].push(layout);
|
|
1244
|
+
});
|
|
1245
|
+
Object.values(tooltipsByEdge).forEach((edgeLayouts) => {
|
|
1246
|
+
if (edgeLayouts.length === 0) {
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
edgeLayouts.sort((a, b) => a.top - b.top);
|
|
1250
|
+
edgeLayouts[0].top = Math.max(minTop, edgeLayouts[0].top);
|
|
1251
|
+
for (let i = 1; i < edgeLayouts.length; i++) {
|
|
1252
|
+
const previousLayout = edgeLayouts[i - 1];
|
|
1253
|
+
const currentLayout = edgeLayouts[i];
|
|
1254
|
+
const minAllowedTop = previousLayout.top +
|
|
1255
|
+
previousLayout.height +
|
|
1256
|
+
SPLIT_TOOLTIP_GAP_PX;
|
|
1257
|
+
currentLayout.top = Math.max(currentLayout.top, minAllowedTop);
|
|
1258
|
+
}
|
|
1259
|
+
const lastLayout = edgeLayouts[edgeLayouts.length - 1];
|
|
1260
|
+
const overflow = lastLayout.top + lastLayout.height - maxBottom;
|
|
1261
|
+
if (overflow > 0) {
|
|
1262
|
+
lastLayout.top -= overflow;
|
|
1263
|
+
for (let i = edgeLayouts.length - 2; i >= 0; i--) {
|
|
1264
|
+
const currentLayout = edgeLayouts[i];
|
|
1265
|
+
const nextLayout = edgeLayouts[i + 1];
|
|
1266
|
+
const maxAllowedTop = nextLayout.top -
|
|
1267
|
+
currentLayout.height -
|
|
1268
|
+
SPLIT_TOOLTIP_GAP_PX;
|
|
1269
|
+
currentLayout.top = Math.min(currentLayout.top, maxAllowedTop);
|
|
1270
|
+
}
|
|
1271
|
+
const underflow = minTop - edgeLayouts[0].top;
|
|
1272
|
+
if (underflow > 0) {
|
|
1273
|
+
edgeLayouts.forEach((layout) => {
|
|
1274
|
+
layout.top += underflow;
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
edgeLayouts.forEach((layout) => {
|
|
1279
|
+
const maxTop = maxBottom - layout.height;
|
|
1280
|
+
layout.top = Math.max(minTop, Math.min(layout.top, maxTop));
|
|
1281
|
+
});
|
|
1282
|
+
});
|
|
476
1283
|
}
|
|
477
1284
|
}
|
|
1285
|
+
Object.defineProperty(Tooltip, "nextTooltipId", {
|
|
1286
|
+
enumerable: true,
|
|
1287
|
+
configurable: true,
|
|
1288
|
+
writable: true,
|
|
1289
|
+
value: 0
|
|
1290
|
+
});
|