@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.
@@ -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 { type Selection } from 'd3';
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 = Line | Bar | Area | Scatter;
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 splitTooltipOwner;
28
- private readonly tooltipStyleKeys;
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 {};