@internetstiftelsen/charts 0.9.2 → 0.10.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.
Files changed (46) hide show
  1. package/README.md +137 -3
  2. package/dist/area.d.ts +2 -0
  3. package/dist/area.js +39 -31
  4. package/dist/bar.d.ts +20 -1
  5. package/dist/bar.js +395 -519
  6. package/dist/base-chart.d.ts +21 -1
  7. package/dist/base-chart.js +166 -93
  8. package/dist/chart-group.d.ts +137 -0
  9. package/dist/chart-group.js +1155 -0
  10. package/dist/chart-interface.d.ts +1 -1
  11. package/dist/donut-center-content.d.ts +1 -0
  12. package/dist/donut-center-content.js +21 -38
  13. package/dist/donut-chart.js +30 -15
  14. package/dist/gauge-chart.d.ts +20 -0
  15. package/dist/gauge-chart.js +229 -133
  16. package/dist/legend-state.d.ts +19 -0
  17. package/dist/legend-state.js +81 -0
  18. package/dist/legend.d.ts +5 -2
  19. package/dist/legend.js +45 -38
  20. package/dist/line.js +3 -1
  21. package/dist/pie-chart.d.ts +3 -0
  22. package/dist/pie-chart.js +45 -19
  23. package/dist/scatter.d.ts +16 -0
  24. package/dist/scatter.js +165 -0
  25. package/dist/tooltip.d.ts +2 -1
  26. package/dist/tooltip.js +21 -25
  27. package/dist/types.d.ts +19 -1
  28. package/dist/utils.js +11 -19
  29. package/dist/validation.d.ts +4 -0
  30. package/dist/validation.js +19 -0
  31. package/dist/x-axis.d.ts +10 -0
  32. package/dist/x-axis.js +190 -149
  33. package/dist/xy-chart.d.ts +40 -1
  34. package/dist/xy-chart.js +488 -165
  35. package/dist/y-axis.d.ts +7 -2
  36. package/dist/y-axis.js +99 -10
  37. package/docs/chart-group.md +213 -0
  38. package/docs/components.md +321 -0
  39. package/docs/donut-chart.md +193 -0
  40. package/docs/gauge-chart.md +175 -0
  41. package/docs/getting-started.md +311 -0
  42. package/docs/pie-chart.md +123 -0
  43. package/docs/theming.md +162 -0
  44. package/docs/word-cloud-chart.md +98 -0
  45. package/docs/xy-chart.md +517 -0
  46. package/package.json +6 -4
