@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/dist/tooltip.js CHANGED
@@ -1,29 +1,7 @@
1
- import { pointer, select } from 'd3';
2
- import { getSeriesColor } from './types.js';
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_BORDER_WIDTH_PX = 1;
10
- const TOOLTIP_BOX_ARROW_LENGTH_PX = 10;
11
- const TOOLTIP_BOX_ARROW_HALF_HEIGHT_PX = 6;
12
- const TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX = 1;
13
- const TOOLTIP_CONNECTOR_Z_INDEX = 3;
14
- const TOOLTIP_ARROW_BORDER_Z_INDEX = 4;
15
- const TOOLTIP_ARROW_FILL_Z_INDEX = 5;
16
- const TOOLTIP_BODY_Z_INDEX = 6;
17
- const TOOLTIP_TOTAL_BORDER_WIDTH_PX = TOOLTIP_BORDER_WIDTH_PX * 2;
18
- const SPLIT_TOOLTIP_GAP_PX = 8;
19
- const DEFAULT_TOOLTIP_MAX_WIDTH_PX = 280;
20
- const DEFAULT_TOOLTIP_TRANSITION = {
21
- show: false,
22
- duration: 120,
23
- easing: 'ease-out',
24
- };
25
- const TOOLTIP_HIDDEN_TRANSFORM = 'translateY(2px)';
26
- const TOOLTIP_VISIBLE_TRANSFORM = 'translateY(0)';
1
+ import { mergeDeep } from './utils.js';
2
+ import { TooltipDom } from './tooltip/dom.js';
3
+ import { attachXYTooltipArea } from './tooltip/xy-interaction.js';
4
+ import { DEFAULT_TOOLTIP_MAX_WIDTH_PX, DEFAULT_TOOLTIP_TRANSITION, } from './tooltip/types.js';
27
5
  export class Tooltip {
28
6
  constructor(config = {}) {
29
7
  Object.defineProperty(this, "id", {
@@ -92,25 +70,13 @@ export class Tooltip {
92
70
  writable: true,
93
71
  value: void 0
94
72
  });
95
- Object.defineProperty(this, "splitTooltipOwner", {
73
+ Object.defineProperty(this, "dom", {
96
74
  enumerable: true,
97
75
  configurable: true,
98
76
  writable: true,
99
77
  value: void 0
100
78
  });
101
- Object.defineProperty(this, "tooltipStyleKeys", {
102
- enumerable: true,
103
- configurable: true,
104
- writable: true,
105
- value: new WeakMap()
106
- });
107
- Object.defineProperty(this, "tooltipDiv", {
108
- enumerable: true,
109
- configurable: true,
110
- writable: true,
111
- value: null
112
- });
113
- Object.defineProperty(this, "tooltipTheme", {
79
+ Object.defineProperty(this, "detachTooltipScrollListeners", {
114
80
  enumerable: true,
115
81
  configurable: true,
116
82
  writable: true,
@@ -119,7 +85,6 @@ export class Tooltip {
119
85
  const { mode = 'split', position = 'side', barAnchorPosition = 'middle', maxWidth, transition, formatter, labelFormatter, customFormatter, exportHooks, } = config;
120
86
  const tooltipId = Tooltip.nextTooltipId++;
121
87
  this.id = `iisChartTooltip-${tooltipId}`;
122
- this.splitTooltipOwner = `${this.id}-split`;
123
88
  this.mode = mode;
124
89
  this.position = position;
125
90
  this.barAnchorPosition = barAnchorPosition;
@@ -135,6 +100,12 @@ export class Tooltip {
135
100
  this.labelFormatter = labelFormatter;
136
101
  this.customFormatter = customFormatter;
137
102
  this.exportHooks = exportHooks;
103
+ this.dom = new TooltipDom({
104
+ id: this.id,
105
+ splitTooltipOwner: `${this.id}-split`,
106
+ maxWidth: this.maxWidth,
107
+ transition: this.transition,
108
+ });
138
109
  }
139
110
  getExportConfig() {
140
111
  return {
@@ -156,17 +127,7 @@ export class Tooltip {
156
127
  });
157
128
  }
158
129
  initialize(theme) {
159
- const existingTooltip = select(`#${this.id}`);
160
- const tooltip = existingTooltip.empty()
161
- ? select('body')
162
- .append('div')
163
- .attr('class', 'chart-tooltip')
164
- .attr('id', this.id)
165
- : existingTooltip;
166
- this.removeSplitTooltips();
167
- this.applyTooltipStylesIfNeeded(tooltip, theme);
168
- this.tooltipDiv = tooltip;
169
- this.hideTooltipSelection(tooltip);
130
+ this.dom.initialize(theme);
170
131
  }
171
132
  attachToArea(svg, data, series, xKey, x, y, theme, plotArea, parseValue, isHorizontal = false, categoryScaleType = 'band', resolveSeriesValue = (targetSeries, dataPoint) => {
172
133
  const rawValue = dataPoint[targetSeries.dataKey];
@@ -175,1409 +136,45 @@ export class Tooltip {
175
136
  }
176
137
  return parseValue(rawValue);
177
138
  }) {
178
- if (!this.tooltipDiv || data.length === 0) {
179
- return;
180
- }
181
- const tooltip = this.tooltipDiv;
182
- const formatter = this.formatter;
183
- const labelFormatter = this.labelFormatter;
184
- const customFormatter = this.customFormatter;
185
- const tooltipMode = this.mode;
186
- const normalizeFormatterValue = (value) => {
187
- if (value === null ||
188
- value === undefined ||
189
- typeof value === 'string' ||
190
- typeof value === 'number' ||
191
- typeof value === 'boolean' ||
192
- value instanceof Date) {
193
- return value;
194
- }
195
- return String(value);
196
- };
197
- const getCategoryScaleValue = (value, scaleType) => {
198
- switch (scaleType) {
199
- case 'band':
200
- return String(value);
201
- case 'time':
202
- return value instanceof Date
203
- ? value
204
- : new Date(String(value));
205
- case 'linear':
206
- case 'log':
207
- return typeof value === 'number' ? value : Number(value);
208
- }
209
- };
210
- const getXPosition = (dataPoint) => {
211
- const scaled = x(getCategoryScaleValue(dataPoint[xKey], categoryScaleType));
212
- return (scaled || 0) + (x.bandwidth ? x.bandwidth() / 2 : 0);
213
- };
214
- const getYPosition = (dataPoint) => {
215
- const scaled = y(getCategoryScaleValue(dataPoint[xKey], categoryScaleType));
216
- return (scaled || 0) + (y.bandwidth ? y.bandwidth() / 2 : 0);
217
- };
218
- const stripHtml = (content) => {
219
- return content
220
- .replace(/<br\s*\/?>/gi, '. ')
221
- .replace(/<[^>]+>/g, ' ')
222
- .replace(/\s+/g, ' ')
223
- .trim();
224
- };
225
- const buildTooltipLabel = (dataPoint) => {
226
- const labelValue = dataPoint[xKey];
227
- return labelFormatter
228
- ? labelFormatter(String(labelValue), dataPoint)
229
- : String(labelValue);
230
- };
231
- const buildTooltipRow = (dataPoint, currentSeries) => {
232
- const value = dataPoint[currentSeries.dataKey];
233
- if (formatter) {
234
- return formatter(currentSeries.dataKey, normalizeFormatterValue(value), dataPoint);
235
- }
236
- return `${currentSeries.dataKey}: ${value}`;
237
- };
238
- const buildSharedTooltipContent = (dataPoint) => {
239
- if (customFormatter) {
240
- return customFormatter(dataPoint, series);
241
- }
242
- const label = buildTooltipLabel(dataPoint);
243
- const rows = series.map((currentSeries) => buildTooltipRow(dataPoint, currentSeries));
244
- return `<strong>${label}</strong><br/>${rows.join('<br/>')}`;
245
- };
246
- const buildSplitTooltipContent = (dataPoint, currentSeries) => {
247
- if (customFormatter) {
248
- return customFormatter(dataPoint, [currentSeries]);
249
- }
250
- const label = buildTooltipLabel(dataPoint);
251
- return `<strong>${label}</strong><br/>${buildTooltipRow(dataPoint, currentSeries)}`;
252
- };
253
- const buildAccessibleLabel = (dataPoint) => {
254
- return stripHtml(buildSharedTooltipContent(dataPoint));
255
- };
256
- const isTooltipFocusTarget = (element) => {
257
- return (element instanceof SVGElement &&
258
- element.classList.contains('tooltip-focus-target'));
259
- };
260
- const dataPointPositions = data.map((dataPoint) => isHorizontal ? getYPosition(dataPoint) : getXPosition(dataPoint));
261
- const getClosestIndexFromPointer = (mouseX, mouseY) => {
262
- const pointerPosition = isHorizontal ? mouseY : mouseX;
263
- let closestIndex = 0;
264
- let minDistance = Math.abs(pointerPosition - dataPointPositions[0]);
265
- for (let i = 1; i < dataPointPositions.length; i++) {
266
- const distance = Math.abs(pointerPosition - dataPointPositions[i]);
267
- if (distance < minDistance) {
268
- minDistance = distance;
269
- closestIndex = i;
270
- }
271
- }
272
- return closestIndex;
273
- };
274
- const overlay = svg
275
- .append('rect')
276
- .attr('class', 'tooltip-overlay')
277
- .attr('x', plotArea.left)
278
- .attr('y', plotArea.top)
279
- .attr('width', plotArea.width)
280
- .attr('height', plotArea.height)
281
- .attr('aria-hidden', 'true')
282
- .style('fill', 'none')
283
- .style('pointer-events', 'all');
284
- const focusCircleSeries = series.filter((currentSeries) => {
285
- if (currentSeries.type === 'line' &&
286
- currentSeries.points.show === 'never') {
287
- return false;
288
- }
289
- return (currentSeries.type === 'line' ||
290
- currentSeries.type === 'area' ||
291
- currentSeries.type === 'scatter');
292
- });
293
- const barSeries = series.filter((currentSeries) => {
294
- return currentSeries.type === 'bar';
295
- });
296
- const hasBarSeries = barSeries.length > 0;
297
- const focusCircles = focusCircleSeries.map((currentSeries) => {
298
- const seriesColor = getSeriesColor(currentSeries);
299
- return svg
300
- .append('circle')
301
- .attr('class', `focus-circle-${sanitizeForCSS(currentSeries.dataKey)}`)
302
- .attr('r', theme.line.point.size + 1)
303
- .attr('fill', theme.line.point.color || seriesColor)
304
- .attr('stroke', theme.line.point.strokeColor || seriesColor)
305
- .attr('stroke-width', theme.line.point.strokeWidth)
306
- .attr('aria-hidden', 'true')
307
- .style('opacity', 0)
308
- .style('pointer-events', 'none');
309
- });
310
- const clearVisualState = () => {
311
- focusCircles.forEach((circle) => circle.style('opacity', 0));
312
- if (!hasBarSeries) {
313
- return;
314
- }
315
- barSeries.forEach((currentSeries) => {
316
- svg.selectAll(`.bar-${sanitizeForCSS(currentSeries.dataKey)}`).style('opacity', 1);
317
- });
318
- };
319
- const updateVisualStateAtIndex = (closestIndex) => {
320
- const dataPoint = data[closestIndex];
321
- const dataPointPosition = dataPointPositions[closestIndex];
322
- focusCircleSeries.forEach((currentSeries, seriesIndex) => {
323
- const value = resolveSeriesValue(currentSeries, dataPoint, closestIndex);
324
- if (!Number.isFinite(value)) {
325
- focusCircles[seriesIndex].style('opacity', 0);
326
- return;
327
- }
328
- if (isHorizontal) {
329
- focusCircles[seriesIndex]
330
- .attr('cx', x(value) ?? 0)
331
- .attr('cy', dataPointPosition)
332
- .style('opacity', 1);
333
- return;
334
- }
335
- focusCircles[seriesIndex]
336
- .attr('cx', dataPointPosition)
337
- .attr('cy', y(value) ?? 0)
338
- .style('opacity', 1);
339
- });
340
- if (!hasBarSeries) {
341
- return;
342
- }
343
- barSeries.forEach((currentSeries) => {
344
- svg.selectAll(`.bar-${sanitizeForCSS(currentSeries.dataKey)}`).style('opacity', (_, index) => index === closestIndex ? 1 : 0.5);
345
- });
346
- };
347
- const showSharedTooltipAtIndex = (closestIndex) => {
348
- const dataPoint = data[closestIndex];
349
- const dataPointPosition = dataPointPositions[closestIndex];
350
- updateVisualStateAtIndex(closestIndex);
351
- this.hideSplitTooltips();
352
- const content = buildSharedTooltipContent(dataPoint);
353
- const measuredTooltip = this.measureTooltip(tooltip, content);
354
- if (!measuredTooltip) {
355
- this.hideTooltipSelection(tooltip);
356
- return;
357
- }
358
- const svgRect = svg.node().getBoundingClientRect();
359
- const values = series.map((currentSeries) => resolveSeriesValue(currentSeries, dataPoint, closestIndex));
360
- const finiteValues = values.filter((value) => Number.isFinite(value));
361
- const minValue = finiteValues.length
362
- ? Math.min(...finiteValues)
363
- : 0;
364
- const maxValue = finiteValues.length
365
- ? Math.max(...finiteValues)
366
- : 0;
367
- const sharedAnchor = {
368
- left: 0,
369
- right: 0,
370
- top: 0,
371
- bottom: 0,
372
- centerX: 0,
373
- centerY: 0,
374
- };
375
- if (isHorizontal) {
376
- const minX = x(minValue);
377
- const maxX = x(maxValue);
378
- const centerX = svgRect.left + window.scrollX + (minX + maxX) / 2;
379
- const centerY = svgRect.top + window.scrollY + dataPointPosition;
380
- sharedAnchor.left = centerX;
381
- sharedAnchor.right = centerX;
382
- sharedAnchor.top = centerY;
383
- sharedAnchor.bottom = centerY;
384
- sharedAnchor.centerX = centerX;
385
- sharedAnchor.centerY = centerY;
386
- }
387
- else {
388
- const minY = y(maxValue);
389
- const maxY = y(minValue);
390
- const centerX = svgRect.left + window.scrollX + dataPointPosition;
391
- const topY = svgRect.top + window.scrollY + minY;
392
- const bottomY = svgRect.top + window.scrollY + maxY;
393
- sharedAnchor.left = centerX;
394
- sharedAnchor.right = centerX;
395
- sharedAnchor.top = topY;
396
- sharedAnchor.bottom = bottomY;
397
- sharedAnchor.centerX = centerX;
398
- sharedAnchor.centerY = (topY + bottomY) / 2;
399
- }
400
- const target = this.resolveSharedTooltipTarget(sharedAnchor);
401
- const arrowEdge = this.resolveTooltipArrowEdge(sharedAnchor, target, measuredTooltip.width, measuredTooltip.height);
402
- const position = this.getAnchoredTooltipPosition(sharedAnchor, target, measuredTooltip.width, measuredTooltip.height, arrowEdge);
403
- if (!position) {
404
- this.hideTooltipSelection(tooltip);
405
- return;
406
- }
407
- this.renderTooltipWithoutConnector(tooltip, position.left, position.top);
408
- };
409
- const getSeriesTooltipAnchor = (currentSeries, dataPoint, index) => {
410
- const value = resolveSeriesValue(currentSeries, dataPoint, index);
411
- if (!Number.isFinite(value)) {
412
- return null;
413
- }
414
- const svgNode = svg.node();
415
- if (!svgNode) {
416
- return null;
417
- }
418
- if (currentSeries.type === 'bar') {
419
- return this.resolveBarTooltipAnchor(svgNode, currentSeries.dataKey, index);
420
- }
421
- const categoryPosition = dataPointPositions[index];
422
- const valuePosition = isHorizontal
423
- ? (x(value) ?? 0)
424
- : (y(value) ?? 0);
425
- return this.resolvePointTooltipAnchor(svgNode, isHorizontal, categoryPosition, valuePosition);
426
- };
427
- const showSplitTooltipAtIndex = (closestIndex) => {
428
- const dataPoint = data[closestIndex];
429
- updateVisualStateAtIndex(closestIndex);
430
- this.hideTooltipSelection(tooltip);
431
- this.hideSplitTooltips();
432
- const layouts = [];
433
- series.forEach((currentSeries, seriesIndex) => {
434
- const rawValue = dataPoint[currentSeries.dataKey];
435
- if (rawValue === null || rawValue === undefined) {
436
- return;
437
- }
438
- const anchor = getSeriesTooltipAnchor(currentSeries, dataPoint, closestIndex);
439
- if (!anchor) {
440
- return;
441
- }
442
- const splitTooltip = this.getSplitTooltip(seriesIndex, theme);
443
- const content = buildSplitTooltipContent(dataPoint, currentSeries);
444
- const measuredTooltip = this.measureTooltip(splitTooltip, content);
445
- if (!measuredTooltip) {
446
- return;
447
- }
448
- const target = this.resolveSplitTooltipTarget(currentSeries, anchor);
449
- const arrowEdge = this.resolveTooltipArrowEdge(anchor, target, measuredTooltip.width, measuredTooltip.height);
450
- const position = this.getAnchoredTooltipPosition(anchor, target, measuredTooltip.width, measuredTooltip.height, arrowEdge);
451
- if (!position) {
452
- return;
453
- }
454
- layouts.push({
455
- div: splitTooltip,
456
- anchor,
457
- width: measuredTooltip.width,
458
- height: measuredTooltip.height,
459
- left: position.left,
460
- top: position.top,
461
- arrowEdge,
462
- targetX: target.x,
463
- targetY: target.y,
464
- });
465
- });
466
- if (layouts.length === 0) {
467
- return;
468
- }
469
- this.resolveSplitTooltipPositions(layouts, isHorizontal);
470
- layouts.forEach((layout) => {
471
- this.renderTooltipWithConnector(layout.div, layout.arrowEdge, layout.left, layout.top, layout.width, layout.height, layout.targetX, layout.targetY, layout.anchor);
472
- });
473
- };
474
- const hideTooltip = () => {
475
- this.hideTooltipSelection(tooltip);
476
- this.hideSplitTooltips();
477
- clearVisualState();
478
- };
479
- let pendingTooltipFrame = null;
480
- let pendingTooltipIndex = null;
481
- const tooltipSvgNode = svg.node();
482
- const requestFrame = (callback) => {
483
- if (typeof window.requestAnimationFrame === 'function') {
484
- return window.requestAnimationFrame(callback);
485
- }
486
- return window.setTimeout(() => {
487
- callback(window.performance.now());
488
- }, 16);
489
- };
490
- const cancelFrame = (frameId) => {
491
- if (typeof window.cancelAnimationFrame === 'function') {
492
- window.cancelAnimationFrame(frameId);
493
- return;
494
- }
495
- window.clearTimeout(frameId);
496
- };
497
- const renderTooltipAtIndex = (index) => {
498
- if (tooltipMode === 'split') {
499
- showSplitTooltipAtIndex(index);
500
- return;
501
- }
502
- showSharedTooltipAtIndex(index);
503
- };
504
- const cancelPendingTooltipRender = () => {
505
- if (pendingTooltipFrame === null) {
506
- return;
507
- }
508
- cancelFrame(pendingTooltipFrame);
509
- pendingTooltipFrame = null;
510
- pendingTooltipIndex = null;
511
- };
512
- const requestTooltipRenderAtIndex = (index) => {
513
- pendingTooltipIndex = index;
514
- if (pendingTooltipFrame !== null) {
515
- return;
516
- }
517
- pendingTooltipFrame = requestFrame(() => {
518
- const nextIndex = pendingTooltipIndex;
519
- pendingTooltipFrame = null;
520
- pendingTooltipIndex = null;
521
- if (nextIndex === null) {
522
- return;
523
- }
524
- if (!tooltipSvgNode?.isConnected) {
525
- return;
526
- }
527
- renderTooltipAtIndex(nextIndex);
528
- });
529
- };
530
- const getFocusTargetBounds = (index) => {
531
- if (isHorizontal) {
532
- if (y.bandwidth) {
533
- const targetHeight = y.bandwidth();
534
- return {
535
- x: plotArea.left,
536
- y: dataPointPositions[index] - targetHeight / 2,
537
- width: plotArea.width,
538
- height: targetHeight,
539
- };
540
- }
541
- const top = index === 0
542
- ? plotArea.top
543
- : (dataPointPositions[index - 1] +
544
- dataPointPositions[index]) /
545
- 2;
546
- const bottom = index === dataPointPositions.length - 1
547
- ? plotArea.bottom
548
- : (dataPointPositions[index] +
549
- dataPointPositions[index + 1]) /
550
- 2;
551
- return {
552
- x: plotArea.left,
553
- y: top,
554
- width: plotArea.width,
555
- height: bottom - top,
556
- };
557
- }
558
- if (x.bandwidth) {
559
- const targetWidth = x.bandwidth();
560
- return {
561
- x: dataPointPositions[index] - targetWidth / 2,
562
- y: plotArea.top,
563
- width: targetWidth,
564
- height: plotArea.height,
565
- };
566
- }
567
- const left = index === 0
568
- ? plotArea.left
569
- : (dataPointPositions[index - 1] +
570
- dataPointPositions[index]) /
571
- 2;
572
- const right = index === dataPointPositions.length - 1
573
- ? plotArea.right
574
- : (dataPointPositions[index] +
575
- dataPointPositions[index + 1]) /
576
- 2;
577
- return {
578
- x: left,
579
- y: plotArea.top,
580
- width: right - left,
581
- height: plotArea.height,
582
- };
583
- };
584
- overlay
585
- .on('mousemove', (event) => {
586
- const [mouseX, mouseY] = pointer(event, svg.node());
587
- const closestIndex = getClosestIndexFromPointer(mouseX, mouseY);
588
- requestTooltipRenderAtIndex(closestIndex);
589
- })
590
- .on('mouseout', () => {
591
- if (isTooltipFocusTarget(document.activeElement)) {
592
- return;
593
- }
594
- cancelPendingTooltipRender();
595
- hideTooltip();
596
- });
597
- const focusTargets = svg
598
- .append('g')
599
- .attr('class', 'tooltip-focus-targets')
600
- .selectAll('rect')
601
- .data(data)
602
- .join('rect')
603
- .attr('class', 'tooltip-focus-target')
604
- .attr('data-index', (_, i) => i)
605
- .attr('x', (_, i) => getFocusTargetBounds(i).x)
606
- .attr('y', (_, i) => getFocusTargetBounds(i).y)
607
- .attr('width', (_, i) => getFocusTargetBounds(i).width)
608
- .attr('height', (_, i) => getFocusTargetBounds(i).height)
609
- .attr('tabindex', 0)
610
- .attr('fill', 'transparent')
611
- .attr('stroke', 'none')
612
- .attr('stroke-width', 0)
613
- .attr('aria-label', (dataPoint) => buildAccessibleLabel(dataPoint))
614
- .style('pointer-events', 'none');
615
- const focusTargetNodes = focusTargets.nodes();
616
- const getNextFocusTargetIndex = (currentIndex, key) => {
617
- switch (key) {
618
- case 'ArrowRight':
619
- case 'ArrowDown':
620
- return currentIndex + 1;
621
- case 'ArrowLeft':
622
- case 'ArrowUp':
623
- return currentIndex - 1;
624
- case 'Home':
625
- return 0;
626
- case 'End':
627
- return focusTargetNodes.length - 1;
628
- default:
629
- return -1;
630
- }
631
- };
632
- focusTargets
633
- .on('focus', function () {
634
- const currentIndex = focusTargetNodes.indexOf(this);
635
- if (currentIndex === -1) {
636
- return;
637
- }
638
- cancelPendingTooltipRender();
639
- select(this).attr('stroke', '#111827').attr('stroke-width', 2);
640
- renderTooltipAtIndex(currentIndex);
641
- })
642
- .on('blur', function (event) {
643
- select(this).attr('stroke', 'none').attr('stroke-width', 0);
644
- if (isTooltipFocusTarget(event.relatedTarget)) {
645
- return;
646
- }
647
- cancelPendingTooltipRender();
648
- hideTooltip();
649
- })
650
- .on('keydown', function (event) {
651
- const currentIndex = focusTargetNodes.indexOf(this);
652
- if (currentIndex === -1) {
653
- return;
654
- }
655
- const nextIndex = getNextFocusTargetIndex(currentIndex, event.key);
656
- if (nextIndex < 0 || nextIndex >= focusTargetNodes.length) {
657
- return;
658
- }
659
- event.preventDefault();
660
- focusTargetNodes[nextIndex].focus();
139
+ this.detachTooltipScrollListeners?.();
140
+ this.detachTooltipScrollListeners = attachXYTooltipArea({
141
+ svg,
142
+ data,
143
+ series,
144
+ xKey,
145
+ x,
146
+ y,
147
+ theme,
148
+ plotArea,
149
+ parseValue,
150
+ isHorizontal,
151
+ categoryScaleType,
152
+ resolveSeriesValue,
153
+ mode: this.mode,
154
+ position: this.position,
155
+ barAnchorPosition: this.barAnchorPosition,
156
+ formatter: this.formatter,
157
+ labelFormatter: this.labelFormatter,
158
+ customFormatter: this.customFormatter,
159
+ dom: this.dom,
661
160
  });
662
161
  }
663
162
  setContent(content) {
664
- if (!this.tooltipDiv) {
665
- return;
666
- }
667
- this.setTooltipMarkup(this.tooltipDiv, content);
163
+ this.dom.setContent(content);
668
164
  }
669
165
  getBounds() {
670
- const node = this.tooltipDiv?.node();
671
- if (!node) {
672
- return null;
673
- }
674
- return node.getBoundingClientRect();
166
+ return this.dom.getBounds();
675
167
  }
676
168
  showAt(left, top) {
677
- if (!this.tooltipDiv) {
678
- return;
679
- }
680
- if (!Number.isFinite(left) || !Number.isFinite(top)) {
681
- this.hide();
682
- return;
683
- }
684
- this.showTooltipAt(this.tooltipDiv, left, top);
169
+ this.dom.showAt(left, top);
685
170
  }
686
171
  hide() {
687
- const tooltip = this.tooltipDiv ?? select(`#${this.id}`);
688
- if (!tooltip.empty()) {
689
- this.hideTooltipSelection(tooltip);
690
- }
691
- this.hideSplitTooltips();
172
+ this.dom.hide();
692
173
  }
693
174
  cleanup() {
694
- this.removeRootTooltip();
695
- this.removeSplitTooltips();
696
- this.tooltipDiv = null;
697
- }
698
- applyTooltipStylesIfNeeded(tooltip, theme) {
699
- const node = tooltip.node();
700
- if (!node) {
701
- return;
702
- }
703
- const styleKey = this.getTooltipStyleKey(theme);
704
- if (this.tooltipStyleKeys.get(node) === styleKey) {
705
- this.tooltipTheme = theme.tooltip;
706
- return;
707
- }
708
- this.tooltipStyleKeys.set(node, styleKey);
709
- this.writeTooltipStyles(tooltip, theme);
710
- }
711
- getTooltipStyleKey(theme) {
712
- return [
713
- theme.tooltip.background,
714
- theme.tooltip.border,
715
- theme.tooltip.color,
716
- theme.tooltip.fontFamily,
717
- theme.tooltip.fontSize,
718
- theme.tooltip.fontWeight,
719
- this.maxWidth,
720
- this.transition.show,
721
- this.transition.duration,
722
- this.transition.easing,
723
- ].join('|');
724
- }
725
- writeTooltipStyles(tooltip, theme) {
726
- this.tooltipTheme = theme.tooltip;
727
- tooltip
728
- .style('position', 'absolute')
729
- .style('background-color', theme.tooltip.background)
730
- .style('border', `${TOOLTIP_BORDER_WIDTH_PX}px solid ${theme.tooltip.border}`)
731
- .style('border-radius', '4px')
732
- .style('padding', '8px')
733
- .style('box-shadow', '0 2px 4px rgba(0,0,0,0.1)')
734
- .style('color', theme.tooltip.color)
735
- .style('font-family', theme.tooltip.fontFamily)
736
- .style('font-size', `${theme.tooltip.fontSize}px`)
737
- .style('font-weight', theme.tooltip.fontWeight)
738
- .style('box-sizing', 'border-box')
739
- .style('overflow-wrap', 'break-word')
740
- .style('overflow', 'visible')
741
- .style('isolation', 'isolate')
742
- .style('pointer-events', 'none')
743
- .style('z-index', '1000');
744
- tooltip.style('max-width', `${this.maxWidth}px`);
745
- if (this.transition.show) {
746
- tooltip
747
- .style('transition', `opacity ${this.transition.duration}ms ${this.transition.easing}, transform ${this.transition.duration}ms ${this.transition.easing}`)
748
- .style('will-change', 'opacity, transform');
749
- return;
750
- }
751
- tooltip
752
- .style('opacity', null)
753
- .style('transform', null)
754
- .style('transition', null)
755
- .style('will-change', null);
756
- }
757
- measureTooltip(tooltip, content) {
758
- this.setTooltipMarkup(tooltip, content);
759
- tooltip.style('left', '-9999px').style('top', '-9999px');
760
- this.hideTooltipSelection(tooltip);
761
- const tooltipNode = tooltip.node();
762
- if (!tooltipNode) {
763
- return null;
764
- }
765
- const tooltipRect = tooltipNode.getBoundingClientRect();
766
- if (!Number.isFinite(tooltipRect.width) ||
767
- !Number.isFinite(tooltipRect.height)) {
768
- return null;
769
- }
770
- return {
771
- width: tooltipRect.width,
772
- height: tooltipRect.height,
773
- };
774
- }
775
- renderTooltipWithConnector(tooltip, arrowEdge, left, top, tooltipWidth, tooltipHeight, targetX, targetY, anchor) {
776
- if (!Number.isFinite(left) ||
777
- !Number.isFinite(top) ||
778
- !Number.isFinite(targetX) ||
779
- !Number.isFinite(targetY)) {
780
- this.hideTooltipSelection(tooltip);
781
- return;
782
- }
783
- const connectorLayout = this.resolveTooltipConnectorLayout(arrowEdge, left, top, tooltipWidth, tooltipHeight, targetX, targetY, anchor);
784
- if (!connectorLayout) {
785
- this.hideTooltipSelection(tooltip);
786
- return;
787
- }
788
- this.appendTooltipConnector(tooltip, connectorLayout);
789
- this.appendTooltipArrow(tooltip, connectorLayout);
790
- this.showTooltipAt(tooltip, left, top);
791
- }
792
- renderTooltipWithoutConnector(tooltip, left, top) {
793
- if (!Number.isFinite(left) || !Number.isFinite(top)) {
794
- this.hideTooltipSelection(tooltip);
795
- return;
796
- }
797
- this.showTooltipAt(tooltip, left, top);
798
- }
799
- showTooltipAt(tooltip, left, top) {
800
- tooltip.style('left', `${left}px`).style('top', `${top}px`);
801
- this.showTooltipSelection(tooltip);
802
- }
803
- showTooltipSelection(tooltip) {
804
- tooltip.style('visibility', 'visible');
805
- if (!this.transition.show) {
806
- return;
807
- }
808
- tooltip
809
- .style('opacity', '1')
810
- .style('transform', TOOLTIP_VISIBLE_TRANSFORM);
811
- }
812
- hideTooltipSelection(tooltip) {
813
- const node = tooltip.node();
814
- if (!node) {
815
- return;
816
- }
817
- this.hideTooltipElement(node);
818
- }
819
- hideTooltipElement(node) {
820
- if (!this.transition.show) {
821
- node.style.visibility = 'hidden';
822
- return;
823
- }
824
- node.style.visibility = 'visible';
825
- node.style.opacity = '0';
826
- node.style.transform = TOOLTIP_HIDDEN_TRANSFORM;
827
- }
828
- setTooltipMarkup(tooltip, content) {
829
- tooltip.html(`<div data-chart-tooltip-body="true">${content}</div>`);
830
- const body = tooltip.select('[data-chart-tooltip-body]');
831
- if (body.empty()) {
832
- return;
833
- }
834
- body.style('position', 'relative').style('z-index', String(TOOLTIP_BODY_Z_INDEX));
835
- }
836
- appendTooltipConnector(tooltip, connectorLayout) {
837
- const tooltipBorder = this.tooltipTheme?.border ?? '#dddddd';
838
- const connector = tooltip
839
- .append('svg')
840
- .attr('data-chart-tooltip-connector', 'true')
841
- .attr('data-chart-tooltip-arrow-edge', connectorLayout.arrowEdge)
842
- .attr('aria-hidden', 'true')
843
- .attr('width', connectorLayout.width)
844
- .attr('height', connectorLayout.height)
845
- .attr('viewBox', `0 0 ${connectorLayout.width} ${connectorLayout.height}`)
846
- .style('position', 'absolute')
847
- .style('left', `${connectorLayout.left}px`)
848
- .style('top', `${connectorLayout.top}px`)
849
- .style('pointer-events', 'none')
850
- .style('overflow', 'visible')
851
- .style('z-index', String(TOOLTIP_CONNECTOR_Z_INDEX));
852
- connector
853
- .append('path')
854
- .attr('data-chart-tooltip-connector-path', 'true')
855
- .attr('d', connectorLayout.path)
856
- .attr('fill', 'none')
857
- .attr('stroke', tooltipBorder)
858
- .attr('stroke-width', 1.25)
859
- .attr('stroke-linecap', 'round')
860
- .attr('stroke-linejoin', 'round');
861
- }
862
- appendTooltipArrow(tooltip, connectorLayout) {
863
- const tooltipBackground = this.tooltipTheme?.background ?? '#ffffff';
864
- const tooltipBorder = this.tooltipTheme?.border ?? '#dddddd';
865
- this.appendTooltipArrowTriangle(tooltip, connectorLayout, 'data-chart-tooltip-arrow', tooltipBorder, TOOLTIP_BOX_ARROW_LENGTH_PX, TOOLTIP_BOX_ARROW_HALF_HEIGHT_PX, TOOLTIP_ARROW_BORDER_Z_INDEX);
866
- this.appendTooltipArrowTriangle(tooltip, connectorLayout, 'data-chart-tooltip-arrow-fill', tooltipBackground, TOOLTIP_BOX_ARROW_LENGTH_PX - 1, TOOLTIP_BOX_ARROW_HALF_HEIGHT_PX - 1, TOOLTIP_ARROW_FILL_Z_INDEX);
867
- }
868
- appendTooltipArrowTriangle(tooltip, connectorLayout, dataAttribute, color, length, halfHeight, zIndex) {
869
- const position = this.resolveTooltipArrowPosition(connectorLayout.arrowEdge, connectorLayout.arrowX, connectorLayout.arrowY, length, halfHeight);
870
- const arrow = tooltip
871
- .append('div')
872
- .attr(dataAttribute, 'true')
873
- .attr('data-chart-tooltip-arrow-edge', connectorLayout.arrowEdge)
874
- .attr('aria-hidden', 'true')
875
- .style('position', 'absolute')
876
- .style('left', `${position.left}px`)
877
- .style('top', `${position.top}px`)
878
- .style('width', '0')
879
- .style('height', '0')
880
- .style('pointer-events', 'none')
881
- .style('z-index', String(zIndex));
882
- if (connectorLayout.arrowEdge === 'left') {
883
- arrow
884
- .style('border-top', `${halfHeight}px solid transparent`)
885
- .style('border-bottom', `${halfHeight}px solid transparent`)
886
- .style('border-right', `${length}px solid ${color}`);
887
- return;
888
- }
889
- if (connectorLayout.arrowEdge === 'right') {
890
- arrow
891
- .style('border-top', `${halfHeight}px solid transparent`)
892
- .style('border-bottom', `${halfHeight}px solid transparent`)
893
- .style('border-left', `${length}px solid ${color}`);
894
- return;
895
- }
896
- if (connectorLayout.arrowEdge === 'top') {
897
- arrow
898
- .style('border-left', `${halfHeight}px solid transparent`)
899
- .style('border-right', `${halfHeight}px solid transparent`)
900
- .style('border-bottom', `${length}px solid ${color}`);
901
- return;
902
- }
903
- arrow
904
- .style('border-left', `${halfHeight}px solid transparent`)
905
- .style('border-right', `${halfHeight}px solid transparent`)
906
- .style('border-top', `${length}px solid ${color}`);
907
- }
908
- resolveTooltipArrowPosition(arrowEdge, boxX, boxY, length, halfHeight) {
909
- switch (arrowEdge) {
910
- case 'left':
911
- return {
912
- left: boxX - length,
913
- top: boxY - halfHeight,
914
- };
915
- case 'right':
916
- return {
917
- left: boxX,
918
- top: boxY - halfHeight,
919
- };
920
- case 'top':
921
- return {
922
- left: boxX - halfHeight,
923
- top: boxY - length,
924
- };
925
- case 'bottom':
926
- return {
927
- left: boxX - halfHeight,
928
- top: boxY,
929
- };
930
- }
931
- }
932
- resolveBarTooltipAnchor(svgNode, dataKey, index) {
933
- const barNode = svgNode.querySelector(`.bar-${sanitizeForCSS(dataKey)}[data-index="${index}"]`);
934
- if (!barNode) {
935
- return null;
936
- }
937
- const rect = barNode.getBoundingClientRect();
938
- if (rect.width > 0 || rect.height > 0) {
939
- return {
940
- left: rect.left + window.scrollX,
941
- right: rect.right + window.scrollX,
942
- top: rect.top + window.scrollY,
943
- bottom: rect.bottom + window.scrollY,
944
- centerX: rect.left + window.scrollX + rect.width / 2,
945
- centerY: rect.top + window.scrollY + rect.height / 2,
946
- };
947
- }
948
- const svgRect = svgNode.getBoundingClientRect();
949
- const xValue = Number(barNode.getAttribute('x') ?? '0');
950
- const yValue = Number(barNode.getAttribute('y') ?? '0');
951
- const widthValue = Number(barNode.getAttribute('width') ?? '0');
952
- const heightValue = Number(barNode.getAttribute('height') ?? '0');
953
- const left = svgRect.left + window.scrollX + xValue;
954
- const top = svgRect.top + window.scrollY + yValue;
955
- const right = left + widthValue;
956
- const bottom = top + heightValue;
957
- return {
958
- left,
959
- right,
960
- top,
961
- bottom,
962
- centerX: left + widthValue / 2,
963
- centerY: top + heightValue / 2,
964
- };
965
- }
966
- resolvePointTooltipAnchor(svgNode, isHorizontal, categoryPosition, valuePosition) {
967
- if (!Number.isFinite(categoryPosition) ||
968
- !Number.isFinite(valuePosition)) {
969
- return null;
970
- }
971
- const svgRect = svgNode.getBoundingClientRect();
972
- const anchorX = svgRect.left +
973
- window.scrollX +
974
- (isHorizontal ? valuePosition : categoryPosition);
975
- const anchorY = svgRect.top +
976
- window.scrollY +
977
- (isHorizontal ? categoryPosition : valuePosition);
978
- if (!Number.isFinite(anchorX) || !Number.isFinite(anchorY)) {
979
- return null;
980
- }
981
- return {
982
- left: anchorX,
983
- right: anchorX,
984
- top: anchorY,
985
- bottom: anchorY,
986
- centerX: anchorX,
987
- centerY: anchorY,
988
- };
989
- }
990
- getSplitTooltip(index, theme) {
991
- const tooltipId = `${this.splitTooltipOwner}-${index}`;
992
- const existingTooltip = select(`#${tooltipId}`);
993
- const tooltip = existingTooltip.empty()
994
- ? select('body')
995
- .append('div')
996
- .attr('class', 'chart-tooltip chart-tooltip--split')
997
- .attr('id', tooltipId)
998
- .attr('data-chart-tooltip-owner', this.splitTooltipOwner)
999
- .attr('data-chart-tooltip-index', String(index))
1000
- : existingTooltip;
1001
- this.applyTooltipStylesIfNeeded(tooltip, theme);
1002
- return tooltip;
1003
- }
1004
- hideSplitTooltips() {
1005
- document
1006
- .querySelectorAll(`[data-chart-tooltip-owner="${this.splitTooltipOwner}"]`)
1007
- .forEach((node) => {
1008
- this.hideTooltipElement(node);
1009
- });
1010
- }
1011
- removeSplitTooltips() {
1012
- document
1013
- .querySelectorAll(`[data-chart-tooltip-owner="${this.splitTooltipOwner}"]`)
1014
- .forEach((node) => {
1015
- node.remove();
1016
- });
1017
- }
1018
- removeRootTooltip() {
1019
- const tooltip = this.tooltipDiv ?? select(`#${this.id}`);
1020
- if (!tooltip.empty()) {
1021
- tooltip.remove();
1022
- }
1023
- }
1024
- resolveTooltipArrowEdge(anchor, target, tooltipWidth, tooltipHeight) {
1025
- if (this.position === 'vertical') {
1026
- return this.resolveVerticalPlacementArrowEdge(target, tooltipHeight);
1027
- }
1028
- return this.resolveSidePlacementArrowEdge(anchor, tooltipWidth);
1029
- }
1030
- resolveSidePlacementArrowEdge(anchor, tooltipWidth) {
1031
- const viewportLeft = window.scrollX + TOOLTIP_VIEWPORT_PADDING_PX + TOOLTIP_OFFSET_PX;
1032
- const viewportRight = window.scrollX +
1033
- window.innerWidth -
1034
- TOOLTIP_VIEWPORT_PADDING_PX -
1035
- TOOLTIP_OFFSET_PX;
1036
- const availableRightSpace = viewportRight - anchor.right;
1037
- const availableLeftSpace = anchor.left - viewportLeft;
1038
- if (availableRightSpace >= tooltipWidth ||
1039
- availableRightSpace >= availableLeftSpace) {
1040
- return 'left';
1041
- }
1042
- return 'right';
1043
- }
1044
- resolveVerticalPlacementArrowEdge(target, tooltipHeight) {
1045
- const viewportTop = window.scrollY + TOOLTIP_VIEWPORT_PADDING_PX + TOOLTIP_OFFSET_PX;
1046
- const viewportBottom = window.scrollY +
1047
- window.innerHeight -
1048
- TOOLTIP_VIEWPORT_PADDING_PX -
1049
- TOOLTIP_OFFSET_PX;
1050
- const availableTopSpace = target.y - viewportTop;
1051
- const availableBottomSpace = viewportBottom - target.y;
1052
- if (availableTopSpace >= tooltipHeight ||
1053
- availableTopSpace >= availableBottomSpace) {
1054
- return 'bottom';
1055
- }
1056
- return 'top';
1057
- }
1058
- resolveSharedTooltipTarget(anchor) {
1059
- return {
1060
- x: anchor.centerX,
1061
- y: anchor.centerY,
1062
- };
1063
- }
1064
- resolveSplitTooltipTarget(currentSeries, anchor) {
1065
- if (currentSeries.type === 'bar') {
1066
- return {
1067
- x: anchor.centerX,
1068
- y: this.barAnchorPosition === 'top'
1069
- ? anchor.top
1070
- : anchor.centerY,
1071
- };
1072
- }
1073
- return {
1074
- x: anchor.centerX,
1075
- y: anchor.centerY,
1076
- };
1077
- }
1078
- getTooltipConnectorOffset(start, size, target) {
1079
- const minOffset = TOOLTIP_CONNECTOR_INSET_PX;
1080
- const maxOffset = Math.max(minOffset, size - minOffset);
1081
- const preferredOffset = target - start;
1082
- return Math.max(minOffset, Math.min(preferredOffset, maxOffset));
1083
- }
1084
- getAnchoredTooltipPosition(anchor, target, tooltipWidth, tooltipHeight, arrowEdge) {
1085
- const minLeft = window.scrollX + TOOLTIP_VIEWPORT_PADDING_PX;
1086
- const maxLeft = window.scrollX +
1087
- window.innerWidth -
1088
- tooltipWidth -
1089
- TOOLTIP_VIEWPORT_PADDING_PX;
1090
- const minTop = window.scrollY + TOOLTIP_VIEWPORT_PADDING_PX;
1091
- const maxTop = window.scrollY +
1092
- window.innerHeight -
1093
- tooltipHeight -
1094
- TOOLTIP_VIEWPORT_PADDING_PX;
1095
- let left = target.x - tooltipWidth / 2;
1096
- let top = target.y - tooltipHeight / 2;
1097
- if (arrowEdge === 'left') {
1098
- left = anchor.right + TOOLTIP_OFFSET_PX;
1099
- }
1100
- else if (arrowEdge === 'right') {
1101
- left = anchor.left - tooltipWidth - TOOLTIP_OFFSET_PX;
1102
- }
1103
- else if (arrowEdge === 'bottom') {
1104
- top = target.y - tooltipHeight - TOOLTIP_OFFSET_PX;
1105
- }
1106
- else {
1107
- top = target.y + TOOLTIP_OFFSET_PX;
1108
- }
1109
- if (!Number.isFinite(left) || !Number.isFinite(top)) {
1110
- return null;
1111
- }
1112
- return {
1113
- left: Math.max(minLeft, Math.min(left, maxLeft)),
1114
- top: Math.max(minTop, Math.min(top, maxTop)),
1115
- };
1116
- }
1117
- resolveTooltipConnectorLayout(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY, anchor) {
1118
- const localTargetX = targetX - tooltipLeft;
1119
- const localTargetY = targetY - tooltipTop;
1120
- if (!Number.isFinite(localTargetX) || !Number.isFinite(localTargetY)) {
1121
- return null;
1122
- }
1123
- const boxArrowPosition = this.resolveTooltipBoxArrowPosition(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY);
1124
- const arrowTip = this.resolveTooltipArrowTip(arrowEdge, boxArrowPosition.x, boxArrowPosition.y, TOOLTIP_BOX_ARROW_LENGTH_PX);
1125
- const minX = Math.min(arrowTip.x, localTargetX) - TOOLTIP_CONNECTOR_PADDING_PX;
1126
- const maxX = Math.max(arrowTip.x, localTargetX) + TOOLTIP_CONNECTOR_PADDING_PX;
1127
- const minY = Math.min(arrowTip.y, localTargetY) - TOOLTIP_CONNECTOR_PADDING_PX;
1128
- const maxY = Math.max(arrowTip.y, localTargetY) + TOOLTIP_CONNECTOR_PADDING_PX;
1129
- const width = Math.max(1, maxX - minX);
1130
- const height = Math.max(1, maxY - minY);
1131
- const boxX = boxArrowPosition.x - minX;
1132
- const boxY = boxArrowPosition.y - minY;
1133
- const startX = arrowTip.x - minX;
1134
- const startY = arrowTip.y - minY;
1135
- const endX = localTargetX - minX;
1136
- const endY = localTargetY - minY;
1137
- const arrowTipX = tooltipLeft + arrowTip.x;
1138
- const arrowTipY = tooltipTop + arrowTip.y;
1139
- const connectorPath = this.resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY, arrowTipX, arrowTipY, anchor);
1140
- if (!this.hasFiniteNumbers(width, height, boxX, boxY, startX, startY, endX, endY)) {
1141
- return null;
1142
- }
1143
- return {
1144
- left: minX,
1145
- top: minY,
1146
- arrowEdge,
1147
- width,
1148
- height,
1149
- path: connectorPath,
1150
- arrowX: boxArrowPosition.x,
1151
- arrowY: boxArrowPosition.y,
1152
- };
1153
- }
1154
- resolveTooltipBoxArrowPosition(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY) {
1155
- // Arrow offsets are relative to the padding box, while measured
1156
- // tooltip dimensions include both borders.
1157
- const rightInnerBorderX = tooltipWidth - TOOLTIP_TOTAL_BORDER_WIDTH_PX;
1158
- const bottomInnerBorderY = tooltipHeight - TOOLTIP_TOTAL_BORDER_WIDTH_PX;
1159
- switch (arrowEdge) {
1160
- case 'left':
1161
- return {
1162
- x: 0,
1163
- y: this.getTooltipConnectorOffset(tooltipTop, tooltipHeight, targetY),
1164
- };
1165
- case 'right':
1166
- return {
1167
- x: rightInnerBorderX,
1168
- y: this.getTooltipConnectorOffset(tooltipTop, tooltipHeight, targetY),
1169
- };
1170
- case 'top':
1171
- return {
1172
- x: this.getTooltipConnectorOffset(tooltipLeft, tooltipWidth, targetX),
1173
- y: 0,
1174
- };
1175
- case 'bottom':
1176
- return {
1177
- x: this.getTooltipConnectorOffset(tooltipLeft, tooltipWidth, targetX),
1178
- y: bottomInnerBorderY,
1179
- };
1180
- }
1181
- }
1182
- resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY, arrowTipX, arrowTipY, anchor) {
1183
- if (arrowEdge === 'left' || arrowEdge === 'right') {
1184
- if (this.isTooltipArrowTipInsideVerticalAnchorSpan(arrowTipY, anchor)) {
1185
- return '';
1186
- }
1187
- const elbowX = startX + (endX - startX) * TOOLTIP_CONNECTOR_ELBOW_RATIO;
1188
- return `M ${startX},${startY} L ${elbowX},${startY} L ${endX},${endY}`;
1189
- }
1190
- if (this.isTooltipArrowTipInsideHorizontalAnchorSpan(arrowTipX, anchor)) {
1191
- return '';
1192
- }
1193
- const elbowY = startY + (endY - startY) * TOOLTIP_CONNECTOR_ELBOW_RATIO;
1194
- return `M ${startX},${startY} L ${startX},${elbowY} L ${endX},${endY}`;
1195
- }
1196
- isTooltipArrowTipInsideHorizontalAnchorSpan(arrowTipX, anchor) {
1197
- return (arrowTipX >=
1198
- anchor.left - TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX &&
1199
- arrowTipX <= anchor.right + TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX);
1200
- }
1201
- isTooltipArrowTipInsideVerticalAnchorSpan(arrowTipY, anchor) {
1202
- return (arrowTipY >=
1203
- anchor.top - TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX &&
1204
- arrowTipY <=
1205
- anchor.bottom + TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX);
1206
- }
1207
- resolveTooltipArrowTip(arrowEdge, boxX, boxY, length) {
1208
- if (arrowEdge === 'left' || arrowEdge === 'right') {
1209
- return {
1210
- x: arrowEdge === 'left' ? boxX - length : boxX + length,
1211
- y: boxY,
1212
- };
1213
- }
1214
- return {
1215
- x: boxX,
1216
- y: arrowEdge === 'top' ? boxY - length : boxY + length,
1217
- };
1218
- }
1219
- hasFiniteNumbers(...values) {
1220
- return values.every((value) => Number.isFinite(value));
1221
- }
1222
- resolveSplitTooltipCollisions(layouts, position, getOppositeArrowEdge) {
1223
- if (this.position !== position) {
1224
- return;
1225
- }
1226
- const placedLayouts = [];
1227
- const orderedLayouts = [...layouts].sort((a, b) => {
1228
- if (position === 'vertical') {
1229
- return a.targetX - b.targetX || a.targetY - b.targetY;
1230
- }
1231
- return a.targetY - b.targetY || a.targetX - b.targetX;
1232
- });
1233
- orderedLayouts.forEach((layout) => {
1234
- this.flipTooltipIfItReducesCollisions(layout, placedLayouts, getOppositeArrowEdge);
1235
- placedLayouts.push(layout);
1236
- });
1237
- }
1238
- getOppositeSideArrowEdge(arrowEdge) {
1239
- if (arrowEdge === 'left') {
1240
- return 'right';
1241
- }
1242
- if (arrowEdge === 'right') {
1243
- return 'left';
1244
- }
1245
- return null;
1246
- }
1247
- getOppositeVerticalArrowEdge(arrowEdge) {
1248
- if (arrowEdge === 'top') {
1249
- return 'bottom';
1250
- }
1251
- if (arrowEdge === 'bottom') {
1252
- return 'top';
1253
- }
1254
- return null;
1255
- }
1256
- flipTooltipIfItReducesCollisions(layout, placedLayouts, getOppositeArrowEdge) {
1257
- const currentCollisions = this.countSplitTooltipCollisions(layout, placedLayouts);
1258
- if (currentCollisions === 0) {
1259
- return;
1260
- }
1261
- const flippedArrowEdge = getOppositeArrowEdge(layout.arrowEdge);
1262
- if (!flippedArrowEdge) {
1263
- return;
1264
- }
1265
- const flippedPosition = this.getAnchoredTooltipPosition(layout.anchor, { x: layout.targetX, y: layout.targetY }, layout.width, layout.height, flippedArrowEdge);
1266
- if (!flippedPosition) {
1267
- return;
1268
- }
1269
- const flippedLayout = {
1270
- ...layout,
1271
- arrowEdge: flippedArrowEdge,
1272
- left: flippedPosition.left,
1273
- top: flippedPosition.top,
1274
- };
1275
- const flippedCollisions = this.countSplitTooltipCollisions(flippedLayout, placedLayouts);
1276
- if (flippedCollisions > currentCollisions) {
1277
- return;
1278
- }
1279
- if (flippedCollisions === currentCollisions &&
1280
- this.countPlacedLayoutsOnEdge(placedLayouts, flippedArrowEdge) >=
1281
- this.countPlacedLayoutsOnEdge(placedLayouts, layout.arrowEdge)) {
1282
- return;
1283
- }
1284
- layout.arrowEdge = flippedArrowEdge;
1285
- layout.left = flippedPosition.left;
1286
- layout.top = flippedPosition.top;
1287
- }
1288
- countSplitTooltipCollisions(layout, placedLayouts) {
1289
- return placedLayouts.filter((placedLayout) => this.doSplitTooltipLayoutsOverlap(layout, placedLayout)).length;
1290
- }
1291
- countPlacedLayoutsOnEdge(placedLayouts, arrowEdge) {
1292
- return placedLayouts.filter((layout) => layout.arrowEdge === arrowEdge)
1293
- .length;
1294
- }
1295
- doSplitTooltipLayoutsOverlap(a, b) {
1296
- return (a.left < b.left + b.width + SPLIT_TOOLTIP_GAP_PX &&
1297
- a.left + a.width + SPLIT_TOOLTIP_GAP_PX > b.left &&
1298
- a.top < b.top + b.height + SPLIT_TOOLTIP_GAP_PX &&
1299
- a.top + a.height + SPLIT_TOOLTIP_GAP_PX > b.top);
1300
- }
1301
- resolveSplitTooltipPositions(layouts, isHorizontal) {
1302
- if (isHorizontal) {
1303
- this.resolveHorizontalChartSplitTooltipPositions(layouts);
1304
- return;
1305
- }
1306
- this.resolveVerticalChartSplitTooltipPositions(layouts);
1307
- }
1308
- resolveHorizontalChartSplitTooltipPositions(layouts) {
1309
- if (this.position === 'vertical') {
1310
- this.resolveSplitTooltipCollisions(layouts, 'vertical', this.getOppositeVerticalArrowEdge);
1311
- this.resolveHorizontalChartAboveBelowSplitTooltipPositions(layouts);
1312
- return;
1313
- }
1314
- this.resolveHorizontalSideSplitTooltipPositions(layouts);
1315
- }
1316
- resolveVerticalChartSplitTooltipPositions(layouts) {
1317
- if (this.position === 'vertical') {
1318
- this.resolveSplitTooltipCollisions(layouts, 'vertical', this.getOppositeVerticalArrowEdge);
1319
- this.resolveVerticalChartAboveBelowSplitTooltipPositions(layouts);
1320
- return;
1321
- }
1322
- this.resolveSplitTooltipCollisions(layouts, 'side', this.getOppositeSideArrowEdge);
1323
- this.resolveSideSplitTooltipPositions(layouts);
1324
- }
1325
- getSplitTooltipViewportBounds() {
1326
- return {
1327
- minLeft: window.scrollX + TOOLTIP_VIEWPORT_PADDING_PX,
1328
- maxRight: window.scrollX +
1329
- window.innerWidth -
1330
- TOOLTIP_VIEWPORT_PADDING_PX,
1331
- minTop: window.scrollY + TOOLTIP_VIEWPORT_PADDING_PX,
1332
- maxBottom: window.scrollY +
1333
- window.innerHeight -
1334
- TOOLTIP_VIEWPORT_PADDING_PX,
1335
- };
1336
- }
1337
- groupSplitTooltipLayoutsByEdge(layouts) {
1338
- const tooltipsByEdge = {
1339
- left: [],
1340
- right: [],
1341
- top: [],
1342
- bottom: [],
1343
- };
1344
- layouts.forEach((layout) => {
1345
- tooltipsByEdge[layout.arrowEdge].push(layout);
1346
- });
1347
- return tooltipsByEdge;
1348
- }
1349
- resolveSideSplitTooltipPositions(layouts) {
1350
- const { minTop, maxBottom } = this.getSplitTooltipViewportBounds();
1351
- const tooltipsByEdge = this.groupSplitTooltipLayoutsByEdge(layouts);
1352
- Object.values(tooltipsByEdge).forEach((edgeLayouts) => {
1353
- if (edgeLayouts.length === 0) {
1354
- return;
1355
- }
1356
- edgeLayouts.sort((a, b) => a.top - b.top);
1357
- edgeLayouts[0].top = Math.max(minTop, edgeLayouts[0].top);
1358
- for (let i = 1; i < edgeLayouts.length; i++) {
1359
- const previousLayout = edgeLayouts[i - 1];
1360
- const currentLayout = edgeLayouts[i];
1361
- const minAllowedTop = previousLayout.top +
1362
- previousLayout.height +
1363
- SPLIT_TOOLTIP_GAP_PX;
1364
- currentLayout.top = Math.max(currentLayout.top, minAllowedTop);
1365
- }
1366
- const lastLayout = edgeLayouts[edgeLayouts.length - 1];
1367
- const overflow = lastLayout.top + lastLayout.height - maxBottom;
1368
- if (overflow > 0) {
1369
- lastLayout.top -= overflow;
1370
- for (let i = edgeLayouts.length - 2; i >= 0; i--) {
1371
- const currentLayout = edgeLayouts[i];
1372
- const nextLayout = edgeLayouts[i + 1];
1373
- const maxAllowedTop = nextLayout.top -
1374
- currentLayout.height -
1375
- SPLIT_TOOLTIP_GAP_PX;
1376
- currentLayout.top = Math.min(currentLayout.top, maxAllowedTop);
1377
- }
1378
- const underflow = minTop - edgeLayouts[0].top;
1379
- if (underflow > 0) {
1380
- edgeLayouts.forEach((layout) => {
1381
- layout.top += underflow;
1382
- });
1383
- }
1384
- }
1385
- edgeLayouts.forEach((layout) => {
1386
- const maxTop = maxBottom - layout.height;
1387
- layout.top = Math.max(minTop, Math.min(layout.top, maxTop));
1388
- });
1389
- });
1390
- }
1391
- resolveHorizontalSideSplitTooltipPositions(layouts) {
1392
- this.resolveHorizontalSideSplitTooltipCollisions(layouts);
1393
- const minTop = window.scrollY + TOOLTIP_VIEWPORT_PADDING_PX;
1394
- const maxBottom = window.scrollY + window.innerHeight - TOOLTIP_VIEWPORT_PADDING_PX;
1395
- const lanes = [];
1396
- const orderedLayouts = [...layouts].sort((a, b) => a.targetX - b.targetX || a.left - b.left);
1397
- orderedLayouts.forEach((layout) => {
1398
- const maxTop = maxBottom - layout.height;
1399
- const preferredTop = Math.max(minTop, Math.min(layout.top, maxTop));
1400
- const reusableLane = this.findReusableHorizontalTooltipLane(lanes, layout, preferredTop);
1401
- if (reusableLane) {
1402
- this.assignLayoutToHorizontalTooltipLane(layout, reusableLane);
1403
- return;
1404
- }
1405
- const nextLane = {
1406
- top: this.resolveNewHorizontalTooltipLaneTop(preferredTop, layout.height, minTop, maxTop, lanes),
1407
- layouts: [],
1408
- };
1409
- lanes.push(nextLane);
1410
- this.assignLayoutToHorizontalTooltipLane(layout, nextLane);
1411
- });
1412
- }
1413
- resolveHorizontalSideSplitTooltipCollisions(layouts) {
1414
- const placedLayouts = [];
1415
- const orderedLayouts = [...layouts].sort((a, b) => a.targetX - b.targetX || a.left - b.left);
1416
- orderedLayouts.forEach((layout) => {
1417
- this.flipHorizontalSideTooltipIfItReducesCollisions(layout, placedLayouts);
1418
- placedLayouts.push(layout);
1419
- });
1420
- }
1421
- flipHorizontalSideTooltipIfItReducesCollisions(layout, placedLayouts) {
1422
- const currentCollisions = this.countSplitTooltipCollisions(layout, placedLayouts);
1423
- if (currentCollisions === 0) {
1424
- return;
1425
- }
1426
- const flippedArrowEdge = this.getOppositeSideArrowEdge(layout.arrowEdge);
1427
- if (!flippedArrowEdge) {
1428
- return;
1429
- }
1430
- const flippedPosition = this.getAnchoredTooltipPosition(layout.anchor, { x: layout.targetX, y: layout.targetY }, layout.width, layout.height, flippedArrowEdge);
1431
- if (!flippedPosition) {
1432
- return;
1433
- }
1434
- const flippedLayout = {
1435
- ...layout,
1436
- arrowEdge: flippedArrowEdge,
1437
- left: flippedPosition.left,
1438
- top: flippedPosition.top,
1439
- };
1440
- const flippedCollisions = this.countSplitTooltipCollisions(flippedLayout, placedLayouts);
1441
- if (flippedCollisions > currentCollisions) {
1442
- return;
1443
- }
1444
- if (flippedCollisions === currentCollisions &&
1445
- this.countPlacedLayoutsOnEdge(placedLayouts, flippedArrowEdge) >=
1446
- this.countPlacedLayoutsOnEdge(placedLayouts, layout.arrowEdge)) {
1447
- return;
1448
- }
1449
- layout.arrowEdge = flippedArrowEdge;
1450
- layout.left = flippedPosition.left;
1451
- layout.top = flippedPosition.top;
1452
- }
1453
- findReusableHorizontalTooltipLane(lanes, layout, preferredTop) {
1454
- const reusableLanes = lanes
1455
- .filter((lane) => lane.layouts.every((placedLayout) => !this.doSplitTooltipLayoutsOverlapHorizontally(layout, placedLayout)))
1456
- .sort((a, b) => Math.abs(a.top - preferredTop) -
1457
- Math.abs(b.top - preferredTop));
1458
- return reusableLanes[0] ?? null;
1459
- }
1460
- resolveNewHorizontalTooltipLaneTop(preferredTop, tooltipHeight, minTop, maxTop, lanes) {
1461
- const usedTops = new Set(lanes.map((lane) => Math.round(lane.top)));
1462
- const candidates = this.getHorizontalTooltipLaneTopCandidates(preferredTop, tooltipHeight, minTop, maxTop);
1463
- return (candidates.find((candidate) => !usedTops.has(Math.round(candidate)) &&
1464
- lanes.every((lane) => !this.doHorizontalTooltipLanesOverlap(candidate, tooltipHeight, lane))) ??
1465
- candidates[0] ??
1466
- preferredTop);
1467
- }
1468
- getHorizontalTooltipLaneTopCandidates(preferredTop, tooltipHeight, minTop, maxTop) {
1469
- const step = tooltipHeight + SPLIT_TOOLTIP_GAP_PX;
1470
- const candidates = [preferredTop];
1471
- for (let index = 1; index <= 8; index++) {
1472
- candidates.push(preferredTop - step * index);
1473
- candidates.push(preferredTop + step * index);
1474
- }
1475
- return Array.from(new Set(candidates.map((candidate) => Math.round(Math.max(minTop, Math.min(candidate, maxTop)))))).sort((a, b) => Math.abs(a - preferredTop) - Math.abs(b - preferredTop));
1476
- }
1477
- assignLayoutToHorizontalTooltipLane(layout, lane) {
1478
- layout.top = lane.top;
1479
- lane.layouts.push(layout);
1480
- }
1481
- doHorizontalTooltipLanesOverlap(top, height, lane) {
1482
- const laneHeight = lane.layouts.reduce((maxHeight, layout) => Math.max(maxHeight, layout.height), 0);
1483
- return (top < lane.top + laneHeight + SPLIT_TOOLTIP_GAP_PX &&
1484
- top + height + SPLIT_TOOLTIP_GAP_PX > lane.top);
1485
- }
1486
- doSplitTooltipLayoutsOverlapHorizontally(a, b) {
1487
- return (a.left < b.left + b.width + SPLIT_TOOLTIP_GAP_PX &&
1488
- a.left + a.width + SPLIT_TOOLTIP_GAP_PX > b.left);
1489
- }
1490
- resolveHorizontalChartAboveBelowSplitTooltipPositions(layouts) {
1491
- const bounds = this.getSplitTooltipViewportBounds();
1492
- const tooltipsByEdge = this.groupSplitTooltipLayoutsByEdge(layouts);
1493
- this.resolvePackedAboveBelowTooltipRow(tooltipsByEdge.top, bounds);
1494
- this.resolvePackedAboveBelowTooltipRow(tooltipsByEdge.bottom, bounds);
1495
- this.resolveSideSplitTooltipPositions([
1496
- ...tooltipsByEdge.left,
1497
- ...tooltipsByEdge.right,
1498
- ]);
1499
- }
1500
- resolvePackedAboveBelowTooltipRow(layouts, bounds) {
1501
- if (layouts.length === 0) {
1502
- return;
1503
- }
1504
- const orderedLayouts = [...layouts].sort((a, b) => a.left - b.left || a.targetX - b.targetX);
1505
- orderedLayouts.forEach((layout) => {
1506
- const maxTop = Math.max(bounds.minTop, bounds.maxBottom - layout.height);
1507
- layout.top = Math.max(bounds.minTop, Math.min(layout.top, maxTop));
1508
- });
1509
- const firstLayout = orderedLayouts[0];
1510
- const firstMaxLeft = Math.max(bounds.minLeft, bounds.maxRight - firstLayout.width);
1511
- firstLayout.left = Math.max(bounds.minLeft, Math.min(firstLayout.left, firstMaxLeft));
1512
- for (let i = 1; i < orderedLayouts.length; i++) {
1513
- const previousLayout = orderedLayouts[i - 1];
1514
- const currentLayout = orderedLayouts[i];
1515
- const minAllowedLeft = previousLayout.left +
1516
- previousLayout.width +
1517
- SPLIT_TOOLTIP_GAP_PX;
1518
- currentLayout.left = Math.max(currentLayout.left, minAllowedLeft);
1519
- }
1520
- const lastLayout = orderedLayouts[orderedLayouts.length - 1];
1521
- const overflow = lastLayout.left + lastLayout.width - bounds.maxRight;
1522
- if (overflow > 0) {
1523
- lastLayout.left -= overflow;
1524
- for (let i = orderedLayouts.length - 2; i >= 0; i--) {
1525
- const currentLayout = orderedLayouts[i];
1526
- const nextLayout = orderedLayouts[i + 1];
1527
- const maxAllowedLeft = nextLayout.left -
1528
- currentLayout.width -
1529
- SPLIT_TOOLTIP_GAP_PX;
1530
- currentLayout.left = Math.min(currentLayout.left, maxAllowedLeft);
1531
- }
1532
- const underflow = bounds.minLeft - orderedLayouts[0].left;
1533
- if (underflow > 0) {
1534
- orderedLayouts.forEach((layout) => {
1535
- layout.left += underflow;
1536
- });
1537
- }
1538
- }
1539
- orderedLayouts.forEach((layout) => {
1540
- const maxLeft = Math.max(bounds.minLeft, bounds.maxRight - layout.width);
1541
- layout.left = Math.max(bounds.minLeft, Math.min(layout.left, maxLeft));
1542
- });
1543
- }
1544
- resolveVerticalChartAboveBelowSplitTooltipPositions(layouts) {
1545
- const bounds = this.getSplitTooltipViewportBounds();
1546
- const tooltipsByEdge = this.groupSplitTooltipLayoutsByEdge(layouts);
1547
- this.resolveCollisionAwareAboveBelowTooltipPositions(tooltipsByEdge.top, bounds);
1548
- this.resolveCollisionAwareAboveBelowTooltipPositions(tooltipsByEdge.bottom, bounds);
1549
- this.resolveSideSplitTooltipPositions([
1550
- ...tooltipsByEdge.left,
1551
- ...tooltipsByEdge.right,
1552
- ]);
1553
- }
1554
- resolveCollisionAwareAboveBelowTooltipPositions(layouts, bounds) {
1555
- const placedLayouts = [];
1556
- const orderedLayouts = [...layouts].sort((a, b) => a.left - b.left || a.top - b.top);
1557
- orderedLayouts.forEach((layout) => {
1558
- const maxLeft = Math.max(bounds.minLeft, bounds.maxRight - layout.width);
1559
- const maxTop = Math.max(bounds.minTop, bounds.maxBottom - layout.height);
1560
- layout.top = Math.max(bounds.minTop, Math.min(layout.top, maxTop));
1561
- layout.left = this.resolveNonOverlappingAboveBelowTooltipLeft(layout, placedLayouts, bounds.minLeft, maxLeft);
1562
- placedLayouts.push(layout);
1563
- });
1564
- }
1565
- resolveNonOverlappingAboveBelowTooltipLeft(layout, placedLayouts, minLeft, maxLeft) {
1566
- const preferredLeft = Math.max(minLeft, Math.min(layout.left, maxLeft));
1567
- if (!this.doesSplitTooltipOverlapPlacedLayouts({ ...layout, left: preferredLeft }, placedLayouts)) {
1568
- return preferredLeft;
1569
- }
1570
- const candidates = placedLayouts.flatMap((placedLayout) => [
1571
- placedLayout.left + placedLayout.width + SPLIT_TOOLTIP_GAP_PX,
1572
- placedLayout.left - layout.width - SPLIT_TOOLTIP_GAP_PX,
1573
- ]);
1574
- return (Array.from(new Set(candidates.map((candidate) => Math.round(Math.max(minLeft, Math.min(candidate, maxLeft))))))
1575
- .sort((a, b) => Math.abs(a - preferredLeft) -
1576
- Math.abs(b - preferredLeft))
1577
- .find((candidate) => !this.doesSplitTooltipOverlapPlacedLayouts({ ...layout, left: candidate }, placedLayouts)) ?? preferredLeft);
1578
- }
1579
- doesSplitTooltipOverlapPlacedLayouts(layout, placedLayouts) {
1580
- return placedLayouts.some((placedLayout) => this.doSplitTooltipLayoutsOverlap(layout, placedLayout));
175
+ this.detachTooltipScrollListeners?.();
176
+ this.detachTooltipScrollListeners = null;
177
+ this.dom.cleanup();
1581
178
  }
1582
179
  }
1583
180
  Object.defineProperty(Tooltip, "nextTooltipId", {