@internetstiftelsen/charts 0.13.3 → 0.14.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 +4 -1
- package/dist/area.js +2 -1
- package/dist/bar.js +8 -4
- package/dist/base-chart.d.ts +1 -0
- package/dist/base-chart.js +2 -0
- package/dist/donut-chart.d.ts +19 -3
- package/dist/donut-chart.js +129 -25
- package/dist/easing.d.ts +1 -0
- package/dist/easing.js +30 -0
- package/dist/gauge-chart.d.ts +7 -2
- package/dist/gauge-chart.js +43 -18
- package/dist/line.js +2 -1
- package/dist/pie-chart.d.ts +19 -3
- package/dist/pie-chart.js +160 -59
- package/dist/radial-animation.d.ts +69 -0
- package/dist/radial-animation.js +416 -0
- package/dist/radial-chart-base.d.ts +24 -1
- package/dist/radial-chart-base.js +181 -0
- package/dist/scatter.js +2 -1
- package/dist/theme.d.ts +15 -0
- package/dist/theme.js +90 -4
- package/dist/types.d.ts +1 -0
- package/dist/xy-motion/config.js +3 -0
- package/dist/xy-motion/types.d.ts +1 -1
- package/docs/donut-chart.md +57 -14
- package/docs/gauge-chart.md +14 -0
- package/docs/pie-chart.md +58 -16
- package/docs/theming.md +17 -12
- package/docs/xy-chart.md +10 -0
- package/package.json +26 -26
package/dist/gauge-chart.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { arc, easeBounceOut, easeCubicIn, easeCubicInOut, easeCubicOut, easeElasticOut, easeLinear, interpolateNumber, } from 'd3';
|
|
2
|
+
import { createCubicBezierEasing } from './easing.js';
|
|
2
3
|
import { DEFAULT_COLOR_PALETTE } from './theme.js';
|
|
3
4
|
import { ChartValidator } from './validation.js';
|
|
4
5
|
import { RadialChartBase } from './radial-chart-base.js';
|
|
@@ -37,6 +38,7 @@ const DEFAULT_TICK_LABEL_OFFSET = 12;
|
|
|
37
38
|
const DEFAULT_TICK_LABEL_FONT_SIZE = 11;
|
|
38
39
|
const DEFAULT_TICK_LABEL_FONT_WEIGHT = 'normal';
|
|
39
40
|
const DEFAULT_TICK_LABEL_COLOR = '#4b5563';
|
|
41
|
+
const DEFAULT_LABEL_OVERSIZED_BEHAVIOR = 'truncate';
|
|
40
42
|
const DEFAULT_VALUE_LABEL_FONT_SIZE = 28;
|
|
41
43
|
const DEFAULT_VALUE_LABEL_FONT_WEIGHT = '700';
|
|
42
44
|
const DEFAULT_VALUE_LABEL_COLOR = '#111827';
|
|
@@ -49,6 +51,7 @@ const DEFAULT_PROGRESS_RADIUS_INSET = 2;
|
|
|
49
51
|
const MIN_PROGRESS_BAND_THICKNESS = 1;
|
|
50
52
|
const DEFAULT_HALF_CIRCLE_VALUE_TEXT_Y = 32;
|
|
51
53
|
const DEFAULT_FULL_CIRCLE_VALUE_TEXT_MIN_Y = 22;
|
|
54
|
+
const easeSpringOut = createCubicBezierEasing(0.85, 0, 0.15, 1);
|
|
52
55
|
const GAUGE_ANIMATION_EASING_PRESETS = {
|
|
53
56
|
linear: easeLinear,
|
|
54
57
|
'ease-in': easeCubicIn,
|
|
@@ -56,6 +59,7 @@ const GAUGE_ANIMATION_EASING_PRESETS = {
|
|
|
56
59
|
'ease-in-out': easeCubicInOut,
|
|
57
60
|
'bounce-out': easeBounceOut,
|
|
58
61
|
'elastic-out': easeElasticOut,
|
|
62
|
+
'spring-out': easeSpringOut,
|
|
59
63
|
};
|
|
60
64
|
const DUMMY_ARC_DATUM = {
|
|
61
65
|
innerRadius: 0,
|
|
@@ -537,21 +541,31 @@ export class GaugeChart extends RadialChartBase {
|
|
|
537
541
|
return lastPoint.value;
|
|
538
542
|
}
|
|
539
543
|
normalizeTickLabelStyle(config) {
|
|
540
|
-
return {
|
|
541
|
-
fontSize:
|
|
542
|
-
fontFamily:
|
|
543
|
-
fontWeight:
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
color: config?.color ?? DEFAULT_TICK_LABEL_COLOR,
|
|
547
|
-
};
|
|
544
|
+
return this.normalizeGaugeLabelStyle(config, {
|
|
545
|
+
fontSize: DEFAULT_TICK_LABEL_FONT_SIZE,
|
|
546
|
+
fontFamily: this.theme.axis.fontFamily,
|
|
547
|
+
fontWeight: this.theme.axis.fontWeight ?? DEFAULT_TICK_LABEL_FONT_WEIGHT,
|
|
548
|
+
color: DEFAULT_TICK_LABEL_COLOR,
|
|
549
|
+
});
|
|
548
550
|
}
|
|
549
551
|
normalizeValueLabelStyle(config) {
|
|
552
|
+
return this.normalizeGaugeLabelStyle(config, {
|
|
553
|
+
fontSize: DEFAULT_VALUE_LABEL_FONT_SIZE,
|
|
554
|
+
fontFamily: this.theme.axis.fontFamily,
|
|
555
|
+
fontWeight: DEFAULT_VALUE_LABEL_FONT_WEIGHT,
|
|
556
|
+
color: DEFAULT_VALUE_LABEL_COLOR,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
normalizeGaugeLabelStyle(config, defaults) {
|
|
560
|
+
const resolvedConfig = config ?? {};
|
|
550
561
|
return {
|
|
551
|
-
fontSize:
|
|
552
|
-
fontFamily:
|
|
553
|
-
fontWeight:
|
|
554
|
-
color:
|
|
562
|
+
fontSize: resolveDefault(resolvedConfig.fontSize, defaults.fontSize),
|
|
563
|
+
fontFamily: resolveDefault(resolvedConfig.fontFamily, defaults.fontFamily),
|
|
564
|
+
fontWeight: resolveDefault(resolvedConfig.fontWeight, defaults.fontWeight),
|
|
565
|
+
color: resolveDefault(resolvedConfig.color, defaults.color),
|
|
566
|
+
maxLabelWidth: resolvedConfig.maxLabelWidth,
|
|
567
|
+
oversizedBehavior: resolveDefault(resolvedConfig.oversizedBehavior, DEFAULT_LABEL_OVERSIZED_BEHAVIOR),
|
|
568
|
+
forceVisible: resolvedConfig.forceVisible ?? false,
|
|
555
569
|
};
|
|
556
570
|
}
|
|
557
571
|
validateGaugeConfig() {
|
|
@@ -1107,7 +1121,8 @@ export class GaugeChart extends RadialChartBase {
|
|
|
1107
1121
|
: labelPoint.x > 0
|
|
1108
1122
|
? 'start'
|
|
1109
1123
|
: 'end';
|
|
1110
|
-
|
|
1124
|
+
const tickLabelText = this.ticks.formatter(value);
|
|
1125
|
+
const textElement = tickGroup
|
|
1111
1126
|
.append('text')
|
|
1112
1127
|
.attr('class', 'gauge-tick-label')
|
|
1113
1128
|
.attr('x', labelPoint.x)
|
|
@@ -1117,8 +1132,8 @@ export class GaugeChart extends RadialChartBase {
|
|
|
1117
1132
|
.attr('font-size', this.tickLabelStyle.fontSize)
|
|
1118
1133
|
.attr('font-family', this.tickLabelStyle.fontFamily)
|
|
1119
1134
|
.attr('font-weight', this.tickLabelStyle.fontWeight)
|
|
1120
|
-
.attr('fill', this.tickLabelStyle.color)
|
|
1121
|
-
|
|
1135
|
+
.attr('fill', this.tickLabelStyle.color);
|
|
1136
|
+
this.renderGaugeLabelText(textElement, tickLabelText, this.tickLabelStyle);
|
|
1122
1137
|
}
|
|
1123
1138
|
}
|
|
1124
1139
|
renderTargetMarker(gaugeGroup, innerRadius, outerRadius) {
|
|
@@ -1243,8 +1258,8 @@ export class GaugeChart extends RadialChartBase {
|
|
|
1243
1258
|
.attr('font-size', this.valueLabelStyle.fontSize)
|
|
1244
1259
|
.attr('font-weight', this.valueLabelStyle.fontWeight)
|
|
1245
1260
|
.attr('font-family', this.valueLabelStyle.fontFamily)
|
|
1246
|
-
.attr('fill', this.valueLabelStyle.color)
|
|
1247
|
-
|
|
1261
|
+
.attr('fill', this.valueLabelStyle.color);
|
|
1262
|
+
this.renderGaugeLabelText(valueText, this.valueFormatter(initialValue), this.valueLabelStyle, 'baseline');
|
|
1248
1263
|
if (shouldAnimate) {
|
|
1249
1264
|
valueText
|
|
1250
1265
|
.transition()
|
|
@@ -1253,11 +1268,21 @@ export class GaugeChart extends RadialChartBase {
|
|
|
1253
1268
|
.tween('text', () => {
|
|
1254
1269
|
return (progress) => {
|
|
1255
1270
|
const currentValue = startValue + (this.value - startValue) * progress;
|
|
1256
|
-
|
|
1271
|
+
this.renderGaugeLabelText(valueText, this.valueFormatter(currentValue), this.valueLabelStyle, 'baseline');
|
|
1257
1272
|
};
|
|
1258
1273
|
});
|
|
1259
1274
|
}
|
|
1260
1275
|
}
|
|
1276
|
+
renderGaugeLabelText(textElement, text, labelStyle, verticalAnchor = 'middle') {
|
|
1277
|
+
this.renderRadialLabelText(textElement, text, {
|
|
1278
|
+
maxLabelWidth: labelStyle.maxLabelWidth,
|
|
1279
|
+
oversizedBehavior: labelStyle.oversizedBehavior,
|
|
1280
|
+
forceVisible: labelStyle.forceVisible,
|
|
1281
|
+
fontSize: labelStyle.fontSize,
|
|
1282
|
+
fontFamily: labelStyle.fontFamily,
|
|
1283
|
+
fontWeight: labelStyle.fontWeight,
|
|
1284
|
+
}, verticalAnchor);
|
|
1285
|
+
}
|
|
1261
1286
|
attachTooltipLayer(gaugeGroup, innerRadius, outerRadius, progressColor) {
|
|
1262
1287
|
const interactionArc = arc()
|
|
1263
1288
|
.innerRadius(Math.max(0, innerRadius - 20))
|
package/dist/line.js
CHANGED
|
@@ -267,6 +267,7 @@ export class Line {
|
|
|
267
267
|
const border = config.border ?? theme.valueLabel.border;
|
|
268
268
|
const borderRadius = config.borderRadius ?? theme.valueLabel.borderRadius;
|
|
269
269
|
const padding = config.padding ?? theme.valueLabel.padding;
|
|
270
|
+
const forceVisible = config.forceVisible === true;
|
|
270
271
|
const labelGroup = plotGroup
|
|
271
272
|
.append('g')
|
|
272
273
|
.attr('class', `line-value-labels-${sanitizeForCSS(this.dataKey)}`);
|
|
@@ -299,7 +300,7 @@ export class Line {
|
|
|
299
300
|
labelY = yPos + boxHeight / 2 + theme.line.point.size + 4;
|
|
300
301
|
// Check if it fits below
|
|
301
302
|
if (labelY + boxHeight / 2 > plotBottom - 4) {
|
|
302
|
-
shouldRender =
|
|
303
|
+
shouldRender = forceVisible;
|
|
303
304
|
}
|
|
304
305
|
}
|
|
305
306
|
tempText.remove();
|
package/dist/pie-chart.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import type { DataItem, LegendSeries } from './types.js';
|
|
1
|
+
import type { DataItem, LabelOversizedBehavior, LegendSeries } from './types.js';
|
|
2
2
|
import { type BaseChartConfig, type BaseRenderContext } from './base-chart.js';
|
|
3
3
|
import type { ChartComponentBase } from './chart-interface.js';
|
|
4
4
|
import { RadialChartBase } from './radial-chart-base.js';
|
|
5
|
+
import { type RadialAnimationConfig, type RadialAnimationEasingPreset } from './radial-animation.js';
|
|
6
|
+
export type PieAnimationConfig = RadialAnimationConfig;
|
|
7
|
+
export type PieAnimationEasingPreset = RadialAnimationEasingPreset;
|
|
5
8
|
export type PieSort = 'none' | 'ascending' | 'descending' | ((a: PieSegmentData, b: PieSegmentData) => number);
|
|
6
9
|
export type PieConfig = {
|
|
7
10
|
innerRadius?: number;
|
|
@@ -12,6 +15,7 @@ export type PieConfig = {
|
|
|
12
15
|
sort?: PieSort;
|
|
13
16
|
};
|
|
14
17
|
export type PieValueLabelPosition = 'inside' | 'outside' | 'auto';
|
|
18
|
+
export type PieValueLabelFormatter = (label: string, value: number, data: DataItem, percentage: number) => string;
|
|
15
19
|
export type PieValueLabelConfig = {
|
|
16
20
|
show?: boolean;
|
|
17
21
|
position?: PieValueLabelPosition;
|
|
@@ -19,11 +23,18 @@ export type PieValueLabelConfig = {
|
|
|
19
23
|
outsideOffset?: number;
|
|
20
24
|
insideMargin?: number;
|
|
21
25
|
minVerticalSpacing?: number;
|
|
22
|
-
|
|
26
|
+
maxLabelWidth?: number;
|
|
27
|
+
oversizedBehavior?: LabelOversizedBehavior;
|
|
28
|
+
forceVisible?: boolean;
|
|
29
|
+
labelFormatter?: PieValueLabelFormatter;
|
|
30
|
+
valueFormatter?: PieValueLabelFormatter;
|
|
31
|
+
separator?: string;
|
|
32
|
+
formatter?: PieValueLabelFormatter;
|
|
23
33
|
};
|
|
24
34
|
export type PieChartConfig = BaseChartConfig & {
|
|
25
35
|
pie?: PieConfig;
|
|
26
36
|
valueLabel?: PieValueLabelConfig;
|
|
37
|
+
animate?: boolean | PieAnimationConfig;
|
|
27
38
|
valueKey?: string;
|
|
28
39
|
labelKey?: string;
|
|
29
40
|
};
|
|
@@ -43,6 +54,7 @@ export declare class PieChart extends RadialChartBase {
|
|
|
43
54
|
private readonly valueKey;
|
|
44
55
|
private readonly labelKey;
|
|
45
56
|
private readonly valueLabel;
|
|
57
|
+
private readonly motionController;
|
|
46
58
|
private segments;
|
|
47
59
|
constructor(config: PieChartConfig);
|
|
48
60
|
private validatePieData;
|
|
@@ -53,7 +65,9 @@ export declare class PieChart extends RadialChartBase {
|
|
|
53
65
|
private warnOnTinySlices;
|
|
54
66
|
protected getExportComponents(): ChartComponentBase[];
|
|
55
67
|
update(data: DataItem[]): void;
|
|
56
|
-
|
|
68
|
+
protected prepareForLegendChange(): void;
|
|
69
|
+
private resolveValueLabelText;
|
|
70
|
+
private getValueLabelPercentage;
|
|
57
71
|
protected createExportChart(): RadialChartBase;
|
|
58
72
|
protected syncDerivedState(): void;
|
|
59
73
|
protected renderChart({ svg, plotGroup, plotArea, }: BaseRenderContext): void;
|
|
@@ -69,6 +83,8 @@ export declare class PieChart extends RadialChartBase {
|
|
|
69
83
|
private resolveValueLabelPlacement;
|
|
70
84
|
private canFitInsideLabel;
|
|
71
85
|
private resolveOutsideLabel;
|
|
86
|
+
private measureValueLabelDimensions;
|
|
72
87
|
private adjustOutsideLabelPositions;
|
|
88
|
+
private getOutsideLabelSpacing;
|
|
73
89
|
}
|
|
74
90
|
export {};
|
package/dist/pie-chart.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { arc, pie, select } from 'd3';
|
|
2
2
|
import { ChartValidator } from './validation.js';
|
|
3
|
-
import { getContrastTextColor,
|
|
3
|
+
import { getContrastTextColor, sanitizeForCSS } from './utils.js';
|
|
4
4
|
import { RadialChartBase } from './radial-chart-base.js';
|
|
5
|
+
import { buildRadialAnimatedArcData, buildRadialExitTargetPieData, createTransitionCompletionPromise, interpolateRadialArcDatum, interpolateRadialArcShape, normalizeRadialAnimationConfig, renderRadialArcDatum, RadialMotionController, } from './radial-animation.js';
|
|
5
6
|
const HOVER_EXPAND_PX = 8;
|
|
6
|
-
const
|
|
7
|
+
const HOVER_ANIMATION_DURATION_MS = 150;
|
|
7
8
|
const FULL_CIRCLE_RADIANS = Math.PI * 2;
|
|
8
9
|
const OUTSIDE_LABEL_TEXT_OFFSET_PX = 10;
|
|
9
10
|
const OUTSIDE_LABEL_LINE_INSET_PX = 4;
|
|
@@ -15,9 +16,9 @@ const DEFAULT_PIE_VALUE_LABEL = {
|
|
|
15
16
|
outsideOffset: 16,
|
|
16
17
|
insideMargin: 8,
|
|
17
18
|
minVerticalSpacing: 14,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
oversizedBehavior: 'truncate',
|
|
20
|
+
forceVisible: false,
|
|
21
|
+
separator: ': ',
|
|
21
22
|
};
|
|
22
23
|
export class PieChart extends RadialChartBase {
|
|
23
24
|
constructor(config) {
|
|
@@ -76,6 +77,12 @@ export class PieChart extends RadialChartBase {
|
|
|
76
77
|
writable: true,
|
|
77
78
|
value: void 0
|
|
78
79
|
});
|
|
80
|
+
Object.defineProperty(this, "motionController", {
|
|
81
|
+
enumerable: true,
|
|
82
|
+
configurable: true,
|
|
83
|
+
writable: true,
|
|
84
|
+
value: void 0
|
|
85
|
+
});
|
|
79
86
|
Object.defineProperty(this, "segments", {
|
|
80
87
|
enumerable: true,
|
|
81
88
|
configurable: true,
|
|
@@ -108,6 +115,7 @@ export class PieChart extends RadialChartBase {
|
|
|
108
115
|
...DEFAULT_PIE_VALUE_LABEL,
|
|
109
116
|
...config.valueLabel,
|
|
110
117
|
};
|
|
118
|
+
this.motionController = new RadialMotionController(normalizeRadialAnimationConfig(config.animate, 'PieChart'));
|
|
111
119
|
this.initializeDataState();
|
|
112
120
|
}
|
|
113
121
|
validatePieData() {
|
|
@@ -203,10 +211,36 @@ export class PieChart extends RadialChartBase {
|
|
|
203
211
|
});
|
|
204
212
|
}
|
|
205
213
|
update(data) {
|
|
214
|
+
this.motionController.prepareForUpdate();
|
|
206
215
|
super.update(data);
|
|
207
216
|
}
|
|
208
|
-
|
|
209
|
-
|
|
217
|
+
prepareForLegendChange() {
|
|
218
|
+
this.motionController.prepareForUpdate();
|
|
219
|
+
}
|
|
220
|
+
resolveValueLabelText(segment, total) {
|
|
221
|
+
const percentage = this.getValueLabelPercentage(segment, total);
|
|
222
|
+
if (this.valueLabel.formatter) {
|
|
223
|
+
const fullText = this.valueLabel.formatter(segment.label, segment.value, segment.source, percentage);
|
|
224
|
+
return {
|
|
225
|
+
label: fullText,
|
|
226
|
+
value: '',
|
|
227
|
+
separator: '',
|
|
228
|
+
fullText,
|
|
229
|
+
isCustom: true,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const label = this.valueLabel.labelFormatter?.(segment.label, segment.value, segment.source, percentage) ?? segment.label;
|
|
233
|
+
const value = this.valueLabel.valueFormatter?.(segment.label, segment.value, segment.source, percentage) ?? String(segment.value);
|
|
234
|
+
return {
|
|
235
|
+
label,
|
|
236
|
+
value,
|
|
237
|
+
separator: this.valueLabel.separator,
|
|
238
|
+
fullText: `${label}${this.valueLabel.separator}${value}`,
|
|
239
|
+
isCustom: false,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
getValueLabelPercentage(segment, total) {
|
|
243
|
+
return total > 0 ? (segment.value / total) * 100 : 0;
|
|
210
244
|
}
|
|
211
245
|
createExportChart() {
|
|
212
246
|
return new PieChart({
|
|
@@ -224,6 +258,7 @@ export class PieChart extends RadialChartBase {
|
|
|
224
258
|
sort: this.sort,
|
|
225
259
|
},
|
|
226
260
|
valueLabel: this.valueLabel,
|
|
261
|
+
animate: false,
|
|
227
262
|
valueKey: this.valueKey,
|
|
228
263
|
labelKey: this.labelKey,
|
|
229
264
|
});
|
|
@@ -235,16 +270,16 @@ export class PieChart extends RadialChartBase {
|
|
|
235
270
|
renderChart({ svg, plotGroup, plotArea, }) {
|
|
236
271
|
this.renderTitle(svg);
|
|
237
272
|
const visibleSegments = this.getVisibleRadialItems(this.segments);
|
|
273
|
+
const animationContext = this.motionController.getAnimationContext();
|
|
274
|
+
const { cx, cy, outerRadius, innerRadius, fontScale } = this.getRadialLayout(plotArea, this.innerRadiusRatio);
|
|
238
275
|
this.initializeTooltip();
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return sum + segment.value;
|
|
245
|
-
}, 0), fontScale);
|
|
246
|
-
}
|
|
276
|
+
const { segmentGroup, pieData, transitions } = this.renderSegments(plotGroup, visibleSegments, cx, cy, innerRadius, outerRadius, animationContext, this.segments);
|
|
277
|
+
if (this.valueLabel.show && visibleSegments.length > 0) {
|
|
278
|
+
this.renderLabels(segmentGroup, pieData, innerRadius, outerRadius, visibleSegments.reduce((sum, segment) => {
|
|
279
|
+
return sum + segment.value;
|
|
280
|
+
}, 0), fontScale);
|
|
247
281
|
}
|
|
282
|
+
this.setReadyPromise(this.motionController.completeRender(pieData, transitions));
|
|
248
283
|
this.renderInlineLegend(svg);
|
|
249
284
|
}
|
|
250
285
|
getLegendSeries() {
|
|
@@ -262,7 +297,7 @@ export class PieChart extends RadialChartBase {
|
|
|
262
297
|
}
|
|
263
298
|
return null;
|
|
264
299
|
}
|
|
265
|
-
renderSegments(plotGroup, segments, cx, cy, innerRadius, outerRadius) {
|
|
300
|
+
renderSegments(plotGroup, segments, cx, cy, innerRadius, outerRadius, animationContext, allSegments) {
|
|
266
301
|
const pieGenerator = pie()
|
|
267
302
|
.value((d) => d.value)
|
|
268
303
|
.startAngle(this.startAngle)
|
|
@@ -284,30 +319,72 @@ export class PieChart extends RadialChartBase {
|
|
|
284
319
|
.outerRadius(outerRadius + HOVER_EXPAND_PX)
|
|
285
320
|
.cornerRadius(this.cornerRadius);
|
|
286
321
|
const pieData = pieGenerator(segments);
|
|
322
|
+
const exitTargetPieData = buildRadialExitTargetPieData(pieGenerator, segments, allSegments, animationContext, pieData);
|
|
323
|
+
const arcShape = {
|
|
324
|
+
innerRadius,
|
|
325
|
+
outerRadius,
|
|
326
|
+
cornerRadius: this.cornerRadius,
|
|
327
|
+
};
|
|
287
328
|
const segmentGroup = plotGroup
|
|
288
329
|
.append('g')
|
|
289
330
|
.attr('class', 'pie-segments')
|
|
290
331
|
.attr('transform', `translate(${cx}, ${cy})`);
|
|
291
332
|
const total = segments.reduce((sum, segment) => sum + segment.value, 0);
|
|
333
|
+
const animatedArcData = buildRadialAnimatedArcData(pieData, allSegments, animationContext, arcShape, exitTargetPieData);
|
|
334
|
+
const animationByDatum = new Map(animatedArcData.map((entry) => [entry.datum, entry]));
|
|
335
|
+
const getAnimation = (datum) => {
|
|
336
|
+
return animationByDatum.get(datum);
|
|
337
|
+
};
|
|
338
|
+
const renderedArcData = animatedArcData.map((entry) => entry.datum);
|
|
339
|
+
const transitions = [];
|
|
292
340
|
const segmentSelection = segmentGroup
|
|
293
341
|
.selectAll('.pie-segment')
|
|
294
|
-
.data(
|
|
342
|
+
.data(renderedArcData, (d) => getAnimation(d).key)
|
|
295
343
|
.join('path')
|
|
296
344
|
.attr('class', (d) => `pie-segment segment-${sanitizeForCSS(d.data.label)}`)
|
|
297
|
-
.attr('d',
|
|
345
|
+
.attr('d', (d) => {
|
|
346
|
+
const animation = getAnimation(d);
|
|
347
|
+
return renderRadialArcDatum(animation.startDatum, animation.startShape);
|
|
348
|
+
})
|
|
298
349
|
.attr('fill', (d) => d.data.color)
|
|
299
|
-
.attr('
|
|
300
|
-
.attr('
|
|
301
|
-
.
|
|
350
|
+
.attr('opacity', (d) => getAnimation(d).initialOpacity)
|
|
351
|
+
.attr('tabindex', (d) => (getAnimation(d).interactive ? 0 : null))
|
|
352
|
+
.attr('aria-hidden', (d) => getAnimation(d).interactive ? null : 'true')
|
|
353
|
+
.attr('aria-label', (d) => {
|
|
354
|
+
return getAnimation(d).interactive
|
|
355
|
+
? this.buildAriaLabel(d, total)
|
|
356
|
+
: null;
|
|
357
|
+
})
|
|
358
|
+
.style('cursor', (d) => getAnimation(d).interactive ? 'pointer' : 'default')
|
|
359
|
+
.style('pointer-events', (d) => getAnimation(d).interactive ? 'all' : 'none')
|
|
302
360
|
.style('transition', 'opacity 0.15s ease');
|
|
303
|
-
|
|
361
|
+
if (animationContext) {
|
|
362
|
+
const transition = segmentSelection
|
|
363
|
+
.transition()
|
|
364
|
+
.duration(animationContext.duration)
|
|
365
|
+
.ease(animationContext.easing)
|
|
366
|
+
.attrTween('d', (d) => {
|
|
367
|
+
const animation = getAnimation(d);
|
|
368
|
+
const datumInterpolator = interpolateRadialArcDatum(animation.startDatum, animation.endDatum);
|
|
369
|
+
const shapeInterpolator = interpolateRadialArcShape(animation.startShape, animation.endShape);
|
|
370
|
+
return (progress) => {
|
|
371
|
+
return renderRadialArcDatum(datumInterpolator(progress), shapeInterpolator(progress));
|
|
372
|
+
};
|
|
373
|
+
})
|
|
374
|
+
.attr('opacity', (d) => getAnimation(d).finalOpacity);
|
|
375
|
+
transitions.push(createTransitionCompletionPromise(transition));
|
|
376
|
+
}
|
|
377
|
+
const interactiveSegmentSelection = segmentSelection.filter((d) => {
|
|
378
|
+
return getAnimation(d).interactive;
|
|
379
|
+
});
|
|
380
|
+
interactiveSegmentSelection
|
|
304
381
|
.on('mouseenter', (event, d) => {
|
|
305
382
|
const target = event.currentTarget;
|
|
306
383
|
select(target)
|
|
307
384
|
.transition()
|
|
308
|
-
.duration(
|
|
385
|
+
.duration(HOVER_ANIMATION_DURATION_MS)
|
|
309
386
|
.attr('d', hoverArcGenerator(d));
|
|
310
|
-
|
|
387
|
+
interactiveSegmentSelection
|
|
311
388
|
.filter((_, i, nodes) => nodes[i] !== target)
|
|
312
389
|
.style('opacity', 0.5);
|
|
313
390
|
this.showTooltipFromPointer(event, this.buildTooltipContent(d, segments));
|
|
@@ -319,18 +396,18 @@ export class PieChart extends RadialChartBase {
|
|
|
319
396
|
const target = event.currentTarget;
|
|
320
397
|
select(target)
|
|
321
398
|
.transition()
|
|
322
|
-
.duration(
|
|
399
|
+
.duration(HOVER_ANIMATION_DURATION_MS)
|
|
323
400
|
.attr('d', arcGenerator(d));
|
|
324
|
-
|
|
401
|
+
interactiveSegmentSelection.style('opacity', 1);
|
|
325
402
|
this.hideTooltip();
|
|
326
403
|
})
|
|
327
404
|
.on('focus', (event, d) => {
|
|
328
405
|
const target = event.currentTarget;
|
|
329
406
|
select(target)
|
|
330
407
|
.transition()
|
|
331
|
-
.duration(
|
|
408
|
+
.duration(HOVER_ANIMATION_DURATION_MS)
|
|
332
409
|
.attr('d', hoverArcGenerator(d));
|
|
333
|
-
|
|
410
|
+
interactiveSegmentSelection
|
|
334
411
|
.filter((_, i, nodes) => nodes[i] !== target)
|
|
335
412
|
.style('opacity', 0.5);
|
|
336
413
|
this.showTooltipAtElement(target, this.buildTooltipContent(d, segments));
|
|
@@ -339,9 +416,9 @@ export class PieChart extends RadialChartBase {
|
|
|
339
416
|
const target = event.currentTarget;
|
|
340
417
|
select(target)
|
|
341
418
|
.transition()
|
|
342
|
-
.duration(
|
|
419
|
+
.duration(HOVER_ANIMATION_DURATION_MS)
|
|
343
420
|
.attr('d', arcGenerator(d));
|
|
344
|
-
|
|
421
|
+
interactiveSegmentSelection.style('opacity', 1);
|
|
345
422
|
this.hideTooltip();
|
|
346
423
|
})
|
|
347
424
|
.on('keydown', (event) => {
|
|
@@ -350,6 +427,7 @@ export class PieChart extends RadialChartBase {
|
|
|
350
427
|
return {
|
|
351
428
|
segmentGroup,
|
|
352
429
|
pieData,
|
|
430
|
+
transitions,
|
|
353
431
|
};
|
|
354
432
|
}
|
|
355
433
|
handleSegmentKeyNavigation(event) {
|
|
@@ -415,30 +493,44 @@ export class PieChart extends RadialChartBase {
|
|
|
415
493
|
.innerRadius(insideLabelRadius)
|
|
416
494
|
.outerRadius(insideLabelRadius);
|
|
417
495
|
const fontSize = this.renderTheme.legend.fontSize * fontScale;
|
|
496
|
+
const fontFamily = this.renderTheme.axis.fontFamily;
|
|
418
497
|
const fontWeight = this.renderTheme.axis.fontWeight ?? 'normal';
|
|
498
|
+
const labelOverflowOptions = {
|
|
499
|
+
maxLabelWidth: this.valueLabel.maxLabelWidth,
|
|
500
|
+
oversizedBehavior: this.valueLabel.oversizedBehavior,
|
|
501
|
+
forceVisible: this.valueLabel.forceVisible,
|
|
502
|
+
fontSize,
|
|
503
|
+
fontFamily,
|
|
504
|
+
fontWeight,
|
|
505
|
+
};
|
|
419
506
|
const outsideLabels = [];
|
|
420
507
|
pieData.forEach((d) => {
|
|
421
508
|
const percentage = total > 0 ? (d.data.value / total) * 100 : 0;
|
|
422
|
-
const
|
|
423
|
-
const
|
|
509
|
+
const valueLabel = this.resolveValueLabelText(d.data, total);
|
|
510
|
+
const labelDimensions = this.measureValueLabelDimensions(valueLabel, labelOverflowOptions);
|
|
511
|
+
const placement = this.resolveValueLabelPlacement(d, labelDimensions, percentage, innerRadius, outerRadius, insideLabelRadius);
|
|
424
512
|
if (placement === 'inside') {
|
|
425
513
|
const [x, y] = insideArc.centroid(d);
|
|
426
|
-
labelGroup
|
|
514
|
+
const textElement = labelGroup
|
|
427
515
|
.append('text')
|
|
428
516
|
.attr('class', `pie-label pie-label--inside pie-label-${sanitizeForCSS(d.data.label)}`)
|
|
429
517
|
.attr('x', x)
|
|
430
518
|
.attr('y', y)
|
|
431
519
|
.attr('text-anchor', 'middle')
|
|
432
520
|
.attr('dominant-baseline', 'middle')
|
|
433
|
-
.attr('font-family',
|
|
521
|
+
.attr('font-family', fontFamily)
|
|
434
522
|
.attr('font-size', `${fontSize}px`)
|
|
435
523
|
.attr('font-weight', fontWeight)
|
|
436
|
-
.attr('fill', getContrastTextColor(d.data.color))
|
|
437
|
-
|
|
524
|
+
.attr('fill', getContrastTextColor(d.data.color));
|
|
525
|
+
if (valueLabel.isCustom) {
|
|
526
|
+
this.renderRadialLabelText(textElement, valueLabel.fullText, labelOverflowOptions);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
this.renderRadialStructuredLabelText(textElement, valueLabel.label, valueLabel.value, valueLabel.separator, labelOverflowOptions);
|
|
438
530
|
return;
|
|
439
531
|
}
|
|
440
532
|
if (placement === 'outside') {
|
|
441
|
-
outsideLabels.push(this.resolveOutsideLabel(d, outerRadius));
|
|
533
|
+
outsideLabels.push(this.resolveOutsideLabel(d, outerRadius, valueLabel, labelDimensions.height));
|
|
442
534
|
}
|
|
443
535
|
});
|
|
444
536
|
if (outsideLabels.length === 0) {
|
|
@@ -468,24 +560,30 @@ export class PieChart extends RadialChartBase {
|
|
|
468
560
|
.attr('fill', 'none')
|
|
469
561
|
.attr('stroke', '#9ca3af')
|
|
470
562
|
.attr('stroke-width', 1);
|
|
471
|
-
labelGroup
|
|
563
|
+
const textElement = labelGroup
|
|
472
564
|
.append('text')
|
|
473
565
|
.attr('class', `pie-label pie-label--outside pie-label-${sanitizeForCSS(outsideLabel.datum.data.label)}`)
|
|
474
566
|
.attr('x', textX)
|
|
475
567
|
.attr('y', outsideLabel.y)
|
|
476
568
|
.attr('text-anchor', outsideLabel.textAnchor)
|
|
477
569
|
.attr('dominant-baseline', 'middle')
|
|
478
|
-
.attr('font-family',
|
|
570
|
+
.attr('font-family', fontFamily)
|
|
479
571
|
.attr('font-size', `${fontSize}px`)
|
|
480
572
|
.attr('font-weight', fontWeight)
|
|
481
|
-
.attr('fill', this.renderTheme.valueLabel.color)
|
|
482
|
-
|
|
573
|
+
.attr('fill', this.renderTheme.valueLabel.color);
|
|
574
|
+
if (outsideLabel.valueLabel.isCustom) {
|
|
575
|
+
this.renderRadialLabelText(textElement, outsideLabel.valueLabel.fullText, labelOverflowOptions);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
this.renderRadialStructuredLabelText(textElement, outsideLabel.valueLabel.label, outsideLabel.valueLabel.value, outsideLabel.valueLabel.separator, labelOverflowOptions);
|
|
483
579
|
});
|
|
484
580
|
}
|
|
485
|
-
resolveValueLabelPlacement(datum,
|
|
486
|
-
const fitsInside = this.canFitInsideLabel(datum,
|
|
581
|
+
resolveValueLabelPlacement(datum, labelDimensions, percentage, innerRadius, outerRadius, insideLabelRadius) {
|
|
582
|
+
const fitsInside = this.canFitInsideLabel(datum, labelDimensions, innerRadius, outerRadius, insideLabelRadius);
|
|
487
583
|
if (this.valueLabel.position === 'inside') {
|
|
488
|
-
return fitsInside
|
|
584
|
+
return fitsInside || this.valueLabel.forceVisible
|
|
585
|
+
? 'inside'
|
|
586
|
+
: 'hidden';
|
|
489
587
|
}
|
|
490
588
|
if (this.valueLabel.position === 'outside') {
|
|
491
589
|
return 'outside';
|
|
@@ -498,23 +596,14 @@ export class PieChart extends RadialChartBase {
|
|
|
498
596
|
}
|
|
499
597
|
return 'inside';
|
|
500
598
|
}
|
|
501
|
-
canFitInsideLabel(datum,
|
|
502
|
-
if (!this.svg) {
|
|
503
|
-
return false;
|
|
504
|
-
}
|
|
505
|
-
const svgNode = this.svg.node();
|
|
506
|
-
if (!svgNode) {
|
|
507
|
-
return false;
|
|
508
|
-
}
|
|
509
|
-
const textWidth = measureTextWidth(labelText, fontSize, this.renderTheme.axis.fontFamily, fontWeight, svgNode);
|
|
599
|
+
canFitInsideLabel(datum, labelDimensions, innerRadius, outerRadius, insideLabelRadius) {
|
|
510
600
|
const angle = Math.max(0, datum.endAngle - datum.startAngle);
|
|
511
601
|
const availableArcLength = angle * insideLabelRadius - this.valueLabel.insideMargin * 2;
|
|
512
602
|
const availableRadialThickness = outerRadius - innerRadius - this.valueLabel.insideMargin * 2;
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
availableRadialThickness >= verticalFitThreshold);
|
|
603
|
+
return (availableArcLength >= labelDimensions.width &&
|
|
604
|
+
availableRadialThickness >= labelDimensions.height);
|
|
516
605
|
}
|
|
517
|
-
resolveOutsideLabel(datum, outerRadius) {
|
|
606
|
+
resolveOutsideLabel(datum, outerRadius, valueLabel, height) {
|
|
518
607
|
const midAngle = (datum.startAngle + datum.endAngle) / 2;
|
|
519
608
|
const point = this.getArcPoint(midAngle, outerRadius + this.valueLabel.outsideOffset);
|
|
520
609
|
const side = point.x >= 0 ? 'right' : 'left';
|
|
@@ -523,8 +612,16 @@ export class PieChart extends RadialChartBase {
|
|
|
523
612
|
y: point.y,
|
|
524
613
|
side,
|
|
525
614
|
textAnchor: side === 'right' ? 'start' : 'end',
|
|
615
|
+
valueLabel,
|
|
616
|
+
height,
|
|
526
617
|
};
|
|
527
618
|
}
|
|
619
|
+
measureValueLabelDimensions(valueLabel, labelOverflowOptions) {
|
|
620
|
+
if (valueLabel.isCustom) {
|
|
621
|
+
return this.measureRadialLabelDimensions(valueLabel.fullText, labelOverflowOptions);
|
|
622
|
+
}
|
|
623
|
+
return this.measureRadialStructuredLabelDimensions(valueLabel.label, valueLabel.value, valueLabel.separator, labelOverflowOptions);
|
|
624
|
+
}
|
|
528
625
|
adjustOutsideLabelPositions(labels, outerRadius) {
|
|
529
626
|
const adjustForSide = (side) => {
|
|
530
627
|
const sideLabels = labels
|
|
@@ -537,7 +634,8 @@ export class PieChart extends RadialChartBase {
|
|
|
537
634
|
const bottomLimit = outerRadius;
|
|
538
635
|
sideLabels[0].y = Math.max(topLimit, sideLabels[0].y);
|
|
539
636
|
for (let i = 1; i < sideLabels.length; i++) {
|
|
540
|
-
const minY = sideLabels[i - 1].y +
|
|
637
|
+
const minY = sideLabels[i - 1].y +
|
|
638
|
+
this.getOutsideLabelSpacing(sideLabels[i - 1], sideLabels[i]);
|
|
541
639
|
sideLabels[i].y = Math.max(sideLabels[i].y, minY);
|
|
542
640
|
}
|
|
543
641
|
const overflow = sideLabels[sideLabels.length - 1].y - bottomLimit;
|
|
@@ -545,7 +643,7 @@ export class PieChart extends RadialChartBase {
|
|
|
545
643
|
sideLabels[sideLabels.length - 1].y -= overflow;
|
|
546
644
|
for (let i = sideLabels.length - 2; i >= 0; i--) {
|
|
547
645
|
const maxY = sideLabels[i + 1].y -
|
|
548
|
-
this.
|
|
646
|
+
this.getOutsideLabelSpacing(sideLabels[i], sideLabels[i + 1]);
|
|
549
647
|
sideLabels[i].y = Math.min(sideLabels[i].y, maxY);
|
|
550
648
|
}
|
|
551
649
|
const underflow = topLimit - sideLabels[0].y;
|
|
@@ -564,4 +662,7 @@ export class PieChart extends RadialChartBase {
|
|
|
564
662
|
const leftLabels = adjustForSide('left');
|
|
565
663
|
return [...rightLabels, ...leftLabels];
|
|
566
664
|
}
|
|
665
|
+
getOutsideLabelSpacing(previousLabel, currentLabel) {
|
|
666
|
+
return Math.max(this.valueLabel.minVerticalSpacing, (previousLabel.height + currentLabel.height) / 2);
|
|
667
|
+
}
|
|
567
668
|
}
|