@internetstiftelsen/charts 0.9.2 → 0.10.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.
@@ -0,0 +1,213 @@
1
+ # ChartGroup API
2
+
3
+ Compose multiple existing charts into a shared layout with one coordinated
4
+ legend.
5
+
6
+ ## Constructor
7
+
8
+ ```typescript
9
+ new ChartGroup(config: ChartGroupConfig)
10
+ ```
11
+
12
+ ### Config Options
13
+
14
+ | Option | Type | Default | Description |
15
+ | ------------- | --------------------------------- | ---------------------- | --------------------------------------------------------------------------- |
16
+ | `cols` | `number` | required | Base number of layout columns |
17
+ | `gap` | `number` | `20` | Base horizontal and vertical gap between chart cells in px |
18
+ | `syncY` | `boolean` | `false` | Sync the Y domain across visible vertical `XYChart` children |
19
+ | `height` | `number` | container height | Fixed total group height. Omit it to size from the container |
20
+ | `chartHeight` | `number` | `DEFAULT_CHART_HEIGHT` | Fallback child height when neither the child nor container provides one |
21
+ | `theme` | `DeepPartial<ChartTheme>` | - | Theme override for the shared group legend and title |
22
+ | `responsive` | `ChartGroupResponsiveConfig` | - | Declarative responsive overrides for group-level `cols` and `gap` |
23
+
24
+ `ChartGroup` manages child widths. If a child chart has an explicit `width`,
25
+ that width is ignored inside the group and a warning is emitted.
26
+
27
+ ## Example
28
+
29
+ ```typescript
30
+ import { ChartGroup } from '@internetstiftelsen/charts/chart-group';
31
+ import { XYChart } from '@internetstiftelsen/charts/xy-chart';
32
+ import { Line } from '@internetstiftelsen/charts/line';
33
+ import { Bar } from '@internetstiftelsen/charts/bar';
34
+ import { Legend } from '@internetstiftelsen/charts/legend';
35
+ import { Title } from '@internetstiftelsen/charts/title';
36
+
37
+ const lineChart = new XYChart({ data: lineData });
38
+ lineChart
39
+ .addChild(new Line({ dataKey: 'revenue' }))
40
+ .addChild(new Line({ dataKey: 'expenses' }));
41
+
42
+ const barChart = new XYChart({ data: barData });
43
+ barChart
44
+ .addChild(new Bar({ dataKey: 'revenue' }))
45
+ .addChild(new Bar({ dataKey: 'expenses' }));
46
+
47
+ const group = new ChartGroup({
48
+ cols: 3,
49
+ gap: 20,
50
+ height: 420,
51
+ syncY: true,
52
+ });
53
+
54
+ group
55
+ .addChild(new Title({ text: 'Revenue vs Expenses' }))
56
+ .addChart(barChart, { span: 1 })
57
+ .addChart(lineChart, { span: 2 })
58
+ .addChild(new Legend());
59
+
60
+ group.render('#chart-group');
61
+ ```
62
+
63
+ The group legend merges child legend items by key. If multiple child charts use
64
+ the same legend key, toggling that key affects every matching child chart.
65
+
66
+ ## Layout
67
+
68
+ Use `addChart(chart, options)` to place a child chart in the group:
69
+
70
+ ```typescript
71
+ group.addChart(chart, {
72
+ span?: number,
73
+ height?: number,
74
+ hidden?: boolean,
75
+ order?: number,
76
+ responsive?: {
77
+ breakpoints?: {
78
+ [name]: {
79
+ minWidth?: number,
80
+ maxWidth?: number,
81
+ span?: number,
82
+ height?: number,
83
+ hidden?: boolean,
84
+ order?: number,
85
+ }
86
+ }
87
+ },
88
+ });
89
+ ```
90
+
91
+ - `span` defaults to `1` and is clamped to the active column count
92
+ - `height` overrides the row item height for that child only
93
+ - `hidden` removes the child from layout, legend merging, `syncY`, and export
94
+ - `order` reorders the visible children before layout
95
+ - Child height resolution is:
96
+ `item.height ?? childChart.height ?? rowFallbackHeight`
97
+ - `rowFallbackHeight` comes from the available group height when the group has a
98
+ fixed height or container height, otherwise it falls back to
99
+ `group.chartHeight`
100
+
101
+ Child charts keep their own axes, tooltips, responsive overrides, and render
102
+ logic. Their own legend rendering is suppressed while they are mounted inside
103
+ the group so only the shared group legend is shown.
104
+
105
+ When `syncY` is enabled, `ChartGroup` computes one shared vertical numeric
106
+ domain for its `XYChart` children and applies it before render and export. This
107
+ is useful for layouts like `Bar + Line` where one chart keeps the visible Y
108
+ axis while the other only shows aligned horizontal grid lines.
109
+
110
+ ## Responsive Layout
111
+
112
+ `ChartGroup` uses the same breakpoint semantics as chart-level `responsive`
113
+ config:
114
+
115
+ - Breakpoints support both `minWidth` and `maxWidth`
116
+ - A numeric breakpoint value is shorthand for `minWidth`
117
+ - All matching breakpoints merge in declaration order
118
+
119
+ ```typescript
120
+ const group = new ChartGroup({
121
+ cols: 3,
122
+ gap: 20,
123
+ responsive: {
124
+ breakpoints: {
125
+ tablet: {
126
+ maxWidth: 1023,
127
+ cols: 2,
128
+ },
129
+ mobile: {
130
+ maxWidth: 640,
131
+ cols: 1,
132
+ gap: 16,
133
+ },
134
+ },
135
+ },
136
+ });
137
+
138
+ group
139
+ .addChart(barChart, {
140
+ span: 1,
141
+ responsive: {
142
+ breakpoints: {
143
+ mobile: {
144
+ maxWidth: 640,
145
+ hidden: true,
146
+ },
147
+ },
148
+ },
149
+ })
150
+ .addChart(lineChart, {
151
+ span: 2,
152
+ responsive: {
153
+ breakpoints: {
154
+ mobile: {
155
+ maxWidth: 640,
156
+ span: 1,
157
+ order: -1,
158
+ },
159
+ },
160
+ },
161
+ });
162
+ ```
163
+
164
+ After a breakpoint's `maxWidth`, that breakpoint stops matching. Use the base
165
+ group config plus `minWidth` breakpoints for mobile-first layouts, or the base
166
+ group config plus `maxWidth` breakpoints for desktop-down layouts.
167
+
168
+ ## Title
169
+
170
+ `ChartGroup` accepts `Title` via `addChild()` and renders it above the grouped
171
+ chart layout:
172
+
173
+ ```typescript
174
+ group.addChild(new Title({ text: 'Revenue vs Expenses' }));
175
+ ```
176
+
177
+ ## Legend
178
+
179
+ `ChartGroup` accepts `Legend` via `addChild()`. Supported group legend modes:
180
+
181
+ - `inline` (default)
182
+ - `hidden`
183
+
184
+ `disconnected` is not supported for `ChartGroup` in v1.
185
+
186
+ Programmatic legend APIs are available on the group and mirror `BaseChart`:
187
+
188
+ ```typescript
189
+ group.getLegendItems();
190
+ group.toggleLegendSeries('revenue');
191
+ group.setLegendSeriesVisible('expenses', false);
192
+ group.setLegendVisibility({ revenue: true, expenses: false });
193
+ group.onLegendChange(() => {
194
+ // React to shared legend updates
195
+ });
196
+ ```
197
+
198
+ ## Export
199
+
200
+ `ChartGroup` supports combined visual exports:
201
+
202
+ ```typescript
203
+ await group.export('svg');
204
+ await group.export('png');
205
+ await group.export('jpg');
206
+ await group.export('pdf');
207
+ ```
208
+
209
+ - Export width can be overridden with `options.width`
210
+ - Export height is layout-derived in v1 and cannot be overridden
211
+ - Group titles are included in the combined export
212
+ - Child chart legends are suppressed in the combined export
213
+ - Non-visual exports (`json`, `csv`, `xlsx`) are not supported in v1
@@ -0,0 +1,308 @@
1
+ # Components
2
+
3
+ Charts are built by composing components. All components are added via the `addChild()` method.
4
+
5
+ ## XAxis
6
+
7
+ Renders the X axis.
8
+
9
+ ```typescript
10
+ new XAxis({
11
+ display?: boolean, // Render axis and reserve layout space (default: true)
12
+ dataKey?: string, // Key in data objects for X values (auto-detected if omitted)
13
+ labelKey?: string, // Optional display label key (uses dataKey values if omitted)
14
+ groupLabelKey?: string, // Optional key used for second-row grouped labels
15
+ showGroupLabels?: boolean, // Show second-row grouped labels (default: false)
16
+ groupLabelGap?: number, // Vertical gap between tick row and grouped row
17
+ tickFormat?: string | ((value: string | number | Date) => string) | null
18
+ })
19
+ ```
20
+
21
+ Grouped label styles come from `theme.axis.groupLabel` and are bold by default.
22
+
23
+ ### Example
24
+
25
+ ```javascript
26
+ chart.addChild(new XAxis({ dataKey: 'date' }));
27
+ ```
28
+
29
+ Callback formatter example:
30
+
31
+ ```typescript
32
+ new XAxis({
33
+ dataKey: 'month',
34
+ tickFormat: (value) => `Month ${value}`,
35
+ });
36
+ ```
37
+
38
+ Grouped axis example:
39
+
40
+ ```typescript
41
+ new XAxis({
42
+ // dataKey can be omitted if XAxis auto-detection is enough
43
+ dataKey: '__iis_grouped_category_id__',
44
+ showGroupLabels: true,
45
+ });
46
+ ```
47
+
48
+ ---
49
+
50
+ ## YAxis
51
+
52
+ Renders the Y axis.
53
+
54
+ ```typescript
55
+ new YAxis({
56
+ display?: boolean, // Render axis and reserve layout space (default: true)
57
+ tickFormat?: string | ((value: string | number | Date) => string) | null
58
+ })
59
+ ```
60
+
61
+ ### Format Examples
62
+
63
+ ```javascript
64
+ new YAxis(); // Raw numbers: 35000
65
+ new YAxis({ tickFormat: 's' }); // SI-prefix: 35k
66
+ new YAxis({ tickFormat: '$,' }); // Currency: $35,000
67
+ new YAxis({ tickFormat: '.1%' }); // Percentage: 35.0%
68
+ new YAxis({ tickFormat: '.2f' }); // Fixed decimal: 35000.00
69
+ new YAxis({ tickFormat: (value) => `Value ${value}` });
70
+ ```
71
+
72
+ See [D3 format specifiers](https://github.com/d3/d3-format) for all options.
73
+
74
+ ---
75
+
76
+ ## Grid
77
+
78
+ Renders grid lines in the plot area.
79
+
80
+ ```typescript
81
+ new Grid({
82
+ value?: boolean, // Show grid lines for the value axis (default: true)
83
+ category?: boolean, // Show grid lines for the category axis (default: true)
84
+ })
85
+ ```
86
+
87
+ `value` and `category` stay semantically stable when `XYChart.orientation`
88
+ changes. In vertical charts, `value: true` renders horizontal grid lines. In
89
+ horizontal charts, `value: true` renders vertical grid lines.
90
+
91
+ ### Example
92
+
93
+ ```javascript
94
+ // Value-axis grid only
95
+ chart.addChild(new Grid({ category: false, value: true }));
96
+
97
+ // Both axes
98
+ chart.addChild(new Grid());
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Tooltip
104
+
105
+ Renders interactive tooltips on hover and keyboard focus.
106
+
107
+ ```typescript
108
+ new Tooltip({
109
+ formatter?: (dataKey: string, value: DataValue, data: DataItem) => string
110
+ })
111
+ ```
112
+
113
+ The formatter receives:
114
+
115
+ - `dataKey` - The series key (e.g., 'revenue')
116
+ - `value` - The data value at this point
117
+ - `data` - The full data item object
118
+
119
+ ### Example
120
+
121
+ ```javascript
122
+ new Tooltip({
123
+ formatter: (dataKey, value, data) =>
124
+ `<strong>${dataKey}</strong><br/>
125
+ Value: ${value.toLocaleString()}<br/>
126
+ Date: ${data.date}`,
127
+ });
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Legend
133
+
134
+ Renders an interactive legend. Click items to toggle series visibility.
135
+
136
+ ```typescript
137
+ new Legend({
138
+ mode?: 'inline' | 'disconnected' | 'hidden', // Rendering mode (default: 'inline')
139
+ position?: 'bottom', // Position (currently only 'bottom')
140
+ disconnectedTarget?: string | HTMLElement, // External mount target when mode is 'disconnected'
141
+ marginTop?: number, // Space above legend (default: 20)
142
+ marginBottom?: number, // Space below legend (default: 10)
143
+ paddingX?: number, // Horizontal inset for legend layout (default: theme.legend.paddingX)
144
+ itemSpacingX?: number, // Horizontal space between legend items (default: theme.legend.itemSpacingX)
145
+ itemSpacingY?: number, // Vertical space between wrapped legend rows (default: theme.legend.itemSpacingY)
146
+ })
147
+ ```
148
+
149
+ Legend items automatically wrap to new rows when they do not fit available
150
+ width. Each wrapped row is centered independently.
151
+
152
+ - `inline`: current default behavior (legend rendered inside the chart SVG)
153
+ - `disconnected`: legend rendered in a separate SVG outside chart layout
154
+ - `hidden`: no legend rendering; use chart legend APIs for custom HTML legends
155
+
156
+ ### Example
157
+
158
+ ```javascript
159
+ chart.addChild(new Legend({ position: 'bottom' }));
160
+ ```
161
+
162
+ ### Custom HTML Legend
163
+
164
+ ```javascript
165
+ chart.addChild(new Legend({ mode: 'hidden' }));
166
+
167
+ const items = chart.getLegendItems();
168
+ // Render items in HTML and call:
169
+ chart.toggleLegendSeries(items[0].dataKey);
170
+ ```
171
+
172
+ Legend control APIs also work without mounting a `Legend` component at all. This
173
+ is what allows [`ChartGroup`](./chart-group.md) to coordinate multiple child
174
+ charts from one shared legend.
175
+
176
+ ---
177
+
178
+ ## Title
179
+
180
+ Renders a title for the chart.
181
+
182
+ ```typescript
183
+ new Title({
184
+ display?: boolean, // Render title and reserve layout space (default: true)
185
+ text: string, // Title text (required)
186
+ fontSize?: number, // Font size in pixels (default: 18)
187
+ fontWeight?: string, // Font weight (default: 'bold')
188
+ align?: 'left' | 'center' | 'right', // Alignment (default: 'center')
189
+ marginTop?: number, // Space above title (default: 10)
190
+ marginBottom?: number, // Space below title (default: 15)
191
+ })
192
+ ```
193
+
194
+ ### Example
195
+
196
+ ```javascript
197
+ chart.addChild(
198
+ new Title({
199
+ text: 'Monthly Revenue',
200
+ align: 'left',
201
+ fontSize: 20,
202
+ }),
203
+ );
204
+ ```
205
+
206
+ ---
207
+
208
+ ## Export Hooks
209
+
210
+ Components can provide `exportHooks` to adjust export-only settings or mutate
211
+ the exported SVG right before serialization. Hooks run only for visual exports
212
+ (`svg`, `png`, `jpg`, `pdf`) and never touch the live chart instance. Use
213
+ `beforeRender` to return config overrides and `before` to mutate the export
214
+ SVG.
215
+
216
+ ```typescript
217
+ const title = new Title({
218
+ text: 'Original',
219
+ exportHooks: {
220
+ before({ svg }) {
221
+ const text = svg.querySelector('.title text');
222
+ if (text) {
223
+ text.textContent = 'Exported';
224
+ }
225
+ },
226
+ },
227
+ });
228
+
229
+ chart.addChild(title);
230
+ ```
231
+
232
+ You can control the export size via options:
233
+
234
+ ```typescript
235
+ await chart.export('svg', { width: 1200, height: 800 });
236
+ ```
237
+
238
+ You can also return a partial config to re-render the export:
239
+
240
+ ```typescript
241
+ new Line({
242
+ dataKey: 'revenue',
243
+ exportHooks: {
244
+ beforeRender(_context, currentConfig) {
245
+ if (!currentConfig.valueLabel?.show) {
246
+ return {
247
+ valueLabel: {
248
+ show: true,
249
+ },
250
+ };
251
+ }
252
+ },
253
+ },
254
+ });
255
+ ```
256
+
257
+ For container-query responsive behavior, use chart-level `responsive`
258
+ breakpoints in chart constructor config (see
259
+ [XYChart API](./xy-chart.md#responsive-overrides)).
260
+
261
+ ## Export Formats
262
+
263
+ `chart.export()` is async and supports:
264
+
265
+ - `svg`
266
+ - `json`
267
+ - `csv`
268
+ - `xlsx` (lazy-loads optional `xlsx`)
269
+ - `png`
270
+ - `jpg`
271
+ - `pdf` (lazy-loads optional `jspdf`)
272
+
273
+ ```typescript
274
+ await chart.export('png', { download: true });
275
+ await chart.export('csv', { download: true, delimiter: ';' });
276
+ await chart.export('xlsx', { download: true, sheetName: 'Data' });
277
+ await chart.export('pdf', { download: true, pdfMargin: 16 });
278
+ ```
279
+
280
+ Export option highlights:
281
+
282
+ - `columns`: choose specific columns for `csv` and `xlsx`
283
+ - `delimiter`: CSV delimiter (default `,`)
284
+ - `pixelRatio`: render scale for `png`, `jpg`, `pdf`
285
+ - `backgroundColor`: defaults transparent for `png`, white for `jpg`/`pdf`
286
+ - `jpegQuality`: JPEG quality for `jpg` (default `0.92`)
287
+ - `sheetName`: sheet name for `xlsx` (default `Sheet1`)
288
+ - `pdfMargin`: page margin for `pdf` (default `0`)
289
+
290
+ `json` export returns only the chart data payload
291
+
292
+ ---
293
+
294
+ ## Component Order
295
+
296
+ Components are rendered in the order they're added. A typical order:
297
+
298
+ ```javascript
299
+ chart
300
+ .addChild(new Title({ text: 'Chart Title' }))
301
+ .addChild(new Grid({ value: true }))
302
+ .addChild(new XAxis({ dataKey: 'date' }))
303
+ .addChild(new YAxis())
304
+ .addChild(new Tooltip())
305
+ .addChild(new Legend({ position: 'bottom' }))
306
+ .addChild(new Line({ dataKey: 'value1' }))
307
+ .addChild(new Line({ dataKey: 'value2' }));
308
+ ```
@@ -0,0 +1,193 @@
1
+ # DonutChart API
2
+
3
+ A chart for displaying proportional data as segments of a donut/pie.
4
+
5
+ ## Constructor
6
+
7
+ ```typescript
8
+ new DonutChart(config: DonutChartConfig)
9
+ ```
10
+
11
+ ### Config Options
12
+
13
+ | Option | Type | Default | Description |
14
+ | ------------ | ------------------------- | --------- | --------------------------------------------------------------------- |
15
+ | `data` | `DataItem[]` | required | Array of data objects |
16
+ | `width` | `number` | - | Explicit chart width in pixels |
17
+ | `height` | `number` | - | Explicit chart height in pixels |
18
+ | `valueKey` | `string` | `'value'` | Key for numeric values in data |
19
+ | `labelKey` | `string` | `'name'` | Key for segment labels in data |
20
+ | `donut` | `DonutConfig` | - | Donut-specific configuration |
21
+ | `valueLabel` | `DonutValueLabelConfig` | - | On-chart outside label/value rendering configuration |
22
+ | `theme` | `DeepPartial<ChartTheme>` | - | Theme customization |
23
+ | `responsive` | `ResponsiveConfig` | - | Declarative container-query responsive overrides (theme + components) |
24
+
25
+ ### Donut Config
26
+
27
+ ```typescript
28
+ donut: {
29
+ innerRadius?: number, // 0-1, percentage of outer radius (default: 0.5)
30
+ padAngle?: number, // Radians between segments (default: 0.02)
31
+ cornerRadius?: number, // Corner radius in pixels (default: 0)
32
+ }
33
+ ```
34
+
35
+ ### ValueLabel Config
36
+
37
+ ```typescript
38
+ valueLabel: {
39
+ show?: boolean, // default: false
40
+ position?: 'outside' | 'auto', // default: 'auto'
41
+ outsideOffset?: number, // default: 16
42
+ minVerticalSpacing?: number, // default: 14
43
+ formatter?: (label, value, data, percentage) => string, // default: `${label}: ${value}`
44
+ }
45
+ ```
46
+
47
+ Donut value labels are rendered outside the ring with leader lines. `auto`
48
+ currently resolves to the same outside placement as `outside`.
49
+
50
+ ## Example
51
+
52
+ ```javascript
53
+ import { DonutChart } from '@internetstiftelsen/charts/donut-chart';
54
+ import { DonutCenterContent } from '@internetstiftelsen/charts/donut-center-content';
55
+ import { Legend } from '@internetstiftelsen/charts/legend';
56
+ import { Tooltip } from '@internetstiftelsen/charts/tooltip';
57
+ import { Title } from '@internetstiftelsen/charts/title';
58
+
59
+ const data = [
60
+ { name: 'Desktop', value: 450 },
61
+ { name: 'Mobile', value: 320 },
62
+ { name: 'Tablet', value: 130 },
63
+ ];
64
+
65
+ const chart = new DonutChart({
66
+ data,
67
+ valueKey: 'value',
68
+ labelKey: 'name',
69
+ donut: {
70
+ innerRadius: 0.6,
71
+ padAngle: 0.02,
72
+ cornerRadius: 4,
73
+ },
74
+ valueLabel: {
75
+ show: true,
76
+ formatter: (label, _value, _data, percentage) =>
77
+ `${label}: ${percentage.toFixed(1)}%`,
78
+ },
79
+ });
80
+
81
+ chart
82
+ .addChild(new Title({ text: 'Device Distribution' }))
83
+ .addChild(
84
+ new DonutCenterContent({
85
+ mainValue: '900',
86
+ title: 'Total',
87
+ subtitle: 'visitors',
88
+ }),
89
+ )
90
+ .addChild(new Legend({ position: 'bottom' }))
91
+ .addChild(new Tooltip());
92
+
93
+ chart.render('#chart-container');
94
+ ```
95
+
96
+ ## Responsive Height and Center Text
97
+
98
+ - DonutChart uses container-driven `100%` height (same behavior model as width).
99
+ - Set an explicit container height for predictable visual sizing.
100
+ - Donut center-content text shrinks with chart size so it scales with the donut.
101
+ - Donut value-label text also scales down with the chart size.
102
+
103
+ ## Custom Colors
104
+
105
+ You can specify colors directly in your data:
106
+
107
+ ```javascript
108
+ const data = [
109
+ { name: 'Desktop', value: 450, color: '#4ecdc4' },
110
+ { name: 'Mobile', value: 320, color: '#ff6b6b' },
111
+ { name: 'Tablet', value: 130, color: '#45b7d1' },
112
+ ];
113
+ ```
114
+
115
+ Or use the theme's color palette (colors are auto-assigned by index).
116
+
117
+ ---
118
+
119
+ ## DonutCenterContent
120
+
121
+ Displays text content in the center of the donut.
122
+
123
+ ```typescript
124
+ new DonutCenterContent(config: DonutCenterContentConfig)
125
+ ```
126
+
127
+ ### Config Options
128
+
129
+ | Option | Type | Description |
130
+ | ---------------- | ----------- | --------------------------------- |
131
+ | `mainValue` | `string` | Large primary value (e.g., "900") |
132
+ | `title` | `string` | Title text below main value |
133
+ | `subtitle` | `string` | Smaller subtitle text |
134
+ | `mainValueStyle` | `TextStyle` | Style for main value |
135
+ | `titleStyle` | `TextStyle` | Style for title |
136
+ | `subtitleStyle` | `TextStyle` | Style for subtitle |
137
+
138
+ ### TextStyle Options
139
+
140
+ ```typescript
141
+ {
142
+ fontSize?: number,
143
+ fontWeight?: string,
144
+ fontFamily?: string,
145
+ color?: string,
146
+ }
147
+ ```
148
+
149
+ ### Example
150
+
151
+ ```javascript
152
+ new DonutCenterContent({
153
+ mainValue: '$1.2M',
154
+ title: 'Revenue',
155
+ subtitle: 'Q4 2024',
156
+ mainValueStyle: {
157
+ fontSize: 36,
158
+ fontWeight: 'bold',
159
+ color: '#333',
160
+ },
161
+ titleStyle: {
162
+ fontSize: 14,
163
+ color: '#666',
164
+ },
165
+ subtitleStyle: {
166
+ fontSize: 12,
167
+ color: '#999',
168
+ },
169
+ });
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Supported Components
175
+
176
+ DonutChart supports the following components via `addChild()`:
177
+
178
+ - `DonutCenterContent` - Center text content
179
+ - `Title` - Chart title
180
+ - `Legend` - Interactive legend (click to toggle segments)
181
+ - `Tooltip` - Hover tooltips with segment info
182
+
183
+ ---
184
+
185
+ ## Tooltip Formatting
186
+
187
+ The default tooltip shows label, value, and percentage. Customize with a formatter:
188
+
189
+ ```javascript
190
+ new Tooltip({
191
+ formatter: (dataKey, value, data) => `${dataKey}: ${value.toLocaleString()} visitors`,
192
+ });
193
+ ```