@internetstiftelsen/charts 0.10.1 → 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 (45) hide show
  1. package/README.md +64 -0
  2. package/dist/area.d.ts +9 -1
  3. package/dist/area.js +174 -38
  4. package/dist/bar.d.ts +9 -1
  5. package/dist/bar.js +130 -47
  6. package/dist/base-chart.js +11 -1
  7. package/dist/donut-chart.js +3 -18
  8. package/dist/gauge-chart.d.ts +3 -4
  9. package/dist/gauge-chart.js +7 -53
  10. package/dist/lazy-mount.d.ts +13 -0
  11. package/dist/lazy-mount.js +90 -0
  12. package/dist/line.d.ts +9 -1
  13. package/dist/line.js +141 -23
  14. package/dist/pie-chart.js +5 -29
  15. package/dist/radial-chart-base.d.ts +4 -3
  16. package/dist/radial-chart-base.js +27 -12
  17. package/dist/scatter.d.ts +5 -1
  18. package/dist/scatter.js +89 -8
  19. package/dist/theme.js +17 -0
  20. package/dist/tooltip.d.ts +55 -3
  21. package/dist/tooltip.js +950 -137
  22. package/dist/types.d.ts +20 -0
  23. package/dist/xy-animation.d.ts +3 -0
  24. package/dist/xy-animation.js +2 -0
  25. package/dist/xy-chart.d.ts +11 -1
  26. package/dist/xy-chart.js +107 -10
  27. package/dist/xy-motion/config.d.ts +2 -0
  28. package/dist/xy-motion/config.js +177 -0
  29. package/dist/xy-motion/driver.d.ts +9 -0
  30. package/dist/xy-motion/driver.js +10 -0
  31. package/dist/xy-motion/helpers.d.ts +17 -0
  32. package/dist/xy-motion/helpers.js +105 -0
  33. package/dist/xy-motion/live-state.d.ts +8 -0
  34. package/dist/xy-motion/live-state.js +240 -0
  35. package/dist/xy-motion/noop-xy-motion-driver.d.ts +9 -0
  36. package/dist/xy-motion/noop-xy-motion-driver.js +15 -0
  37. package/dist/xy-motion/types.d.ts +85 -0
  38. package/dist/xy-motion/types.js +1 -0
  39. package/dist/xy-motion/xy-motion-driver.d.ts +19 -0
  40. package/dist/xy-motion/xy-motion-driver.js +130 -0
  41. package/docs/components.md +36 -0
  42. package/docs/getting-started.md +35 -0
  43. package/docs/theming.md +14 -0
  44. package/docs/xy-chart.md +67 -1
  45. package/package.json +1 -1
