@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
package/README.md CHANGED
@@ -6,11 +6,15 @@ A framework-agnostic, composable charting library built on D3.js with TypeScript
6
6
 
7
7
  - **Framework Agnostic** - Works with vanilla JS, React, Vue, Svelte, or any framework
8
8
  - **Composable Architecture** - Build charts by composing components
9
- - **Multiple Chart Types** - XYChart (lines, areas, bars), WordCloudChart, DonutChart, PieChart, and GaugeChart
10
- - **Radial Value Labels** - Pie and donut charts support optional on-chart labels with custom formatters
9
+ - **Multiple Chart Types** - XYChart (lines, scatter, areas, bars), WordCloudChart, DonutChart, PieChart, and GaugeChart
10
+ - **Combined Chart Layouts** - `ChartGroup` composes existing charts into shared dashboards with one coordinated legend
11
+ - **Divergent Bar Support** - Bar charts automatically render from zero and diverge around `0` for mixed positive/negative values
12
+ - **Mirrored Bar Sides** - Horizontal bars can mirror a series to the left for population-pyramid style charts without changing source data
13
+ - **Custom Value Labels** - XY, pie, and donut charts support optional on-chart labels with custom formatters
11
14
  - **Optional Gauge Animation** - Animate gauge value transitions with `gauge.animate`
12
15
  - **Stacking Control** - Bar stacking modes with optional reversed visual series order
13
- - **Flexible Scales** - Band, linear, time, and logarithmic scales
16
+ - **Axis Direction Control** - Use `scales.x.reverse` / `scales.y.reverse` to flip an axis when needed
17
+ - **Flexible Scales** - Band, linear, time, and logarithmic scales (bar value axes stay linear)
14
18
  - **Explicit or Responsive Sizing** - Set top-level `width`/`height` or let the container drive size
15
19
  - **Auto Resize** - Built-in ResizeObserver handles responsive behavior
16
20
  - **Responsive Policy** - Chart-level container-query overrides for theme and components
@@ -24,6 +28,22 @@ A framework-agnostic, composable charting library built on D3.js with TypeScript
24
28
  npm install @internetstiftelsen/charts
25
29
  ```
26
30
 
31
+ ## Agent Skill
32
+
33
+ This repo also ships a Codex-compatible skill in
34
+ `skills/build-internetstiftelsen-charts`.
35
+
36
+ Install it globally for Codex with `skills.sh`:
37
+
38
+ ```bash
39
+ npx skills add git@gitlab.com:internetstiftelsen/internal/webbgruppen/packages/charts.git \
40
+ -a codex \
41
+ -g \
42
+ --skill build-internetstiftelsen-charts
43
+ ```
44
+
45
+ Restart Codex after installation so the new skill is discovered.
46
+
27
47
  ## Local Development
28
48
 
29
49
  ```bash
@@ -90,6 +110,119 @@ from the render container.
90
110
  Theme overrides are deep-partial, so nested overrides like
91
111
  `theme.axis.fontSize` preserve the rest of the theme defaults.
92
112
 
