@internetstiftelsen/charts 0.15.0 → 0.17.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.
@@ -0,0 +1,791 @@
1
+ import { pointer, select } from 'd3';
2
+ import { getSeriesColor } from '../types.js';
3
+ import { getContrastTextColor, sanitizeForCSS } from '../utils.js';
4
+ import { clipTooltipAnchorToBounds, getAnchoredTooltipPosition, getSplitTooltipViewportBounds, 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, colorMode, formatter, labelFormatter, customFormatter, dom, } = config;
7
+ const tooltip = dom.getRootTooltip();
8
+ if (!tooltip || data.length === 0) {
9
+ return null;
10
+ }
11
+ const resolvedPosition = position === 'auto' ? (isHorizontal ? 'vertical' : 'side') : position;
12
+ const resolvedBarAnchorPosition = isHorizontal ? 'auto' : barAnchorPosition;
13
+ const resolveSeriesValue = config.resolveSeriesValue ??
14
+ ((targetSeries, dataPoint) => {
15
+ const rawValue = dataPoint[targetSeries.dataKey];
16
+ if (rawValue === null || rawValue === undefined) {
17
+ return NaN;
18
+ }
19
+ return parseValue(rawValue);
20
+ });
21
+ const getXPosition = (dataPoint) => {
22
+ const scaled = x(getCategoryScaleValue(dataPoint[xKey], categoryScaleType));
23
+ return (scaled || 0) + (x.bandwidth ? x.bandwidth() / 2 : 0);
24
+ };
25
+ const getYPosition = (dataPoint) => {
26
+ const scaled = y(getCategoryScaleValue(dataPoint[xKey], categoryScaleType));
27
+ return (scaled || 0) + (y.bandwidth ? y.bandwidth() / 2 : 0);
28
+ };
29
+ const getTooltipValueSign = (value) => {
30
+ if (value < 0) {
31
+ return -1;
32
+ }
33
+ if (value > 0) {
34
+ return 1;
35
+ }
36
+ return 0;
37
+ };
38
+ const buildTooltipLabel = (dataPoint) => {
39
+ const labelValue = dataPoint[xKey];
40
+ return labelFormatter
41
+ ? labelFormatter(String(labelValue), dataPoint)
42
+ : String(labelValue);
43
+ };
44
+ const buildTooltipRow = (dataPoint, currentSeries) => {
45
+ const value = dataPoint[currentSeries.dataKey];
46
+ if (formatter) {
47
+ return formatter(currentSeries.dataKey, normalizeFormatterValue(value), dataPoint);
48
+ }
49
+ return `${currentSeries.dataKey}: ${value}`;
50
+ };
51
+ const getVisibleTooltipSeries = (dataPoint) => {
52
+ return series.filter((currentSeries) => {
53
+ const value = dataPoint[currentSeries.dataKey];
54
+ return value !== null && value !== undefined;
55
+ });
56
+ };
57
+ const getSeriesTooltipStyle = (currentSeries, dataPoint, index) => {
58
+ if (colorMode !== 'series') {
59
+ return undefined;
60
+ }
61
+ const seriesColor = currentSeries.type === 'bar' && currentSeries.colorAdapter
62
+ ? currentSeries.colorAdapter(dataPoint, index)
63
+ : getSeriesColor(currentSeries);
64
+ return {
65
+ background: seriesColor,
66
+ border: seriesColor,
67
+ color: getContrastTextColor(seriesColor),
68
+ };
69
+ };
70
+ const getSharedTooltipStyle = (visibleSeries, dataPoint, index) => {
71
+ if (visibleSeries.length !== 1) {
72
+ return undefined;
73
+ }
74
+ return getSeriesTooltipStyle(visibleSeries[0], dataPoint, index);
75
+ };
76
+ const buildSharedTooltipContent = (dataPoint, visibleSeries) => {
77
+ if (visibleSeries.length === 0) {
78
+ return null;
79
+ }
80
+ if (customFormatter) {
81
+ return customFormatter(dataPoint, visibleSeries);
82
+ }
83
+ const label = buildTooltipLabel(dataPoint);
84
+ const rows = visibleSeries.map((currentSeries) => buildTooltipRow(dataPoint, currentSeries));
85
+ return `<strong>${label}</strong><br/>${rows.join('<br/>')}`;
86
+ };
87
+ const buildSplitTooltipContent = (dataPoint, currentSeries) => {
88
+ if (customFormatter) {
89
+ return customFormatter(dataPoint, [currentSeries]);
90
+ }
91
+ const label = buildTooltipLabel(dataPoint);
92
+ return `<strong>${label}</strong><br/>${buildTooltipRow(dataPoint, currentSeries)}`;
93
+ };
94
+ const buildAccessibleLabel = (dataPoint) => {
95
+ const content = buildSharedTooltipContent(dataPoint, getVisibleTooltipSeries(dataPoint));
96
+ return content ? stripHtml(content) : buildTooltipLabel(dataPoint);
97
+ };
98
+ const dataPointPositions = data.map((dataPoint) => isHorizontal ? getYPosition(dataPoint) : getXPosition(dataPoint));
99
+ const focusCircleSeries = series.filter((currentSeries) => {
100
+ if (currentSeries.type === 'line' &&
101
+ currentSeries.points.show === 'never') {
102
+ return false;
103
+ }
104
+ return (currentSeries.type === 'line' ||
105
+ currentSeries.type === 'area' ||
106
+ currentSeries.type === 'scatter');
107
+ });
108
+ const barSeries = series.filter((currentSeries) => {
109
+ return currentSeries.type === 'bar';
110
+ });
111
+ const hasBarSeries = barSeries.length > 0;
112
+ const overlay = svg
113
+ .append('rect')
114
+ .attr('class', 'tooltip-overlay')
115
+ .attr('x', plotArea.left)
116
+ .attr('y', plotArea.top)
117
+ .attr('width', plotArea.width)
118
+ .attr('height', plotArea.height)
119
+ .attr('aria-hidden', 'true')
120
+ .style('fill', 'none')
121
+ .style('pointer-events', 'all');
122
+ const focusCircles = focusCircleSeries.map((currentSeries) => {
123
+ const seriesColor = getSeriesColor(currentSeries);
124
+ return svg
125
+ .append('circle')
126
+ .attr('class', `focus-circle-${sanitizeForCSS(currentSeries.dataKey)}`)
127
+ .attr('r', theme.line.point.size + 1)
128
+ .attr('fill', theme.line.point.color || seriesColor)
129
+ .attr('stroke', theme.line.point.strokeColor || seriesColor)
130
+ .attr('stroke-width', theme.line.point.strokeWidth)
131
+ .attr('aria-hidden', 'true')
132
+ .style('opacity', 0)
133
+ .style('pointer-events', 'none');
134
+ });
135
+ const clearVisualState = () => {
136
+ focusCircles.forEach((circle) => circle.style('opacity', 0));
137
+ if (!hasBarSeries) {
138
+ return;
139
+ }
140
+ barSeries.forEach((currentSeries) => {
141
+ svg.selectAll(`.bar-${sanitizeForCSS(currentSeries.dataKey)}`).style('opacity', 1);
142
+ });
143
+ };
144
+ const updateVisualStateAtIndex = (closestIndex) => {
145
+ const dataPoint = data[closestIndex];
146
+ const dataPointPosition = dataPointPositions[closestIndex];
147
+ focusCircleSeries.forEach((currentSeries, seriesIndex) => {
148
+ const value = resolveSeriesValue(currentSeries, dataPoint, closestIndex);
149
+ if (!Number.isFinite(value)) {
150
+ focusCircles[seriesIndex].style('opacity', 0);
151
+ return;
152
+ }
153
+ if (isHorizontal) {
154
+ focusCircles[seriesIndex]
155
+ .attr('cx', x(value) ?? 0)
156
+ .attr('cy', dataPointPosition)
157
+ .style('opacity', 1);
158
+ return;
159
+ }
160
+ focusCircles[seriesIndex]
161
+ .attr('cx', dataPointPosition)
162
+ .attr('cy', y(value) ?? 0)
163
+ .style('opacity', 1);
164
+ });
165
+ if (!hasBarSeries) {
166
+ return;
167
+ }
168
+ barSeries.forEach((currentSeries) => {
169
+ svg.selectAll(`.bar-${sanitizeForCSS(currentSeries.dataKey)}`).style('opacity', (_, index) => index === closestIndex ? 1 : 0.5);
170
+ });
171
+ };
172
+ const showSharedTooltipAtIndex = (closestIndex) => {
173
+ const dataPoint = data[closestIndex];
174
+ const dataPointPosition = dataPointPositions[closestIndex];
175
+ const visibleSeries = getVisibleTooltipSeries(dataPoint);
176
+ updateVisualStateAtIndex(closestIndex);
177
+ dom.hideSplitTooltips();
178
+ dom.applyRootTooltipStyles(theme, getSharedTooltipStyle(visibleSeries, dataPoint, closestIndex));
179
+ const content = buildSharedTooltipContent(dataPoint, visibleSeries);
180
+ if (!content) {
181
+ dom.hideTooltipSelection(tooltip);
182
+ return;
183
+ }
184
+ const measuredTooltip = dom.measureTooltip(tooltip, content);
185
+ if (!measuredTooltip) {
186
+ dom.hideTooltipSelection(tooltip);
187
+ return;
188
+ }
189
+ const svgNode = svg.node();
190
+ if (!svgNode) {
191
+ dom.hideTooltipSelection(tooltip);
192
+ return;
193
+ }
194
+ const sharedAnchor = resolveSharedTooltipAnchor({
195
+ svgNode,
196
+ dataPoint,
197
+ dataPointPosition,
198
+ series,
199
+ x,
200
+ y,
201
+ isHorizontal,
202
+ resolveSeriesValue,
203
+ closestIndex,
204
+ });
205
+ const placementBounds = getTooltipPlacementBounds(svgNode);
206
+ const visibleAnchor = clipTooltipAnchorToBounds(sharedAnchor, placementBounds);
207
+ const target = resolveSharedTooltipTarget(visibleAnchor);
208
+ const arrowEdge = resolveTooltipArrowEdge(resolvedPosition, visibleAnchor, target, measuredTooltip.width, measuredTooltip.height, placementBounds);
209
+ const tooltipPosition = getAnchoredTooltipPosition(visibleAnchor, target, measuredTooltip.width, measuredTooltip.height, arrowEdge, placementBounds);
210
+ if (!tooltipPosition) {
211
+ dom.hideTooltipSelection(tooltip);
212
+ return;
213
+ }
214
+ dom.renderTooltipWithoutConnector(tooltip, tooltipPosition.left, tooltipPosition.top);
215
+ };
216
+ const getSeriesTooltipAnchor = (currentSeries, index, value) => {
217
+ if (!Number.isFinite(value)) {
218
+ return null;
219
+ }
220
+ const svgNode = svg.node();
221
+ if (!svgNode) {
222
+ return null;
223
+ }
224
+ if (currentSeries.type === 'bar') {
225
+ return resolveBarTooltipAnchor(svgNode, currentSeries.dataKey, index);
226
+ }
227
+ const categoryPosition = dataPointPositions[index];
228
+ const valuePosition = isHorizontal ? (x(value) ?? 0) : (y(value) ?? 0);
229
+ return resolvePointTooltipAnchor(svgNode, isHorizontal, categoryPosition, valuePosition);
230
+ };
231
+ const showSplitTooltipAtIndex = (closestIndex) => {
232
+ const dataPoint = data[closestIndex];
233
+ const svgNode = svg.node();
234
+ if (!svgNode) {
235
+ dom.hideSplitTooltips();
236
+ return;
237
+ }
238
+ const placementBounds = getTooltipPlacementBounds(svgNode);
239
+ updateVisualStateAtIndex(closestIndex);
240
+ dom.hideTooltipSelection(tooltip);
241
+ const layouts = [];
242
+ series.forEach((currentSeries, seriesIndex) => {
243
+ const rawValue = dataPoint[currentSeries.dataKey];
244
+ if (rawValue === null || rawValue === undefined) {
245
+ return;
246
+ }
247
+ const value = resolveSeriesValue(currentSeries, dataPoint, closestIndex);
248
+ const anchor = getSeriesTooltipAnchor(currentSeries, closestIndex, value);
249
+ if (!anchor) {
250
+ return;
251
+ }
252
+ const visibleAnchor = clipTooltipAnchorToBounds(anchor, placementBounds);
253
+ const splitTooltip = dom.getSplitTooltip(seriesIndex, theme, getSeriesTooltipStyle(currentSeries, dataPoint, closestIndex));
254
+ const content = buildSplitTooltipContent(dataPoint, currentSeries);
255
+ const measuredTooltip = dom.measureTooltip(splitTooltip, content);
256
+ if (!measuredTooltip) {
257
+ return;
258
+ }
259
+ const target = resolveSplitTooltipTarget(currentSeries, visibleAnchor, resolvedBarAnchorPosition);
260
+ const targetMode = currentSeries.type === 'bar' &&
261
+ resolvedBarAnchorPosition === 'auto'
262
+ ? 'auto'
263
+ : 'fixed';
264
+ layouts.push({
265
+ div: splitTooltip,
266
+ anchor: visibleAnchor,
267
+ width: measuredTooltip.width,
268
+ height: measuredTooltip.height,
269
+ left: 0,
270
+ top: 0,
271
+ arrowEdge: 'left',
272
+ targetX: target.x,
273
+ targetY: target.y,
274
+ order: seriesIndex,
275
+ targetMode,
276
+ valueSign: getTooltipValueSign(value),
277
+ });
278
+ });
279
+ if (layouts.length === 0) {
280
+ dom.hideSplitTooltips();
281
+ return;
282
+ }
283
+ resolveSplitTooltipPositions(layouts, resolvedPosition, placementBounds, isHorizontal);
284
+ dom.hideUnusedSplitTooltips(layouts.map((layout) => layout.div));
285
+ layouts.forEach((layout) => {
286
+ dom.renderTooltipWithConnector(layout.div, layout.arrowEdge, layout.left, layout.top, layout.width, layout.height, layout.targetX, layout.targetY, layout.anchor);
287
+ });
288
+ };
289
+ const showSingleTooltip = (request) => {
290
+ const dataPoint = data[request.index];
291
+ const svgNode = svg.node();
292
+ if (!svgNode) {
293
+ dom.hideTooltipSelection(tooltip);
294
+ return null;
295
+ }
296
+ const placementBounds = getTooltipPlacementBounds(svgNode);
297
+ const candidates = [];
298
+ series.forEach((currentSeries, seriesIndex) => {
299
+ const rawValue = dataPoint[currentSeries.dataKey];
300
+ if (rawValue === null || rawValue === undefined) {
301
+ return;
302
+ }
303
+ const value = resolveSeriesValue(currentSeries, dataPoint, request.index);
304
+ const anchor = getSeriesTooltipAnchor(currentSeries, request.index, value);
305
+ if (!anchor) {
306
+ return;
307
+ }
308
+ const visibleAnchor = clipTooltipAnchorToBounds(anchor, placementBounds);
309
+ const target = resolveSplitTooltipTarget(currentSeries, visibleAnchor, resolvedBarAnchorPosition);
310
+ candidates.push({
311
+ series: currentSeries,
312
+ seriesIndex,
313
+ anchor: visibleAnchor,
314
+ target,
315
+ });
316
+ });
317
+ const selectedCandidate = candidates.find((candidate) => candidate.seriesIndex === request.seriesIndex) ?? getClosestSingleTooltipCandidate(candidates, request);
318
+ updateVisualStateAtIndex(request.index);
319
+ dom.hideSplitTooltips();
320
+ if (!selectedCandidate) {
321
+ dom.hideTooltipSelection(tooltip);
322
+ return null;
323
+ }
324
+ dom.applyRootTooltipStyles(theme, getSeriesTooltipStyle(selectedCandidate.series, dataPoint, request.index));
325
+ const content = buildSplitTooltipContent(dataPoint, selectedCandidate.series);
326
+ const measuredTooltip = dom.measureTooltip(tooltip, content);
327
+ if (!measuredTooltip) {
328
+ dom.hideTooltipSelection(tooltip);
329
+ return null;
330
+ }
331
+ const arrowEdge = resolveTooltipArrowEdge(resolvedPosition, selectedCandidate.anchor, selectedCandidate.target, measuredTooltip.width, measuredTooltip.height, placementBounds);
332
+ const tooltipPosition = getAnchoredTooltipPosition(selectedCandidate.anchor, selectedCandidate.target, measuredTooltip.width, measuredTooltip.height, arrowEdge, placementBounds);
333
+ if (!tooltipPosition) {
334
+ dom.hideTooltipSelection(tooltip);
335
+ return null;
336
+ }
337
+ dom.renderTooltipWithConnector(tooltip, arrowEdge, tooltipPosition.left, tooltipPosition.top, measuredTooltip.width, measuredTooltip.height, selectedCandidate.target.x, selectedCandidate.target.y, selectedCandidate.anchor);
338
+ return {
339
+ index: request.index,
340
+ seriesIndex: selectedCandidate.seriesIndex,
341
+ };
342
+ };
343
+ let activeTooltipRequest = null;
344
+ const hideTooltip = () => {
345
+ activeTooltipRequest = null;
346
+ dom.hideTooltipSelection(tooltip);
347
+ dom.hideSplitTooltips();
348
+ clearVisualState();
349
+ };
350
+ const tooltipSvgNode = svg.node();
351
+ const queuedRender = createQueuedTooltipRender(tooltipSvgNode, (request) => {
352
+ if (mode === 'single') {
353
+ activeTooltipRequest = showSingleTooltip(request);
354
+ return;
355
+ }
356
+ activeTooltipRequest = { index: request.index };
357
+ if (mode === 'split') {
358
+ showSplitTooltipAtIndex(request.index);
359
+ return;
360
+ }
361
+ showSharedTooltipAtIndex(request.index);
362
+ });
363
+ overlay
364
+ .on('mousemove', (event) => {
365
+ const [mouseX, mouseY] = pointer(event, svg.node());
366
+ const closestIndex = getClosestIndexFromPointer(mouseX, mouseY, dataPointPositions, isHorizontal);
367
+ const pointerPosition = getDocumentPointerPosition(event);
368
+ queuedRender.request({
369
+ index: closestIndex,
370
+ pointerX: pointerPosition.x,
371
+ pointerY: pointerPosition.y,
372
+ });
373
+ })
374
+ .on('mouseout', () => {
375
+ if (isTooltipFocusTarget(document.activeElement)) {
376
+ return;
377
+ }
378
+ queuedRender.cancel();
379
+ hideTooltip();
380
+ });
381
+ const focusTargets = svg
382
+ .append('g')
383
+ .attr('class', 'tooltip-focus-targets')
384
+ .selectAll('rect')
385
+ .data(data)
386
+ .join('rect')
387
+ .attr('class', 'tooltip-focus-target')
388
+ .attr('data-index', (_, i) => i)
389
+ .attr('x', (_, i) => getFocusTargetBounds(i, dataPointPositions, plotArea, x, y, isHorizontal).x)
390
+ .attr('y', (_, i) => getFocusTargetBounds(i, dataPointPositions, plotArea, x, y, isHorizontal).y)
391
+ .attr('width', (_, i) => getFocusTargetBounds(i, dataPointPositions, plotArea, x, y, isHorizontal).width)
392
+ .attr('height', (_, i) => getFocusTargetBounds(i, dataPointPositions, plotArea, x, y, isHorizontal).height)
393
+ .attr('tabindex', 0)
394
+ .attr('fill', 'transparent')
395
+ .attr('stroke', 'none')
396
+ .attr('stroke-width', 0)
397
+ .attr('aria-label', (dataPoint) => buildAccessibleLabel(dataPoint))
398
+ .style('pointer-events', 'none');
399
+ const focusTargetNodes = focusTargets.nodes();
400
+ focusTargets
401
+ .on('focus', function () {
402
+ const currentIndex = focusTargetNodes.indexOf(this);
403
+ if (currentIndex === -1) {
404
+ return;
405
+ }
406
+ queuedRender.cancel();
407
+ select(this).attr('stroke', '#111827').attr('stroke-width', 2);
408
+ if (mode === 'single') {
409
+ activeTooltipRequest = showSingleTooltip({
410
+ index: currentIndex,
411
+ });
412
+ return;
413
+ }
414
+ activeTooltipRequest = { index: currentIndex };
415
+ if (mode === 'split') {
416
+ showSplitTooltipAtIndex(currentIndex);
417
+ return;
418
+ }
419
+ showSharedTooltipAtIndex(currentIndex);
420
+ })
421
+ .on('blur', function (event) {
422
+ select(this).attr('stroke', 'none').attr('stroke-width', 0);
423
+ if (isTooltipFocusTarget(event.relatedTarget)) {
424
+ return;
425
+ }
426
+ queuedRender.cancel();
427
+ hideTooltip();
428
+ })
429
+ .on('keydown', function (event) {
430
+ const currentIndex = focusTargetNodes.indexOf(this);
431
+ if (currentIndex === -1) {
432
+ return;
433
+ }
434
+ const nextIndex = getNextFocusTargetIndex(currentIndex, event.key, focusTargetNodes.length);
435
+ if (nextIndex < 0 || nextIndex >= focusTargetNodes.length) {
436
+ return;
437
+ }
438
+ event.preventDefault();
439
+ focusTargetNodes[nextIndex].focus();
440
+ });
441
+ return attachTooltipScrollListeners(tooltipSvgNode, () => {
442
+ if (activeTooltipRequest === null) {
443
+ return;
444
+ }
445
+ queuedRender.request(activeTooltipRequest);
446
+ });
447
+ }
448
+ function normalizeFormatterValue(value) {
449
+ if (value === null ||
450
+ value === undefined ||
451
+ typeof value === 'string' ||
452
+ typeof value === 'number' ||
453
+ typeof value === 'boolean' ||
454
+ value instanceof Date) {
455
+ return value;
456
+ }
457
+ return String(value);
458
+ }
459
+ function getCategoryScaleValue(value, scaleType) {
460
+ switch (scaleType) {
461
+ case 'band':
462
+ return String(value);
463
+ case 'time':
464
+ return value instanceof Date ? value : new Date(String(value));
465
+ case 'linear':
466
+ case 'log':
467
+ return typeof value === 'number' ? value : Number(value);
468
+ }
469
+ }
470
+ function stripHtml(content) {
471
+ return content
472
+ .replace(/<br\s*\/?>/gi, '. ')
473
+ .replace(/<[^>]+>/g, ' ')
474
+ .replace(/\s+/g, ' ')
475
+ .trim();
476
+ }
477
+ function isTooltipFocusTarget(element) {
478
+ return (element instanceof SVGElement &&
479
+ element.classList.contains('tooltip-focus-target'));
480
+ }
481
+ function getClosestIndexFromPointer(mouseX, mouseY, dataPointPositions, isHorizontal) {
482
+ const pointerPosition = isHorizontal ? mouseY : mouseX;
483
+ let closestIndex = 0;
484
+ let minDistance = Math.abs(pointerPosition - dataPointPositions[0]);
485
+ for (let i = 1; i < dataPointPositions.length; i++) {
486
+ const distance = Math.abs(pointerPosition - dataPointPositions[i]);
487
+ if (distance < minDistance) {
488
+ minDistance = distance;
489
+ closestIndex = i;
490
+ }
491
+ }
492
+ return closestIndex;
493
+ }
494
+ function getDocumentPointerPosition(event) {
495
+ return {
496
+ x: event.clientX + window.scrollX,
497
+ y: event.clientY + window.scrollY,
498
+ };
499
+ }
500
+ function getClosestSingleTooltipCandidate(candidates, request) {
501
+ if (candidates.length === 0) {
502
+ return null;
503
+ }
504
+ if (request.pointerX === undefined ||
505
+ request.pointerY === undefined ||
506
+ !Number.isFinite(request.pointerX) ||
507
+ !Number.isFinite(request.pointerY)) {
508
+ return candidates[0];
509
+ }
510
+ return candidates.reduce((closest, candidate) => {
511
+ const closestDistance = getSingleTooltipDistance(closest, request);
512
+ const candidateDistance = getSingleTooltipDistance(candidate, request);
513
+ return candidateDistance < closestDistance ? candidate : closest;
514
+ });
515
+ }
516
+ function getSingleTooltipDistance(candidate, request) {
517
+ const deltaX = (request.pointerX ?? 0) - candidate.target.x;
518
+ const deltaY = (request.pointerY ?? 0) - candidate.target.y;
519
+ return Math.hypot(deltaX, deltaY);
520
+ }
521
+ function resolveSharedTooltipAnchor({ svgNode, dataPoint, dataPointPosition, series, x, y, isHorizontal, resolveSeriesValue, closestIndex, }) {
522
+ const svgRect = svgNode.getBoundingClientRect();
523
+ const values = series.map((currentSeries) => resolveSeriesValue(currentSeries, dataPoint, closestIndex));
524
+ const finiteValues = values.filter((value) => Number.isFinite(value));
525
+ const minValue = finiteValues.length ? Math.min(...finiteValues) : 0;
526
+ const maxValue = finiteValues.length ? Math.max(...finiteValues) : 0;
527
+ if (isHorizontal) {
528
+ const minX = x(minValue);
529
+ const maxX = x(maxValue);
530
+ const centerX = svgRect.left + window.scrollX + (minX + maxX) / 2;
531
+ const centerY = svgRect.top + window.scrollY + dataPointPosition;
532
+ return {
533
+ left: centerX,
534
+ right: centerX,
535
+ top: centerY,
536
+ bottom: centerY,
537
+ centerX,
538
+ centerY,
539
+ };
540
+ }
541
+ const minY = y(maxValue);
542
+ const maxY = y(minValue);
543
+ const centerX = svgRect.left + window.scrollX + dataPointPosition;
544
+ const topY = svgRect.top + window.scrollY + minY;
545
+ const bottomY = svgRect.top + window.scrollY + maxY;
546
+ return {
547
+ left: centerX,
548
+ right: centerX,
549
+ top: topY,
550
+ bottom: bottomY,
551
+ centerX,
552
+ centerY: (topY + bottomY) / 2,
553
+ };
554
+ }
555
+ function resolveBarTooltipAnchor(svgNode, dataKey, index) {
556
+ const barNode = svgNode.querySelector(`.bar-${sanitizeForCSS(dataKey)}[data-index="${index}"]`);
557
+ if (!barNode) {
558
+ return null;
559
+ }
560
+ const rect = barNode.getBoundingClientRect();
561
+ if (rect.width > 0 || rect.height > 0) {
562
+ return {
563
+ left: rect.left + window.scrollX,
564
+ right: rect.right + window.scrollX,
565
+ top: rect.top + window.scrollY,
566
+ bottom: rect.bottom + window.scrollY,
567
+ centerX: rect.left + window.scrollX + rect.width / 2,
568
+ centerY: rect.top + window.scrollY + rect.height / 2,
569
+ };
570
+ }
571
+ const svgRect = svgNode.getBoundingClientRect();
572
+ const xValue = Number(barNode.getAttribute('x') ?? '0');
573
+ const yValue = Number(barNode.getAttribute('y') ?? '0');
574
+ const widthValue = Number(barNode.getAttribute('width') ?? '0');
575
+ const heightValue = Number(barNode.getAttribute('height') ?? '0');
576
+ const left = svgRect.left + window.scrollX + xValue;
577
+ const top = svgRect.top + window.scrollY + yValue;
578
+ const right = left + widthValue;
579
+ const bottom = top + heightValue;
580
+ return {
581
+ left,
582
+ right,
583
+ top,
584
+ bottom,
585
+ centerX: left + widthValue / 2,
586
+ centerY: top + heightValue / 2,
587
+ };
588
+ }
589
+ function resolvePointTooltipAnchor(svgNode, isHorizontal, categoryPosition, valuePosition) {
590
+ if (!Number.isFinite(categoryPosition) || !Number.isFinite(valuePosition)) {
591
+ return null;
592
+ }
593
+ const svgRect = svgNode.getBoundingClientRect();
594
+ const anchorX = svgRect.left +
595
+ window.scrollX +
596
+ (isHorizontal ? valuePosition : categoryPosition);
597
+ const anchorY = svgRect.top +
598
+ window.scrollY +
599
+ (isHorizontal ? categoryPosition : valuePosition);
600
+ if (!Number.isFinite(anchorX) || !Number.isFinite(anchorY)) {
601
+ return null;
602
+ }
603
+ return {
604
+ left: anchorX,
605
+ right: anchorX,
606
+ top: anchorY,
607
+ bottom: anchorY,
608
+ centerX: anchorX,
609
+ centerY: anchorY,
610
+ };
611
+ }
612
+ function createQueuedTooltipRender(tooltipSvgNode, renderTooltip) {
613
+ let pendingTooltipFrame = null;
614
+ let pendingTooltipRequest = null;
615
+ const request = (nextRequest) => {
616
+ pendingTooltipRequest = nextRequest;
617
+ if (pendingTooltipFrame !== null) {
618
+ return;
619
+ }
620
+ pendingTooltipFrame = requestFrame(() => {
621
+ const currentRequest = pendingTooltipRequest;
622
+ pendingTooltipFrame = null;
623
+ pendingTooltipRequest = null;
624
+ if (currentRequest === null) {
625
+ return;
626
+ }
627
+ if (!tooltipSvgNode?.isConnected) {
628
+ return;
629
+ }
630
+ renderTooltip(currentRequest);
631
+ });
632
+ };
633
+ const cancel = () => {
634
+ if (pendingTooltipFrame === null) {
635
+ return;
636
+ }
637
+ cancelFrame(pendingTooltipFrame);
638
+ pendingTooltipFrame = null;
639
+ pendingTooltipRequest = null;
640
+ };
641
+ return { request, cancel };
642
+ }
643
+ function requestFrame(callback) {
644
+ if (typeof window.requestAnimationFrame === 'function') {
645
+ return window.requestAnimationFrame(callback);
646
+ }
647
+ return window.setTimeout(() => {
648
+ callback(window.performance.now());
649
+ }, 16);
650
+ }
651
+ function cancelFrame(frameId) {
652
+ if (typeof window.cancelAnimationFrame === 'function') {
653
+ window.cancelAnimationFrame(frameId);
654
+ return;
655
+ }
656
+ window.clearTimeout(frameId);
657
+ }
658
+ function getFocusTargetBounds(index, dataPointPositions, plotArea, x, y, isHorizontal) {
659
+ if (isHorizontal) {
660
+ if (y.bandwidth) {
661
+ const targetHeight = y.bandwidth();
662
+ return {
663
+ x: plotArea.left,
664
+ y: dataPointPositions[index] - targetHeight / 2,
665
+ width: plotArea.width,
666
+ height: targetHeight,
667
+ };
668
+ }
669
+ const top = index === 0
670
+ ? plotArea.top
671
+ : (dataPointPositions[index - 1] + dataPointPositions[index]) /
672
+ 2;
673
+ const bottom = index === dataPointPositions.length - 1
674
+ ? plotArea.bottom
675
+ : (dataPointPositions[index] + dataPointPositions[index + 1]) /
676
+ 2;
677
+ return {
678
+ x: plotArea.left,
679
+ y: top,
680
+ width: plotArea.width,
681
+ height: bottom - top,
682
+ };
683
+ }
684
+ if (x.bandwidth) {
685
+ const targetWidth = x.bandwidth();
686
+ return {
687
+ x: dataPointPositions[index] - targetWidth / 2,
688
+ y: plotArea.top,
689
+ width: targetWidth,
690
+ height: plotArea.height,
691
+ };
692
+ }
693
+ const left = index === 0
694
+ ? plotArea.left
695
+ : (dataPointPositions[index - 1] + dataPointPositions[index]) / 2;
696
+ const right = index === dataPointPositions.length - 1
697
+ ? plotArea.right
698
+ : (dataPointPositions[index] + dataPointPositions[index + 1]) / 2;
699
+ return {
700
+ x: left,
701
+ y: plotArea.top,
702
+ width: right - left,
703
+ height: plotArea.height,
704
+ };
705
+ }
706
+ function getNextFocusTargetIndex(currentIndex, key, focusTargetCount) {
707
+ switch (key) {
708
+ case 'ArrowRight':
709
+ case 'ArrowDown':
710
+ return currentIndex + 1;
711
+ case 'ArrowLeft':
712
+ case 'ArrowUp':
713
+ return currentIndex - 1;
714
+ case 'Home':
715
+ return 0;
716
+ case 'End':
717
+ return focusTargetCount - 1;
718
+ default:
719
+ return -1;
720
+ }
721
+ }
722
+ function getTooltipPlacementBounds(svgNode) {
723
+ let bounds = getSplitTooltipViewportBounds();
724
+ let currentElement = svgNode.parentElement;
725
+ while (currentElement) {
726
+ if (isScrollableElement(currentElement)) {
727
+ bounds = intersectTooltipBounds(bounds, getElementTooltipBounds(currentElement));
728
+ }
729
+ currentElement = currentElement.parentElement;
730
+ }
731
+ return bounds;
732
+ }
733
+ function getElementTooltipBounds(element) {
734
+ const rect = element.getBoundingClientRect();
735
+ const left = rect.left + window.scrollX;
736
+ const top = rect.top + window.scrollY;
737
+ return {
738
+ minLeft: left,
739
+ maxRight: left + rect.width,
740
+ minTop: top,
741
+ maxBottom: top + rect.height,
742
+ };
743
+ }
744
+ function intersectTooltipBounds(bounds, nextBounds) {
745
+ if (nextBounds.maxRight <= nextBounds.minLeft ||
746
+ nextBounds.maxBottom <= nextBounds.minTop) {
747
+ return bounds;
748
+ }
749
+ const minLeft = Math.max(bounds.minLeft, nextBounds.minLeft);
750
+ const maxRight = Math.min(bounds.maxRight, nextBounds.maxRight);
751
+ const minTop = Math.max(bounds.minTop, nextBounds.minTop);
752
+ const maxBottom = Math.min(bounds.maxBottom, nextBounds.maxBottom);
753
+ if (maxRight <= minLeft || maxBottom <= minTop) {
754
+ return bounds;
755
+ }
756
+ return {
757
+ minLeft,
758
+ maxRight,
759
+ minTop,
760
+ maxBottom,
761
+ };
762
+ }
763
+ function attachTooltipScrollListeners(svgNode, onScroll) {
764
+ if (!svgNode) {
765
+ return null;
766
+ }
767
+ const scrollTargets = getTooltipScrollTargets(svgNode);
768
+ scrollTargets.forEach((target) => {
769
+ target.addEventListener('scroll', onScroll, { passive: true });
770
+ });
771
+ return () => {
772
+ scrollTargets.forEach((target) => {
773
+ target.removeEventListener('scroll', onScroll);
774
+ });
775
+ };
776
+ }
777
+ function getTooltipScrollTargets(svgNode) {
778
+ const scrollTargets = new Set([window]);
779
+ let currentElement = svgNode.parentElement;
780
+ while (currentElement) {
781
+ if (isScrollableElement(currentElement)) {
782
+ scrollTargets.add(currentElement);
783
+ }
784
+ currentElement = currentElement.parentElement;
785
+ }
786
+ return [...scrollTargets];
787
+ }
788
+ function isScrollableElement(element) {
789
+ const style = window.getComputedStyle(element);
790
+ return [style.overflow, style.overflowX, style.overflowY].some((value) => value === 'auto' || value === 'scroll' || value === 'overlay');
791
+ }