@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
@@ -235,9 +235,6 @@ export class DonutChart extends RadialChartBase {
235
235
  .append('g')
236
236
  .attr('class', 'donut-segments')
237
237
  .attr('transform', `translate(${cx}, ${cy})`);
238
- const resolveTooltipDiv = () => this.tooltip
239
- ? select(`#${this.tooltip.id}`)
240
- : null;
241
238
  segmentGroup
242
239
  .selectAll('.donut-segment')
243
240
  .data(pieData)
@@ -256,19 +253,10 @@ export class DonutChart extends RadialChartBase {
256
253
  .selectAll('.donut-segment')
257
254
  .filter((_, i, nodes) => nodes[i] !== event.currentTarget)
258
255
  .style('opacity', 0.5);
259
- const tooltipDiv = resolveTooltipDiv();
260
- if (tooltipDiv && !tooltipDiv.empty()) {
261
- tooltipDiv
262
- .style('visibility', 'visible')
263
- .html(this.buildTooltipContent(d, segments));
264
- this.positionTooltipFromPointer(event, tooltipDiv);
265
- }
256
+ this.showTooltipFromPointer(event, this.buildTooltipContent(d, segments));
266
257
  })
267
258
  .on('mousemove', (event) => {
268
- const tooltipDiv = resolveTooltipDiv();
269
- if (tooltipDiv && !tooltipDiv.empty()) {
270
- this.positionTooltipFromPointer(event, tooltipDiv);
271
- }
259
+ this.positionTooltipFromPointer(event);
272
260
  })
