@internetstiftelsen/charts 0.14.3 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,6 +11,7 @@ A framework-agnostic, composable charting library built on D3.js with TypeScript
11
11
  - **Divergent Bar Support** - Bar charts automatically render from zero and diverge around `0` for mixed positive/negative values
12
12
  - **Mirrored Bar Sides** - Horizontal bars can mirror a series to the left for population-pyramid style charts without changing source data
13
13
  - **Custom Value Labels** - XY, pie, donut, and gauge charts support configurable labels with formatters, max-width overflow behavior, and forced rendering when labels would otherwise be hidden
14
+ - **Axis Label Overflow** - X/Y tick labels and grouped X-axis labels support max-width overflow behavior
14
15
  - **Optional XY Animation** - Animate XY series on first render and `chart.update(...)` with `animate`
15
16
  - **Optional Radial Animation** - Animate pie and donut segments on first render and `chart.update(...)` with `animate`
16
17
  - **Optional Gauge Animation** - Animate gauge value transitions with `gauge.animate`
@@ -349,6 +350,9 @@ const chart = new XYChart({
349
350
  });
350
351
  ```
351
352
 
353
+ The exported `defaultResponsiveConfig` also switches tooltip components to
354
+ `mode: 'shared'` at its `sm` breakpoint for compact XY charts.
355
+
352
356
  ## Word Cloud
353
357
 
354
358
  ```javascript
@@ -385,17 +389,16 @@ their own centers on initial render and `chart.update(...)`.
385
389
 
386
390
  ## Export
387
391
 
388
- `chart.export()` supports `svg`, `json`, `csv`, `xlsx`, `png`, `jpg`, and `pdf`.
392
+ `chart.export()` supports `svg`, `json`, `csv`, `xlsx`, `png`, and `jpg`.
389
393
 
390
394
  ```javascript
391
395
  await chart.export('png', { download: true });
392
396
  await chart.export('csv', { download: true, delimiter: ';' });
393
397
  await chart.export('xlsx', { download: true, sheetName: 'Data' });
394
- await chart.export('pdf', { download: true, pdfMargin: 16 });
395
398
  ```
396
399
 
397
- `xlsx` and `pdf` are lazy-loaded and require optional dependencies
398
- (`write-excel-file` and `jspdf`) only when those formats are used.
400
+ `xlsx` is lazy-loaded and requires the optional `write-excel-file` dependency
401
+ only when that format is used.
399
402
 
400
403
  ## Import
401
404
 
@@ -9,7 +9,7 @@ import type { Legend } from './legend.js';
9
9
  import type { Title } from './title.js';
10
10
  import { LayoutManager, type PlotAreaBounds } from './layout-manager.js';
11
11
  import { LegendStateController } from './legend-state.js';
