@internetstiftelsen/charts 0.3.2 → 0.4.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/bar.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Selection } from 'd3';
2
- import type { BarConfig, BarStackingContext, BarValueLabelConfig, ChartTheme, D3Scale, DataItem, Orientation, ScaleType } from './types.js';
2
+ import type { BarConfig, BarStackingContext, BarValueLabelConfig, ChartTheme, D3Scale, DataItem, Orientation, ScaleType, ExportHooks, BarConfigBase } from './types.js';
3
3
  import type { ChartComponent } from './chart-interface.js';
4
- export declare class Bar implements ChartComponent {
4
+ export declare class Bar implements ChartComponent<BarConfigBase> {
5
5
  readonly type: "bar";
6
6
  readonly dataKey: string;
7
7
  readonly fill: string;
@@ -9,7 +9,10 @@ export declare class Bar implements ChartComponent {
9
9
  readonly orientation: Orientation;
10
10
  readonly maxBarSize?: number;
11
11
  readonly valueLabel?: BarValueLabelConfig;
12
+ readonly exportHooks?: ExportHooks<BarConfigBase>;
12
13
  constructor(config: BarConfig);
14
+ getExportConfig(): BarConfigBase;
15
+ createExportComponent(override?: Partial<BarConfigBase>): ChartComponent;
13
16
  private getScaledPosition;
14
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): void;
15
18
  private renderVertical;
package/bar.js CHANGED
@@ -1,4 +1,4 @@
1
- import { sanitizeForCSS } from './utils.js';
1
+ import { sanitizeForCSS, mergeDeep } from './utils.js';
2
2
  export class Bar {
3
3
  constructor(config) {
4
4
  Object.defineProperty(this, "type", {
@@ -43,12 +43,36 @@ export class Bar {
43
43
  writable: true,
44
44
  value: void 0
45
45
  });
46
+ Object.defineProperty(this, "exportHooks", {
47
+ enumerable: true,
48
+ configurable: true,
49
+ writable: true,
50
+ value: void 0
51
+ });
46
52
  this.dataKey = config.dataKey;
47
53
  this.fill = config.fill || '#8884d8';
48
54
  this.colorAdapter = config.colorAdapter;
49
55
  this.orientation = config.orientation || 'vertical';
50
56
  this.maxBarSize = config.maxBarSize;
51
57
  this.valueLabel = config.valueLabel;
58
+ this.exportHooks = config.exportHooks;
59
+ }
60
+ getExportConfig() {
61
+ return {
62
+ dataKey: this.dataKey,
63
+ fill: this.fill,
64
+ colorAdapter: this.colorAdapter,
65
+ orientation: this.orientation,
66
+ maxBarSize: this.maxBarSize,
67
+ valueLabel: this.valueLabel,
68
+ };
69
+ }
70
+ createExportComponent(override) {
71
+ const merged = mergeDeep(this.getExportConfig(), override);
72
+ return new Bar({
73
+ ...merged,
74
+ exportHooks: this.exportHooks,
75
+ });
52
76
  }
53
77
  getScaledPosition(data, key, scale, scaleType) {
54
78
  const value = data[key];
package/base-chart.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Selection } from 'd3';
2
- import type { DataItem, ChartTheme, AxisScaleConfig, ExportFormat, ExportOptions, D3Scale } from './types.js';
2
+ import type { DataItem, ChartTheme, AxisScaleConfig, ExportFormat, ExportOptions, D3Scale, ExportHookContext, ExportRenderContext } from './types.js';
3
3
  import type { ChartComponent, LayoutAwareComponent } from './chart-interface.js';
4
4
  import type { XAxis } from './x-axis.js';
5
5
  import type { YAxis } from './y-axis.js';
@@ -54,6 +54,11 @@ export declare abstract class BaseChart {
54
54
  * Override in subclasses to provide chart-specific components
55
55
  */
56
56
  protected getLayoutComponents(): LayoutAwareComponent[];
57
+ protected getExportComponents(): ChartComponent[];
58
+ protected collectExportOverrides(context: ExportRenderContext): Map<ChartComponent, Record<string, unknown>>;
59
+ protected runExportHooks(context: ExportHookContext): void;
60
+ private renderExportChart;
61
+ protected prepareLayout(): void;
57
62
  /**
58
63
  * Setup ResizeObserver for automatic resize handling
59
64
  */
@@ -62,6 +67,7 @@ export declare abstract class BaseChart {
62
67
  * Subclasses must implement this method to define their rendering logic
63
68
  */
64
69
  protected abstract renderChart(): void;
70
+ protected abstract createExportChart(): BaseChart;
65
71
  /**
66
72
  * Updates the chart with new data
67
73
  */
@@ -82,6 +88,6 @@ export declare abstract class BaseChart {
82
88
  * Downloads the exported content as a file
83
89
  */
84
90
  private downloadContent;
85
- protected exportSVG(): string;
91
+ protected exportSVG(options?: ExportOptions): string;
86
92
  protected exportJSON(): string;
87
93
  }
package/base-chart.js CHANGED
@@ -164,6 +164,7 @@ export class BaseChart {
164
164
  .attr('height', this.height)
165
165
  .style('display', 'block');
166
166
  this.container.appendChild(this.svg.node());
167
+ this.prepareLayout();
167
168
  // Calculate layout
168
169
  const layoutTheme = {
169
170
  ...this.theme,
@@ -184,16 +185,90 @@ export class BaseChart {
184
185
  */
185
186
  getLayoutComponents() {
186
187
  const components = [];
187
- if (this.title)
188
+ if (this.title) {
188
189
  components.push(this.title);
189
- if (this.xAxis)
190
+ }
191
+ if (this.xAxis) {
190
192
  components.push(this.xAxis);
191
- if (this.yAxis)
193
+ }
194
+ if (this.yAxis) {
192
195
  components.push(this.yAxis);
193
- if (this.legend)
196
+ }
197
+ if (this.legend) {
194
198
  components.push(this.legend);
199
+ }
195
200
  return components;
196
201
  }
202
+ getExportComponents() {
203
+ const components = [];
204
+ if (this.title) {
205
+ components.push(this.title);
206
+ }
207
+ if (this.grid) {
208
+ components.push(this.grid);
209
+ }
210
+ if (this.xAxis) {
211
+ components.push(this.xAxis);
212
+ }
213
+ if (this.yAxis) {
214
+ components.push(this.yAxis);
215
+ }
216
+ if (this.tooltip) {
217
+ components.push(this.tooltip);
218
+ }
219
+ if (this.legend) {
220
+ components.push(this.legend);
221
+ }
222
+ return components;
223
+ }
224
+ collectExportOverrides(context) {
225
+ const overrides = new Map();
226
+ const components = this.getExportComponents();
227
+ components.forEach((component) => {
228
+ const exportable = component;
229
+ const currentConfig = exportable.getExportConfig?.() ?? {};
230
+ const result = component.exportHooks?.beforeRender?.call(component, context, currentConfig);
231
+ if (result &&
232
+ typeof result === 'object' &&
233
+ exportable.createExportComponent) {
234
+ overrides.set(component, result);
235
+ }
236
+ });
237
+ return overrides;
238
+ }
239
+ runExportHooks(context) {
240
+ const components = this.getExportComponents();
241
+ components.forEach((component) => {
242
+ component.exportHooks?.before?.call(component, context);
243
+ });
244
+ }
245
+ renderExportChart(chart, width, height) {
246
+ const container = document.createElement('div');
247
+ const containerId = `chart-export-${Math.random()
248
+ .toString(36)
249
+ .slice(2)}`;
250
+ container.id = containerId;
251
+ container.style.position = 'absolute';
252
+ container.style.left = '-99999px';
253
+ container.style.top = '0';
254
+ container.style.width = `${width}px`;
255
+ container.style.height = `${height}px`;
256
+ container.style.visibility = 'hidden';
257
+ document.body.appendChild(container);
258
+ chart.render(`#${containerId}`);
259
+ const svg = chart.svg?.node();
260
+ if (!svg) {
261
+ chart.destroy();
262
+ document.body.removeChild(container);
263
+ throw new Error('Failed to render export SVG');
264
+ }
265
+ chart.destroy();
266
+ document.body.removeChild(container);
267
+ return svg;
268
+ }
269
+ // Hook for subclasses to update component layout estimates before layout calc
270
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
271
+ prepareLayout() { }
197
272
  /**
198
273
  * Setup ResizeObserver for automatic resize handling
199
274
  */
@@ -251,7 +326,7 @@ export class BaseChart {
251
326
  * @returns The exported content as a string if download is false/undefined, void if download is true
252
327
  */
253
328
  export(format, options) {
254
- const content = format === 'svg' ? this.exportSVG() : this.exportJSON();
329
+ const content = format === 'svg' ? this.exportSVG(options) : this.exportJSON();
255
330
  if (options?.download) {
256
331
  this.downloadContent(content, format, options);
257
332
  return;
@@ -273,15 +348,52 @@ export class BaseChart {
273
348
  document.body.removeChild(link);
274
349
  URL.revokeObjectURL(url);
275
350
  }
276
- exportSVG() {
351
+ exportSVG(options) {
277
352
  if (!this.svg) {
278
353
  throw new Error('Chart must be rendered before export');
279
354
  }
355
+ const exportWidth = options?.width ?? this.width;
356
+ const exportHeight = options?.height ?? this.height;
357
+ const requiresExportRender = exportWidth !== this.width || exportHeight !== this.height;
280
358
  const clone = this.svg.node().cloneNode(true);
281
359
  clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
282
- clone.setAttribute('width', String(this.width));
283
- clone.setAttribute('height', String(this.height));
284
- return clone.outerHTML;
360
+ clone.setAttribute('width', String(exportWidth));
361
+ clone.setAttribute('height', String(exportHeight));
362
+ const baseContext = {
363
+ format: 'svg',
364
+ options,
365
+ width: exportWidth,
366
+ height: exportHeight,
367
+ };
368
+ const overrides = this.collectExportOverrides(baseContext);
369
+ if (overrides.size === 0 && !requiresExportRender) {
370
+ this.runExportHooks({
371
+ ...baseContext,
372
+ svg: clone,
373
+ });
374
+ return clone.outerHTML;
375
+ }
376
+ const exportChart = this.createExportChart();
377
+ const components = this.getExportComponents();
378
+ components.forEach((component) => {
379
+ const exportable = component;
380
+ const override = overrides.get(component);
381
+ if (exportable.createExportComponent) {
382
+ exportChart.addChild(exportable.createExportComponent(override));
383
+ }
384
+ else {
385
+ exportChart.addChild(component);
386
+ }
387
+ });
388
+ const exportSvg = this.renderExportChart(exportChart, exportWidth, exportHeight);
389
+ exportSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
390
+ exportSvg.setAttribute('width', String(exportWidth));
391
+ exportSvg.setAttribute('height', String(exportHeight));
392
+ this.runExportHooks({
393
+ ...baseContext,
394
+ svg: exportSvg,
395
+ });
396
+ return exportSvg.outerHTML;
285
397
  }
286
398
  exportJSON() {
287
399
  return JSON.stringify({
@@ -1,11 +1,13 @@
1
- export interface ChartComponent {
1
+ import type { ExportHooks } from './types.js';
2
+ export interface ChartComponent<TConfig = any> {
2
3
  type: 'line' | 'bar' | 'xAxis' | 'yAxis' | 'grid' | 'tooltip' | 'legend' | 'title' | 'donutCenterContent';
4
+ exportHooks?: ExportHooks<TConfig>;
3
5
  }
4
6
  export type ComponentSpace = {
5
7
  width: number;
6
8
  height: number;
7
9
  position: 'top' | 'bottom' | 'left' | 'right';
8
10
  };
9
- export interface LayoutAwareComponent extends ChartComponent {
11
+ export interface LayoutAwareComponent<TConfig = any> extends ChartComponent<TConfig> {
10
12
  getRequiredSpace(): ComponentSpace;
11
13
  }
@@ -1,5 +1,5 @@
1
1
  import { type Selection } from 'd3';
2
- import type { ChartTheme } from './types.js';
2
+ import type { ChartTheme, ExportHooks } from './types.js';
3
3
  import type { ChartComponent } from './chart-interface.js';
4
4
  type TextStyle = {
5
5
  fontSize?: number;
@@ -7,7 +7,7 @@ type TextStyle = {
7
7
  fontFamily?: string;
8
8
  color?: string;
9
9
  };
10
- export type DonutCenterContentConfig = {
10
+ export type DonutCenterContentConfigBase = {
11
11
  mainValue?: string;
12
12
  title?: string;
13
13
  subtitle?: string;
@@ -15,13 +15,19 @@ export type DonutCenterContentConfig = {
15
15
  titleStyle?: TextStyle;
16
16
  subtitleStyle?: TextStyle;
17
17
  };
18
- export declare class DonutCenterContent implements ChartComponent {
18
+ export type DonutCenterContentConfig = DonutCenterContentConfigBase & {
19
+ exportHooks?: ExportHooks<DonutCenterContentConfigBase>;
20
+ };
21
+ export declare class DonutCenterContent implements ChartComponent<DonutCenterContentConfigBase> {
19
22
  readonly type: "donutCenterContent";
20
23
  readonly mainValue?: string;
21
24
  readonly title?: string;
22
25
  readonly subtitle?: string;
26
+ readonly exportHooks?: ExportHooks<DonutCenterContentConfigBase>;
23
27
  private readonly config;
24
28
  constructor(config?: DonutCenterContentConfig);
29
+ getExportConfig(): DonutCenterContentConfigBase;
30
+ createExportComponent(override?: Partial<DonutCenterContentConfigBase>): ChartComponent;
25
31
  render(svg: Selection<SVGSVGElement, undefined, null, undefined>, cx: number, cy: number, theme: ChartTheme): void;
26
32
  }
27
33
  export {};
@@ -1,3 +1,4 @@
1
+ import { mergeDeep } from './utils.js';
1
2
  export class DonutCenterContent {
2
3
  constructor(config = {}) {
3
4
  Object.defineProperty(this, "type", {
@@ -24,6 +25,12 @@ export class DonutCenterContent {
24
25
  writable: true,
25
26
  value: void 0
26
27
  });
28
+ Object.defineProperty(this, "exportHooks", {
29
+ enumerable: true,
30
+ configurable: true,
31
+ writable: true,
32
+ value: void 0
33
+ });
27
34
  Object.defineProperty(this, "config", {
28
35
  enumerable: true,
29
36
  configurable: true,
@@ -33,8 +40,26 @@ export class DonutCenterContent {
33
40
  this.mainValue = config.mainValue;
34
41
  this.title = config.title;
35
42
  this.subtitle = config.subtitle;
43
+ this.exportHooks = config.exportHooks;
36
44
  this.config = config;
37
45
  }
46
+ getExportConfig() {
47
+ return {
48
+ mainValue: this.mainValue,
49
+ title: this.title,
50
+ subtitle: this.subtitle,
51
+ mainValueStyle: this.config.mainValueStyle,
52
+ titleStyle: this.config.titleStyle,
53
+ subtitleStyle: this.config.subtitleStyle,
54
+ };
55
+ }
56
+ createExportComponent(override) {
57
+ const merged = mergeDeep(this.getExportConfig(), override);
58
+ return new DonutCenterContent({
59
+ ...merged,
60
+ exportHooks: this.exportHooks,
61
+ });
62
+ }
38
63
  render(svg, cx, cy, theme) {
39
64
  const defaults = theme.donut.centerContent;
40
65
  const elements = [];
package/donut-chart.d.ts CHANGED
@@ -23,8 +23,10 @@ export declare class DonutChart extends BaseChart {
23
23
  private validateDonutData;
24
24
  private prepareSegments;
25
25
  addChild(component: ChartComponent): this;
26
+ protected getExportComponents(): ChartComponent[];
26
27
  update(data: DataItem[]): void;
27
28
  protected getLayoutComponents(): LayoutAwareComponent[];
29
+ protected createExportChart(): BaseChart;
28
30
  protected renderChart(): void;
29
31
  private positionTooltip;
30
32
  private buildTooltipContent;
package/donut-chart.js CHANGED
@@ -97,6 +97,22 @@ export class DonutChart extends BaseChart {
97
97
  }
98
98
  return this;
99
99
  }
100
+ getExportComponents() {
101
+ const components = [];
102
+ if (this.title) {
103
+ components.push(this.title);
104
+ }
105
+ if (this.centerContent) {
106
+ components.push(this.centerContent);
107
+ }
108
+ if (this.tooltip) {
109
+ components.push(this.tooltip);
110
+ }
111
+ if (this.legend) {
112
+ components.push(this.legend);
113
+ }
114
+ return components;
115
+ }
100
116
  update(data) {
101
117
  this.data = data;
102
118
  this.validateDonutData();
@@ -105,12 +121,27 @@ export class DonutChart extends BaseChart {
105
121
  }
106
122
  getLayoutComponents() {
107
123
  const components = [];
108
- if (this.title)
124
+ if (this.title) {
109
125
  components.push(this.title);
110
- if (this.legend)
126
+ }
127
+ if (this.legend) {
111
128
  components.push(this.legend);
129
+ }
112
130
  return components;
113
131
  }
132
+ createExportChart() {
133
+ return new DonutChart({
134
+ data: this.data,
135
+ theme: this.theme,
136
+ donut: {
137
+ innerRadius: this.innerRadiusRatio,
138
+ padAngle: this.padAngle,
139
+ cornerRadius: this.cornerRadius,
140
+ },
141
+ valueKey: this.valueKey,
142
+ labelKey: this.labelKey,
143
+ });
144
+ }
114
145
  renderChart() {
115
146
  if (!this.plotArea || !this.svg || !this.plotGroup) {
116
147
  throw new Error('Plot area not calculated');
package/grid.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  import { type Selection } from 'd3';
2
- import type { GridConfig, ChartTheme, D3Scale } from './types.js';
2
+ import type { GridConfig, ChartTheme, D3Scale, ExportHooks, GridConfigBase } from './types.js';
3
3
  import type { ChartComponent } from './chart-interface.js';
4
- export declare class Grid implements ChartComponent {
4
+ export declare class Grid implements ChartComponent<GridConfigBase> {
5
5
  readonly type: "grid";
6
6
  readonly horizontal: boolean;
7
7
  readonly vertical: boolean;
8
+ readonly exportHooks?: ExportHooks<GridConfigBase>;
8
9
  constructor(config?: GridConfig);
10
+ getExportConfig(): GridConfigBase;
11
+ createExportComponent(override?: Partial<GridConfigBase>): ChartComponent;
9
12
  render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, x: D3Scale, y: D3Scale, theme: ChartTheme): void;
10
13
  }
package/grid.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { axisBottom, axisLeft } from 'd3';
2
+ import { mergeDeep } from './utils.js';
2
3
  export class Grid {
3
4
  constructor(config) {
4
5
  Object.defineProperty(this, "type", {
@@ -19,8 +20,28 @@ export class Grid {
19
20
  writable: true,
20
21
  value: void 0
21
22
  });
23
+ Object.defineProperty(this, "exportHooks", {
24
+ enumerable: true,
25
+ configurable: true,
26
+ writable: true,
27
+ value: void 0
28
+ });
22
29
  this.horizontal = config?.horizontal ?? true;
23
30
  this.vertical = config?.vertical ?? true;
31
+ this.exportHooks = config?.exportHooks;
32
+ }
33
+ getExportConfig() {
34
+ return {
35
+ horizontal: this.horizontal,
36
+ vertical: this.vertical,
37
+ };
38
+ }
39
+ createExportComponent(override) {
40
+ const merged = mergeDeep(this.getExportConfig(), override);
41
+ return new Grid({
42
+ ...merged,
43
+ exportHooks: this.exportHooks,
44
+ });
24
45
  }
25
46
  render(plotGroup, x, y, theme) {
26
47
  // Get plot area dimensions from the scale ranges
package/legend.d.ts CHANGED
@@ -1,15 +1,18 @@
1
1
  import { type Selection } from 'd3';
2
- import type { LegendConfig, ChartTheme, LegendSeries } from './types.js';
2
+ import type { LegendConfig, ChartTheme, LegendSeries, ExportHooks, LegendConfigBase } from './types.js';
3
3
  import type { LayoutAwareComponent, ComponentSpace } from './chart-interface.js';
4
- export declare class Legend implements LayoutAwareComponent {
4
+ export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
5
5
  readonly type: "legend";
6
6
  readonly position: LegendConfig['position'];
7
+ readonly exportHooks?: ExportHooks<LegendConfigBase>;
7
8
  private readonly marginTop;
8
9
  private readonly marginBottom;
9
10
  private readonly itemHeight;
10
11
  private visibilityState;
11
12
  private onToggleCallback?;
12
13
  constructor(config?: LegendConfig);
14
+ getExportConfig(): LegendConfigBase;
15
+ createExportComponent(override?: Partial<LegendConfigBase>): LayoutAwareComponent;
13
16
  setToggleCallback(callback: () => void): void;
14
17
  isSeriesVisible(dataKey: string): boolean;
15
18
  private getCheckmarkPath;
package/legend.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { getSeriesColor } from './types.js';
2
+ import { mergeDeep } from './utils.js';
2
3
  export class Legend {
3
4
  constructor(config) {
4
5
  Object.defineProperty(this, "type", {
@@ -13,6 +14,12 @@ export class Legend {
13
14
  writable: true,
14
15
  value: void 0
15
16
  });
17
+ Object.defineProperty(this, "exportHooks", {
18
+ enumerable: true,
19
+ configurable: true,
20
+ writable: true,
21
+ value: void 0
22
+ });
16
23
  Object.defineProperty(this, "marginTop", {
17
24
  enumerable: true,
18
25
  configurable: true,
@@ -46,6 +53,23 @@ export class Legend {
46
53
  this.position = config?.position || 'bottom';
47
54
  this.marginTop = config?.marginTop ?? 20;
48
55
  this.marginBottom = config?.marginBottom ?? 10;
56
+ this.exportHooks = config?.exportHooks;
57
+ }
58
+ getExportConfig() {
59
+ return {
60
+ position: this.position,
61
+ marginTop: this.marginTop,
62
+ marginBottom: this.marginBottom,
63
+ };
64
+ }
65
+ createExportComponent(override) {
66
+ const merged = mergeDeep(this.getExportConfig(), override);
67
+ const legend = new Legend({
68
+ ...merged,
69
+ exportHooks: this.exportHooks,
70
+ });
71
+ legend.visibilityState = new Map(this.visibilityState);
72
+ return legend;
49
73
  }
50
74
  setToggleCallback(callback) {
51
75
  this.onToggleCallback = callback;
package/line.d.ts CHANGED
@@ -1,13 +1,16 @@
1
1
  import { type Selection } from 'd3';
2
- import type { LineConfig, DataItem, D3Scale, ScaleType, ChartTheme, LineValueLabelConfig } from './types.js';
2
+ import type { LineConfig, DataItem, D3Scale, ScaleType, ChartTheme, LineValueLabelConfig, ExportHooks, LineConfigBase } from './types.js';
3
3
  import type { ChartComponent } from './chart-interface.js';
4
- export declare class Line implements ChartComponent {
4
+ export declare class Line implements ChartComponent<LineConfigBase> {
5
5
  readonly type: "line";
6
6
  readonly dataKey: string;
7
7
  readonly stroke: string;
8
8
  readonly strokeWidth?: number;
9
9
  readonly valueLabel?: LineValueLabelConfig;
10
+ readonly exportHooks?: ExportHooks<LineConfigBase>;
10
11
  constructor(config: LineConfig);
12
+ getExportConfig(): LineConfigBase;
13
+ createExportComponent(override?: Partial<LineConfigBase>): ChartComponent;
11
14
  render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: D3Scale, y: D3Scale, parseValue: (value: unknown) => number, xScaleType: ScaleType | undefined, theme: ChartTheme): void;
12
15
  private renderValueLabels;
13
16
  }
package/line.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { line } from 'd3';
2
- import { sanitizeForCSS } from './utils.js';
2
+ import { sanitizeForCSS, mergeDeep } from './utils.js';
3
3
  export class Line {
4
4
  constructor(config) {
5
5
  Object.defineProperty(this, "type", {
@@ -32,10 +32,32 @@ export class Line {
32
32
  writable: true,
33
33
  value: void 0
34
34
  });
35
+ Object.defineProperty(this, "exportHooks", {
36
+ enumerable: true,
37
+ configurable: true,
38
+ writable: true,
39
+ value: void 0
40
+ });
35
41
  this.dataKey = config.dataKey;
36
42
  this.stroke = config.stroke || '#8884d8';
37
43
  this.strokeWidth = config.strokeWidth;
38
44
  this.valueLabel = config.valueLabel;
45
+ this.exportHooks = config.exportHooks;
46
+ }
47
+ getExportConfig() {
48
+ return {
49
+ dataKey: this.dataKey,
50
+ stroke: this.stroke,
51
+ strokeWidth: this.strokeWidth,
52
+ valueLabel: this.valueLabel,
53
+ };
54
+ }
55
+ createExportComponent(override) {
56
+ const merged = mergeDeep(this.getExportConfig(), override);
57
+ return new Line({
58
+ ...merged,
59
+ exportHooks: this.exportHooks,
60
+ });
39
61
  }
40
62
  render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme) {
41
63
  const getXPosition = (d) => {
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.3.2",
2
+ "version": "0.4.0",
3
3
  "name": "@internetstiftelsen/charts",
4
4
  "type": "module",
5
5
  "sideEffects": false,
package/title.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { type Selection } from 'd3';
2
- import type { TitleConfig, ChartTheme } from './types.js';
2
+ import type { TitleConfig, ChartTheme, ExportHooks, TitleConfigBase } from './types.js';
3
3
  import type { LayoutAwareComponent, ComponentSpace } from './chart-interface.js';
4
- export declare class Title implements LayoutAwareComponent {
4
+ export declare class Title implements LayoutAwareComponent<TitleConfigBase> {
5
5
  readonly type: "title";
6
6
  readonly text: string;
7
+ readonly exportHooks?: ExportHooks<TitleConfigBase>;
7
8
  private readonly fontSize;
8
9
  private readonly fontWeight;
9
10
  private readonly fontFamily?;
@@ -11,6 +12,8 @@ export declare class Title implements LayoutAwareComponent {
11
12
  private readonly marginTop;
12
13
  private readonly marginBottom;
13
14
  constructor(config: TitleConfig);
15
+ getExportConfig(): TitleConfigBase;
16
+ createExportComponent(override?: Partial<TitleConfigBase>): LayoutAwareComponent;
14
17
  /**
15
18
  * Returns the space required by the title
16
19
  */
package/title.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { mergeDeep } from './utils.js';
1
2
  export class Title {
2
3
  constructor(config) {
3
4
  Object.defineProperty(this, "type", {
@@ -12,6 +13,12 @@ export class Title {
12
13
  writable: true,
13
14
  value: void 0
14
15
  });
16
+ Object.defineProperty(this, "exportHooks", {
17
+ enumerable: true,
18
+ configurable: true,
19
+ writable: true,
20
+ value: void 0
21
+ });
15
22
  Object.defineProperty(this, "fontSize", {
16
23
  enumerable: true,
17
24
  configurable: true,
@@ -55,6 +62,25 @@ export class Title {
55
62
  this.align = config.align ?? 'center';
56
63
  this.marginTop = config.marginTop ?? 10;
57
64
  this.marginBottom = config.marginBottom ?? 15;
65
+ this.exportHooks = config.exportHooks;
66
+ }
67
+ getExportConfig() {
68
+ return {
69
+ text: this.text,
70
+ fontSize: this.fontSize,
71
+ fontWeight: this.fontWeight,
72
+ fontFamily: this.fontFamily,
73
+ align: this.align,
74
+ marginTop: this.marginTop,
75
+ marginBottom: this.marginBottom,
76
+ };
77
+ }
78
+ createExportComponent(override) {
79
+ const merged = mergeDeep(this.getExportConfig(), override);
80
+ return new Title({
81
+ ...merged,
82
+ exportHooks: this.exportHooks,
83
+ });
58
84
  }
59
85
  /**
60
86
  * Returns the space required by the title
package/tooltip.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { type Selection } from 'd3';
2
- import type { TooltipConfig, DataItem, DataValue, D3Scale, ChartTheme } from './types.js';
2
+ import type { TooltipConfig, DataItem, DataValue, D3Scale, ChartTheme, ExportHooks, TooltipConfigBase } from './types.js';
3
3
  import type { ChartComponent } from './chart-interface.js';
4
4
  import type { Line } from './line.js';
5
5
  import type { Bar } from './bar.js';
6
6
  import type { PlotAreaBounds } from './layout-manager.js';
7
- export declare class Tooltip implements ChartComponent {
7
+ export declare class Tooltip implements ChartComponent<TooltipConfigBase> {
8
8
  readonly id = "iisChartTooltip";
9
9
  readonly type: "tooltip";
10
10
  readonly formatter?: (dataKey: string, value: DataValue, data: DataItem) => string;
@@ -14,8 +14,11 @@ export declare class Tooltip implements ChartComponent {
14
14
  stroke?: string;
15
15
  fill?: string;
16
16
  }[]) => string;
17
+ readonly exportHooks?: ExportHooks<TooltipConfigBase>;
17
18
  private tooltipDiv;
18
19
  constructor(config?: TooltipConfig);
20
+ getExportConfig(): TooltipConfigBase;
21
+ createExportComponent(override?: Partial<TooltipConfigBase>): ChartComponent;
19
22
  initialize(theme: ChartTheme): void;
20
23
  attachToArea(svg: Selection<SVGSVGElement, undefined, null, undefined>, data: DataItem[], series: (Line | Bar)[], xKey: string, x: D3Scale, y: D3Scale, theme: ChartTheme, plotArea: PlotAreaBounds, parseValue: (value: unknown) => number, isHorizontal?: boolean): void;
21
24
  cleanup(): void;
package/tooltip.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { pointer, select } from 'd3';
2
2
  import { getSeriesColor } from './types.js';
3
- import { sanitizeForCSS } from './utils.js';
3
+ import { sanitizeForCSS, mergeDeep } from './utils.js';
4
4
  export class Tooltip {
5
5
  constructor(config) {
6
6
  Object.defineProperty(this, "id", {
@@ -33,6 +33,12 @@ export class Tooltip {
33
33
  writable: true,
34
34
  value: void 0
35
35
  });
36
+ Object.defineProperty(this, "exportHooks", {
37
+ enumerable: true,
38
+ configurable: true,
39
+ writable: true,
40
+ value: void 0
41
+ });
36
42
  Object.defineProperty(this, "tooltipDiv", {
37
43
  enumerable: true,
38
44
  configurable: true,
@@ -42,6 +48,21 @@ export class Tooltip {
42
48
  this.formatter = config?.formatter;
43
49
  this.labelFormatter = config?.labelFormatter;
44
50
  this.customFormatter = config?.customFormatter;
51
+ this.exportHooks = config?.exportHooks;
52
+ }
53
+ getExportConfig() {
54
+ return {
55
+ formatter: this.formatter,
56
+ labelFormatter: this.labelFormatter,
57
+ customFormatter: this.customFormatter,
58
+ };
59
+ }
60
+ createExportComponent(override) {
61
+ const merged = mergeDeep(this.getExportConfig(), override);
62
+ return new Tooltip({
63
+ ...merged,
64
+ exportHooks: this.exportHooks,
65
+ });
45
66
  }
46
67
  initialize(theme) {
47
68
  this.cleanup();
package/types.d.ts CHANGED
@@ -4,6 +4,21 @@ export type ExportFormat = 'svg' | 'json';
4
4
  export type ExportOptions = {
5
5
  download?: boolean;
6
6
  filename?: string;
7
+ width?: number;
8
+ height?: number;
9
+ };
10
+ export type ExportRenderContext = {
11
+ format: ExportFormat;
12
+ options?: ExportOptions;
13
+ width: number;
14
+ height: number;
15
+ };
16
+ export type ExportHookContext = ExportRenderContext & {
17
+ svg: SVGSVGElement;
18
+ };
19
+ export type ExportHooks<TConfig = Record<string, unknown>> = {
20
+ beforeRender?: (context: ExportRenderContext, currentConfig: TConfig) => void | Partial<TConfig>;
21
+ before?: (context: ExportHookContext) => void;
7
22
  };
8
23
  export type ColorPalette = string[];
9
24
  export type ChartTheme = {
@@ -94,13 +109,16 @@ export type BarValueLabelConfig = ValueLabelConfig & {
94
109
  position?: 'inside' | 'outside';
95
110
  insidePosition?: 'top' | 'middle' | 'bottom';
96
111
  };
97
- export type LineConfig = {
112
+ export type LineConfigBase = {
98
113
  dataKey: string;
99
114
  stroke?: string;
100
115
  strokeWidth?: number;
101
116
  valueLabel?: LineValueLabelConfig;
102
117
  };
103
- export type BarConfig = {
118
+ export type LineConfig = LineConfigBase & {
119
+ exportHooks?: ExportHooks<LineConfigBase>;
120
+ };
121
+ export type BarConfigBase = {
104
122
  dataKey: string;
105
123
  fill?: string;
106
124
  colorAdapter?: (data: DataItem, index: number) => string;
@@ -108,6 +126,9 @@ export type BarConfig = {
108
126
  maxBarSize?: number;
109
127
  valueLabel?: BarValueLabelConfig;
110
128
  };
129
+ export type BarConfig = BarConfigBase & {
130
+ exportHooks?: ExportHooks<BarConfigBase>;
131
+ };
111
132
  export type BarStackConfig = {
112
133
  mode?: BarStackMode;
113
134
  gap?: number;
@@ -117,7 +138,7 @@ export declare function getSeriesColor(series: {
117
138
  fill?: string;
118
139
  }): string;
119
140
  export type LabelOversizedBehavior = 'truncate' | 'wrap' | 'hide';
120
- export type XAxisConfig = {
141
+ export type XAxisConfigBase = {
121
142
  dataKey?: string;
122
143
  rotatedLabels?: boolean;
123
144
  maxLabelWidth?: number;
@@ -127,17 +148,26 @@ export type XAxisConfig = {
127
148
  minLabelGap?: number;
128
149
  preserveEndLabels?: boolean;
129
150
  };
130
- export type YAxisConfig = {
151
+ export type XAxisConfig = XAxisConfigBase & {
152
+ exportHooks?: ExportHooks<XAxisConfigBase>;
153
+ };
154
+ export type YAxisConfigBase = {
131
155
  tickFormat?: string | ((value: number) => string) | null;
132
156
  rotatedLabels?: boolean;
133
157
  maxLabelWidth?: number;
134
158
  oversizedBehavior?: LabelOversizedBehavior;
135
159
  };
136
- export type GridConfig = {
160
+ export type YAxisConfig = YAxisConfigBase & {
161
+ exportHooks?: ExportHooks<YAxisConfigBase>;
162
+ };
163
+ export type GridConfigBase = {
137
164
  horizontal?: boolean;
138
165
  vertical?: boolean;
139
166
  };
140
- export type TooltipConfig = {
167
+ export type GridConfig = GridConfigBase & {
168
+ exportHooks?: ExportHooks<GridConfigBase>;
169
+ };
170
+ export type TooltipConfigBase = {
141
171
  formatter?: (dataKey: string, value: DataValue, data: DataItem) => string;
142
172
  labelFormatter?: (label: string, data: DataItem) => string;
143
173
  customFormatter?: (data: DataItem, series: {
@@ -146,17 +176,23 @@ export type TooltipConfig = {
146
176
  fill?: string;
147
177
  }[]) => string;
148
178
  };
149
- export type LegendConfig = {
179
+ export type TooltipConfig = TooltipConfigBase & {
180
+ exportHooks?: ExportHooks<TooltipConfigBase>;
181
+ };
182
+ export type LegendConfigBase = {
150
183
  position?: 'bottom';
151
184
  marginTop?: number;
152
185
  marginBottom?: number;
153
186
  };
187
+ export type LegendConfig = LegendConfigBase & {
188
+ exportHooks?: ExportHooks<LegendConfigBase>;
189
+ };
154
190
  export type LegendSeries = {
155
191
  dataKey: string;
156
192
  stroke?: string;
157
193
  fill?: string;
158
194
  };
159
- export type TitleConfig = {
195
+ export type TitleConfigBase = {
160
196
  text: string;
161
197
  fontSize?: number;
162
198
  fontWeight?: string;
@@ -165,6 +201,9 @@ export type TitleConfig = {
165
201
  marginTop?: number;
166
202
  marginBottom?: number;
167
203
  };
204
+ export type TitleConfig = TitleConfigBase & {
205
+ exportHooks?: ExportHooks<TitleConfigBase>;
206
+ };
168
207
  export type ScaleType = 'band' | 'linear' | 'time' | 'log';
169
208
  export type D3Scale = any;
170
209
  export type ScaleDomainValue = string | number | Date;
package/utils.d.ts CHANGED
@@ -60,3 +60,4 @@ export declare function breakWord(word: string, maxWidth: number, fontSize: stri
60
60
  * @returns Array of lines
61
61
  */
62
62
  export declare function wrapText(text: string, maxWidth: number, fontSize: string | number, fontFamily: string, fontWeight: string, svg: SVGSVGElement): string[];
63
+ export declare function mergeDeep<T extends Record<string, unknown>>(base: T, override?: Partial<T>): T;
package/utils.js CHANGED
@@ -168,3 +168,25 @@ export function wrapText(text, maxWidth, fontSize, fontFamily, fontWeight, svg)
168
168
  }
169
169
  return lines.length > 0 ? lines : [text];
170
170
  }
171
+ export function mergeDeep(base, override) {
172
+ if (!override) {
173
+ return { ...base };
174
+ }
175
+ const result = { ...base };
176
+ Object.keys(override).forEach((key) => {
177
+ const overrideValue = override[key];
178
+ const baseValue = base[key];
179
+ if (overrideValue &&
180
+ typeof overrideValue === 'object' &&
181
+ !Array.isArray(overrideValue) &&
182
+ baseValue &&
183
+ typeof baseValue === 'object' &&
184
+ !Array.isArray(baseValue)) {
185
+ result[key] = mergeDeep(baseValue, overrideValue);
186
+ }
187
+ else if (overrideValue !== undefined) {
188
+ result[key] = overrideValue;
189
+ }
190
+ });
191
+ return result;
192
+ }
package/x-axis.d.ts CHANGED
@@ -1,24 +1,30 @@
1
1
  import { type Selection } from 'd3';
2
- import type { XAxisConfig, ChartTheme, D3Scale } from './types.js';
2
+ import type { XAxisConfig, ChartTheme, D3Scale, ExportHooks, XAxisConfigBase } from './types.js';
3
3
  import type { LayoutAwareComponent, ComponentSpace } from './chart-interface.js';
4
- export declare class XAxis implements LayoutAwareComponent {
4
+ export declare class XAxis implements LayoutAwareComponent<XAxisConfigBase> {
5
5
  readonly type: "xAxis";
6
6
  readonly dataKey?: string;
7
7
  private readonly rotatedLabels;
8
8
  private readonly tickPadding;
9
- private readonly fontSize;
9
+ private fontSize;
10
10
  private readonly maxLabelWidth?;
11
11
  private readonly oversizedBehavior;
12
12
  private readonly tickFormat;
13
13
  private wrapLineCount;
14
+ private estimatedHeight;
14
15
  private readonly autoHideOverlapping;
15
16
  private readonly minLabelGap;
16
17
  private readonly preserveEndLabels;
18
+ readonly exportHooks?: ExportHooks<XAxisConfigBase>;
17
19
  constructor(config?: XAxisConfig);
20
+ getExportConfig(): XAxisConfigBase;
21
+ createExportComponent(override?: Partial<XAxisConfigBase>): LayoutAwareComponent;
18
22
  /**
19
23
  * Returns the space required by the x-axis
20
24
  */
21
25
  getRequiredSpace(): ComponentSpace;
26
+ estimateLayoutSpace(labels: unknown[], theme: ChartTheme, svg: SVGSVGElement): void;
27
+ clearEstimatedSpace(): void;
22
28
  render(svg: Selection<SVGSVGElement, undefined, null, undefined>, x: D3Scale, theme: ChartTheme, yPosition: number): void;
23
29
  private applyLabelConstraints;
24
30
  private wrapTextElement;
package/x-axis.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { axisBottom } from 'd3';
2
- import { measureTextWidth, truncateText, wrapText } from './utils.js';
2
+ import { measureTextWidth, truncateText, wrapText, mergeDeep } from './utils.js';
3
3
  export class XAxis {
4
4
  constructor(config) {
5
5
  Object.defineProperty(this, "type", {
@@ -57,6 +57,12 @@ export class XAxis {
57
57
  writable: true,
58
58
  value: 1
59
59
  });
60
+ Object.defineProperty(this, "estimatedHeight", {
61
+ enumerable: true,
62
+ configurable: true,
63
+ writable: true,
64
+ value: null
65
+ });
60
66
  Object.defineProperty(this, "autoHideOverlapping", {
61
67
  enumerable: true,
62
68
  configurable: true,
@@ -75,6 +81,12 @@ export class XAxis {
75
81
  writable: true,
76
82
  value: void 0
77
83
  });
84
+ Object.defineProperty(this, "exportHooks", {
85
+ enumerable: true,
86
+ configurable: true,
87
+ writable: true,
88
+ value: void 0
89
+ });
78
90
  this.dataKey = config?.dataKey;
79
91
  this.rotatedLabels = config?.rotatedLabels ?? false;
80
92
  this.maxLabelWidth = config?.maxLabelWidth;
@@ -83,11 +95,38 @@ export class XAxis {
83
95
  this.autoHideOverlapping = config?.autoHideOverlapping ?? false;
84
96
  this.minLabelGap = config?.minLabelGap ?? 8;
85
97
  this.preserveEndLabels = config?.preserveEndLabels ?? true;
98
+ this.exportHooks = config?.exportHooks;
99
+ }
100
+ getExportConfig() {
101
+ return {
102
+ dataKey: this.dataKey,
103
+ rotatedLabels: this.rotatedLabels,
104
+ maxLabelWidth: this.maxLabelWidth,
105
+ oversizedBehavior: this.oversizedBehavior,
106
+ tickFormat: this.tickFormat,
107
+ autoHideOverlapping: this.autoHideOverlapping,
108
+ minLabelGap: this.minLabelGap,
109
+ preserveEndLabels: this.preserveEndLabels,
110
+ };
111
+ }
112
+ createExportComponent(override) {
113
+ const merged = mergeDeep(this.getExportConfig(), override);
114
+ return new XAxis({
115
+ ...merged,
116
+ exportHooks: this.exportHooks,
117
+ });
86
118
  }
87
119
  /**
88
120
  * Returns the space required by the x-axis
89
121
  */
90
122
  getRequiredSpace() {
123
+ if (this.estimatedHeight !== null) {
124
+ return {
125
+ width: 0,
126
+ height: this.estimatedHeight,
127
+ position: 'bottom',
128
+ };
129
+ }
91
130
  // Height = tick padding + font size + some extra space for descenders
92
131
  // Rotated labels need more vertical space (roughly 2.5x for -45deg rotation)
93
132
  const baseHeight = this.tickPadding + this.fontSize + 5;
@@ -104,6 +143,53 @@ export class XAxis {
104
143
  position: 'bottom',
105
144
  };
106
145
  }
146
+ estimateLayoutSpace(labels, theme, svg) {
147
+ if (!labels.length) {
148
+ this.estimatedHeight = null;
149
+ return;
150
+ }
151
+ const parsedFontSize = typeof theme.axis.fontSize === 'string'
152
+ ? parseFloat(theme.axis.fontSize)
153
+ : theme.axis.fontSize;
154
+ this.fontSize = Number.isFinite(parsedFontSize)
155
+ ? parsedFontSize
156
+ : this.fontSize;
157
+ const fontSize = this.fontSize;
158
+ const fontFamily = theme.axis.fontFamily;
159
+ const fontWeight = theme.axis.fontWeight || 'normal';
160
+ let maxWidth = 0;
161
+ let maxLines = 1;
162
+ for (const label of labels) {
163
+ const text = String(label ?? '');
164
+ if (!text)
165
+ continue;
166
+ const textWidth = measureTextWidth(text, fontSize, fontFamily, fontWeight, svg);
167
+ if (this.maxLabelWidth && this.oversizedBehavior === 'wrap') {
168
+ const lines = wrapText(text, this.maxLabelWidth, fontSize, fontFamily, fontWeight, svg);
169
+ maxLines = Math.max(maxLines, lines.length || 1);
170
+ maxWidth = Math.max(maxWidth, this.maxLabelWidth);
171
+ continue;
172
+ }
173
+ const effectiveWidth = this.maxLabelWidth
174
+ ? Math.min(textWidth, this.maxLabelWidth)
175
+ : textWidth;
176
+ maxWidth = Math.max(maxWidth, effectiveWidth);
177
+ }
178
+ const lineHeight = fontSize * 1.2;
179
+ const textHeight = lineHeight * maxLines;
180
+ if (this.rotatedLabels) {
181
+ const radians = Math.PI / 4;
182
+ const verticalFootprint = Math.sin(radians) * maxWidth + Math.cos(radians) * textHeight;
183
+ this.estimatedHeight = this.tickPadding + verticalFootprint + 5;
184
+ }
185
+ else {
186
+ this.estimatedHeight = this.tickPadding + textHeight + 5;
187
+ }
188
+ this.wrapLineCount = Math.max(this.wrapLineCount, maxLines);
189
+ }
190
+ clearEstimatedSpace() {
191
+ this.estimatedHeight = null;
192
+ }
107
193
  render(svg, x, theme, yPosition) {
108
194
  const axisGenerator = axisBottom(x)
109
195
  .tickSizeOuter(0)
package/xy-chart.d.ts CHANGED
@@ -10,7 +10,10 @@ export declare class XYChart extends BaseChart {
10
10
  private barStackGap;
11
11
  constructor(config: XYChartConfig);
12
12
  addChild(component: ChartComponent): this;
13
+ protected getExportComponents(): ChartComponent[];
14
+ protected createExportChart(): BaseChart;
13
15
  private rerender;
16
+ protected prepareLayout(): void;
14
17
  protected renderChart(): void;
15
18
  private getXKey;
16
19
  private setupScales;
package/xy-chart.js CHANGED
@@ -68,9 +68,51 @@ export class XYChart extends BaseChart {
68
68
  }
69
69
  return this;
70
70
  }
71
+ getExportComponents() {
72
+ const components = [];
73
+ if (this.title) {
74
+ components.push(this.title);
75
+ }
76
+ if (this.grid) {
77
+ components.push(this.grid);
78
+ }
79
+ components.push(...this.series);
80
+ if (this.xAxis) {
81
+ components.push(this.xAxis);
82
+ }
83
+ if (this.yAxis) {
84
+ components.push(this.yAxis);
85
+ }
86
+ if (this.tooltip) {
87
+ components.push(this.tooltip);
88
+ }
89
+ if (this.legend) {
90
+ components.push(this.legend);
91
+ }
92
+ return components;
93
+ }
94
+ createExportChart() {
95
+ return new XYChart({
96
+ data: this.data,
97
+ theme: this.theme,
98
+ scales: this.scaleConfig,
99
+ barStack: {
100
+ mode: this.barStackMode,
101
+ gap: this.barStackGap,
102
+ },
103
+ });
104
+ }
71
105
  rerender() {
72
106
  this.update(this.data);
73
107
  }
108
+ prepareLayout() {
109
+ this.xAxis?.clearEstimatedSpace?.();
110
+ if (!this.xAxis || !this.svg)
111
+ return;
112
+ const xKey = this.getXKey();
113
+ const labels = this.data.map((item) => item[xKey]);
114
+ this.xAxis.estimateLayoutSpace?.(labels, this.theme, this.svg.node());
115
+ }
74
116
  renderChart() {
75
117
  if (!this.plotArea) {
76
118
  throw new Error('Plot area not calculated');
package/y-axis.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type Selection } from 'd3';
2
- import type { ChartTheme, YAxisConfig, D3Scale } from './types.js';
2
+ import type { ChartTheme, YAxisConfig, D3Scale, ExportHooks, YAxisConfigBase } from './types.js';
3
3
  import type { LayoutAwareComponent, ComponentSpace } from './chart-interface.js';
4
- export declare class YAxis implements LayoutAwareComponent {
4
+ export declare class YAxis implements LayoutAwareComponent<YAxisConfigBase> {
5
5
  readonly type: "yAxis";
6
6
  private readonly tickPadding;
7
7
  private readonly fontSize;
@@ -9,7 +9,10 @@ export declare class YAxis implements LayoutAwareComponent {
9
9
  private readonly tickFormat;
10
10
  private readonly rotatedLabels;
11
11
  private readonly oversizedBehavior;
12
+ readonly exportHooks?: ExportHooks<YAxisConfigBase>;
12
13
  constructor(config?: YAxisConfig);
14
+ getExportConfig(): YAxisConfigBase;
15
+ createExportComponent(override?: Partial<YAxisConfigBase>): LayoutAwareComponent;
13
16
  /**
14
17
  * Returns the space required by the y-axis
15
18
  */
package/y-axis.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { axisLeft } from 'd3';
2
- import { measureTextWidth, truncateText, wrapText } from './utils.js';
2
+ import { measureTextWidth, truncateText, wrapText, mergeDeep } from './utils.js';
3
3
  export class YAxis {
4
4
  constructor(config) {
5
5
  Object.defineProperty(this, "type", {
@@ -45,10 +45,32 @@ export class YAxis {
45
45
  writable: true,
46
46
  value: void 0
47
47
  });
48
+ Object.defineProperty(this, "exportHooks", {
49
+ enumerable: true,
50
+ configurable: true,
51
+ writable: true,
52
+ value: void 0
53
+ });
48
54
  this.tickFormat = config?.tickFormat ?? null;
49
55
  this.rotatedLabels = config?.rotatedLabels ?? false;
50
56
  this.maxLabelWidth = config?.maxLabelWidth ?? 40; // Default 40 for backward compatibility
51
57
  this.oversizedBehavior = config?.oversizedBehavior ?? 'truncate';
58
+ this.exportHooks = config?.exportHooks;
59
+ }
60
+ getExportConfig() {
61
+ return {
62
+ tickFormat: this.tickFormat,
63
+ rotatedLabels: this.rotatedLabels,
64
+ maxLabelWidth: this.maxLabelWidth,
65
+ oversizedBehavior: this.oversizedBehavior,
66
+ };
67
+ }
68
+ createExportComponent(override) {
69
+ const merged = mergeDeep(this.getExportConfig(), override);
70
+ return new YAxis({
71
+ ...merged,
72
+ exportHooks: this.exportHooks,
73
+ });
52
74
  }
53
75
  /**
54
76
  * Returns the space required by the y-axis