@@ -0,0 +1,517 @@
1
+ # XYChart API
2
+
3
+ The main chart class for creating XY-coordinate charts (line, scatter, area, bar, or mixed).
4
+
5
+ ## Constructor
6
+
7
+ ```typescript
8
+ new XYChart(config: XYChartConfig)
9
+ ```
10
+
11
+ ### Config Options
12
+
13
+ | Option | Type | Default | Description |
14
+ | ------------- | ---------------------------------- | ---------------------------------------------------- | --------------------------------------------------------------------- |
15
+ | `data` | `DataItem[] \| GroupedDataGroup[]` | required | Flat rows or grouped nested rows |
16
+ | `width` | `number` | - | Explicit chart width in pixels |
17
+ | `height` | `number` | - | Explicit chart height in pixels |
18
+ | `theme` | `DeepPartial<ChartTheme>` | - | Theme customization |
19
+ | `scales` | `AxisScaleConfig` | - | Scale configuration |
20
+ | `orientation` | `'vertical' \| 'horizontal'` | `'vertical'` | Chart orientation. Horizontal mode currently supports bar-only charts |
21
+ | `responsive` | `ResponsiveConfig` | - | Container-query responsive overrides (theme + components) |
22
+ | `barStack` | `BarStackConfig` | `{ mode: 'normal', gap: 0.1, reverseSeries: false }` | Bar stacking configuration |
23
+ | `areaStack` | `AreaStackConfig` | `{ mode: 'none' }` | Area stacking configuration |
24
+
25
+ ### Theme Options
26
+
27
+ ```typescript
28
+ theme: {
29
+ margins: { // Base margins around plot area
30
+ top: number, // default: 20
31
+ right: number, // default: 20
32
+ bottom: number, // default: 20
33
+ left: number, // default: 20
34
+ },
35
+ colorPalette: string[], // Colors for auto-assignment
36
+ grid: {
37
+ color: string, // Grid line color (default: '#e0e0e0')
38
+ opacity: number, // Grid line opacity (default: 0.5)
39
+ },
40
+ axis: {
41
+ fontFamily: string,
42
+ fontSize: string,
43
+ },
44
+ }
45
+ ```
46
+
47
+ Use top-level `width` and `height` for fixed-size charts. If omitted, the chart
48
+ sizes itself from the render container.
49
+
50
+ ### Grouped Dataset Shape
51
+
52
+ Grouped categorical datasets can be passed as nested groups:
53
+
54
+ ```json
55
+ [
56
+ {
57
+ "group": "Internetanv. 8+ år",
58
+ "data": [
59
+ {
60
+ "Kategori": "Man",
61
+ "Använt sociala medier varje dag": "83%",
62
+ "Använt sociala medier minst varje vecka": "90%"
63
+ },
64
+ {
65
+ "Kategori": "Kvinna",
66
+ "Använt sociala medier varje dag": "86%",
67
+ "Använt sociala medier minst varje vecka": "92%"
68
+ }
69
+ ]
70
+ }
71
+ ]
72
+ ```
73
+
74
+ Rules for grouped datasets:
75
+
76
+ - The first key of each row object is used as the category key (for example `Kategori`).
77
+ - Remaining keys are treated as metric columns.
78
+ - Each `group` must be a non-empty string.
79
+ - Group labels are rendered on a second x-axis row when `XAxis.showGroupLabels` is enabled.
80
+ - CSV/XLSX exports use spreadsheet-style grouped rows (blank first two headers, blank continuation group cells, no spacer rows).
81
+
82
+ ### Scale Options
83
+
84
+ ```typescript
85
+ scales: {
86
+ x: {
87
+ type: 'band' | 'linear' | 'time' | 'log',
88
+ domain?: ScaleDomainValue[],
89
+ padding?: number,
90
+ groupGap?: number, // adds visual slot gaps between grouped categories
91
+ reverse?: boolean, // reverse the rendered axis direction
92
+ nice?: boolean,
93
+ },
94
+ y: {
95
+ type: 'band' | 'linear' | 'time' | 'log',
96
+ domain?: ScaleDomainValue[],
97
+ padding?: number,
98
+ reverse?: boolean, // reverse the rendered axis direction
99
+ nice?: boolean,
100
+ },
101
+ }
102
+ ```
103
+
104
+ Categorical `y` axes render in data order from top to bottom by default. Set
105
+ `reverse: true` on either axis to intentionally flip its direction for category,
106
+ numeric, time, or log scales.
107
+
108
+ ## Scale Types
109
+
110
+ `XYChart` supports categorical, numeric, temporal, and logarithmic axes.
111
+
112
+ ### Time Scale
113
+
114
+ Use `time` when the axis values are `Date` objects:
115
+
116
+ ```javascript
117
+ const chart = new XYChart({
118
+ data: [
119
+ { date: new Date('2024-01-01'), value: 100 },
120
+ { date: new Date('2024-01-02'), value: 150 },
121
+ { date: new Date('2024-01-03'), value: 120 },
122
+ ],
123
+ scales: {
124
+ x: { type: 'time', nice: true },
125
+ y: { type: 'linear', nice: true },
126
+ },
127
+ });
128
+ ```
129
+
130
+ ### Logarithmic Scale
131
+
132
+ Use `log` for exponential data. Log scales require positive values and a
133
+ strictly positive domain:
134
+
135
+ ```javascript
136
+ const chart = new XYChart({
137
+ data: [
138
+ { x: 1, y: 10 },
139
+ { x: 2, y: 100 },
140
+ { x: 3, y: 1000 },
141
+ { x: 4, y: 10000 },
142
+ ],
143
+ scales: {
144
+ y: { type: 'log', domain: [1, 10000] },
145
+ },
146
+ });
147
+ ```
148
+
149
+ Bar series cannot use logarithmic value axes because bars always render from a
150
+ zero baseline.
151
+
152
+ ### Custom Domains
153
+
154
+ Set `domain` to override the automatic extent calculation:
155
+
156
+ ```javascript
157
+ const chart = new XYChart({
158
+ data,
159
+ scales: {
160
+ y: {
161
+ type: 'linear',
162
+ domain: [0, 100],
163
+ nice: true,
164
+ },
165
+ },
166
+ });
167
+ ```
168
+
169
+ For bar charts, automatic numeric value domains always include `0`. If you set
170
+ an explicit numeric domain or `min`/`max` for the bar value axis, that final
171
+ domain must still include `0`.
172
+
173
+ On horizontal bar charts, category order is controlled with `scales.x.reverse`
174
+ because `x` remains the categorical dimension even when it renders vertically.
175
+
176
+ ## Responsive Overrides
177
+
178
+ Use chart-level `responsive` to declare breakpoint-specific theme and component
179
+ overrides before layout is calculated:
180
+
181
+ ```typescript
182
+ const chart = new XYChart({
183
+ data,
184
+ responsive: {
185
+ breakpoints: {
186
+ sm: {
187
+ maxWidth: 640,
188
+ theme: {
189
+ axis: {
190
+ fontSize: 11,
191
+ },
192
+ },
193
+ components: [
194
+ {
195
+ match: { type: 'xAxis' },
196
+ override: {
197
+ display: false,
198
+ },
199
+ },
200
+ ],
201
+ },
202
+ md: {
203
+ minWidth: 641,
204
+ maxWidth: 768,
205
+ theme: {
206
+ axis: {
207
+ fontSize: 12,
208
+ },
209
+ },
210
+ },
211
+ },
212
+ },
213
+ });
214
+ ```
215
+
216
+ `minWidth` and `maxWidth` are both optional, but breakpoints that omit both are
217
+ ignored. Multiple breakpoints can match at the same width; matching breakpoints
218
+ merge in declaration order, so later entries win when they override the same
219
+ values.
220
+
221
+ If you need dynamic logic, `responsive.beforeRender` still runs before spacing
222
+ is measured after declarative breakpoint overrides are merged. It receives
223
+ `context.activeBreakpoints` with every match and `context.breakpoint` as the
224
+ last matching breakpoint name.
225
+
226
+ ## Validation
227
+
228
+ `XYChart` validates configuration and series data early:
229
+
230
+ - `data` must be a non-empty array.
231
+ - Series `dataKey` values must exist in the dataset.
232
+ - Rendered series values must resolve to numeric values.
233
+ - Log scales reject zero or negative domain values and zero or negative series values.
234
+ - Bar series require a linear value axis and explicit numeric bar domains must include `0`.
235
+
236
+ ## Methods
237
+
238
+ ### addChild(component)
239
+
240
+ Adds a component to the chart. Returns `this` for chaining.
241
+
242
+ ```javascript
243
+ chart
244
+ .addChild(new XAxis({ dataKey: 'date' }))
245
+ .addChild(new YAxis())
246
+ .addChild(new Line({ dataKey: 'value' }));
247
+ ```
248
+
249
+ ### render(target)
250
+
251
+ Renders the chart to a DOM element. Accepts a CSS selector or HTMLElement. Automatically sets up resize handling.
252
+
253
+ ```javascript
254
+ chart.render('#chart-container');
255
+ // or
256
+ chart.render(document.getElementById('chart-container'));
257
+ ```
258
+
259
+ ### update(data)
260
+
261
+ Updates the chart with new data and re-renders.
262
+
263
+ ```javascript
264
+ chart.update(newData);
265
+ ```
266
+
267
+ ### destroy()
268
+
269
+ Cleans up all resources, removes resize observer, and clears the chart from the DOM.
270
+
271
+ ```javascript
272
+ chart.destroy();
273
+ ```
274
+
275
+ ---
276
+
277
+ ## Line
278
+
279
+ Renders a line series on the chart.
280
+
281
+ ```typescript
282
+ new Line({
283
+ dataKey: string, // Key in data objects for Y values (required)
284
+ stroke?: string, // Line color (auto-assigned if omitted)
285
+ strokeWidth?: number, // Line width in pixels (default: 2)
286
+ valueLabel?: {
287
+ show?: boolean,
288
+ formatter?: (dataKey, value, data) => string
289
+ } // Point value badges
290
+ })
291
+ ```
292
+
293
+ ### Example
294
+
295
+ ```javascript
296
+ // Auto-assigned colors
297
+ chart.addChild(new Line({ dataKey: 'revenue' }));
298
+ chart.addChild(new Line({ dataKey: 'expenses' }));
299
+
300
+ // Manual colors
301
+ chart.addChild(new Line({ dataKey: 'revenue', stroke: '#00ff00' }));
302
+ chart.addChild(new Line({ dataKey: 'expenses', stroke: '#ff0000' }));
303
+ ```
304
+
305
+ ---
306
+
307
+ ## Scatter
308
+
309
+ Renders a point-only scatter series on the chart.
310
+
311
+ ```typescript
312
+ new Scatter({
313
+ dataKey: string, // Key in data objects for Y values (required)
314
+ stroke?: string, // Point color (auto-assigned if omitted)
315
+ pointSize?: number, // Point radius in pixels (default: theme.line.point.size)
316
+ valueLabel?: {
317
+ show?: boolean,
318
+ formatter?: (dataKey, value, data) => string
319
+ } // Point value badges
320
+ })
321
+ ```
322
+
323
+ ### Example
324
+
325
+ ```javascript
326
+ chart.addChild(new Scatter({ dataKey: 'revenue' }));
327
+ chart.addChild(new Scatter({ dataKey: 'expenses', pointSize: 7 }));
328
+ ```
329
+
330
+ ---
331
+
332
+ ## Bar
333
+
334
+ Renders a bar series on the chart.
335
+
336
+ ```typescript
337
+ new Bar({
338
+ dataKey: string, // Key in data objects for values (required)
339
+ fill?: string, // Bar color (auto-assigned if omitted)
340
+ maxBarSize?: number, // Max width/height depending on chart orientation
341
+ side?: 'left' | 'right', // Mirror horizontal bars to the left without changing source data
342
+ valueLabel?: {
343
+ show?: boolean,
344
+ position?: 'inside' | 'outside',
345
+ insidePosition?: 'top' | 'middle' | 'bottom',
346
+ formatter?: (dataKey, value, data) => string
347
+ }
348
+ })
349
+ ```
350
+
351
+ ### Example
352
+
353
+ ```javascript
354
+ chart.addChild(new Bar({ dataKey: 'sales' }));
355
+ chart.addChild(new Bar({ dataKey: 'returns', fill: '#ff6b6b' }));
356
+ ```
357
+
358
+ Use `XYChart.orientation: 'horizontal'` for horizontal bar charts:
359
+
360
+ ```javascript
361
+ const chart = new XYChart({
362
+ data,
363
+ orientation: 'horizontal',
364
+ });
365
+
366
+ chart
367
+ .addChild(new XAxis({ dataKey: 'metric' }))
368
+ .addChild(new YAxis())
369
+ .addChild(new Bar({ dataKey: 'current' }))
370
+ .addChild(new Bar({ dataKey: 'benchmark' }));
371
+ ```
372
+
373
+ Horizontal and vertical bar charts now render from a true zero baseline. When a
374
+ bar dataset contains both positive and negative values, bars automatically
375
+ diverge around `0`.
376
+
377
+ ```javascript
378
+ const chart = new XYChart({
379
+ data: [
380
+ { metric: 'Pricing', delta: -18 },
381
+ { metric: 'Feature set', delta: 24 },
382
+ { metric: 'Support', delta: 11 },
383
+ ],
384
+ orientation: 'horizontal',
385
+ });
386
+
387
+ chart
388
+ .addChild(new XAxis({ dataKey: 'metric' }))
389
+ .addChild(new YAxis())
390
+ .addChild(new Bar({ dataKey: 'delta' }));
391
+ ```
392
+
393
+ Population-pyramid style charts can keep both series positive in source data and
394
+ mirror one series to the left at render time:
395
+
396
+ ```javascript
397
+ const chart = new XYChart({
398
+ data: [
399
+ {
400
+ source: 'Digitala tidningar/nyhetssajter',
401
+ yngre: '51%',
402
+ äldre: '19%',
403
+ },
404
+ {
405
+ source: 'Tv-kanal (SVT, TV4 etc.)',
406
+ yngre: '45%',
407
+ äldre: '86%',
408
+ },
409
+ ],
410
+ orientation: 'horizontal',
411
+ scales: {
412
+ y: { type: 'linear', min: -100, max: 100, nice: false },
413
+ },
414
+ barStack: { mode: 'normal' },
415
+ });
416
+
417
+ chart
418
+ .addChild(
419
+ new XAxis({
420
+ dataKey: 'source',
421
+ tickFormat: (value) => `${Math.abs(Number(value))}%`,
422
+ }),
423
+ )
424
+ .addChild(new YAxis())
425
+ .addChild(new Bar({ dataKey: 'yngre', side: 'left' }))
426
+ .addChild(new Bar({ dataKey: 'äldre' }));
427
+ ```
428
+
429
+ Horizontal orientation currently supports bar-only charts. Mixed horizontal
430
+ bar/line, bar/scatter, or bar/area charts are rejected.
431
+
432
+ ### Stacking Modes
433
+
434
+ Bar charts support different stacking modes:
435
+
436
+ - `none` - Bars side by side (default)
437
+ - `normal` - Stacked bars
438
+ - `percent` - 100% stacked bars
439
+ - `layer` - Overlapping bars
440
+
441
+ Use `barStack.reverseSeries: true` to reverse bar series display order for rendering, legend entries, and tooltip rows without changing data exports.
442
+
443
+ ---
444
+
445
+ ## Area
446
+
447
+ Renders an area series on the chart.
448
+
449
+ ```typescript
450
+ new Area({
451
+ dataKey: string, // Key in data objects for Y values (required)
452
+ fill?: string, // Area fill color (auto-assigned if omitted)
453
+ stroke?: string, // Optional line color (defaults to fill color)
454
+ opacity?: number, // Fill opacity (default: 0.3)
455
+ curve?: 'linear' | 'monotone' | 'step' | 'natural' | 'basis' | 'cardinal',
456
+ stackId?: string | number, // Group key used for area stacking
457
+ baseline?: number, // Baseline for non-stacked area (default: 0)
458
+ showLine?: boolean, // Show top stroke line (default: true)
459
+ showPoints?: boolean, // Show points on top line (default: false)
460
+ valueLabel?: {
461
+ show?: boolean,
462
+ formatter?: (dataKey, value, data) => string
463
+ } // Point value badges
464
+ })
465
+ ```
466
+
467
+ ### Example
468
+
469
+ ```javascript
470
+ chart.addChild(new Area({ dataKey: 'revenue' }));
471
+ chart.addChild(
472
+ new Area({
473
+ dataKey: 'expenses',
474
+ fill: '#ff6b6b',
475
+ opacity: 0.2,
476
+ curve: 'monotone',
477
+ showPoints: true,
478
+ }),
479
+ );
480
+ ```
481
+
482
+ For `Line`, `Scatter`, `Bar`, and `Area`, `valueLabel.formatter` receives
483
+ `(dataKey, value, data)`, where `value` is the parsed numeric series value used
484
+ for rendering that label.
485
+
486
+ ### Area Stacking
487
+
488
+ Area charts support stacking when series share the same `stackId`:
489
+
490
+ - `none` - No area stacking (default)
491
+ - `normal` - Absolute stacking
492
+ - `percent` - Normalized 100% stacking
493
+
494
+ ```javascript
495
+ const chart = new XYChart({
496
+ data,
497
+ areaStack: { mode: 'percent' },
498
+ });
499
+
500
+ chart.addChild(new Area({ dataKey: 'desktop', stackId: 'traffic' })).addChild(new Area({ dataKey: 'mobile', stackId: 'traffic' }));
501
+ ```
502
+
503
+ ---
504
+
505
+ ## Mixed Charts
506
+
507
+ Combine lines, scatter series, areas, and bars in the same chart:
508
+
509
+ ```javascript
510
+ const chart = new XYChart({ data });
511
+
512
+ chart
513
+ .addChild(new Bar({ dataKey: 'volume' }))
514
+ .addChild(new Scatter({ dataKey: 'orders' }))
515
+ .addChild(new Area({ dataKey: 'averageRange', opacity: 0.2 }))
516
+ .addChild(new Line({ dataKey: 'price' }));
517
+ ```
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.9.2",
2
+ "version": "0.10.1",
3
3
  "name": "@internetstiftelsen/charts",
