@internetstiftelsen/charts 0.8.0 → 0.9.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.
package/README.md CHANGED
@@ -6,7 +6,8 @@ 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), DonutChart, PieChart, and GaugeChart
9
+ - **Multiple Chart Types** - XYChart (lines, areas, bars), WordCloudChart, DonutChart, PieChart, and GaugeChart
10
+ - **Optional Gauge Animation** - Animate gauge value transitions with `gauge.animate`
10
11
  - **Stacking Control** - Bar stacking modes with optional reversed visual series order
11
12
  - **Flexible Scales** - Band, linear, time, and logarithmic scales
12
13
  - **Auto Resize** - Built-in ResizeObserver handles responsive behavior
@@ -81,6 +82,37 @@ chart
81
82
  chart.render('#chart-container');
82
83
  ```
83
84
 
85
+ ## Word Cloud
86
+
87
+ ```javascript
88
+ import { WordCloudChart } from '@internetstiftelsen/charts/word-cloud-chart';
89
+
90
+ const data = [
91
+ { word: 'internet', count: 96 },
92
+ { word: 'social', count: 82 },
93
+ { word: 'news', count: 75 },
94
+ ];
95
+
96
+ const chart = new WordCloudChart({
97
+ data,
98
+ wordCloud: {
99
+ minValue: 5,
100
+ minWordLength: 3,
101
+ minFontSize: 3,
102
+ maxFontSize: 20,
103
+ padding: 1,
104
+ spiral: 'archimedean',
105
+ },
106
+ });
107
+
108
+ chart.render('#word-cloud');
109
+ ```
110
+
111
+ `minFontSize` and `maxFontSize` are percentages of the smaller plot-area
112
+ dimension and define the relative size range passed into `d3-cloud`. The chart
113
+ expects flat `{ word, count }` rows, aggregates duplicate words after trimming,
114
+ and maps theme typography and colors directly into the layout and rendered SVG.
115
+
84
116
  ## Export
85
117
 
86
118
  `chart.export()` supports `svg`, `json`, `csv`, `xlsx`, `png`, `jpg`, and `pdf`.
@@ -104,12 +136,9 @@ It auto-detects grouped and normal (flat) table layouts.
104
136
  import { toChartData } from '@internetstiftelsen/charts/utils';
105
137
  import { XYChart } from '@internetstiftelsen/charts/xy-chart';
106
138
 
107
- const data = toChartData(
108
- '\t\tDaily\tWeekly\nAll users\tSegment A\t85%\t92%\n\tSegment B\t84%\t91%',
109
- {
110
- categoryKey: 'Category',
111
- },
112
- );
139
+ const data = toChartData('\t\tDaily\tWeekly\nAll users\tSegment A\t85%\t92%\n\tSegment B\t84%\t91%', {
140
+ categoryKey: 'Category',
141
+ });
113
142
 
114
143
  const chart = new XYChart({ data });
115
144
  chart.render('#chart-container');
@@ -118,16 +147,32 @@ chart.render('#chart-container');
118
147
  The parser supports JSON-escaped string payloads and grouped carry-forward row
119
148
  structure (blank first column on continuation rows).
120
149
 
150
+ Supported input shapes:
151
+
152
+ - Plain tab-delimited strings
153
+ - JSON-escaped string payloads
154
+
155
+ Auto-detection behavior:
156
+
157
+ - Grouped rows when a carry-forward group structure is present
158
+ - Flat rows when no grouped continuation rows are detected
159
+
160
+ Grouped parsing rules:
161
+
162
+ - Header row starts with two structural columns (`group`, `category`) before metrics
163
+ - Continuation rows leave the first column blank to inherit the previous group
164
+ - Blank separator rows are ignored
165
+
121
166
  ## Documentation
122
167
 
123
168
  - [Getting Started](./docs/getting-started.md) - Installation, Vanilla JS, React integration
124
169
  - [XYChart](./docs/xy-chart.md) - Line, area, and bar charts API
170
+ - [WordCloudChart](./docs/word-cloud-chart.md) - Word frequency visualization API
125
171
  - [DonutChart](./docs/donut-chart.md) - Donut/pie charts API
126
172
  - [PieChart](./docs/pie-chart.md) - Pie chart API
127
173
  - [GaugeChart](./docs/gauge-chart.md) - Gauge chart API
128
174
  - [Components](./docs/components.md) - Axes, Grid, Tooltip, Legend, Title
129
175
  - [Theming](./docs/theming.md) - Colors, fonts, and styling
130
- - [Advanced](./docs/advanced.md) - Scales, TypeScript, architecture, performance
131
176
 
132
177
  ## Browser Support
133
178
 
package/area.d.ts CHANGED
@@ -19,7 +19,6 @@ export declare class Area implements ChartComponent<AreaConfigBase> {
19
19
  constructor(config: AreaConfig);
20
20
  getExportConfig(): AreaConfigBase;
21
21
  createExportComponent(override?: Partial<AreaConfigBase>): ChartComponent;
22
- private getScaledPosition;
23
22
  private getStackValues;
24
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;
25
24
  private renderValueLabels;
package/area.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { area, curveBasis, curveCardinal, curveLinear, curveMonotoneX, curveNatural, curveStep, line, } from 'd3';
2
2
  import { mergeDeep, sanitizeForCSS } from './utils.js';
3
+ import { getScalePosition } from './scale-utils.js';
3
4
  const AREA_CURVE_FACTORIES = {
4
5
  linear: curveLinear,
5
6
  monotone: curveMonotoneX,
@@ -132,24 +133,6 @@ export class Area {
132
133
  exportHooks: this.exportHooks,
133
134
  });
134
135
  }
135
- getScaledPosition(data, key, scale, scaleType) {
136
- const value = data[key];
137
- let scaledValue;
138
- switch (scaleType) {
139
- case 'band':
140
- scaledValue = String(value);
141
- break;
142
- case 'time':
143
- scaledValue =
144
- value instanceof Date ? value : new Date(String(value));
145
- break;
146
- case 'linear':
147
- case 'log':
148
- scaledValue = typeof value === 'number' ? value : Number(value);
149
- break;
150
- }
151
- return scale(scaledValue) || 0;
152
- }
153
136
  getStackValues(dataPoint, xKey, parseValue, stackingContext) {
154
137
  const value = parseValue(dataPoint[this.dataKey]);
155
138
  if (!stackingContext || stackingContext.mode === 'none') {
@@ -176,7 +159,7 @@ export class Area {
176
159
  }
177
160
  render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme, stackingContext, valueLabelLayer) {
178
161
  const getXPosition = (d) => {
179
- const scaled = this.getScaledPosition(d.data, xKey, x, xScaleType);
162
+ const scaled = getScalePosition(x, d.data[xKey], xScaleType);
180
163
  return scaled + (x.bandwidth ? x.bandwidth() / 2 : 0);
181
164
  };
182
165
  const hasValidValue = (d) => {
package/bar.d.ts CHANGED
@@ -13,7 +13,6 @@ export declare class Bar implements ChartComponent<BarConfigBase> {
13
13
  constructor(config: BarConfig);
14
14
  getExportConfig(): BarConfigBase;
15
15
  createExportComponent(override?: Partial<BarConfigBase>): ChartComponent;
16
- private getScaledPosition;
17
16
  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): void;
18
17
  private renderVertical;
19
18
  private renderHorizontal;
package/bar.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { getContrastTextColor, sanitizeForCSS, mergeDeep } from './utils.js';
2
+ import { getScalePosition } from './scale-utils.js';
2
3
  const LABEL_INSET_DEFAULT = 4;
3
4
  const LABEL_INSET_STACKED = 6;
4
5
  const LABEL_MIN_PADDING_DEFAULT = 8;
@@ -88,24 +89,6 @@ export class Bar {
88
89
  exportHooks: this.exportHooks,
89
90
  });
90
91
  }
91
- getScaledPosition(data, key, scale, scaleType) {
92
- const value = data[key];
93
- let scaledValue;
94
- switch (scaleType) {
95
- case 'band':
96
- scaledValue = String(value);
97
- break;
98
- case 'time':
99
- scaledValue =
100
- value instanceof Date ? value : new Date(String(value));
101
- break;
102
- case 'linear':
103
- case 'log':
104
- scaledValue = typeof value === 'number' ? value : Number(value);
105
- break;
106
- }
107
- return scale(scaledValue) || 0;
108
- }
109
92
  render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme, stackingContext) {
110
93
  if (this.orientation === 'vertical') {
111
94
  this.renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType, stackingContext);
@@ -178,7 +161,7 @@ export class Bar {
178
161
  .attr('class', `bar-${sanitizedKey}`)
179
162
  .attr('data-index', (_, i) => i)
180
163
  .attr('x', (d) => {
181
- const xPos = this.getScaledPosition(d, xKey, x, xScaleType);
164
+ const xPos = getScalePosition(x, d[xKey], xScaleType);
182
165
  return xScaleType === 'band'
183
166
  ? xPos + barOffset
184
167
  : xPos - barWidth / 2;
@@ -301,7 +284,7 @@ export class Bar {
301
284
  }
302
285
  })
303
286
  .attr('y', (d) => {
304
- const yPos = this.getScaledPosition(d, xKey, y, yScaleType);
287
+ const yPos = getScalePosition(y, d[xKey], yScaleType);
305
288
  return yScaleType === 'band'
306
289
  ? yPos + barOffset
307
290
  : yPos - barHeight / 2;
@@ -388,7 +371,7 @@ export class Bar {
388
371
  const categoryKey = String(d[xKey]);
389
372
  const value = parseValue(d[this.dataKey]);
390
373
  const valueText = String(value);
391
- const xPos = this.getScaledPosition(d, xKey, x, xScaleType);
374
+ const xPos = getScalePosition(x, d[xKey], xScaleType);
392
375
  const barColor = this.colorAdapter
393
376
  ? this.colorAdapter(d, i)
394
377
  : this.fill;
@@ -572,7 +555,7 @@ export class Bar {
572
555
  const categoryKey = String(d[xKey]);
573
556
  const value = parseValue(d[this.dataKey]);
574
557
  const valueText = String(value);
575
- const yPos = this.getScaledPosition(d, xKey, y, yScaleType);
558
+ const yPos = getScalePosition(y, d[xKey], yScaleType);
576
559
  const barColor = this.colorAdapter
577
560
  ? this.colorAdapter(d, i)
578
561
  : this.fill;
package/base-chart.d.ts CHANGED
@@ -18,6 +18,34 @@ type ResponsiveOverrides = {
18
18
  theme?: DeepPartial<ChartTheme>;
19
19
  components: Map<ChartComponent, Record<string, unknown>>;
20
20
  };
21
+ type BaseLayoutComponentsOptions = {
22
+ title?: boolean;
23
+ xAxis?: boolean;
24
+ yAxis?: boolean;
25
+ inlineLegend?: boolean;
26
+ };
27
+ type BaseExportComponentsOptions = {
28
+ title?: boolean;
29
+ grid?: boolean;
30
+ xAxis?: boolean;
31
+ yAxis?: boolean;
32
+ tooltip?: boolean;
33
+ legend?: boolean;
34
+ };
35
+ export type BaseLayoutContext = {
36
+ svg: Selection<SVGSVGElement, undefined, null, undefined>;
37
+ svgNode: SVGSVGElement;
38
+ };
39
+ export type BaseRenderContext = BaseLayoutContext & {
40
+ plotGroup: Selection<SVGGElement, undefined, null, undefined>;
41
+ plotArea: PlotAreaBounds;
42
+ };
43
+ type ComponentSlot<TComponent extends ChartComponent = ChartComponent> = {
44
+ type: TComponent['type'];
45
+ get: () => TComponent | null;
46
+ set: (component: TComponent | null) => void;
47
+ onRegister?: (component: TComponent) => void;
48
+ };
21
49
  export type BaseChartConfig = {
22
50
  data: ChartData;
23
51
  theme?: Partial<ChartTheme>;
@@ -49,12 +77,14 @@ export declare abstract class BaseChart {
49
77
  protected resizeObserver: ResizeObserver | null;
50
78
  protected layoutManager: LayoutManager;
51
79
  protected plotArea: PlotAreaBounds | null;
80
+ private readyPromise;
52
81
  private disconnectedLegendContainer;
82
+ private renderThemeOverride;
53
83
  protected constructor(config: BaseChartConfig);
54
84
  /**
55
85
  * Adds a component (axis, grid, tooltip, etc.) to the chart
56
86
  */
57
- abstract addChild(component: ChartComponent): this;
87
+ addChild(component: ChartComponent): this;
58
88
  /**
59
89
  * Renders the chart to the specified target element
60
90
  */
@@ -71,13 +101,18 @@ export declare abstract class BaseChart {
71
101
  }): ResponsiveRenderContext;
72
102
  private resolveBreakpointName;
73
103
  private resolveRenderTheme;
74
- private applyThemeOverride;
104
+ private applyRenderTheme;
105
+ protected get renderTheme(): ChartTheme;
75
106
  /**
76
107
  * Get layout-aware components in order
77
108
  * Override in subclasses to provide chart-specific components
78
109
  */
79
110
  protected getLayoutComponents(): LayoutAwareComponent[];
111
+ protected getBaseLayoutComponents(options: BaseLayoutComponentsOptions): LayoutAwareComponent[];
80
112
  protected getExportComponents(): ChartComponent[];
113
+ protected getOverrideableComponents(): ChartComponent[];
114
+ protected getBaseExportComponents(options: BaseExportComponentsOptions): ChartComponent[];
115
+ protected registerBaseComponent(component: ChartComponent): boolean;
81
116
  protected collectExportOverrides(context: ExportRenderContext): Map<ChartComponent, Record<string, unknown>>;
82
117
  protected collectResponsiveOverrides(context: ResponsiveRenderContext): ResponsiveOverrides;
83
118
  protected runExportHooks(context: ExportHookContext): void;
@@ -85,7 +120,14 @@ export declare abstract class BaseChart {
85
120
  private createOverrideComponents;
86
121
  protected applyComponentOverrides(overrides: Map<ChartComponent, ChartComponent>): () => void;
87
122
  private renderExportChart;
88
- protected prepareLayout(): void;
123
+ protected renderTitle(svg: Selection<SVGSVGElement, undefined, null, undefined>): void;
124
+ protected renderInlineLegend(svg: Selection<SVGSVGElement, undefined, null, undefined>): void;
125
+ protected measureInlineLegend(svgNode: SVGSVGElement): void;
126
+ protected filterVisibleItems<T>(items: T[], getDataKey: (item: T) => string): T[];
127
+ protected validateSourceData(_data: ChartData): void;
128
+ protected syncDerivedState(_previousData?: DataItem[]): void;
129
+ protected initializeDataState(): void;
130
+ protected prepareLayout(context: BaseLayoutContext): void;
89
131
  /**
90
132
  * Setup ResizeObserver for automatic resize handling
91
133
  */
@@ -93,8 +135,10 @@ export declare abstract class BaseChart {
93
135
  /**
94
136
  * Subclasses must implement this method to define their rendering logic
95
137
  */
96
- protected abstract renderChart(): void;
138
+ protected abstract renderChart(context: BaseRenderContext): void;
97
139
  protected abstract createExportChart(): BaseChart;
140
+ protected setReadyPromise(promise: Promise<void>): void;
141
+ whenReady(): Promise<void>;
98
142
  protected getLegendSeries(): LegendSeries[];
99
143
  getLegendItems(): LegendItem[];
100
144
  isLegendSeriesVisible(dataKey: string): boolean;
@@ -114,6 +158,11 @@ export declare abstract class BaseChart {
114
158
  private resolveDisconnectedLegendHost;
115
159
  private cleanupDisconnectedLegendContainer;
116
160
  protected parseValue(value: unknown): number;
161
+ protected rerender(): void;
162
+ protected tryRegisterComponent(component: ChartComponent, slots: readonly ComponentSlot[]): boolean;
163
+ protected applySlotOverrides(overrides: Map<ChartComponent, ChartComponent>, slots: readonly ComponentSlot[]): () => void;
164
+ protected applyArrayComponentOverrides<TComponent extends ChartComponent>(components: TComponent[], overrides: Map<ChartComponent, ChartComponent>, isComponent: (component: ChartComponent) => component is TComponent): () => void;
165
+ private getBaseComponentSlots;
117
166
  /**
118
167
  * Exports the chart in the specified format
119
168
  * @param format - The export format
@@ -131,7 +180,7 @@ export declare abstract class BaseChart {
131
180
  private exportXLSX;
132
181
  private exportImage;
133
182
  private exportPDF;
134
- protected exportSVG(options?: ExportOptions, formatForHooks?: VisualExportFormat): string;
183
+ protected exportSVG(options?: ExportOptions, formatForHooks?: VisualExportFormat): Promise<string>;
135
184
  protected exportJSON(): string;
136
185
  }
137
186
  export {};