@internetstiftelsen/charts 0.10.0 → 0.11.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.
Files changed (57) hide show
  1. package/README.md +65 -1
  2. package/dist/area.d.ts +11 -1
  3. package/dist/area.js +199 -55
  4. package/dist/bar.d.ts +26 -1
  5. package/dist/bar.js +425 -306
  6. package/dist/base-chart.d.ts +5 -0
  7. package/dist/base-chart.js +91 -67
  8. package/dist/chart-group.d.ts +16 -0
  9. package/dist/chart-group.js +201 -143
  10. package/dist/donut-center-content.d.ts +1 -0
  11. package/dist/donut-center-content.js +21 -38
  12. package/dist/donut-chart.js +32 -32
  13. package/dist/gauge-chart.d.ts +23 -4
  14. package/dist/gauge-chart.js +235 -185
  15. package/dist/lazy-mount.d.ts +13 -0
  16. package/dist/lazy-mount.js +90 -0
  17. package/dist/legend.js +10 -9
  18. package/dist/line.d.ts +9 -1
  19. package/dist/line.js +144 -24
  20. package/dist/pie-chart.d.ts +3 -0
  21. package/dist/pie-chart.js +49 -47
  22. package/dist/radial-chart-base.d.ts +4 -3
  23. package/dist/radial-chart-base.js +27 -12
  24. package/dist/scatter.d.ts +5 -1
  25. package/dist/scatter.js +92 -9
  26. package/dist/theme.js +17 -0
  27. package/dist/tooltip.d.ts +55 -3
  28. package/dist/tooltip.js +968 -159
  29. package/dist/types.d.ts +23 -1
  30. package/dist/utils.js +11 -19
  31. package/dist/x-axis.d.ts +10 -0
  32. package/dist/x-axis.js +190 -149
  33. package/dist/xy-animation.d.ts +3 -0
  34. package/dist/xy-animation.js +2 -0
  35. package/dist/xy-chart.d.ts +35 -1
  36. package/dist/xy-chart.js +358 -153
  37. package/dist/xy-motion/config.d.ts +2 -0
  38. package/dist/xy-motion/config.js +177 -0
  39. package/dist/xy-motion/driver.d.ts +9 -0
  40. package/dist/xy-motion/driver.js +10 -0
  41. package/dist/xy-motion/helpers.d.ts +17 -0
  42. package/dist/xy-motion/helpers.js +105 -0
  43. package/dist/xy-motion/live-state.d.ts +8 -0
  44. package/dist/xy-motion/live-state.js +240 -0
  45. package/dist/xy-motion/noop-xy-motion-driver.d.ts +9 -0
  46. package/dist/xy-motion/noop-xy-motion-driver.js +15 -0
  47. package/dist/xy-motion/types.d.ts +85 -0
  48. package/dist/xy-motion/types.js +1 -0
  49. package/dist/xy-motion/xy-motion-driver.d.ts +19 -0
  50. package/dist/xy-motion/xy-motion-driver.js +130 -0
  51. package/dist/y-axis.d.ts +7 -2
  52. package/dist/y-axis.js +99 -10
  53. package/docs/components.md +50 -1
  54. package/docs/getting-started.md +35 -0
  55. package/docs/theming.md +14 -0
  56. package/docs/xy-chart.md +88 -7
  57. package/package.json +5 -4
package/dist/tooltip.js CHANGED
@@ -1,13 +1,29 @@
1
1
  import { pointer, select } from 'd3';
2
2
  import { getSeriesColor } from './types.js';