113
+ ## Chart Groups
114
+
115
+ Use `ChartGroup` when you want to combine existing charts into one layout while
116
+ keeping each child chart fully functional.
117
+
118
+ ```javascript
119
+ import { ChartGroup } from '@internetstiftelsen/charts/chart-group';
120
+ import { XYChart } from '@internetstiftelsen/charts/xy-chart';
121
+ import { Line } from '@internetstiftelsen/charts/line';
122
+ import { Bar } from '@internetstiftelsen/charts/bar';
123
+ import { Legend } from '@internetstiftelsen/charts/legend';
124
+ import { Title } from '@internetstiftelsen/charts/title';
125
+
126
+ const lineChart = new XYChart({ data: lineData });
127
+ lineChart.addChild(new Line({ dataKey: 'revenue' }));
128
+
129
+ const barChart = new XYChart({ data: barData });
130
+ barChart.addChild(new Bar({ dataKey: 'revenue' }));
131
+
132
+ const group = new ChartGroup({
133
+ cols: 2,
134
+ gap: 20,
135
+ height: 420,
136
+ syncY: true,
137
+ });
138
+
139
+ group
140
+ .addChild(new Title({ text: 'Revenue vs Expenses' }))
141
+ .addChart(barChart)
142
+ .addChart(lineChart)
143
+ .addChild(new Legend());
144
+
145
+ group.render('#chart-group');
146
+ ```
147
+
148
+ Legend state now works even without mounting a `Legend` on each child chart, so
149
+ grouped charts can share one coordinated legend while preserving child tooltip,
150
+ axis, and responsive behavior. `ChartGroup.height` behaves like chart height:
151
+ set it for a fixed total group height, or omit it to size from the render
152
+ container. Set `syncY: true` when you want vertical `XYChart` children to share
153
+ the same Y domain so grid lines stay aligned while only one child renders a
154
+ visible Y axis.
155
+
156
+ `ChartGroup` also supports declarative responsive layout overrides. Group
157
+ breakpoints can change `cols` and `gap`, while `addChart(..., options)` can
158
+ override `span`, `height`, `order`, or `hidden` per child. Just like chart
159
+ responsive config, both `minWidth` and `maxWidth` are supported and all
160
+ matching breakpoints merge in declaration order:
161
+
162
+ ```typescript
163
+ const group = new ChartGroup({
164
+ cols: 3,
165
+ responsive: {
166
+ breakpoints: {
167
+ tablet: { maxWidth: 1023, cols: 2 },
168
+ mobile: { maxWidth: 640, cols: 1, gap: 16 },
169
+ },
170
+ },
171
+ });
172
+
173
+ group
174
+ .addChart(barChart, {
175
+ responsive: {
176
+ breakpoints: {
177
+ mobile: { maxWidth: 640, hidden: true },
178
+ },
179
+ },
180
+ })
181
+ .addChart(lineChart, {
182
+ span: 2,
183
+ responsive: {
184
+ breakpoints: {
185
+ mobile: { maxWidth: 640, span: 1, order: -1 },
186
+ },
187
+ },
188
+ });
189
+ ```
190
+
191
+ ## Divergent Bars
192
+
193
+ Bar charts always render from a zero baseline. When bar data contains both
194
+ positive and negative values, the bars automatically diverge around `0` in both
195
+ vertical and horizontal orientations.
196
+
197
+ ```javascript
198
+ import { XYChart } from '@internetstiftelsen/charts/xy-chart';
199
+ import { Bar } from '@internetstiftelsen/charts/bar';
200
+ import { XAxis } from '@internetstiftelsen/charts/x-axis';
201
+ import { YAxis } from '@internetstiftelsen/charts/y-axis';
202
+
203
+ const chart = new XYChart({
204
+ data: [
205
+ { metric: 'Pricing', delta: -18 },
206
+ { metric: 'Feature set', delta: 24 },
207
+ { metric: 'Support', delta: 11 },
208
+ ],
209
+ orientation: 'horizontal',
210
+ });
211
+
212
+ chart
213
+ .addChild(new XAxis({ dataKey: 'metric' }))
214
+ .addChild(new YAxis())
215
+ .addChild(new Bar({ dataKey: 'delta' }));
216
+ ```
217
+
218
+ Automatic numeric bar domains always include `0`. If you configure an explicit
219
+ numeric domain or `min`/`max` for the bar value axis, that final domain must
220
+ still include `0`.
221
+
222
+ Categorical `y` axes now preserve data order from top to bottom by default.
223
+ Use `scales.x.reverse` or `scales.y.reverse` when you want to intentionally flip
224
+ an axis direction.
225
+
93
226
  Responsive overrides are declarative and merge all matching breakpoints in
94
227
  declaration order:
95
228
 
@@ -211,6 +344,7 @@ Grouped parsing rules:
211
344
 
212
345
  - [Getting Started](./docs/getting-started.md) - Installation, Vanilla JS, React integration
213
346
  - [XYChart](./docs/xy-chart.md) - Line, area, and bar charts API
347
+ - [ChartGroup](./docs/chart-group.md) - Combined chart layouts with a shared legend
214
348
  - [WordCloudChart](./docs/word-cloud-chart.md) - Word frequency visualization API
