@internetstiftelsen/charts 0.6.0 → 0.6.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/donut-chart.d.ts +2 -0
- package/donut-chart.js +15 -5
- package/gauge-chart.d.ts +2 -0
- package/gauge-chart.js +15 -5
- package/legend.d.ts +15 -1
- package/legend.js +203 -46
- package/package.json +2 -1
- package/pie-chart.d.ts +2 -0
- package/pie-chart.js +15 -5
- package/theme.js +6 -0
- package/types.d.ts +6 -0
- package/xy-chart.d.ts +1 -0
- package/xy-chart.js +29 -29
package/donut-chart.d.ts
CHANGED
|
@@ -26,9 +26,11 @@ export declare class DonutChart extends BaseChart {
|
|
|
26
26
|
protected getExportComponents(): ChartComponent[];
|
|
27
27
|
update(data: DataItem[]): void;
|
|
28
28
|
protected getLayoutComponents(): LayoutAwareComponent[];
|
|
29
|
+
protected prepareLayout(): void;
|
|
29
30
|
protected createExportChart(): BaseChart;
|
|
30
31
|
protected renderChart(): void;
|
|
31
32
|
private resolveFontScale;
|
|
33
|
+
private getLegendSeries;
|
|
32
34
|
private positionTooltip;
|
|
33
35
|
private buildTooltipContent;
|
|
34
36
|
private renderSegments;
|
package/donut-chart.js
CHANGED
|
@@ -129,6 +129,12 @@ export class DonutChart extends BaseChart {
|
|
|
129
129
|
}
|
|
130
130
|
return components;
|
|
131
131
|
}
|
|
132
|
+
prepareLayout() {
|
|
133
|
+
const svgNode = this.svg?.node();
|
|
134
|
+
if (svgNode && this.legend) {
|
|
135
|
+
this.legend.estimateLayoutSpace(this.getLegendSeries(), this.theme, this.width, svgNode);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
132
138
|
createExportChart() {
|
|
133
139
|
return new DonutChart({
|
|
134
140
|
data: this.data,
|
|
@@ -167,11 +173,7 @@ export class DonutChart extends BaseChart {
|
|
|
167
173
|
}
|
|
168
174
|
if (this.legend) {
|
|
169
175
|
const pos = this.layoutManager.getComponentPosition(this.legend);
|
|
170
|
-
|
|
171
|
-
dataKey: seg.label,
|
|
172
|
-
fill: seg.color,
|
|
173
|
-
}));
|
|
174
|
-
this.legend.render(this.svg, legendSeries, this.theme, this.width, pos.x, pos.y);
|
|
176
|
+
this.legend.render(this.svg, this.getLegendSeries(), this.theme, this.width, pos.x, pos.y);
|
|
175
177
|
}
|
|
176
178
|
}
|
|
177
179
|
resolveFontScale(outerRadius) {
|
|
@@ -180,6 +182,14 @@ export class DonutChart extends BaseChart {
|
|
|
180
182
|
const rawScale = outerRadius / referenceRadius;
|
|
181
183
|
return Math.max(0.5, Math.min(1, rawScale));
|
|
182
184
|
}
|
|
185
|
+
getLegendSeries() {
|
|
186
|
+
return this.segments.map((segment) => {
|
|
187
|
+
return {
|
|
188
|
+
dataKey: segment.label,
|
|
189
|
+
fill: segment.color,
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
}
|
|
183
193
|
positionTooltip(event, tooltipDiv) {
|
|
184
194
|
const node = tooltipDiv.node();
|
|
185
195
|
if (!node)
|
package/gauge-chart.d.ts
CHANGED
|
@@ -112,6 +112,7 @@ export declare class GaugeChart extends BaseChart {
|
|
|
112
112
|
protected getExportComponents(): ChartComponent[];
|
|
113
113
|
update(data: DataItem[]): void;
|
|
114
114
|
protected getLayoutComponents(): LayoutAwareComponent[];
|
|
115
|
+
protected prepareLayout(): void;
|
|
115
116
|
protected createExportChart(): BaseChart;
|
|
116
117
|
protected renderChart(): void;
|
|
117
118
|
private buildAriaLabel;
|
|
@@ -135,4 +136,5 @@ export declare class GaugeChart extends BaseChart {
|
|
|
135
136
|
private buildTooltipContent;
|
|
136
137
|
private positionTooltip;
|
|
137
138
|
private renderLegend;
|
|
139
|
+
private getLegendSeries;
|
|
138
140
|
}
|
package/gauge-chart.js
CHANGED
|
@@ -544,6 +544,12 @@ export class GaugeChart extends BaseChart {
|
|
|
544
544
|
}
|
|
545
545
|
return components;
|
|
546
546
|
}
|
|
547
|
+
prepareLayout() {
|
|
548
|
+
const svgNode = this.svg?.node();
|
|
549
|
+
if (svgNode && this.legend) {
|
|
550
|
+
this.legend.estimateLayoutSpace(this.getLegendSeries(), this.theme, this.width, svgNode);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
547
553
|
createExportChart() {
|
|
548
554
|
return new GaugeChart({
|
|
549
555
|
data: this.data,
|
|
@@ -1032,10 +1038,14 @@ export class GaugeChart extends BaseChart {
|
|
|
1032
1038
|
return;
|
|
1033
1039
|
}
|
|
1034
1040
|
const legendPosition = this.layoutManager.getComponentPosition(this.legend);
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1041
|
+
this.legend.render(this.svg, this.getLegendSeries(), this.theme, this.width, legendPosition.x, legendPosition.y);
|
|
1042
|
+
}
|
|
1043
|
+
getLegendSeries() {
|
|
1044
|
+
return this.segments.map((segment) => {
|
|
1045
|
+
return {
|
|
1046
|
+
dataKey: segment.legendLabel,
|
|
1047
|
+
fill: segment.color,
|
|
1048
|
+
};
|
|
1049
|
+
});
|
|
1040
1050
|
}
|
|
1041
1051
|
}
|
package/legend.d.ts
CHANGED
|
@@ -7,18 +7,32 @@ export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
|
|
|
7
7
|
readonly exportHooks?: ExportHooks<LegendConfigBase>;
|
|
8
8
|
private readonly marginTop;
|
|
9
9
|
private readonly marginBottom;
|
|
10
|
-
private readonly
|
|
10
|
+
private readonly paddingX?;
|
|
11
|
+
private readonly itemSpacingX?;
|
|
12
|
+
private readonly itemSpacingY?;
|
|
13
|
+
private readonly gapBetweenBoxAndText;
|
|
11
14
|
private visibilityState;
|
|
12
15
|
private onToggleCallback?;
|
|
16
|
+
private estimatedLayout;
|
|
17
|
+
private estimatedLayoutSignature;
|
|
13
18
|
constructor(config?: LegendConfig);
|
|
14
19
|
getExportConfig(): LegendConfigBase;
|
|
15
20
|
createExportComponent(override?: Partial<LegendConfigBase>): LayoutAwareComponent;
|
|
16
21
|
setToggleCallback(callback: () => void): void;
|
|
17
22
|
isSeriesVisible(dataKey: string): boolean;
|
|
23
|
+
estimateLayoutSpace(series: LegendSeries[], theme: ChartTheme, width: number, svg: SVGSVGElement): void;
|
|
18
24
|
private getCheckmarkPath;
|
|
19
25
|
/**
|
|
20
26
|
* Returns the space required by the legend
|
|
21
27
|
*/
|
|
22
28
|
getRequiredSpace(): ComponentSpace;
|
|
23
29
|
render(svg: Selection<SVGSVGElement, undefined, null, undefined>, series: LegendSeries[], theme: ChartTheme, width: number, _x?: number, y?: number): void;
|
|
30
|
+
private computeLayout;
|
|
31
|
+
private resolveLayoutSettings;
|
|
32
|
+
private buildLegendItems;
|
|
33
|
+
private measureLegendItemWidths;
|
|
34
|
+
private buildRows;
|
|
35
|
+
private positionRows;
|
|
36
|
+
private getLayoutSignature;
|
|
37
|
+
private getFallbackRowHeight;
|
|
24
38
|
}
|
package/legend.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { select } from 'd3';
|
|
1
2
|
import { getSeriesColor } from './types.js';
|
|
2
3
|
import { getContrastTextColor, mergeDeep } from './utils.js';
|
|
3
4
|
export class Legend {
|
|
@@ -32,11 +33,29 @@ export class Legend {
|
|
|
32
33
|
writable: true,
|
|
33
34
|
value: void 0
|
|
34
35
|
});
|
|
35
|
-
Object.defineProperty(this, "
|
|
36
|
+
Object.defineProperty(this, "paddingX", {
|
|
36
37
|
enumerable: true,
|
|
37
38
|
configurable: true,
|
|
38
39
|
writable: true,
|
|
39
|
-
value:
|
|
40
|
+
value: void 0
|
|
41
|
+
});
|
|
42
|
+
Object.defineProperty(this, "itemSpacingX", {
|
|
43
|
+
enumerable: true,
|
|
44
|
+
configurable: true,
|
|
45
|
+
writable: true,
|
|
46
|
+
value: void 0
|
|
47
|
+
});
|
|
48
|
+
Object.defineProperty(this, "itemSpacingY", {
|
|
49
|
+
enumerable: true,
|
|
50
|
+
configurable: true,
|
|
51
|
+
writable: true,
|
|
52
|
+
value: void 0
|
|
53
|
+
});
|
|
54
|
+
Object.defineProperty(this, "gapBetweenBoxAndText", {
|
|
55
|
+
enumerable: true,
|
|
56
|
+
configurable: true,
|
|
57
|
+
writable: true,
|
|
58
|
+
value: 8
|
|
40
59
|
});
|
|
41
60
|
Object.defineProperty(this, "visibilityState", {
|
|
42
61
|
enumerable: true,
|
|
@@ -50,9 +69,24 @@ export class Legend {
|
|
|
50
69
|
writable: true,
|
|
51
70
|
value: void 0
|
|
52
71
|
});
|
|
72
|
+
Object.defineProperty(this, "estimatedLayout", {
|
|
73
|
+
enumerable: true,
|
|
74
|
+
configurable: true,
|
|
75
|
+
writable: true,
|
|
76
|
+
value: null
|
|
77
|
+
});
|
|
78
|
+
Object.defineProperty(this, "estimatedLayoutSignature", {
|
|
79
|
+
enumerable: true,
|
|
80
|
+
configurable: true,
|
|
81
|
+
writable: true,
|
|
82
|
+
value: null
|
|
83
|
+
});
|
|
53
84
|
this.position = config?.position || 'bottom';
|
|
54
85
|
this.marginTop = config?.marginTop ?? 20;
|
|
55
86
|
this.marginBottom = config?.marginBottom ?? 10;
|
|
87
|
+
this.paddingX = config?.paddingX;
|
|
88
|
+
this.itemSpacingX = config?.itemSpacingX;
|
|
89
|
+
this.itemSpacingY = config?.itemSpacingY;
|
|
56
90
|
this.exportHooks = config?.exportHooks;
|
|
57
91
|
}
|
|
58
92
|
getExportConfig() {
|
|
@@ -60,6 +94,9 @@ export class Legend {
|
|
|
60
94
|
position: this.position,
|
|
61
95
|
marginTop: this.marginTop,
|
|
62
96
|
marginBottom: this.marginBottom,
|
|
97
|
+
paddingX: this.paddingX,
|
|
98
|
+
itemSpacingX: this.itemSpacingX,
|
|
99
|
+
itemSpacingY: this.itemSpacingY,
|
|
63
100
|
};
|
|
64
101
|
}
|
|
65
102
|
createExportComponent(override) {
|
|
@@ -77,6 +114,11 @@ export class Legend {
|
|
|
77
114
|
isSeriesVisible(dataKey) {
|
|
78
115
|
return this.visibilityState.get(dataKey) ?? true;
|
|
79
116
|
}
|
|
117
|
+
estimateLayoutSpace(series, theme, width, svg) {
|
|
118
|
+
const signature = this.getLayoutSignature(series, width, theme);
|
|
119
|
+
this.estimatedLayout = this.computeLayout(series, theme, width, svg);
|
|
120
|
+
this.estimatedLayoutSignature = signature;
|
|
121
|
+
}
|
|
80
122
|
getCheckmarkPath(size) {
|
|
81
123
|
const scale = (size / 24) * 0.7;
|
|
82
124
|
const offsetX = size * 0.15;
|
|
@@ -89,56 +131,32 @@ export class Legend {
|
|
|
89
131
|
getRequiredSpace() {
|
|
90
132
|
return {
|
|
91
133
|
width: 0, // Legend spans full width
|
|
92
|
-
height: this.
|
|
134
|
+
height: this.estimatedLayout?.requiredHeight ??
|
|
135
|
+
this.marginTop + this.marginBottom,
|
|
93
136
|
position: 'bottom',
|
|
94
137
|
};
|
|
95
138
|
}
|
|
96
139
|
render(svg, series, theme, width, _x = 0, y = 0) {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
this.visibilityState.set(item.dataKey, true);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
// Create temporary text elements to measure widths
|
|
111
|
-
const tempSvg = svg.append('g').style('visibility', 'hidden');
|
|
112
|
-
const itemWidths = legendItems.map((item) => {
|
|
113
|
-
const textElem = tempSvg
|
|
114
|
-
.append('text')
|
|
115
|
-
.attr('font-size', `${theme.legend.fontSize}px`)
|
|
116
|
-
.attr('font-family', theme.axis.fontFamily)
|
|
117
|
-
.text(item.label);
|
|
118
|
-
const textWidth = textElem.node()?.getBBox().width || 0;
|
|
119
|
-
textElem.remove();
|
|
120
|
-
return boxSize + gapBetweenBoxAndText + textWidth;
|
|
121
|
-
});
|
|
122
|
-
tempSvg.remove();
|
|
123
|
-
// Calculate positions for each item
|
|
124
|
-
const itemPositions = [];
|
|
125
|
-
let currentX = 0;
|
|
126
|
-
itemWidths.forEach((itemWidth) => {
|
|
127
|
-
itemPositions.push(currentX);
|
|
128
|
-
currentX += itemWidth + itemSpacing;
|
|
129
|
-
});
|
|
130
|
-
const totalLegendWidth = currentX - itemSpacing;
|
|
131
|
-
const legendX = (width - totalLegendWidth) / 2;
|
|
140
|
+
const svgNode = svg.node();
|
|
141
|
+
if (!svgNode) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const signature = this.getLayoutSignature(series, width, theme);
|
|
145
|
+
const layout = this.estimatedLayout && this.estimatedLayoutSignature === signature
|
|
146
|
+
? this.estimatedLayout
|
|
147
|
+
: this.computeLayout(series, theme, width, svgNode);
|
|
148
|
+
this.estimatedLayout = layout;
|
|
149
|
+
this.estimatedLayoutSignature = signature;
|
|
132
150
|
const legendY = y + this.marginTop;
|
|
133
151
|
const legend = svg
|
|
134
152
|
.append('g')
|
|
135
153
|
.attr('class', 'legend')
|
|
136
|
-
.attr('transform', `translate(
|
|
154
|
+
.attr('transform', `translate(0, ${legendY})`);
|
|
137
155
|
const legendGroups = legend
|
|
138
156
|
.selectAll('g')
|
|
139
|
-
.data(
|
|
157
|
+
.data(layout.positionedItems)
|
|
140
158
|
.join('g')
|
|
141
|
-
.attr('transform', (
|
|
159
|
+
.attr('transform', (d) => `translate(${d.x}, ${d.y})`)
|
|
142
160
|
.style('cursor', 'pointer')
|
|
143
161
|
.on('click', (_event, d) => {
|
|
144
162
|
const currentState = this.visibilityState.get(d.dataKey) ?? true;
|
|
@@ -150,8 +168,8 @@ export class Legend {
|
|
|
150
168
|
// Add checkbox rect
|
|
151
169
|
legendGroups
|
|
152
170
|
.append('rect')
|
|
153
|
-
.attr('width', boxSize)
|
|
154
|
-
.attr('height', boxSize)
|
|
171
|
+
.attr('width', theme.legend.boxSize)
|
|
172
|
+
.attr('height', theme.legend.boxSize)
|
|
155
173
|
.attr('fill', (d) => {
|
|
156
174
|
const isVisible = this.visibilityState.get(d.dataKey) ?? true;
|
|
157
175
|
return isVisible ? d.color : theme.legend.uncheckedColor;
|
|
@@ -160,7 +178,7 @@ export class Legend {
|
|
|
160
178
|
// Add checkmark when visible
|
|
161
179
|
legendGroups
|
|
162
180
|
.append('path')
|
|
163
|
-
.attr('d', this.getCheckmarkPath(boxSize))
|
|
181
|
+
.attr('d', this.getCheckmarkPath(theme.legend.boxSize))
|
|
164
182
|
.attr('fill', 'none')
|
|
165
183
|
.attr('stroke', (d) => getContrastTextColor(d.color))
|
|
166
184
|
.attr('stroke-width', 2)
|
|
@@ -173,10 +191,149 @@ export class Legend {
|
|
|
173
191
|
// Add label text
|
|
174
192
|
legendGroups
|
|
175
193
|
.append('text')
|
|
176
|
-
.attr('x', boxSize + gapBetweenBoxAndText)
|
|
177
|
-
.attr('y', boxSize / 2 + 4)
|
|
194
|
+
.attr('x', theme.legend.boxSize + this.gapBetweenBoxAndText)
|
|
195
|
+
.attr('y', theme.legend.boxSize / 2 + 4)
|
|
178
196
|
.attr('font-size', `${theme.legend.fontSize}px`)
|
|
179
197
|
.attr('font-family', theme.axis.fontFamily)
|
|
180
198
|
.text((d) => d.label);
|
|
181
199
|
}
|
|
200
|
+
computeLayout(series, theme, width, svg) {
|
|
201
|
+
const settings = this.resolveLayoutSettings(theme);
|
|
202
|
+
const legendItems = this.buildLegendItems(series);
|
|
203
|
+
legendItems.forEach((item) => {
|
|
204
|
+
if (!this.visibilityState.has(item.dataKey)) {
|
|
205
|
+
this.visibilityState.set(item.dataKey, true);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
const measuredItems = this.measureLegendItemWidths(legendItems, theme, svg);
|
|
209
|
+
const rows = this.buildRows(measuredItems, width, settings);
|
|
210
|
+
const positionedItems = this.positionRows(rows, width, settings);
|
|
211
|
+
const rowCount = Math.max(rows.length, 1);
|
|
212
|
+
const rowsHeight = rows.length > 0
|
|
213
|
+
? rows.reduce((sum, row) => {
|
|
214
|
+
return sum + row.height;
|
|
215
|
+
}, 0)
|
|
216
|
+
: this.getFallbackRowHeight(theme);
|
|
217
|
+
const requiredHeight = this.marginTop +
|
|
218
|
+
rowsHeight +
|
|
219
|
+
Math.max(0, rowCount - 1) * settings.itemSpacingY +
|
|
220
|
+
this.marginBottom;
|
|
221
|
+
return {
|
|
222
|
+
positionedItems,
|
|
223
|
+
requiredHeight,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
resolveLayoutSettings(theme) {
|
|
227
|
+
return {
|
|
228
|
+
paddingX: this.paddingX ?? theme.legend.paddingX,
|
|
229
|
+
itemSpacingX: this.itemSpacingX ?? theme.legend.itemSpacingX,
|
|
230
|
+
itemSpacingY: this.itemSpacingY ?? theme.legend.itemSpacingY,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
buildLegendItems(series) {
|
|
234
|
+
return series.map((item) => ({
|
|
235
|
+
label: item.dataKey,
|
|
236
|
+
color: getSeriesColor(item),
|
|
237
|
+
dataKey: item.dataKey,
|
|
238
|
+
width: 0,
|
|
239
|
+
height: 0,
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
measureLegendItemWidths(legendItems, theme, svg) {
|
|
243
|
+
const tempSvg = select(svg).append('g').style('visibility', 'hidden');
|
|
244
|
+
const measuredItems = legendItems.map((item) => {
|
|
245
|
+
const textElem = tempSvg
|
|
246
|
+
.append('text')
|
|
247
|
+
.attr('font-size', `${theme.legend.fontSize}px`)
|
|
248
|
+
.attr('font-family', theme.axis.fontFamily)
|
|
249
|
+
.text(item.label);
|
|
250
|
+
const textBBox = textElem.node()?.getBBox();
|
|
251
|
+
const textWidth = textBBox?.width || 0;
|
|
252
|
+
const textHeight = textBBox?.height || theme.legend.fontSize;
|
|
253
|
+
textElem.remove();
|
|
254
|
+
return {
|
|
255
|
+
...item,
|
|
256
|
+
width: theme.legend.boxSize +
|
|
257
|
+
this.gapBetweenBoxAndText +
|
|
258
|
+
textWidth,
|
|
259
|
+
height: Math.max(theme.legend.boxSize, textHeight),
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
tempSvg.remove();
|
|
263
|
+
return measuredItems;
|
|
264
|
+
}
|
|
265
|
+
buildRows(legendItems, width, settings) {
|
|
266
|
+
const rows = [];
|
|
267
|
+
const availableWidth = Math.max(0, width - settings.paddingX * 2);
|
|
268
|
+
let currentRow = {
|
|
269
|
+
items: [],
|
|
270
|
+
width: 0,
|
|
271
|
+
height: 0,
|
|
272
|
+
};
|
|
273
|
+
legendItems.forEach((item) => {
|
|
274
|
+
const nextWidth = currentRow.items.length === 0
|
|
275
|
+
? item.width
|
|
276
|
+
: currentRow.width + settings.itemSpacingX + item.width;
|
|
277
|
+
if (currentRow.items.length > 0 &&
|
|
278
|
+
nextWidth > availableWidth) {
|
|
279
|
+
rows.push(currentRow);
|
|
280
|
+
currentRow = {
|
|
281
|
+
items: [item],
|
|
282
|
+
width: item.width,
|
|
283
|
+
height: item.height,
|
|
284
|
+
};
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
currentRow.items.push(item);
|
|
288
|
+
currentRow.width = nextWidth;
|
|
289
|
+
currentRow.height = Math.max(currentRow.height, item.height);
|
|
290
|
+
});
|
|
291
|
+
if (currentRow.items.length > 0) {
|
|
292
|
+
rows.push(currentRow);
|
|
293
|
+
}
|
|
294
|
+
return rows;
|
|
295
|
+
}
|
|
296
|
+
positionRows(rows, width, settings) {
|
|
297
|
+
const positionedItems = [];
|
|
298
|
+
const availableWidth = Math.max(0, width - settings.paddingX * 2);
|
|
299
|
+
let accumulatedRowHeight = 0;
|
|
300
|
+
rows.forEach((row, rowIndex) => {
|
|
301
|
+
const rowX = settings.paddingX +
|
|
302
|
+
Math.max(0, (availableWidth - row.width) / 2);
|
|
303
|
+
const rowY = accumulatedRowHeight + rowIndex * settings.itemSpacingY;
|
|
304
|
+
let currentX = rowX;
|
|
305
|
+
row.items.forEach((item, itemIndex) => {
|
|
306
|
+
const centeredY = rowY + (row.height - item.height) / 2;
|
|
307
|
+
positionedItems.push({
|
|
308
|
+
...item,
|
|
309
|
+
x: currentX,
|
|
310
|
+
y: centeredY,
|
|
311
|
+
});
|
|
312
|
+
currentX += item.width;
|
|
313
|
+
if (itemIndex < row.items.length - 1) {
|
|
314
|
+
currentX += settings.itemSpacingX;
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
accumulatedRowHeight += row.height;
|
|
318
|
+
});
|
|
319
|
+
return positionedItems;
|
|
320
|
+
}
|
|
321
|
+
getLayoutSignature(series, width, theme) {
|
|
322
|
+
return [
|
|
323
|
+
width,
|
|
324
|
+
theme.legend.boxSize,
|
|
325
|
+
theme.legend.fontSize,
|
|
326
|
+
theme.axis.fontFamily,
|
|
327
|
+
theme.legend.paddingX,
|
|
328
|
+
theme.legend.itemSpacingX,
|
|
329
|
+
theme.legend.itemSpacingY,
|
|
330
|
+
this.paddingX ?? '',
|
|
331
|
+
this.itemSpacingX ?? '',
|
|
332
|
+
this.itemSpacingY ?? '',
|
|
333
|
+
series.map((item) => item.dataKey).join('|'),
|
|
334
|
+
].join(':');
|
|
335
|
+
}
|
|
336
|
+
getFallbackRowHeight(theme) {
|
|
337
|
+
return Math.max(theme.legend.boxSize, theme.legend.fontSize);
|
|
338
|
+
}
|
|
182
339
|
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.6.
|
|
2
|
+
"version": "0.6.1",
|
|
3
3
|
"name": "@internetstiftelsen/charts",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"dev": "vite",
|
|
19
19
|
"build": "tsc -b && vite build",
|
|
20
20
|
"lint": "eslint .",
|
|
21
|
+
"format": "prettier --write ./src",
|
|
21
22
|
"preview": "vite preview",
|
|
22
23
|
"test": "vitest",
|
|
23
24
|
"test:run": "vitest run",
|
package/pie-chart.d.ts
CHANGED
|
@@ -59,9 +59,11 @@ export declare class PieChart extends BaseChart {
|
|
|
59
59
|
protected getExportComponents(): ChartComponent[];
|
|
60
60
|
update(data: DataItem[]): void;
|
|
61
61
|
protected getLayoutComponents(): LayoutAwareComponent[];
|
|
62
|
+
protected prepareLayout(): void;
|
|
62
63
|
protected createExportChart(): BaseChart;
|
|
63
64
|
protected renderChart(): void;
|
|
64
65
|
private resolveFontScale;
|
|
66
|
+
private getLegendSeries;
|
|
65
67
|
private resolveSortComparator;
|
|
66
68
|
private renderSegments;
|
|
67
69
|
private handleSegmentKeyNavigation;
|
package/pie-chart.js
CHANGED
|
@@ -218,6 +218,12 @@ export class PieChart extends BaseChart {
|
|
|
218
218
|
}
|
|
219
219
|
return components;
|
|
220
220
|
}
|
|
221
|
+
prepareLayout() {
|
|
222
|
+
const svgNode = this.svg?.node();
|
|
223
|
+
if (svgNode && this.legend) {
|
|
224
|
+
this.legend.estimateLayoutSpace(this.getLegendSeries(), this.theme, this.width, svgNode);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
221
227
|
createExportChart() {
|
|
222
228
|
return new PieChart({
|
|
223
229
|
data: this.data,
|
|
@@ -264,11 +270,7 @@ export class PieChart extends BaseChart {
|
|
|
264
270
|
}
|
|
265
271
|
if (this.legend) {
|
|
266
272
|
const pos = this.layoutManager.getComponentPosition(this.legend);
|
|
267
|
-
|
|
268
|
-
dataKey: seg.label,
|
|
269
|
-
fill: seg.color,
|
|
270
|
-
}));
|
|
271
|
-
this.legend.render(this.svg, legendSeries, this.theme, this.width, pos.x, pos.y);
|
|
273
|
+
this.legend.render(this.svg, this.getLegendSeries(), this.theme, this.width, pos.x, pos.y);
|
|
272
274
|
}
|
|
273
275
|
}
|
|
274
276
|
resolveFontScale(outerRadius) {
|
|
@@ -277,6 +279,14 @@ export class PieChart extends BaseChart {
|
|
|
277
279
|
const rawScale = outerRadius / referenceRadius;
|
|
278
280
|
return Math.max(0.5, Math.min(1, rawScale));
|
|
279
281
|
}
|
|
282
|
+
getLegendSeries() {
|
|
283
|
+
return this.segments.map((segment) => {
|
|
284
|
+
return {
|
|
285
|
+
dataKey: segment.label,
|
|
286
|
+
fill: segment.color,
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
}
|
|
280
290
|
resolveSortComparator() {
|
|
281
291
|
if (typeof this.sort === 'function') {
|
|
282
292
|
return this.sort;
|
package/theme.js
CHANGED
|
@@ -36,6 +36,9 @@ export const defaultTheme = {
|
|
|
36
36
|
boxSize: 24,
|
|
37
37
|
uncheckedColor: '#d0d0d0',
|
|
38
38
|
fontSize: 14,
|
|
39
|
+
paddingX: 0,
|
|
40
|
+
itemSpacingX: 20,
|
|
41
|
+
itemSpacingY: 8,
|
|
39
42
|
},
|
|
40
43
|
line: {
|
|
41
44
|
strokeWidth: 4,
|
|
@@ -115,6 +118,9 @@ export const newspaperTheme = {
|
|
|
115
118
|
boxSize: 18,
|
|
116
119
|
uncheckedColor: '#d3d3d3',
|
|
117
120
|
fontSize: 13,
|
|
121
|
+
paddingX: 0,
|
|
122
|
+
itemSpacingX: 20,
|
|
123
|
+
itemSpacingY: 8,
|
|
118
124
|
},
|
|
119
125
|
line: {
|
|
120
126
|
strokeWidth: 2.5,
|
package/types.d.ts
CHANGED
|
@@ -63,6 +63,9 @@ export type ChartTheme = {
|
|
|
63
63
|
boxSize: number;
|
|
64
64
|
uncheckedColor: string;
|
|
65
65
|
fontSize: number;
|
|
66
|
+
paddingX: number;
|
|
67
|
+
itemSpacingX: number;
|
|
68
|
+
itemSpacingY: number;
|
|
66
69
|
};
|
|
67
70
|
line: {
|
|
68
71
|
strokeWidth: number;
|
|
@@ -227,6 +230,9 @@ export type LegendConfigBase = {
|
|
|
227
230
|
position?: 'bottom';
|
|
228
231
|
marginTop?: number;
|
|
229
232
|
marginBottom?: number;
|
|
233
|
+
paddingX?: number;
|
|
234
|
+
itemSpacingX?: number;
|
|
235
|
+
itemSpacingY?: number;
|
|
230
236
|
};
|
|
231
237
|
export type LegendConfig = LegendConfigBase & {
|
|
232
238
|
exportHooks?: ExportHooks<LegendConfigBase>;
|
package/xy-chart.d.ts
CHANGED
package/xy-chart.js
CHANGED
|
@@ -129,19 +129,22 @@ export class XYChart extends BaseChart {
|
|
|
129
129
|
this.update(this.sourceData);
|
|
130
130
|
}
|
|
131
131
|
prepareLayout() {
|
|
132
|
+
const svgNode = this.svg?.node();
|
|
132
133
|
this.xAxis?.clearEstimatedSpace?.();
|
|
133
|
-
if (
|
|
134
|
-
|
|
134
|
+
if (svgNode && this.xAxis) {
|
|
135
|
+
const xKey = this.getXKey();
|
|
136
|
+
const labelKey = this.xAxis.labelKey;
|
|
137
|
+
const labels = this.data.map((item) => {
|
|
138
|
+
if (labelKey) {
|
|
139
|
+
return item[labelKey];
|
|
140
|
+
}
|
|
141
|
+
return item[xKey];
|
|
142
|
+
});
|
|
143
|
+
this.xAxis.estimateLayoutSpace?.(labels, this.theme, svgNode);
|
|
144
|
+
}
|
|
145
|
+
if (svgNode && this.legend) {
|
|
146
|
+
this.legend.estimateLayoutSpace(this.getLegendSeries(), this.theme, this.width, svgNode);
|
|
135
147
|
}
|
|
136
|
-
const xKey = this.getXKey();
|
|
137
|
-
const labelKey = this.xAxis.labelKey;
|
|
138
|
-
const labels = this.data.map((item) => {
|
|
139
|
-
if (labelKey) {
|
|
140
|
-
return item[labelKey];
|
|
141
|
-
}
|
|
142
|
-
return item[xKey];
|
|
143
|
-
});
|
|
144
|
-
this.xAxis.estimateLayoutSpace?.(labels, this.theme, this.svg.node());
|
|
145
148
|
}
|
|
146
149
|
renderChart() {
|
|
147
150
|
if (!this.plotArea) {
|
|
@@ -198,24 +201,7 @@ export class XYChart extends BaseChart {
|
|
|
198
201
|
}
|
|
199
202
|
if (this.legend) {
|
|
200
203
|
const legendPos = this.layoutManager.getComponentPosition(this.legend);
|
|
201
|
-
this.legend.render(this.svg, this.
|
|
202
|
-
if (series.type === 'line') {
|
|
203
|
-
return {
|
|
204
|
-
dataKey: series.dataKey,
|
|
205
|
-
stroke: series.stroke,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
if (series.type === 'bar') {
|
|
209
|
-
return {
|
|
210
|
-
dataKey: series.dataKey,
|
|
211
|
-
fill: series.fill,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
return {
|
|
215
|
-
dataKey: series.dataKey,
|
|
216
|
-
fill: series.fill,
|
|
217
|
-
};
|
|
218
|
-
}), this.theme, this.width, legendPos.x, legendPos.y);
|
|
204
|
+
this.legend.render(this.svg, this.getLegendSeries(), this.theme, this.width, legendPos.x, legendPos.y);
|
|
219
205
|
}
|
|
220
206
|
}
|
|
221
207
|
getXKey() {
|
|
@@ -224,6 +210,20 @@ export class XYChart extends BaseChart {
|
|
|
224
210
|
}
|
|
225
211
|
return (Object.keys(this.data[0]).find((key) => !this.series.some((s) => s.dataKey === key)) || 'column');
|
|
226
212
|
}
|
|
213
|
+
getLegendSeries() {
|
|
214
|
+
return this.series.map((series) => {
|
|
215
|
+
if (series.type === 'line') {
|
|
216
|
+
return {
|
|
217
|
+
dataKey: series.dataKey,
|
|
218
|
+
stroke: series.stroke,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
dataKey: series.dataKey,
|
|
223
|
+
fill: series.fill,
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
227
|
getCategoryScaleType() {
|
|
228
228
|
return this.scaleConfig.x?.type || 'band';
|
|
229
229
|
}
|