@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.
- package/README.md +64 -0
- package/dist/area.d.ts +9 -1
- package/dist/area.js +174 -38
- package/dist/bar.d.ts +9 -1
- package/dist/bar.js +130 -47
- package/dist/base-chart.js +11 -1
- package/dist/donut-chart.js +3 -18
- package/dist/gauge-chart.d.ts +3 -4
- package/dist/gauge-chart.js +7 -53
- package/dist/lazy-mount.d.ts +13 -0
- package/dist/lazy-mount.js +90 -0
- package/dist/line.d.ts +9 -1
- package/dist/line.js +141 -23
- package/dist/pie-chart.js +5 -29
- package/dist/radial-chart-base.d.ts +4 -3
- package/dist/radial-chart-base.js +27 -12
- package/dist/scatter.d.ts +5 -1
- package/dist/scatter.js +89 -8
- package/dist/theme.js +17 -0
- package/dist/tooltip.d.ts +55 -3
- package/dist/tooltip.js +950 -137
- package/dist/types.d.ts +20 -0
- package/dist/xy-animation.d.ts +3 -0
- package/dist/xy-animation.js +2 -0
- package/dist/xy-chart.d.ts +11 -1
- package/dist/xy-chart.js +107 -10
- package/dist/xy-motion/config.d.ts +2 -0
- package/dist/xy-motion/config.js +177 -0
- package/dist/xy-motion/driver.d.ts +9 -0
- package/dist/xy-motion/driver.js +10 -0
- package/dist/xy-motion/helpers.d.ts +17 -0
- package/dist/xy-motion/helpers.js +105 -0
- package/dist/xy-motion/live-state.d.ts +8 -0
- package/dist/xy-motion/live-state.js +240 -0
- package/dist/xy-motion/noop-xy-motion-driver.d.ts +9 -0
- package/dist/xy-motion/noop-xy-motion-driver.js +15 -0
- package/dist/xy-motion/types.d.ts +85 -0
- package/dist/xy-motion/types.js +1 -0
- package/dist/xy-motion/xy-motion-driver.d.ts +19 -0
- package/dist/xy-motion/xy-motion-driver.js +130 -0
- package/docs/components.md +36 -0
- package/docs/getting-started.md +35 -0
- package/docs/theming.md +14 -0
- package/docs/xy-chart.md +67 -1
- package/package.json +1 -1
package/dist/donut-chart.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
if (tooltipDiv && !tooltipDiv.empty()) {
|
|
281
|
-
tooltipDiv.style('visibility', 'hidden');
|
|
282
|
-
}
|
|
267
|
+
this.hideTooltip();
|
|
283
268
|
});
|
|
284
269
|
return {
|
|
285
270
|
segmentGroup,
|
package/dist/gauge-chart.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DataItem, LegendSeries } from './types.js';
|
|
2
|
-
import { BaseChart,
|
|
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
|
|
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
|
}
|
package/dist/gauge-chart.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { arc, easeBounceOut, easeCubicIn, easeCubicInOut, easeCubicOut, easeElasticOut, easeLinear, interpolateNumber,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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):
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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(
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
183
|
+
const isInitialAnimation = animation?.mode === 'initial';
|
|
184
|
+
const circles = plotGroup
|
|
94
185
|
.selectAll(`.circle-${sanitizedKey}`)
|
|
95
|
-
.data(
|
|
186
|
+
.data(validLineData)
|
|
96
187
|
.join('circle')
|
|
97
188
|
.attr('class', `circle-${sanitizedKey}`)
|
|
98
|
-
.attr('cx',
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
21
|
-
protected
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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(
|
|
50
|
+
this.applyTooltipPosition(x, y, rect.height, rect.width);
|
|
42
51
|
}
|
|
43
|
-
|
|
44
|
-
|
|
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 =
|
|
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(
|
|
77
|
+
this.applyTooltipPosition(x, y, tooltipRect.height, tooltipRect.width);
|
|
78
|
+
}
|
|
79
|
+
hideTooltip() {
|
|
80
|
+
this.tooltip?.hide();
|
|
66
81
|
}
|
|
67
|
-
applyTooltipPosition(
|
|
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
|
-
|
|
85
|
+
this.tooltip?.showAt(x, y);
|
|
71
86
|
}
|
|
72
87
|
resolveRadialFontScale(outerRadius, theme) {
|
|
73
88
|
const referenceHeight = this.configuredHeight ?? DEFAULT_CHART_HEIGHT;
|