215
349
  - [DonutChart](./docs/donut-chart.md) - Donut/pie charts API
216
350
  - [PieChart](./docs/pie-chart.md) - Pie chart API
package/dist/area.d.ts CHANGED
@@ -21,5 +21,7 @@ export declare class Area implements ChartComponent<AreaConfigBase> {
21
21
  createExportComponent(override?: Partial<AreaConfigBase>): ChartComponent<AreaConfigBase>;
22
22
  private getStackValues;
23
23
  render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: D3Scale, y: D3Scale, parseValue: (value: unknown) => number, xScaleType: ScaleType | undefined, theme: ChartTheme, stackingContext?: AreaStackingContext, valueLabelLayer?: Selection<SVGGElement, undefined, null, undefined>): void;
24
+ private renderLinePath;
25
+ private renderPoints;
24
26
  private renderValueLabels;
25
27
  }
package/dist/area.js CHANGED
@@ -199,43 +199,49 @@ export class Area {
199
199
  .attr('stroke', 'none')
200
200
  .attr('d', areaGenerator);
201
201
  if (this.showLine) {
202
- const lineGenerator = line()
203
- .defined((d) => d.valid)
204
- .curve(curveFactory)
205
- .x(getXPosition)
206
- .y((d) => y(d.y1) || 0);
207
- const lineStrokeWidth = this.strokeWidth ?? theme.line.strokeWidth;
208
- plotGroup
209
- .append('path')
210
- .datum(areaData)
211
- .attr('class', `area-line-${sanitizedKey}`)
212
- .attr('fill', 'none')
213
- .attr('stroke', this.stroke)
214
- .attr('stroke-width', lineStrokeWidth)
215
- .attr('d', lineGenerator);
202
+ this.renderLinePath(plotGroup, areaData, curveFactory, getXPosition, y, theme, sanitizedKey);
216
203
  }
217
204
  if (this.showPoints) {
218
- const validData = areaData.filter((d) => d.valid);
219
- const pointSize = this.pointSize ?? theme.line.point.size;
220
- const pointStrokeWidth = theme.line.point.strokeWidth;
221
- const pointStrokeColor = theme.line.point.strokeColor || this.stroke;
222
- const pointColor = theme.line.point.color || this.stroke;
223
- plotGroup
224
- .selectAll(`.area-point-${sanitizedKey}`)
225
- .data(validData)
226
- .join('circle')
227
- .attr('class', `area-point-${sanitizedKey}`)
228
- .attr('cx', getXPosition)
229
- .attr('cy', (d) => y(d.y1) || 0)
230
- .attr('r', pointSize)
231
- .attr('fill', pointColor)
232
- .attr('stroke', pointStrokeColor)
233
- .attr('stroke-width', pointStrokeWidth);
205
+ this.renderPoints(plotGroup, areaData, getXPosition, y, theme, sanitizedKey);
234
206
  }
235
207
  if (this.valueLabel?.show) {
236
208
  this.renderValueLabels(valueLabelLayer ?? plotGroup, areaData.filter((d) => d.valid), y, parseValue, theme, getXPosition);
237
209
  }
238
210
  }
