@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 +7 -4
- package/dist/base-chart.d.ts +1 -2
- package/dist/base-chart.js +2 -21
- package/dist/chart-group.d.ts +1 -2
- package/dist/chart-group.js +1 -16
- package/dist/theme.js +6 -0
- package/dist/tooltip/dom.d.ts +56 -0
- package/dist/tooltip/dom.js +438 -0
- package/dist/tooltip/geometry.d.ts +18 -0
- package/dist/tooltip/geometry.js +395 -0
- package/dist/tooltip/types.d.ts +77 -0
- package/dist/tooltip/types.js +24 -0
- package/dist/tooltip/xy-interaction.d.ts +33 -0
- package/dist/tooltip/xy-interaction.js +608 -0
- package/dist/tooltip.d.ts +4 -74
- package/dist/tooltip.js +41 -1444
- package/dist/types.d.ts +3 -2
- package/dist/x-axis.d.ts +11 -1
- package/dist/x-axis.js +150 -10
- package/dist/xy-chart.d.ts +1 -0
- package/dist/xy-chart.js +10 -4
- package/docs/chart-group.md +0 -1
- package/docs/components.md +19 -25
- package/docs/xy-chart.md +3 -10
- package/package.json +17 -18
- package/dist/export-pdf.d.ts +0 -8
- package/dist/export-pdf.js +0 -67
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`,
|
|
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`
|
|
398
|
-
|
|
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
|
|
package/dist/base-chart.d.ts
CHANGED
|
@@ -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'
|
|
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;
|
package/dist/base-chart.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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();
|
package/dist/chart-group.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export type ChartGroupChartOptions = {
|
|
|
40
40
|
order?: number;
|
|
41
41
|
responsive?: ChartGroupItemResponsiveConfig;
|
|
42
42
|
};
|
|
43
|
-
export type ChartGroupExportFormat = 'svg' | 'png' | 'jpg'
|
|
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;
|
package/dist/chart-group.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|