12
- type VisualExportFormat = 'svg' | 'png' | 'jpg' | 'pdf';
12
+ type VisualExportFormat = 'svg' | 'png' | 'jpg';
13
13
  type RenderDimensions = {
14
14
  width: number;
15
15
  height: number;
@@ -262,7 +262,6 @@ export declare abstract class BaseChart {
262
262
  private exportSize;
263
263
  private exportXLSX;
264
264
  private exportImage;
265
- private exportPDF;
266
265
  protected exportSVG(options?: ExportOptions, formatForHooks?: VisualExportFormat): Promise<string>;
267
266
  private requireRenderedSvg;
268
267
  private resolveExportContext;
@@ -5,7 +5,6 @@ import { LayoutManager } from './layout-manager.js';
5
5
  import { serializeCSV } from './export-tabular.js';
6
6
  import { exportRasterBlob } from './export-image.js';
7
7
  import { exportXLSXBlob } from './export-xlsx.js';
8
- import { exportPDFBlob } from './export-pdf.js';
9
8
  import { normalizeChartData } from './grouped-data.js';
10
9
  import { LegendStateController } from './legend-state.js';
11
10
  import { mergeDeep } from './utils.js';
@@ -1251,7 +1250,7 @@ export class BaseChart {
1251
1250
  content = await this.exportImage(format, options);
1252
1251
  }
1253
1252
  else {
1254
- content = await this.exportPDF(options);
1253
+ throw new Error(`Unsupported export format: ${format}`);
1255
1254
  }
1256
1255
  if (options?.download) {
1257
1256
  this.downloadContent(content, format, options);
@@ -1296,7 +1295,7 @@ export class BaseChart {
1296
1295
  if (format === 'jpg') {
1297
1296
  return 'image/jpeg';
1298
1297
  }
1299
- return 'application/pdf';
1298
+ throw new Error(`Unsupported export format: ${format}`);
1300
1299
  }
1301
1300
  exportCSV(options) {
1302
1301
  return serializeCSV(this.sourceData, options);
@@ -1325,24 +1324,6 @@ export class BaseChart {
1325
1324
  jpegQuality: options?.jpegQuality ?? 0.92,
1326
1325
  });
1327
1326
  }
1328
- async exportPDF(options) {
1329
- const { width, height } = this.exportSize(options);
1330
- const svg = await this.exportSVG(options, 'pdf');
1331
- const pngBlob = await exportRasterBlob({
1332
- format: 'png',
1333
- svg,
1334
- width,
1335
- height,
1336
- pixelRatio: options?.pixelRatio ?? 1,
1337
- backgroundColor: options?.backgroundColor ?? '#ffffff',
1338
- jpegQuality: options?.jpegQuality ?? 0.92,
1339
- });
1340
- return exportPDFBlob(pngBlob, {
1341
- width,
1342
- height,
1343
- margin: options?.pdfMargin ?? 0,
1344
- });
1345
- }
1346
1327
  async exportSVG(options, formatForHooks = 'svg') {
1347
1328
  const liveSvg = this.requireRenderedSvg();
1348
1329
  await this.whenReady();
@@ -40,7 +40,7 @@ export type ChartGroupChartOptions = {
40
40
  order?: number;
41
41
  responsive?: ChartGroupItemResponsiveConfig;
42
42
  };
43
- export type ChartGroupExportFormat = 'svg' | 'png' | 'jpg' | 'pdf';
43
+ export type ChartGroupExportFormat = 'svg' | 'png' | 'jpg';
44
44
  export declare class ChartGroup {
45
45
  private readonly cols;
46
46
  private readonly gap;
@@ -127,7 +127,6 @@ export declare class ChartGroup {
127
127
  private requireRenderedContainer;
128
128
  private resolveExportContent;
129
129
  private createRasterExportOptions;
130
- private exportPdfContent;
131
130
  private resolveExportLayoutState;
132
131
  private createExportLayoutState;
133
132
  private createExportRenderContext;
@@ -1,6 +1,5 @@
1
1
  import { create } from 'd3';
2
2
  import { exportRasterBlob } from './export-image.js';
3
- import { exportPDFBlob } from './export-pdf.js';
4
3
  import { LegendStateController } from './legend-state.js';
5
4
  import { DEFAULT_CHART_HEIGHT, DEFAULT_CHART_WIDTH, defaultTheme, } from './theme.js';
6
5
  import { ChartValidator } from './validation.js';
@@ -988,9 +987,6 @@ export class ChartGroup {
988
987
  return svg;
989
988
  }
990
989
  const rasterOptions = this.createRasterExportOptions(svg, width, height, options);
991
- if (format === 'pdf') {
992
- return this.exportPdfContent(rasterOptions, width, height, options);
993
- }
994
990
  return exportRasterBlob({
995
991
  format,
996
992
  ...rasterOptions,
@@ -1006,17 +1002,6 @@ export class ChartGroup {
1006
1002
  jpegQuality: options?.jpegQuality ?? 0.92,
1007
1003
  };
1008
1004
  }
1009
- async exportPdfContent(rasterOptions, width, height, options) {
1010
- const pngBlob = await exportRasterBlob({
1011
- format: 'png',
1012
- ...rasterOptions,
1013
- });
1014
- return exportPDFBlob(pngBlob, {
1015
- width,
1016
- height,
1017
- margin: options?.pdfMargin ?? 0,
1018
- });
1019
- }
1020
1005
  resolveExportLayoutState(width, context) {
1021
1006
  const textComponents = this.textComponents
1022
1007
  .map((component) => {
@@ -1327,6 +1312,6 @@ export class ChartGroup {
1327
1312
  if (format === 'jpg') {
1328
1313
  return 'image/jpeg';
1329
1314
  }
1330
- return 'application/pdf';
1315
+ throw new Error(`Unsupported export format: ${format}`);
1331
1316
  }
1332
1317
  }
package/dist/theme.js CHANGED
@@ -325,6 +325,12 @@ export const defaultResponsiveConfig = {
325
325
  tooltip: { fontSize: 11 },
326
326
  valueLabel: { fontSize: 10 },
327
327
  },
328
+ components: [
329
+ {
330
+ match: { type: 'tooltip' },
331
+ override: { mode: 'shared' },
332
+ },
333
+ ],
328
334
  },
329
335
  md: {
330
336
  minWidth: 480,
@@ -0,0 +1,56 @@
1
+ import type { ChartTheme, TooltipTransitionConfig } from '../types.js';
2
+ import { type TooltipAnchor, type TooltipArrowEdge, type TooltipDivSelection } from './types.js';
3
+ type TooltipDomConfig = {
4
+ id: string;
5
+ splitTooltipOwner: string;
6
+ maxWidth: number;
7
+ transition: Required<TooltipTransitionConfig>;
8
+ };
9
+ export declare class TooltipDom {
10
+ private readonly id;
11
+ private readonly splitTooltipOwner;
12
+ private readonly maxWidth;
13
+ private readonly transition;
14
+ private readonly tooltipStyleKeys;
15
+ private readonly tooltipTransitionFrameIds;
16
+ private tooltipDiv;
17
+ private tooltipTheme;
18
+ constructor(config: TooltipDomConfig);
19
+ initialize(theme: ChartTheme): void;
20
+ getRootTooltip(): TooltipDivSelection | null;
21
+ setContent(content: string): void;
22
+ getBounds(): DOMRect | null;
23
+ showAt(left: number, top: number): void;
24
+ hide(): void;
25
+ cleanup(): void;
26
+ measureTooltip(tooltip: TooltipDivSelection, content: string): {
27
+ width: number;
28
+ height: number;
29
+ } | null;
30
+ renderTooltipWithConnector(tooltip: TooltipDivSelection, arrowEdge: TooltipArrowEdge, left: number, top: number, tooltipWidth: number, tooltipHeight: number, targetX: number, targetY: number, anchor: TooltipAnchor): void;
31
+ renderTooltipWithoutConnector(tooltip: TooltipDivSelection, left: number, top: number): void;
32
+ hideTooltipSelection(tooltip: TooltipDivSelection): void;
33
+ getSplitTooltip(index: number, theme: ChartTheme): TooltipDivSelection;
34
+ hideSplitTooltips(): void;
35
+ hideUnusedSplitTooltips(visibleTooltips: TooltipDivSelection[]): void;
36
+ private applyTooltipStylesIfNeeded;
37
+ private getTooltipStyleKey;
38
+ private writeTooltipStyles;
39
+ private showTooltipAt;
40
+ private showTooltipSelection;
41
+ private hideTooltipElement;
42
+ private getTooltipTransitionStyle;
43
+ private isTooltipVisible;
44
+ private getTooltipPosition;
45
+ private hasVisibleSlideOffset;
46
+ private slideTooltipFromOffset;
47
+ private requestTooltipTransitionFrame;
48
+ private cancelTooltipTransitionFrame;
49
+ private setTooltipMarkup;
50
+ private appendTooltipConnector;
51
+ private appendTooltipArrow;
52
+ private appendTooltipArrowTriangle;
53
+ private removeSplitTooltips;
54
+ private removeRootTooltip;
55
+ }
56
+ export {};
@@ -0,0 +1,438 @@
1
+ import { select } from 'd3';
2
+ import { resolveTooltipArrowPosition, resolveTooltipConnectorLayout, } from './geometry.js';
3
+ import { TOOLTIP_ARROW_BORDER_Z_INDEX, TOOLTIP_ARROW_FILL_Z_INDEX, TOOLTIP_BODY_Z_INDEX, TOOLTIP_BORDER_WIDTH_PX, TOOLTIP_BOX_ARROW_HALF_HEIGHT_PX, TOOLTIP_BOX_ARROW_LENGTH_PX, TOOLTIP_CONNECTOR_Z_INDEX, TOOLTIP_HIDDEN_TRANSFORM, TOOLTIP_ROOT_Z_INDEX, TOOLTIP_VISIBLE_TRANSFORM, } from './types.js';
4
+ export class TooltipDom {
5
+ constructor(config) {
6
+ Object.defineProperty(this, "id", {
7
+ enumerable: true,
8
+ configurable: true,
9
+ writable: true,
10
+ value: void 0
11
+ });
12
+ Object.defineProperty(this, "splitTooltipOwner", {
13
+ enumerable: true,
14
+ configurable: true,
15
+ writable: true,
16
+ value: void 0
17
+ });
18
+ Object.defineProperty(this, "maxWidth", {
19
+ enumerable: true,
20
+ configurable: true,
21
+ writable: true,
22
+ value: void 0
23
+ });
24
+ Object.defineProperty(this, "transition", {
25
+ enumerable: true,
26
+ configurable: true,
27
+ writable: true,
28
+ value: void 0
29
+ });
30
+ Object.defineProperty(this, "tooltipStyleKeys", {
31
+ enumerable: true,
32
+ configurable: true,
33
+ writable: true,
34
+ value: new WeakMap()
35
+ });
36
+ Object.defineProperty(this, "tooltipTransitionFrameIds", {
37
+ enumerable: true,
38
+ configurable: true,
39
+ writable: true,
40
+ value: new WeakMap()
41
+ });
42
+ Object.defineProperty(this, "tooltipDiv", {
43
+ enumerable: true,
44
+ configurable: true,
45
+ writable: true,
46
+ value: null
47
+ });
48
+ Object.defineProperty(this, "tooltipTheme", {
49
+ enumerable: true,
50
+ configurable: true,
51
+ writable: true,
52
+ value: null
53
+ });
54
+ this.id = config.id;
55
+ this.splitTooltipOwner = config.splitTooltipOwner;
56
+ this.maxWidth = config.maxWidth;
57
+ this.transition = config.transition;
58
+ }
59
+ initialize(theme) {
60
+ const existingTooltip = select(`#${this.id}`);
61
+ const tooltip = existingTooltip.empty()
62
+ ? select('body')
63
+ .append('div')
64
+ .attr('class', 'chart-tooltip')
65
+ .attr('id', this.id)
66
+ : existingTooltip;
67
+ this.removeSplitTooltips();
68
+ this.applyTooltipStylesIfNeeded(tooltip, theme);
69
+ this.tooltipDiv = tooltip;
70
+ this.hideTooltipSelection(tooltip);
71
+ }
72
+ getRootTooltip() {
73
+ return this.tooltipDiv;
74
+ }
75
+ setContent(content) {
76
+ if (!this.tooltipDiv) {
77
+ return;
78
+ }
79
+ this.setTooltipMarkup(this.tooltipDiv, content);
80
+ }
81
+ getBounds() {
82
+ const node = this.tooltipDiv?.node();
83
+ if (!node) {
84
+ return null;
85
+ }
86
+ return node.getBoundingClientRect();
87
+ }
88
+ showAt(left, top) {
89
+ if (!this.tooltipDiv) {
90
+ return;
91
+ }
92
+ if (!Number.isFinite(left) || !Number.isFinite(top)) {
93
+ this.hide();
94
+ return;
95
+ }
96
+ this.showTooltipAt(this.tooltipDiv, left, top);
97
+ }
98
+ hide() {
99
+ const tooltip = this.tooltipDiv ?? select(`#${this.id}`);
100
+ if (!tooltip.empty()) {
101
+ this.hideTooltipSelection(tooltip);
102
+ }
103
+ this.hideSplitTooltips();
104
+ }
105
+ cleanup() {
106
+ this.removeRootTooltip();
107
+ this.removeSplitTooltips();
108
+ this.tooltipDiv = null;
109
+ }
110
+ measureTooltip(tooltip, content) {
111
+ this.setTooltipMarkup(tooltip, content);
112
+ const tooltipNode = tooltip.node();
113
+ if (!tooltipNode) {
114
+ return null;
115
+ }
116
+ if (!this.isTooltipVisible(tooltipNode)) {
117
+ tooltip.style('left', '-9999px').style('top', '-9999px');
118
+ this.hideTooltipSelection(tooltip);
119
+ }
120
+ const tooltipRect = tooltipNode.getBoundingClientRect();
121
+ if (!Number.isFinite(tooltipRect.width) ||
122
+ !Number.isFinite(tooltipRect.height)) {
123
+ return null;
124
+ }
125
+ return {
126
+ width: tooltipRect.width,
127
+ height: tooltipRect.height,
128
+ };
129
+ }
130
+ renderTooltipWithConnector(tooltip, arrowEdge, left, top, tooltipWidth, tooltipHeight, targetX, targetY, anchor) {
131
+ if (!Number.isFinite(left) ||
132
+ !Number.isFinite(top) ||
133
+ !Number.isFinite(targetX) ||
134
+ !Number.isFinite(targetY)) {
135
+ this.hideTooltipSelection(tooltip);
136
+ return;
137
+ }
138
+ const connectorLayout = resolveTooltipConnectorLayout(arrowEdge, left, top, tooltipWidth, tooltipHeight, targetX, targetY, anchor);
139
+ if (!connectorLayout) {
140
+ this.hideTooltipSelection(tooltip);
141
+ return;
142
+ }
143
+ this.appendTooltipConnector(tooltip, connectorLayout);
144
+ this.appendTooltipArrow(tooltip, connectorLayout);
145
+ this.showTooltipAt(tooltip, left, top);
146
+ }
147
+ renderTooltipWithoutConnector(tooltip, left, top) {
148
+ if (!Number.isFinite(left) || !Number.isFinite(top)) {
149
+ this.hideTooltipSelection(tooltip);
150
+ return;
151
+ }
152
+ this.showTooltipAt(tooltip, left, top);
153
+ }
154
+ hideTooltipSelection(tooltip) {
155
+ const node = tooltip.node();
156
+ if (!node) {
157
+ return;
158
+ }
159
+ this.hideTooltipElement(node);
160
+ }
161
+ getSplitTooltip(index, theme) {
162
+ const tooltipId = `${this.splitTooltipOwner}-${index}`;
163
+ const existingTooltip = select(`#${tooltipId}`);
164
+ const tooltip = existingTooltip.empty()
165
+ ? select('body')
166
+ .append('div')
167
+ .attr('class', 'chart-tooltip chart-tooltip--split')
168
+ .attr('id', tooltipId)
169
+ .attr('data-chart-tooltip-owner', this.splitTooltipOwner)
170
+ .attr('data-chart-tooltip-index', String(index))
171
+ : existingTooltip;
172
+ this.applyTooltipStylesIfNeeded(tooltip, theme);
173
+ return tooltip;
174
+ }
175
+ hideSplitTooltips() {
176
+ document
177
+ .querySelectorAll(`[data-chart-tooltip-owner="${this.splitTooltipOwner}"]`)
178
+ .forEach((node) => {
179
+ this.hideTooltipElement(node);
180
+ });
181
+ }
182
+ hideUnusedSplitTooltips(visibleTooltips) {
183
+ const visibleNodes = new Set(visibleTooltips
184
+ .map((tooltip) => tooltip.node())
185
+ .filter((node) => Boolean(node)));
186
+ document
187
+ .querySelectorAll(`[data-chart-tooltip-owner="${this.splitTooltipOwner}"]`)
188
+ .forEach((node) => {
189
+ if (visibleNodes.has(node)) {
190
+ return;
191
+ }
192
+ this.hideTooltipElement(node);
193
+ });
194
+ }
195
+ applyTooltipStylesIfNeeded(tooltip, theme) {
196
+ const node = tooltip.node();
197
+ if (!node) {
198
+ return;
199
+ }
200
+ const styleKey = this.getTooltipStyleKey(theme);
201
+ if (this.tooltipStyleKeys.get(node) === styleKey) {
202
+ this.tooltipTheme = theme.tooltip;
203
+ return;
204
+ }
205
+ this.tooltipStyleKeys.set(node, styleKey);
206
+ this.writeTooltipStyles(tooltip, theme);
207
+ }
208
+ getTooltipStyleKey(theme) {
209
+ return [
210
+ theme.tooltip.background,
211
+ theme.tooltip.border,
212
+ theme.tooltip.color,
213
+ theme.tooltip.fontFamily,
214
+ theme.tooltip.fontSize,
215
+ theme.tooltip.fontWeight,
216
+ this.maxWidth,
217
+ this.transition.show,
218
+ this.transition.duration,
219
+ this.transition.easing,
220
+ ].join('|');
221
+ }
222
+ writeTooltipStyles(tooltip, theme) {
223
+ this.tooltipTheme = theme.tooltip;
224
+ tooltip
225
+ .style('position', 'absolute')
226
+ .style('background-color', theme.tooltip.background)
227
+ .style('border', `${TOOLTIP_BORDER_WIDTH_PX}px solid ${theme.tooltip.border}`)
228
+ .style('border-radius', '4px')
229
+ .style('padding', '8px')
230
+ .style('box-shadow', '0 2px 4px rgba(0,0,0,0.1)')
231
+ .style('color', theme.tooltip.color)
232
+ .style('font-family', theme.tooltip.fontFamily)
233
+ .style('font-size', `${theme.tooltip.fontSize}px`)
234
+ .style('font-weight', theme.tooltip.fontWeight)
235
+ .style('box-sizing', 'border-box')
236
+ .style('overflow-wrap', 'break-word')
237
+ .style('overflow', 'visible')
238
+ .style('isolation', 'isolate')
239
+ .style('pointer-events', 'none')
240
+ .style('z-index', String(TOOLTIP_ROOT_Z_INDEX));
241
+ tooltip.style('max-width', `${this.maxWidth}px`);
242
+ if (this.transition.show) {
243
+ tooltip
244
+ .style('transition', this.getTooltipTransitionStyle())
245
+ .style('will-change', 'opacity, transform');
246
+ return;
247
+ }
248
+ tooltip
249
+ .style('opacity', null)
250
+ .style('transform', null)
251
+ .style('transition', null)
252
+ .style('will-change', null);
253
+ }
254
+ showTooltipAt(tooltip, left, top) {
255
+ const tooltipNode = tooltip.node();
256
+ const previousPosition = tooltipNode && this.isTooltipVisible(tooltipNode)
257
+ ? this.getTooltipPosition(tooltipNode)
258
+ : null;
259
+ tooltip.style('left', `${left}px`).style('top', `${top}px`);
260
+ this.showTooltipSelection(tooltip, previousPosition
261
+ ? {
262
+ x: previousPosition.left - left,
263
+ y: previousPosition.top - top,
264
+ }
265
+ : null);
266
+ }
267
+ showTooltipSelection(tooltip, slideOffset = null) {
268
+ tooltip.style('visibility', 'visible');
269
+ if (!this.transition.show) {
270
+ return;
271
+ }
272
+ tooltip.style('opacity', '1');
273
+ const node = tooltip.node();
274
+ if (!node) {
275
+ return;
276
+ }
277
+ if (!slideOffset || !this.hasVisibleSlideOffset(slideOffset)) {
278
+ this.cancelTooltipTransitionFrame(node);
279
+ tooltip.style('transform', TOOLTIP_VISIBLE_TRANSFORM);
280
+ return;
281
+ }
282
+ this.slideTooltipFromOffset(node, slideOffset);
283
+ }
284
+ hideTooltipElement(node) {
285
+ this.cancelTooltipTransitionFrame(node);
286
+ if (!this.transition.show) {
287
+ node.style.visibility = 'hidden';
288
+ return;
289
+ }
290
+ node.style.visibility = 'visible';
291
+ node.style.opacity = '0';
292
+ node.style.transform = TOOLTIP_HIDDEN_TRANSFORM;
293
+ }
294
+ getTooltipTransitionStyle() {
295
+ return `opacity ${this.transition.duration}ms ${this.transition.easing}, transform ${this.transition.duration}ms ${this.transition.easing}`;
296
+ }
297
+ isTooltipVisible(node) {
298
+ return (node.style.visibility === 'visible' && node.style.opacity !== '0');
299
+ }
300
+ getTooltipPosition(node) {
301
+ const left = Number.parseFloat(node.style.left || '0');
302
+ const top = Number.parseFloat(node.style.top || '0');
303
+ if (!Number.isFinite(left) || !Number.isFinite(top)) {
304
+ return null;
305
+ }
306
+ return { left, top };
307
+ }
308
+ hasVisibleSlideOffset(offset) {
309
+ return Math.abs(offset.x) > 0.5 || Math.abs(offset.y) > 0.5;
310
+ }
311
+ slideTooltipFromOffset(node, offset) {
312
+ const transition = node.style.transition || this.getTooltipTransitionStyle();
313
+ this.cancelTooltipTransitionFrame(node);
314
+ node.style.setProperty('transition', 'none');
315
+ node.style.setProperty('transform', `translate(${offset.x}px, ${offset.y}px)`);
316
+ node.getBoundingClientRect();
317
+ node.style.setProperty('transition', transition);
318
+ const frameId = this.requestTooltipTransitionFrame(() => {
319
+ this.tooltipTransitionFrameIds.delete(node);
320
+ node.style.setProperty('transform', TOOLTIP_VISIBLE_TRANSFORM);
321
+ });
322
+ this.tooltipTransitionFrameIds.set(node, frameId);
323
+ }
324
+ requestTooltipTransitionFrame(callback) {
325
+ if (typeof window.requestAnimationFrame === 'function') {
326
+ return window.requestAnimationFrame(callback);
327
+ }
328
+ return window.setTimeout(() => {
329
+ callback(window.performance.now());
330
+ }, 16);
331
+ }
332
+ cancelTooltipTransitionFrame(node) {
333
+ const frameId = this.tooltipTransitionFrameIds.get(node);
334
+ if (frameId === undefined) {
335
+ return;
336
+ }
337
+ if (typeof window.cancelAnimationFrame === 'function') {
338
+ window.cancelAnimationFrame(frameId);
339
+ }
340
+ else {
341
+ window.clearTimeout(frameId);
342
+ }
343
+ this.tooltipTransitionFrameIds.delete(node);
344
+ }
345
+ setTooltipMarkup(tooltip, content) {
346
+ tooltip.html(`<div data-chart-tooltip-body="true">${content}</div>`);
347
+ const body = tooltip.select('[data-chart-tooltip-body]');
348
+ if (body.empty()) {
349
+ return;
350
+ }
351
+ body.style('position', 'relative').style('z-index', String(TOOLTIP_BODY_Z_INDEX));
352
+ }
353
+ appendTooltipConnector(tooltip, connectorLayout) {
354
+ const tooltipBorder = this.tooltipTheme?.border ?? '#dddddd';
355
+ const connector = tooltip
356
+ .append('svg')
357
+ .attr('data-chart-tooltip-connector', 'true')
358
+ .attr('data-chart-tooltip-arrow-edge', connectorLayout.arrowEdge)
359
+ .attr('aria-hidden', 'true')
360
+ .attr('width', connectorLayout.width)
361
+ .attr('height', connectorLayout.height)
362
+ .attr('viewBox', `0 0 ${connectorLayout.width} ${connectorLayout.height}`)
363
+ .style('position', 'absolute')
364
+ .style('left', `${connectorLayout.left}px`)
365
+ .style('top', `${connectorLayout.top}px`)
366
+ .style('pointer-events', 'none')
367
+ .style('overflow', 'visible')
368
+ .style('z-index', String(TOOLTIP_CONNECTOR_Z_INDEX));
369
+ connector
370
+ .append('path')
371
+ .attr('data-chart-tooltip-connector-path', 'true')
372
+ .attr('d', connectorLayout.path)
373
+ .attr('fill', 'none')
374
+ .attr('stroke', tooltipBorder)
375
+ .attr('stroke-width', 1.25)
376
+ .attr('stroke-linecap', 'round')
377
+ .attr('stroke-linejoin', 'round');
378
+ }
379
+ appendTooltipArrow(tooltip, connectorLayout) {
380
+ const tooltipBackground = this.tooltipTheme?.background ?? '#ffffff';
381
+ const tooltipBorder = this.tooltipTheme?.border ?? '#dddddd';
382
+ 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);
383
+ 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);
384
+ }
385
+ appendTooltipArrowTriangle(tooltip, connectorLayout, dataAttribute, color, length, halfHeight, zIndex) {
386
+ const position = resolveTooltipArrowPosition(connectorLayout.arrowEdge, connectorLayout.arrowX, connectorLayout.arrowY, length, halfHeight);
387
+ const arrow = tooltip
388
+ .append('div')
389
+ .attr(dataAttribute, 'true')
390
+ .attr('data-chart-tooltip-arrow-edge', connectorLayout.arrowEdge)
391
+ .attr('aria-hidden', 'true')
392
+ .style('position', 'absolute')
393
+ .style('left', `${position.left}px`)
394
+ .style('top', `${position.top}px`)
395
+ .style('width', '0')
396
+ .style('height', '0')
397
+ .style('pointer-events', 'none')
398
+ .style('z-index', String(zIndex));
399
+ if (connectorLayout.arrowEdge === 'left') {
400
+ arrow
401
+ .style('border-top', `${halfHeight}px solid transparent`)
402
+ .style('border-bottom', `${halfHeight}px solid transparent`)
403
+ .style('border-right', `${length}px solid ${color}`);
404
+ return;
405
+ }
406
+ if (connectorLayout.arrowEdge === 'right') {
407
+ arrow
408
+ .style('border-top', `${halfHeight}px solid transparent`)
409
+ .style('border-bottom', `${halfHeight}px solid transparent`)
410
+ .style('border-left', `${length}px solid ${color}`);
411
+ return;
412
+ }
413
+ if (connectorLayout.arrowEdge === 'top') {
414
+ arrow
415
+ .style('border-left', `${halfHeight}px solid transparent`)
416
+ .style('border-right', `${halfHeight}px solid transparent`)
417
+ .style('border-bottom', `${length}px solid ${color}`);
418
+ return;
419
+ }
420
+ arrow
421
+ .style('border-left', `${halfHeight}px solid transparent`)
422
+ .style('border-right', `${halfHeight}px solid transparent`)
423
+ .style('border-top', `${length}px solid ${color}`);
424
+ }
425
+ removeSplitTooltips() {
426
+ document
427
+ .querySelectorAll(`[data-chart-tooltip-owner="${this.splitTooltipOwner}"]`)
428
+ .forEach((node) => {
429
+ node.remove();
430
+ });
431
+ }
432
+ removeRootTooltip() {
433
+ const tooltip = this.tooltipDiv ?? select(`#${this.id}`);
434
+ if (!tooltip.empty()) {
435
+ tooltip.remove();
436
+ }
437
+ }
438
+ }