@internetstiftelsen/charts 0.9.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 +44 -0
- package/{area.d.ts → dist/area.d.ts} +1 -1
- package/{bar.d.ts → dist/bar.d.ts} +3 -4
- package/{bar.js → dist/bar.js} +3 -11
- package/{base-chart.d.ts → dist/base-chart.d.ts} +30 -18
- package/{base-chart.js → dist/base-chart.js} +170 -50
- package/dist/chart-interface.d.ts +19 -0
- package/{donut-center-content.d.ts → dist/donut-center-content.d.ts} +1 -1
- package/{donut-chart.d.ts → dist/donut-chart.d.ts} +19 -4
- package/{donut-chart.js → dist/donut-chart.js} +140 -2
- package/{gauge-chart.d.ts → dist/gauge-chart.d.ts} +2 -2
- package/{gauge-chart.js → dist/gauge-chart.js} +2 -0
- 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/{pie-chart.d.ts → dist/pie-chart.d.ts} +4 -11
- package/{pie-chart.js → dist/pie-chart.js} +23 -21
- package/{radial-chart-base.js → dist/radial-chart-base.js} +3 -1
- 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} +0 -2
- package/{utils.js → dist/utils.js} +0 -5
- package/{word-cloud-chart.d.ts → dist/word-cloud-chart.d.ts} +1 -1
- package/{word-cloud-chart.js → dist/word-cloud-chart.js} +2 -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} +8 -5
- package/{xy-chart.js → dist/xy-chart.js} +31 -5
- 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 +38 -36
- package/chart-interface.d.ts +0 -13
- /package/{area.js → dist/area.js} +0 -0
- /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/{line.js → dist/line.js} +0 -0
- /package/{radial-chart-base.d.ts → dist/radial-chart-base.d.ts} +0 -0
- /package/{scale-utils.d.ts → dist/scale-utils.d.ts} +0 -0
- /package/{scale-utils.js → dist/scale-utils.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
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import type { DataItem, LegendSeries } from './types.js';
|
|
2
2
|
import { type BaseChartConfig, type BaseRenderContext } from './base-chart.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ChartComponentBase } from './chart-interface.js';
|
|
4
4
|
import { RadialChartBase } from './radial-chart-base.js';
|
|
5
5
|
export type DonutConfig = {
|
|
6
6
|
innerRadius?: number;
|
|
7
7
|
padAngle?: number;
|
|
8
8
|
cornerRadius?: number;
|
|
9
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
|
+
};
|
|
10
18
|
export type DonutChartConfig = BaseChartConfig & {
|
|
11
19
|
donut?: DonutConfig;
|
|
20
|
+
valueLabel?: DonutValueLabelConfig;
|
|
12
21
|
valueKey?: string;
|
|
13
22
|
labelKey?: string;
|
|
14
23
|
};
|
|
@@ -18,19 +27,25 @@ export declare class DonutChart extends RadialChartBase {
|
|
|
18
27
|
private readonly cornerRadius;
|
|
19
28
|
private readonly valueKey;
|
|
20
29
|
private readonly labelKey;
|
|
30
|
+
private readonly valueLabel;
|
|
21
31
|
private segments;
|
|
22
32
|
private centerContent;
|
|
23
33
|
constructor(config: DonutChartConfig);
|
|
24
34
|
private validateDonutData;
|
|
25
35
|
private prepareSegments;
|
|
26
|
-
addChild(component:
|
|
27
|
-
protected getExportComponents():
|
|
36
|
+
addChild(component: ChartComponentBase): this;
|
|
37
|
+
protected getExportComponents(): ChartComponentBase[];
|
|
28
38
|
update(data: DataItem[]): void;
|
|
29
39
|
protected createExportChart(): RadialChartBase;
|
|
30
|
-
protected applyComponentOverrides(overrides: Map<
|
|
40
|
+
protected applyComponentOverrides(overrides: Map<ChartComponentBase, ChartComponentBase>): () => void;
|
|
31
41
|
protected syncDerivedState(): void;
|
|
32
42
|
protected renderChart({ svg, plotGroup, plotArea, }: BaseRenderContext): void;
|
|
33
43
|
protected getLegendSeries(): LegendSeries[];
|
|
34
44
|
private buildTooltipContent;
|
|
45
|
+
private formatValueLabelText;
|
|
35
46
|
private renderSegments;
|
|
47
|
+
private renderLabels;
|
|
48
|
+
private getArcPoint;
|
|
49
|
+
private resolveOutsideLabel;
|
|
50
|
+
private adjustOutsideLabelPositions;
|
|
36
51
|
}
|
|
@@ -4,6 +4,8 @@ import { ChartValidator } from './validation.js';
|
|
|
4
4
|
import { RadialChartBase } from './radial-chart-base.js';
|
|
5
5
|
const HOVER_EXPAND_PX = 8;
|
|
6
6
|
const ANIMATION_DURATION_MS = 150;
|
|
7
|
+
const OUTSIDE_LABEL_TEXT_OFFSET_PX = 10;
|
|
8
|
+
const OUTSIDE_LABEL_LINE_INSET_PX = 4;
|
|
7
9
|
export class DonutChart extends RadialChartBase {
|
|
8
10
|
constructor(config) {
|
|
9
11
|
super(config);
|
|
@@ -37,6 +39,12 @@ export class DonutChart extends RadialChartBase {
|
|
|
37
39
|
writable: true,
|
|
38
40
|
value: void 0
|
|
39
41
|
});
|
|
42
|
+
Object.defineProperty(this, "valueLabel", {
|
|
43
|
+
enumerable: true,
|
|
44
|
+
configurable: true,
|
|
45
|
+
writable: true,
|
|
46
|
+
value: void 0
|
|
47
|
+
});
|
|
40
48
|
Object.defineProperty(this, "segments", {
|
|
41
49
|
enumerable: true,
|
|
42
50
|
configurable: true,
|
|
@@ -56,6 +64,14 @@ export class DonutChart extends RadialChartBase {
|
|
|
56
64
|
this.cornerRadius = donut.cornerRadius ?? this.theme.donut.cornerRadius;
|
|
57
65
|
this.valueKey = config.valueKey ?? 'value';
|
|
58
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
|
+
};
|
|
59
75
|
this.initializeDataState();
|
|
60
76
|
}
|
|
61
77
|
validateDonutData() {
|
|
@@ -68,13 +84,20 @@ export class DonutChart extends RadialChartBase {
|
|
|
68
84
|
throw new Error(`DonutChart: data item at index ${index} has negative value '${item[this.valueKey]}' for key '${this.valueKey}'`);
|
|
69
85
|
}
|
|
70
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
|
+
}
|
|
71
93
|
}
|
|
72
94
|
prepareSegments() {
|
|
73
95
|
this.segments = this.data.map((item, index) => ({
|
|
74
96
|
label: String(item[this.labelKey]),
|
|
75
97
|
value: this.parseValue(item[this.valueKey]),
|
|
76
|
-
color: item
|
|
98
|
+
color: (typeof item['color'] === 'string' ? item['color'] : null) ||
|
|
77
99
|
this.theme.colorPalette[index % this.theme.colorPalette.length],
|
|
100
|
+
source: item,
|
|
78
101
|
}));
|
|
79
102
|
}
|
|
80
103
|
addChild(component) {
|
|
@@ -109,6 +132,8 @@ export class DonutChart extends RadialChartBase {
|
|
|
109
132
|
createExportChart() {
|
|
110
133
|
return new DonutChart({
|
|
111
134
|
data: this.data,
|
|
135
|
+
width: this.configuredWidth,
|
|
136
|
+
height: this.configuredHeight,
|
|
112
137
|
theme: this.theme,
|
|
113
138
|
responsive: this.responsiveConfig,
|
|
114
139
|
donut: {
|
|
@@ -116,6 +141,7 @@ export class DonutChart extends RadialChartBase {
|
|
|
116
141
|
padAngle: this.padAngle,
|
|
117
142
|
cornerRadius: this.cornerRadius,
|
|
118
143
|
},
|
|
144
|
+
valueLabel: this.valueLabel,
|
|
119
145
|
valueKey: this.valueKey,
|
|
120
146
|
labelKey: this.labelKey,
|
|
121
147
|
});
|
|
@@ -145,7 +171,12 @@ export class DonutChart extends RadialChartBase {
|
|
|
145
171
|
const visibleSegments = this.getVisibleRadialItems(this.segments);
|
|
146
172
|
const { cx, cy, outerRadius, innerRadius, fontScale } = this.getRadialLayout(plotArea, this.innerRadiusRatio);
|
|
147
173
|
this.initializeTooltip();
|
|
148
|
-
this.renderSegments(plotGroup, visibleSegments, cx, cy, innerRadius, outerRadius);
|
|
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
|
+
}
|
|
149
180
|
if (this.centerContent) {
|
|
150
181
|
this.centerContent.render(svg, cx, cy, this.renderTheme, fontScale);
|
|
151
182
|
}
|
|
@@ -168,6 +199,9 @@ export class DonutChart extends RadialChartBase {
|
|
|
168
199
|
}
|
|
169
200
|
return `<strong>${d.data.label}</strong><br/>${d.data.value} (${percentage}%)`;
|
|
170
201
|
}
|
|
202
|
+
formatValueLabelText(segment, total) {
|
|
203
|
+
return this.valueLabel.formatter(segment.label, segment.value, segment.source, total > 0 ? (segment.value / total) * 100 : 0);
|
|
204
|
+
}
|
|
171
205
|
renderSegments(plotGroup, segments, cx, cy, innerRadius, outerRadius) {
|
|
172
206
|
const pieGenerator = pie()
|
|
173
207
|
.value((d) => d.value)
|
|
@@ -232,5 +266,109 @@ export class DonutChart extends RadialChartBase {
|
|
|
232
266
|
tooltipDiv.style('visibility', 'hidden');
|
|
233
267
|
}
|
|
234
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')];
|
|
235
373
|
}
|
|
236
374
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DataItem, LegendSeries } from './types.js';
|
|
2
2
|
import { BaseChart, type BaseChartConfig, type BaseRenderContext } from './base-chart.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ChartComponentBase } from './chart-interface.js';
|
|
4
4
|
export type GaugeSegment = {
|
|
5
5
|
from: number;
|
|
6
6
|
to: number;
|
|
@@ -120,7 +120,7 @@ export declare class GaugeChart extends BaseChart {
|
|
|
120
120
|
private clampToDomain;
|
|
121
121
|
private prepareSegments;
|
|
122
122
|
private defaultFormat;
|
|
123
|
-
protected getExportComponents():
|
|
123
|
+
protected getExportComponents(): ChartComponentBase[];
|
|
124
124
|
update(data: DataItem[]): void;
|
|
125
125
|
protected createExportChart(): BaseChart;
|
|
126
126
|
protected syncDerivedState(): void;
|
|
@@ -685,6 +685,8 @@ export class GaugeChart extends BaseChart {
|
|
|
685
685
|
createExportChart() {
|
|
686
686
|
return new GaugeChart({
|
|
687
687
|
data: this.data,
|
|
688
|
+
width: this.configuredWidth,
|
|
689
|
+
height: this.configuredHeight,
|
|
688
690
|
theme: this.theme,
|
|
689
691
|
responsive: this.responsiveConfig,
|
|
690
692
|
valueKey: this.valueKey,
|
|
@@ -8,6 +8,6 @@ export declare class Grid implements ChartComponent<GridConfigBase> {
|
|
|
8
8
|
readonly exportHooks?: ExportHooks<GridConfigBase>;
|
|
9
9
|
constructor(config?: GridConfig);
|
|
10
10
|
getExportConfig(): GridConfigBase;
|
|
11
|
-
createExportComponent(override?: Partial<GridConfigBase>): ChartComponent
|
|
11
|
+
createExportComponent(override?: Partial<GridConfigBase>): ChartComponent<GridConfigBase>;
|
|
12
12
|
render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, x: D3Scale, y: D3Scale, theme: ChartTheme): void;
|
|
13
13
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
1
|
+
import type { LayoutAwareComponentBase } from './chart-interface.js';
|
|
2
|
+
import type { ResolvedChartTheme } from './types.js';
|
|
3
3
|
export type PlotAreaBounds = {
|
|
4
4
|
left: number;
|
|
5
5
|
right: number;
|
|
@@ -20,16 +20,16 @@ export declare class LayoutManager {
|
|
|
20
20
|
private theme;
|
|
21
21
|
private plotBounds;
|
|
22
22
|
private componentPositions;
|
|
23
|
-
constructor(theme:
|
|
23
|
+
constructor(theme: ResolvedChartTheme);
|
|
24
24
|
/**
|
|
25
25
|
* Calculate layout based on registered components
|
|
26
26
|
* Returns the plot area bounds
|
|
27
27
|
*/
|
|
28
|
-
calculateLayout(components:
|
|
28
|
+
calculateLayout(components: LayoutAwareComponentBase[]): PlotAreaBounds;
|
|
29
29
|
/**
|
|
30
30
|
* Get the position for a specific component
|
|
31
31
|
*/
|
|
32
|
-
getComponentPosition(component:
|
|
32
|
+
getComponentPosition(component: LayoutAwareComponentBase): ComponentPosition;
|
|
33
33
|
/**
|
|
34
34
|
* Calculate positions for all components based on their space requirements
|
|
35
35
|
* Components are positioned in registration order, stacking outward from the plot area
|
|
@@ -20,7 +20,7 @@ export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
|
|
|
20
20
|
private estimatedLayoutSignature;
|
|
21
21
|
constructor(config?: LegendConfig);
|
|
22
22
|
getExportConfig(): LegendConfigBase;
|
|
23
|
-
createExportComponent(override?: Partial<LegendConfigBase>): LayoutAwareComponent
|
|
23
|
+
createExportComponent(override?: Partial<LegendConfigBase>): LayoutAwareComponent<LegendConfigBase>;
|
|
24
24
|
setToggleCallback(callback: () => void): void;
|
|
25
25
|
isInlineMode(): boolean;
|
|
26
26
|
isDisconnectedMode(): boolean;
|
|
@@ -40,6 +40,8 @@ export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
|
|
|
40
40
|
getRequiredSpace(): ComponentSpace;
|
|
41
41
|
getMeasuredHeight(): number;
|
|
42
42
|
render(svg: Selection<SVGSVGElement, undefined, null, undefined>, series: LegendSeries[], theme: ChartTheme, width: number, _x?: number, y?: number): void;
|
|
43
|
+
private isLegendItemVisible;
|
|
44
|
+
private isToggleActivationKey;
|
|
43
45
|
private computeLayout;
|
|
44
46
|
private resolveLayoutSettings;
|
|
45
47
|
private buildLegendItems;
|
|
@@ -232,10 +232,36 @@ export class Legend {
|
|
|
232
232
|
.selectAll('g')
|
|
233
233
|
.data(layout.positionedItems)
|
|
234
234
|
.join('g')
|
|
235
|
+
.attr('class', 'legend-item')
|
|
235
236
|
.attr('transform', (d) => `translate(${d.x}, ${d.y})`)
|
|
237
|
+
.attr('tabindex', 0)
|
|
238
|
+
.attr('role', 'checkbox')
|
|
239
|
+
.attr('aria-label', (d) => `Toggle series ${d.label}`)
|
|
240
|
+
.attr('aria-checked', (d) => {
|
|
241
|
+
return String(this.isLegendItemVisible(d.dataKey));
|
|
242
|
+
})
|
|
236
243
|
.style('cursor', 'pointer')
|
|
237
244
|
.on('click', (_event, d) => {
|
|
238
245
|
this.toggleSeries(d.dataKey);
|
|
246
|
+
})
|
|
247
|
+
.on('keydown', (event, d) => {
|
|
248
|
+
if (!this.isToggleActivationKey(event.key)) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
event.preventDefault();
|
|
252
|
+
this.toggleSeries(d.dataKey);
|
|
253
|
+
})
|
|
254
|
+
.on('focus', function () {
|
|
255
|
+
select(this)
|
|
256
|
+
.select('rect')
|
|
257
|
+
.attr('stroke', '#111827')
|
|
258
|
+
.attr('stroke-width', 2);
|
|
259
|
+
})
|
|
260
|
+
.on('blur', function () {
|
|
261
|
+
select(this)
|
|
262
|
+
.select('rect')
|
|
263
|
+
.attr('stroke', null)
|
|
264
|
+
.attr('stroke-width', null);
|
|
239
265
|
});
|
|
240
266
|
// Add checkbox rect
|
|
241
267
|
legendGroups
|
|
@@ -269,6 +295,12 @@ export class Legend {
|
|
|
269
295
|
.attr('font-family', theme.axis.fontFamily)
|
|
270
296
|
.text((d) => d.label);
|
|
271
297
|
}
|
|
298
|
+
isLegendItemVisible(dataKey) {
|
|
299
|
+
return this.visibilityState.get(dataKey) ?? true;
|
|
300
|
+
}
|
|
301
|
+
isToggleActivationKey(key) {
|
|
302
|
+
return key === 'Enter' || key === ' ' || key === 'Spacebar';
|
|
303
|
+
}
|
|
272
304
|
computeLayout(series, theme, width, svg) {
|
|
273
305
|
const settings = this.resolveLayoutSettings(theme);
|
|
274
306
|
const legendItems = this.buildLegendItems(series);
|
|
@@ -10,7 +10,7 @@ export declare class Line implements ChartComponent<LineConfigBase> {
|
|
|
10
10
|
readonly exportHooks?: ExportHooks<LineConfigBase>;
|
|
11
11
|
constructor(config: LineConfig);
|
|
12
12
|
getExportConfig(): LineConfigBase;
|
|
13
|
-
createExportComponent(override?: Partial<LineConfigBase>): ChartComponent
|
|
13
|
+
createExportComponent(override?: Partial<LineConfigBase>): ChartComponent<LineConfigBase>;
|
|
14
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
15
|
private renderValueLabels;
|
|
16
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DataItem, LegendSeries } from './types.js';
|
|
2
2
|
import { type BaseChartConfig, type BaseRenderContext } from './base-chart.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ChartComponentBase } from './chart-interface.js';
|
|
4
4
|
import { RadialChartBase } from './radial-chart-base.js';
|
|
5
5
|
export type PieSort = 'none' | 'ascending' | 'descending' | ((a: PieSegmentData, b: PieSegmentData) => number);
|
|
6
6
|
export type PieConfig = {
|
|
@@ -19,19 +19,11 @@ export type PieValueLabelConfig = {
|
|
|
19
19
|
outsideOffset?: number;
|
|
20
20
|
insideMargin?: number;
|
|
21
21
|
minVerticalSpacing?: number;
|
|
22
|
-
|
|
23
|
-
export type PieLabelsConfig = {
|
|
24
|
-
show?: boolean;
|
|
25
|
-
mode?: 'adaptive' | 'inside' | 'outside';
|
|
26
|
-
minInsidePercentage?: number;
|
|
27
|
-
outsideOffset?: number;
|
|
28
|
-
minVerticalSpacing?: number;
|
|
22
|
+
formatter?: (label: string, value: number, data: DataItem, percentage: number) => string;
|
|
29
23
|
};
|
|
30
24
|
export type PieChartConfig = BaseChartConfig & {
|
|
31
25
|
pie?: PieConfig;
|
|
32
26
|
valueLabel?: PieValueLabelConfig;
|
|
33
|
-
/** @deprecated Use valueLabel instead */
|
|
34
|
-
labels?: PieLabelsConfig;
|
|
35
27
|
valueKey?: string;
|
|
36
28
|
labelKey?: string;
|
|
37
29
|
};
|
|
@@ -56,8 +48,9 @@ export declare class PieChart extends RadialChartBase {
|
|
|
56
48
|
private validatePieData;
|
|
57
49
|
private prepareSegments;
|
|
58
50
|
private warnOnTinySlices;
|
|
59
|
-
protected getExportComponents():
|
|
51
|
+
protected getExportComponents(): ChartComponentBase[];
|
|
60
52
|
update(data: DataItem[]): void;
|
|
53
|
+
private formatValueLabelText;
|
|
61
54
|
protected createExportChart(): RadialChartBase;
|
|
62
55
|
protected syncDerivedState(): void;
|
|
63
56
|
protected renderChart({ svg, plotGroup, plotArea, }: BaseRenderContext): void;
|
|
@@ -81,21 +81,15 @@ export class PieChart extends RadialChartBase {
|
|
|
81
81
|
this.sort = pieConfig.sort ?? 'none';
|
|
82
82
|
this.valueKey = config.valueKey ?? 'value';
|
|
83
83
|
this.labelKey = config.labelKey ?? 'name';
|
|
84
|
-
const legacyLabels = config.labels;
|
|
85
|
-
const legacyPosition = legacyLabels?.mode === 'adaptive' ? 'auto' : legacyLabels?.mode;
|
|
86
84
|
this.valueLabel = {
|
|
87
|
-
show: config.valueLabel?.show ??
|
|
88
|
-
position: config.valueLabel?.position ??
|
|
89
|
-
minInsidePercentage: config.valueLabel?.minInsidePercentage ??
|
|
90
|
-
|
|
91
|
-
8,
|
|
92
|
-
outsideOffset: config.valueLabel?.outsideOffset ??
|
|
93
|
-
legacyLabels?.outsideOffset ??
|
|
94
|
-
16,
|
|
85
|
+
show: config.valueLabel?.show ?? false,
|
|
86
|
+
position: config.valueLabel?.position ?? 'auto',
|
|
87
|
+
minInsidePercentage: config.valueLabel?.minInsidePercentage ?? 8,
|
|
88
|
+
outsideOffset: config.valueLabel?.outsideOffset ?? 16,
|
|
95
89
|
insideMargin: config.valueLabel?.insideMargin ?? 8,
|
|
96
|
-
minVerticalSpacing: config.valueLabel?.minVerticalSpacing ??
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
minVerticalSpacing: config.valueLabel?.minVerticalSpacing ?? 14,
|
|
91
|
+
formatter: config.valueLabel?.formatter ??
|
|
92
|
+
((label, value, _data, _percentage) => `${label}: ${value}`),
|
|
99
93
|
};
|
|
100
94
|
this.initializeDataState();
|
|
101
95
|
}
|
|
@@ -148,7 +142,9 @@ export class PieChart extends RadialChartBase {
|
|
|
148
142
|
nextSegments.push({
|
|
149
143
|
label,
|
|
150
144
|
value,
|
|
151
|
-
color: item
|
|
145
|
+
color: (typeof item['color'] === 'string'
|
|
146
|
+
? item['color']
|
|
147
|
+
: null) ||
|
|
152
148
|
this.theme.colorPalette[nextSegments.length % this.theme.colorPalette.length],
|
|
153
149
|
source: item,
|
|
154
150
|
});
|
|
@@ -183,9 +179,14 @@ export class PieChart extends RadialChartBase {
|
|
|
183
179
|
update(data) {
|
|
184
180
|
super.update(data);
|
|
185
181
|
}
|
|
182
|
+
formatValueLabelText(segment, total) {
|
|
183
|
+
return this.valueLabel.formatter(segment.label, segment.value, segment.source, total > 0 ? (segment.value / total) * 100 : 0);
|
|
184
|
+
}
|
|
186
185
|
createExportChart() {
|
|
187
186
|
return new PieChart({
|
|
188
187
|
data: this.data,
|
|
188
|
+
width: this.configuredWidth,
|
|
189
|
+
height: this.configuredHeight,
|
|
189
190
|
theme: this.theme,
|
|
190
191
|
responsive: this.responsiveConfig,
|
|
191
192
|
pie: {
|
|
@@ -416,7 +417,8 @@ export class PieChart extends RadialChartBase {
|
|
|
416
417
|
const outsideLabels = [];
|
|
417
418
|
pieData.forEach((d) => {
|
|
418
419
|
const percentage = total > 0 ? (d.data.value / total) * 100 : 0;
|
|
419
|
-
const
|
|
420
|
+
const labelText = this.formatValueLabelText(d.data, total);
|
|
421
|
+
const placement = this.resolveValueLabelPlacement(d, labelText, percentage, innerRadius, outerRadius, insideLabelRadius, fontSize, fontWeight);
|
|
420
422
|
if (placement === 'inside') {
|
|
421
423
|
const [x, y] = insideArc.centroid(d);
|
|
422
424
|
labelGroup
|
|
@@ -430,7 +432,7 @@ export class PieChart extends RadialChartBase {
|
|
|
430
432
|
.attr('font-size', `${fontSize}px`)
|
|
431
433
|
.attr('font-weight', fontWeight)
|
|
432
434
|
.attr('fill', getContrastTextColor(d.data.color))
|
|
433
|
-
.text(
|
|
435
|
+
.text(labelText);
|
|
434
436
|
return;
|
|
435
437
|
}
|
|
436
438
|
if (placement === 'outside') {
|
|
@@ -475,11 +477,11 @@ export class PieChart extends RadialChartBase {
|
|
|
475
477
|
.attr('font-size', `${fontSize}px`)
|
|
476
478
|
.attr('font-weight', fontWeight)
|
|
477
479
|
.attr('fill', this.renderTheme.valueLabel.color)
|
|
478
|
-
.text(outsideLabel.datum.data
|
|
480
|
+
.text(this.formatValueLabelText(outsideLabel.datum.data, total));
|
|
479
481
|
});
|
|
480
482
|
}
|
|
481
|
-
resolveValueLabelPlacement(datum, percentage, innerRadius, outerRadius, insideLabelRadius, fontSize, fontWeight) {
|
|
482
|
-
const fitsInside = this.canFitInsideLabel(datum, innerRadius, outerRadius, insideLabelRadius, fontSize, fontWeight);
|
|
483
|
+
resolveValueLabelPlacement(datum, labelText, percentage, innerRadius, outerRadius, insideLabelRadius, fontSize, fontWeight) {
|
|
484
|
+
const fitsInside = this.canFitInsideLabel(datum, labelText, innerRadius, outerRadius, insideLabelRadius, fontSize, fontWeight);
|
|
483
485
|
if (this.valueLabel.position === 'inside') {
|
|
484
486
|
return fitsInside ? 'inside' : 'hidden';
|
|
485
487
|
}
|
|
@@ -494,7 +496,7 @@ export class PieChart extends RadialChartBase {
|
|
|
494
496
|
}
|
|
495
497
|
return 'inside';
|
|
496
498
|
}
|
|
497
|
-
canFitInsideLabel(datum, innerRadius, outerRadius, insideLabelRadius, fontSize, fontWeight) {
|
|
499
|
+
canFitInsideLabel(datum, labelText, innerRadius, outerRadius, insideLabelRadius, fontSize, fontWeight) {
|
|
498
500
|
if (!this.svg) {
|
|
499
501
|
return false;
|
|
500
502
|
}
|
|
@@ -502,7 +504,7 @@ export class PieChart extends RadialChartBase {
|
|
|
502
504
|
if (!svgNode) {
|
|
503
505
|
return false;
|
|
504
506
|
}
|
|
505
|
-
const textWidth = measureTextWidth(
|
|
507
|
+
const textWidth = measureTextWidth(labelText, fontSize, this.renderTheme.axis.fontFamily, fontWeight, svgNode);
|
|
506
508
|
const angle = Math.max(0, datum.endAngle - datum.startAngle);
|
|
507
509
|
const availableArcLength = angle * insideLabelRadius - this.valueLabel.insideMargin * 2;
|
|
508
510
|
const availableRadialThickness = outerRadius - innerRadius - this.valueLabel.insideMargin * 2;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseChart } from './base-chart.js';
|
|
2
|
+
import { DEFAULT_CHART_HEIGHT } from './theme.js';
|
|
2
3
|
const TOOLTIP_OFFSET_PX = 12;
|
|
3
4
|
const EDGE_MARGIN_PX = 10;
|
|
4
5
|
export class RadialChartBase extends BaseChart {
|
|
@@ -69,7 +70,8 @@ export class RadialChartBase extends BaseChart {
|
|
|
69
70
|
tooltipDiv.style('left', `${x}px`).style('top', `${y}px`);
|
|
70
71
|
}
|
|
71
72
|
resolveRadialFontScale(outerRadius, theme) {
|
|
72
|
-
const
|
|
73
|
+
const referenceHeight = this.configuredHeight ?? DEFAULT_CHART_HEIGHT;
|
|
74
|
+
const plotHeight = Math.max(1, referenceHeight - theme.margins.top - theme.margins.bottom);
|
|
73
75
|
const referenceRadius = Math.max(1, plotHeight / 2);
|
|
74
76
|
const rawScale = outerRadius / referenceRadius;
|
|
75
77
|
return Math.max(0.5, Math.min(1, rawScale));
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type ChartTheme, type ResponsiveConfig } from './types.js';
|
|
2
2
|
export declare const DEFAULT_COLOR_PALETTE: string[];
|
|
3
|
+
export declare const DEFAULT_CHART_WIDTH = 928;
|
|
4
|
+
export declare const DEFAULT_CHART_HEIGHT = 600;
|
|
3
5
|
export declare const defaultTheme: ChartTheme;
|
|
4
6
|
export declare const newspaperTheme: ChartTheme;
|
|
5
7
|
export declare const defaultResponsiveConfig: ResponsiveConfig;
|
|
@@ -8,9 +8,9 @@ export const DEFAULT_COLOR_PALETTE = [
|
|
|
8
8
|
'#ff9fb4', // ruby-light
|
|
9
9
|
'#1f2a36', // cyberspace
|
|
10
10
|
];
|
|
11
|
+
export const DEFAULT_CHART_WIDTH = 928;
|
|
12
|
+
export const DEFAULT_CHART_HEIGHT = 600;
|
|
11
13
|
export const defaultTheme = {
|
|
12
|
-
width: 928,
|
|
13
|
-
height: 600,
|
|
14
14
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
|
|
15
15
|
margins: {
|
|
16
16
|
top: 20,
|
|
@@ -25,7 +25,7 @@ export const defaultTheme = {
|
|
|
25
25
|
colorPalette: [...DEFAULT_COLOR_PALETTE],
|
|
26
26
|
axis: {
|
|
27
27
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
|
|
28
|
-
fontSize:
|
|
28
|
+
fontSize: 14,
|
|
29
29
|
fontWeight: 'normal',
|
|
30
30
|
groupLabel: {
|
|
31
31
|
fontWeight: '700',
|
|
@@ -82,8 +82,6 @@ export const defaultTheme = {
|
|
|
82
82
|
},
|
|
83
83
|
};
|
|
84
84
|
export const newspaperTheme = {
|
|
85
|
-
width: 928,
|
|
86
|
-
height: 600,
|
|
87
85
|
fontFamily: 'Georgia, "Times New Roman", Times, serif',
|
|
88
86
|
margins: {
|
|
89
87
|
top: 20,
|
|
@@ -107,7 +105,7 @@ export const newspaperTheme = {
|
|
|
107
105
|
],
|
|
108
106
|
axis: {
|
|
109
107
|
fontFamily: 'Georgia, "Times New Roman", Times, serif',
|
|
110
|
-
fontSize:
|
|
108
|
+
fontSize: 13,
|
|
111
109
|
fontWeight: '600',
|
|
112
110
|
groupLabel: {
|
|
113
111
|
fontWeight: '700',
|
|
@@ -166,29 +164,26 @@ export const newspaperTheme = {
|
|
|
166
164
|
};
|
|
167
165
|
export const defaultResponsiveConfig = {
|
|
168
166
|
breakpoints: {
|
|
169
|
-
sm:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
},
|
|
190
|
-
};
|
|
191
|
-
}
|
|
167
|
+
sm: {
|
|
168
|
+
maxWidth: 479,
|
|
169
|
+
theme: {
|
|
170
|
+
axis: { fontSize: 11 },
|
|
171
|
+
legend: { fontSize: 11 },
|
|
172
|
+
valueLabel: { fontSize: 10 },
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
md: {
|
|
176
|
+
minWidth: 480,
|
|
177
|
+
maxWidth: 767,
|
|
178
|
+
theme: {
|
|
179
|
+
axis: { fontSize: 12 },
|
|
180
|
+
legend: { fontSize: 12 },
|
|
181
|
+
valueLabel: { fontSize: 11 },
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
lg: {
|
|
185
|
+
minWidth: 768,
|
|
186
|
+
},
|
|
192
187
|
},
|
|
193
188
|
};
|
|
194
189
|
export const themes = {
|