211
+ renderLinePath(plotGroup, areaData, curveFactory, getXPosition, y, theme, sanitizedKey) {
212
+ const lineGenerator = line()
213
+ .defined((d) => d.valid)
214
+ .curve(curveFactory)
215
+ .x(getXPosition)
216
+ .y((d) => y(d.y1) || 0);
217
+ const lineStrokeWidth = this.strokeWidth ?? theme.line.strokeWidth;
218
+ plotGroup
219
+ .append('path')
220
+ .datum(areaData)
221
+ .attr('class', `area-line-${sanitizedKey}`)
222
+ .attr('fill', 'none')
223
+ .attr('stroke', this.stroke)
224
+ .attr('stroke-width', lineStrokeWidth)
225
+ .attr('d', lineGenerator);
226
+ }
227
+ renderPoints(plotGroup, areaData, getXPosition, y, theme, sanitizedKey) {
228
+ const validData = areaData.filter((d) => d.valid);
229
+ const pointSize = this.pointSize ?? theme.line.point.size;
230
+ const pointStrokeWidth = theme.line.point.strokeWidth;
231
+ const pointStrokeColor = theme.line.point.strokeColor || this.stroke;
232
+ const pointColor = theme.line.point.color || this.stroke;
233
+ plotGroup
234
+ .selectAll(`.area-point-${sanitizedKey}`)
235
+ .data(validData)
236
+ .join('circle')
237
+ .attr('class', `area-point-${sanitizedKey}`)
238
+ .attr('cx', getXPosition)
239
+ .attr('cy', (d) => y(d.y1) || 0)
240
+ .attr('r', pointSize)
241
+ .attr('fill', pointColor)
242
+ .attr('stroke', pointStrokeColor)
243
+ .attr('stroke-width', pointStrokeWidth);
244
+ }
239
245
  renderValueLabels(plotGroup, data, y, parseValue, theme, getXPosition) {
240
246
  const config = this.valueLabel;
241
247
  const fontSize = config.fontSize ?? theme.valueLabel.fontSize;
@@ -260,7 +266,9 @@ export class Area {
260
266
  if (!Number.isFinite(parsedValue)) {
261
267
  return;
262
268
  }
263
- const valueText = String(parsedValue);
269
+ const valueText = config.formatter
270
+ ? config.formatter(this.dataKey, parsedValue, d.data)
271
+ : String(parsedValue);
264
272
  const xPos = getXPosition(d);
265
273
  const yPos = y(d.y1) || 0;
266
274
  const tempText = labelGroup
package/dist/bar.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Selection } from 'd3';
2
- import type { BarConfig, BarStackingContext, BarValueLabelConfig, ChartTheme, D3Scale, DataItem, ScaleType, ExportHooks, BarConfigBase } from './types.js';
2
+ import type { BarConfig, BarStackingContext, BarSide, BarValueLabelConfig, ChartTheme, D3Scale, DataItem, Orientation, ScaleType, ExportHooks, BarConfigBase } from './types.js';
3
3
  import type { ChartComponent } from './chart-interface.js';
4
4
  export declare class Bar implements ChartComponent<BarConfigBase> {
5
5
  readonly type: "bar";
@@ -7,14 +7,33 @@ export declare class Bar implements ChartComponent<BarConfigBase> {
7
7
  readonly fill: string;
8
8
  readonly colorAdapter?: (data: DataItem, index: number) => string;
9
9
  readonly maxBarSize?: number;
10
+ readonly side: BarSide;
10
11
  readonly valueLabel?: BarValueLabelConfig;
11
12
  readonly exportHooks?: ExportHooks<BarConfigBase>;
12
13
  constructor(config: BarConfig);
13
14
  getExportConfig(): BarConfigBase;
14
15
  createExportComponent(override?: Partial<BarConfigBase>): ChartComponent<BarConfigBase>;
16
+ getRenderedValue(value: number, orientation?: Orientation): number;
15
17
  render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: D3Scale, y: D3Scale, parseValue: (value: unknown) => number, xScaleType?: ScaleType, theme?: ChartTheme, stackingContext?: BarStackingContext, orientation?: 'vertical' | 'horizontal'): void;
16
18
  private renderVertical;
17
19
  private renderHorizontal;
20
+ private resolveValueLabelConfig;
21
+ private resolveValueLabelPlacement;
22
+ private resolveValueLabelStyle;
23
+ private getValueLabelText;
24
+ private getBarColor;
25
+ private measureLabelBox;
26
+ private getLabelColor;
27
+ private appendValueLabel;
28
+ private getVerticalLabelPlacement;
29
+ private getVerticalOutsideLabelPlacement;
30
+ private getVerticalInsideLabelY;
31
+ private getHorizontalLabelPlacement;
32
+ private getHorizontalOutsideLabelPlacement;
33
+ private getHorizontalInsideLabelX;
34
+ private isHorizontalLabelWithinBounds;
35
+ private renderVerticalValueLabel;
36
+ private renderHorizontalValueLabel;
18
37
  private renderVerticalValueLabels;
19
38
  private renderHorizontalValueLabels;
20
39
  }