@internetstiftelsen/charts 0.9.0 → 0.9.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 (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/{area.d.ts → dist/area.d.ts} +1 -1
  4. package/{bar.d.ts → dist/bar.d.ts} +3 -4
  5. package/{bar.js → dist/bar.js} +3 -11
  6. package/{base-chart.d.ts → dist/base-chart.d.ts} +30 -18
  7. package/{base-chart.js → dist/base-chart.js} +170 -50
  8. package/dist/chart-interface.d.ts +19 -0
  9. package/{donut-center-content.d.ts → dist/donut-center-content.d.ts} +1 -1
  10. package/{donut-chart.d.ts → dist/donut-chart.d.ts} +19 -4
  11. package/{donut-chart.js → dist/donut-chart.js} +140 -2
  12. package/{gauge-chart.d.ts → dist/gauge-chart.d.ts} +2 -2
  13. package/{gauge-chart.js → dist/gauge-chart.js} +2 -0
  14. package/{grid.d.ts → dist/grid.d.ts} +1 -1
  15. package/{layout-manager.d.ts → dist/layout-manager.d.ts} +5 -5
  16. package/{legend.d.ts → dist/legend.d.ts} +3 -1
  17. package/{legend.js → dist/legend.js} +32 -0
  18. package/{line.d.ts → dist/line.d.ts} +1 -1
  19. package/{pie-chart.d.ts → dist/pie-chart.d.ts} +4 -11
  20. package/{pie-chart.js → dist/pie-chart.js} +23 -21
  21. package/{radial-chart-base.js → dist/radial-chart-base.js} +3 -1
  22. package/{theme.d.ts → dist/theme.d.ts} +2 -0
  23. package/{theme.js → dist/theme.js} +24 -29
  24. package/{title.d.ts → dist/title.d.ts} +1 -1
  25. package/{tooltip.d.ts → dist/tooltip.d.ts} +1 -1
  26. package/{tooltip.js → dist/tooltip.js} +239 -74
  27. package/{types.d.ts → dist/types.d.ts} +27 -10
  28. package/{utils.d.ts → dist/utils.d.ts} +0 -2
  29. package/{utils.js → dist/utils.js} +0 -5
  30. package/{word-cloud-chart.d.ts → dist/word-cloud-chart.d.ts} +1 -1
  31. package/{word-cloud-chart.js → dist/word-cloud-chart.js} +2 -0
  32. package/{x-axis.d.ts → dist/x-axis.d.ts} +2 -1
  33. package/{x-axis.js → dist/x-axis.js} +18 -14
  34. package/{xy-chart.d.ts → dist/xy-chart.d.ts} +8 -5
  35. package/{xy-chart.js → dist/xy-chart.js} +31 -5
  36. package/{y-axis.d.ts → dist/y-axis.d.ts} +1 -1
  37. package/{y-axis.js → dist/y-axis.js} +4 -4
  38. package/package.json +38 -36
  39. package/chart-interface.d.ts +0 -13
  40. /package/{area.js → dist/area.js} +0 -0
  41. /package/{chart-interface.js → dist/chart-interface.js} +0 -0
  42. /package/{donut-center-content.js → dist/donut-center-content.js} +0 -0
  43. /package/{export-image.d.ts → dist/export-image.d.ts} +0 -0
  44. /package/{export-image.js → dist/export-image.js} +0 -0
  45. /package/{export-pdf.d.ts → dist/export-pdf.d.ts} +0 -0
  46. /package/{export-pdf.js → dist/export-pdf.js} +0 -0
  47. /package/{export-tabular.d.ts → dist/export-tabular.d.ts} +0 -0
  48. /package/{export-tabular.js → dist/export-tabular.js} +0 -0
  49. /package/{export-xlsx.d.ts → dist/export-xlsx.d.ts} +0 -0
  50. /package/{export-xlsx.js → dist/export-xlsx.js} +0 -0
  51. /package/{grid.js → dist/grid.js} +0 -0
  52. /package/{grouped-data.d.ts → dist/grouped-data.d.ts} +0 -0
  53. /package/{grouped-data.js → dist/grouped-data.js} +0 -0
  54. /package/{grouped-tabular.d.ts → dist/grouped-tabular.d.ts} +0 -0
  55. /package/{grouped-tabular.js → dist/grouped-tabular.js} +0 -0
  56. /package/{layout-manager.js → dist/layout-manager.js} +0 -0
  57. /package/{line.js → dist/line.js} +0 -0
  58. /package/{radial-chart-base.d.ts → dist/radial-chart-base.d.ts} +0 -0
  59. /package/{scale-utils.d.ts → dist/scale-utils.d.ts} +0 -0
  60. /package/{scale-utils.js → dist/scale-utils.js} +0 -0
  61. /package/{title.js → dist/title.js} +0 -0
  62. /package/{types.js → dist/types.js} +0 -0
  63. /package/{validation.d.ts → dist/validation.d.ts} +0 -0
  64. /package/{validation.js → dist/validation.js} +0 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Internetstiftelsen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -7,9 +7,11 @@ A framework-agnostic, composable charting library built on D3.js with TypeScript
7
7
  - **Framework Agnostic** - Works with vanilla JS, React, Vue, Svelte, or any framework
8
8
  - **Composable Architecture** - Build charts by composing components
9
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
10
11
  - **Optional Gauge Animation** - Animate gauge value transitions with `gauge.animate`
11
12
  - **Stacking Control** - Bar stacking modes with optional reversed visual series order
12
13
  - **Flexible Scales** - Band, linear, time, and logarithmic scales
14
+ - **Explicit or Responsive Sizing** - Set top-level `width`/`height` or let the container drive size
13
15
  - **Auto Resize** - Built-in ResizeObserver handles responsive behavior
14
16
  - **Responsive Policy** - Chart-level container-query overrides for theme and components
15
17
  - **Type Safe** - Written in TypeScript with full type definitions
@@ -82,6 +84,48 @@ chart
82
84
  chart.render('#chart-container');
83
85
  ```
84
86
 
87
+ Use top-level `width` and `height` for fixed-size charts, or omit them to size
88
+ from the render container.
89
+
90
+ Theme overrides are deep-partial, so nested overrides like
91
+ `theme.axis.fontSize` preserve the rest of the theme defaults.
92
+
93
+ Responsive overrides are declarative and merge all matching breakpoints in
94
+ declaration order:
95
+
96
+ ```typescript
97
+ const chart = new XYChart({
98
+ data,
99
+ responsive: {
100
+ breakpoints: {
101
+ sm: {
102
+ maxWidth: 640,
103
+ theme: {
104
+ axis: {
105
+ fontSize: 11,
106
+ },
107
+ },
108
+ components: [
109
+ {
110
+ match: { type: 'xAxis' },
111
+ override: { display: false },
112
+ },
113
+ ],
114
+ },
115
+ md: {
116
+ minWidth: 641,
117
+ maxWidth: 768,
118
+ theme: {
119
+ axis: {
120
+ fontSize: 12,
121
+ },
122
+ },
123
+ },
124
+ },
125
+ },
126
+ });
127
+ ```
128
+
85
129
  ## Word Cloud
86
130
 
87
131
  ```javascript
@@ -18,7 +18,7 @@ export declare class Area implements ChartComponent<AreaConfigBase> {
18
18
  readonly exportHooks?: ExportHooks<AreaConfigBase>;
19
19
  constructor(config: AreaConfig);
20
20
  getExportConfig(): AreaConfigBase;
21
- createExportComponent(override?: Partial<AreaConfigBase>): ChartComponent;
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
24
  private renderValueLabels;
@@ -1,19 +1,18 @@
1
1
  import type { Selection } from 'd3';
2
- import type { BarConfig, BarStackingContext, BarValueLabelConfig, ChartTheme, D3Scale, DataItem, Orientation, ScaleType, ExportHooks, BarConfigBase } from './types.js';
2
+ import type { BarConfig, BarStackingContext, BarValueLabelConfig, ChartTheme, D3Scale, DataItem, 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";
6
6
  readonly dataKey: string;
7
7
  readonly fill: string;
8
8
  readonly colorAdapter?: (data: DataItem, index: number) => string;
9
- readonly orientation: Orientation;
10
9
  readonly maxBarSize?: number;
11
10
  readonly valueLabel?: BarValueLabelConfig;
12
11
  readonly exportHooks?: ExportHooks<BarConfigBase>;
13
12
  constructor(config: BarConfig);
14
13
  getExportConfig(): BarConfigBase;
15
- createExportComponent(override?: Partial<BarConfigBase>): ChartComponent;
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;
14
+ createExportComponent(override?: Partial<BarConfigBase>): ChartComponent<BarConfigBase>;
15
+ 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;
17
16
  private renderVertical;
18
17
  private renderHorizontal;
19
18
  private renderVerticalValueLabels;
@@ -40,12 +40,6 @@ export class Bar {
40
40
  writable: true,
41
41
  value: void 0
42
42
  });
43
- Object.defineProperty(this, "orientation", {
44
- enumerable: true,
45
- configurable: true,
46
- writable: true,
47
- value: void 0
48
- });
49
43
  Object.defineProperty(this, "maxBarSize", {
50
44
  enumerable: true,
51
45
  configurable: true,
@@ -67,7 +61,6 @@ export class Bar {
67
61
  this.dataKey = config.dataKey;
68
62
  this.fill = config.fill || '#8884d8';
69
63
  this.colorAdapter = config.colorAdapter;
70
- this.orientation = config.orientation || 'vertical';
71
64
  this.maxBarSize = config.maxBarSize;
72
65
  this.valueLabel = config.valueLabel;
73
66
  this.exportHooks = config.exportHooks;
@@ -77,7 +70,6 @@ export class Bar {
77
70
  dataKey: this.dataKey,
78
71
  fill: this.fill,
79
72
  colorAdapter: this.colorAdapter,
80
- orientation: this.orientation,
81
73
  maxBarSize: this.maxBarSize,
82
74
  valueLabel: this.valueLabel,
83
75
  };
@@ -89,8 +81,8 @@ export class Bar {
89
81
  exportHooks: this.exportHooks,
90
82
  });
91
83
  }
92
- render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme, stackingContext) {
93
- if (this.orientation === 'vertical') {
84
+ render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme, stackingContext, orientation = 'vertical') {
85
+ if (orientation === 'vertical') {
94
86
  this.renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType, stackingContext);
95
87
  }
96
88
  else {
@@ -98,7 +90,7 @@ export class Bar {
98
90
  }
99
91
  // Render value labels if enabled
100
92
  if (this.valueLabel?.show && theme) {
101
- if (this.orientation === 'vertical') {
93
+ if (orientation === 'vertical') {
102
94
  this.renderVerticalValueLabels(plotGroup, data, xKey, x, y, parseValue, xScaleType, theme, stackingContext);
103
95
  }
104
96
  else {
@@ -1,6 +1,6 @@
1
1
  import { type Selection } from 'd3';
2
- import type { ChartData, DataItem, ChartTheme, AxisScaleConfig, ExportFormat, ExportOptions, D3Scale, ExportHookContext, ExportRenderContext, LegendItem, LegendSeries, ResponsiveConfig, ResponsiveRenderContext, DeepPartial } from './types.js';
3
- import type { ChartComponent, LayoutAwareComponent } from './chart-interface.js';
2
+ import type { ChartData, DataItem, ChartTheme, ResolvedChartTheme, AxisScaleConfig, ExportFormat, ExportOptions, D3Scale, ExportHookContext, ExportRenderContext, LegendItem, LegendSeries, ResponsiveConfig, ResponsiveRenderContext, DeepPartial } from './types.js';
3
+ import type { ChartComponentBase, LayoutAwareComponentBase } from './chart-interface.js';
4
4
  import type { XAxis } from './x-axis.js';
5
5
  import type { YAxis } from './y-axis.js';
6
6
  import type { Grid } from './grid.js';
@@ -12,11 +12,12 @@ type VisualExportFormat = 'svg' | 'png' | 'jpg' | 'pdf';
12
12
  type RenderDimensions = {
13
13
  width: number;
14
14
  height: number;
15
+ svgWidthAttr: number | string;
15
16
  svgHeightAttr: number | string;
16
17
  };
17
18
  type ResponsiveOverrides = {
18
19
  theme?: DeepPartial<ChartTheme>;
19
- components: Map<ChartComponent, Record<string, unknown>>;
20
+ components: Map<ChartComponentBase, Record<string, unknown>>;
20
21
  };
21
22
  type BaseLayoutComponentsOptions = {
22
23
  title?: boolean;
@@ -40,7 +41,7 @@ export type BaseRenderContext = BaseLayoutContext & {
40
41
  plotGroup: Selection<SVGGElement, undefined, null, undefined>;
41
42
  plotArea: PlotAreaBounds;
42
43
  };
43
- type ComponentSlot<TComponent extends ChartComponent = ChartComponent> = {
44
+ type ComponentSlot<TComponent extends ChartComponentBase = ChartComponentBase> = {
44
45
  type: TComponent['type'];
45
46
  get: () => TComponent | null;
46
47
  set: (component: TComponent | null) => void;
@@ -48,7 +49,9 @@ type ComponentSlot<TComponent extends ChartComponent = ChartComponent> = {
48
49
  };
49
50
  export type BaseChartConfig = {
50
51
  data: ChartData;
51
- theme?: Partial<ChartTheme>;
52
+ width?: number;
53
+ height?: number;
54
+ theme?: DeepPartial<ChartTheme>;
52
55
  scales?: AxisScaleConfig;
53
56
  responsive?: ResponsiveConfig;
54
57
  };
@@ -58,6 +61,8 @@ export type BaseChartConfig = {
58
61
  export declare abstract class BaseChart {
59
62
  protected data: DataItem[];
60
63
  protected sourceData: ChartData;
64
+ protected readonly configuredWidth?: number;
65
+ protected readonly configuredHeight?: number;
61
66
  protected readonly theme: ChartTheme;
62
67
  protected readonly scaleConfig: AxisScaleConfig;
63
68
  protected readonly responsiveConfig?: ResponsiveConfig;
@@ -80,11 +85,12 @@ export declare abstract class BaseChart {
80
85
  private readyPromise;
81
86
  private disconnectedLegendContainer;
82
87
  private renderThemeOverride;
88
+ private renderSizeOverride;
83
89
  protected constructor(config: BaseChartConfig);
84
90
  /**
85
91
  * Adds a component (axis, grid, tooltip, etc.) to the chart
86
92
  */
87
- addChild(component: ChartComponent): this;
93
+ addChild(component: ChartComponentBase): this;
88
94
  /**
89
95
  * Renders the chart to the specified target element
90
96
  */
@@ -95,30 +101,36 @@ export declare abstract class BaseChart {
95
101
  */
96
102
  private performRender;
97
103
  protected resolveRenderDimensions(containerRect: DOMRect): RenderDimensions;
104
+ private resolveAccessibleLabel;
105
+ private syncAccessibleLabelFromSvg;
98
106
  protected resolveResponsiveContext(context: {
99
107
  width: number;
100
108
  height: number;
101
109
  }): ResponsiveRenderContext;
102
- private resolveBreakpointName;
110
+ private resolveActiveBreakpoints;
103
111
  private resolveRenderTheme;
104
112
  private applyRenderTheme;
105
113
  protected get renderTheme(): ChartTheme;
114
+ protected get resolvedRenderTheme(): ResolvedChartTheme;
106
115
  /**
107
116
  * Get layout-aware components in order
108
117
  * Override in subclasses to provide chart-specific components
109
118
  */
110
- protected getLayoutComponents(): LayoutAwareComponent[];
111
- protected getBaseLayoutComponents(options: BaseLayoutComponentsOptions): LayoutAwareComponent[];
112
- protected getExportComponents(): ChartComponent[];
113
- protected getOverrideableComponents(): ChartComponent[];
114
- protected getBaseExportComponents(options: BaseExportComponentsOptions): ChartComponent[];
115
- protected registerBaseComponent(component: ChartComponent): boolean;
116
- protected collectExportOverrides(context: ExportRenderContext): Map<ChartComponent, Record<string, unknown>>;
119
+ protected getLayoutComponents(): LayoutAwareComponentBase[];
120
+ protected getBaseLayoutComponents(options: BaseLayoutComponentsOptions): LayoutAwareComponentBase[];
121
+ protected getExportComponents(): ChartComponentBase[];
122
+ protected getOverrideableComponents(): ChartComponentBase[];
123
+ protected getBaseExportComponents(options: BaseExportComponentsOptions): ChartComponentBase[];
124
+ protected registerBaseComponent(component: ChartComponentBase): boolean;
125
+ protected collectExportOverrides(context: ExportRenderContext): Map<ChartComponentBase, Record<string, unknown>>;
117
126
  protected collectResponsiveOverrides(context: ResponsiveRenderContext): ResponsiveOverrides;
127
+ private collectResponsiveBreakpointOverrides;
128
+ private createResponsiveComponentSnapshots;
129
+ private applyResponsiveComponentOverrideEntries;
118
130
  protected runExportHooks(context: ExportHookContext): void;
119
131
  private mergeComponentOverrideMaps;
120
132
  private createOverrideComponents;
121
- protected applyComponentOverrides(overrides: Map<ChartComponent, ChartComponent>): () => void;
133
+ protected applyComponentOverrides(overrides: Map<ChartComponentBase, ChartComponentBase>): () => void;
122
134
  private renderExportChart;
123
135
  protected renderTitle(svg: Selection<SVGSVGElement, undefined, null, undefined>): void;
124
136
  protected renderInlineLegend(svg: Selection<SVGSVGElement, undefined, null, undefined>): void;
@@ -159,9 +171,9 @@ export declare abstract class BaseChart {
159
171
  private cleanupDisconnectedLegendContainer;
160
172
  protected parseValue(value: unknown): number;
161
173
  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;
174
+ protected tryRegisterComponent(component: ChartComponentBase, slots: readonly ComponentSlot[]): boolean;
175
+ protected applySlotOverrides(overrides: Map<ChartComponentBase, ChartComponentBase>, slots: readonly ComponentSlot[]): () => void;
176
+ protected applyArrayComponentOverrides<TComponent extends ChartComponentBase>(components: TComponent[], overrides: Map<ChartComponentBase, ChartComponentBase>, isComponent: (component: ChartComponentBase) => component is TComponent): () => void;
165
177
  private getBaseComponentSlots;
166
178
  /**
167
179
  * Exports the chart in the specified format
@@ -1,5 +1,5 @@
1
1
  import { create } from 'd3';
2
- import { defaultTheme } from './theme.js';
2
+ import { defaultTheme, DEFAULT_CHART_HEIGHT, DEFAULT_CHART_WIDTH, } from './theme.js';
3
3
  import { ChartValidator } from './validation.js';
4
4
  import { LayoutManager } from './layout-manager.js';
5
5
  import { serializeCSV } from './export-tabular.js';
@@ -8,6 +8,37 @@ import { exportXLSXBlob } from './export-xlsx.js';
8
8
  import { exportPDFBlob } from './export-pdf.js';
9
9
  import { normalizeChartData } from './grouped-data.js';
10
10
  import { mergeDeep } from './utils.js';
11
+ function normalizeChartDimension(value, name) {
12
+ if (value === undefined) {
13
+ return undefined;
14
+ }
15
+ if (!Number.isFinite(value) || value <= 0) {
16
+ throw new Error(`Chart ${name} must be a positive finite number`);
17
+ }
18
+ return value;
19
+ }
20
+ function normalizeResponsiveBreakpoint(definition) {
21
+ if (typeof definition === 'number') {
22
+ return Number.isFinite(definition) ? { minWidth: definition } : null;
23
+ }
24
+ if (!definition || typeof definition !== 'object') {
25
+ return null;
26
+ }
27
+ const minWidth = Number.isFinite(definition.minWidth)
28
+ ? definition.minWidth
29
+ : undefined;
30
+ const maxWidth = Number.isFinite(definition.maxWidth)
31
+ ? definition.maxWidth
32
+ : undefined;
33
+ if (minWidth === undefined && maxWidth === undefined) {
34
+ return null;
35
+ }
36
+ return {
37
+ ...definition,
38
+ minWidth,
39
+ maxWidth,
40
+ };
41
+ }
11
42
  /**
12
43
  * Base chart class that provides common functionality for all chart types
13
44
  */
@@ -25,6 +56,18 @@ export class BaseChart {
25
56
  writable: true,
26
57
  value: void 0
27
58
  });
59
+ Object.defineProperty(this, "configuredWidth", {
60
+ enumerable: true,
61
+ configurable: true,
62
+ writable: true,
63
+ value: void 0
64
+ });
65
+ Object.defineProperty(this, "configuredHeight", {
66
+ enumerable: true,
67
+ configurable: true,
68
+ writable: true,
69
+ value: void 0
70
+ });
28
71
  Object.defineProperty(this, "theme", {
29
72
  enumerable: true,
30
73
  configurable: true,
@@ -157,16 +200,24 @@ export class BaseChart {
157
200
  writable: true,
158
201
  value: null
159
202
  });
203
+ Object.defineProperty(this, "renderSizeOverride", {
204
+ enumerable: true,
205
+ configurable: true,
206
+ writable: true,
207
+ value: null
208
+ });
160
209
  const normalized = normalizeChartData(config.data);
161
210
  ChartValidator.validateData(normalized.data);
162
211
  this.sourceData = config.data;
163
212
  this.data = normalized.data;
164
- this.theme = { ...defaultTheme, ...config.theme };
165
- this.width = this.theme.width;
166
- this.height = this.theme.height;
213
+ this.configuredWidth = normalizeChartDimension(config.width, 'width');
214
+ this.configuredHeight = normalizeChartDimension(config.height, 'height');
215
+ this.theme = mergeDeep(defaultTheme, config.theme);
216
+ this.width = this.configuredWidth ?? DEFAULT_CHART_WIDTH;
217
+ this.height = this.configuredHeight ?? DEFAULT_CHART_HEIGHT;
167
218
  this.scaleConfig = config.scales || {};
168
219
  this.responsiveConfig = config.responsive;
169
- this.layoutManager = new LayoutManager(this.theme);
220
+ this.layoutManager = new LayoutManager(this.resolvedRenderTheme);
170
221
  }
171
222
  /**
172
223
  * Adds a component (axis, grid, tooltip, etc.) to the chart
@@ -228,8 +279,10 @@ export class BaseChart {
228
279
  // Clear and setup SVG
229
280
  this.container.innerHTML = '';
230
281
  this.svg = create('svg')
231
- .attr('width', '100%')
282
+ .attr('width', dimensions.svgWidthAttr)
232
283
  .attr('height', dimensions.svgHeightAttr)
284
+ .attr('role', 'img')
285
+ .attr('aria-label', this.resolveAccessibleLabel())
233
286
  .style('display', 'block');
234
287
  this.container.appendChild(this.svg.node());
235
288
  const svgNode = this.svg.node();
@@ -241,12 +294,7 @@ export class BaseChart {
241
294
  svgNode,
242
295
  });
243
296
  // Calculate layout
244
- const layoutTheme = {
245
- ...this.renderTheme,
246
- width: this.width,
247
- height: this.height,
248
- };
249
- this.layoutManager = new LayoutManager(layoutTheme);
297
+ this.layoutManager = new LayoutManager(this.resolvedRenderTheme);
250
298
  const components = this.getLayoutComponents();
251
299
  const plotArea = this.layoutManager.calculateLayout(components);
252
300
  this.plotArea = plotArea;
@@ -268,35 +316,65 @@ export class BaseChart {
268
316
  }
269
317
  }
270
318
  resolveRenderDimensions(containerRect) {
271
- const width = containerRect.width || this.theme.width;
272
- const height = containerRect.height || this.theme.height;
319
+ const width = this.renderSizeOverride?.width ??
320
+ this.configuredWidth ??
321
+ (containerRect.width || DEFAULT_CHART_WIDTH);
322
+ const height = this.renderSizeOverride?.height ??
323
+ this.configuredHeight ??
324
+ (containerRect.height || DEFAULT_CHART_HEIGHT);
273
325
  return {
274
326
  width,
275
327
  height,
276
- svgHeightAttr: '100%',
328
+ svgWidthAttr: this.renderSizeOverride?.width ??
329
+ this.configuredWidth ??
330
+ '100%',
331
+ svgHeightAttr: this.renderSizeOverride?.height ??
332
+ this.configuredHeight ??
333
+ '100%',
277
334
  };
278
335
  }
336
+ resolveAccessibleLabel() {
337
+ const titleText = this.title?.text.trim();
338
+ if (titleText) {
339
+ return titleText;
340
+ }
341
+ return 'Chart';
342
+ }
343
+ syncAccessibleLabelFromSvg(svg) {
344
+ const titleText = svg.querySelector('.title text')?.textContent?.trim();
345
+ if (titleText) {
346
+ svg.setAttribute('aria-label', titleText);
347
+ return;
348
+ }
349
+ svg.setAttribute('aria-label', this.resolveAccessibleLabel());
350
+ }
279
351
  resolveResponsiveContext(context) {
352
+ const activeBreakpoints = this.resolveActiveBreakpoints(context.width).map(({ name }) => name);
280
353
  return {
281
354
  ...context,
282
- breakpoint: this.resolveBreakpointName(context.width),
355
+ activeBreakpoints,
356
+ breakpoint: activeBreakpoints.length > 0
357
+ ? activeBreakpoints[activeBreakpoints.length - 1]
358
+ : null,
283
359
  };
284
360
  }
285
- resolveBreakpointName(width) {
361
+ resolveActiveBreakpoints(width) {
286
362
  const breakpoints = this.responsiveConfig?.breakpoints;
287
363
  if (!breakpoints) {
288
- return null;
364
+ return [];
289
365
  }
290
- const sorted = Object.entries(breakpoints)
291
- .filter(([, minWidth]) => Number.isFinite(minWidth))
292
- .sort((a, b) => a[1] - b[1]);
293
- let active = null;
294
- sorted.forEach(([name, minWidth]) => {
295
- if (width >= minWidth) {
296
- active = name;
366
+ return Object.entries(breakpoints).flatMap(([name, definition]) => {
367
+ const config = normalizeResponsiveBreakpoint(definition);
368
+ if (!config) {
369
+ return [];
297
370
  }
371
+ const matchesMinWidth = config.minWidth === undefined || width >= config.minWidth;
372
+ const matchesMaxWidth = config.maxWidth === undefined || width <= config.maxWidth;
373
+ if (!matchesMinWidth || !matchesMaxWidth) {
374
+ return [];
375
+ }
376
+ return [{ name, config }];
298
377
  });
299
- return active;
300
378
  }
301
379
  resolveRenderTheme(responsiveOverrides) {
302
380
  if (!responsiveOverrides.theme) {
@@ -316,6 +394,13 @@ export class BaseChart {
316
394
  get renderTheme() {
317
395
  return this.renderThemeOverride ?? this.theme;
318
396
  }
397
+ get resolvedRenderTheme() {
398
+ return {
399
+ ...this.renderTheme,
400
+ width: this.width,
401
+ height: this.height,
402
+ };
403
+ }
319
404
  /**
320
405
  * Get layout-aware components in order
321
406
  * Override in subclasses to provide chart-specific components
@@ -388,7 +473,7 @@ export class BaseChart {
388
473
  components.forEach((component) => {
389
474
  const exportable = component;
390
475
  const currentConfig = exportable.getExportConfig?.() ?? {};
391
- const result = component.exportHooks?.beforeRender?.call(component, context, currentConfig);
476
+ const result = exportable.exportHooks?.beforeRender?.call(component, context, currentConfig);
392
477
  if (result &&
393
478
  typeof result === 'object' &&
394
479
  exportable.createExportComponent) {
@@ -400,33 +485,65 @@ export class BaseChart {
400
485
  collectResponsiveOverrides(context) {
401
486
  const beforeRender = this.responsiveConfig?.beforeRender;
402
487
  const components = this.getOverrideableComponents();
403
- const componentOverrides = new Map();
488
+ const breakpointOverrides = this.collectResponsiveBreakpointOverrides(context, components);
404
489
  if (!beforeRender) {
405
- return {
406
- components: componentOverrides,
407
- };
490
+ return breakpointOverrides;
408
491
  }
409
- const snapshots = components.map((component, index) => {
492
+ const effectiveTheme = breakpointOverrides.theme
493
+ ? mergeDeep(this.theme, breakpointOverrides.theme)
494
+ : this.theme;
495
+ const snapshots = this.createResponsiveComponentSnapshots(components, breakpointOverrides.components);
496
+ const result = beforeRender(context, {
497
+ theme: effectiveTheme,
498
+ components: snapshots,
499
+ });
500
+ if (!result || typeof result !== 'object') {
501
+ return breakpointOverrides;
502
+ }
503
+ const componentOverrides = this.mergeComponentOverrideMaps(breakpointOverrides.components);
504
+ this.applyResponsiveComponentOverrideEntries(result.components, components, componentOverrides);
505
+ return {
506
+ theme: breakpointOverrides.theme
507
+ ? mergeDeep(breakpointOverrides.theme, result.theme)
508
+ : result.theme,
509
+ components: componentOverrides,
510
+ };
511
+ }
512
+ collectResponsiveBreakpointOverrides(context, components) {
513
+ const matchedBreakpoints = this.resolveActiveBreakpoints(context.width);
514
+ const componentOverrides = new Map();
515
+ let themeOverride;
516
+ matchedBreakpoints.forEach(({ config }) => {
517
+ if (config.theme) {
518
+ themeOverride = themeOverride
519
+ ? mergeDeep(themeOverride, config.theme)
520
+ : config.theme;
521
+ }
522
+ this.applyResponsiveComponentOverrideEntries(config.components, components, componentOverrides);
523
+ });
524
+ return {
525
+ theme: themeOverride,
526
+ components: componentOverrides,
527
+ };
528
+ }
529
+ createResponsiveComponentSnapshots(components, overrides) {
530
+ return components.map((component, index) => {
410
531
  const exportable = component;
411
532
  const currentConfig = exportable.getExportConfig?.() ?? {};
412
533
  const dataKey = component.dataKey;
534
+ const override = overrides.get(component);
413
535
  return {
414
536
  index,
415
537
  type: component.type,
416
538
  dataKey: typeof dataKey === 'string' ? dataKey : undefined,
417
- currentConfig,
539
+ currentConfig: override
540
+ ? mergeDeep(currentConfig, override)
541
+ : currentConfig,
418
542
  };
419
543
  });
420
- const result = beforeRender(context, {
421
- theme: this.theme,
422
- components: snapshots,
423
- });
424
- if (!result || typeof result !== 'object') {
425
- return {
426
- components: componentOverrides,
427
- };
428
- }
429
- result.components?.forEach((entry) => {
544
+ }
545
+ applyResponsiveComponentOverrideEntries(entries, components, overrides) {
546
+ entries?.forEach((entry) => {
430
547
  const match = entry.match ?? {};
431
548
  const override = entry.override;
432
549
  if (!override || typeof override !== 'object') {
@@ -441,19 +558,16 @@ export class BaseChart {
441
558
  if (!matchesIndex || !matchesType || !matchesDataKey) {
442
559
  return;
443
560
  }
444
- const existing = componentOverrides.get(component);
445
- componentOverrides.set(component, existing ? mergeDeep(existing, override) : { ...override });
561
+ const existing = overrides.get(component);
562
+ overrides.set(component, existing ? mergeDeep(existing, override) : { ...override });
446
563
  });
447
564
  });
448
- return {
449
- theme: result.theme,
450
- components: componentOverrides,
451
- };
452
565
  }
453
566
  runExportHooks(context) {
454
567
  const components = this.getExportComponents();
455
568
  components.forEach((component) => {
456
- component.exportHooks?.before?.call(component, context);
569
+ const exportable = component;
570
+ exportable.exportHooks?.before?.call(component, context);
457
571
  });
458
572
  }
459
573
  mergeComponentOverrideMaps(...maps) {
@@ -964,9 +1078,14 @@ export class BaseChart {
964
1078
  ...baseContext,
965
1079
  svg: clone,
966
1080
  });
1081
+ this.syncAccessibleLabelFromSvg(clone);
967
1082
  return clone.outerHTML;
968
1083
  }
969
1084
  const exportChart = this.createExportChart();
1085
+ exportChart.renderSizeOverride = {
1086
+ width: exportWidth,
1087
+ height: exportHeight,
1088
+ };
970
1089
  const components = this.getExportComponents();
971
1090
  components.forEach((component) => {
972
1091
  const exportable = component;
@@ -986,6 +1105,7 @@ export class BaseChart {
986
1105
  ...baseContext,
987
1106
  svg: exportSvg,
988
1107
  });
1108
+ this.syncAccessibleLabelFromSvg(exportSvg);
989
1109
  return exportSvg.outerHTML;
990
1110
  }
991
1111
  exportJSON() {
@@ -0,0 +1,19 @@
1
+ import type { ExportHooks } from './types.js';
2
+ export type ChartComponentType = 'line' | 'area' | 'bar' | 'xAxis' | 'yAxis' | 'grid' | 'tooltip' | 'legend' | 'title' | 'donutCenterContent';
3
+ export interface ChartComponentBase {
4
+ type: ChartComponentType;
5
+ }
6
+ export interface ChartComponent<TConfig = unknown> extends ChartComponentBase {
7
+ type: ChartComponentType;
8
+ exportHooks?: ExportHooks<TConfig>;
9
+ }
10
+ export type ComponentSpace = {
11
+ width: number;
12
+ height: number;
13
+ position: 'top' | 'bottom' | 'left' | 'right';
14
+ };
15
+ export interface LayoutAwareComponentBase extends ChartComponentBase {
16
+ getRequiredSpace(): ComponentSpace;
17
+ }
18
+ export interface LayoutAwareComponent<TConfig = unknown> extends ChartComponent<TConfig>, LayoutAwareComponentBase {
19
+ }
@@ -27,7 +27,7 @@ export declare class DonutCenterContent implements ChartComponent<DonutCenterCon
27
27
  private readonly config;
28
28
  constructor(config?: DonutCenterContentConfig);
29
29
  getExportConfig(): DonutCenterContentConfigBase;
30
- createExportComponent(override?: Partial<DonutCenterContentConfigBase>): ChartComponent;
30
+ createExportComponent(override?: Partial<DonutCenterContentConfigBase>): ChartComponent<DonutCenterContentConfigBase>;
31
31
  render(svg: Selection<SVGSVGElement, undefined, null, undefined>, cx: number, cy: number, theme: ChartTheme, fontScale?: number): void;
32
32
  }
33
33
  export {};