@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.
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", {
@@ -56,6 +34,12 @@ export class Tooltip {
56
34
  writable: true,
57
35
  value: void 0
58
36
  });
37
+ Object.defineProperty(this, "colorMode", {
38
+ enumerable: true,
39
+ configurable: true,
40
+ writable: true,
41
+ value: void 0
42
+ });
59
43
  Object.defineProperty(this, "maxWidth", {
60
44
  enumerable: true,
61
45
  configurable: true,
@@ -92,37 +76,25 @@ export class Tooltip {
92
76
  writable: true,
93
77
  value: void 0
94
78
  });
95
- Object.defineProperty(this, "splitTooltipOwner", {
79
+ Object.defineProperty(this, "dom", {
96
80
  enumerable: true,
97
81
  configurable: true,
98
82
  writable: true,
99
83
  value: void 0
100
84
  });
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", {
85
+ Object.defineProperty(this, "detachTooltipScrollListeners", {
114
86
  enumerable: true,
115
87
  configurable: true,
116
88
  writable: true,
117
89
  value: null
118
90
  });
119
- const { mode = 'split', position = 'side', barAnchorPosition = 'middle', maxWidth, transition, formatter, labelFormatter, customFormatter, exportHooks, } = config;
91
+ const { mode = 'split', position = 'auto', barAnchorPosition = 'auto', colorMode = 'theme', maxWidth, transition, formatter, labelFormatter, customFormatter, exportHooks, } = config;
120
92
  const tooltipId = Tooltip.nextTooltipId++;
121
93
  this.id = `iisChartTooltip-${tooltipId}`;
122
- this.splitTooltipOwner = `${this.id}-split`;
123
94
  this.mode = mode;
124
95
  this.position = position;
125
96
  this.barAnchorPosition = barAnchorPosition;
97
+ this.colorMode = colorMode;
126
98
  this.maxWidth =
127
99
  maxWidth !== undefined && Number.isFinite(maxWidth) && maxWidth > 0
128
100
  ? maxWidth
@@ -135,12 +107,19 @@ export class Tooltip {
135
107
  this.labelFormatter = labelFormatter;
136
108
  this.customFormatter = customFormatter;
137
109
  this.exportHooks = exportHooks;
110
+ this.dom = new TooltipDom({
111
+ id: this.id,
112
+ splitTooltipOwner: `${this.id}-split`,
113
+ maxWidth: this.maxWidth,
114
+ transition: this.transition,
115
+ });
138
116
  }
139
117
  getExportConfig() {
140
118
  return {
141
119
  mode: this.mode,
142
120
  position: this.position,
143
121
  barAnchorPosition: this.barAnchorPosition,
122
+ colorMode: this.colorMode,
144
123
  maxWidth: this.maxWidth,
145
124
  transition: this.transition,
146
125
  formatter: this.formatter,
@@ -156,17 +135,7 @@ export class Tooltip {
156
135
  });
157
136
  }
158
137
  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);
138
+ this.dom.initialize(theme);
170
139
  }
171
140
  attachToArea(svg, data, series, xKey, x, y, theme, plotArea, parseValue, isHorizontal = false, categoryScaleType = 'band', resolveSeriesValue = (targetSeries, dataPoint) => {
172
141
  const rawValue = dataPoint[targetSeries.dataKey];
@@ -175,1409 +144,46 @@ export class Tooltip {
175
144
  }
176
145
  return parseValue(rawValue);
177
146
  }) {
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();
147
+ this.detachTooltipScrollListeners?.();
148
+ this.detachTooltipScrollListeners = attachXYTooltipArea({
149
+ svg,
150
+ data,
151
+ series,
152
+ xKey,
153
+ x,
154
+ y,
155
+ theme,
156
+ plotArea,
157
+ parseValue,
158
+ isHorizontal,
159
+ categoryScaleType,
160
+ resolveSeriesValue,
161
+ mode: this.mode,
162
+ position: this.position,
163
+ barAnchorPosition: this.barAnchorPosition,
164
+ colorMode: this.colorMode,
165
+ formatter: this.formatter,
166
+ labelFormatter: this.labelFormatter,
167
+ customFormatter: this.customFormatter,
168
+ dom: this.dom,
661
169
  });
662
170
  }
663
171
  setContent(content) {
664
- if (!this.tooltipDiv) {
665
- return;
666
- }
667
- this.setTooltipMarkup(this.tooltipDiv, content);
172
+ this.dom.setContent(content);
668
173
  }
669
174
  getBounds() {
670
- const node = this.tooltipDiv?.node();
671
- if (!node) {
672
- return null;
673
- }
674
- return node.getBoundingClientRect();
175
+ return this.dom.getBounds();
675
176
  }
676
177
  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);
178
+ this.dom.showAt(left, top);
685
179
  }
686
180
  hide() {
687
- const tooltip = this.tooltipDiv ?? select(`#${this.id}`);
688
- if (!tooltip.empty()) {
689
- this.hideTooltipSelection(tooltip);
690
- }
691
- this.hideSplitTooltips();
181
+ this.dom.hide();
692
182
  }
693
183
  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));
184
+ this.detachTooltipScrollListeners?.();
185
+ this.detachTooltipScrollListeners = null;
186
+ this.dom.cleanup();
1581
187
  }
1582
188
  }
1583
189
  Object.defineProperty(Tooltip, "nextTooltipId", {