3
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_BOX_ARROW_LENGTH_PX = 10;
10
+ const TOOLTIP_BOX_ARROW_HALF_HEIGHT_PX = 6;
11
+ const TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX = 1;
12
+ const SPLIT_TOOLTIP_GAP_PX = 8;
13
+ const DEFAULT_TOOLTIP_TRANSITION = {
14
+ show: false,
15
+ duration: 120,
16
+ easing: 'ease-out',
17
+ };
18
+ const TOOLTIP_HIDDEN_TRANSFORM = 'translateY(2px)';
19
+ const TOOLTIP_VISIBLE_TRANSFORM = 'translateY(0)';
4
20
  export class Tooltip {
5
- constructor(config) {
21
+ constructor(config = {}) {
6
22
  Object.defineProperty(this, "id", {
7
23
  enumerable: true,
8
24
  configurable: true,
9
25
  writable: true,
10
- value: 'iisChartTooltip'
26
+ value: void 0
11
27
  });
12
28
  Object.defineProperty(this, "type", {
13
29
  enumerable: true,
@@ -15,6 +31,30 @@ export class Tooltip {
15
31
  writable: true,
16
32
  value: 'tooltip'
17
33
  });
34
+ Object.defineProperty(this, "mode", {
35
+ enumerable: true,
36
+ configurable: true,
37
+ writable: true,
38
+ value: void 0
39
+ });
40
+ Object.defineProperty(this, "position", {
41
+ enumerable: true,
42
+ configurable: true,
43
+ writable: true,
44
+ value: void 0
45
+ });
46
+ Object.defineProperty(this, "barAnchorPosition", {
47
+ enumerable: true,
48
+ configurable: true,
49
+ writable: true,
50
+ value: void 0
51
+ });
52
+ Object.defineProperty(this, "transition", {
53
+ enumerable: true,
54
+ configurable: true,
55
+ writable: true,
56
+ value: void 0
57
+ });
18
58
  Object.defineProperty(this, "formatter", {
19
59
  enumerable: true,
20
60
  configurable: true,
@@ -39,19 +79,52 @@ export class Tooltip {
39
79
  writable: true,
40
80
  value: void 0
41
81
  });
82
+ Object.defineProperty(this, "splitTooltipOwner", {
83
+ enumerable: true,
84
+ configurable: true,
85
+ writable: true,
86
+ value: void 0
87
+ });
88
+ Object.defineProperty(this, "tooltipStyleKeys", {
89
+ enumerable: true,
90
+ configurable: true,
91
+ writable: true,
92
+ value: new WeakMap()
93
+ });
42
94
  Object.defineProperty(this, "tooltipDiv", {
43
95
  enumerable: true,
44
96
  configurable: true,
45
97
  writable: true,
46
98
  value: null
47
99
  });
48
- this.formatter = config?.formatter;
49
- this.labelFormatter = config?.labelFormatter;
50
- this.customFormatter = config?.customFormatter;
51
- this.exportHooks = config?.exportHooks;
100
+ Object.defineProperty(this, "tooltipTheme", {
101
+ enumerable: true,
102
+ configurable: true,
103
+ writable: true,
104
+ value: null
105
+ });
106
+ const { mode = 'split', position = 'side', barAnchorPosition = 'middle', transition, formatter, labelFormatter, customFormatter, exportHooks, } = config;
107
+ const tooltipId = Tooltip.nextTooltipId++;
108
+ this.id = `iisChartTooltip-${tooltipId}`;
109
+ this.splitTooltipOwner = `${this.id}-split`;
110
+ this.mode = mode;
111
+ this.position = position;
112
+ this.barAnchorPosition = barAnchorPosition;
113
+ this.transition = {
114
+ ...DEFAULT_TOOLTIP_TRANSITION,
115
+ ...transition,
116
+ };
117
+ this.formatter = formatter;
118
+ this.labelFormatter = labelFormatter;
119
+ this.customFormatter = customFormatter;
120
+ this.exportHooks = exportHooks;
52
121
  }
53
122
  getExportConfig() {
54
123
  return {
124
+ mode: this.mode,
125
+ position: this.position,
126
+ barAnchorPosition: this.barAnchorPosition,
127
+ transition: this.transition,
55
128
  formatter: this.formatter,
56
129
  labelFormatter: this.labelFormatter,
57
130
  customFormatter: this.customFormatter,
@@ -72,19 +145,10 @@ export class Tooltip {
72
145
  .attr('class', 'chart-tooltip')
73
146
  .attr('id', this.id)
74
147
  : existingTooltip;
75
- tooltip
76
- .style('position', 'absolute')
77
- .style('visibility', 'hidden')
78
- .style('background-color', 'white')
79
- .style('border', '1px solid #ddd')
80
- .style('border-radius', '4px')
81
- .style('padding', '8px')
82
- .style('box-shadow', '0 2px 4px rgba(0,0,0,0.1)')
83
- .style('font-family', theme.axis.fontFamily)
84
- .style('font-size', '12px')
85
- .style('pointer-events', 'none')
86
- .style('z-index', '1000');
148
+ this.removeSplitTooltips();
149
+ this.applyTooltipStylesIfNeeded(tooltip, theme);
87
150
  this.tooltipDiv = tooltip;
151
+ this.hideTooltipSelection(tooltip);
88
152
  }
89
153
  attachToArea(svg, data, series, xKey, x, y, theme, plotArea, parseValue, isHorizontal = false, categoryScaleType = 'band', resolveSeriesValue = (targetSeries, dataPoint) => {
90
154
  const rawValue = dataPoint[targetSeries.dataKey];
@@ -93,9 +157,14 @@ export class Tooltip {
93
157
  }
94
158
  return parseValue(rawValue);
95
159
  }) {
96
- if (!this.tooltipDiv) {
160
+ if (!this.tooltipDiv || data.length === 0) {
97
161
  return;
98
162
  }
163
+ const tooltip = this.tooltipDiv;
164
+ const formatter = this.formatter;
165
+ const labelFormatter = this.labelFormatter;
166
+ const customFormatter = this.customFormatter;
167
+ const tooltipMode = this.mode;
99
168
  const normalizeFormatterValue = (value) => {
100
169
  if (value === null ||
101
170
  value === undefined ||
@@ -107,13 +176,6 @@ export class Tooltip {
107
176
  }
108
177
  return String(value);
109
178
  };
110
- if (data.length === 0) {
111
- return;
112
- }
113
- const tooltip = this.tooltipDiv;
114
- const formatter = this.formatter;
115
- const labelFormatter = this.labelFormatter;
116
- const customFormatter = this.customFormatter;
117
179
  const getCategoryScaleValue = (value, scaleType) => {
118
180
  switch (scaleType) {
119
181
  case 'band':
@@ -128,13 +190,11 @@ export class Tooltip {
128
190
  }
129
191
  };
130
192
  const getXPosition = (dataPoint) => {
131
- const xValue = dataPoint[xKey];
132
- const scaled = x(getCategoryScaleValue(xValue, categoryScaleType));
193
+ const scaled = x(getCategoryScaleValue(dataPoint[xKey], categoryScaleType));
133
194
  return (scaled || 0) + (x.bandwidth ? x.bandwidth() / 2 : 0);
134
195
  };
135
196
  const getYPosition = (dataPoint) => {
136
- const yValue = dataPoint[xKey];
137
- const scaled = y(getCategoryScaleValue(yValue, categoryScaleType));
197
+ const scaled = y(getCategoryScaleValue(dataPoint[xKey], categoryScaleType));
138
198
  return (scaled || 0) + (y.bandwidth ? y.bandwidth() / 2 : 0);
139
199
  };
140
200
  const stripHtml = (content) => {
@@ -144,46 +204,36 @@ export class Tooltip {
144
204
  .replace(/\s+/g, ' ')
145
205
  .trim();
146
206
  };
147
- const buildTooltipContent = (dataPoint) => {
148
- if (customFormatter) {
149
- return customFormatter(dataPoint, series);
150
- }
207
+ const buildTooltipLabel = (dataPoint) => {
151
208
  const labelValue = dataPoint[xKey];
152
- const label = labelFormatter
209
+ return labelFormatter
153
210
  ? labelFormatter(String(labelValue), dataPoint)
154
211
  : String(labelValue);
155
- let content = `<strong>${label}</strong><br/>`;
156
- series.forEach((s) => {
157
- const value = dataPoint[s.dataKey];
158
- if (formatter) {
159
- content +=
160
- formatter(s.dataKey, normalizeFormatterValue(value), dataPoint) + '<br/>';
161
- return;
162
- }
163
- content += `${s.dataKey}: ${value}<br/>`;
164
- });
165
- return content;
166
212
  };
167
- const buildAccessibleLabel = (dataPoint) => {
213
+ const buildTooltipRow = (dataPoint, currentSeries) => {
214
+ const value = dataPoint[currentSeries.dataKey];
215
+ if (formatter) {
216
+ return formatter(currentSeries.dataKey, normalizeFormatterValue(value), dataPoint);
217
+ }
218
+ return `${currentSeries.dataKey}: ${value}`;
219
+ };
220
+ const buildSharedTooltipContent = (dataPoint) => {
168
221
  if (customFormatter) {
169
- return stripHtml(customFormatter(dataPoint, series));
222
+ return customFormatter(dataPoint, series);
170
223
  }
171
- const labelValue = dataPoint[xKey];
172
- const label = labelFormatter
173
- ? labelFormatter(String(labelValue), dataPoint)
174
- : String(labelValue);
175
- const parts = [label];
176
- series.forEach((s) => {
177
- const value = dataPoint[s.dataKey];
178
- if (formatter) {
179
- parts.push(stripHtml(formatter(s.dataKey, normalizeFormatterValue(value), dataPoint)));
180
- return;
181
- }
182
- parts.push(`${s.dataKey}: ${value === null || value === undefined
183
- ? 'no data'
184
- : value}`);
185
- });
186
- return parts.join('. ');
224
+ const label = buildTooltipLabel(dataPoint);
225
+ const rows = series.map((currentSeries) => buildTooltipRow(dataPoint, currentSeries));
226
+ return `<strong>${label}</strong><br/>${rows.join('<br/>')}`;
227
+ };
228
+ const buildSplitTooltipContent = (dataPoint, currentSeries) => {
229
+ if (customFormatter) {
230
+ return customFormatter(dataPoint, [currentSeries]);
231
+ }
232
+ const label = buildTooltipLabel(dataPoint);
233
+ return `<strong>${label}</strong><br/>${buildTooltipRow(dataPoint, currentSeries)}`;
234
+ };
235
+ const buildAccessibleLabel = (dataPoint) => {
236
+ return stripHtml(buildSharedTooltipContent(dataPoint));
187
237
  };
188
238
  const isTooltipFocusTarget = (element) => {
189
239
  return (element instanceof SVGElement &&
@@ -203,7 +253,6 @@ export class Tooltip {
203
253
  }
204
254
  return closestIndex;
205
255
  };
206
- // Create overlay rect for mouse tracking using plot area bounds
207
256
  const overlay = svg
208
257
  .append('rect')
209
258
  .attr('class', 'tooltip-overlay')
@@ -214,14 +263,20 @@ export class Tooltip {
214
263
  .attr('aria-hidden', 'true')
215
264
  .style('fill', 'none')
216
265
  .style('pointer-events', 'all');
217
- const pointSeries = series.filter((s) => s.type === 'line' || s.type === 'area' || s.type === 'scatter');
218
- const barSeries = series.filter((s) => s.type === 'bar');
266
+ const pointSeries = series.filter((currentSeries) => {
267
+ return (currentSeries.type === 'line' ||
268
+ currentSeries.type === 'area' ||
269
+ currentSeries.type === 'scatter');
270
+ });
271
+ const barSeries = series.filter((currentSeries) => {
272
+ return currentSeries.type === 'bar';
273
+ });
219
274
  const hasBarSeries = barSeries.length > 0;
220
- const focusCircles = pointSeries.map((s) => {
221
- const seriesColor = getSeriesColor(s);
275
+ const focusCircles = pointSeries.map((currentSeries) => {
276
+ const seriesColor = getSeriesColor(currentSeries);
222
277
  return svg
223
278
  .append('circle')
224
- .attr('class', `focus-circle-${sanitizeForCSS(s.dataKey)}`)
279
+ .attr('class', `focus-circle-${sanitizeForCSS(currentSeries.dataKey)}`)
225
280
  .attr('r', theme.line.point.size + 1)
226
281
  .attr('fill', theme.line.point.color || seriesColor)
227
282
  .attr('stroke', theme.line.point.strokeColor || seriesColor)
@@ -235,48 +290,51 @@ export class Tooltip {
235
290
  if (!hasBarSeries) {
236
291
  return;
237
292
  }
238
- barSeries.forEach((s) => {
239
- const sanitizedKey = sanitizeForCSS(s.dataKey);
240
- svg.selectAll(`.bar-${sanitizedKey}`).style('opacity', 1);
293
+ barSeries.forEach((currentSeries) => {
294
+ svg.selectAll(`.bar-${sanitizeForCSS(currentSeries.dataKey)}`).style('opacity', 1);
241
295
  });
242
296
  };
243
- const showTooltipAtIndex = (closestIndex) => {
297
+ const updateVisualStateAtIndex = (closestIndex) => {
244
298
  const dataPoint = data[closestIndex];
245
299
  const dataPointPosition = dataPointPositions[closestIndex];
246
- pointSeries.forEach((s, i) => {
247
- const value = resolveSeriesValue(s, dataPoint, closestIndex);
300
+ pointSeries.forEach((currentSeries, seriesIndex) => {
301
+ const value = resolveSeriesValue(currentSeries, dataPoint, closestIndex);
248
302
  if (!Number.isFinite(value)) {
249
- focusCircles[i].style('opacity', 0);
303
+ focusCircles[seriesIndex].style('opacity', 0);
250
304
  return;
251
305
  }
252
306
  if (isHorizontal) {
253
- focusCircles[i]
307
+ focusCircles[seriesIndex]
254
308
  .attr('cx', x(value) ?? 0)
255
309
  .attr('cy', dataPointPosition)
256
310
  .style('opacity', 1);
257
311
  return;
258
312
  }
259
- focusCircles[i]
313
+ focusCircles[seriesIndex]
260
314
  .attr('cx', dataPointPosition)
261
315
  .attr('cy', y(value) ?? 0)
262
316
  .style('opacity', 1);
263
317
  });
264
- if (hasBarSeries) {
265
- barSeries.forEach((s) => {
266
- const sanitizedKey = sanitizeForCSS(s.dataKey);
267
- svg.selectAll(`.bar-${sanitizedKey}`).style('opacity', (_, i) => (i === closestIndex ? 1 : 0.5));
268
- });
318
+ if (!hasBarSeries) {
319
+ return;
320
+ }
321
+ barSeries.forEach((currentSeries) => {
322
+ svg.selectAll(`.bar-${sanitizeForCSS(currentSeries.dataKey)}`).style('opacity', (_, index) => index === closestIndex ? 1 : 0.5);
323
+ });
324
+ };
325
+ const showSharedTooltipAtIndex = (closestIndex) => {
326
+ const dataPoint = data[closestIndex];
327
+ const dataPointPosition = dataPointPositions[closestIndex];
328
+ updateVisualStateAtIndex(closestIndex);
329
+ this.hideSplitTooltips();
330
+ const content = buildSharedTooltipContent(dataPoint);
331
+ const measuredTooltip = this.measureTooltip(tooltip, content);
332
+ if (!measuredTooltip) {
333
+ this.hideTooltipSelection(tooltip);
334
+ return;
269
335
  }
270
- tooltip
271
- .style('visibility', 'visible')
272
- .html(buildTooltipContent(dataPoint));
273
- const tooltipNode = tooltip.node();
274
- const tooltipRect = tooltipNode.getBoundingClientRect();
275
- const tooltipWidth = tooltipRect.width;
276
- const tooltipHeight = tooltipRect.height;
277
336
  const svgRect = svg.node().getBoundingClientRect();
278
- const offsetX = 12;
279
- const values = series.map((s) => resolveSeriesValue(s, dataPoint, closestIndex));
337
+ const values = series.map((currentSeries) => resolveSeriesValue(currentSeries, dataPoint, closestIndex));
280
338
  const finiteValues = values.filter((value) => Number.isFinite(value));
281
339
  const minValue = finiteValues.length
282
340
  ? Math.min(...finiteValues)
@@ -284,58 +342,171 @@ export class Tooltip {
284
342
  const maxValue = finiteValues.length
285
343
  ? Math.max(...finiteValues)
286
344
  : 0;
287
- let tooltipX;
288
- let tooltipY;
345
+ const sharedAnchor = {
346
+ left: 0,
347
+ right: 0,
348
+ top: 0,
349
+ bottom: 0,
350
+ centerX: 0,
351
+ centerY: 0,
352
+ };
289
353
  if (isHorizontal) {
290
354
  const minX = x(minValue);
291
355
  const maxX = x(maxValue);
292
- const midX = (minX + maxX) / 2;
293
- tooltipX = svgRect.left + window.scrollX + midX + offsetX;
294
- tooltipY =
295
- svgRect.top +
296
- window.scrollY +
297
- dataPointPosition -
298
- tooltipHeight / 2;
356
+ const centerX = svgRect.left + window.scrollX + (minX + maxX) / 2;
357
+ const centerY = svgRect.top + window.scrollY + dataPointPosition;
358
+ sharedAnchor.left = centerX;
359
+ sharedAnchor.right = centerX;
360
+ sharedAnchor.top = centerY;
361
+ sharedAnchor.bottom = centerY;
362
+ sharedAnchor.centerX = centerX;
363
+ sharedAnchor.centerY = centerY;
299
364
  }
300
365
  else {
301
366
  const minY = y(maxValue);
302
367
  const maxY = y(minValue);
303
- const midY = (minY + maxY) / 2;
304
- tooltipX =
305
- svgRect.left + window.scrollX + dataPointPosition + offsetX;
306
- tooltipY =
307
- svgRect.top + window.scrollY + midY - tooltipHeight / 2;
308
- }
309
- const viewportWidth = window.innerWidth;
310
- if (tooltipX + tooltipWidth > viewportWidth - 10) {
311
- if (isHorizontal) {
312
- const minX = x(minValue);
313
- tooltipX =
314
- svgRect.left +
315
- window.scrollX +
316
- minX -
317
- tooltipWidth -
318
- offsetX;
368
+ const centerX = svgRect.left + window.scrollX + dataPointPosition;
369
+ const topY = svgRect.top + window.scrollY + minY;
370
+ const bottomY = svgRect.top + window.scrollY + maxY;
371
+ sharedAnchor.left = centerX;
372
+ sharedAnchor.right = centerX;
373
+ sharedAnchor.top = topY;
374
+ sharedAnchor.bottom = bottomY;
375
+ sharedAnchor.centerX = centerX;
376
+ sharedAnchor.centerY = (topY + bottomY) / 2;
377
+ }
378
+ const target = this.resolveSharedTooltipTarget(sharedAnchor);
379
+ const arrowEdge = this.resolveTooltipArrowEdge(sharedAnchor, target, measuredTooltip.width, measuredTooltip.height);
380
+ const position = this.getAnchoredTooltipPosition(sharedAnchor, target, measuredTooltip.width, measuredTooltip.height, arrowEdge);
381
+ if (!position) {
382
+ this.hideTooltipSelection(tooltip);
383
+ return;
384
+ }
385
+ this.renderTooltipWithoutConnector(tooltip, position.left, position.top);
386
+ };
387
+ const getSeriesTooltipAnchor = (currentSeries, dataPoint, index) => {
388
+ const value = resolveSeriesValue(currentSeries, dataPoint, index);
389
+ if (!Number.isFinite(value)) {
390
+ return null;
391
+ }
392
+ const svgNode = svg.node();
393
+ if (!svgNode) {
394
+ return null;
395
+ }
396
+ if (currentSeries.type === 'bar') {
397
+ return this.resolveBarTooltipAnchor(svgNode, currentSeries.dataKey, index);
398
+ }
399
+ const categoryPosition = dataPointPositions[index];
400
+ const valuePosition = isHorizontal
401
+ ? (x(value) ?? 0)
402
+ : (y(value) ?? 0);
403
+ return this.resolvePointTooltipAnchor(svgNode, isHorizontal, categoryPosition, valuePosition);
404
+ };
405
+ const showSplitTooltipAtIndex = (closestIndex) => {
406
+ const dataPoint = data[closestIndex];
407
+ updateVisualStateAtIndex(closestIndex);
408
+ this.hideTooltipSelection(tooltip);
409
+ this.hideSplitTooltips();
410
+ const layouts = [];
411
+ series.forEach((currentSeries, seriesIndex) => {
412
+ const rawValue = dataPoint[currentSeries.dataKey];
413
+ if (rawValue === null || rawValue === undefined) {
414
+ return;
415
+ }
416
+ const anchor = getSeriesTooltipAnchor(currentSeries, dataPoint, closestIndex);
417
+ if (!anchor) {
418
+ return;
319
419
  }
320
- else {
321
- tooltipX =
322
- svgRect.left +
323
- window.scrollX +
324
- dataPointPosition -
325
- tooltipWidth -
326
- offsetX;
420
+ const splitTooltip = this.getSplitTooltip(seriesIndex, theme);
421
+ const content = buildSplitTooltipContent(dataPoint, currentSeries);
422
+ const measuredTooltip = this.measureTooltip(splitTooltip, content);
423
+ if (!measuredTooltip) {
424
+ return;
327
425
  }
426
+ const target = this.resolveSplitTooltipTarget(currentSeries, anchor);
427
+ const arrowEdge = this.resolveTooltipArrowEdge(anchor, target, measuredTooltip.width, measuredTooltip.height);
428
+ const position = this.getAnchoredTooltipPosition(anchor, target, measuredTooltip.width, measuredTooltip.height, arrowEdge);
429
+ if (!position) {
430
+ return;
431
+ }
432
+ layouts.push({
433
+ div: splitTooltip,
434
+ anchor,
435
+ width: measuredTooltip.width,
436
+ height: measuredTooltip.height,
437
+ left: position.left,
438
+ top: position.top,
439
+ arrowEdge,
440
+ targetX: target.x,
441
+ targetY: target.y,
442
+ });
443
+ });
444
+ if (layouts.length === 0) {
445
+ return;
328
446
  }
329
- tooltipX = Math.max(10, tooltipX);
330
- tooltipY = Math.max(10, Math.min(tooltipY, window.innerHeight + window.scrollY - tooltipHeight - 10));
331
- tooltip
332
- .style('left', `${tooltipX}px`)
333
- .style('top', `${tooltipY}px`);
447
+ this.resolveSplitTooltipCollisions(layouts, 'vertical', this.getOppositeVerticalArrowEdge);
448
+ this.resolveSplitTooltipCollisions(layouts, 'side', this.getOppositeSideArrowEdge);
449
+ this.resolveSplitTooltipPositions(layouts);
450
+ layouts.forEach((layout) => {
451
+ this.renderTooltipWithConnector(layout.div, layout.arrowEdge, layout.left, layout.top, layout.width, layout.height, layout.targetX, layout.targetY);
452
+ });
334
453
  };
335
454
  const hideTooltip = () => {
336
- tooltip.style('visibility', 'hidden');
455
+ this.hideTooltipSelection(tooltip);
456
+ this.hideSplitTooltips();
337
457
  clearVisualState();
338
458
  };
459
+ let pendingTooltipFrame = null;
460
+ let pendingTooltipIndex = null;
461
+ const tooltipSvgNode = svg.node();
462
+ const requestFrame = (callback) => {
463
+ if (typeof window.requestAnimationFrame === 'function') {
464
+ return window.requestAnimationFrame(callback);
465
+ }
466
+ return window.setTimeout(() => {
467
+ callback(window.performance.now());
468
+ }, 16);
469
+ };
470
+ const cancelFrame = (frameId) => {
471
+ if (typeof window.cancelAnimationFrame === 'function') {
472
+ window.cancelAnimationFrame(frameId);
473
+ return;
474
+ }
475
+ window.clearTimeout(frameId);
476
+ };
477
+ const renderTooltipAtIndex = (index) => {
478
+ if (tooltipMode === 'split') {
479
+ showSplitTooltipAtIndex(index);
480
+ return;
481
+ }
482
+ showSharedTooltipAtIndex(index);
483
+ };
484
+ const cancelPendingTooltipRender = () => {
485
+ if (pendingTooltipFrame === null) {
486
+ return;
487
+ }
488
+ cancelFrame(pendingTooltipFrame);
489
+ pendingTooltipFrame = null;
490
+ pendingTooltipIndex = null;
491
+ };
492
+ const requestTooltipRenderAtIndex = (index) => {
493
+ pendingTooltipIndex = index;
494
+ if (pendingTooltipFrame !== null) {
495
+ return;
496
+ }
497
+ pendingTooltipFrame = requestFrame(() => {
498
+ const nextIndex = pendingTooltipIndex;
499
+ pendingTooltipFrame = null;
500
+ pendingTooltipIndex = null;
501
+ if (nextIndex === null) {
502
+ return;
503
+ }
504
+ if (!tooltipSvgNode?.isConnected) {
505
+ return;
506
+ }
507
+ renderTooltipAtIndex(nextIndex);
508
+ });
509
+ };
339
510
  const getFocusTargetBounds = (index) => {
340
511
  if (isHorizontal) {
341
512
  if (y.bandwidth) {
@@ -393,12 +564,14 @@ export class Tooltip {
393
564
  overlay
394
565
  .on('mousemove', (event) => {
395
566
  const [mouseX, mouseY] = pointer(event, svg.node());
396
- showTooltipAtIndex(getClosestIndexFromPointer(mouseX, mouseY));
567
+ const closestIndex = getClosestIndexFromPointer(mouseX, mouseY);
568
+ requestTooltipRenderAtIndex(closestIndex);
397
569
  })
398
570
  .on('mouseout', () => {
399
571
  if (isTooltipFocusTarget(document.activeElement)) {
400
572
  return;
401
573
  }
574
+ cancelPendingTooltipRender();
402
575
  hideTooltip();
403
576
  });
404
577
  const focusTargets = svg
@@ -420,20 +593,38 @@ export class Tooltip {
420
593
  .attr('aria-label', (dataPoint) => buildAccessibleLabel(dataPoint))
421
594
  .style('pointer-events', 'none');
422
595
  const focusTargetNodes = focusTargets.nodes();
596
+ const getNextFocusTargetIndex = (currentIndex, key) => {
597
+ switch (key) {
598
+ case 'ArrowRight':
599
+ case 'ArrowDown':
600
+ return currentIndex + 1;
601
+ case 'ArrowLeft':
602
+ case 'ArrowUp':
603
+ return currentIndex - 1;
604
+ case 'Home':
605
+ return 0;
606
+ case 'End':
607
+ return focusTargetNodes.length - 1;
608
+ default:
609
+ return -1;
610
+ }
611
+ };
423
612
  focusTargets
424
613
  .on('focus', function () {
425
614
  const currentIndex = focusTargetNodes.indexOf(this);
426
615
  if (currentIndex === -1) {
427
616
  return;
428
617
  }
618
+ cancelPendingTooltipRender();
429
619
  select(this).attr('stroke', '#111827').attr('stroke-width', 2);
430
- showTooltipAtIndex(currentIndex);
620
+ renderTooltipAtIndex(currentIndex);
431
621
  })
432
622
  .on('blur', function (event) {
433
623
  select(this).attr('stroke', 'none').attr('stroke-width', 0);
434
624
  if (isTooltipFocusTarget(event.relatedTarget)) {
435
625
  return;
436
626
  }
627
+ cancelPendingTooltipRender();
437
628
  hideTooltip();
438
629
  })
439
630
  .on('keydown', function (event) {
@@ -441,41 +632,659 @@ export class Tooltip {
441
632
  if (currentIndex === -1) {
442
633
  return;
443
634
  }
444
- let nextIndex = null;
445
- switch (event.key) {
446
- case 'ArrowRight':
447
- case 'ArrowDown':
448
- nextIndex = currentIndex + 1;
449
- break;
450
- case 'ArrowLeft':
451
- case 'ArrowUp':
452
- nextIndex = currentIndex - 1;
453
- break;
454
- case 'Home':
455
- nextIndex = 0;
456
- break;
457
- case 'End':
458
- nextIndex = focusTargetNodes.length - 1;
459
- break;
460
- default:
461
- return;
462
- }
463
- if (nextIndex === null ||
464
- nextIndex < 0 ||
465
- nextIndex >= focusTargetNodes.length) {
635
+ const nextIndex = getNextFocusTargetIndex(currentIndex, event.key);
636
+ if (nextIndex < 0 || nextIndex >= focusTargetNodes.length) {
466
637
  return;
467
638
  }
468
639
  event.preventDefault();
469
640
  focusTargetNodes[nextIndex].focus();
470
641
  });
471
642
  }
643
+ setContent(content) {
644
+ if (!this.tooltipDiv) {
645
+ return;
646
+ }
647
+ this.setTooltipMarkup(this.tooltipDiv, content);
648
+ }
649
+ getBounds() {
650
+ const node = this.tooltipDiv?.node();
651
+ if (!node) {
652
+ return null;
653
+ }
654
+ return node.getBoundingClientRect();
655
+ }
656
+ showAt(left, top) {
657
+ if (!this.tooltipDiv) {
658
+ return;
659
+ }
660
+ if (!Number.isFinite(left) || !Number.isFinite(top)) {
661
+ this.hide();
662
+ return;
663
+ }
664
+ this.showTooltipAt(this.tooltipDiv, left, top);
665
+ }
666
+ hide() {
667
+ const tooltip = this.tooltipDiv ?? select(`#${this.id}`);
668
+ if (!tooltip.empty()) {
669
+ this.hideTooltipSelection(tooltip);
670
+ }
671
+ this.hideSplitTooltips();
672
+ }
472
673
  cleanup() {
674
+ this.removeRootTooltip();
675
+ this.removeSplitTooltips();
676
+ this.tooltipDiv = null;
677
+ }
678
+ applyTooltipStylesIfNeeded(tooltip, theme) {
679
+ const node = tooltip.node();
680
+ if (!node) {
681
+ return;
682
+ }
683
+ const styleKey = this.getTooltipStyleKey(theme);
684
+ if (this.tooltipStyleKeys.get(node) === styleKey) {
685
+ this.tooltipTheme = theme.tooltip;
686
+ return;
687
+ }
688
+ this.tooltipStyleKeys.set(node, styleKey);
689
+ this.writeTooltipStyles(tooltip, theme);
690
+ }
691
+ getTooltipStyleKey(theme) {
692
+ return [
693
+ theme.tooltip.background,
694
+ theme.tooltip.border,
695
+ theme.tooltip.color,
696
+ theme.tooltip.fontFamily,
697
+ theme.tooltip.fontSize,
698
+ theme.tooltip.fontWeight,
699
+ this.transition.show,
700
+ this.transition.duration,
701
+ this.transition.easing,
702
+ ].join('|');
703
+ }
704
+ writeTooltipStyles(tooltip, theme) {
705
+ this.tooltipTheme = theme.tooltip;
706
+ tooltip
707
+ .style('position', 'absolute')
708
+ .style('background-color', theme.tooltip.background)
709
+ .style('border', `1px solid ${theme.tooltip.border}`)
710
+ .style('border-radius', '4px')
711
+ .style('padding', '8px')
712
+ .style('box-shadow', '0 2px 4px rgba(0,0,0,0.1)')
713
+ .style('color', theme.tooltip.color)
714
+ .style('font-family', theme.tooltip.fontFamily)
715
+ .style('font-size', `${theme.tooltip.fontSize}px`)
716
+ .style('font-weight', theme.tooltip.fontWeight)
717
+ .style('overflow', 'visible')
718
+ .style('isolation', 'isolate')
719
+ .style('pointer-events', 'none')
720
+ .style('z-index', '1000');
721
+ if (this.transition.show) {
722
+ tooltip
723
+ .style('transition', `opacity ${this.transition.duration}ms ${this.transition.easing}, transform ${this.transition.duration}ms ${this.transition.easing}`)
724
+ .style('will-change', 'opacity, transform');
725
+ return;
726
+ }
727
+ tooltip
728
+ .style('opacity', null)
729
+ .style('transform', null)
730
+ .style('transition', null)
731
+ .style('will-change', null);
732
+ }
733
+ measureTooltip(tooltip, content) {
734
+ this.setTooltipMarkup(tooltip, content);
735
+ tooltip.style('left', '-9999px').style('top', '-9999px');
736
+ this.hideTooltipSelection(tooltip);
737
+ const tooltipNode = tooltip.node();
738
+ if (!tooltipNode) {
739
+ return null;
740
+ }
741
+ const tooltipRect = tooltipNode.getBoundingClientRect();
742
+ if (!Number.isFinite(tooltipRect.width) ||
743
+ !Number.isFinite(tooltipRect.height)) {
744
+ return null;
745
+ }
746
+ return {
747
+ width: tooltipRect.width,
748
+ height: tooltipRect.height,
749
+ };
750
+ }
751
+ renderTooltipWithConnector(tooltip, arrowEdge, left, top, tooltipWidth, tooltipHeight, targetX, targetY) {
752
+ if (!Number.isFinite(left) ||
753
+ !Number.isFinite(top) ||
754
+ !Number.isFinite(targetX) ||
755
+ !Number.isFinite(targetY)) {
756
+ this.hideTooltipSelection(tooltip);
757
+ return;
758
+ }
759
+ const connectorLayout = this.resolveTooltipConnectorLayout(arrowEdge, left, top, tooltipWidth, tooltipHeight, targetX, targetY);
760
+ if (!connectorLayout) {
761
+ this.hideTooltipSelection(tooltip);
762
+ return;
763
+ }
764
+ this.appendTooltipConnector(tooltip, connectorLayout);
765
+ this.showTooltipAt(tooltip, left, top);
766
+ }
767
+ renderTooltipWithoutConnector(tooltip, left, top) {
768
+ if (!Number.isFinite(left) || !Number.isFinite(top)) {
769
+ this.hideTooltipSelection(tooltip);
770
+ return;
771
+ }
772
+ this.showTooltipAt(tooltip, left, top);
773
+ }
774
+ showTooltipAt(tooltip, left, top) {
775
+ tooltip.style('left', `${left}px`).style('top', `${top}px`);
776
+ this.showTooltipSelection(tooltip);
777
+ }
778
+ showTooltipSelection(tooltip) {
779
+ tooltip.style('visibility', 'visible');
780
+ if (!this.transition.show) {
781
+ return;
782
+ }
783
+ tooltip
784
+ .style('opacity', '1')
785
+ .style('transform', TOOLTIP_VISIBLE_TRANSFORM);
786
+ }
787
+ hideTooltipSelection(tooltip) {
788
+ const node = tooltip.node();
789
+ if (!node) {
790
+ return;
791
+ }
792
+ this.hideTooltipElement(node);
793
+ }
794
+ hideTooltipElement(node) {
795
+ if (!this.transition.show) {
796
+ node.style.visibility = 'hidden';
797
+ return;
798
+ }
799
+ node.style.visibility = 'visible';
800
+ node.style.opacity = '0';
801
+ node.style.transform = TOOLTIP_HIDDEN_TRANSFORM;
802
+ }
803
+ setTooltipMarkup(tooltip, content) {
804
+ tooltip.html(`<div data-chart-tooltip-body="true">${content}</div>`);
805
+ const body = tooltip.select('[data-chart-tooltip-body]');
806
+ if (body.empty()) {
807
+ return;
808
+ }
809
+ body.style('position', 'relative').style('z-index', '1');
810
+ }
811
+ appendTooltipConnector(tooltip, connectorLayout) {
812
+ const tooltipBackground = this.tooltipTheme?.background ?? '#ffffff';
813
+ const tooltipBorder = this.tooltipTheme?.border ?? '#dddddd';
814
+ const connector = tooltip
815
+ .append('svg')
816
+ .attr('data-chart-tooltip-connector', 'true')
817
+ .attr('data-chart-tooltip-arrow-edge', connectorLayout.arrowEdge)
818
+ .attr('aria-hidden', 'true')
819
+ .attr('width', connectorLayout.width)
820
+ .attr('height', connectorLayout.height)
821
+ .attr('viewBox', `0 0 ${connectorLayout.width} ${connectorLayout.height}`)
822
+ .style('position', 'absolute')
823
+ .style('left', `${connectorLayout.left}px`)
824
+ .style('top', `${connectorLayout.top}px`)
825
+ .style('pointer-events', 'none')
826
+ .style('overflow', 'visible')
827
+ .style('z-index', '0');
828
+ connector
829
+ .append('path')
830
+ .attr('data-chart-tooltip-connector-path', 'true')
831
+ .attr('d', connectorLayout.path)
832
+ .attr('fill', 'none')
833
+ .attr('stroke', tooltipBorder)
834
+ .attr('stroke-width', 1.25)
835
+ .attr('stroke-linecap', 'round')
836
+ .attr('stroke-linejoin', 'round');
837
+ connector
838
+ .append('path')
839
+ .attr('data-chart-tooltip-arrow', 'true')
840
+ .attr('d', connectorLayout.arrowPath)
841
+ .attr('fill', tooltipBackground)
842
+ .attr('stroke', 'none');
843
+ connector
844
+ .append('path')
845
+ .attr('data-chart-tooltip-arrow-base-mask', 'true')
846
+ .attr('d', connectorLayout.arrowBaseMaskPath)
847
+ .attr('fill', 'none')
848
+ .attr('stroke', tooltipBackground)
849
+ .attr('stroke-width', 2)
850
+ .attr('stroke-linecap', 'butt');
851
+ connector
852
+ .append('path')
853
+ .attr('data-chart-tooltip-arrow-border', 'true')
854
+ .attr('d', connectorLayout.arrowBorderPath)
855
+ .attr('fill', 'none')
856
+ .attr('stroke', tooltipBorder)
857
+ .attr('stroke-width', 1)
858
+ .attr('stroke-linecap', 'round')
859
+ .attr('stroke-linejoin', 'round');
860
+ }
861
+ resolveBarTooltipAnchor(svgNode, dataKey, index) {
862
+ const barNode = svgNode.querySelector(`.bar-${sanitizeForCSS(dataKey)}[data-index="${index}"]`);
863
+ if (!barNode) {
864
+ return null;
865
+ }
866
+ const rect = barNode.getBoundingClientRect();
867
+ if (rect.width > 0 || rect.height > 0) {
868
+ return {
869
+ left: rect.left + window.scrollX,
870
+ right: rect.right + window.scrollX,
871
+ top: rect.top + window.scrollY,
872
+ bottom: rect.bottom + window.scrollY,
873
+ centerX: rect.left + window.scrollX + rect.width / 2,
874
+ centerY: rect.top + window.scrollY + rect.height / 2,
875
+ };
876
+ }
877
+ const svgRect = svgNode.getBoundingClientRect();
878
+ const xValue = Number(barNode.getAttribute('x') ?? '0');
879
+ const yValue = Number(barNode.getAttribute('y') ?? '0');
880
+ const widthValue = Number(barNode.getAttribute('width') ?? '0');
881
+ const heightValue = Number(barNode.getAttribute('height') ?? '0');
882
+ const left = svgRect.left + window.scrollX + xValue;
883
+ const top = svgRect.top + window.scrollY + yValue;
884
+ const right = left + widthValue;
885
+ const bottom = top + heightValue;
886
+ return {
887
+ left,
888
+ right,
889
+ top,
890
+ bottom,
891
+ centerX: left + widthValue / 2,
892
+ centerY: top + heightValue / 2,
893
+ };
894
+ }
895
+ resolvePointTooltipAnchor(svgNode, isHorizontal, categoryPosition, valuePosition) {
896
+ if (!Number.isFinite(categoryPosition) ||
897
+ !Number.isFinite(valuePosition)) {
898
+ return null;
899
+ }
900
+ const svgRect = svgNode.getBoundingClientRect();
901
+ const anchorX = svgRect.left +
902
+ window.scrollX +
903
+ (isHorizontal ? valuePosition : categoryPosition);
904
+ const anchorY = svgRect.top +
905
+ window.scrollY +
906
+ (isHorizontal ? categoryPosition : valuePosition);
907
+ if (!Number.isFinite(anchorX) || !Number.isFinite(anchorY)) {
908
+ return null;
909
+ }
910
+ return {
911
+ left: anchorX,
912
+ right: anchorX,
913
+ top: anchorY,
914
+ bottom: anchorY,
915
+ centerX: anchorX,
916
+ centerY: anchorY,
917
+ };
918
+ }
919
+ getSplitTooltip(index, theme) {
920
+ const tooltipId = `${this.splitTooltipOwner}-${index}`;
921
+ const existingTooltip = select(`#${tooltipId}`);
922
+ const tooltip = existingTooltip.empty()
923
+ ? select('body')
924
+ .append('div')
925
+ .attr('class', 'chart-tooltip chart-tooltip--split')
926
+ .attr('id', tooltipId)
927
+ .attr('data-chart-tooltip-owner', this.splitTooltipOwner)
928
+ .attr('data-chart-tooltip-index', String(index))
929
+ : existingTooltip;
930
+ this.applyTooltipStylesIfNeeded(tooltip, theme);
931
+ return tooltip;
932
+ }
933
+ hideSplitTooltips() {
934
+ document
935
+ .querySelectorAll(`[data-chart-tooltip-owner="${this.splitTooltipOwner}"]`)
936
+ .forEach((node) => {
937
+ this.hideTooltipElement(node);
938
+ });
939
+ }
940
+ removeSplitTooltips() {
941
+ document
942
+ .querySelectorAll(`[data-chart-tooltip-owner="${this.splitTooltipOwner}"]`)
943
+ .forEach((node) => {
944
+ node.remove();
945
+ });
946
+ }
947
+ removeRootTooltip() {
473
948
  const tooltip = this.tooltipDiv ?? select(`#${this.id}`);
474
- if (tooltip.empty()) {
475
- this.tooltipDiv = null;
949
+ if (!tooltip.empty()) {
950
+ tooltip.remove();
951
+ }
952
+ }
953
+ resolveTooltipArrowEdge(anchor, target, tooltipWidth, tooltipHeight) {
954
+ if (this.position === 'vertical') {
955
+ return this.resolveVerticalPlacementArrowEdge(target, tooltipHeight);
956
+ }
957
+ return this.resolveSidePlacementArrowEdge(anchor, tooltipWidth);
958
+ }
959
+ resolveSidePlacementArrowEdge(anchor, tooltipWidth) {
960
+ const viewportLeft = window.scrollX + TOOLTIP_VIEWPORT_PADDING_PX + TOOLTIP_OFFSET_PX;
961
+ const viewportRight = window.scrollX +
962
+ window.innerWidth -
963
+ TOOLTIP_VIEWPORT_PADDING_PX -
964
+ TOOLTIP_OFFSET_PX;
965
+ const availableRightSpace = viewportRight - anchor.right;
966
+ const availableLeftSpace = anchor.left - viewportLeft;
967
+ if (availableRightSpace >= tooltipWidth ||
968
+ availableRightSpace >= availableLeftSpace) {
969
+ return 'left';
970
+ }
971
+ return 'right';
972
+ }
973
+ resolveVerticalPlacementArrowEdge(target, tooltipHeight) {
974
+ const viewportTop = window.scrollY + TOOLTIP_VIEWPORT_PADDING_PX + TOOLTIP_OFFSET_PX;
975
+ const viewportBottom = window.scrollY +
976
+ window.innerHeight -
977
+ TOOLTIP_VIEWPORT_PADDING_PX -
978
+ TOOLTIP_OFFSET_PX;
979
+ const availableTopSpace = target.y - viewportTop;
980
+ const availableBottomSpace = viewportBottom - target.y;
981
+ if (availableTopSpace >= tooltipHeight ||
982
+ availableTopSpace >= availableBottomSpace) {
983
+ return 'bottom';
984
+ }
985
+ return 'top';
986
+ }
987
+ resolveSharedTooltipTarget(anchor) {
988
+ return {
989
+ x: anchor.centerX,
990
+ y: anchor.centerY,
991
+ };
992
+ }
993
+ resolveSplitTooltipTarget(currentSeries, anchor) {
994
+ if (currentSeries.type === 'bar') {
995
+ return {
996
+ x: anchor.centerX,
997
+ y: this.barAnchorPosition === 'top'
998
+ ? anchor.top
999
+ : anchor.centerY,
1000
+ };
1001
+ }
1002
+ return {
1003
+ x: anchor.centerX,
1004
+ y: anchor.centerY,
1005
+ };
1006
+ }
1007
+ getTooltipConnectorOffset(start, size, target) {
1008
+ const minOffset = TOOLTIP_CONNECTOR_INSET_PX;
1009
+ const maxOffset = Math.max(minOffset, size - minOffset);
1010
+ const preferredOffset = target - start;
1011
+ return Math.max(minOffset, Math.min(preferredOffset, maxOffset));
1012
+ }
1013
+ getAnchoredTooltipPosition(anchor, target, tooltipWidth, tooltipHeight, arrowEdge) {
1014
+ const minLeft = window.scrollX + TOOLTIP_VIEWPORT_PADDING_PX;
1015
+ const maxLeft = window.scrollX +
1016
+ window.innerWidth -
1017
+ tooltipWidth -
1018
+ TOOLTIP_VIEWPORT_PADDING_PX;
1019
+ const minTop = window.scrollY + TOOLTIP_VIEWPORT_PADDING_PX;
1020
+ const maxTop = window.scrollY +
1021
+ window.innerHeight -
1022
+ tooltipHeight -
1023
+ TOOLTIP_VIEWPORT_PADDING_PX;
1024
+ let left = target.x - tooltipWidth / 2;
1025
+ let top = target.y - tooltipHeight / 2;
1026
+ if (arrowEdge === 'left') {
1027
+ left = anchor.right + TOOLTIP_OFFSET_PX;
1028
+ }
1029
+ else if (arrowEdge === 'right') {
1030
+ left = anchor.left - tooltipWidth - TOOLTIP_OFFSET_PX;
1031
+ }
1032
+ else if (arrowEdge === 'bottom') {
1033
+ top = target.y - tooltipHeight - TOOLTIP_OFFSET_PX;
1034
+ }
1035
+ else {
1036
+ top = target.y + TOOLTIP_OFFSET_PX;
1037
+ }
1038
+ if (!Number.isFinite(left) || !Number.isFinite(top)) {
1039
+ return null;
1040
+ }
1041
+ return {
1042
+ left: Math.max(minLeft, Math.min(left, maxLeft)),
1043
+ top: Math.max(minTop, Math.min(top, maxTop)),
1044
+ };
1045
+ }
1046
+ resolveTooltipConnectorLayout(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY) {
1047
+ const localTargetX = targetX - tooltipLeft;
1048
+ const localTargetY = targetY - tooltipTop;
1049
+ if (!Number.isFinite(localTargetX) || !Number.isFinite(localTargetY)) {
1050
+ return null;
1051
+ }
1052
+ const boxArrowPosition = this.resolveTooltipBoxArrowPosition(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY);
1053
+ const arrow = this.resolveTooltipBoxArrow(arrowEdge, boxArrowPosition.x, boxArrowPosition.y);
1054
+ const minX = Math.min(arrow.tipX, arrow.baseStartX, arrow.baseEndX, localTargetX) - TOOLTIP_CONNECTOR_PADDING_PX;
1055
+ const maxX = Math.max(arrow.tipX, arrow.baseStartX, arrow.baseEndX, localTargetX) + TOOLTIP_CONNECTOR_PADDING_PX;
1056
+ const minY = Math.min(arrow.tipY, arrow.baseStartY, arrow.baseEndY, localTargetY) - TOOLTIP_CONNECTOR_PADDING_PX;
1057
+ const maxY = Math.max(arrow.tipY, arrow.baseStartY, arrow.baseEndY, localTargetY) + TOOLTIP_CONNECTOR_PADDING_PX;
1058
+ const width = Math.max(1, maxX - minX);
1059
+ const height = Math.max(1, maxY - minY);
1060
+ const boxX = boxArrowPosition.x - minX;
1061
+ const boxY = boxArrowPosition.y - minY;
1062
+ const startX = arrow.tipX - minX;
1063
+ const startY = arrow.tipY - minY;
1064
+ const arrowBaseStartX = arrow.baseStartX - minX;
1065
+ const arrowBaseStartY = arrow.baseStartY - minY;
1066
+ const arrowBaseEndX = arrow.baseEndX - minX;
1067
+ const arrowBaseEndY = arrow.baseEndY - minY;
1068
+ const endX = localTargetX - minX;
1069
+ const endY = localTargetY - minY;
1070
+ const connectorPath = this.resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY);
1071
+ if (!this.hasFiniteNumbers(width, height, boxX, boxY, startX, startY, arrowBaseStartX, arrowBaseStartY, arrowBaseEndX, arrowBaseEndY, endX, endY)) {
1072
+ return null;
1073
+ }
1074
+ return {
1075
+ left: minX,
1076
+ top: minY,
1077
+ arrowEdge,
1078
+ width,
1079
+ height,
1080
+ path: connectorPath,
1081
+ arrowPath: `M ${startX},${startY} L ${arrowBaseStartX},${arrowBaseStartY} L ${arrowBaseEndX},${arrowBaseEndY} Z`,
1082
+ arrowBaseMaskPath: this.resolveTooltipArrowBaseMaskPath(arrowEdge, boxX, boxY, arrowBaseStartX, arrowBaseStartY, arrowBaseEndX, arrowBaseEndY),
1083
+ arrowBorderPath: `M ${arrowBaseStartX},${arrowBaseStartY} L ${startX},${startY} L ${arrowBaseEndX},${arrowBaseEndY}`,
1084
+ };
1085
+ }
1086
+ resolveTooltipBoxArrowPosition(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY) {
1087
+ switch (arrowEdge) {
1088
+ case 'left':
1089
+ return {
1090
+ x: 0,
1091
+ y: this.getTooltipConnectorOffset(tooltipTop, tooltipHeight, targetY),
1092
+ };
1093
+ case 'right':
1094
+ return {
1095
+ x: tooltipWidth,
1096
+ y: this.getTooltipConnectorOffset(tooltipTop, tooltipHeight, targetY),
1097
+ };
1098
+ case 'top':
1099
+ return {
1100
+ x: this.getTooltipConnectorOffset(tooltipLeft, tooltipWidth, targetX),
1101
+ y: 0,
1102
+ };
1103
+ case 'bottom':
1104
+ return {
1105
+ x: this.getTooltipConnectorOffset(tooltipLeft, tooltipWidth, targetX),
1106
+ y: tooltipHeight,
1107
+ };
1108
+ }
1109
+ }
1110
+ resolveTooltipArrowBaseMaskPath(arrowEdge, boxX, boxY, startX, startY, endX, endY) {
1111
+ if (arrowEdge === 'top') {
1112
+ return `M ${startX - 1},${boxY + 1} L ${endX + 1},${boxY + 1}`;
1113
+ }
1114
+ if (arrowEdge === 'bottom') {
1115
+ return `M ${startX - 1},${boxY - 1} L ${endX + 1},${boxY - 1}`;
1116
+ }
1117
+ if (arrowEdge === 'left') {
1118
+ return `M ${boxX + 1},${startY - 1} L ${boxX + 1},${endY + 1}`;
1119
+ }
1120
+ return `M ${boxX - 1},${startY - 1} L ${boxX - 1},${endY + 1}`;
1121
+ }
1122
+ resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY) {
1123
+ if (arrowEdge === 'left' || arrowEdge === 'right') {
1124
+ if (Math.abs(endY - startY) <=
1125
+ TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX) {
1126
+ return '';
1127
+ }
1128
+ const elbowX = startX + (endX - startX) * TOOLTIP_CONNECTOR_ELBOW_RATIO;
1129
+ return `M ${startX},${startY} L ${elbowX},${startY} L ${endX},${endY}`;
1130
+ }
1131
+ if (Math.abs(endX - startX) <= TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX) {
1132
+ return '';
1133
+ }
1134
+ const elbowY = startY + (endY - startY) * TOOLTIP_CONNECTOR_ELBOW_RATIO;
1135
+ return `M ${startX},${startY} L ${startX},${elbowY} L ${endX},${endY}`;
1136
+ }
1137
+ resolveTooltipBoxArrow(arrowEdge, boxX, boxY) {
1138
+ if (arrowEdge === 'left' || arrowEdge === 'right') {
1139
+ const baseX = boxX;
1140
+ const tipX = arrowEdge === 'left'
1141
+ ? baseX - TOOLTIP_BOX_ARROW_LENGTH_PX
1142
+ : baseX + TOOLTIP_BOX_ARROW_LENGTH_PX;
1143
+ return {
1144
+ tipX,
1145
+ tipY: boxY,
1146
+ baseStartX: baseX,
1147
+ baseStartY: boxY - TOOLTIP_BOX_ARROW_HALF_HEIGHT_PX,
1148
+ baseEndX: baseX,
1149
+ baseEndY: boxY + TOOLTIP_BOX_ARROW_HALF_HEIGHT_PX,
1150
+ };
1151
+ }
1152
+ const baseY = boxY;
1153
+ const tipY = arrowEdge === 'top'
1154
+ ? baseY - TOOLTIP_BOX_ARROW_LENGTH_PX
1155
+ : baseY + TOOLTIP_BOX_ARROW_LENGTH_PX;
1156
+ return {
1157
+ tipX: boxX,
1158
+ tipY,
1159
+ baseStartX: boxX - TOOLTIP_BOX_ARROW_HALF_HEIGHT_PX,
1160
+ baseStartY: baseY,
1161
+ baseEndX: boxX + TOOLTIP_BOX_ARROW_HALF_HEIGHT_PX,
1162
+ baseEndY: baseY,
1163
+ };
1164
+ }
1165
+ hasFiniteNumbers(...values) {
1166
+ return values.every((value) => Number.isFinite(value));
1167
+ }
1168
+ resolveSplitTooltipCollisions(layouts, position, getOppositeArrowEdge) {
1169
+ if (this.position !== position) {
476
1170
  return;
477
1171
  }
478
- tooltip.style('visibility', 'hidden');
479
- this.tooltipDiv = null;
1172
+ const placedLayouts = [];
1173
+ const orderedLayouts = [...layouts].sort((a, b) => a.targetY - b.targetY);
1174
+ orderedLayouts.forEach((layout) => {
1175
+ this.flipTooltipIfItReducesCollisions(layout, placedLayouts, getOppositeArrowEdge);
1176
+ placedLayouts.push(layout);
1177
+ });
1178
+ }
1179
+ getOppositeSideArrowEdge(arrowEdge) {
1180
+ if (arrowEdge === 'left') {
1181
+ return 'right';
1182
+ }
1183
+ if (arrowEdge === 'right') {
1184
+ return 'left';
1185
+ }
1186
+ return null;
1187
+ }
1188
+ getOppositeVerticalArrowEdge(arrowEdge) {
1189
+ if (arrowEdge === 'top') {
1190
+ return 'bottom';
1191
+ }
1192
+ if (arrowEdge === 'bottom') {
1193
+ return 'top';
1194
+ }
1195
+ return null;
1196
+ }
1197
+ flipTooltipIfItReducesCollisions(layout, placedLayouts, getOppositeArrowEdge) {
1198
+ const currentCollisions = this.countSplitTooltipCollisions(layout, placedLayouts);
1199
+ if (currentCollisions === 0) {
1200
+ return;
1201
+ }
1202
+ const flippedArrowEdge = getOppositeArrowEdge(layout.arrowEdge);
1203
+ if (!flippedArrowEdge) {
1204
+ return;
1205
+ }
1206
+ const flippedPosition = this.getAnchoredTooltipPosition(layout.anchor, { x: layout.targetX, y: layout.targetY }, layout.width, layout.height, flippedArrowEdge);
1207
+ if (!flippedPosition) {
1208
+ return;
1209
+ }
1210
+ const flippedLayout = {
1211
+ ...layout,
1212
+ arrowEdge: flippedArrowEdge,
1213
+ left: flippedPosition.left,
1214
+ top: flippedPosition.top,
1215
+ };
1216
+ const flippedCollisions = this.countSplitTooltipCollisions(flippedLayout, placedLayouts);
1217
+ if (flippedCollisions >= currentCollisions) {
1218
+ return;
1219
+ }
1220
+ layout.arrowEdge = flippedArrowEdge;
1221
+ layout.left = flippedPosition.left;
1222
+ layout.top = flippedPosition.top;
1223
+ }
1224
+ countSplitTooltipCollisions(layout, placedLayouts) {
1225
+ return placedLayouts.filter((placedLayout) => this.doSplitTooltipLayoutsOverlap(layout, placedLayout)).length;
1226
+ }
1227
+ doSplitTooltipLayoutsOverlap(a, b) {
1228
+ return (a.left < b.left + b.width + SPLIT_TOOLTIP_GAP_PX &&
1229
+ a.left + a.width + SPLIT_TOOLTIP_GAP_PX > b.left &&
1230
+ a.top < b.top + b.height + SPLIT_TOOLTIP_GAP_PX &&
1231
+ a.top + a.height + SPLIT_TOOLTIP_GAP_PX > b.top);
1232
+ }
1233
+ resolveSplitTooltipPositions(layouts) {
1234
+ const minTop = window.scrollY + TOOLTIP_VIEWPORT_PADDING_PX;
1235
+ const maxBottom = window.scrollY + window.innerHeight - TOOLTIP_VIEWPORT_PADDING_PX;
1236
+ const tooltipsByEdge = {
1237
+ left: [],
1238
+ right: [],
1239
+ top: [],
1240
+ bottom: [],
1241
+ };
1242
+ layouts.forEach((layout) => {
1243
+ tooltipsByEdge[layout.arrowEdge].push(layout);
1244
+ });
1245
+ Object.values(tooltipsByEdge).forEach((edgeLayouts) => {
1246
+ if (edgeLayouts.length === 0) {
1247
+ return;
1248
+ }
1249
+ edgeLayouts.sort((a, b) => a.top - b.top);
1250
+ edgeLayouts[0].top = Math.max(minTop, edgeLayouts[0].top);
1251
+ for (let i = 1; i < edgeLayouts.length; i++) {
1252
+ const previousLayout = edgeLayouts[i - 1];
1253
+ const currentLayout = edgeLayouts[i];
1254
+ const minAllowedTop = previousLayout.top +
1255
+ previousLayout.height +
1256
+ SPLIT_TOOLTIP_GAP_PX;
1257
+ currentLayout.top = Math.max(currentLayout.top, minAllowedTop);
1258
+ }
1259
+ const lastLayout = edgeLayouts[edgeLayouts.length - 1];
1260
+ const overflow = lastLayout.top + lastLayout.height - maxBottom;
1261
+ if (overflow > 0) {
1262
+ lastLayout.top -= overflow;
1263
+ for (let i = edgeLayouts.length - 2; i >= 0; i--) {
1264
+ const currentLayout = edgeLayouts[i];
1265
+ const nextLayout = edgeLayouts[i + 1];
1266
+ const maxAllowedTop = nextLayout.top -
1267
+ currentLayout.height -
1268
+ SPLIT_TOOLTIP_GAP_PX;
1269
+ currentLayout.top = Math.min(currentLayout.top, maxAllowedTop);
1270
+ }
1271
+ const underflow = minTop - edgeLayouts[0].top;
1272
+ if (underflow > 0) {
1273
+ edgeLayouts.forEach((layout) => {
1274
+ layout.top += underflow;
1275
+ });
1276
+ }
1277
+ }
1278
+ edgeLayouts.forEach((layout) => {
1279
+ const maxTop = maxBottom - layout.height;
1280
+ layout.top = Math.max(minTop, Math.min(layout.top, maxTop));
1281
+ });
1282
+ });
480
1283
  }
481
1284
  }
1285
+ Object.defineProperty(Tooltip, "nextTooltipId", {
1286
+ enumerable: true,
1287
+ configurable: true,
1288
+ writable: true,
1289
+ value: 0
1290
+ });