@internetstiftelsen/charts 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -377
- package/base-chart.d.ts +3 -2
- package/base-chart.js +1 -0
- package/chart-interface.d.ts +1 -1
- package/donut-center-content.d.ts +27 -0
- package/donut-center-content.js +98 -0
- package/donut-chart.d.ts +32 -0
- package/donut-chart.js +240 -0
- package/legend.d.ts +2 -4
- package/package.json +1 -1
- package/theme.js +44 -0
- package/types.d.ts +30 -0
package/README.md
CHANGED
|
@@ -4,15 +4,14 @@ A framework-agnostic, composable charting library built on D3.js with TypeScript
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **Framework Agnostic
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **Type Safe
|
|
13
|
-
- **Data Validation
|
|
14
|
-
- **
|
|
15
|
-
- **Automatic Color Assignment**: Smart color palette system with sensible defaults
|
|
7
|
+
- **Framework Agnostic** - Works with vanilla JS, React, Vue, Svelte, or any framework
|
|
8
|
+
- **Composable Architecture** - Build charts by composing components
|
|
9
|
+
- **Multiple Chart Types** - XYChart (lines, bars) and DonutChart
|
|
10
|
+
- **Flexible Scales** - Band, linear, time, and logarithmic scales
|
|
11
|
+
- **Auto Resize** - Built-in ResizeObserver handles responsive behavior
|
|
12
|
+
- **Type Safe** - Written in TypeScript with full type definitions
|
|
13
|
+
- **Data Validation** - Built-in validation with helpful error messages
|
|
14
|
+
- **Auto Colors** - Smart color palette with sensible defaults
|
|
16
15
|
|
|
17
16
|
## Installation
|
|
18
17
|
|
|
@@ -22,396 +21,41 @@ npm install @internetstiftelsen/charts
|
|
|
22
21
|
|
|
23
22
|
## Quick Start
|
|
24
23
|
|
|
25
|
-
### Vanilla JavaScript
|
|
26
|
-
|
|
27
24
|
```javascript
|
|
28
25
|
import { XYChart } from '@internetstiftelsen/charts/xy-chart';
|
|
29
26
|
import { Line } from '@internetstiftelsen/charts/line';
|
|
30
|
-
import { Bar } from '@internetstiftelsen/charts/bar';
|
|
31
27
|
import { XAxis } from '@internetstiftelsen/charts/x-axis';
|
|
32
28
|
import { YAxis } from '@internetstiftelsen/charts/y-axis';
|
|
33
|
-
import { Grid } from '@internetstiftelsen/charts/grid';
|
|
34
|
-
import { Tooltip } from '@internetstiftelsen/charts/tooltip';
|
|
35
|
-
import { Legend } from '@internetstiftelsen/charts/legend';
|
|
36
29
|
|
|
37
|
-
// Your data
|
|
38
30
|
const data = [
|
|
39
|
-
{ date: '
|
|
40
|
-
{ date: '
|
|
41
|
-
{ date: '2012', revenue: 200, expenses: 110 },
|
|
42
|
-
{ date: '2013', revenue: 250, expenses: 130 },
|
|
31
|
+
{ date: '2023', revenue: 100, expenses: 80 },
|
|
32
|
+
{ date: '2024', revenue: 150, expenses: 90 },
|
|
43
33
|
];
|
|
44
34
|
|
|
45
|
-
// Create chart
|
|
46
35
|
const chart = new XYChart({ data });
|
|
47
36
|
|
|
48
|
-
// Add components
|
|
49
37
|
chart
|
|
50
|
-
.addChild(new Title({ text: 'Revenue vs Expenses' }))
|
|
51
|
-
.addChild(new Grid({ horizontal: true, vertical: false }))
|
|
52
38
|
.addChild(new XAxis({ dataKey: 'date' }))
|
|
53
39
|
.addChild(new YAxis())
|
|
54
|
-
.addChild(
|
|
55
|
-
|
|
56
|
-
formatter: (dataKey, value) => `<strong>${dataKey}</strong>: $${value}k`,
|
|
57
|
-
})
|
|
58
|
-
)
|
|
59
|
-
.addChild(new Legend({ position: 'bottom' }))
|
|
60
|
-
.addChild(new Line({ dataKey: 'revenue' })) // Auto-assigned color
|
|
61
|
-
.addChild(new Line({ dataKey: 'expenses' })); // Auto-assigned color
|
|
40
|
+
.addChild(new Line({ dataKey: 'revenue' }))
|
|
41
|
+
.addChild(new Line({ dataKey: 'expenses' }));
|
|
62
42
|
|
|
63
|
-
// Render to DOM (automatically resizes with container)
|
|
64
43
|
chart.render('#chart-container');
|
|
65
|
-
|
|
66
|
-
// Later: update with new data
|
|
67
|
-
chart.update(newData);
|
|
68
|
-
|
|
69
|
-
// Clean up when done
|
|
70
|
-
chart.destroy();
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### With React (Demo Wrapper)
|
|
74
|
-
|
|
75
|
-
```jsx
|
|
76
|
-
import { useRef, useEffect } from 'react';
|
|
77
|
-
import { XYChart, Line, Bar, XAxis, YAxis, Grid, Tooltip, Legend } from './charts';
|
|
78
|
-
|
|
79
|
-
function Chart({ data }) {
|
|
80
|
-
const containerRef = useRef(null);
|
|
81
|
-
const chartRef = useRef(null);
|
|
82
|
-
|
|
83
|
-
useEffect(() => {
|
|
84
|
-
if (containerRef.current) {
|
|
85
|
-
// Create chart
|
|
86
|
-
const chart = new XYChart({ data });
|
|
87
|
-
|
|
88
|
-
chart
|
|
89
|
-
.addChild(new Grid({ horizontal: true }))
|
|
90
|
-
.addChild(new XAxis({ dataKey: 'column' }))
|
|
91
|
-
.addChild(new YAxis())
|
|
92
|
-
.addChild(new Tooltip())
|
|
93
|
-
.addChild(new Legend({ position: 'bottom' }))
|
|
94
|
-
.addChild(new Line({ dataKey: 'value1' }))
|
|
95
|
-
.addChild(new Line({ dataKey: 'value2' }));
|
|
96
|
-
|
|
97
|
-
chart.render(containerRef.current);
|
|
98
|
-
chartRef.current = chart;
|
|
99
|
-
|
|
100
|
-
return () => {
|
|
101
|
-
chart.destroy();
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
}, []);
|
|
105
|
-
|
|
106
|
-
// Update when data changes
|
|
107
|
-
useEffect(() => {
|
|
108
|
-
if (chartRef.current && data) {
|
|
109
|
-
chartRef.current.update(data);
|
|
110
|
-
}
|
|
111
|
-
}, [data]);
|
|
112
|
-
|
|
113
|
-
return <div ref={containerRef} />;
|
|
114
|
-
}
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
## API Reference
|
|
118
|
-
|
|
119
|
-
### XYChart
|
|
120
|
-
|
|
121
|
-
The main chart class for creating XY-coordinate charts (line, bar, or mixed).
|
|
122
|
-
|
|
123
|
-
#### Constructor
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
126
|
-
new XYChart(config: XYChartConfig)
|
|
127
44
|
```
|
|
128
45
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
- `data: DataItem[]` - Array of data objects (required)
|
|
132
|
-
- `theme?: Partial<ChartTheme>` - Theme customization
|
|
133
|
-
- `width: number` - Chart max-width in pixels (default: 928)
|
|
134
|
-
- `height: number` - Chart height in pixels (default: 600)
|
|
135
|
-
- `margins: { top, right, bottom, left }` - Base margins around plot area (default: { top: 20, right: 20, bottom: 30, left: 40 })
|
|
136
|
-
- `colorPalette: string[]` - Array of colors for auto-assignment
|
|
137
|
-
- `gridColor: string` - Grid line color (default: '#e0e0e0')
|
|
138
|
-
- `axis: { fontFamily, fontSize }` - Axis text styling
|
|
139
|
-
- `scales?: AxisScaleConfig` - Scale configuration
|
|
140
|
-
- `x?: { type: 'band' | 'linear' | 'time' | 'log', domain?: any[], padding?: number, nice?: boolean }`
|
|
141
|
-
- `y?: { type: 'band' | 'linear' | 'time' | 'log', domain?: any[], padding?: number, nice?: boolean }`
|
|
142
|
-
|
|
143
|
-
#### Methods
|
|
46
|
+
## Documentation
|
|
144
47
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
**`update(data: DataItem[]): void`**
|
|
152
|
-
Updates the chart with new data and re-renders.
|
|
153
|
-
|
|
154
|
-
**`destroy(): void`**
|
|
155
|
-
Cleans up all resources, removes resize observer, and clears the chart from the DOM.
|
|
156
|
-
|
|
157
|
-
### Components
|
|
158
|
-
|
|
159
|
-
#### Line
|
|
160
|
-
|
|
161
|
-
Renders a line series on the chart.
|
|
162
|
-
|
|
163
|
-
```typescript
|
|
164
|
-
new Line({
|
|
165
|
-
dataKey: string, // Key in data objects for Y values (required)
|
|
166
|
-
stroke? : string, // Line color (auto-assigned if omitted)
|
|
167
|
-
strokeWidth? : number, // Line width in pixels (default: 2)
|
|
168
|
-
})
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
#### Bar
|
|
172
|
-
|
|
173
|
-
Renders a bar series on the chart.
|
|
174
|
-
|
|
175
|
-
```typescript
|
|
176
|
-
new Bar({
|
|
177
|
-
dataKey: string, // Key in data objects for Y values (required)
|
|
178
|
-
fill? : string, // Bar color (auto-assigned if omitted)
|
|
179
|
-
})
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
#### XAxis
|
|
183
|
-
|
|
184
|
-
Renders the X axis.
|
|
185
|
-
|
|
186
|
-
```typescript
|
|
187
|
-
new XAxis({
|
|
188
|
-
dataKey? : string, // Key in data objects for X values (auto-detected if omitted)
|
|
189
|
-
})
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
#### YAxis
|
|
193
|
-
|
|
194
|
-
Renders the Y axis.
|
|
195
|
-
|
|
196
|
-
```typescript
|
|
197
|
-
new YAxis({
|
|
198
|
-
tickFormat?: string | null, // D3 format specifier (e.g., 's' for SI-prefix like "35k"). Default: null (no formatting)
|
|
199
|
-
})
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
**Examples:**
|
|
203
|
-
```javascript
|
|
204
|
-
new YAxis() // Shows raw numbers: 35000
|
|
205
|
-
new YAxis({ tickFormat: 's' }) // Shows SI-prefix: 35k
|
|
206
|
-
new YAxis({ tickFormat: '$,' }) // Shows formatted: $35,000
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
#### Grid
|
|
210
|
-
|
|
211
|
-
Renders grid lines.
|
|
212
|
-
|
|
213
|
-
```typescript
|
|
214
|
-
new Grid({
|
|
215
|
-
horizontal? : boolean, // Show horizontal lines (default: true)
|
|
216
|
-
vertical? : boolean, // Show vertical lines (default: true)
|
|
217
|
-
})
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
#### Tooltip
|
|
221
|
-
|
|
222
|
-
Renders interactive tooltips on hover.
|
|
223
|
-
|
|
224
|
-
```typescript
|
|
225
|
-
new Tooltip({
|
|
226
|
-
formatter? : (dataKey: string, value: any, data: DataItem) => string
|
|
227
|
-
})
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
**Example formatter:**
|
|
231
|
-
|
|
232
|
-
```javascript
|
|
233
|
-
new Tooltip({
|
|
234
|
-
formatter: (dataKey, value, data) =>
|
|
235
|
-
`<strong>${dataKey}</strong><br/>Value: ${value}<br/>Date: ${data.date}`
|
|
236
|
-
})
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
#### Legend
|
|
240
|
-
|
|
241
|
-
Renders a legend for the chart.
|
|
242
|
-
|
|
243
|
-
```typescript
|
|
244
|
-
new Legend({
|
|
245
|
-
position?: 'bottom', // Position (currently only 'bottom' supported)
|
|
246
|
-
marginTop?: number, // Space above legend (default: 20)
|
|
247
|
-
marginBottom?: number, // Space below legend (default: 10)
|
|
248
|
-
})
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
#### Title
|
|
252
|
-
|
|
253
|
-
Renders a title for the chart.
|
|
254
|
-
|
|
255
|
-
```typescript
|
|
256
|
-
new Title({
|
|
257
|
-
text: string, // Title text (required)
|
|
258
|
-
fontSize?: number, // Font size in pixels (default: 18)
|
|
259
|
-
fontWeight?: string, // Font weight (default: 'bold')
|
|
260
|
-
align?: 'left' | 'center' | 'right', // Alignment (default: 'center')
|
|
261
|
-
marginTop?: number, // Space above title (default: 10)
|
|
262
|
-
marginBottom?: number, // Space below title (default: 15)
|
|
263
|
-
})
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
## Advanced Usage
|
|
267
|
-
|
|
268
|
-
### Custom Scale Types
|
|
269
|
-
|
|
270
|
-
Use time scales for temporal data:
|
|
271
|
-
|
|
272
|
-
```javascript
|
|
273
|
-
const chart = new XYChart({
|
|
274
|
-
data: [
|
|
275
|
-
{ date: new Date('2024-01-01'), value: 100 },
|
|
276
|
-
{ date: new Date('2024-01-02'), value: 150 },
|
|
277
|
-
],
|
|
278
|
-
scales: {
|
|
279
|
-
x: { type: 'time', nice: true },
|
|
280
|
-
y: { type: 'linear', nice: true },
|
|
281
|
-
},
|
|
282
|
-
});
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
Use logarithmic scales for exponential data:
|
|
286
|
-
|
|
287
|
-
```javascript
|
|
288
|
-
const chart = new XYChart({
|
|
289
|
-
data: [
|
|
290
|
-
{ x: 1, y: 10 },
|
|
291
|
-
{ x: 2, y: 100 },
|
|
292
|
-
{ x: 3, y: 1000 },
|
|
293
|
-
],
|
|
294
|
-
scales: {
|
|
295
|
-
y: { type: 'log', domain: [1, 10000] },
|
|
296
|
-
},
|
|
297
|
-
});
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
### Custom Theming
|
|
301
|
-
|
|
302
|
-
```javascript
|
|
303
|
-
const chart = new XYChart({
|
|
304
|
-
data,
|
|
305
|
-
theme: {
|
|
306
|
-
width: 1200, // Max-width (chart won't exceed this)
|
|
307
|
-
height: 600,
|
|
308
|
-
margins: {
|
|
309
|
-
top: 30,
|
|
310
|
-
right: 30,
|
|
311
|
-
bottom: 40,
|
|
312
|
-
left: 60,
|
|
313
|
-
},
|
|
314
|
-
colorPalette: ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f7b731'],
|
|
315
|
-
gridColor: '#333333',
|
|
316
|
-
axis: {
|
|
317
|
-
fontFamily: 'Inter, sans-serif',
|
|
318
|
-
fontSize: '12',
|
|
319
|
-
},
|
|
320
|
-
},
|
|
321
|
-
});
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
### Manual Color Assignment
|
|
325
|
-
|
|
326
|
-
```javascript
|
|
327
|
-
chart
|
|
328
|
-
.addChild(new Line({ dataKey: 'revenue', stroke: '#00ff00' }))
|
|
329
|
-
.addChild(new Line({ dataKey: 'expenses', stroke: '#ff0000' }));
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
### Responsive Charts
|
|
333
|
-
|
|
334
|
-
Charts automatically resize with their container using ResizeObserver. The chart width adapts to the container up to the `theme.width` (which acts as max-width).
|
|
335
|
-
|
|
336
|
-
```javascript
|
|
337
|
-
// Container width: 500px → Chart width: 500px
|
|
338
|
-
// Container width: 1200px → Chart width: 928px (theme default max-width)
|
|
339
|
-
|
|
340
|
-
// Custom max-width
|
|
341
|
-
const chart = new XYChart({
|
|
342
|
-
data,
|
|
343
|
-
theme: { width: 1200 }, // Chart won't exceed 1200px
|
|
344
|
-
});
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
**No manual resize calls needed** - the chart automatically responds to container size changes!
|
|
348
|
-
|
|
349
|
-
## Data Validation
|
|
350
|
-
|
|
351
|
-
The library includes built-in validation with helpful error messages:
|
|
352
|
-
|
|
353
|
-
```javascript
|
|
354
|
-
// Empty data
|
|
355
|
-
new XYChart({ data: [] });
|
|
356
|
-
// Error: Data array cannot be empty
|
|
357
|
-
|
|
358
|
-
// Missing dataKey
|
|
359
|
-
new Line({ dataKey: 'nonexistent' });
|
|
360
|
-
// Error: Line: dataKey "nonexistent" not found in data items at indices: 0, 1, 2
|
|
361
|
-
|
|
362
|
-
// Invalid numeric data
|
|
363
|
-
new Line({ dataKey: 'textField' });
|
|
364
|
-
// Error: Line: No valid numeric values found for dataKey "textField"
|
|
365
|
-
```
|
|
48
|
+
- [Getting Started](./docs/getting-started.md) - Installation, Vanilla JS, React integration
|
|
49
|
+
- [XYChart](./docs/xy-chart.md) - Line and bar charts API
|
|
50
|
+
- [DonutChart](./docs/donut-chart.md) - Donut/pie charts API
|
|
51
|
+
- [Components](./docs/components.md) - Axes, Grid, Tooltip, Legend, Title
|
|
52
|
+
- [Theming](./docs/theming.md) - Colors, fonts, and styling
|
|
53
|
+
- [Advanced](./docs/advanced.md) - Scales, TypeScript, architecture, performance
|
|
366
54
|
|
|
367
55
|
## Browser Support
|
|
368
56
|
|
|
369
57
|
Modern browsers with ES6+ support. Uses D3.js v7.
|
|
370
58
|
|
|
371
|
-
##
|
|
372
|
-
|
|
373
|
-
Full TypeScript support included:
|
|
374
|
-
|
|
375
|
-
```typescript
|
|
376
|
-
import type { DataItem, ChartTheme, LineConfig } from './charts/types';
|
|
377
|
-
|
|
378
|
-
const data: DataItem[] = [
|
|
379
|
-
{ x: 1, y: 100 },
|
|
380
|
-
{ x: 2, y: 200 },
|
|
381
|
-
];
|
|
382
|
-
|
|
383
|
-
const config: LineConfig = {
|
|
384
|
-
dataKey: 'y',
|
|
385
|
-
stroke: '#8884d8',
|
|
386
|
-
strokeWidth: 2,
|
|
387
|
-
};
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
## Architecture
|
|
391
|
-
|
|
392
|
-
The library follows a composable, layout-driven design:
|
|
393
|
-
|
|
394
|
-
- **BaseChart**: Abstract base class providing common functionality (lifecycle, rendering, validation)
|
|
395
|
-
- **XYChart**: Concrete implementation for XY-coordinate charts (lines, bars, or mixed)
|
|
396
|
-
- **LayoutManager**: Calculates component positions and plot area dimensions (D3 margin convention)
|
|
397
|
-
- **LayoutAwareComponent**: Interface for self-measuring components (Title, Legend, Axes)
|
|
398
|
-
- **Components**: Modular components that implement `ChartComponent` or `LayoutAwareComponent`
|
|
399
|
-
- **Validation**: Centralized validation layer with `ChartValidator`
|
|
400
|
-
- **Scales**: Flexible scale factory supporting multiple D3 scale types
|
|
401
|
-
|
|
402
|
-
Key principles:
|
|
403
|
-
- **Layout-driven**: Components report their space requirements, plot area adjusts automatically
|
|
404
|
-
- **Separation of concerns**: Only the plot area (grid) scales; UI elements stay fixed size
|
|
405
|
-
- **D3 conventions**: Follows D3's margin convention pattern for clean, predictable layouts
|
|
406
|
-
|
|
407
|
-
This architecture makes it easy to add new chart types or series (Area, Scatter, etc.) by extending BaseChart or implementing new series components.
|
|
408
|
-
|
|
409
|
-
## Performance
|
|
59
|
+
## License
|
|
410
60
|
|
|
411
|
-
|
|
412
|
-
- **Smart Re-rendering**: Only re-renders when necessary (data updates or container resize)
|
|
413
|
-
- **Automatic Cleanup**: ResizeObserver and tooltips properly cleaned up on destroy
|
|
414
|
-
- **Minimal DOM Manipulation**: Uses D3's efficient data binding
|
|
415
|
-
- **SVG Optimization**: Clean SVG generation with proper cleanup
|
|
416
|
-
- **Small Bundle**: ~105 KB gzipped (including D3)
|
|
417
|
-
- **Small Bundle**: ~105 KB gzipped (including D3)
|
|
61
|
+
MIT
|
package/base-chart.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
2
|
import type { DataItem, ChartTheme, AxisScaleConfig, ExportFormat, ExportOptions, D3Scale } from './types.js';
|
|
3
|
-
import type { ChartComponent } from './chart-interface.js';
|
|
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';
|
|
6
6
|
import type { Grid } from './grid.js';
|
|
@@ -50,8 +50,9 @@ export declare abstract class BaseChart {
|
|
|
50
50
|
private performRender;
|
|
51
51
|
/**
|
|
52
52
|
* Get layout-aware components in order
|
|
53
|
+
* Override in subclasses to provide chart-specific components
|
|
53
54
|
*/
|
|
54
|
-
|
|
55
|
+
protected getLayoutComponents(): LayoutAwareComponent[];
|
|
55
56
|
/**
|
|
56
57
|
* Setup ResizeObserver for automatic resize handling
|
|
57
58
|
*/
|
package/base-chart.js
CHANGED
package/chart-interface.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface ChartComponent {
|
|
2
|
-
type: 'line' | 'bar' | 'xAxis' | 'yAxis' | 'grid' | 'tooltip' | 'legend' | 'title';
|
|
2
|
+
type: 'line' | 'bar' | 'xAxis' | 'yAxis' | 'grid' | 'tooltip' | 'legend' | 'title' | 'donutCenterContent';
|
|
3
3
|
}
|
|
4
4
|
export type ComponentSpace = {
|
|
5
5
|
width: number;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Selection } from 'd3';
|
|
2
|
+
import type { ChartTheme } from './types.js';
|
|
3
|
+
import type { ChartComponent } from './chart-interface.js';
|
|
4
|
+
type TextStyle = {
|
|
5
|
+
fontSize?: number;
|
|
6
|
+
fontWeight?: string;
|
|
7
|
+
fontFamily?: string;
|
|
8
|
+
color?: string;
|
|
9
|
+
};
|
|
10
|
+
export type DonutCenterContentConfig = {
|
|
11
|
+
mainValue?: string;
|
|
12
|
+
title?: string;
|
|
13
|
+
subtitle?: string;
|
|
14
|
+
mainValueStyle?: TextStyle;
|
|
15
|
+
titleStyle?: TextStyle;
|
|
16
|
+
subtitleStyle?: TextStyle;
|
|
17
|
+
};
|
|
18
|
+
export declare class DonutCenterContent implements ChartComponent {
|
|
19
|
+
readonly type: "donutCenterContent";
|
|
20
|
+
readonly mainValue?: string;
|
|
21
|
+
readonly title?: string;
|
|
22
|
+
readonly subtitle?: string;
|
|
23
|
+
private readonly config;
|
|
24
|
+
constructor(config?: DonutCenterContentConfig);
|
|
25
|
+
render(svg: Selection<SVGSVGElement, undefined, null, undefined>, cx: number, cy: number, theme: ChartTheme): void;
|
|
26
|
+
}
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export class DonutCenterContent {
|
|
2
|
+
constructor(config = {}) {
|
|
3
|
+
Object.defineProperty(this, "type", {
|
|
4
|
+
enumerable: true,
|
|
5
|
+
configurable: true,
|
|
6
|
+
writable: true,
|
|
7
|
+
value: 'donutCenterContent'
|
|
8
|
+
});
|
|
9
|
+
Object.defineProperty(this, "mainValue", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: true,
|
|
13
|
+
value: void 0
|
|
14
|
+
});
|
|
15
|
+
Object.defineProperty(this, "title", {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
value: void 0
|
|
20
|
+
});
|
|
21
|
+
Object.defineProperty(this, "subtitle", {
|
|
22
|
+
enumerable: true,
|
|
23
|
+
configurable: true,
|
|
24
|
+
writable: true,
|
|
25
|
+
value: void 0
|
|
26
|
+
});
|
|
27
|
+
Object.defineProperty(this, "config", {
|
|
28
|
+
enumerable: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
writable: true,
|
|
31
|
+
value: void 0
|
|
32
|
+
});
|
|
33
|
+
this.mainValue = config.mainValue;
|
|
34
|
+
this.title = config.title;
|
|
35
|
+
this.subtitle = config.subtitle;
|
|
36
|
+
this.config = config;
|
|
37
|
+
}
|
|
38
|
+
render(svg, cx, cy, theme) {
|
|
39
|
+
const defaults = theme.donut.centerContent;
|
|
40
|
+
const elements = [];
|
|
41
|
+
if (this.mainValue) {
|
|
42
|
+
const style = this.config.mainValueStyle;
|
|
43
|
+
elements.push({
|
|
44
|
+
text: this.mainValue,
|
|
45
|
+
fontSize: style?.fontSize ?? defaults.mainValue.fontSize,
|
|
46
|
+
fontWeight: style?.fontWeight ?? defaults.mainValue.fontWeight,
|
|
47
|
+
fontFamily: style?.fontFamily ??
|
|
48
|
+
defaults.mainValue.fontFamily ??
|
|
49
|
+
theme.fontFamily,
|
|
50
|
+
color: style?.color ?? defaults.mainValue.color,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (this.title) {
|
|
54
|
+
const style = this.config.titleStyle;
|
|
55
|
+
elements.push({
|
|
56
|
+
text: this.title,
|
|
57
|
+
fontSize: style?.fontSize ?? defaults.title.fontSize,
|
|
58
|
+
fontWeight: style?.fontWeight ?? defaults.title.fontWeight,
|
|
59
|
+
fontFamily: style?.fontFamily ??
|
|
60
|
+
defaults.title.fontFamily ??
|
|
61
|
+
theme.fontFamily,
|
|
62
|
+
color: style?.color ?? defaults.title.color,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (this.subtitle) {
|
|
66
|
+
const style = this.config.subtitleStyle;
|
|
67
|
+
elements.push({
|
|
68
|
+
text: this.subtitle,
|
|
69
|
+
fontSize: style?.fontSize ?? defaults.subtitle.fontSize,
|
|
70
|
+
fontWeight: style?.fontWeight ?? defaults.subtitle.fontWeight,
|
|
71
|
+
fontFamily: style?.fontFamily ??
|
|
72
|
+
defaults.subtitle.fontFamily ??
|
|
73
|
+
theme.fontFamily,
|
|
74
|
+
color: style?.color ?? defaults.subtitle.color,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (elements.length === 0)
|
|
78
|
+
return;
|
|
79
|
+
const lineSpacing = 6;
|
|
80
|
+
const totalHeight = elements.reduce((sum, el, i) => sum + el.fontSize + (i < elements.length - 1 ? lineSpacing : 0), 0);
|
|
81
|
+
const group = svg.append('g').attr('class', 'donut-center-content');
|
|
82
|
+
let currentY = cy - totalHeight / 2;
|
|
83
|
+
for (const el of elements) {
|
|
84
|
+
group
|
|
85
|
+
.append('text')
|
|
86
|
+
.attr('x', cx)
|
|
87
|
+
.attr('y', currentY + el.fontSize / 2)
|
|
88
|
+
.attr('text-anchor', 'middle')
|
|
89
|
+
.attr('dominant-baseline', 'middle')
|
|
90
|
+
.style('font-size', `${el.fontSize}px`)
|
|
91
|
+
.style('font-weight', el.fontWeight)
|
|
92
|
+
.style('font-family', el.fontFamily)
|
|
93
|
+
.style('fill', el.color)
|
|
94
|
+
.text(el.text);
|
|
95
|
+
currentY += el.fontSize + lineSpacing;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
package/donut-chart.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { DataItem } from './types.js';
|
|
2
|
+
import { BaseChart, type BaseChartConfig } from './base-chart.js';
|
|
3
|
+
import type { ChartComponent, LayoutAwareComponent } from './chart-interface.js';
|
|
4
|
+
export type DonutConfig = {
|
|
5
|
+
innerRadius?: number;
|
|
6
|
+
padAngle?: number;
|
|
7
|
+
cornerRadius?: number;
|
|
8
|
+
};
|
|
9
|
+
export type DonutChartConfig = BaseChartConfig & {
|
|
10
|
+
donut?: DonutConfig;
|
|
11
|
+
valueKey?: string;
|
|
12
|
+
labelKey?: string;
|
|
13
|
+
};
|
|
14
|
+
export declare class DonutChart extends BaseChart {
|
|
15
|
+
private readonly innerRadiusRatio;
|
|
16
|
+
private readonly padAngle;
|
|
17
|
+
private readonly cornerRadius;
|
|
18
|
+
private readonly valueKey;
|
|
19
|
+
private readonly labelKey;
|
|
20
|
+
private segments;
|
|
21
|
+
private centerContent;
|
|
22
|
+
constructor(config: DonutChartConfig);
|
|
23
|
+
private validateDonutData;
|
|
24
|
+
private prepareSegments;
|
|
25
|
+
addChild(component: ChartComponent): this;
|
|
26
|
+
update(data: DataItem[]): void;
|
|
27
|
+
protected getLayoutComponents(): LayoutAwareComponent[];
|
|
28
|
+
protected renderChart(): void;
|
|
29
|
+
private positionTooltip;
|
|
30
|
+
private buildTooltipContent;
|
|
31
|
+
private renderSegments;
|
|
32
|
+
}
|
package/donut-chart.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { arc, pie, select } from 'd3';
|
|
2
|
+
import { BaseChart } from './base-chart.js';
|
|
3
|
+
import { sanitizeForCSS } from './utils.js';
|
|
4
|
+
import { ChartValidator } from './validation.js';
|
|
5
|
+
const HOVER_EXPAND_PX = 8;
|
|
6
|
+
const ANIMATION_DURATION_MS = 150;
|
|
7
|
+
const TOOLTIP_OFFSET_PX = 12;
|
|
8
|
+
const EDGE_MARGIN_PX = 10;
|
|
9
|
+
export class DonutChart extends BaseChart {
|
|
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, "segments", {
|
|
43
|
+
enumerable: true,
|
|
44
|
+
configurable: true,
|
|
45
|
+
writable: true,
|
|
46
|
+
value: []
|
|
47
|
+
});
|
|
48
|
+
Object.defineProperty(this, "centerContent", {
|
|
49
|
+
enumerable: true,
|
|
50
|
+
configurable: true,
|
|
51
|
+
writable: true,
|
|
52
|
+
value: null
|
|
53
|
+
});
|
|
54
|
+
const donut = config.donut ?? {};
|
|
55
|
+
this.innerRadiusRatio =
|
|
56
|
+
donut.innerRadius ?? this.theme.donut.innerRadius;
|
|
57
|
+
this.padAngle = donut.padAngle ?? this.theme.donut.padAngle;
|
|
58
|
+
this.cornerRadius = donut.cornerRadius ?? this.theme.donut.cornerRadius;
|
|
59
|
+
this.valueKey = config.valueKey ?? 'value';
|
|
60
|
+
this.labelKey = config.labelKey ?? 'name';
|
|
61
|
+
this.validateDonutData();
|
|
62
|
+
this.prepareSegments();
|
|
63
|
+
}
|
|
64
|
+
validateDonutData() {
|
|
65
|
+
ChartValidator.validateDataKey(this.data, this.labelKey, 'DonutChart');
|
|
66
|
+
ChartValidator.validateDataKey(this.data, this.valueKey, 'DonutChart');
|
|
67
|
+
ChartValidator.validateNumericData(this.data, this.valueKey, 'DonutChart');
|
|
68
|
+
for (const [index, item] of this.data.entries()) {
|
|
69
|
+
const value = this.parseValue(item[this.valueKey]);
|
|
70
|
+
if (value < 0) {
|
|
71
|
+
throw new Error(`DonutChart: data item at index ${index} has negative value '${item[this.valueKey]}' for key '${this.valueKey}'`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
prepareSegments() {
|
|
76
|
+
this.segments = this.data.map((item, index) => ({
|
|
77
|
+
label: String(item[this.labelKey]),
|
|
78
|
+
value: this.parseValue(item[this.valueKey]),
|
|
79
|
+
color: item.color ||
|
|
80
|
+
this.theme.colorPalette[index % this.theme.colorPalette.length],
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
addChild(component) {
|
|
84
|
+
const type = component.type;
|
|
85
|
+
if (type === 'tooltip') {
|
|
86
|
+
this.tooltip = component;
|
|
87
|
+
}
|
|
88
|
+
else if (type === 'legend') {
|
|
89
|
+
this.legend = component;
|
|
90
|
+
this.legend.setToggleCallback(() => this.update(this.data));
|
|
91
|
+
}
|
|
92
|
+
else if (type === 'title') {
|
|
93
|
+
this.title = component;
|
|
94
|
+
}
|
|
95
|
+
else if (type === 'donutCenterContent') {
|
|
96
|
+
this.centerContent = component;
|
|
97
|
+
}
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
update(data) {
|
|
101
|
+
this.data = data;
|
|
102
|
+
this.validateDonutData();
|
|
103
|
+
this.prepareSegments();
|
|
104
|
+
super.update(data);
|
|
105
|
+
}
|
|
106
|
+
getLayoutComponents() {
|
|
107
|
+
const components = [];
|
|
108
|
+
if (this.title)
|
|
109
|
+
components.push(this.title);
|
|
110
|
+
if (this.legend)
|
|
111
|
+
components.push(this.legend);
|
|
112
|
+
return components;
|
|
113
|
+
}
|
|
114
|
+
renderChart() {
|
|
115
|
+
if (!this.plotArea || !this.svg || !this.plotGroup) {
|
|
116
|
+
throw new Error('Plot area not calculated');
|
|
117
|
+
}
|
|
118
|
+
if (this.title) {
|
|
119
|
+
const pos = this.layoutManager.getComponentPosition(this.title);
|
|
120
|
+
this.title.render(this.svg, this.theme, this.width, pos.x, pos.y);
|
|
121
|
+
}
|
|
122
|
+
const visibleSegments = this.legend
|
|
123
|
+
? this.segments.filter((seg) => this.legend.isSeriesVisible(seg.label))
|
|
124
|
+
: this.segments;
|
|
125
|
+
const cx = this.plotArea.left + this.plotArea.width / 2;
|
|
126
|
+
const cy = this.plotArea.top + this.plotArea.height / 2;
|
|
127
|
+
const outerRadius = Math.min(this.plotArea.width, this.plotArea.height) / 2;
|
|
128
|
+
const innerRadius = outerRadius * this.innerRadiusRatio;
|
|
129
|
+
this.renderSegments(visibleSegments, cx, cy, innerRadius, outerRadius);
|
|
130
|
+
if (this.centerContent) {
|
|
131
|
+
this.centerContent.render(this.svg, cx, cy, this.theme);
|
|
132
|
+
}
|
|
133
|
+
if (this.legend) {
|
|
134
|
+
const pos = this.layoutManager.getComponentPosition(this.legend);
|
|
135
|
+
const legendSeries = this.segments.map((seg) => ({
|
|
136
|
+
dataKey: seg.label,
|
|
137
|
+
fill: seg.color,
|
|
138
|
+
}));
|
|
139
|
+
this.legend.render(this.svg, legendSeries, this.theme, this.width, pos.x, pos.y);
|
|
140
|
+
}
|
|
141
|
+
if (this.tooltip) {
|
|
142
|
+
this.tooltip.initialize(this.theme);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
positionTooltip(event, tooltipDiv) {
|
|
146
|
+
const node = tooltipDiv.node();
|
|
147
|
+
if (!node)
|
|
148
|
+
return;
|
|
149
|
+
const rect = node.getBoundingClientRect();
|
|
150
|
+
let x = event.pageX + TOOLTIP_OFFSET_PX;
|
|
151
|
+
let y = event.pageY - rect.height / 2;
|
|
152
|
+
if (x + rect.width > window.innerWidth - EDGE_MARGIN_PX) {
|
|
153
|
+
x = event.pageX - rect.width - TOOLTIP_OFFSET_PX;
|
|
154
|
+
}
|
|
155
|
+
x = Math.max(EDGE_MARGIN_PX, x);
|
|
156
|
+
y = Math.max(EDGE_MARGIN_PX, Math.min(y, window.innerHeight +
|
|
157
|
+
window.scrollY -
|
|
158
|
+
rect.height -
|
|
159
|
+
EDGE_MARGIN_PX));
|
|
160
|
+
tooltipDiv.style('left', `${x}px`).style('top', `${y}px`);
|
|
161
|
+
}
|
|
162
|
+
buildTooltipContent(d, segments) {
|
|
163
|
+
const total = segments.reduce((sum, s) => sum + s.value, 0);
|
|
164
|
+
const percentage = total > 0 ? ((d.data.value / total) * 100).toFixed(1) : '0.0';
|
|
165
|
+
const dataItem = this.data.find((item) => String(item[this.labelKey]) === d.data.label);
|
|
166
|
+
if (this.tooltip?.customFormatter) {
|
|
167
|
+
return this.tooltip.customFormatter(dataItem || {}, [
|
|
168
|
+
{ dataKey: d.data.label, fill: d.data.color },
|
|
169
|
+
]);
|
|
170
|
+
}
|
|
171
|
+
if (this.tooltip?.formatter) {
|
|
172
|
+
return `<strong>${d.data.label}</strong><br/>${this.tooltip.formatter(d.data.label, d.data.value, dataItem || {})}`;
|
|
173
|
+
}
|
|
174
|
+
return `<strong>${d.data.label}</strong><br/>${d.data.value} (${percentage}%)`;
|
|
175
|
+
}
|
|
176
|
+
renderSegments(segments, cx, cy, innerRadius, outerRadius) {
|
|
177
|
+
if (!this.plotGroup || !this.svg)
|
|
178
|
+
return;
|
|
179
|
+
const pieGenerator = pie()
|
|
180
|
+
.value((d) => d.value)
|
|
181
|
+
.padAngle(this.padAngle)
|
|
182
|
+
.sort(null);
|
|
183
|
+
const arcGenerator = arc()
|
|
184
|
+
.innerRadius(innerRadius)
|
|
185
|
+
.outerRadius(outerRadius)
|
|
186
|
+
.cornerRadius(this.cornerRadius);
|
|
187
|
+
const hoverArcGenerator = arc()
|
|
188
|
+
.innerRadius(innerRadius)
|
|
189
|
+
.outerRadius(outerRadius + HOVER_EXPAND_PX)
|
|
190
|
+
.cornerRadius(this.cornerRadius);
|
|
191
|
+
const pieData = pieGenerator(segments);
|
|
192
|
+
const segmentGroup = this.plotGroup
|
|
193
|
+
.append('g')
|
|
194
|
+
.attr('class', 'donut-segments')
|
|
195
|
+
.attr('transform', `translate(${cx}, ${cy})`);
|
|
196
|
+
const tooltipDiv = this.tooltip
|
|
197
|
+
? select(`#${this.tooltip.id}`)
|
|
198
|
+
: null;
|
|
199
|
+
segmentGroup
|
|
200
|
+
.selectAll('.donut-segment')
|
|
201
|
+
.data(pieData)
|
|
202
|
+
.join('path')
|
|
203
|
+
.attr('class', (d) => `donut-segment segment-${sanitizeForCSS(d.data.label)}`)
|
|
204
|
+
.attr('d', arcGenerator)
|
|
205
|
+
.attr('fill', (d) => d.data.color)
|
|
206
|
+
.style('cursor', 'pointer')
|
|
207
|
+
.style('transition', 'opacity 0.15s ease')
|
|
208
|
+
.on('mouseenter', (event, d) => {
|
|
209
|
+
select(event.currentTarget)
|
|
210
|
+
.transition()
|
|
211
|
+
.duration(ANIMATION_DURATION_MS)
|
|
212
|
+
.attr('d', hoverArcGenerator(d));
|
|
213
|
+
segmentGroup
|
|
214
|
+
.selectAll('.donut-segment')
|
|
215
|
+
.filter((_, i, nodes) => nodes[i] !== event.currentTarget)
|
|
216
|
+
.style('opacity', 0.5);
|
|
217
|
+
if (tooltipDiv && !tooltipDiv.empty()) {
|
|
218
|
+
tooltipDiv
|
|
219
|
+
.style('visibility', 'visible')
|
|
220
|
+
.html(this.buildTooltipContent(d, segments));
|
|
221
|
+
this.positionTooltip(event, tooltipDiv);
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
.on('mousemove', (event) => {
|
|
225
|
+
if (tooltipDiv && !tooltipDiv.empty()) {
|
|
226
|
+
this.positionTooltip(event, tooltipDiv);
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
.on('mouseleave', (event, d) => {
|
|
230
|
+
select(event.currentTarget)
|
|
231
|
+
.transition()
|
|
232
|
+
.duration(ANIMATION_DURATION_MS)
|
|
233
|
+
.attr('d', arcGenerator(d));
|
|
234
|
+
segmentGroup.selectAll('.donut-segment').style('opacity', 1);
|
|
235
|
+
if (tooltipDiv && !tooltipDiv.empty()) {
|
|
236
|
+
tooltipDiv.style('visibility', 'hidden');
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
package/legend.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
|
-
import type { LegendConfig, ChartTheme } from './types.js';
|
|
3
|
-
import type { Line } from './line.js';
|
|
4
|
-
import type { Bar } from './bar.js';
|
|
2
|
+
import type { LegendConfig, ChartTheme, LegendSeries } from './types.js';
|
|
5
3
|
import type { LayoutAwareComponent, ComponentSpace } from './chart-interface.js';
|
|
6
4
|
export declare class Legend implements LayoutAwareComponent {
|
|
7
5
|
readonly type: "legend";
|
|
@@ -20,5 +18,5 @@ export declare class Legend implements LayoutAwareComponent {
|
|
|
20
18
|
* Returns the space required by the legend
|
|
21
19
|
*/
|
|
22
20
|
getRequiredSpace(): ComponentSpace;
|
|
23
|
-
render(svg: Selection<SVGSVGElement, undefined, null, undefined>, series:
|
|
21
|
+
render(svg: Selection<SVGSVGElement, undefined, null, undefined>, series: LegendSeries[], theme: ChartTheme, width: number, _x?: number, y?: number): void;
|
|
24
22
|
}
|
package/package.json
CHANGED
package/theme.js
CHANGED
|
@@ -50,6 +50,28 @@ export const defaultTheme = {
|
|
|
50
50
|
borderRadius: 4,
|
|
51
51
|
padding: 4,
|
|
52
52
|
},
|
|
53
|
+
donut: {
|
|
54
|
+
innerRadius: 0.5,
|
|
55
|
+
padAngle: 0.02,
|
|
56
|
+
cornerRadius: 0,
|
|
57
|
+
centerContent: {
|
|
58
|
+
mainValue: {
|
|
59
|
+
fontSize: 32,
|
|
60
|
+
fontWeight: 'bold',
|
|
61
|
+
color: '#1f2a36',
|
|
62
|
+
},
|
|
63
|
+
title: {
|
|
64
|
+
fontSize: 14,
|
|
65
|
+
fontWeight: 'normal',
|
|
66
|
+
color: '#6b7280',
|
|
67
|
+
},
|
|
68
|
+
subtitle: {
|
|
69
|
+
fontSize: 12,
|
|
70
|
+
fontWeight: 'normal',
|
|
71
|
+
color: '#9ca3af',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
53
75
|
};
|
|
54
76
|
export const newspaperTheme = {
|
|
55
77
|
width: 928,
|
|
@@ -104,6 +126,28 @@ export const newspaperTheme = {
|
|
|
104
126
|
borderRadius: 2,
|
|
105
127
|
padding: 3,
|
|
106
128
|
},
|
|
129
|
+
donut: {
|
|
130
|
+
innerRadius: 0.5,
|
|
131
|
+
padAngle: 0.015,
|
|
132
|
+
cornerRadius: 0,
|
|
133
|
+
centerContent: {
|
|
134
|
+
mainValue: {
|
|
135
|
+
fontSize: 28,
|
|
136
|
+
fontWeight: 'bold',
|
|
137
|
+
color: '#1a1a1a',
|
|
138
|
+
},
|
|
139
|
+
title: {
|
|
140
|
+
fontSize: 13,
|
|
141
|
+
fontWeight: 'normal',
|
|
142
|
+
color: '#4a4a4a',
|
|
143
|
+
},
|
|
144
|
+
subtitle: {
|
|
145
|
+
fontSize: 11,
|
|
146
|
+
fontWeight: 'normal',
|
|
147
|
+
color: '#6b6b6b',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
107
151
|
};
|
|
108
152
|
export const themes = {
|
|
109
153
|
default: defaultTheme,
|
package/types.d.ts
CHANGED
|
@@ -50,6 +50,31 @@ export type ChartTheme = {
|
|
|
50
50
|
borderRadius: number;
|
|
51
51
|
padding: number;
|
|
52
52
|
};
|
|
53
|
+
donut: {
|
|
54
|
+
innerRadius: number;
|
|
55
|
+
padAngle: number;
|
|
56
|
+
cornerRadius: number;
|
|
57
|
+
centerContent: {
|
|
58
|
+
mainValue: {
|
|
59
|
+
fontSize: number;
|
|
60
|
+
fontWeight: string;
|
|
61
|
+
fontFamily?: string;
|
|
62
|
+
color: string;
|
|
63
|
+
};
|
|
64
|
+
title: {
|
|
65
|
+
fontSize: number;
|
|
66
|
+
fontWeight: string;
|
|
67
|
+
fontFamily?: string;
|
|
68
|
+
color: string;
|
|
69
|
+
};
|
|
70
|
+
subtitle: {
|
|
71
|
+
fontSize: number;
|
|
72
|
+
fontWeight: string;
|
|
73
|
+
fontFamily?: string;
|
|
74
|
+
color: string;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
};
|
|
53
78
|
};
|
|
54
79
|
export type ValueLabelConfig = {
|
|
55
80
|
fontSize?: number;
|
|
@@ -126,6 +151,11 @@ export type LegendConfig = {
|
|
|
126
151
|
marginTop?: number;
|
|
127
152
|
marginBottom?: number;
|
|
128
153
|
};
|
|
154
|
+
export type LegendSeries = {
|
|
155
|
+
dataKey: string;
|
|
156
|
+
stroke?: string;
|
|
157
|
+
fill?: string;
|
|
158
|
+
};
|
|
129
159
|
export type TitleConfig = {
|
|
130
160
|
text: string;
|
|
131
161
|
fontSize?: number;
|