@internetstiftelsen/charts 0.8.0 → 0.9.1
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/LICENSE +21 -0
- package/README.md +97 -8
- package/{area.d.ts → dist/area.d.ts} +1 -2
- package/{area.js → dist/area.js} +2 -19
- package/{bar.d.ts → dist/bar.d.ts} +3 -5
- package/{bar.js → dist/bar.js} +8 -33
- package/{base-chart.d.ts → dist/base-chart.d.ts} +75 -14
- package/{base-chart.js → dist/base-chart.js} +429 -122
- package/dist/chart-interface.d.ts +19 -0
- package/{donut-center-content.d.ts → dist/donut-center-content.d.ts} +1 -1
- package/dist/donut-chart.d.ts +51 -0
- package/dist/donut-chart.js +374 -0
- package/{gauge-chart.d.ts → dist/gauge-chart.d.ts} +19 -8
- package/{gauge-chart.js → dist/gauge-chart.js} +317 -106
- package/{grid.d.ts → dist/grid.d.ts} +1 -1
- package/{layout-manager.d.ts → dist/layout-manager.d.ts} +5 -5
- package/{legend.d.ts → dist/legend.d.ts} +3 -1
- package/{legend.js → dist/legend.js} +32 -0
- package/{line.d.ts → dist/line.d.ts} +1 -1
- package/{line.js → dist/line.js} +3 -25
- package/{pie-chart.d.ts → dist/pie-chart.d.ts} +10 -21
- package/{pie-chart.js → dist/pie-chart.js} +51 -172
- package/dist/radial-chart-base.d.ts +25 -0
- package/dist/radial-chart-base.js +79 -0
- package/dist/scale-utils.d.ts +3 -0
- package/dist/scale-utils.js +14 -0
- package/{theme.d.ts → dist/theme.d.ts} +2 -0
- package/{theme.js → dist/theme.js} +24 -29
- package/{title.d.ts → dist/title.d.ts} +1 -1
- package/{tooltip.d.ts → dist/tooltip.d.ts} +1 -1
- package/{tooltip.js → dist/tooltip.js} +239 -74
- package/{types.d.ts → dist/types.d.ts} +27 -10
- package/{utils.d.ts → dist/utils.d.ts} +7 -2
- package/{utils.js → dist/utils.js} +24 -5
- package/dist/word-cloud-chart.d.ts +32 -0
- package/dist/word-cloud-chart.js +201 -0
- package/{x-axis.d.ts → dist/x-axis.d.ts} +2 -1
- package/{x-axis.js → dist/x-axis.js} +18 -14
- package/{xy-chart.d.ts → dist/xy-chart.d.ts} +14 -9
- package/{xy-chart.js → dist/xy-chart.js} +107 -130
- package/{y-axis.d.ts → dist/y-axis.d.ts} +1 -1
- package/{y-axis.js → dist/y-axis.js} +4 -4
- package/package.json +39 -35
- package/chart-interface.d.ts +0 -13
- package/donut-chart.d.ts +0 -38
- package/donut-chart.js +0 -316
- /package/{chart-interface.js → dist/chart-interface.js} +0 -0
- /package/{donut-center-content.js → dist/donut-center-content.js} +0 -0
- /package/{export-image.d.ts → dist/export-image.d.ts} +0 -0
- /package/{export-image.js → dist/export-image.js} +0 -0
- /package/{export-pdf.d.ts → dist/export-pdf.d.ts} +0 -0
- /package/{export-pdf.js → dist/export-pdf.js} +0 -0
- /package/{export-tabular.d.ts → dist/export-tabular.d.ts} +0 -0
- /package/{export-tabular.js → dist/export-tabular.js} +0 -0
- /package/{export-xlsx.d.ts → dist/export-xlsx.d.ts} +0 -0
- /package/{export-xlsx.js → dist/export-xlsx.js} +0 -0
- /package/{grid.js → dist/grid.js} +0 -0
- /package/{grouped-data.d.ts → dist/grouped-data.d.ts} +0 -0
- /package/{grouped-data.js → dist/grouped-data.js} +0 -0
- /package/{grouped-tabular.d.ts → dist/grouped-tabular.d.ts} +0 -0
- /package/{grouped-tabular.js → dist/grouped-tabular.js} +0 -0
- /package/{layout-manager.js → dist/layout-manager.js} +0 -0
- /package/{title.js → dist/title.js} +0 -0
- /package/{types.js → dist/types.js} +0 -0
- /package/{validation.d.ts → dist/validation.d.ts} +0 -0
- /package/{validation.js → dist/validation.js} +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ExportHooks } from './types.js';
|
|
2
|
+
export type ChartComponentType = 'line' | 'area' | 'bar' | 'xAxis' | 'yAxis' | 'grid' | 'tooltip' | 'legend' | 'title' | 'donutCenterContent';
|
|
3
|
+
export interface ChartComponentBase {
|
|
4
|
+
type: ChartComponentType;
|
|
5
|
+
}
|
|
6
|
+
export interface ChartComponent<TConfig = unknown> extends ChartComponentBase {
|
|
7
|
+
type: ChartComponentType;
|
|
8
|
+
exportHooks?: ExportHooks<TConfig>;
|
|
9
|
+
}
|
|
10
|
+
export type ComponentSpace = {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
position: 'top' | 'bottom' | 'left' | 'right';
|
|
14
|
+
};
|
|
15
|
+
export interface LayoutAwareComponentBase extends ChartComponentBase {
|
|
16
|
+
getRequiredSpace(): ComponentSpace;
|
|
17
|
+
}
|
|
18
|
+
export interface LayoutAwareComponent<TConfig = unknown> extends ChartComponent<TConfig>, LayoutAwareComponentBase {
|
|
19
|
+
}
|
|
@@ -27,7 +27,7 @@ export declare class DonutCenterContent implements ChartComponent<DonutCenterCon
|
|
|
27
27
|
private readonly config;
|
|
28
28
|
constructor(config?: DonutCenterContentConfig);
|
|
29
29
|
getExportConfig(): DonutCenterContentConfigBase;
|
|
30
|
-
createExportComponent(override?: Partial<DonutCenterContentConfigBase>): ChartComponent
|
|
30
|
+
createExportComponent(override?: Partial<DonutCenterContentConfigBase>): ChartComponent<DonutCenterContentConfigBase>;
|
|
31
31
|
render(svg: Selection<SVGSVGElement, undefined, null, undefined>, cx: number, cy: number, theme: ChartTheme, fontScale?: number): void;
|
|
32
32
|
}
|
|
33
33
|
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { DataItem, LegendSeries } from './types.js';
|
|
2
|
+
import { type BaseChartConfig, type BaseRenderContext } from './base-chart.js';
|
|
3
|
+
import type { ChartComponentBase } from './chart-interface.js';
|
|
4
|
+
import { RadialChartBase } from './radial-chart-base.js';
|
|
5
|
+
export type DonutConfig = {
|
|
6
|
+
innerRadius?: number;
|
|
7
|
+
padAngle?: number;
|
|
8
|
+
cornerRadius?: number;
|
|
9
|
+
};
|
|
10
|
+
export type DonutValueLabelPosition = 'outside' | 'auto';
|
|
11
|
+
export type DonutValueLabelConfig = {
|
|
12
|
+
show?: boolean;
|
|
13
|
+
position?: DonutValueLabelPosition;
|
|
14
|
+
outsideOffset?: number;
|
|
15
|
+
minVerticalSpacing?: number;
|
|
16
|
+
formatter?: (label: string, value: number, data: DataItem, percentage: number) => string;
|
|
17
|
+
};
|
|
18
|
+
export type DonutChartConfig = BaseChartConfig & {
|
|
19
|
+
donut?: DonutConfig;
|
|
20
|
+
valueLabel?: DonutValueLabelConfig;
|
|
21
|
+
valueKey?: string;
|
|
22
|
+
labelKey?: string;
|
|
23
|
+
};
|
|
24
|
+
export declare class DonutChart extends RadialChartBase {
|
|
25
|
+
private readonly innerRadiusRatio;
|
|
26
|
+
private readonly padAngle;
|
|
27
|
+
private readonly cornerRadius;
|
|
28
|
+
private readonly valueKey;
|
|
29
|
+
private readonly labelKey;
|
|
30
|
+
private readonly valueLabel;
|
|
31
|
+
private segments;
|
|
32
|
+
private centerContent;
|
|
33
|
+
constructor(config: DonutChartConfig);
|
|
34
|
+
private validateDonutData;
|
|
35
|
+
private prepareSegments;
|
|
36
|
+
addChild(component: ChartComponentBase): this;
|
|
37
|
+
protected getExportComponents(): ChartComponentBase[];
|
|
38
|
+
update(data: DataItem[]): void;
|
|
39
|
+
protected createExportChart(): RadialChartBase;
|
|
40
|
+
protected applyComponentOverrides(overrides: Map<ChartComponentBase, ChartComponentBase>): () => void;
|
|
41
|
+
protected syncDerivedState(): void;
|
|
42
|
+
protected renderChart({ svg, plotGroup, plotArea, }: BaseRenderContext): void;
|
|
43
|
+
protected getLegendSeries(): LegendSeries[];
|
|
44
|
+
private buildTooltipContent;
|
|
45
|
+
private formatValueLabelText;
|
|
46
|
+
private renderSegments;
|
|
47
|
+
private renderLabels;
|
|
48
|
+
private getArcPoint;
|
|
49
|
+
private resolveOutsideLabel;
|
|
50
|
+
private adjustOutsideLabelPositions;
|
|
51
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { arc, pie, select } from 'd3';
|
|
2
|
+
import { sanitizeForCSS } from './utils.js';
|
|
3
|
+
import { ChartValidator } from './validation.js';
|
|
4
|
+
import { RadialChartBase } from './radial-chart-base.js';
|
|
5
|
+
const HOVER_EXPAND_PX = 8;
|
|
6
|
+
const ANIMATION_DURATION_MS = 150;
|
|
7
|
+
const OUTSIDE_LABEL_TEXT_OFFSET_PX = 10;
|
|
8
|
+
const OUTSIDE_LABEL_LINE_INSET_PX = 4;
|
|
9
|
+
export class DonutChart extends RadialChartBase {
|
|
10
|
+
constructor(config) {
|
|
11
|
+
super(config);
|
|
12
|
+
Object.defineProperty(this, "innerRadiusRatio", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
writable: true,
|
|
16
|
+
value: void 0
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(this, "padAngle", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
configurable: true,
|
|
21
|
+
writable: true,
|
|
22
|
+
value: void 0
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(this, "cornerRadius", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
configurable: true,
|
|
27
|
+
writable: true,
|
|
28
|
+
value: void 0
|
|
29
|
+
});
|
|
30
|
+
Object.defineProperty(this, "valueKey", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
configurable: true,
|
|
33
|
+
writable: true,
|
|
34
|
+
value: void 0
|
|
35
|
+
});
|
|
36
|
+
Object.defineProperty(this, "labelKey", {
|
|
37
|
+
enumerable: true,
|
|
38
|
+
configurable: true,
|
|
39
|
+
writable: true,
|
|
40
|
+
value: void 0
|
|
41
|
+
});
|
|
42
|
+
Object.defineProperty(this, "valueLabel", {
|
|
43
|
+
enumerable: true,
|
|
44
|
+
configurable: true,
|
|
45
|
+
writable: true,
|
|
46
|
+
value: void 0
|
|
47
|
+
});
|
|
48
|
+
Object.defineProperty(this, "segments", {
|
|
49
|
+
enumerable: true,
|
|
50
|
+
configurable: true,
|
|
51
|
+
writable: true,
|
|
52
|
+
value: []
|
|
53
|
+
});
|
|
54
|
+
Object.defineProperty(this, "centerContent", {
|
|
55
|
+
enumerable: true,
|
|
56
|
+
configurable: true,
|
|
57
|
+
writable: true,
|
|
58
|
+
value: null
|
|
59
|
+
});
|
|
60
|
+
const donut = config.donut ?? {};
|
|
61
|
+
this.innerRadiusRatio =
|
|
62
|
+
donut.innerRadius ?? this.theme.donut.innerRadius;
|
|
63
|
+
this.padAngle = donut.padAngle ?? this.theme.donut.padAngle;
|
|
64
|
+
this.cornerRadius = donut.cornerRadius ?? this.theme.donut.cornerRadius;
|
|
65
|
+
this.valueKey = config.valueKey ?? 'value';
|
|
66
|
+
this.labelKey = config.labelKey ?? 'name';
|
|
67
|
+
this.valueLabel = {
|
|
68
|
+
show: config.valueLabel?.show ?? false,
|
|
69
|
+
position: config.valueLabel?.position ?? 'auto',
|
|
70
|
+
outsideOffset: config.valueLabel?.outsideOffset ?? 16,
|
|
71
|
+
minVerticalSpacing: config.valueLabel?.minVerticalSpacing ?? 14,
|
|
72
|
+
formatter: config.valueLabel?.formatter ??
|
|
73
|
+
((label, value, _data, _percentage) => `${label}: ${value}`),
|
|
74
|
+
};
|
|
75
|
+
this.initializeDataState();
|
|
76
|
+
}
|
|
77
|
+
validateDonutData() {
|
|
78
|
+
ChartValidator.validateDataKey(this.data, this.labelKey, 'DonutChart');
|
|
79
|
+
ChartValidator.validateDataKey(this.data, this.valueKey, 'DonutChart');
|
|
80
|
+
ChartValidator.validateNumericData(this.data, this.valueKey, 'DonutChart');
|
|
81
|
+
for (const [index, item] of this.data.entries()) {
|
|
82
|
+
const value = this.parseValue(item[this.valueKey]);
|
|
83
|
+
if (value < 0) {
|
|
84
|
+
throw new Error(`DonutChart: data item at index ${index} has negative value '${item[this.valueKey]}' for key '${this.valueKey}'`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (this.valueLabel.outsideOffset < 0) {
|
|
88
|
+
throw new Error(`DonutChart: valueLabel.outsideOffset must be >= 0, received '${this.valueLabel.outsideOffset}'`);
|
|
89
|
+
}
|
|
90
|
+
if (this.valueLabel.minVerticalSpacing < 0) {
|
|
91
|
+
throw new Error(`DonutChart: valueLabel.minVerticalSpacing must be >= 0, received '${this.valueLabel.minVerticalSpacing}'`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
prepareSegments() {
|
|
95
|
+
this.segments = this.data.map((item, index) => ({
|
|
96
|
+
label: String(item[this.labelKey]),
|
|
97
|
+
value: this.parseValue(item[this.valueKey]),
|
|
98
|
+
color: (typeof item['color'] === 'string' ? item['color'] : null) ||
|
|
99
|
+
this.theme.colorPalette[index % this.theme.colorPalette.length],
|
|
100
|
+
source: item,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
addChild(component) {
|
|
104
|
+
if (this.tryRegisterComponent(component, [
|
|
105
|
+
{
|
|
106
|
+
type: 'donutCenterContent',
|
|
107
|
+
get: () => this.centerContent,
|
|
108
|
+
set: (entry) => {
|
|
109
|
+
this.centerContent = entry;
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
])) {
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
return super.addChild(component);
|
|
116
|
+
}
|
|
117
|
+
getExportComponents() {
|
|
118
|
+
return [
|
|
119
|
+
...this.getBaseExportComponents({
|
|
120
|
+
title: true,
|
|
121
|
+
}),
|
|
122
|
+
...(this.centerContent ? [this.centerContent] : []),
|
|
123
|
+
...this.getBaseExportComponents({
|
|
124
|
+
tooltip: true,
|
|
125
|
+
legend: this.legend?.isInlineMode(),
|
|
126
|
+
}),
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
update(data) {
|
|
130
|
+
super.update(data);
|
|
131
|
+
}
|
|
132
|
+
createExportChart() {
|
|
133
|
+
return new DonutChart({
|
|
134
|
+
data: this.data,
|
|
135
|
+
width: this.configuredWidth,
|
|
136
|
+
height: this.configuredHeight,
|
|
137
|
+
theme: this.theme,
|
|
138
|
+
responsive: this.responsiveConfig,
|
|
139
|
+
donut: {
|
|
140
|
+
innerRadius: this.innerRadiusRatio,
|
|
141
|
+
padAngle: this.padAngle,
|
|
142
|
+
cornerRadius: this.cornerRadius,
|
|
143
|
+
},
|
|
144
|
+
valueLabel: this.valueLabel,
|
|
145
|
+
valueKey: this.valueKey,
|
|
146
|
+
labelKey: this.labelKey,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
applyComponentOverrides(overrides) {
|
|
150
|
+
const restoreBase = super.applyComponentOverrides(overrides);
|
|
151
|
+
const restoreCenterContent = this.applySlotOverrides(overrides, [
|
|
152
|
+
{
|
|
153
|
+
type: 'donutCenterContent',
|
|
154
|
+
get: () => this.centerContent,
|
|
155
|
+
set: (component) => {
|
|
156
|
+
this.centerContent = component;
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
]);
|
|
160
|
+
return () => {
|
|
161
|
+
restoreCenterContent();
|
|
162
|
+
restoreBase();
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
syncDerivedState() {
|
|
166
|
+
this.validateDonutData();
|
|
167
|
+
this.prepareSegments();
|
|
168
|
+
}
|
|
169
|
+
renderChart({ svg, plotGroup, plotArea, }) {
|
|
170
|
+
this.renderTitle(svg);
|
|
171
|
+
const visibleSegments = this.getVisibleRadialItems(this.segments);
|
|
172
|
+
const { cx, cy, outerRadius, innerRadius, fontScale } = this.getRadialLayout(plotArea, this.innerRadiusRatio);
|
|
173
|
+
this.initializeTooltip();
|
|
174
|
+
const { segmentGroup, pieData } = this.renderSegments(plotGroup, visibleSegments, cx, cy, innerRadius, outerRadius);
|
|
175
|
+
if (this.valueLabel.show && visibleSegments.length > 0) {
|
|
176
|
+
this.renderLabels(segmentGroup, pieData, outerRadius, visibleSegments.reduce((sum, segment) => {
|
|
177
|
+
return sum + segment.value;
|
|
178
|
+
}, 0), fontScale);
|
|
179
|
+
}
|
|
180
|
+
if (this.centerContent) {
|
|
181
|
+
this.centerContent.render(svg, cx, cy, this.renderTheme, fontScale);
|
|
182
|
+
}
|
|
183
|
+
this.renderInlineLegend(svg);
|
|
184
|
+
}
|
|
185
|
+
getLegendSeries() {
|
|
186
|
+
return this.getRadialLegendSeries(this.segments);
|
|
187
|
+
}
|
|
188
|
+
buildTooltipContent(d, segments) {
|
|
189
|
+
const total = segments.reduce((sum, s) => sum + s.value, 0);
|
|
190
|
+
const percentage = total > 0 ? ((d.data.value / total) * 100).toFixed(1) : '0.0';
|
|
191
|
+
const dataItem = this.data.find((item) => String(item[this.labelKey]) === d.data.label);
|
|
192
|
+
if (this.tooltip?.customFormatter) {
|
|
193
|
+
return this.tooltip.customFormatter(dataItem || {}, [
|
|
194
|
+
{ dataKey: d.data.label, fill: d.data.color },
|
|
195
|
+
]);
|
|
196
|
+
}
|
|
197
|
+
if (this.tooltip?.formatter) {
|
|
198
|
+
return `<strong>${d.data.label}</strong><br/>${this.tooltip.formatter(d.data.label, d.data.value, dataItem || {})}`;
|
|
199
|
+
}
|
|
200
|
+
return `<strong>${d.data.label}</strong><br/>${d.data.value} (${percentage}%)`;
|
|
201
|
+
}
|
|
202
|
+
formatValueLabelText(segment, total) {
|
|
203
|
+
return this.valueLabel.formatter(segment.label, segment.value, segment.source, total > 0 ? (segment.value / total) * 100 : 0);
|
|
204
|
+
}
|
|
205
|
+
renderSegments(plotGroup, segments, cx, cy, innerRadius, outerRadius) {
|
|
206
|
+
const pieGenerator = pie()
|
|
207
|
+
.value((d) => d.value)
|
|
208
|
+
.padAngle(this.padAngle)
|
|
209
|
+
.sort(null);
|
|
210
|
+
const arcGenerator = arc()
|
|
211
|
+
.innerRadius(innerRadius)
|
|
212
|
+
.outerRadius(outerRadius)
|
|
213
|
+
.cornerRadius(this.cornerRadius);
|
|
214
|
+
const hoverArcGenerator = arc()
|
|
215
|
+
.innerRadius(innerRadius)
|
|
216
|
+
.outerRadius(outerRadius + HOVER_EXPAND_PX)
|
|
217
|
+
.cornerRadius(this.cornerRadius);
|
|
218
|
+
const pieData = pieGenerator(segments);
|
|
219
|
+
const segmentGroup = plotGroup
|
|
220
|
+
.append('g')
|
|
221
|
+
.attr('class', 'donut-segments')
|
|
222
|
+
.attr('transform', `translate(${cx}, ${cy})`);
|
|
223
|
+
const resolveTooltipDiv = () => this.tooltip
|
|
224
|
+
? select(`#${this.tooltip.id}`)
|
|
225
|
+
: null;
|
|
226
|
+
segmentGroup
|
|
227
|
+
.selectAll('.donut-segment')
|
|
228
|
+
.data(pieData)
|
|
229
|
+
.join('path')
|
|
230
|
+
.attr('class', (d) => `donut-segment segment-${sanitizeForCSS(d.data.label)}`)
|
|
231
|
+
.attr('d', arcGenerator)
|
|
232
|
+
.attr('fill', (d) => d.data.color)
|
|
233
|
+
.style('cursor', 'pointer')
|
|
234
|
+
.style('transition', 'opacity 0.15s ease')
|
|
235
|
+
.on('mouseenter', (event, d) => {
|
|
236
|
+
select(event.currentTarget)
|
|
237
|
+
.transition()
|
|
238
|
+
.duration(ANIMATION_DURATION_MS)
|
|
239
|
+
.attr('d', hoverArcGenerator(d));
|
|
240
|
+
segmentGroup
|
|
241
|
+
.selectAll('.donut-segment')
|
|
242
|
+
.filter((_, i, nodes) => nodes[i] !== event.currentTarget)
|
|
243
|
+
.style('opacity', 0.5);
|
|
244
|
+
const tooltipDiv = resolveTooltipDiv();
|
|
245
|
+
if (tooltipDiv && !tooltipDiv.empty()) {
|
|
246
|
+
tooltipDiv
|
|
247
|
+
.style('visibility', 'visible')
|
|
248
|
+
.html(this.buildTooltipContent(d, segments));
|
|
249
|
+
this.positionTooltipFromPointer(event, tooltipDiv);
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
.on('mousemove', (event) => {
|
|
253
|
+
const tooltipDiv = resolveTooltipDiv();
|
|
254
|
+
if (tooltipDiv && !tooltipDiv.empty()) {
|
|
255
|
+
this.positionTooltipFromPointer(event, tooltipDiv);
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
.on('mouseleave', (event, d) => {
|
|
259
|
+
select(event.currentTarget)
|
|
260
|
+
.transition()
|
|
261
|
+
.duration(ANIMATION_DURATION_MS)
|
|
262
|
+
.attr('d', arcGenerator(d));
|
|
263
|
+
segmentGroup.selectAll('.donut-segment').style('opacity', 1);
|
|
264
|
+
const tooltipDiv = resolveTooltipDiv();
|
|
265
|
+
if (tooltipDiv && !tooltipDiv.empty()) {
|
|
266
|
+
tooltipDiv.style('visibility', 'hidden');
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
segmentGroup,
|
|
271
|
+
pieData,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
renderLabels(segmentGroup, pieData, outerRadius, total, fontScale) {
|
|
275
|
+
if (pieData.length === 0) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const labelGroup = segmentGroup
|
|
279
|
+
.append('g')
|
|
280
|
+
.attr('class', 'donut-labels');
|
|
281
|
+
const fontSize = this.renderTheme.valueLabel.fontSize * fontScale;
|
|
282
|
+
const fontFamily = this.renderTheme.valueLabel.fontFamily;
|
|
283
|
+
const fontWeight = this.renderTheme.valueLabel.fontWeight;
|
|
284
|
+
const outsideLabels = pieData.map((datum) => this.resolveOutsideLabel(datum, outerRadius));
|
|
285
|
+
const adjustedOutsideLabels = this.adjustOutsideLabelPositions(outsideLabels, outerRadius);
|
|
286
|
+
const linesGroup = labelGroup
|
|
287
|
+
.append('g')
|
|
288
|
+
.attr('class', 'donut-label-leader-lines');
|
|
289
|
+
adjustedOutsideLabels.forEach((outsideLabel) => {
|
|
290
|
+
const midAngle = (outsideLabel.datum.startAngle + outsideLabel.datum.endAngle) /
|
|
291
|
+
2;
|
|
292
|
+
const anchorX = outerRadius + this.valueLabel.outsideOffset;
|
|
293
|
+
const textX = outsideLabel.side === 'right'
|
|
294
|
+
? anchorX + OUTSIDE_LABEL_TEXT_OFFSET_PX
|
|
295
|
+
: -anchorX - OUTSIDE_LABEL_TEXT_OFFSET_PX;
|
|
296
|
+
const lineEndX = outsideLabel.side === 'right'
|
|
297
|
+
? textX - OUTSIDE_LABEL_TEXT_OFFSET_PX / 2
|
|
298
|
+
: textX + OUTSIDE_LABEL_TEXT_OFFSET_PX / 2;
|
|
299
|
+
const start = this.getArcPoint(midAngle, outerRadius + OUTSIDE_LABEL_LINE_INSET_PX);
|
|
300
|
+
const elbow = this.getArcPoint(midAngle, outerRadius + this.valueLabel.outsideOffset * 0.65);
|
|
301
|
+
const elbowY = outsideLabel.y;
|
|
302
|
+
linesGroup
|
|
303
|
+
.append('path')
|
|
304
|
+
.attr('class', `donut-label-line donut-label-line-${sanitizeForCSS(outsideLabel.datum.data.label)}`)
|
|
305
|
+
.attr('d', `M ${start.x},${start.y} L ${elbow.x},${elbowY} L ${lineEndX},${outsideLabel.y}`)
|
|
306
|
+
.attr('fill', 'none')
|
|
307
|
+
.attr('stroke', this.renderTheme.valueLabel.border)
|
|
308
|
+
.attr('stroke-width', 1);
|
|
309
|
+
labelGroup
|
|
310
|
+
.append('text')
|
|
311
|
+
.attr('class', `donut-label donut-label--outside donut-label-${sanitizeForCSS(outsideLabel.datum.data.label)}`)
|
|
312
|
+
.attr('x', textX)
|
|
313
|
+
.attr('y', outsideLabel.y)
|
|
314
|
+
.attr('text-anchor', outsideLabel.textAnchor)
|
|
315
|
+
.attr('dominant-baseline', 'middle')
|
|
316
|
+
.attr('font-family', fontFamily)
|
|
317
|
+
.attr('font-size', `${fontSize}px`)
|
|
318
|
+
.attr('font-weight', fontWeight)
|
|
319
|
+
.attr('fill', this.renderTheme.valueLabel.color)
|
|
320
|
+
.text(this.formatValueLabelText(outsideLabel.datum.data, total));
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
getArcPoint(angle, radius) {
|
|
324
|
+
return {
|
|
325
|
+
x: Math.sin(angle) * radius,
|
|
326
|
+
y: -Math.cos(angle) * radius,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
resolveOutsideLabel(datum, outerRadius) {
|
|
330
|
+
const midAngle = (datum.startAngle + datum.endAngle) / 2;
|
|
331
|
+
const point = this.getArcPoint(midAngle, outerRadius + this.valueLabel.outsideOffset);
|
|
332
|
+
const side = point.x >= 0 ? 'right' : 'left';
|
|
333
|
+
return {
|
|
334
|
+
datum,
|
|
335
|
+
y: point.y,
|
|
336
|
+
side,
|
|
337
|
+
textAnchor: side === 'right' ? 'start' : 'end',
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
adjustOutsideLabelPositions(labels, outerRadius) {
|
|
341
|
+
const adjustForSide = (side) => {
|
|
342
|
+
const sideLabels = labels
|
|
343
|
+
.filter((label) => label.side === side)
|
|
344
|
+
.sort((a, b) => a.y - b.y);
|
|
345
|
+
if (sideLabels.length === 0) {
|
|
346
|
+
return [];
|
|
347
|
+
}
|
|
348
|
+
const topLimit = -outerRadius;
|
|
349
|
+
const bottomLimit = outerRadius;
|
|
350
|
+
sideLabels[0].y = Math.max(topLimit, sideLabels[0].y);
|
|
351
|
+
for (let i = 1; i < sideLabels.length; i++) {
|
|
352
|
+
const minY = sideLabels[i - 1].y + this.valueLabel.minVerticalSpacing;
|
|
353
|
+
sideLabels[i].y = Math.max(sideLabels[i].y, minY);
|
|
354
|
+
}
|
|
355
|
+
const overflow = sideLabels[sideLabels.length - 1].y - bottomLimit;
|
|
356
|
+
if (overflow > 0) {
|
|
357
|
+
sideLabels[sideLabels.length - 1].y -= overflow;
|
|
358
|
+
for (let i = sideLabels.length - 2; i >= 0; i--) {
|
|
359
|
+
const maxY = sideLabels[i + 1].y -
|
|
360
|
+
this.valueLabel.minVerticalSpacing;
|
|
361
|
+
sideLabels[i].y = Math.min(sideLabels[i].y, maxY);
|
|
362
|
+
}
|
|
363
|
+
const underflow = topLimit - sideLabels[0].y;
|
|
364
|
+
if (underflow > 0) {
|
|
365
|
+
sideLabels.forEach((label) => {
|
|
366
|
+
label.y += underflow;
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return sideLabels;
|
|
371
|
+
};
|
|
372
|
+
return [...adjustForSide('left'), ...adjustForSide('right')];
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DataItem, LegendSeries } from './types.js';
|
|
2
|
-
import { BaseChart, type BaseChartConfig } from './base-chart.js';
|
|
3
|
-
import type {
|
|
2
|
+
import { BaseChart, type BaseChartConfig, type BaseRenderContext } from './base-chart.js';
|
|
3
|
+
import type { ChartComponentBase } from './chart-interface.js';
|
|
4
4
|
export type GaugeSegment = {
|
|
5
5
|
from: number;
|
|
6
6
|
to: number;
|
|
@@ -35,10 +35,17 @@ export type GaugeLabelStyle = {
|
|
|
35
35
|
fontWeight?: number | string;
|
|
36
36
|
color?: string;
|
|
37
37
|
};
|
|
38
|
+
export type GaugeAnimationEasingPreset = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce-out' | 'elastic-out';
|
|
39
|
+
export type GaugeAnimationConfig = {
|
|
40
|
+
show?: boolean;
|
|
41
|
+
duration?: number;
|
|
42
|
+
easing?: GaugeAnimationEasingPreset | `linear(${string})` | ((progress: number) => number);
|
|
43
|
+
};
|
|
38
44
|
export type GaugeConfig = {
|
|
39
45
|
value?: number;
|
|
40
46
|
min?: number;
|
|
41
47
|
max?: number;
|
|
48
|
+
animate?: boolean | GaugeAnimationConfig;
|
|
42
49
|
halfCircle?: boolean;
|
|
43
50
|
startAngle?: number;
|
|
44
51
|
endAngle?: number;
|
|
@@ -71,6 +78,7 @@ export declare class GaugeChart extends BaseChart {
|
|
|
71
78
|
private readonly targetValueKey;
|
|
72
79
|
private readonly minValue;
|
|
73
80
|
private readonly maxValue;
|
|
81
|
+
private readonly animation;
|
|
74
82
|
private readonly halfCircle;
|
|
75
83
|
private readonly startAngle;
|
|
76
84
|
private readonly endAngle;
|
|
@@ -91,11 +99,15 @@ export declare class GaugeChart extends BaseChart {
|
|
|
91
99
|
private segments;
|
|
92
100
|
private value;
|
|
93
101
|
private targetValue;
|
|
102
|
+
private lastRenderedValue;
|
|
94
103
|
constructor(config: GaugeChartConfig);
|
|
95
104
|
private normalizeNeedleConfig;
|
|
96
105
|
private normalizeMarkerConfig;
|
|
97
106
|
private getThemePaletteColor;
|
|
98
107
|
private normalizeTickConfig;
|
|
108
|
+
private normalizeAnimationConfig;
|
|
109
|
+
private resolveAnimationEasing;
|
|
110
|
+
private parseCssLinearEasing;
|
|
99
111
|
private normalizeTickLabelStyle;
|
|
100
112
|
private normalizeValueLabelStyle;
|
|
101
113
|
private validateGaugeConfig;
|
|
@@ -108,13 +120,13 @@ export declare class GaugeChart extends BaseChart {
|
|
|
108
120
|
private clampToDomain;
|
|
109
121
|
private prepareSegments;
|
|
110
122
|
private defaultFormat;
|
|
111
|
-
|
|
112
|
-
protected getExportComponents(): ChartComponent[];
|
|
123
|
+
protected getExportComponents(): ChartComponentBase[];
|
|
113
124
|
update(data: DataItem[]): void;
|
|
114
|
-
protected getLayoutComponents(): LayoutAwareComponent[];
|
|
115
|
-
protected prepareLayout(): void;
|
|
116
125
|
protected createExportChart(): BaseChart;
|
|
117
|
-
protected
|
|
126
|
+
protected syncDerivedState(): void;
|
|
127
|
+
protected renderChart({ svg, plotGroup, plotArea, }: BaseRenderContext): void;
|
|
128
|
+
private resolveAnimationStartValue;
|
|
129
|
+
private shouldAnimateTransition;
|
|
118
130
|
private buildAriaLabel;
|
|
119
131
|
private findSegmentStatusLabel;
|
|
120
132
|
private getVisibleSegments;
|
|
@@ -135,6 +147,5 @@ export declare class GaugeChart extends BaseChart {
|
|
|
135
147
|
private resolveTooltipDiv;
|
|
136
148
|
private buildTooltipContent;
|
|
137
149
|
private positionTooltip;
|
|
138
|
-
private renderLegend;
|
|
139
150
|
protected getLegendSeries(): LegendSeries[];
|
|
140
151
|
}
|