@internetstiftelsen/charts 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 { getContrastTextColor, 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];
@@ -338,7 +362,7 @@ export class Bar {
338
362
  const fontSize = config.fontSize ?? theme.valueLabel.fontSize;
339
363
  const fontFamily = config.fontFamily ?? theme.valueLabel.fontFamily;
340
364
  const fontWeight = config.fontWeight ?? theme.valueLabel.fontWeight;
341
- const color = config.color ?? theme.valueLabel.color;
365
+ const defaultLabelColor = config.color ?? theme.valueLabel.color;
342
366
  const background = config.background ?? theme.valueLabel.background;
343
367
  const border = config.border ?? theme.valueLabel.border;
344
368
  const borderRadius = config.borderRadius ?? theme.valueLabel.borderRadius;
@@ -346,11 +370,14 @@ export class Bar {
346
370
  const labelGroup = plotGroup
347
371
  .append('g')
348
372
  .attr('class', `bar-value-labels-${sanitizeForCSS(this.dataKey)}`);
349
- data.forEach((d) => {
373
+ data.forEach((d, i) => {
350
374
  const categoryKey = String(d[xKey]);
351
375
  const value = parseValue(d[this.dataKey]);
352
376
  const valueText = String(value);
353
377
  const xPos = this.getScaledPosition(d, xKey, x, xScaleType);
378
+ const barColor = this.colorAdapter
379
+ ? this.colorAdapter(d, i)
380
+ : this.fill;
354
381
  // Calculate bar position based on stacking mode
355
382
  let barTop;
356
383
  let barBottom;
@@ -473,6 +500,9 @@ export class Bar {
473
500
  }
474
501
  tempText.remove();
475
502
  if (shouldRender) {
503
+ const labelColor = position === 'inside' && config.color === undefined
504
+ ? getContrastTextColor(barColor)
505
+ : defaultLabelColor;
476
506
  const group = labelGroup.append('g');
477
507
  if (position === 'outside') {
478
508
  // Draw rounded rectangle background
@@ -498,7 +528,7 @@ export class Bar {
498
528
  .style('font-size', `${fontSize}px`)
499
529
  .style('font-family', fontFamily)
500
530
  .style('font-weight', fontWeight)
501
- .style('fill', color)
531
+ .style('fill', labelColor)
502
532
  .style('pointer-events', 'none')
503
533
  .text(valueText);
504
534
  }
@@ -550,7 +580,7 @@ export class Bar {
550
580
  const fontSize = config.fontSize ?? theme.valueLabel.fontSize;
551
581
  const fontFamily = config.fontFamily ?? theme.valueLabel.fontFamily;
552
582
  const fontWeight = config.fontWeight ?? theme.valueLabel.fontWeight;
553
- const color = config.color ?? theme.valueLabel.color;
583
+ const defaultLabelColor = config.color ?? theme.valueLabel.color;
554
584
  const background = config.background ?? theme.valueLabel.background;
555
585
  const border = config.border ?? theme.valueLabel.border;
556
586
  const borderRadius = config.borderRadius ?? theme.valueLabel.borderRadius;
@@ -558,11 +588,14 @@ export class Bar {
558
588
  const labelGroup = plotGroup
559
589
  .append('g')
560
590
  .attr('class', `bar-value-labels-${sanitizeForCSS(this.dataKey)}`);
561
- data.forEach((d) => {
591
+ data.forEach((d, i) => {
562
592
  const categoryKey = String(d[xKey]);
563
593
  const value = parseValue(d[this.dataKey]);
564
594
  const valueText = String(value);
565
595
  const yPos = this.getScaledPosition(d, xKey, y, yScaleType);
596
+ const barColor = this.colorAdapter
597
+ ? this.colorAdapter(d, i)
598
+ : this.fill;
566
599
  // Calculate bar position based on stacking mode
567
600
  let barLeft;
568
601
  let barRight;
@@ -687,6 +720,9 @@ export class Bar {
687
720
  }
688
721
  tempText.remove();
689
722
  if (shouldRender) {
723
+ const labelColor = position === 'inside' && config.color === undefined
724
+ ? getContrastTextColor(barColor)
725
+ : defaultLabelColor;
690
726
  const group = labelGroup.append('g');
691
727
  if (position === 'outside') {
692
728
  // Draw rounded rectangle background
@@ -712,7 +748,7 @@ export class Bar {
712
748
  .style('font-size', `${fontSize}px`)
713
749
  .style('font-family', fontFamily)
714
750
  .style('font-weight', fontWeight)
715
- .style('fill', color)
751
+ .style('fill', labelColor)
716
752
  .style('pointer-events', 'none')
717
753
  .text(valueText);
718
754
  }
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,10 @@ 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;
57
61
  protected prepareLayout(): void;
58
62
  /**
59
63
  * Setup ResizeObserver for automatic resize handling
@@ -63,6 +67,7 @@ export declare abstract class BaseChart {
63
67
  * Subclasses must implement this method to define their rendering logic
64
68
  */
65
69
  protected abstract renderChart(): void;
70
+ protected abstract createExportChart(): BaseChart;
66
71
  /**
67
72
  * Updates the chart with new data
68
73
  */
@@ -83,6 +88,6 @@ export declare abstract class BaseChart {
83
88
  * Downloads the exported content as a file
84
89
  */
85
90
  private downloadContent;
86
- protected exportSVG(): string;
91
+ protected exportSVG(options?: ExportOptions): string;
87
92
  protected exportJSON(): string;
88
93
  }
package/base-chart.js CHANGED
@@ -185,16 +185,87 @@ export class BaseChart {
185
185
  */
186
186
  getLayoutComponents() {
187
187
  const components = [];
188
- if (this.title)
188
+ if (this.title) {
189
189
  components.push(this.title);
190
- if (this.xAxis)
190
+ }
191
+ if (this.xAxis) {
191
192
  components.push(this.xAxis);
192
- if (this.yAxis)
193
+ }
194
+ if (this.yAxis) {
193
195
  components.push(this.yAxis);
194
- if (this.legend)
196
+ }
197
+ if (this.legend) {
195
198
  components.push(this.legend);
199
+ }
196
200
  return components;
197
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
+ }
198
269
  // Hook for subclasses to update component layout estimates before layout calc
199
270
  // eslint-disable-next-line @typescript-eslint/no-empty-function
200
271
  prepareLayout() { }
@@ -255,7 +326,7 @@ export class BaseChart {
255
326
  * @returns The exported content as a string if download is false/undefined, void if download is true
256
327
  */
257
328
  export(format, options) {
258
- const content = format === 'svg' ? this.exportSVG() : this.exportJSON();
329
+ const content = format === 'svg' ? this.exportSVG(options) : this.exportJSON();
259
330
  if (options?.download) {
260
331
  this.downloadContent(content, format, options);
261
332
  return;
@@ -277,15 +348,52 @@ export class BaseChart {
277
348
  document.body.removeChild(link);
278
349
  URL.revokeObjectURL(url);
279
350
  }
280
- exportSVG() {
351
+ exportSVG(options) {
281
352
  if (!this.svg) {
282
353
  throw new Error('Chart must be rendered before export');
283
354
  }
355
+ const exportWidth = options?.width ?? this.width;
356
+ const exportHeight = options?.height ?? this.height;
357
+ const requiresExportRender = exportWidth !== this.width || exportHeight !== this.height;
284
358
  const clone = this.svg.node().cloneNode(true);
285
359
  clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
286
- clone.setAttribute('width', String(this.width));
287
- clone.setAttribute('height', String(this.height));
288
- 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;
289
397
  }
290
398
  exportJSON() {
291
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,19 +1,21 @@
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;
16
- private parseColor;
17
19
  /**
18
20
  * Returns the space required by the legend
19
21
  */
package/legend.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { getSeriesColor } from './types.js';
2
+ import { getContrastTextColor, 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;
@@ -59,29 +83,6 @@ export class Legend {
59
83
  const offsetY = size * 0.15;
60
84
  return `M ${4 * scale + offsetX} ${12 * scale + offsetY} L ${9 * scale + offsetX} ${17 * scale + offsetY} L ${20 * scale + offsetX} ${6 * scale + offsetY}`;
61
85
  }
62
- parseColor(color) {
63
- // Handle hex colors
64
- if (color.startsWith('#')) {
65
- const hex = color.slice(1);
66
- const r = parseInt(hex.slice(0, 2), 16);
67
- const g = parseInt(hex.slice(2, 4), 16);
68
- const b = parseInt(hex.slice(4, 6), 16);
69
- return { r, g, b };
70
- }
71
- // Handle rgb/rgba colors
72
- if (color.startsWith('rgb')) {
73
- const match = color.match(/\d+/g);
74
- if (match) {
75
- return {
76
- r: parseInt(match[0]),
77
- g: parseInt(match[1]),
78
- b: parseInt(match[2]),
79
- };
80
- }
81
- }
82
- // Default to black if we can't parse
83
- return { r: 0, g: 0, b: 0 };
84
- }
85
86
  /**
86
87
  * Returns the space required by the legend
87
88
  */
@@ -161,13 +162,7 @@ export class Legend {
161
162
  .append('path')
162
163
  .attr('d', this.getCheckmarkPath(boxSize))
163
164
  .attr('fill', 'none')
164
- .attr('stroke', (d) => {
165
- // Calculate luminance to determine if we need black or white checkmark
166
- const color = d.color;
167
- const rgb = this.parseColor(color);
168
- const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
169
- return luminance > 0.5 ? '#000' : '#fff';
170
- })
165
+ .attr('stroke', (d) => getContrastTextColor(d.color))
171
166
  .attr('stroke-width', 2)
172
167
  .attr('stroke-linecap', 'round')
173
168
  .attr('stroke-linejoin', 'round')
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.3",
2
+ "version": "0.4.1",
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,5 @@ 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 getContrastTextColor(backgroundColor: string, lightTextColor?: string, darkTextColor?: string): string;
64
+ export declare function mergeDeep<T extends Record<string, unknown>>(base: T, override?: Partial<T>): T;
package/utils.js CHANGED
@@ -168,3 +168,63 @@ export function wrapText(text, maxWidth, fontSize, fontFamily, fontWeight, svg)
168
168
  }
169
169
  return lines.length > 0 ? lines : [text];
170
170
  }
171
+ function parseColorToRGB(color) {
172
+ const normalizedColor = color.trim();
173
+ if (normalizedColor.startsWith('#')) {
174
+ const hex = normalizedColor.slice(1);
175
+ if (hex.length === 3) {
176
+ const r = parseInt(hex[0] + hex[0], 16);
177
+ const g = parseInt(hex[1] + hex[1], 16);
178
+ const b = parseInt(hex[2] + hex[2], 16);
179
+ return { r, g, b };
180
+ }
181
+ if (hex.length === 6) {
182
+ const r = parseInt(hex.slice(0, 2), 16);
183
+ const g = parseInt(hex.slice(2, 4), 16);
184
+ const b = parseInt(hex.slice(4, 6), 16);
185
+ return { r, g, b };
186
+ }
187
+ }
188
+ if (normalizedColor.startsWith('rgb')) {
189
+ const matches = normalizedColor.match(/\d+/g);
190
+ if (matches && matches.length >= 3) {
191
+ return {
192
+ r: parseInt(matches[0], 10),
193
+ g: parseInt(matches[1], 10),
194
+ b: parseInt(matches[2], 10),
195
+ };
196
+ }
197
+ }
198
+ // Fallback matches legend behavior: unknown colors are treated as dark.
199
+ return { r: 0, g: 0, b: 0 };
200
+ }
201
+ export function getContrastTextColor(backgroundColor, lightTextColor = '#fff', darkTextColor = '#000') {
202
+ const { r, g, b } = parseColorToRGB(backgroundColor);
203
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
204
+ if (luminance > 0.5) {
205
+ return darkTextColor;
206
+ }
207
+ return lightTextColor;
208
+ }
209
+ export function mergeDeep(base, override) {
210
+ if (!override) {
211
+ return { ...base };
212
+ }
213
+ const result = { ...base };
214
+ Object.keys(override).forEach((key) => {
215
+ const overrideValue = override[key];
216
+ const baseValue = base[key];
217
+ if (overrideValue &&
218
+ typeof overrideValue === 'object' &&
219
+ !Array.isArray(overrideValue) &&
220
+ baseValue &&
221
+ typeof baseValue === 'object' &&
222
+ !Array.isArray(baseValue)) {
223
+ result[key] = mergeDeep(baseValue, overrideValue);
224
+ }
225
+ else if (overrideValue !== undefined) {
226
+ result[key] = overrideValue;
227
+ }
228
+ });
229
+ return result;
230
+ }
package/x-axis.d.ts CHANGED
@@ -1,7 +1,7 @@
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;
@@ -15,7 +15,10 @@ export declare class XAxis implements LayoutAwareComponent {
15
15
  private readonly autoHideOverlapping;
16
16
  private readonly minLabelGap;
17
17
  private readonly preserveEndLabels;
18
+ readonly exportHooks?: ExportHooks<XAxisConfigBase>;
18
19
  constructor(config?: XAxisConfig);
20
+ getExportConfig(): XAxisConfigBase;
21
+ createExportComponent(override?: Partial<XAxisConfigBase>): LayoutAwareComponent;
19
22
  /**
20
23
  * Returns the space required by the x-axis
21
24
  */
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", {
@@ -81,6 +81,12 @@ export class XAxis {
81
81
  writable: true,
82
82
  value: void 0
83
83
  });
84
+ Object.defineProperty(this, "exportHooks", {
85
+ enumerable: true,
86
+ configurable: true,
87
+ writable: true,
88
+ value: void 0
89
+ });
84
90
  this.dataKey = config?.dataKey;
85
91
  this.rotatedLabels = config?.rotatedLabels ?? false;
86
92
  this.maxLabelWidth = config?.maxLabelWidth;
@@ -89,6 +95,26 @@ export class XAxis {
89
95
  this.autoHideOverlapping = config?.autoHideOverlapping ?? false;
90
96
  this.minLabelGap = config?.minLabelGap ?? 8;
91
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
+ });
92
118
  }
93
119
  /**
94
120
  * Returns the space required by the x-axis
@@ -153,10 +179,8 @@ export class XAxis {
153
179
  const textHeight = lineHeight * maxLines;
154
180
  if (this.rotatedLabels) {
155
181
  const radians = Math.PI / 4;
156
- const verticalFootprint = Math.sin(radians) * maxWidth +
157
- Math.cos(radians) * textHeight;
158
- this.estimatedHeight =
159
- this.tickPadding + verticalFootprint + 5;
182
+ const verticalFootprint = Math.sin(radians) * maxWidth + Math.cos(radians) * textHeight;
183
+ this.estimatedHeight = this.tickPadding + verticalFootprint + 5;
160
184
  }
161
185
  else {
162
186
  this.estimatedHeight = this.tickPadding + textHeight + 5;
package/xy-chart.d.ts CHANGED
@@ -10,6 +10,8 @@ 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;
14
16
  protected prepareLayout(): void;
15
17
  protected renderChart(): void;
package/xy-chart.js CHANGED
@@ -68,6 +68,40 @@ 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
  }
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