@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,502 @@
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
+ })
287
+ ```
288
+
289
+ ### Example
290
+
291
+ ```javascript
292
+ // Auto-assigned colors
293
+ chart.addChild(new Line({ dataKey: 'revenue' }));
294
+ chart.addChild(new Line({ dataKey: 'expenses' }));
295
+
296
+ // Manual colors
297
+ chart.addChild(new Line({ dataKey: 'revenue', stroke: '#00ff00' }));
298
+ chart.addChild(new Line({ dataKey: 'expenses', stroke: '#ff0000' }));
299
+ ```
300
+
301
+ ---
302
+
303
+ ## Scatter
304
+
305
+ Renders a point-only scatter series on the chart.
306
+
307
+ ```typescript
308
+ new Scatter({
309
+ dataKey: string, // Key in data objects for Y values (required)
310
+ stroke?: string, // Point color (auto-assigned if omitted)
311
+ pointSize?: number, // Point radius in pixels (default: theme.line.point.size)
312
+ valueLabel?: { show?: boolean } // Point value badges
313
+ })
314
+ ```
315
+
316
+ ### Example
317
+
318
+ ```javascript
319
+ chart.addChild(new Scatter({ dataKey: 'revenue' }));
320
+ chart.addChild(new Scatter({ dataKey: 'expenses', pointSize: 7 }));
321
+ ```
322
+
323
+ ---
324
+
325
+ ## Bar
326
+
327
+ Renders a bar series on the chart.
328
+
329
+ ```typescript
330
+ new Bar({
331
+ dataKey: string, // Key in data objects for values (required)
332
+ fill?: string, // Bar color (auto-assigned if omitted)
333
+ maxBarSize?: number, // Max width/height depending on chart orientation
334
+ side?: 'left' | 'right', // Mirror horizontal bars to the left without changing source data
335
+ valueLabel?: {
336
+ show?: boolean,
337
+ position?: 'inside' | 'outside',
338
+ insidePosition?: 'top' | 'middle' | 'bottom'
339
+ }
340
+ })
341
+ ```
342
+
343
+ ### Example
344
+
345
+ ```javascript
346
+ chart.addChild(new Bar({ dataKey: 'sales' }));
347
+ chart.addChild(new Bar({ dataKey: 'returns', fill: '#ff6b6b' }));
348
+ ```
349
+
350
+ Use `XYChart.orientation: 'horizontal'` for horizontal bar charts:
351
+
352
+ ```javascript
353
+ const chart = new XYChart({
354
+ data,
355
+ orientation: 'horizontal',
356
+ });
357
+
358
+ chart
359
+ .addChild(new XAxis({ dataKey: 'metric' }))
360
+ .addChild(new YAxis())
361
+ .addChild(new Bar({ dataKey: 'current' }))
362
+ .addChild(new Bar({ dataKey: 'benchmark' }));
363
+ ```
364
+
365
+ Horizontal and vertical bar charts now render from a true zero baseline. When a
366
+ bar dataset contains both positive and negative values, bars automatically
367
+ diverge around `0`.
368
+
369
+ ```javascript
370
+ const chart = new XYChart({
371
+ data: [
372
+ { metric: 'Pricing', delta: -18 },
373
+ { metric: 'Feature set', delta: 24 },
374
+ { metric: 'Support', delta: 11 },
375
+ ],
376
+ orientation: 'horizontal',
377
+ });
378
+
379
+ chart
380
+ .addChild(new XAxis({ dataKey: 'metric' }))
381
+ .addChild(new YAxis())
382
+ .addChild(new Bar({ dataKey: 'delta' }));
383
+ ```
384
+
385
+ Population-pyramid style charts can keep both series positive in source data and
386
+ mirror one series to the left at render time:
387
+
388
+ ```javascript
389
+ const chart = new XYChart({
390
+ data: [
391
+ {
392
+ source: 'Digitala tidningar/nyhetssajter',
393
+ yngre: '51%',
394
+ äldre: '19%',
395
+ },
396
+ {
397
+ source: 'Tv-kanal (SVT, TV4 etc.)',
398
+ yngre: '45%',
399
+ äldre: '86%',
400
+ },
401
+ ],
402
+ orientation: 'horizontal',
403
+ scales: {
404
+ y: { type: 'linear', min: -100, max: 100, nice: false },
405
+ },
406
+ barStack: { mode: 'normal' },
407
+ });
408
+
409
+ chart
410
+ .addChild(
411
+ new XAxis({
412
+ dataKey: 'source',
413
+ tickFormat: (value) => `${Math.abs(Number(value))}%`,
414
+ }),
415
+ )
416
+ .addChild(new YAxis())
417
+ .addChild(new Bar({ dataKey: 'yngre', side: 'left' }))
418
+ .addChild(new Bar({ dataKey: 'äldre' }));
419
+ ```
420
+
421
+ Horizontal orientation currently supports bar-only charts. Mixed horizontal
422
+ bar/line, bar/scatter, or bar/area charts are rejected.
423
+
424
+ ### Stacking Modes
425
+
426
+ Bar charts support different stacking modes:
427
+
428
+ - `none` - Bars side by side (default)
429
+ - `normal` - Stacked bars
430
+ - `percent` - 100% stacked bars
431
+ - `layer` - Overlapping bars
432
+
433
+ Use `barStack.reverseSeries: true` to reverse bar series display order for rendering, legend entries, and tooltip rows without changing data exports.
434
+
435
+ ---
436
+
437
+ ## Area
438
+
439
+ Renders an area series on the chart.
440
+
441
+ ```typescript
442
+ new Area({
443
+ dataKey: string, // Key in data objects for Y values (required)
444
+ fill?: string, // Area fill color (auto-assigned if omitted)
445
+ stroke?: string, // Optional line color (defaults to fill color)
446
+ opacity?: number, // Fill opacity (default: 0.3)
447
+ curve?: 'linear' | 'monotone' | 'step' | 'natural' | 'basis' | 'cardinal',
448
+ stackId?: string | number, // Group key used for area stacking
449
+ baseline?: number, // Baseline for non-stacked area (default: 0)
450
+ showLine?: boolean, // Show top stroke line (default: true)
451
+ showPoints?: boolean, // Show points on top line (default: false)
452
+ valueLabel?: { show?: boolean } // Point value badges
453
+ })
454
+ ```
455
+
456
+ ### Example
457
+
458
+ ```javascript
459
+ chart.addChild(new Area({ dataKey: 'revenue' }));
460
+ chart.addChild(
461
+ new Area({
462
+ dataKey: 'expenses',
463
+ fill: '#ff6b6b',
464
+ opacity: 0.2,
465
+ curve: 'monotone',
466
+ showPoints: true,
467
+ }),
468
+ );
469
+ ```
470
+
471
+ ### Area Stacking
472
+
473
+ Area charts support stacking when series share the same `stackId`:
474
+
475
+ - `none` - No area stacking (default)
476
+ - `normal` - Absolute stacking
477
+ - `percent` - Normalized 100% stacking
478
+
479
+ ```javascript
480
+ const chart = new XYChart({
481
+ data,
482
+ areaStack: { mode: 'percent' },
483
+ });
484
+
485
+ chart.addChild(new Area({ dataKey: 'desktop', stackId: 'traffic' })).addChild(new Area({ dataKey: 'mobile', stackId: 'traffic' }));
486
+ ```
487
+
488
+ ---
489
+
490
+ ## Mixed Charts
491
+
492
+ Combine lines, scatter series, areas, and bars in the same chart:
493
+
494
+ ```javascript
495
+ const chart = new XYChart({ data });
496
+
497
+ chart
498
+ .addChild(new Bar({ dataKey: 'volume' }))
499
+ .addChild(new Scatter({ dataKey: 'orders' }))
500
+ .addChild(new Area({ dataKey: 'averageRange', opacity: 0.2 }))
501
+ .addChild(new Line({ dataKey: 'price' }));
502
+ ```
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.9.2",
2
+ "version": "0.10.0",
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
  ],