@@ -0,0 +1,240 @@
1
+ import { select } from 'd3';
2
+ import { sanitizeForCSS } from '../utils.js';
3
+ import { cloneXYSeriesSnapshotCollection, createXYSeriesSnapshotId, } from './helpers.js';
4
+ export function captureLiveAnimationState(plotGroup, visibleSeries, lastSeriesSnapshot) {
5
+ const snapshotCollection = cloneXYSeriesSnapshotCollection(lastSeriesSnapshot);
6
+ const pathState = new Map();
7
+ if (!plotGroup) {
8
+ return {
9
+ snapshotCollection,
10
+ pathState,
11
+ };
12
+ }
13
+ visibleSeries.forEach((series) => {
14
+ captureLiveSeriesState(series, plotGroup, snapshotCollection, pathState, lastSeriesSnapshot);
15
+ });
16
+ return {
17
+ snapshotCollection,
18
+ pathState,
19
+ };
20
+ }
21
+ function captureLiveSeriesState(series, plotGroup, snapshotCollection, pathState, lastSeriesSnapshot) {
22
+ switch (series.type) {
23
+ case 'bar':
24
+ captureLiveBarSeriesState(series, plotGroup, snapshotCollection);
25
+ return;
26
+ case 'line':
27
+ captureLiveLineSeriesState(series, plotGroup, snapshotCollection, pathState);
28
+ return;
29
+ case 'scatter':
30
+ captureLiveScatterSeriesState(series, plotGroup, snapshotCollection);
31
+ return;
32
+ case 'area':
33
+ captureLiveAreaSeriesState(series, plotGroup, snapshotCollection, pathState, lastSeriesSnapshot);
34
+ return;
35
+ }
36
+ }
37
+ function captureLiveBarSeriesState(series, plotGroup, snapshotCollection) {
38
+ const seriesId = createXYSeriesSnapshotId(series.type, series.dataKey);
39
+ const snapshot = captureLiveBarSnapshot(plotGroup, series.dataKey);
40
+ if (!snapshot) {
41
+ return;
42
+ }
43
+ snapshotCollection.set(seriesId, new Map([
44
+ ...(snapshotCollection.get(seriesId) ?? new Map()),
45
+ ...snapshot,
46
+ ]));
47
+ }
48
+ function captureLiveLineSeriesState(series, plotGroup, snapshotCollection, pathState) {
49
+ const seriesId = createXYSeriesSnapshotId(series.type, series.dataKey);
50
+ const snapshot = captureLivePointSnapshot(plotGroup, `.circle-${sanitizeForCSS(series.dataKey)}`);
51
+ const linePathState = captureLivePathState(plotGroup, `.line-${sanitizeForCSS(series.dataKey)}`);
52
+ if (snapshot) {
53
+ snapshotCollection.set(seriesId, new Map([
54
+ ...(snapshotCollection.get(seriesId) ?? new Map()),
55
+ ...snapshot,
56
+ ]));
57
+ }
58
+ if (linePathState.path !== null || linePathState.revealProgress !== null) {
59
+ pathState.set(seriesId, {
60
+ linePath: linePathState.path,
61
+ lineRevealProgress: linePathState.revealProgress,
62
+ });
63
+ }
64
+ }
65
+ function captureLiveScatterSeriesState(series, plotGroup, snapshotCollection) {
66
+ const seriesId = createXYSeriesSnapshotId(series.type, series.dataKey);
67
+ const snapshot = captureLivePointSnapshot(plotGroup, `.scatter-point-${sanitizeForCSS(series.dataKey)}`);
68
+ if (!snapshot) {
69
+ return;
70
+ }
71
+ snapshotCollection.set(seriesId, new Map([
72
+ ...(snapshotCollection.get(seriesId) ?? new Map()),
73
+ ...snapshot,
74
+ ]));
75
+ }
76
+ function captureLiveAreaSeriesState(series, plotGroup, snapshotCollection, pathState, lastSeriesSnapshot) {
77
+ const seriesId = createXYSeriesSnapshotId(series.type, series.dataKey);
78
+ const snapshot = captureLiveAreaSnapshot(plotGroup, series.dataKey, lastSeriesSnapshot.get(seriesId));
79
+ const areaPathState = captureLivePathState(plotGroup, `.area-${sanitizeForCSS(series.dataKey)}`);
80
+ const areaLinePathState = captureLivePathState(plotGroup, `.area-line-${sanitizeForCSS(series.dataKey)}`);
81
+ if (snapshot) {
82
+ snapshotCollection.set(seriesId, new Map([
83
+ ...(snapshotCollection.get(seriesId) ?? new Map()),
84
+ ...snapshot,
85
+ ]));
86
+ }
87
+ if (areaPathState.path !== null ||
88
+ areaLinePathState.path !== null ||
89
+ areaPathState.revealProgress !== null ||
90
+ areaLinePathState.revealProgress !== null) {
91
+ pathState.set(seriesId, {
92
+ areaPath: areaPathState.path,
93
+ areaLinePath: areaLinePathState.path,
94
+ areaRevealProgress: areaPathState.revealProgress,
95
+ areaLineRevealProgress: areaLinePathState.revealProgress,
96
+ });
97
+ }
98
+ }
99
+ function captureLiveBarSnapshot(plotGroup, dataKey) {
100
+ const bars = plotGroup.selectAll(`.bar-${sanitizeForCSS(dataKey)}`);
101
+ if (bars.empty()) {
102
+ return null;
103
+ }
104
+ bars.interrupt();
105
+ const snapshot = new Map();
106
+ bars.each((datum, index, nodes) => {
107
+ if (!datum.snapshotKey) {
108
+ return;
109
+ }
110
+ const node = nodes[index];
111
+ const x = getNumericAttribute(node, 'x');
112
+ const y = getNumericAttribute(node, 'y');
113
+ const width = getNumericAttribute(node, 'width');
114
+ const height = getNumericAttribute(node, 'height');
115
+ if (x === null || y === null || width === null || height === null) {
116
+ return;
117
+ }
118
+ snapshot.set(datum.snapshotKey, {
119
+ x,
120
+ y,
121
+ width,
122
+ height,
123
+ });
124
+ });
125
+ return snapshot.size > 0 ? snapshot : null;
126
+ }
127
+ function captureLivePointSnapshot(plotGroup, selector) {
128
+ const points = plotGroup.selectAll(selector);
129
+ if (points.empty()) {
130
+ return null;
131
+ }
132
+ points.interrupt();
133
+ const snapshot = new Map();
134
+ points.each((datum, index, nodes) => {
135
+ if (!datum.snapshotKey) {
136
+ return;
137
+ }
138
+ const node = nodes[index];
139
+ const x = getNumericAttribute(node, 'cx');
140
+ const y = getNumericAttribute(node, 'cy');
141
+ if (x === null || y === null) {
142
+ return;
143
+ }
144
+ snapshot.set(datum.snapshotKey, {
145
+ x,
146
+ y,
147
+ });
148
+ });
149
+ return snapshot.size > 0 ? snapshot : null;
150
+ }
151
+ function captureLiveAreaSnapshot(plotGroup, dataKey, previousSnapshot) {
152
+ const pointSnapshot = captureLivePointSnapshot(plotGroup, `.area-point-${sanitizeForCSS(dataKey)}`);
153
+ if (!pointSnapshot) {
154
+ return null;
155
+ }
156
+ const snapshot = new Map();
157
+ pointSnapshot.forEach((point, snapshotKey) => {
158
+ snapshot.set(snapshotKey, {
159
+ x: point.x,
160
+ y0: previousSnapshot?.get(snapshotKey)?.y0 ?? point.y,
161
+ y1: point.y,
162
+ });
163
+ });
164
+ return snapshot.size > 0 ? snapshot : null;
165
+ }
166
+ function captureLivePathState(plotGroup, selector) {
167
+ const path = plotGroup.select(selector);
168
+ if (path.empty()) {
169
+ return {
170
+ path: null,
171
+ revealProgress: null,
172
+ };
173
+ }
174
+ const node = path.node();
175
+ const revealProgress = node ? captureLeftToRightRevealProgress(node) : null;
176
+ path.interrupt();
177
+ interruptLeftToRightReveal(path.node());
178
+ return {
179
+ path: path.attr('d') || null,
180
+ revealProgress,
181
+ };
182
+ }
183
+ function getNumericAttribute(element, attribute) {
184
+ if (!element) {
185
+ return null;
186
+ }
187
+ const value = Number(element.getAttribute(attribute));
188
+ return Number.isFinite(value) ? value : null;
189
+ }
190
+ function captureLeftToRightRevealProgress(target) {
191
+ const clipRect = getRevealClipRect(target);
192
+ if (!clipRect) {
193
+ return null;
194
+ }
195
+ const targetBounds = target.getBBox();
196
+ const clipWidth = getNumericAttribute(clipRect, 'width');
197
+ const clipX = getNumericAttribute(clipRect, 'x');
198
+ if (clipWidth === null || clipX === null) {
199
+ return null;
200
+ }
201
+ const padding = targetBounds.x - clipX;
202
+ const totalWidth = targetBounds.width + padding * 2;
203
+ if (totalWidth <= 0) {
204
+ return null;
205
+ }
206
+ const progress = clipWidth / totalWidth;
207
+ if (!Number.isFinite(progress) || progress >= 1) {
208
+ return null;
209
+ }
210
+ return Math.max(0, Math.min(1, progress));
211
+ }
212
+ function interruptLeftToRightReveal(target) {
213
+ const clipRect = target ? getRevealClipRect(target) : null;
214
+ if (!clipRect) {
215
+ return;
216
+ }
217
+ select(clipRect).interrupt();
218
+ }
219
+ function getRevealClipRect(target) {
220
+ const svg = target.ownerSVGElement;
221
+ if (!svg) {
222
+ return null;
223
+ }
224
+ const clipPathId = extractClipPathId(target.getAttribute('clip-path'));
225
+ if (!clipPathId) {
226
+ return null;
227
+ }
228
+ const clipPath = target.ownerDocument?.getElementById(clipPathId);
229
+ if (!clipPath || !clipPath.classList.contains('xy-animation-reveal-clip')) {
230
+ return null;
231
+ }
232
+ return clipPath.querySelector('rect');
233
+ }
234
+ function extractClipPathId(value) {
235
+ if (!value) {
236
+ return null;
237
+ }
238
+ const match = value.match(/url\((?:['"])?#([^'")]+)(?:['"])?\)/);
239
+ return match?.[1] ?? null;
240
+ }
@@ -0,0 +1,9 @@
1
+ import type { XYMotionDriverContract } from './driver.js';
2
+ import type { XYAreaAnimationContext, XYBarAnimationContext, XYMotionRenderResult, XYMotionSeries, XYMotionUpdateContext, XYPointAnimationContext } from './types.js';
3
+ export declare class NoopXYMotionDriver implements XYMotionDriverContract {
4
+ prepareForUpdate(_context: XYMotionUpdateContext): void;
5
+ getPointAnimationContext(_series: XYMotionSeries, _baselineY: number): XYPointAnimationContext | undefined;
6
+ getAreaAnimationContext(_series: XYMotionSeries): XYAreaAnimationContext | undefined;
7
+ getBarAnimationContext(_series: XYMotionSeries, _baselineValuePosition: number): XYBarAnimationContext | undefined;
8
+ completeRender(_result: XYMotionRenderResult): Promise<void>;
9
+ }
@@ -0,0 +1,15 @@
1
+ export class NoopXYMotionDriver {
2
+ prepareForUpdate(_context) { }
3
+ getPointAnimationContext(_series, _baselineY) {
4
+ return undefined;
5
+ }
6
+ getAreaAnimationContext(_series) {
7
+ return undefined;
8
+ }
9
+ getBarAnimationContext(_series, _baselineValuePosition) {
10
+ return undefined;
11
+ }
12
+ completeRender(_result) {
13
+ return Promise.resolve();
14
+ }
15
+ }
@@ -0,0 +1,85 @@
1
+ import type { Selection } from 'd3';
2
+ export type XYAnimationEasingPreset = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce-out' | 'elastic-out';
3
+ export type XYAnimationConfig = {
4
+ show?: boolean;
5
+ duration?: number;
6
+ easing?: XYAnimationEasingPreset | `linear(${string})` | ((progress: number) => number);
7
+ };
8
+ export type XYRenderAnimationMode = 'none' | 'initial' | 'update';
9
+ export type XYPointSnapshot = {
10
+ x: number;
11
+ y: number;
12
+ };
13
+ export type XYAreaPointSnapshot = {
14
+ x: number;
15
+ y0: number;
16
+ y1: number;
17
+ };
18
+ export type XYBarSnapshot = {
19
+ x: number;
20
+ y: number;
21
+ width: number;
22
+ height: number;
23
+ };
24
+ export type XYSeriesDatumSnapshot = XYPointSnapshot | XYAreaPointSnapshot | XYBarSnapshot;
25
+ export type XYSeriesSnapshot<TSnapshot = XYSeriesDatumSnapshot> = Map<string, TSnapshot>;
26
+ export type XYSeriesSnapshotCollection = Map<string, XYSeriesSnapshot<XYSeriesDatumSnapshot>>;
27
+ export type NormalizedXYAnimation = {
28
+ show: boolean;
29
+ duration: number;
30
+ easing: (progress: number) => number;
31
+ };
32
+ export type XYPointAnimationContext = {
33
+ mode: Exclude<XYRenderAnimationMode, 'none'>;
34
+ duration: number;
35
+ easing: (progress: number) => number;
36
+ baselineY: number;
37
+ previousSnapshot?: XYSeriesSnapshot<XYPointSnapshot>;
38
+ previousPath?: string | null;
39
+ previousRevealProgress?: number | null;
40
+ };
41
+ export type XYAreaAnimationContext = {
42
+ mode: Exclude<XYRenderAnimationMode, 'none'>;
43
+ duration: number;
44
+ easing: (progress: number) => number;
45
+ previousSnapshot?: XYSeriesSnapshot<XYAreaPointSnapshot>;
46
+ previousAreaPath?: string | null;
47
+ previousLinePath?: string | null;
48
+ previousAreaRevealProgress?: number | null;
49
+ previousLineRevealProgress?: number | null;
50
+ };
51
+ export type XYBarAnimationContext = {
52
+ mode: Exclude<XYRenderAnimationMode, 'none'>;
53
+ duration: number;
54
+ easing: (progress: number) => number;
55
+ baselineValuePosition: number;
56
+ previousSnapshot?: XYSeriesSnapshot<XYBarSnapshot>;
57
+ };
58
+ export type XYSeriesRenderResult<TSnapshot = XYSeriesDatumSnapshot> = {
59
+ snapshot: XYSeriesSnapshot<TSnapshot>;
60
+ transitions: Promise<void>[];
61
+ };
62
+ export type XYMotionSeries = {
63
+ type: 'line' | 'scatter' | 'bar' | 'area';
64
+ dataKey: string;
65
+ };
66
+ export type XYMotionRenderResult = {
67
+ snapshotCollection: XYSeriesSnapshotCollection;
68
+ transitions: Promise<void>[];
69
+ };
70
+ export type XYMotionUpdateContext = {
71
+ plotGroup: Selection<SVGGElement, undefined, null, undefined> | null;
72
+ visibleSeries: XYMotionSeries[];
73
+ };
74
+ export type XYLivePathState = {
75
+ linePath?: string | null;
76
+ areaPath?: string | null;
77
+ areaLinePath?: string | null;
78
+ lineRevealProgress?: number | null;
79
+ areaRevealProgress?: number | null;
80
+ areaLineRevealProgress?: number | null;
81
+ };
82
+ export type XYLivePathStateCollection = Map<string, XYLivePathState>;
83
+ export type XYSnapshotBoundDatum = {
84
+ snapshotKey?: string;
85
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ import type { XYMotionDriverContract } from './driver.js';
2
+ import type { NormalizedXYAnimation, XYAreaAnimationContext, XYBarAnimationContext, XYMotionRenderResult, XYMotionSeries, XYMotionUpdateContext, XYPointAnimationContext } from './types.js';
3
+ export declare class XYMotionDriver implements XYMotionDriverContract {
4
+ private readonly animation;
5
+ private hasRenderedLive;
6
+ private nextRenderAnimationMode;
7
+ private pendingAnimationSnapshot;
8
+ private pendingPathState;
9
+ private lastSeriesSnapshot;
10
+ constructor(animation: NormalizedXYAnimation);
11
+ prepareForUpdate(context: XYMotionUpdateContext): void;
12
+ getPointAnimationContext(series: XYMotionSeries, baselineY: number): XYPointAnimationContext | undefined;
13
+ getAreaAnimationContext(series: XYMotionSeries): XYAreaAnimationContext | undefined;
14
+ getBarAnimationContext(series: XYMotionSeries, baselineValuePosition: number): XYBarAnimationContext | undefined;
15
+ completeRender(result: XYMotionRenderResult): Promise<void>;
16
+ private getRenderMode;
17
+ private getPreviousSeriesSnapshot;
18
+ private getPreviousPathState;
19
+ }
@@ -0,0 +1,130 @@
1
+ import { createXYSeriesSnapshotId } from './helpers.js';
2
+ import { captureLiveAnimationState } from './live-state.js';
3
+ export class XYMotionDriver {
4
+ constructor(animation) {
5
+ Object.defineProperty(this, "animation", {
6
+ enumerable: true,
7
+ configurable: true,
8
+ writable: true,
9
+ value: void 0
10
+ });
11
+ Object.defineProperty(this, "hasRenderedLive", {
12
+ enumerable: true,
13
+ configurable: true,
14
+ writable: true,
15
+ value: false
16
+ });
17
+ Object.defineProperty(this, "nextRenderAnimationMode", {
18
+ enumerable: true,
19
+ configurable: true,
20
+ writable: true,
21
+ value: void 0
22
+ });
23
+ Object.defineProperty(this, "pendingAnimationSnapshot", {
24
+ enumerable: true,
25
+ configurable: true,
26
+ writable: true,
27
+ value: null
28
+ });
29
+ Object.defineProperty(this, "pendingPathState", {
30
+ enumerable: true,
31
+ configurable: true,
32
+ writable: true,
33
+ value: new Map()
34
+ });
35
+ Object.defineProperty(this, "lastSeriesSnapshot", {
36
+ enumerable: true,
37
+ configurable: true,
38
+ writable: true,
39
+ value: new Map()
40
+ });
41
+ this.animation = animation;
42
+ this.nextRenderAnimationMode = 'initial';
43
+ }
44
+ prepareForUpdate(context) {
45
+ if (!this.hasRenderedLive) {
46
+ this.pendingAnimationSnapshot = null;
47
+ this.pendingPathState = new Map();
48
+ this.nextRenderAnimationMode = 'none';
49
+ return;
50
+ }
51
+ const liveState = captureLiveAnimationState(context.plotGroup, context.visibleSeries, this.lastSeriesSnapshot);
52
+ this.pendingAnimationSnapshot = liveState.snapshotCollection;
53
+ this.pendingPathState = liveState.pathState;
54
+ this.nextRenderAnimationMode =
55
+ this.animation.duration > 0 ? 'update' : 'none';
56
+ }
57
+ getPointAnimationContext(series, baselineY) {
58
+ const mode = this.getRenderMode();
59
+ if (mode === 'none') {
60
+ return undefined;
61
+ }
62
+ return {
63
+ mode,
64
+ duration: this.animation.duration,
65
+ easing: this.animation.easing,
66
+ baselineY,
67
+ previousSnapshot: this.getPreviousSeriesSnapshot(series.type, series.dataKey),
68
+ previousPath: series.type === 'line'
69
+ ? this.getPreviousPathState(series.type, series.dataKey)
70
+ ?.linePath
71
+ : undefined,
72
+ previousRevealProgress: series.type === 'line'
73
+ ? this.getPreviousPathState(series.type, series.dataKey)
74
+ ?.lineRevealProgress
75
+ : undefined,
76
+ };
77
+ }
78
+ getAreaAnimationContext(series) {
79
+ const mode = this.getRenderMode();
80
+ if (mode === 'none') {
81
+ return undefined;
82
+ }
83
+ return {
84
+ mode,
85
+ duration: this.animation.duration,
86
+ easing: this.animation.easing,
87
+ previousSnapshot: this.getPreviousSeriesSnapshot(series.type, series.dataKey),
88
+ previousAreaPath: this.getPreviousPathState(series.type, series.dataKey)?.areaPath,
89
+ previousAreaRevealProgress: this.getPreviousPathState(series.type, series.dataKey)?.areaRevealProgress,
90
+ previousLinePath: this.getPreviousPathState(series.type, series.dataKey)?.areaLinePath,
91
+ previousLineRevealProgress: this.getPreviousPathState(series.type, series.dataKey)?.areaLineRevealProgress,
92
+ };
93
+ }
94
+ getBarAnimationContext(series, baselineValuePosition) {
95
+ const mode = this.getRenderMode();
96
+ if (mode === 'none') {
97
+ return undefined;
98
+ }
99
+ return {
100
+ mode,
101
+ duration: this.animation.duration,
102
+ easing: this.animation.easing,
103
+ baselineValuePosition,
104
+ previousSnapshot: this.getPreviousSeriesSnapshot(series.type, series.dataKey),
105
+ };
106
+ }
107
+ completeRender(result) {
108
+ this.lastSeriesSnapshot = result.snapshotCollection;
109
+ this.hasRenderedLive = true;
110
+ this.nextRenderAnimationMode = 'none';
111
+ this.pendingAnimationSnapshot = null;
112
+ this.pendingPathState = new Map();
113
+ if (result.transitions.length === 0) {
114
+ return Promise.resolve();
115
+ }
116
+ return Promise.allSettled(result.transitions).then(() => undefined);
117
+ }
118
+ getRenderMode() {
119
+ if (this.animation.duration === 0) {
120
+ return 'none';
121
+ }
122
+ return this.nextRenderAnimationMode;
123
+ }
124
+ getPreviousSeriesSnapshot(seriesType, dataKey) {
125
+ return this.pendingAnimationSnapshot?.get(createXYSeriesSnapshotId(seriesType, dataKey));
126
+ }
127
+ getPreviousPathState(seriesType, dataKey) {
128
+ return this.pendingPathState.get(createXYSeriesSnapshotId(seriesType, dataKey));
129
+ }
130
+ }
@@ -119,6 +119,14 @@ Renders interactive tooltips on hover and keyboard focus.
119
119
 
120
120
  ```typescript
121
121
  new Tooltip({
122
+ mode?: 'shared' | 'split',
123
+ position?: 'side' | 'vertical',
124
+ barAnchorPosition?: 'top' | 'middle',
125
+ transition?: {
126
+ show?: boolean,
127
+ duration?: number,
128
+ easing?: string
129
+ },
122
130
  formatter?: (dataKey: string, value: DataValue, data: DataItem) => string
123
131
  })
124
132
  ```
@@ -129,6 +137,34 @@ The formatter receives:
129
137
  - `value` - The data value at this point
130
138
  - `data` - The full data item object
131
139
 
140
+ Tooltip modes:
141
+
142
+ - `shared` - One tooltip per hovered category/value group
143
+ - `split` - Default. One tooltip per visible series at the hovered category/value group
144
+
145
+ Split tooltip positions:
146
+
147
+ - `side` - Default. Place split tooltips to the side of each data point
148
+ - `vertical` - Place split tooltips above or below each data point
149
+
150
+ When `split` mode is active, `customFormatter` receives the current series only for
151
+ that tooltip box instead of the full visible series list. Split tooltips include
152
+ directional arrows and avoid overlapping on the same side of the chart when
153
+ possible.
154
+
155
+ For bars, `barAnchorPosition` controls whether split tooltips point to the `top`
156
+ or `middle` of each bar.
157
+
158
+ Set `transition.show: true` to fade tooltips in and out. Tooltip position and
159
+ connector geometry update immediately; only opacity and the small entrance
160
+ offset transition.
161
+
162
+ Tooltip box, text, connector, and arrow colors use `theme.tooltip`.
163
+
164
+ Formatter, label formatter, and custom formatter output is inserted as HTML.
165
+ Only return trusted content or sanitize user-provided strings before returning
166
+ them from formatter callbacks.
167
+
132
168
  ### Example
133
169
 
134
170
  ```javascript
@@ -76,6 +76,41 @@ const chart = new XYChart({
76
76
  });
77
77
  ```
78
78
 
79
+ ## Lazy Loading
80
+
81
+ When a page contains charts further down the document, you can defer the chart
82
+ imports until the container is near the viewport.
83
+
84
+ ```typescript
85
+ import { mountChartWhenVisible } from '@internetstiftelsen/charts/lazy-mount';
86
+
87
+ const lazyChart = mountChartWhenVisible(
88
+ '#chart-container',
89
+ async (container) => {
90
+ const [{ XYChart }, { Line }, { XAxis }, { YAxis }] = await Promise.all([import('@internetstiftelsen/charts/xy-chart'), import('@internetstiftelsen/charts/line'), import('@internetstiftelsen/charts/x-axis'), import('@internetstiftelsen/charts/y-axis')]);
91
+
92
+ const chart = new XYChart({ data });
93
+ chart
94
+ .addChild(new XAxis({ dataKey: 'date' }))
95
+ .addChild(new YAxis())
96
+ .addChild(new Line({ dataKey: 'revenue' }));
97
+
98
+ chart.render(container);
99
+ return chart;
100
+ },
101
+ {
102
+ rootMargin: '240px 0px',
103
+ },
104
+ );
105
+
106
+ // Optional if you want to trigger the load early
107
+ await lazyChart.load();
108
+ ```
109
+
110
+ `mountChartWhenVisible` keeps the loading strategy separate from chart
111
+ construction, which makes it a good building block for custom wrappers or
112
+ attribute-driven mounting later on.
113
+
79
114
  ## Grouping Charts
80
115
 
81
116
  ```javascript
package/docs/theming.md CHANGED
@@ -27,6 +27,14 @@ const chart = new XYChart({
27
27
  fontFamily: 'Inter, sans-serif',
28
28
  fontSize: 12,
29
29
  },
30
+ tooltip: {
31
+ background: '#102030',
32
+ border: '#405060',
33
+ color: '#f7fafc',
34
+ fontFamily: 'Inter, sans-serif',
35
+ fontSize: 13,
36
+ fontWeight: '600',
37
+ },
30
38
  },
31
39
  });
32
40
  ```
@@ -51,6 +59,12 @@ import { defaultTheme, newspaperTheme, themes } from '@internetstiftelsen/charts
51
59
  | `legend.paddingX` | `number` | `0` | Horizontal legend layout padding |
52
60
  | `legend.itemSpacingX` | `number` | `20` | Horizontal spacing between legend items |
53
61
  | `legend.itemSpacingY` | `number` | `8` | Vertical spacing between wrapped legend rows |
62
+ | `tooltip.background` | `string` | `'#ffffff'` | Tooltip box and arrow fill color |
63
+ | `tooltip.border` | `string` | `'#dddddd'` | Tooltip box, connector, and arrow border |
64
+ | `tooltip.color` | `string` | `'#1f2a36'` | Tooltip text color |
65
+ | `tooltip.fontFamily` | `string` | - | Tooltip text font |
66
+ | `tooltip.fontSize` | `number` | `12` | Tooltip text size in pixels |
67
+ | `tooltip.fontWeight` | `string` | `'normal'` | Tooltip text weight |
54
68
 
55
69
  ---
56
70