4
4
  "type": "module",
5
5
  "sideEffects": false,
@@ -12,6 +12,7 @@
12
12
  },
13
13
  "files": [
14
14
  "dist",
15
+ "docs",
15
16
  "README.md",
16
17
  "LICENSE"
17
18
  ],
@@ -43,7 +44,7 @@
43
44
  "devDependencies": {
44
45
  "@eslint/js": "^9.39.4",
45
46
  "@handsontable/react-wrapper": "^16.2.0",
46
- "@internetstiftelsen/styleguide": "^5.1.24",
47
+ "@internetstiftelsen/styleguide": "^5.1.25",
47
48
  "@radix-ui/react-label": "^2.1.8",
48
49
  "@radix-ui/react-select": "^2.2.6",
49
50
  "@radix-ui/react-switch": "^1.2.6",
@@ -69,6 +70,7 @@
69
70
  "jsdom": "^27.4.0",
70
71
  "lucide-react": "^0.548.0",
71
72
  "prettier": "3.6.2",
73
+ "radix-ui": "^1.4.3",
72
74
  "react": "^19.2.4",
73
75
  "react-dom": "^19.2.4",
74
76
  "sass": "^1.98.0",
@@ -77,8 +79,8 @@
77
79
  "tsc-alias": "^1.8.16",
78
80
  "tw-animate-css": "^1.4.0",
79
81
  "typescript": "~5.9.3",
80
- "typescript-eslint": "^8.57.1",
82
+ "typescript-eslint": "^8.57.2",
81
83
  "vite": "^7.3.1",
82
- "vitest": "^4.1.0"
84
+ "vitest": "^4.1.1"
83
85
  }
84
86
  }