@internetstiftelsen/charts 0.15.0 → 0.16.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 -0
- package/dist/theme.js +6 -0
- package/dist/tooltip/dom.d.ts +56 -0
- package/dist/tooltip/dom.js +438 -0
- package/dist/tooltip/geometry.d.ts +18 -0
- package/dist/tooltip/geometry.js +395 -0
- package/dist/tooltip/types.d.ts +77 -0
- package/dist/tooltip/types.js +24 -0
- package/dist/tooltip/xy-interaction.d.ts +33 -0
- package/dist/tooltip/xy-interaction.js +608 -0
- package/dist/tooltip.d.ts +4 -74
- package/dist/tooltip.js +41 -1444
- package/dist/types.d.ts +2 -0
- package/dist/x-axis.d.ts +11 -1
- package/dist/x-axis.js +150 -10
- package/dist/xy-chart.d.ts +1 -0
- package/dist/xy-chart.js +10 -4
- package/docs/components.md +16 -19
- package/docs/xy-chart.md +3 -10
- package/package.json +1 -1
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import { pointer, select } from 'd3';
|
|
2
|
+
import { getSeriesColor } from '../types.js';
|
|
3
|
+
import { sanitizeForCSS } from '../utils.js';
|
|
4
|
+
import { getAnchoredTooltipPosition, resolveSharedTooltipTarget, resolveSplitTooltipPositions, resolveSplitTooltipTarget, resolveTooltipArrowEdge, } from './geometry.js';
|
|
5
|
+
export function attachXYTooltipArea(config) {
|
|
6
|
+
const { svg, data, series, xKey, x, y, theme, plotArea, parseValue, isHorizontal, categoryScaleType, mode, position, barAnchorPosition, formatter, labelFormatter, customFormatter, dom, } = config;
|
|
7
|
+
const tooltip = dom.getRootTooltip();
|
|
8
|
+
if (!tooltip || data.length === 0) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const resolveSeriesValue = config.resolveSeriesValue ??
|
|
12
|
+
((targetSeries, dataPoint) => {
|
|
13
|
+
const rawValue = dataPoint[targetSeries.dataKey];
|
|
14
|
+
if (rawValue === null || rawValue === undefined) {
|
|
15
|
+
return NaN;
|
|
16
|
+
}
|
|
17
|
+
return parseValue(rawValue);
|
|
18
|
+
});
|
|
19
|
+
const getXPosition = (dataPoint) => {
|
|
20
|
+
const scaled = x(getCategoryScaleValue(dataPoint[xKey], categoryScaleType));
|
|
21
|
+
return (scaled || 0) + (x.bandwidth ? x.bandwidth() / 2 : 0);
|
|
22
|
+
};
|
|
23
|
+
const getYPosition = (dataPoint) => {
|
|
24
|
+
const scaled = y(getCategoryScaleValue(dataPoint[xKey], categoryScaleType));
|
|
25
|
+
return (scaled || 0) + (y.bandwidth ? y.bandwidth() / 2 : 0);
|
|
26
|
+
};
|
|
27
|
+
const buildTooltipLabel = (dataPoint) => {
|
|
28
|
+
const labelValue = dataPoint[xKey];
|
|
29
|
+
return labelFormatter
|
|
30
|
+
? labelFormatter(String(labelValue), dataPoint)
|
|
31
|
+
: String(labelValue);
|
|
32
|
+
};
|
|
33
|
+
const buildTooltipRow = (dataPoint, currentSeries) => {
|
|
34
|
+
const value = dataPoint[currentSeries.dataKey];
|
|
35
|
+
if (formatter) {
|
|
36
|
+
return formatter(currentSeries.dataKey, normalizeFormatterValue(value), dataPoint);
|
|
37
|
+
}
|
|
38
|
+
return `${currentSeries.dataKey}: ${value}`;
|
|
39
|
+
};
|
|
40
|
+
const getVisibleTooltipSeries = (dataPoint) => {
|
|
41
|
+
return series.filter((currentSeries) => {
|
|
42
|
+
const value = dataPoint[currentSeries.dataKey];
|
|
43
|
+
return value !== null && value !== undefined;
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
const buildSharedTooltipContent = (dataPoint) => {
|
|
47
|
+
const visibleSeries = getVisibleTooltipSeries(dataPoint);
|
|
48
|
+
if (visibleSeries.length === 0) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (customFormatter) {
|
|
52
|
+
return customFormatter(dataPoint, visibleSeries);
|
|
53
|
+
}
|
|
54
|
+
const label = buildTooltipLabel(dataPoint);
|
|
55
|
+
const rows = visibleSeries.map((currentSeries) => buildTooltipRow(dataPoint, currentSeries));
|
|
56
|
+
return `<strong>${label}</strong><br/>${rows.join('<br/>')}`;
|
|
57
|
+
};
|
|
58
|
+
const buildSplitTooltipContent = (dataPoint, currentSeries) => {
|
|
59
|
+
if (customFormatter) {
|
|
60
|
+
return customFormatter(dataPoint, [currentSeries]);
|
|
61
|
+
}
|
|
62
|
+
const label = buildTooltipLabel(dataPoint);
|
|
63
|
+
return `<strong>${label}</strong><br/>${buildTooltipRow(dataPoint, currentSeries)}`;
|
|
64
|
+
};
|
|
65
|
+
const buildAccessibleLabel = (dataPoint) => {
|
|
66
|
+
const content = buildSharedTooltipContent(dataPoint);
|
|
67
|
+
return content ? stripHtml(content) : buildTooltipLabel(dataPoint);
|
|
68
|
+
};
|
|
69
|
+
const dataPointPositions = data.map((dataPoint) => isHorizontal ? getYPosition(dataPoint) : getXPosition(dataPoint));
|
|
70
|
+
const focusCircleSeries = series.filter((currentSeries) => {
|
|
71
|
+
if (currentSeries.type === 'line' &&
|
|
72
|
+
currentSeries.points.show === 'never') {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return (currentSeries.type === 'line' ||
|
|
76
|
+
currentSeries.type === 'area' ||
|
|
77
|
+
currentSeries.type === 'scatter');
|
|
78
|
+
});
|
|
79
|
+
const barSeries = series.filter((currentSeries) => {
|
|
80
|
+
return currentSeries.type === 'bar';
|
|
81
|
+
});
|
|
82
|
+
const hasBarSeries = barSeries.length > 0;
|
|
83
|
+
const overlay = svg
|
|
84
|
+
.append('rect')
|
|
85
|
+
.attr('class', 'tooltip-overlay')
|
|
86
|
+
.attr('x', plotArea.left)
|
|
87
|
+
.attr('y', plotArea.top)
|
|
88
|
+
.attr('width', plotArea.width)
|
|
89
|
+
.attr('height', plotArea.height)
|
|
90
|
+
.attr('aria-hidden', 'true')
|
|
91
|
+
.style('fill', 'none')
|
|
92
|
+
.style('pointer-events', 'all');
|
|
93
|
+
const focusCircles = focusCircleSeries.map((currentSeries) => {
|
|
94
|
+
const seriesColor = getSeriesColor(currentSeries);
|
|
95
|
+
return svg
|
|
96
|
+
.append('circle')
|
|
97
|
+
.attr('class', `focus-circle-${sanitizeForCSS(currentSeries.dataKey)}`)
|
|
98
|
+
.attr('r', theme.line.point.size + 1)
|
|
99
|
+
.attr('fill', theme.line.point.color || seriesColor)
|
|
100
|
+
.attr('stroke', theme.line.point.strokeColor || seriesColor)
|
|
101
|
+
.attr('stroke-width', theme.line.point.strokeWidth)
|
|
102
|
+
.attr('aria-hidden', 'true')
|
|
103
|
+
.style('opacity', 0)
|
|
104
|
+
.style('pointer-events', 'none');
|
|
105
|
+
});
|
|
106
|
+
const clearVisualState = () => {
|
|
107
|
+
focusCircles.forEach((circle) => circle.style('opacity', 0));
|
|
108
|
+
if (!hasBarSeries) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
barSeries.forEach((currentSeries) => {
|
|
112
|
+
svg.selectAll(`.bar-${sanitizeForCSS(currentSeries.dataKey)}`).style('opacity', 1);
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
const updateVisualStateAtIndex = (closestIndex) => {
|
|
116
|
+
const dataPoint = data[closestIndex];
|
|
117
|
+
const dataPointPosition = dataPointPositions[closestIndex];
|
|
118
|
+
focusCircleSeries.forEach((currentSeries, seriesIndex) => {
|
|
119
|
+
const value = resolveSeriesValue(currentSeries, dataPoint, closestIndex);
|
|
120
|
+
if (!Number.isFinite(value)) {
|
|
121
|
+
focusCircles[seriesIndex].style('opacity', 0);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (isHorizontal) {
|
|
125
|
+
focusCircles[seriesIndex]
|
|
126
|
+
.attr('cx', x(value) ?? 0)
|
|
127
|
+
.attr('cy', dataPointPosition)
|
|
128
|
+
.style('opacity', 1);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
focusCircles[seriesIndex]
|
|
132
|
+
.attr('cx', dataPointPosition)
|
|
133
|
+
.attr('cy', y(value) ?? 0)
|
|
134
|
+
.style('opacity', 1);
|
|
135
|
+
});
|
|
136
|
+
if (!hasBarSeries) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
barSeries.forEach((currentSeries) => {
|
|
140
|
+
svg.selectAll(`.bar-${sanitizeForCSS(currentSeries.dataKey)}`).style('opacity', (_, index) => index === closestIndex ? 1 : 0.5);
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
const showSharedTooltipAtIndex = (closestIndex) => {
|
|
144
|
+
const dataPoint = data[closestIndex];
|
|
145
|
+
const dataPointPosition = dataPointPositions[closestIndex];
|
|
146
|
+
updateVisualStateAtIndex(closestIndex);
|
|
147
|
+
dom.hideSplitTooltips();
|
|
148
|
+
const content = buildSharedTooltipContent(dataPoint);
|
|
149
|
+
if (!content) {
|
|
150
|
+
dom.hideTooltipSelection(tooltip);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const measuredTooltip = dom.measureTooltip(tooltip, content);
|
|
154
|
+
if (!measuredTooltip) {
|
|
155
|
+
dom.hideTooltipSelection(tooltip);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const svgNode = svg.node();
|
|
159
|
+
if (!svgNode) {
|
|
160
|
+
dom.hideTooltipSelection(tooltip);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const sharedAnchor = resolveSharedTooltipAnchor({
|
|
164
|
+
svgNode,
|
|
165
|
+
dataPoint,
|
|
166
|
+
dataPointPosition,
|
|
167
|
+
series,
|
|
168
|
+
x,
|
|
169
|
+
y,
|
|
170
|
+
isHorizontal,
|
|
171
|
+
resolveSeriesValue,
|
|
172
|
+
closestIndex,
|
|
173
|
+
});
|
|
174
|
+
const target = resolveSharedTooltipTarget(sharedAnchor);
|
|
175
|
+
const arrowEdge = resolveTooltipArrowEdge(position, sharedAnchor, target, measuredTooltip.width, measuredTooltip.height);
|
|
176
|
+
const tooltipPosition = getAnchoredTooltipPosition(sharedAnchor, target, measuredTooltip.width, measuredTooltip.height, arrowEdge);
|
|
177
|
+
if (!tooltipPosition) {
|
|
178
|
+
dom.hideTooltipSelection(tooltip);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
dom.renderTooltipWithoutConnector(tooltip, tooltipPosition.left, tooltipPosition.top);
|
|
182
|
+
};
|
|
183
|
+
const getSeriesTooltipAnchor = (currentSeries, dataPoint, index) => {
|
|
184
|
+
const value = resolveSeriesValue(currentSeries, dataPoint, index);
|
|
185
|
+
if (!Number.isFinite(value)) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const svgNode = svg.node();
|
|
189
|
+
if (!svgNode) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
if (currentSeries.type === 'bar') {
|
|
193
|
+
return resolveBarTooltipAnchor(svgNode, currentSeries.dataKey, index);
|
|
194
|
+
}
|
|
195
|
+
const categoryPosition = dataPointPositions[index];
|
|
196
|
+
const valuePosition = isHorizontal ? (x(value) ?? 0) : (y(value) ?? 0);
|
|
197
|
+
return resolvePointTooltipAnchor(svgNode, isHorizontal, categoryPosition, valuePosition);
|
|
198
|
+
};
|
|
199
|
+
const showSplitTooltipAtIndex = (closestIndex) => {
|
|
200
|
+
const dataPoint = data[closestIndex];
|
|
201
|
+
updateVisualStateAtIndex(closestIndex);
|
|
202
|
+
dom.hideTooltipSelection(tooltip);
|
|
203
|
+
const layouts = [];
|
|
204
|
+
series.forEach((currentSeries, seriesIndex) => {
|
|
205
|
+
const rawValue = dataPoint[currentSeries.dataKey];
|
|
206
|
+
if (rawValue === null || rawValue === undefined) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const anchor = getSeriesTooltipAnchor(currentSeries, dataPoint, closestIndex);
|
|
210
|
+
if (!anchor) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const splitTooltip = dom.getSplitTooltip(seriesIndex, theme);
|
|
214
|
+
const content = buildSplitTooltipContent(dataPoint, currentSeries);
|
|
215
|
+
const measuredTooltip = dom.measureTooltip(splitTooltip, content);
|
|
216
|
+
if (!measuredTooltip) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const target = resolveSplitTooltipTarget(currentSeries, anchor, barAnchorPosition);
|
|
220
|
+
layouts.push({
|
|
221
|
+
div: splitTooltip,
|
|
222
|
+
anchor,
|
|
223
|
+
width: measuredTooltip.width,
|
|
224
|
+
height: measuredTooltip.height,
|
|
225
|
+
left: 0,
|
|
226
|
+
top: 0,
|
|
227
|
+
arrowEdge: 'left',
|
|
228
|
+
targetX: target.x,
|
|
229
|
+
targetY: target.y,
|
|
230
|
+
order: seriesIndex,
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
if (layouts.length === 0) {
|
|
234
|
+
dom.hideSplitTooltips();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
resolveSplitTooltipPositions(layouts, position);
|
|
238
|
+
dom.hideUnusedSplitTooltips(layouts.map((layout) => layout.div));
|
|
239
|
+
layouts.forEach((layout) => {
|
|
240
|
+
dom.renderTooltipWithConnector(layout.div, layout.arrowEdge, layout.left, layout.top, layout.width, layout.height, layout.targetX, layout.targetY, layout.anchor);
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
let activeTooltipIndex = null;
|
|
244
|
+
const hideTooltip = () => {
|
|
245
|
+
activeTooltipIndex = null;
|
|
246
|
+
dom.hideTooltipSelection(tooltip);
|
|
247
|
+
dom.hideSplitTooltips();
|
|
248
|
+
clearVisualState();
|
|
249
|
+
};
|
|
250
|
+
const tooltipSvgNode = svg.node();
|
|
251
|
+
const queuedRender = createQueuedTooltipRender(tooltipSvgNode, (index) => {
|
|
252
|
+
activeTooltipIndex = index;
|
|
253
|
+
if (mode === 'split') {
|
|
254
|
+
showSplitTooltipAtIndex(index);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
showSharedTooltipAtIndex(index);
|
|
258
|
+
});
|
|
259
|
+
overlay
|
|
260
|
+
.on('mousemove', (event) => {
|
|
261
|
+
const [mouseX, mouseY] = pointer(event, svg.node());
|
|
262
|
+
const closestIndex = getClosestIndexFromPointer(mouseX, mouseY, dataPointPositions, isHorizontal);
|
|
263
|
+
queuedRender.request(closestIndex);
|
|
264
|
+
})
|
|
265
|
+
.on('mouseout', () => {
|
|
266
|
+
if (isTooltipFocusTarget(document.activeElement)) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
queuedRender.cancel();
|
|
270
|
+
hideTooltip();
|
|
271
|
+
});
|
|
272
|
+
const focusTargets = svg
|
|
273
|
+
.append('g')
|
|
274
|
+
.attr('class', 'tooltip-focus-targets')
|
|
275
|
+
.selectAll('rect')
|
|
276
|
+
.data(data)
|
|
277
|
+
.join('rect')
|
|
278
|
+
.attr('class', 'tooltip-focus-target')
|
|
279
|
+
.attr('data-index', (_, i) => i)
|
|
280
|
+
.attr('x', (_, i) => getFocusTargetBounds(i, dataPointPositions, plotArea, x, y, isHorizontal).x)
|
|
281
|
+
.attr('y', (_, i) => getFocusTargetBounds(i, dataPointPositions, plotArea, x, y, isHorizontal).y)
|
|
282
|
+
.attr('width', (_, i) => getFocusTargetBounds(i, dataPointPositions, plotArea, x, y, isHorizontal).width)
|
|
283
|
+
.attr('height', (_, i) => getFocusTargetBounds(i, dataPointPositions, plotArea, x, y, isHorizontal).height)
|
|
284
|
+
.attr('tabindex', 0)
|
|
285
|
+
.attr('fill', 'transparent')
|
|
286
|
+
.attr('stroke', 'none')
|
|
287
|
+
.attr('stroke-width', 0)
|
|
288
|
+
.attr('aria-label', (dataPoint) => buildAccessibleLabel(dataPoint))
|
|
289
|
+
.style('pointer-events', 'none');
|
|
290
|
+
const focusTargetNodes = focusTargets.nodes();
|
|
291
|
+
focusTargets
|
|
292
|
+
.on('focus', function () {
|
|
293
|
+
const currentIndex = focusTargetNodes.indexOf(this);
|
|
294
|
+
if (currentIndex === -1) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
queuedRender.cancel();
|
|
298
|
+
select(this).attr('stroke', '#111827').attr('stroke-width', 2);
|
|
299
|
+
activeTooltipIndex = currentIndex;
|
|
300
|
+
if (mode === 'split') {
|
|
301
|
+
showSplitTooltipAtIndex(currentIndex);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
showSharedTooltipAtIndex(currentIndex);
|
|
305
|
+
})
|
|
306
|
+
.on('blur', function (event) {
|
|
307
|
+
select(this).attr('stroke', 'none').attr('stroke-width', 0);
|
|
308
|
+
if (isTooltipFocusTarget(event.relatedTarget)) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
queuedRender.cancel();
|
|
312
|
+
hideTooltip();
|
|
313
|
+
})
|
|
314
|
+
.on('keydown', function (event) {
|
|
315
|
+
const currentIndex = focusTargetNodes.indexOf(this);
|
|
316
|
+
if (currentIndex === -1) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const nextIndex = getNextFocusTargetIndex(currentIndex, event.key, focusTargetNodes.length);
|
|
320
|
+
if (nextIndex < 0 || nextIndex >= focusTargetNodes.length) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
event.preventDefault();
|
|
324
|
+
focusTargetNodes[nextIndex].focus();
|
|
325
|
+
});
|
|
326
|
+
return attachTooltipScrollListeners(tooltipSvgNode, () => {
|
|
327
|
+
if (activeTooltipIndex === null) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
queuedRender.request(activeTooltipIndex);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
function normalizeFormatterValue(value) {
|
|
334
|
+
if (value === null ||
|
|
335
|
+
value === undefined ||
|
|
336
|
+
typeof value === 'string' ||
|
|
337
|
+
typeof value === 'number' ||
|
|
338
|
+
typeof value === 'boolean' ||
|
|
339
|
+
value instanceof Date) {
|
|
340
|
+
return value;
|
|
341
|
+
}
|
|
342
|
+
return String(value);
|
|
343
|
+
}
|
|
344
|
+
function getCategoryScaleValue(value, scaleType) {
|
|
345
|
+
switch (scaleType) {
|
|
346
|
+
case 'band':
|
|
347
|
+
return String(value);
|
|
348
|
+
case 'time':
|
|
349
|
+
return value instanceof Date ? value : new Date(String(value));
|
|
350
|
+
case 'linear':
|
|
351
|
+
case 'log':
|
|
352
|
+
return typeof value === 'number' ? value : Number(value);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function stripHtml(content) {
|
|
356
|
+
return content
|
|
357
|
+
.replace(/<br\s*\/?>/gi, '. ')
|
|
358
|
+
.replace(/<[^>]+>/g, ' ')
|
|
359
|
+
.replace(/\s+/g, ' ')
|
|
360
|
+
.trim();
|
|
361
|
+
}
|
|
362
|
+
function isTooltipFocusTarget(element) {
|
|
363
|
+
return (element instanceof SVGElement &&
|
|
364
|
+
element.classList.contains('tooltip-focus-target'));
|
|
365
|
+
}
|
|
366
|
+
function getClosestIndexFromPointer(mouseX, mouseY, dataPointPositions, isHorizontal) {
|
|
367
|
+
const pointerPosition = isHorizontal ? mouseY : mouseX;
|
|
368
|
+
let closestIndex = 0;
|
|
369
|
+
let minDistance = Math.abs(pointerPosition - dataPointPositions[0]);
|
|
370
|
+
for (let i = 1; i < dataPointPositions.length; i++) {
|
|
371
|
+
const distance = Math.abs(pointerPosition - dataPointPositions[i]);
|
|
372
|
+
if (distance < minDistance) {
|
|
373
|
+
minDistance = distance;
|
|
374
|
+
closestIndex = i;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return closestIndex;
|
|
378
|
+
}
|
|
379
|
+
function resolveSharedTooltipAnchor({ svgNode, dataPoint, dataPointPosition, series, x, y, isHorizontal, resolveSeriesValue, closestIndex, }) {
|
|
380
|
+
const svgRect = svgNode.getBoundingClientRect();
|
|
381
|
+
const values = series.map((currentSeries) => resolveSeriesValue(currentSeries, dataPoint, closestIndex));
|
|
382
|
+
const finiteValues = values.filter((value) => Number.isFinite(value));
|
|
383
|
+
const minValue = finiteValues.length ? Math.min(...finiteValues) : 0;
|
|
384
|
+
const maxValue = finiteValues.length ? Math.max(...finiteValues) : 0;
|
|
385
|
+
if (isHorizontal) {
|
|
386
|
+
const minX = x(minValue);
|
|
387
|
+
const maxX = x(maxValue);
|
|
388
|
+
const centerX = svgRect.left + window.scrollX + (minX + maxX) / 2;
|
|
389
|
+
const centerY = svgRect.top + window.scrollY + dataPointPosition;
|
|
390
|
+
return {
|
|
391
|
+
left: centerX,
|
|
392
|
+
right: centerX,
|
|
393
|
+
top: centerY,
|
|
394
|
+
bottom: centerY,
|
|
395
|
+
centerX,
|
|
396
|
+
centerY,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
const minY = y(maxValue);
|
|
400
|
+
const maxY = y(minValue);
|
|
401
|
+
const centerX = svgRect.left + window.scrollX + dataPointPosition;
|
|
402
|
+
const topY = svgRect.top + window.scrollY + minY;
|
|
403
|
+
const bottomY = svgRect.top + window.scrollY + maxY;
|
|
404
|
+
return {
|
|
405
|
+
left: centerX,
|
|
406
|
+
right: centerX,
|
|
407
|
+
top: topY,
|
|
408
|
+
bottom: bottomY,
|
|
409
|
+
centerX,
|
|
410
|
+
centerY: (topY + bottomY) / 2,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function resolveBarTooltipAnchor(svgNode, dataKey, index) {
|
|
414
|
+
const barNode = svgNode.querySelector(`.bar-${sanitizeForCSS(dataKey)}[data-index="${index}"]`);
|
|
415
|
+
if (!barNode) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
const rect = barNode.getBoundingClientRect();
|
|
419
|
+
if (rect.width > 0 || rect.height > 0) {
|
|
420
|
+
return {
|
|
421
|
+
left: rect.left + window.scrollX,
|
|
422
|
+
right: rect.right + window.scrollX,
|
|
423
|
+
top: rect.top + window.scrollY,
|
|
424
|
+
bottom: rect.bottom + window.scrollY,
|
|
425
|
+
centerX: rect.left + window.scrollX + rect.width / 2,
|
|
426
|
+
centerY: rect.top + window.scrollY + rect.height / 2,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
const svgRect = svgNode.getBoundingClientRect();
|
|
430
|
+
const xValue = Number(barNode.getAttribute('x') ?? '0');
|
|
431
|
+
const yValue = Number(barNode.getAttribute('y') ?? '0');
|
|
432
|
+
const widthValue = Number(barNode.getAttribute('width') ?? '0');
|
|
433
|
+
const heightValue = Number(barNode.getAttribute('height') ?? '0');
|
|
434
|
+
const left = svgRect.left + window.scrollX + xValue;
|
|
435
|
+
const top = svgRect.top + window.scrollY + yValue;
|
|
436
|
+
const right = left + widthValue;
|
|
437
|
+
const bottom = top + heightValue;
|
|
438
|
+
return {
|
|
439
|
+
left,
|
|
440
|
+
right,
|
|
441
|
+
top,
|
|
442
|
+
bottom,
|
|
443
|
+
centerX: left + widthValue / 2,
|
|
444
|
+
centerY: top + heightValue / 2,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function resolvePointTooltipAnchor(svgNode, isHorizontal, categoryPosition, valuePosition) {
|
|
448
|
+
if (!Number.isFinite(categoryPosition) || !Number.isFinite(valuePosition)) {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
const svgRect = svgNode.getBoundingClientRect();
|
|
452
|
+
const anchorX = svgRect.left +
|
|
453
|
+
window.scrollX +
|
|
454
|
+
(isHorizontal ? valuePosition : categoryPosition);
|
|
455
|
+
const anchorY = svgRect.top +
|
|
456
|
+
window.scrollY +
|
|
457
|
+
(isHorizontal ? categoryPosition : valuePosition);
|
|
458
|
+
if (!Number.isFinite(anchorX) || !Number.isFinite(anchorY)) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
left: anchorX,
|
|
463
|
+
right: anchorX,
|
|
464
|
+
top: anchorY,
|
|
465
|
+
bottom: anchorY,
|
|
466
|
+
centerX: anchorX,
|
|
467
|
+
centerY: anchorY,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function createQueuedTooltipRender(tooltipSvgNode, renderTooltipAtIndex) {
|
|
471
|
+
let pendingTooltipFrame = null;
|
|
472
|
+
let pendingTooltipIndex = null;
|
|
473
|
+
const request = (index) => {
|
|
474
|
+
pendingTooltipIndex = index;
|
|
475
|
+
if (pendingTooltipFrame !== null) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
pendingTooltipFrame = requestFrame(() => {
|
|
479
|
+
const nextIndex = pendingTooltipIndex;
|
|
480
|
+
pendingTooltipFrame = null;
|
|
481
|
+
pendingTooltipIndex = null;
|
|
482
|
+
if (nextIndex === null) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (!tooltipSvgNode?.isConnected) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
renderTooltipAtIndex(nextIndex);
|
|
489
|
+
});
|
|
490
|
+
};
|
|
491
|
+
const cancel = () => {
|
|
492
|
+
if (pendingTooltipFrame === null) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
cancelFrame(pendingTooltipFrame);
|
|
496
|
+
pendingTooltipFrame = null;
|
|
497
|
+
pendingTooltipIndex = null;
|
|
498
|
+
};
|
|
499
|
+
return { request, cancel };
|
|
500
|
+
}
|
|
501
|
+
function requestFrame(callback) {
|
|
502
|
+
if (typeof window.requestAnimationFrame === 'function') {
|
|
503
|
+
return window.requestAnimationFrame(callback);
|
|
504
|
+
}
|
|
505
|
+
return window.setTimeout(() => {
|
|
506
|
+
callback(window.performance.now());
|
|
507
|
+
}, 16);
|
|
508
|
+
}
|
|
509
|
+
function cancelFrame(frameId) {
|
|
510
|
+
if (typeof window.cancelAnimationFrame === 'function') {
|
|
511
|
+
window.cancelAnimationFrame(frameId);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
window.clearTimeout(frameId);
|
|
515
|
+
}
|
|
516
|
+
function getFocusTargetBounds(index, dataPointPositions, plotArea, x, y, isHorizontal) {
|
|
517
|
+
if (isHorizontal) {
|
|
518
|
+
if (y.bandwidth) {
|
|
519
|
+
const targetHeight = y.bandwidth();
|
|
520
|
+
return {
|
|
521
|
+
x: plotArea.left,
|
|
522
|
+
y: dataPointPositions[index] - targetHeight / 2,
|
|
523
|
+
width: plotArea.width,
|
|
524
|
+
height: targetHeight,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
const top = index === 0
|
|
528
|
+
? plotArea.top
|
|
529
|
+
: (dataPointPositions[index - 1] + dataPointPositions[index]) /
|
|
530
|
+
2;
|
|
531
|
+
const bottom = index === dataPointPositions.length - 1
|
|
532
|
+
? plotArea.bottom
|
|
533
|
+
: (dataPointPositions[index] + dataPointPositions[index + 1]) /
|
|
534
|
+
2;
|
|
535
|
+
return {
|
|
536
|
+
x: plotArea.left,
|
|
537
|
+
y: top,
|
|
538
|
+
width: plotArea.width,
|
|
539
|
+
height: bottom - top,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
if (x.bandwidth) {
|
|
543
|
+
const targetWidth = x.bandwidth();
|
|
544
|
+
return {
|
|
545
|
+
x: dataPointPositions[index] - targetWidth / 2,
|
|
546
|
+
y: plotArea.top,
|
|
547
|
+
width: targetWidth,
|
|
548
|
+
height: plotArea.height,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
const left = index === 0
|
|
552
|
+
? plotArea.left
|
|
553
|
+
: (dataPointPositions[index - 1] + dataPointPositions[index]) / 2;
|
|
554
|
+
const right = index === dataPointPositions.length - 1
|
|
555
|
+
? plotArea.right
|
|
556
|
+
: (dataPointPositions[index] + dataPointPositions[index + 1]) / 2;
|
|
557
|
+
return {
|
|
558
|
+
x: left,
|
|
559
|
+
y: plotArea.top,
|
|
560
|
+
width: right - left,
|
|
561
|
+
height: plotArea.height,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function getNextFocusTargetIndex(currentIndex, key, focusTargetCount) {
|
|
565
|
+
switch (key) {
|
|
566
|
+
case 'ArrowRight':
|
|
567
|
+
case 'ArrowDown':
|
|
568
|
+
return currentIndex + 1;
|
|
569
|
+
case 'ArrowLeft':
|
|
570
|
+
case 'ArrowUp':
|
|
571
|
+
return currentIndex - 1;
|
|
572
|
+
case 'Home':
|
|
573
|
+
return 0;
|
|
574
|
+
case 'End':
|
|
575
|
+
return focusTargetCount - 1;
|
|
576
|
+
default:
|
|
577
|
+
return -1;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function attachTooltipScrollListeners(svgNode, onScroll) {
|
|
581
|
+
if (!svgNode) {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
const scrollTargets = getTooltipScrollTargets(svgNode);
|
|
585
|
+
scrollTargets.forEach((target) => {
|
|
586
|
+
target.addEventListener('scroll', onScroll, { passive: true });
|
|
587
|
+
});
|
|
588
|
+
return () => {
|
|
589
|
+
scrollTargets.forEach((target) => {
|
|
590
|
+
target.removeEventListener('scroll', onScroll);
|
|
591
|
+
});
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function getTooltipScrollTargets(svgNode) {
|
|
595
|
+
const scrollTargets = new Set([window]);
|
|
596
|
+
let currentElement = svgNode.parentElement;
|
|
597
|
+
while (currentElement) {
|
|
598
|
+
if (isScrollableElement(currentElement)) {
|
|
599
|
+
scrollTargets.add(currentElement);
|
|
600
|
+
}
|
|
601
|
+
currentElement = currentElement.parentElement;
|
|
602
|
+
}
|
|
603
|
+
return [...scrollTargets];
|
|
604
|
+
}
|
|
605
|
+
function isScrollableElement(element) {
|
|
606
|
+
const style = window.getComputedStyle(element);
|
|
607
|
+
return [style.overflow, style.overflowX, style.overflowY].some((value) => value === 'auto' || value === 'scroll' || value === 'overlay');
|
|
608
|
+
}
|
package/dist/tooltip.d.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Selection } from 'd3';
|
|
2
2
|
import type { TooltipConfig, DataItem, DataValue, D3Scale, ChartTheme, ExportHooks, TooltipConfigBase, ScaleType, TooltipMode, TooltipPosition, TooltipBarAnchorPosition, TooltipTransitionConfig } from './types.js';
|
|
3
3
|
import type { ChartComponent } from './chart-interface.js';
|
|
4
|
-
import type { Line } from './line.js';
|
|
5
|
-
import type { Bar } from './bar.js';
|
|
6
|
-
import type { Area } from './area.js';
|
|
7
|
-
import type { Scatter } from './scatter.js';
|
|
8
4
|
import type { PlotAreaBounds } from './layout-manager.js';
|
|
9
|
-
type XYTooltipSeries
|
|
5
|
+
import { type XYTooltipSeries } from './tooltip/types.js';
|
|
10
6
|
export declare class Tooltip implements ChartComponent<TooltipConfigBase> {
|
|
11
7
|
private static nextTooltipId;
|
|
12
8
|
readonly id: string;
|
|
@@ -24,10 +20,8 @@ export declare class Tooltip implements ChartComponent<TooltipConfigBase> {
|
|
|
24
20
|
fill?: string;
|
|
25
21
|
}[]) => string;
|
|
26
22
|
readonly exportHooks?: ExportHooks<TooltipConfigBase>;
|
|
27
|
-
private readonly
|
|
28
|
-
private
|
|
29
|
-
private tooltipDiv;
|
|
30
|
-
private tooltipTheme;
|
|
23
|
+
private readonly dom;
|
|
24
|
+
private detachTooltipScrollListeners;
|
|
31
25
|
constructor(config?: TooltipConfig);
|
|
32
26
|
getExportConfig(): TooltipConfigBase;
|
|
33
27
|
createExportComponent(override?: Partial<TooltipConfigBase>): ChartComponent<TooltipConfigBase>;
|
|
@@ -38,68 +32,4 @@ export declare class Tooltip implements ChartComponent<TooltipConfigBase> {
|
|
|
38
32
|
showAt(left: number, top: number): void;
|
|
39
33
|
hide(): void;
|
|
40
34
|
cleanup(): void;
|
|
41
|
-
private applyTooltipStylesIfNeeded;
|
|
42
|
-
private getTooltipStyleKey;
|
|
43
|
-
private writeTooltipStyles;
|
|
44
|
-
private measureTooltip;
|
|
45
|
-
private renderTooltipWithConnector;
|
|
46
|
-
private renderTooltipWithoutConnector;
|
|
47
|
-
private showTooltipAt;
|
|
48
|
-
private showTooltipSelection;
|
|
49
|
-
private hideTooltipSelection;
|
|
50
|
-
private hideTooltipElement;
|
|
51
|
-
private setTooltipMarkup;
|
|
52
|
-
private appendTooltipConnector;
|
|
53
|
-
private appendTooltipArrow;
|
|
54
|
-
private appendTooltipArrowTriangle;
|
|
55
|
-
private resolveTooltipArrowPosition;
|
|
56
|
-
private resolveBarTooltipAnchor;
|
|
57
|
-
private resolvePointTooltipAnchor;
|
|
58
|
-
private getSplitTooltip;
|
|
59
|
-
private hideSplitTooltips;
|
|
60
|
-
private removeSplitTooltips;
|
|
61
|
-
private removeRootTooltip;
|
|
62
|
-
private resolveTooltipArrowEdge;
|
|
63
|
-
private resolveSidePlacementArrowEdge;
|
|
64
|
-
private resolveVerticalPlacementArrowEdge;
|
|
65
|
-
private resolveSharedTooltipTarget;
|
|
66
|
-
private resolveSplitTooltipTarget;
|
|
67
|
-
private getTooltipConnectorOffset;
|
|
68
|
-
private getAnchoredTooltipPosition;
|
|
69
|
-
private resolveTooltipConnectorLayout;
|
|
70
|
-
private resolveTooltipBoxArrowPosition;
|
|
71
|
-
private resolveTooltipConnectorPath;
|
|
72
|
-
private isTooltipArrowTipInsideHorizontalAnchorSpan;
|
|
73
|
-
private isTooltipArrowTipInsideVerticalAnchorSpan;
|
|
74
|
-
private resolveTooltipArrowTip;
|
|
75
|
-
private hasFiniteNumbers;
|
|
76
|
-
private resolveSplitTooltipCollisions;
|
|
77
|
-
private getOppositeSideArrowEdge;
|
|
78
|
-
private getOppositeVerticalArrowEdge;
|
|
79
|
-
private flipTooltipIfItReducesCollisions;
|
|
80
|
-
private countSplitTooltipCollisions;
|
|
81
|
-
private countPlacedLayoutsOnEdge;
|
|
82
|
-
private doSplitTooltipLayoutsOverlap;
|
|
83
|
-
private resolveSplitTooltipPositions;
|
|
84
|
-
private resolveHorizontalChartSplitTooltipPositions;
|
|
85
|
-
private resolveVerticalChartSplitTooltipPositions;
|
|
86
|
-
private getSplitTooltipViewportBounds;
|
|
87
|
-
private groupSplitTooltipLayoutsByEdge;
|
|
88
|
-
private resolveSideSplitTooltipPositions;
|
|
89
|
-
private resolveHorizontalSideSplitTooltipPositions;
|
|
90
|
-
private resolveHorizontalSideSplitTooltipCollisions;
|
|
91
|
-
private flipHorizontalSideTooltipIfItReducesCollisions;
|
|
92
|
-
private findReusableHorizontalTooltipLane;
|
|
93
|
-
private resolveNewHorizontalTooltipLaneTop;
|
|
94
|
-
private getHorizontalTooltipLaneTopCandidates;
|
|
95
|
-
private assignLayoutToHorizontalTooltipLane;
|
|
96
|
-
private doHorizontalTooltipLanesOverlap;
|
|
97
|
-
private doSplitTooltipLayoutsOverlapHorizontally;
|
|
98
|
-
private resolveHorizontalChartAboveBelowSplitTooltipPositions;
|
|
99
|
-
private resolvePackedAboveBelowTooltipRow;
|
|
100
|
-
private resolveVerticalChartAboveBelowSplitTooltipPositions;
|
|
101
|
-
private resolveCollisionAwareAboveBelowTooltipPositions;
|
|
102
|
-
private resolveNonOverlappingAboveBelowTooltipLeft;
|
|
103
|
-
private doesSplitTooltipOverlapPlacedLayouts;
|
|
104
35
|
}
|
|
105
|
-
export {};
|