@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.
- package/README.md +137 -3
- package/dist/area.d.ts +2 -0
- package/dist/area.js +39 -31
- package/dist/bar.d.ts +20 -1
- package/dist/bar.js +395 -519
- package/dist/base-chart.d.ts +21 -1
- package/dist/base-chart.js +166 -93
- package/dist/chart-group.d.ts +137 -0
- package/dist/chart-group.js +1155 -0
- package/dist/chart-interface.d.ts +1 -1
- package/dist/donut-center-content.d.ts +1 -0
- package/dist/donut-center-content.js +21 -38
- package/dist/donut-chart.js +30 -15
- package/dist/gauge-chart.d.ts +20 -0
- package/dist/gauge-chart.js +229 -133
- package/dist/legend-state.d.ts +19 -0
- package/dist/legend-state.js +81 -0
- package/dist/legend.d.ts +5 -2
- package/dist/legend.js +45 -38
- package/dist/line.js +3 -1
- package/dist/pie-chart.d.ts +3 -0
- package/dist/pie-chart.js +45 -19
- package/dist/scatter.d.ts +16 -0
- package/dist/scatter.js +165 -0
- package/dist/tooltip.d.ts +2 -1
- package/dist/tooltip.js +21 -25
- package/dist/types.d.ts +19 -1
- package/dist/utils.js +11 -19
- package/dist/validation.d.ts +4 -0
- package/dist/validation.js +19 -0
- package/dist/x-axis.d.ts +10 -0
- package/dist/x-axis.js +190 -149
- package/dist/xy-chart.d.ts +40 -1
- package/dist/xy-chart.js +488 -165
- package/dist/y-axis.d.ts +7 -2
- package/dist/y-axis.js +99 -10
- package/docs/chart-group.md +213 -0
- package/docs/components.md +321 -0
- package/docs/donut-chart.md +193 -0
- package/docs/gauge-chart.md +175 -0
- package/docs/getting-started.md +311 -0
- package/docs/pie-chart.md +123 -0
- package/docs/theming.md +162 -0
- package/docs/word-cloud-chart.md +98 -0
- package/docs/xy-chart.md +517 -0
- 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
|
-
- **
|
|
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
|
-
- **
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
}
|