273
261
  .on('mouseleave', (event, d) => {
274
262
  select(event.currentTarget)
@@ -276,10 +264,7 @@ export class DonutChart extends RadialChartBase {
276
264
  .duration(ANIMATION_DURATION_MS)
277
265
  .attr('d', arcGenerator(d));
278
266
  segmentGroup.selectAll('.donut-segment').style('opacity', 1);
279
- const tooltipDiv = resolveTooltipDiv();
280
- if (tooltipDiv && !tooltipDiv.empty()) {
281
- tooltipDiv.style('visibility', 'hidden');
282
- }
267
+ this.hideTooltip();
283
268
  });
284
269
  return {
285
270
  segmentGroup,
@@ -1,6 +1,7 @@
1
1
  import type { DataItem, LegendSeries } from './types.js';
2
- import { BaseChart, type BaseChartConfig, type BaseRenderContext } from './base-chart.js';
2
+ import type { BaseChart, BaseChartConfig, BaseRenderContext } from './base-chart.js';
3
3
  import type { ChartComponentBase } from './chart-interface.js';
4
+ import { RadialChartBase } from './radial-chart-base.js';
4
5
  export type GaugeSegment = {
5
6
  from: number;
6
7
  to: number;
@@ -70,7 +71,7 @@ export type GaugeChartConfig = BaseChartConfig & {
70
71
  valueKey?: string;
71
72
  targetValueKey?: string;
72
73
  };
73
- export declare class GaugeChart extends BaseChart {
74
+ export declare class GaugeChart extends RadialChartBase {
74
75
  private readonly configuredValue;
75
76
  private readonly configuredTargetValue;
76
77
  private readonly configuredSegments;
@@ -164,8 +165,6 @@ export declare class GaugeChart extends BaseChart {
164
165
  private renderCurrentValueMarker;
165
166
  private renderValueText;
166
167
  private attachTooltipLayer;
167
- private resolveTooltipDiv;
168
168
  private buildTooltipContent;
169
- private positionTooltip;
170
169
  protected getLegendSeries(): LegendSeries[];
171
170
  }
@@ -1,7 +1,7 @@
1
- import { arc, easeBounceOut, easeCubicIn, easeCubicInOut, easeCubicOut, easeElasticOut, easeLinear, interpolateNumber, select, } from 'd3';
2
- import { BaseChart, } from './base-chart.js';
1
+ import { arc, easeBounceOut, easeCubicIn, easeCubicInOut, easeCubicOut, easeElasticOut, easeLinear, interpolateNumber, } from 'd3';
3
2
  import { DEFAULT_COLOR_PALETTE } from './theme.js';
4
3
  import { ChartValidator } from './validation.js';
4
+ import { RadialChartBase } from './radial-chart-base.js';
5
5
  function resolveDefault(value, fallback) {
6
6
  return value === undefined ? fallback : value;
7
7
  }
@@ -49,8 +49,6 @@ const DEFAULT_PROGRESS_RADIUS_INSET = 2;
49
49
  const MIN_PROGRESS_BAND_THICKNESS = 1;
50
50
  const DEFAULT_HALF_CIRCLE_VALUE_TEXT_Y = 32;
51
51
  const DEFAULT_FULL_CIRCLE_VALUE_TEXT_MIN_Y = 22;
52
- const TOOLTIP_OFFSET_PX = 12;
53
- const EDGE_MARGIN_PX = 10;
54
52
  const GAUGE_ANIMATION_EASING_PRESETS = {
55
53
  linear: easeLinear,
56
54
  'ease-in': easeCubicIn,
@@ -65,7 +63,7 @@ const DUMMY_ARC_DATUM = {
65
63
  startAngle: 0,
66
64
  endAngle: 0,
67
65
  };
68
- export class GaugeChart extends BaseChart {
66
+ export class GaugeChart extends RadialChartBase {
69
67
  constructor(config) {
70
68
  super(config);
71
69
  Object.defineProperty(this, "configuredValue", {
@@ -786,9 +784,7 @@ export class GaugeChart extends BaseChart {
786
784
  renderChart({ svg, plotGroup, plotArea, }) {
787
785
  svg.attr('role', 'img').attr('aria-label', this.buildAriaLabel());
788
786
  this.renderTitle(svg);
789
- if (this.tooltip) {
790
- this.tooltip.initialize(this.renderTheme);
791
- }
787
+ this.initializeTooltip();
792
788
  const { centerX, centerY, innerRadius, outerRadius } = this.resolveGaugeGeometry(plotArea);
793
789
  const gaugeGroup = plotGroup
794
790
  .append('g')
@@ -1276,39 +1272,15 @@ export class GaugeChart extends BaseChart {
1276
1272
  .style('pointer-events', 'all')
1277
1273
  .style('cursor', 'pointer')
1278
1274
  .on('mouseenter', (event) => {
1279
- const tooltipDiv = this.resolveTooltipDiv();
1280
- if (!tooltipDiv) {
1281
- return;
1282
- }
1283
- tooltipDiv
1284
- .style('visibility', 'visible')
1285
- .html(this.buildTooltipContent(progressColor));
1286
- this.positionTooltip(event, tooltipDiv);
1275
+ this.showTooltipFromPointer(event, this.buildTooltipContent(progressColor));
1287
1276
  })
1288
1277
  .on('mousemove', (event) => {
1289
- const tooltipDiv = this.resolveTooltipDiv();
1290
- if (!tooltipDiv) {
1291
- return;
1292
- }
1293
- tooltipDiv
1294
- .style('visibility', 'visible')
1295
- .html(this.buildTooltipContent(progressColor));
1296
- this.positionTooltip(event, tooltipDiv);
1278
+ this.showTooltipFromPointer(event, this.buildTooltipContent(progressColor));
1297
1279
  })
1298
1280
  .on('mouseleave', () => {
1299
- const tooltipDiv = this.resolveTooltipDiv();
1300
- if (!tooltipDiv) {
1301
- return;
1302
- }
1303
- tooltipDiv.style('visibility', 'hidden');
1281
+ this.hideTooltip();
1304
1282
  });
1305
1283
  }
1306
- resolveTooltipDiv() {
1307
- if (!this.tooltip) {
1308
- return null;
1309
- }
1310
- return select(`#${this.tooltip.id}`);
1311
- }
1312
1284
  buildTooltipContent(progressColor) {
1313
1285
  const payload = {
1314
1286
  value: this.value,
@@ -1338,24 +1310,6 @@ export class GaugeChart extends BaseChart {
1338
1310
  }
1339
1311
  return content;
1340
1312
  }
1341
- positionTooltip(event, tooltipDiv) {
1342
- const node = tooltipDiv.node();
1343
- if (!node) {
1344
- return;
1345
- }
1346
- const rect = node.getBoundingClientRect();
1347
- let x = event.pageX + TOOLTIP_OFFSET_PX;
1348
- let y = event.pageY - rect.height / 2;
1349
- if (x + rect.width > window.innerWidth - EDGE_MARGIN_PX) {
1350
- x = event.pageX - rect.width - TOOLTIP_OFFSET_PX;
1351
- }
1352
- x = Math.max(EDGE_MARGIN_PX, x);
1353
- y = Math.max(EDGE_MARGIN_PX, Math.min(y, window.innerHeight +
1354
- window.scrollY -
1355
- rect.height -
1356
- EDGE_MARGIN_PX));
1357
- tooltipDiv.style('left', `${x}px`).style('top', `${y}px`);
1358
- }
1359
1313
  getLegendSeries() {
1360
1314
  return this.segments.map((segment) => {
1361
1315
  return {
@@ -0,0 +1,13 @@
1
+ export type LazyMountableChart = {
2
+ destroy: () => void;
3
+ };
4
+ export type LazyChartFactory<TChart extends LazyMountableChart> = (container: HTMLElement) => TChart | Promise<TChart>;
5
+ export type LazyChartMountOptions = Pick<IntersectionObserverInit, 'root' | 'rootMargin' | 'threshold'> & {
6
+ onError?: (error: unknown) => void;
7
+ };
8
+ export type LazyChartMountHandle<TChart extends LazyMountableChart> = {
9
+ load: () => Promise<TChart | null>;
10
+ destroy: () => void;
11
+ getChart: () => TChart | null;
12
+ };
13
+ export declare function mountChartWhenVisible<TChart extends LazyMountableChart>(target: string | HTMLElement, factory: LazyChartFactory<TChart>, options?: LazyChartMountOptions): LazyChartMountHandle<TChart>;
@@ -0,0 +1,90 @@
1
+ const DEFAULT_ROOT_MARGIN = '200px 0px';
2
+ const DEFAULT_THRESHOLD = 0.01;
3
+ function resolveTarget(target) {
4
+ if (target instanceof HTMLElement) {
5
+ return target;
6
+ }
7
+ const container = document.querySelector(target);
8
+ if (!container) {
9
+ throw new Error(`Container "${target}" not found`);
10
+ }
11
+ if (!(container instanceof HTMLElement)) {
12
+ throw new Error(`Container "${target}" is not an HTMLElement`);
13
+ }
14
+ return container;
15
+ }
16
+ export function mountChartWhenVisible(target, factory, options = {}) {
17
+ const container = resolveTarget(target);
18
+ let mountedChart = null;
19
+ let pendingLoad = null;
20
+ let observer = null;
21
+ let destroyed = false;
22
+ const disconnectObserver = () => {
23
+ observer?.disconnect();
24
+ observer = null;
25
+ };
26
+ const handleError = (error) => {
27
+ if (options.onError) {
28
+ options.onError(error);
29
+ return;
30
+ }
31
+ console.error(error);
32
+ };
33
+ const load = async () => {
34
+ if (destroyed) {
35
+ return null;
36
+ }
37
+ if (mountedChart) {
38
+ return mountedChart;
39
+ }
40
+ if (pendingLoad) {
41
+ return pendingLoad;
42
+ }
43
+ disconnectObserver();
44
+ const currentLoad = Promise.resolve(factory(container))
45
+ .then((chart) => {
46
+ if (destroyed) {
47
+ chart.destroy();
48
+ return null;
49
+ }
50
+ mountedChart = chart;
51
+ return chart;
52
+ })
53
+ .catch((error) => {
54
+ handleError(error);
55
+ throw error;
56
+ })
57
+ .finally(() => {
58
+ if (pendingLoad === currentLoad && !mountedChart) {
59
+ pendingLoad = null;
60
+ }
61
+ });
62
+ pendingLoad = currentLoad;
63
+ return currentLoad;
64
+ };
65
+ const destroy = () => {
66
+ destroyed = true;
67
+ disconnectObserver();
68
+ mountedChart?.destroy();
69
+ mountedChart = null;
70
+ };
71
+ observer = new IntersectionObserver((entries) => {
72
+ const isVisible = entries.some((entry) => {
73
+ return entry.isIntersecting || entry.intersectionRatio > 0;
74
+ });
75
+ if (!isVisible) {
76
+ return;
77
+ }
78
+ void load().catch(() => { });
79
+ }, {
80
+ root: options.root ?? null,
81
+ rootMargin: options.rootMargin ?? DEFAULT_ROOT_MARGIN,
82
+ threshold: options.threshold ?? DEFAULT_THRESHOLD,
83
+ });
84
+ observer.observe(container);
85
+ return {
86
+ load,
87
+ destroy,
88
+ getChart: () => mountedChart,
89
+ };
90
+ }
package/dist/line.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type Selection } from 'd3';
2
2
  import type { LineConfig, DataItem, D3Scale, ScaleType, ChartTheme, LineValueLabelConfig, ExportHooks, LineConfigBase } from './types.js';
3
3
  import type { ChartComponent } from './chart-interface.js';
4
+ import type { XYPointAnimationContext, XYPointSnapshot, XYSeriesRenderResult } from './xy-motion/types.js';
4
5
  export declare class Line implements ChartComponent<LineConfigBase> {
5
6
  readonly type: "line";
6
7
  readonly dataKey: string;
@@ -11,6 +12,13 @@ export declare class Line implements ChartComponent<LineConfigBase> {
11
12
  constructor(config: LineConfig);
12
13
  getExportConfig(): LineConfigBase;
13
14
  createExportComponent(override?: Partial<LineConfigBase>): ChartComponent<LineConfigBase>;
14
- render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: D3Scale, y: D3Scale, parseValue: (value: unknown) => number, xScaleType: ScaleType | undefined, theme: ChartTheme): void;
15
+ render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: D3Scale, y: D3Scale, parseValue: (value: unknown) => number, xScaleType: ScaleType | undefined, theme: ChartTheme, animation?: XYPointAnimationContext): XYSeriesRenderResult<XYPointSnapshot>;
16
+ private buildLineData;
17
+ private buildAnimatedLineData;
18
+ private createSnapshot;
19
+ private renderLinePath;
20
+ private getInitialPathValue;
21
+ private createRevealTransitions;
22
+ private renderLinePoints;
15
23
  private renderValueLabels;
16
24
  }
package/dist/line.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { line } from 'd3';
2
2
  import { sanitizeForCSS, mergeDeep } from './utils.js';
3
3
  import { getScalePosition } from './scale-utils.js';
4
+ import { buildXYDatumSnapshotKeys, createTransitionCompletionPromise, createLeftToRightRevealTransition, getEnterStaggerTiming, } from './xy-motion/helpers.js';
4
5
  export class Line {
5
6
  constructor(config) {
6
7
  Object.defineProperty(this, "type", {
@@ -60,7 +61,7 @@ export class Line {
60
61
  exportHooks: this.exportHooks,
61
62
  });
62
63
  }
63
- render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme) {
64
+ render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme, animation) {
64
65
  const getXPosition = (d) => {
65
66
  return (getScalePosition(x, d[xKey], xScaleType) +
66
67
  (x.bandwidth ? x.bandwidth() / 2 : 0));
@@ -70,41 +71,158 @@ export class Line {
70
71
  const value = d[this.dataKey];
71
72
  return value !== null && value !== undefined;
72
73
  };
73
- const lineGenerator = line()
74
- .defined(hasValidValue)
75
- .x(getXPosition)
76
- .y((d) => y(parseValue(d[this.dataKey])) || 0);
74
+ const snapshotKeys = buildXYDatumSnapshotKeys(data, xKey);
75
+ const lineData = this.buildLineData(data, snapshotKeys, y, parseValue, getXPosition, hasValidValue);
76
+ const animatedLineData = this.buildAnimatedLineData(lineData, animation);
77
+ const validLineData = lineData.filter((entry) => entry.valid);
78
+ const validAnimatedLineData = animatedLineData.filter((entry) => {
79
+ return entry.valid;
80
+ });
81
+ const transitions = [
82
+ ...this.renderLinePath(plotGroup, lineData, animatedLineData, theme, sanitizeForCSS(this.dataKey), animation),
83
+ ...this.renderLinePoints(plotGroup, validLineData, validAnimatedLineData, theme, animation),
84
+ ];
85
+ const snapshot = this.createSnapshot(validLineData);
86
+ // Render value labels if enabled (only for valid values)
87
+ if (this.valueLabel?.show) {
88
+ this.renderValueLabels(plotGroup, validLineData.map((entry) => entry.data), y, parseValue, theme, getXPosition);
89
+ }
90
+ return {
91
+ snapshot,
92
+ transitions,
93
+ };
94
+ }
95
+ buildLineData(data, snapshotKeys, y, parseValue, getXPosition, hasValidValue) {
96
+ return data.map((entry, index) => {
97
+ const valid = hasValidValue(entry);
98
+ return {
99
+ data: entry,
100
+ valid,
101
+ snapshotKey: snapshotKeys[index] ?? String(index),
102
+ x: getXPosition(entry),
103
+ y: valid ? (y(parseValue(entry[this.dataKey])) ?? 0) : 0,
104
+ };
105
+ });
106
+ }
107
+ buildAnimatedLineData(lineData, animation) {
108
+ return lineData.map((entry) => {
109
+ if (!animation || !entry.valid) {
110
+ return entry;
111
+ }
112
+ const previousSnapshot = animation.previousSnapshot?.get(entry.snapshotKey);
113
+ return {
114
+ ...entry,
115
+ x: previousSnapshot?.x ?? entry.x,
116
+ y: previousSnapshot?.y ?? animation.baselineY,
117
+ };
118
+ });
119
+ }
120
+ createSnapshot(lineData) {
121
+ const snapshot = new Map();
122
+ lineData.forEach((entry) => {
123
+ snapshot.set(entry.snapshotKey, {
124
+ x: entry.x,
125
+ y: entry.y,
126
+ });
127
+ });
128
+ return snapshot;
129
+ }
130
+ renderLinePath(plotGroup, lineData, animatedLineData, theme, sanitizedKey, animation) {
77
131
  const lineStrokeWidth = this.strokeWidth ?? theme.line.strokeWidth;
78
- const pointSize = theme.line.point.size;
79
- const pointStrokeWidth = theme.line.point.strokeWidth;
80
- const pointStrokeColor = theme.line.point.strokeColor || this.stroke;
81
- const pointColor = theme.line.point.color || this.stroke;
82
- // Add line path
83
- plotGroup
132
+ const lineGenerator = line()
133
+ .defined((entry) => entry.valid)
134
+ .x((entry) => entry.x)
135
+ .y((entry) => entry.y);
136
+ const finalPath = lineGenerator(lineData);
137
+ const linePath = plotGroup
84
138
  .append('path')
85
- .datum(data)
139
+ .datum(lineData)
140
+ .attr('class', `line-${sanitizedKey}`)
86
141
  .attr('fill', 'none')
87
142
  .attr('stroke', this.stroke)
88
143
  .attr('stroke-width', lineStrokeWidth)
89
- .attr('d', lineGenerator);
90
- // Add data point circles (only for valid values)
91
- const validData = data.filter(hasValidValue);
144
+ .attr('d', this.getInitialPathValue(finalPath, animatedLineData, lineGenerator, animation));
145
+ if (!animation || !finalPath) {
146
+ return [];
147
+ }
148
+ const revealTransitions = this.createRevealTransitions(linePath.node(), sanitizedKey, lineStrokeWidth, animation);
149
+ const transition = linePath
150
+ .transition()
151
+ .duration(animation.duration)
152
+ .ease(animation.easing)
153
+ .attr('d', finalPath);
154
+ return [
155
+ ...revealTransitions,
156
+ createTransitionCompletionPromise(transition),
157
+ ];
158
+ }
159
+ getInitialPathValue(finalPath, animatedLineData, lineGenerator, animation) {
160
+ if (animation?.mode === 'initial') {
161
+ return finalPath;
162
+ }
163
+ if (!animation) {
164
+ return finalPath;
165
+ }
166
+ return animation.previousPath ?? lineGenerator(animatedLineData);
167
+ }
168
+ createRevealTransitions(linePath, sanitizedKey, lineStrokeWidth, animation) {
169
+ if (animation.mode === 'initial') {
170
+ return createLeftToRightRevealTransition(linePath, animation.duration, animation.easing, `line-${sanitizedKey}-reveal`, lineStrokeWidth);
171
+ }
172
+ if (animation.previousRevealProgress == null) {
173
+ return [];
174
+ }
175
+ return createLeftToRightRevealTransition(linePath, animation.duration, animation.easing, `line-${sanitizedKey}-reveal`, lineStrokeWidth, animation.previousRevealProgress);
176
+ }
177
+ renderLinePoints(plotGroup, validLineData, validAnimatedLineData, theme, animation) {
178
+ const pointSize = theme.line.point.size;
179
+ const pointStrokeWidth = theme.line.point.strokeWidth;
180
+ const pointStrokeColor = theme.line.point.strokeColor || this.stroke;
181
+ const pointColor = theme.line.point.color || this.stroke;
92
182
  const sanitizedKey = sanitizeForCSS(this.dataKey);
93
- plotGroup
183
+ const isInitialAnimation = animation?.mode === 'initial';
184
+ const circles = plotGroup
94
185
  .selectAll(`.circle-${sanitizedKey}`)
95
- .data(validData)
186
+ .data(validLineData)
96
187
  .join('circle')
97
188
  .attr('class', `circle-${sanitizedKey}`)
98
- .attr('cx', getXPosition)
99
- .attr('cy', (d) => y(parseValue(d[this.dataKey])) || 0)
100
- .attr('r', pointSize)
189
+ .attr('cx', (_, index) => (isInitialAnimation
190
+ ? validLineData[index]?.x
191
+ : animation
192
+ ? validAnimatedLineData[index]?.x
193
+ : validLineData[index]?.x) ?? 0)
194
+ .attr('cy', (_, index) => (isInitialAnimation
195
+ ? validLineData[index]?.y
196
+ : animation
197
+ ? validAnimatedLineData[index]?.y
198
+ : validLineData[index]?.y) ?? 0)
199
+ .attr('r', isInitialAnimation ? 0 : pointSize)
101
200
  .attr('fill', pointColor)
102
201
  .attr('stroke', pointStrokeColor)
103
202
  .attr('stroke-width', pointStrokeWidth);
104
- // Render value labels if enabled (only for valid values)
105
- if (this.valueLabel?.show) {
106
- this.renderValueLabels(plotGroup, validData, y, parseValue, theme, getXPosition);
203
+ if (!animation) {
204
+ return [];
205
+ }
206
+ if (isInitialAnimation) {
207
+ const transition = circles
208
+ .transition()
209
+ .delay((_, index) => {
210
+ return getEnterStaggerTiming(index, validLineData.length, animation.duration).delay;
211
+ })
212
+ .duration((_, index) => {
213
+ return getEnterStaggerTiming(index, validLineData.length, animation.duration).duration;
214
+ })
215
+ .ease(animation.easing)
216
+ .attr('r', pointSize);
217
+ return [createTransitionCompletionPromise(transition)];
107
218
  }
219
+ const transition = circles
220
+ .transition()
221
+ .duration(animation.duration)
222
+ .ease(animation.easing)
223
+ .attr('cx', (_, index) => validLineData[index]?.x ?? 0)
224
+ .attr('cy', (_, index) => validLineData[index]?.y ?? 0);
225
+ return [createTransitionCompletionPromise(transition)];
108
226
  }
109
227
  renderValueLabels(plotGroup, data, y, parseValue, theme, getXPosition) {
110
228
  const config = this.valueLabel;
package/dist/pie-chart.js CHANGED
@@ -289,9 +289,6 @@ export class PieChart extends RadialChartBase {
289
289
  .attr('class', 'pie-segments')
290
290
  .attr('transform', `translate(${cx}, ${cy})`);
291
291
  const total = segments.reduce((sum, segment) => sum + segment.value, 0);
292
- const resolveTooltipDiv = () => this.tooltip
293
- ? select(`#${this.tooltip.id}`)
294
- : null;
295
292
  const segmentSelection = segmentGroup
296
293
  .selectAll('.pie-segment')
297
294
  .data(pieData)
@@ -313,19 +310,10 @@ export class PieChart extends RadialChartBase {
313
310
  segmentSelection
314
311
  .filter((_, i, nodes) => nodes[i] !== target)
315
312
  .style('opacity', 0.5);
316
- const tooltipDiv = resolveTooltipDiv();
317
- if (tooltipDiv && !tooltipDiv.empty()) {
318
- tooltipDiv
319
- .style('visibility', 'visible')
320
- .html(this.buildTooltipContent(d, segments));
321
- this.positionTooltipFromPointer(event, tooltipDiv);
322
- }
313
+ this.showTooltipFromPointer(event, this.buildTooltipContent(d, segments));
323
314
  })
324
315
  .on('mousemove', (event) => {
325
- const tooltipDiv = resolveTooltipDiv();
326
- if (tooltipDiv && !tooltipDiv.empty()) {
327
- this.positionTooltipFromPointer(event, tooltipDiv);
328
- }
316
+ this.positionTooltipFromPointer(event);
329
317
  })
330
318
  .on('mouseleave', (event, d) => {
331
319
  const target = event.currentTarget;
@@ -334,10 +322,7 @@ export class PieChart extends RadialChartBase {
334
322
  .duration(ANIMATION_DURATION_MS)
335
323
  .attr('d', arcGenerator(d));
336
324
  segmentSelection.style('opacity', 1);
337
- const tooltipDiv = resolveTooltipDiv();
338
- if (tooltipDiv && !tooltipDiv.empty()) {
339
- tooltipDiv.style('visibility', 'hidden');
340
- }
325
+ this.hideTooltip();
341
326
  })
342
327
  .on('focus', (event, d) => {
343
328
  const target = event.currentTarget;
@@ -348,13 +333,7 @@ export class PieChart extends RadialChartBase {
348
333
  segmentSelection
349
334
  .filter((_, i, nodes) => nodes[i] !== target)
350
335
  .style('opacity', 0.5);
351
- const tooltipDiv = resolveTooltipDiv();
352
- if (tooltipDiv && !tooltipDiv.empty()) {
353
- tooltipDiv
354
- .style('visibility', 'visible')
355
- .html(this.buildTooltipContent(d, segments));
356
- this.positionTooltipAtElement(target, tooltipDiv);
357
- }
336
+ this.showTooltipAtElement(target, this.buildTooltipContent(d, segments));
358
337
  })
359
338
  .on('blur', (event, d) => {
360
339
  const target = event.currentTarget;
@@ -363,10 +342,7 @@ export class PieChart extends RadialChartBase {
363
342
  .duration(ANIMATION_DURATION_MS)
364
343
  .attr('d', arcGenerator(d));
365
344
  segmentSelection.style('opacity', 1);
366
- const tooltipDiv = resolveTooltipDiv();
367
- if (tooltipDiv && !tooltipDiv.empty()) {
368
- tooltipDiv.style('visibility', 'hidden');
369
- }
345
+ this.hideTooltip();
370
346
  })
371
347
  .on('keydown', (event) => {
372
348
  this.handleSegmentKeyNavigation(event);
@@ -1,4 +1,3 @@
1
- import type { Selection } from 'd3';
2
1
  import { BaseChart } from './base-chart.js';
3
2
  import type { PlotAreaBounds } from './layout-manager.js';
4
3
  import type { LegendSeries } from './types.js';
@@ -17,8 +16,10 @@ export declare abstract class RadialChartBase extends BaseChart {
17
16
  fontScale: number;
18
17
  };
19
18
  protected getRadialLegendSeries<T extends RadialLegendItem>(items: T[]): LegendSeries[];
20
- protected positionTooltipFromPointer(event: MouseEvent, tooltipDiv: Selection<HTMLDivElement, unknown, HTMLElement, undefined>): void;
21
- protected positionTooltipAtElement(target: Element, tooltipDiv: Selection<HTMLDivElement, unknown, HTMLElement, undefined>): void;
19
+ protected showTooltipFromPointer(event: MouseEvent, content: string): void;
20
+ protected positionTooltipFromPointer(event: MouseEvent): void;
21
+ protected showTooltipAtElement(target: Element, content: string): void;
22
+ protected hideTooltip(): void;
22
23
  private applyTooltipPosition;
23
24
  private resolveRadialFontScale;
24
25
  }
@@ -27,26 +27,38 @@ export class RadialChartBase extends BaseChart {
27
27
  fill: item.color,
28
28
  }));
29
29
  }
30
- positionTooltipFromPointer(event, tooltipDiv) {
31
- const node = tooltipDiv.node();
32
- if (!node) {
30
+ showTooltipFromPointer(event, content) {
31
+ if (!this.tooltip) {
32
+ return;
33
+ }
34
+ this.tooltip.setContent(content);
35
+ this.positionTooltipFromPointer(event);
36
+ }
37
+ positionTooltipFromPointer(event) {
38
+ if (!this.tooltip) {
39
+ return;
40
+ }
41
+ const rect = this.tooltip.getBounds();
42
+ if (!rect) {
33
43
  return;
34
44
  }
35
- const rect = node.getBoundingClientRect();
36
45
  let x = event.pageX + TOOLTIP_OFFSET_PX;
37
46
  const y = event.pageY - rect.height / 2;
38
47
  if (x + rect.width > window.innerWidth - EDGE_MARGIN_PX) {
39
48
  x = event.pageX - rect.width - TOOLTIP_OFFSET_PX;
40
49
  }
41
- this.applyTooltipPosition(tooltipDiv, x, y, rect.height, rect.width);
50
+ this.applyTooltipPosition(x, y, rect.height, rect.width);
42
51
  }
43
- positionTooltipAtElement(target, tooltipDiv) {
44
- const node = tooltipDiv.node();
45
- if (!node) {
52
+ showTooltipAtElement(target, content) {
53
+ if (!this.tooltip) {
46
54
  return;
47
55
  }
56
+ this.tooltip.setContent(content);
48
57
  const targetRect = target.getBoundingClientRect();
49
- const tooltipRect = node.getBoundingClientRect();
58
+ const tooltipRect = this.tooltip.getBounds();
59
+ if (!tooltipRect) {
60
+ return;
61
+ }
50
62
  let x = targetRect.left +
51
63
  window.scrollX +
52
64
  targetRect.width / 2 +
@@ -62,12 +74,15 @@ export class RadialChartBase extends BaseChart {
62
74
  tooltipRect.width -
63
75
  TOOLTIP_OFFSET_PX;
64
76
  }
65
- this.applyTooltipPosition(tooltipDiv, x, y, tooltipRect.height, tooltipRect.width);
77
+ this.applyTooltipPosition(x, y, tooltipRect.height, tooltipRect.width);
78
+ }
79
+ hideTooltip() {
80
+ this.tooltip?.hide();
66
81
  }
67
- applyTooltipPosition(tooltipDiv, rawX, rawY, height, width) {
82
+ applyTooltipPosition(rawX, rawY, height, width) {
68
83
  const x = Math.max(EDGE_MARGIN_PX, Math.min(rawX, window.innerWidth + window.scrollX - width - EDGE_MARGIN_PX));
69
84
  const y = Math.max(EDGE_MARGIN_PX, Math.min(rawY, window.innerHeight + window.scrollY - height - EDGE_MARGIN_PX));
70
- tooltipDiv.style('left', `${x}px`).style('top', `${y}px`);
85
+ this.tooltip?.showAt(x, y);
71
86
  }
72
87
  resolveRadialFontScale(outerRadius, theme) {
73
88
  const referenceHeight = this.configuredHeight ?? DEFAULT_CHART_HEIGHT;