@internetstiftelsen/charts 0.5.1 → 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/README.md +4 -2
- package/area.d.ts +26 -0
- package/area.js +331 -0
- package/base-chart.d.ts +10 -3
- package/base-chart.js +33 -19
- package/chart-interface.d.ts +1 -1
- package/donut-center-content.d.ts +1 -1
- package/donut-center-content.js +7 -6
- package/donut-chart.d.ts +3 -0
- package/donut-chart.js +23 -6
- package/export-tabular.d.ts +4 -4
- package/export-tabular.js +8 -0
- package/export-xlsx.d.ts +2 -2
- package/gauge-chart.d.ts +140 -0
- package/gauge-chart.js +1051 -0
- package/grouped-data.d.ts +19 -0
- package/grouped-data.js +122 -0
- package/grouped-tabular.d.ts +26 -0
- package/grouped-tabular.js +149 -0
- package/legend.d.ts +15 -1
- package/legend.js +203 -46
- package/package.json +2 -1
- package/pie-chart.d.ts +82 -0
- package/pie-chart.js +675 -0
- package/theme.d.ts +1 -0
- package/theme.js +31 -16
- package/tooltip.d.ts +3 -2
- package/tooltip.js +40 -39
- package/types.d.ts +52 -0
- package/validation.d.ts +4 -0
- package/validation.js +25 -0
- package/x-axis.d.ts +10 -2
- package/x-axis.js +205 -15
- package/xy-chart.d.ts +11 -1
- package/xy-chart.js +310 -93
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ A framework-agnostic, composable charting library built on D3.js with TypeScript
|
|
|
6
6
|
|
|
7
7
|
- **Framework Agnostic** - Works with vanilla JS, React, Vue, Svelte, or any framework
|
|
8
8
|
- **Composable Architecture** - Build charts by composing components
|
|
9
|
-
- **Multiple Chart Types** - XYChart (lines, bars) and
|
|
9
|
+
- **Multiple Chart Types** - XYChart (lines, areas, bars), DonutChart, PieChart, and GaugeChart
|
|
10
10
|
- **Flexible Scales** - Band, linear, time, and logarithmic scales
|
|
11
11
|
- **Auto Resize** - Built-in ResizeObserver handles responsive behavior
|
|
12
12
|
- **Type Safe** - Written in TypeScript with full type definitions
|
|
@@ -60,8 +60,10 @@ await chart.export('pdf', { download: true, pdfMargin: 16 });
|
|
|
60
60
|
## Documentation
|
|
61
61
|
|
|
62
62
|
- [Getting Started](./docs/getting-started.md) - Installation, Vanilla JS, React integration
|
|
63
|
-
- [XYChart](./docs/xy-chart.md) - Line and bar charts API
|
|
63
|
+
- [XYChart](./docs/xy-chart.md) - Line, area, and bar charts API
|
|
64
64
|
- [DonutChart](./docs/donut-chart.md) - Donut/pie charts API
|
|
65
|
+
- [PieChart](./docs/pie-chart.md) - Pie chart API
|
|
66
|
+
- [GaugeChart](./docs/gauge-chart.md) - Gauge chart API
|
|
65
67
|
- [Components](./docs/components.md) - Axes, Grid, Tooltip, Legend, Title
|
|
66
68
|
- [Theming](./docs/theming.md) - Colors, fonts, and styling
|
|
67
69
|
- [Advanced](./docs/advanced.md) - Scales, TypeScript, architecture, performance
|
package/area.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Selection } from 'd3';
|
|
2
|
+
import type { AreaConfig, AreaCurveType, AreaConfigBase, AreaStackingContext, ChartTheme, D3Scale, DataItem, ExportHooks, LineValueLabelConfig, ScaleType } from './types.js';
|
|
3
|
+
import type { ChartComponent } from './chart-interface.js';
|
|
4
|
+
export declare class Area implements ChartComponent<AreaConfigBase> {
|
|
5
|
+
readonly type: "area";
|
|
6
|
+
readonly dataKey: string;
|
|
7
|
+
readonly fill: string;
|
|
8
|
+
readonly stroke: string;
|
|
9
|
+
readonly strokeWidth?: number;
|
|
10
|
+
readonly opacity: number;
|
|
11
|
+
readonly curve: AreaCurveType;
|
|
12
|
+
readonly stackId?: string | number;
|
|
13
|
+
readonly baseline: number;
|
|
14
|
+
readonly showLine: boolean;
|
|
15
|
+
readonly showPoints: boolean;
|
|
16
|
+
readonly pointSize?: number;
|
|
17
|
+
readonly valueLabel?: LineValueLabelConfig;
|
|
18
|
+
readonly exportHooks?: ExportHooks<AreaConfigBase>;
|
|
19
|
+
constructor(config: AreaConfig);
|
|
20
|
+
getExportConfig(): AreaConfigBase;
|
|
21
|
+
createExportComponent(override?: Partial<AreaConfigBase>): ChartComponent;
|
|
22
|
+
private getScaledPosition;
|
|
23
|
+
private getStackValues;
|
|
24
|
+
render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: D3Scale, y: D3Scale, parseValue: (value: unknown) => number, xScaleType: ScaleType | undefined, theme: ChartTheme, stackingContext?: AreaStackingContext, valueLabelLayer?: Selection<SVGGElement, undefined, null, undefined>): void;
|
|
25
|
+
private renderValueLabels;
|
|
26
|
+
}
|
package/area.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { area, curveBasis, curveCardinal, curveLinear, curveMonotoneX, curveNatural, curveStep, line, } from 'd3';
|
|
2
|
+
import { mergeDeep, sanitizeForCSS } from './utils.js';
|
|
3
|
+
const AREA_CURVE_FACTORIES = {
|
|
4
|
+
linear: curveLinear,
|
|
5
|
+
monotone: curveMonotoneX,
|
|
6
|
+
step: curveStep,
|
|
7
|
+
natural: curveNatural,
|
|
8
|
+
basis: curveBasis,
|
|
9
|
+
cardinal: curveCardinal,
|
|
10
|
+
};
|
|
11
|
+
export class Area {
|
|
12
|
+
constructor(config) {
|
|
13
|
+
Object.defineProperty(this, "type", {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
configurable: true,
|
|
16
|
+
writable: true,
|
|
17
|
+
value: 'area'
|
|
18
|
+
});
|
|
19
|
+
Object.defineProperty(this, "dataKey", {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value: void 0
|
|
24
|
+
});
|
|
25
|
+
Object.defineProperty(this, "fill", {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
configurable: true,
|
|
28
|
+
writable: true,
|
|
29
|
+
value: void 0
|
|
30
|
+
});
|
|
31
|
+
Object.defineProperty(this, "stroke", {
|
|
32
|
+
enumerable: true,
|
|
33
|
+
configurable: true,
|
|
34
|
+
writable: true,
|
|
35
|
+
value: void 0
|
|
36
|
+
});
|
|
37
|
+
Object.defineProperty(this, "strokeWidth", {
|
|
38
|
+
enumerable: true,
|
|
39
|
+
configurable: true,
|
|
40
|
+
writable: true,
|
|
41
|
+
value: void 0
|
|
42
|
+
});
|
|
43
|
+
Object.defineProperty(this, "opacity", {
|
|
44
|
+
enumerable: true,
|
|
45
|
+
configurable: true,
|
|
46
|
+
writable: true,
|
|
47
|
+
value: void 0
|
|
48
|
+
});
|
|
49
|
+
Object.defineProperty(this, "curve", {
|
|
50
|
+
enumerable: true,
|
|
51
|
+
configurable: true,
|
|
52
|
+
writable: true,
|
|
53
|
+
value: void 0
|
|
54
|
+
});
|
|
55
|
+
Object.defineProperty(this, "stackId", {
|
|
56
|
+
enumerable: true,
|
|
57
|
+
configurable: true,
|
|
58
|
+
writable: true,
|
|
59
|
+
value: void 0
|
|
60
|
+
});
|
|
61
|
+
Object.defineProperty(this, "baseline", {
|
|
62
|
+
enumerable: true,
|
|
63
|
+
configurable: true,
|
|
64
|
+
writable: true,
|
|
65
|
+
value: void 0
|
|
66
|
+
});
|
|
67
|
+
Object.defineProperty(this, "showLine", {
|
|
68
|
+
enumerable: true,
|
|
69
|
+
configurable: true,
|
|
70
|
+
writable: true,
|
|
71
|
+
value: void 0
|
|
72
|
+
});
|
|
73
|
+
Object.defineProperty(this, "showPoints", {
|
|
74
|
+
enumerable: true,
|
|
75
|
+
configurable: true,
|
|
76
|
+
writable: true,
|
|
77
|
+
value: void 0
|
|
78
|
+
});
|
|
79
|
+
Object.defineProperty(this, "pointSize", {
|
|
80
|
+
enumerable: true,
|
|
81
|
+
configurable: true,
|
|
82
|
+
writable: true,
|
|
83
|
+
value: void 0
|
|
84
|
+
});
|
|
85
|
+
Object.defineProperty(this, "valueLabel", {
|
|
86
|
+
enumerable: true,
|
|
87
|
+
configurable: true,
|
|
88
|
+
writable: true,
|
|
89
|
+
value: void 0
|
|
90
|
+
});
|
|
91
|
+
Object.defineProperty(this, "exportHooks", {
|
|
92
|
+
enumerable: true,
|
|
93
|
+
configurable: true,
|
|
94
|
+
writable: true,
|
|
95
|
+
value: void 0
|
|
96
|
+
});
|
|
97
|
+
const fill = config.fill || '#8884d8';
|
|
98
|
+
this.dataKey = config.dataKey;
|
|
99
|
+
this.fill = fill;
|
|
100
|
+
this.stroke = config.stroke || fill;
|
|
101
|
+
this.strokeWidth = config.strokeWidth;
|
|
102
|
+
this.opacity = config.opacity ?? 0.3;
|
|
103
|
+
this.curve = config.curve || 'linear';
|
|
104
|
+
this.stackId = config.stackId;
|
|
105
|
+
this.baseline = config.baseline ?? 0;
|
|
106
|
+
this.showLine = config.showLine ?? true;
|
|
107
|
+
this.showPoints = config.showPoints ?? false;
|
|
108
|
+
this.pointSize = config.pointSize;
|
|
109
|
+
this.valueLabel = config.valueLabel;
|
|
110
|
+
this.exportHooks = config.exportHooks;
|
|
111
|
+
}
|
|
112
|
+
getExportConfig() {
|
|
113
|
+
return {
|
|
114
|
+
dataKey: this.dataKey,
|
|
115
|
+
fill: this.fill,
|
|
116
|
+
stroke: this.stroke,
|
|
117
|
+
strokeWidth: this.strokeWidth,
|
|
118
|
+
opacity: this.opacity,
|
|
119
|
+
curve: this.curve,
|
|
120
|
+
stackId: this.stackId,
|
|
121
|
+
baseline: this.baseline,
|
|
122
|
+
showLine: this.showLine,
|
|
123
|
+
showPoints: this.showPoints,
|
|
124
|
+
pointSize: this.pointSize,
|
|
125
|
+
valueLabel: this.valueLabel,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
createExportComponent(override) {
|
|
129
|
+
const merged = mergeDeep(this.getExportConfig(), override);
|
|
130
|
+
return new Area({
|
|
131
|
+
...merged,
|
|
132
|
+
exportHooks: this.exportHooks,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
getScaledPosition(data, key, scale, scaleType) {
|
|
136
|
+
const value = data[key];
|
|
137
|
+
let scaledValue;
|
|
138
|
+
switch (scaleType) {
|
|
139
|
+
case 'band':
|
|
140
|
+
scaledValue = String(value);
|
|
141
|
+
break;
|
|
142
|
+
case 'time':
|
|
143
|
+
scaledValue =
|
|
144
|
+
value instanceof Date ? value : new Date(String(value));
|
|
145
|
+
break;
|
|
146
|
+
case 'linear':
|
|
147
|
+
case 'log':
|
|
148
|
+
scaledValue = typeof value === 'number' ? value : Number(value);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
return scale(scaledValue) || 0;
|
|
152
|
+
}
|
|
153
|
+
getStackValues(dataPoint, xKey, parseValue, stackingContext) {
|
|
154
|
+
const value = parseValue(dataPoint[this.dataKey]);
|
|
155
|
+
if (!stackingContext || stackingContext.mode === 'none') {
|
|
156
|
+
return {
|
|
157
|
+
y0: this.baseline,
|
|
158
|
+
y1: value,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const categoryKey = String(dataPoint[xKey]);
|
|
162
|
+
const cumulative = stackingContext.cumulativeData.get(categoryKey) ?? 0;
|
|
163
|
+
if (stackingContext.mode === 'percent') {
|
|
164
|
+
const total = stackingContext.totalData.get(categoryKey) ?? 0;
|
|
165
|
+
if (total === 0) {
|
|
166
|
+
return { y0: 0, y1: 0 };
|
|
167
|
+
}
|
|
168
|
+
const y0 = (cumulative / total) * 100;
|
|
169
|
+
const y1 = ((cumulative + value) / total) * 100;
|
|
170
|
+
return { y0, y1 };
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
y0: cumulative,
|
|
174
|
+
y1: cumulative + value,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme, stackingContext, valueLabelLayer) {
|
|
178
|
+
const getXPosition = (d) => {
|
|
179
|
+
const scaled = this.getScaledPosition(d.data, xKey, x, xScaleType);
|
|
180
|
+
return scaled + (x.bandwidth ? x.bandwidth() / 2 : 0);
|
|
181
|
+
};
|
|
182
|
+
const hasValidValue = (d) => {
|
|
183
|
+
const value = d[this.dataKey];
|
|
184
|
+
if (value === null || value === undefined) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
return Number.isFinite(parseValue(value));
|
|
188
|
+
};
|
|
189
|
+
const areaData = data.map((d) => {
|
|
190
|
+
const valid = hasValidValue(d);
|
|
191
|
+
const stackValues = valid
|
|
192
|
+
? this.getStackValues(d, xKey, parseValue, stackingContext)
|
|
193
|
+
: { y0: this.baseline, y1: this.baseline };
|
|
194
|
+
return {
|
|
195
|
+
data: d,
|
|
196
|
+
valid,
|
|
197
|
+
y0: stackValues.y0,
|
|
198
|
+
y1: stackValues.y1,
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
const curveFactory = AREA_CURVE_FACTORIES[this.curve] || curveLinear;
|
|
202
|
+
const areaGenerator = area()
|
|
203
|
+
.defined((d) => d.valid)
|
|
204
|
+
.curve(curveFactory)
|
|
205
|
+
.x(getXPosition)
|
|
206
|
+
.y0((d) => y(d.y0) || 0)
|
|
207
|
+
.y1((d) => y(d.y1) || 0);
|
|
208
|
+
const areaOpacity = Math.max(0, Math.min(1, this.opacity));
|
|
209
|
+
const sanitizedKey = sanitizeForCSS(this.dataKey);
|
|
210
|
+
plotGroup
|
|
211
|
+
.append('path')
|
|
212
|
+
.datum(areaData)
|
|
213
|
+
.attr('class', `area-${sanitizedKey}`)
|
|
214
|
+
.attr('fill', this.fill)
|
|
215
|
+
.attr('fill-opacity', areaOpacity)
|
|
216
|
+
.attr('stroke', 'none')
|
|
217
|
+
.attr('d', areaGenerator);
|
|
218
|
+
if (this.showLine) {
|
|
219
|
+
const lineGenerator = line()
|
|
220
|
+
.defined((d) => d.valid)
|
|
221
|
+
.curve(curveFactory)
|
|
222
|
+
.x(getXPosition)
|
|
223
|
+
.y((d) => y(d.y1) || 0);
|
|
224
|
+
const lineStrokeWidth = this.strokeWidth ?? theme.line.strokeWidth;
|
|
225
|
+
plotGroup
|
|
226
|
+
.append('path')
|
|
227
|
+
.datum(areaData)
|
|
228
|
+
.attr('class', `area-line-${sanitizedKey}`)
|
|
229
|
+
.attr('fill', 'none')
|
|
230
|
+
.attr('stroke', this.stroke)
|
|
231
|
+
.attr('stroke-width', lineStrokeWidth)
|
|
232
|
+
.attr('d', lineGenerator);
|
|
233
|
+
}
|
|
234
|
+
if (this.showPoints) {
|
|
235
|
+
const validData = areaData.filter((d) => d.valid);
|
|
236
|
+
const pointSize = this.pointSize ?? theme.line.point.size;
|
|
237
|
+
const pointStrokeWidth = theme.line.point.strokeWidth;
|
|
238
|
+
const pointStrokeColor = theme.line.point.strokeColor || this.stroke;
|
|
239
|
+
const pointColor = theme.line.point.color || this.stroke;
|
|
240
|
+
plotGroup
|
|
241
|
+
.selectAll(`.area-point-${sanitizedKey}`)
|
|
242
|
+
.data(validData)
|
|
243
|
+
.join('circle')
|
|
244
|
+
.attr('class', `area-point-${sanitizedKey}`)
|
|
245
|
+
.attr('cx', getXPosition)
|
|
246
|
+
.attr('cy', (d) => y(d.y1) || 0)
|
|
247
|
+
.attr('r', pointSize)
|
|
248
|
+
.attr('fill', pointColor)
|
|
249
|
+
.attr('stroke', pointStrokeColor)
|
|
250
|
+
.attr('stroke-width', pointStrokeWidth);
|
|
251
|
+
}
|
|
252
|
+
if (this.valueLabel?.show) {
|
|
253
|
+
this.renderValueLabels(valueLabelLayer ?? plotGroup, areaData.filter((d) => d.valid), y, parseValue, theme, getXPosition);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
renderValueLabels(plotGroup, data, y, parseValue, theme, getXPosition) {
|
|
257
|
+
const config = this.valueLabel;
|
|
258
|
+
const fontSize = config.fontSize ?? theme.valueLabel.fontSize;
|
|
259
|
+
const fontFamily = config.fontFamily ?? theme.valueLabel.fontFamily;
|
|
260
|
+
const fontWeight = config.fontWeight ?? theme.valueLabel.fontWeight;
|
|
261
|
+
const color = config.color ?? theme.valueLabel.color;
|
|
262
|
+
const background = config.background ?? theme.valueLabel.background ?? '#ffffff';
|
|
263
|
+
const border = config.border ?? theme.valueLabel.border;
|
|
264
|
+
const borderRadius = config.borderRadius ?? theme.valueLabel.borderRadius;
|
|
265
|
+
const padding = config.padding ?? theme.valueLabel.padding;
|
|
266
|
+
const labelGroup = plotGroup
|
|
267
|
+
.append('g')
|
|
268
|
+
.attr('class', `area-value-labels-${sanitizeForCSS(this.dataKey)}`);
|
|
269
|
+
const plotTop = y.range()[1];
|
|
270
|
+
const plotBottom = y.range()[0];
|
|
271
|
+
data.forEach((d) => {
|
|
272
|
+
const rawValue = d.data[this.dataKey];
|
|
273
|
+
if (rawValue === null || rawValue === undefined) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const parsedValue = parseValue(rawValue);
|
|
277
|
+
if (!Number.isFinite(parsedValue)) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const valueText = String(parsedValue);
|
|
281
|
+
const xPos = getXPosition(d);
|
|
282
|
+
const yPos = y(d.y1) || 0;
|
|
283
|
+
const tempText = labelGroup
|
|
284
|
+
.append('text')
|
|
285
|
+
.style('font-size', `${fontSize}px`)
|
|
286
|
+
.style('font-family', fontFamily)
|
|
287
|
+
.style('font-weight', fontWeight)
|
|
288
|
+
.text(valueText);
|
|
289
|
+
const textBBox = tempText.node().getBBox();
|
|
290
|
+
const boxWidth = textBBox.width + padding * 2;
|
|
291
|
+
const boxHeight = textBBox.height + padding * 2;
|
|
292
|
+
const labelX = xPos;
|
|
293
|
+
let labelY = yPos - boxHeight / 2 - theme.line.point.size - 4;
|
|
294
|
+
let shouldRender = true;
|
|
295
|
+
if (labelY - boxHeight / 2 < plotTop + 4) {
|
|
296
|
+
labelY = yPos + boxHeight / 2 + theme.line.point.size + 4;
|
|
297
|
+
if (labelY + boxHeight / 2 > plotBottom - 4) {
|
|
298
|
+
shouldRender = false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
tempText.remove();
|
|
302
|
+
if (!shouldRender) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const group = labelGroup.append('g');
|
|
306
|
+
group
|
|
307
|
+
.append('rect')
|
|
308
|
+
.attr('x', labelX - boxWidth / 2)
|
|
309
|
+
.attr('y', labelY - boxHeight / 2)
|
|
310
|
+
.attr('width', boxWidth)
|
|
311
|
+
.attr('height', boxHeight)
|
|
312
|
+
.attr('rx', borderRadius)
|
|
313
|
+
.attr('ry', borderRadius)
|
|
314
|
+
.attr('fill', background)
|
|
315
|
+
.attr('stroke', border)
|
|
316
|
+
.attr('stroke-width', 1);
|
|
317
|
+
group
|
|
318
|
+
.append('text')
|
|
319
|
+
.attr('x', labelX)
|
|
320
|
+
.attr('y', labelY)
|
|
321
|
+
.attr('text-anchor', 'middle')
|
|
322
|
+
.attr('dominant-baseline', 'central')
|
|
323
|
+
.style('font-size', `${fontSize}px`)
|
|
324
|
+
.style('font-family', fontFamily)
|
|
325
|
+
.style('font-weight', fontWeight)
|
|
326
|
+
.style('fill', color)
|
|
327
|
+
.style('pointer-events', 'none')
|
|
328
|
+
.text(valueText);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
package/base-chart.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
|
-
import type { DataItem, ChartTheme, AxisScaleConfig, ExportFormat, ExportOptions, D3Scale, ExportHookContext, ExportRenderContext } from './types.js';
|
|
2
|
+
import type { ChartData, DataItem, ChartTheme, AxisScaleConfig, ExportFormat, ExportOptions, D3Scale, ExportHookContext, ExportRenderContext } from './types.js';
|
|
3
3
|
import type { ChartComponent, LayoutAwareComponent } from './chart-interface.js';
|
|
4
4
|
import type { XAxis } from './x-axis.js';
|
|
5
5
|
import type { YAxis } from './y-axis.js';
|
|
@@ -9,8 +9,13 @@ import type { Legend } from './legend.js';
|
|
|
9
9
|
import type { Title } from './title.js';
|
|
10
10
|
import { LayoutManager, type PlotAreaBounds } from './layout-manager.js';
|
|
11
11
|
type VisualExportFormat = 'svg' | 'png' | 'jpg' | 'pdf';
|
|
12
|
+
type RenderDimensions = {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
svgHeightAttr: number | string;
|
|
16
|
+
};
|
|
12
17
|
export type BaseChartConfig = {
|
|
13
|
-
data:
|
|
18
|
+
data: ChartData;
|
|
14
19
|
theme?: Partial<ChartTheme>;
|
|
15
20
|
scales?: AxisScaleConfig;
|
|
16
21
|
};
|
|
@@ -19,6 +24,7 @@ export type BaseChartConfig = {
|
|
|
19
24
|
*/
|
|
20
25
|
export declare abstract class BaseChart {
|
|
21
26
|
protected data: DataItem[];
|
|
27
|
+
protected sourceData: ChartData;
|
|
22
28
|
protected readonly theme: ChartTheme;
|
|
23
29
|
protected readonly scaleConfig: AxisScaleConfig;
|
|
24
30
|
protected width: number;
|
|
@@ -51,6 +57,7 @@ export declare abstract class BaseChart {
|
|
|
51
57
|
* Performs the actual rendering logic
|
|
52
58
|
*/
|
|
53
59
|
private performRender;
|
|
60
|
+
protected resolveRenderDimensions(containerRect: DOMRect): RenderDimensions;
|
|
54
61
|
/**
|
|
55
62
|
* Get layout-aware components in order
|
|
56
63
|
* Override in subclasses to provide chart-specific components
|
|
@@ -73,7 +80,7 @@ export declare abstract class BaseChart {
|
|
|
73
80
|
/**
|
|
74
81
|
* Updates the chart with new data
|
|
75
82
|
*/
|
|
76
|
-
update(data:
|
|
83
|
+
update(data: ChartData): void;
|
|
77
84
|
/**
|
|
78
85
|
* Destroys the chart and cleans up resources
|
|
79
86
|
*/
|
package/base-chart.js
CHANGED
|
@@ -6,6 +6,7 @@ import { serializeCSV } from './export-tabular.js';
|
|
|
6
6
|
import { exportRasterBlob } from './export-image.js';
|
|
7
7
|
import { exportXLSXBlob } from './export-xlsx.js';
|
|
8
8
|
import { exportPDFBlob } from './export-pdf.js';
|
|
9
|
+
import { normalizeChartData } from './grouped-data.js';
|
|
9
10
|
/**
|
|
10
11
|
* Base chart class that provides common functionality for all chart types
|
|
11
12
|
*/
|
|
@@ -17,6 +18,12 @@ export class BaseChart {
|
|
|
17
18
|
writable: true,
|
|
18
19
|
value: void 0
|
|
19
20
|
});
|
|
21
|
+
Object.defineProperty(this, "sourceData", {
|
|
22
|
+
enumerable: true,
|
|
23
|
+
configurable: true,
|
|
24
|
+
writable: true,
|
|
25
|
+
value: void 0
|
|
26
|
+
});
|
|
20
27
|
Object.defineProperty(this, "theme", {
|
|
21
28
|
enumerable: true,
|
|
22
29
|
configurable: true,
|
|
@@ -125,9 +132,10 @@ export class BaseChart {
|
|
|
125
132
|
writable: true,
|
|
126
133
|
value: null
|
|
127
134
|
});
|
|
128
|
-
|
|
129
|
-
ChartValidator.validateData(
|
|
130
|
-
this.
|
|
135
|
+
const normalized = normalizeChartData(config.data);
|
|
136
|
+
ChartValidator.validateData(normalized.data);
|
|
137
|
+
this.sourceData = config.data;
|
|
138
|
+
this.data = normalized.data;
|
|
131
139
|
this.theme = { ...defaultTheme, ...config.theme };
|
|
132
140
|
this.width = this.theme.width;
|
|
133
141
|
this.height = this.theme.height;
|
|
@@ -164,18 +172,17 @@ export class BaseChart {
|
|
|
164
172
|
* Performs the actual rendering logic
|
|
165
173
|
*/
|
|
166
174
|
performRender() {
|
|
167
|
-
if (!this.container)
|
|
175
|
+
if (!this.container) {
|
|
168
176
|
return;
|
|
169
|
-
|
|
170
|
-
this.
|
|
171
|
-
|
|
172
|
-
this.height =
|
|
173
|
-
this.container.getBoundingClientRect().height || this.theme.height;
|
|
177
|
+
}
|
|
178
|
+
const dimensions = this.resolveRenderDimensions(this.container.getBoundingClientRect());
|
|
179
|
+
this.width = dimensions.width;
|
|
180
|
+
this.height = dimensions.height;
|
|
174
181
|
// Clear and setup SVG
|
|
175
182
|
this.container.innerHTML = '';
|
|
176
183
|
this.svg = create('svg')
|
|
177
184
|
.attr('width', '100%')
|
|
178
|
-
.attr('height',
|
|
185
|
+
.attr('height', dimensions.svgHeightAttr)
|
|
179
186
|
.style('display', 'block');
|
|
180
187
|
this.container.appendChild(this.svg.node());
|
|
181
188
|
this.prepareLayout();
|
|
@@ -193,6 +200,15 @@ export class BaseChart {
|
|
|
193
200
|
// Render chart content
|
|
194
201
|
this.renderChart();
|
|
195
202
|
}
|
|
203
|
+
resolveRenderDimensions(containerRect) {
|
|
204
|
+
const width = containerRect.width || this.theme.width;
|
|
205
|
+
const height = containerRect.height || this.theme.height;
|
|
206
|
+
return {
|
|
207
|
+
width,
|
|
208
|
+
height,
|
|
209
|
+
svgHeightAttr: '100%',
|
|
210
|
+
};
|
|
211
|
+
}
|
|
196
212
|
/**
|
|
197
213
|
* Get layout-aware components in order
|
|
198
214
|
* Override in subclasses to provide chart-specific components
|
|
@@ -298,8 +314,10 @@ export class BaseChart {
|
|
|
298
314
|
* Updates the chart with new data
|
|
299
315
|
*/
|
|
300
316
|
update(data) {
|
|
301
|
-
|
|
302
|
-
|
|
317
|
+
const normalized = normalizeChartData(data);
|
|
318
|
+
ChartValidator.validateData(normalized.data);
|
|
319
|
+
this.sourceData = data;
|
|
320
|
+
this.data = normalized.data;
|
|
303
321
|
if (!this.container) {
|
|
304
322
|
throw new Error('Chart must be rendered before update()');
|
|
305
323
|
}
|
|
@@ -402,7 +420,7 @@ export class BaseChart {
|
|
|
402
420
|
return 'application/pdf';
|
|
403
421
|
}
|
|
404
422
|
exportCSV(options) {
|
|
405
|
-
return serializeCSV(this.
|
|
423
|
+
return serializeCSV(this.sourceData, options);
|
|
406
424
|
}
|
|
407
425
|
exportSize(options) {
|
|
408
426
|
return {
|
|
@@ -411,7 +429,7 @@ export class BaseChart {
|
|
|
411
429
|
};
|
|
412
430
|
}
|
|
413
431
|
async exportXLSX(options) {
|
|
414
|
-
return exportXLSXBlob(this.
|
|
432
|
+
return exportXLSXBlob(this.sourceData, options);
|
|
415
433
|
}
|
|
416
434
|
async exportImage(format, options) {
|
|
417
435
|
const { width, height } = this.exportSize(options);
|
|
@@ -493,10 +511,6 @@ export class BaseChart {
|
|
|
493
511
|
return exportSvg.outerHTML;
|
|
494
512
|
}
|
|
495
513
|
exportJSON() {
|
|
496
|
-
return JSON.stringify(
|
|
497
|
-
data: this.data,
|
|
498
|
-
theme: this.theme,
|
|
499
|
-
scales: this.scaleConfig,
|
|
500
|
-
}, null, 2);
|
|
514
|
+
return JSON.stringify(this.sourceData, null, 2);
|
|
501
515
|
}
|
|
502
516
|
}
|
package/chart-interface.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExportHooks } from './types.js';
|
|
2
2
|
export interface ChartComponent<TConfig = any> {
|
|
3
|
-
type: 'line' | 'bar' | 'xAxis' | 'yAxis' | 'grid' | 'tooltip' | 'legend' | 'title' | 'donutCenterContent';
|
|
3
|
+
type: 'line' | 'area' | 'bar' | 'xAxis' | 'yAxis' | 'grid' | 'tooltip' | 'legend' | 'title' | 'donutCenterContent';
|
|
4
4
|
exportHooks?: ExportHooks<TConfig>;
|
|
5
5
|
}
|
|
6
6
|
export type ComponentSpace = {
|
|
@@ -28,6 +28,6 @@ export declare class DonutCenterContent implements ChartComponent<DonutCenterCon
|
|
|
28
28
|
constructor(config?: DonutCenterContentConfig);
|
|
29
29
|
getExportConfig(): DonutCenterContentConfigBase;
|
|
30
30
|
createExportComponent(override?: Partial<DonutCenterContentConfigBase>): ChartComponent;
|
|
31
|
-
render(svg: Selection<SVGSVGElement, undefined, null, undefined>, cx: number, cy: number, theme: ChartTheme): void;
|
|
31
|
+
render(svg: Selection<SVGSVGElement, undefined, null, undefined>, cx: number, cy: number, theme: ChartTheme, fontScale?: number): void;
|
|
32
32
|
}
|
|
33
33
|
export {};
|
package/donut-center-content.js
CHANGED
|
@@ -60,14 +60,14 @@ export class DonutCenterContent {
|
|
|
60
60
|
exportHooks: this.exportHooks,
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
|
-
render(svg, cx, cy, theme) {
|
|
63
|
+
render(svg, cx, cy, theme, fontScale = 1) {
|
|
64
64
|
const defaults = theme.donut.centerContent;
|
|
65
65
|
const elements = [];
|
|
66
66
|
if (this.mainValue) {
|
|
67
67
|
const style = this.config.mainValueStyle;
|
|
68
68
|
elements.push({
|
|
69
69
|
text: this.mainValue,
|
|
70
|
-
fontSize: style?.fontSize ?? defaults.mainValue.fontSize,
|
|
70
|
+
fontSize: (style?.fontSize ?? defaults.mainValue.fontSize) * fontScale,
|
|
71
71
|
fontWeight: style?.fontWeight ?? defaults.mainValue.fontWeight,
|
|
72
72
|
fontFamily: style?.fontFamily ??
|
|
73
73
|
defaults.mainValue.fontFamily ??
|
|
@@ -79,7 +79,7 @@ export class DonutCenterContent {
|
|
|
79
79
|
const style = this.config.titleStyle;
|
|
80
80
|
elements.push({
|
|
81
81
|
text: this.title,
|
|
82
|
-
fontSize: style?.fontSize ?? defaults.title.fontSize,
|
|
82
|
+
fontSize: (style?.fontSize ?? defaults.title.fontSize) * fontScale,
|
|
83
83
|
fontWeight: style?.fontWeight ?? defaults.title.fontWeight,
|
|
84
84
|
fontFamily: style?.fontFamily ??
|
|
85
85
|
defaults.title.fontFamily ??
|
|
@@ -91,7 +91,7 @@ export class DonutCenterContent {
|
|
|
91
91
|
const style = this.config.subtitleStyle;
|
|
92
92
|
elements.push({
|
|
93
93
|
text: this.subtitle,
|
|
94
|
-
fontSize: style?.fontSize ?? defaults.subtitle.fontSize,
|
|
94
|
+
fontSize: (style?.fontSize ?? defaults.subtitle.fontSize) * fontScale,
|
|
95
95
|
fontWeight: style?.fontWeight ?? defaults.subtitle.fontWeight,
|
|
96
96
|
fontFamily: style?.fontFamily ??
|
|
97
97
|
defaults.subtitle.fontFamily ??
|
|
@@ -99,9 +99,10 @@ export class DonutCenterContent {
|
|
|
99
99
|
color: style?.color ?? defaults.subtitle.color,
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
|
-
if (elements.length === 0)
|
|
102
|
+
if (elements.length === 0) {
|
|
103
103
|
return;
|
|
104
|
-
|
|
104
|
+
}
|
|
105
|
+
const lineSpacing = Math.max(2, 6 * fontScale);
|
|
105
106
|
const totalHeight = elements.reduce((sum, el, i) => sum + el.fontSize + (i < elements.length - 1 ? lineSpacing : 0), 0);
|
|
106
107
|
const group = svg.append('g').attr('class', 'donut-center-content');
|
|
107
108
|
let currentY = cy - totalHeight / 2;
|
package/donut-chart.d.ts
CHANGED
|
@@ -26,8 +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;
|
|
32
|
+
private resolveFontScale;
|
|
33
|
+
private getLegendSeries;
|
|
31
34
|
private positionTooltip;
|
|
32
35
|
private buildTooltipContent;
|
|
33
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,
|
|
@@ -157,22 +163,33 @@ export class DonutChart extends BaseChart {
|
|
|
157
163
|
const cy = this.plotArea.top + this.plotArea.height / 2;
|
|
158
164
|
const outerRadius = Math.min(this.plotArea.width, this.plotArea.height) / 2;
|
|
159
165
|
const innerRadius = outerRadius * this.innerRadiusRatio;
|
|
166
|
+
const fontScale = this.resolveFontScale(outerRadius);
|
|
160
167
|
if (this.tooltip) {
|
|
161
168
|
this.tooltip.initialize(this.theme);
|
|
162
169
|
}
|
|
163
170
|
this.renderSegments(visibleSegments, cx, cy, innerRadius, outerRadius);
|
|
164
171
|
if (this.centerContent) {
|
|
165
|
-
this.centerContent.render(this.svg, cx, cy, this.theme);
|
|
172
|
+
this.centerContent.render(this.svg, cx, cy, this.theme, fontScale);
|
|
166
173
|
}
|
|
167
174
|
if (this.legend) {
|
|
168
175
|
const pos = this.layoutManager.getComponentPosition(this.legend);
|
|
169
|
-
|
|
170
|
-
dataKey: seg.label,
|
|
171
|
-
fill: seg.color,
|
|
172
|
-
}));
|
|
173
|
-
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);
|
|
174
177
|
}
|
|
175
178
|
}
|
|
179
|
+
resolveFontScale(outerRadius) {
|
|
180
|
+
const plotHeight = Math.max(1, this.theme.height - this.theme.margins.top - this.theme.margins.bottom);
|
|
181
|
+
const referenceRadius = Math.max(1, plotHeight / 2);
|
|
182
|
+
const rawScale = outerRadius / referenceRadius;
|
|
183
|
+
return Math.max(0.5, Math.min(1, rawScale));
|
|
184
|
+
}
|
|
185
|
+
getLegendSeries() {
|
|
186
|
+
return this.segments.map((segment) => {
|
|
187
|
+
return {
|
|
188
|
+
dataKey: segment.label,
|
|
189
|
+
fill: segment.color,
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
}
|
|
176
193
|
positionTooltip(event, tooltipDiv) {
|
|
177
194
|
const node = tooltipDiv.node();
|
|
178
195
|
if (!node)
|