@internetstiftelsen/charts 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,6 +9,7 @@ A framework-agnostic, composable charting library built on D3.js with TypeScript
9
9
  - **Multiple Chart Types** - XYChart (lines, areas, bars), DonutChart, PieChart, and GaugeChart
10
10
  - **Flexible Scales** - Band, linear, time, and logarithmic scales
11
11
  - **Auto Resize** - Built-in ResizeObserver handles responsive behavior
12
+ - **Responsive Policy** - Chart-level container-query overrides for theme and components
12
13
  - **Type Safe** - Written in TypeScript with full type definitions
13
14
  - **Data Validation** - Built-in validation with helpful error messages
14
15
  - **Auto Colors** - Smart color palette with sensible defaults
@@ -19,6 +20,42 @@ A framework-agnostic, composable charting library built on D3.js with TypeScript
19
20
  npm install @internetstiftelsen/charts
20
21
  ```
21
22
 
23
+ ## Local Development
24
+
25
+ ```bash
26
+ pnpm dev
27
+ ```
28
+
29
+ Runs the interactive demo app (`index.html`) with sidebar controls and
30
+ Chart/Data/Showcase tabs.
31
+
32
+ ```bash
33
+ pnpm dev:docs
34
+ ```
35
+
36
+ Runs the marketing landing page (`docs.html`) built on
37
+ `@internetstiftelsen/styleguide`.
38
+
39
+ ## Build Targets
40
+
41
+ ```bash
42
+ pnpm build
43
+ ```
44
+
45
+ Builds the publishable chart library output into `dist`.
46
+
47
+ ```bash
48
+ pnpm build:docs
49
+ ```
50
+
51
+ Builds the static marketing site into `dist-docs` (used for Pages deploys).
52
+
53
+ ```bash
54
+ pnpm build:demo
55
+ ```
56
+
57
+ Builds the demo app using the default Vite config.
58
+
22
59
  ## Quick Start
23
60
 
24
61
  ```javascript
@@ -57,6 +94,29 @@ await chart.export('pdf', { download: true, pdfMargin: 16 });
57
94
  `xlsx` and `pdf` are lazy-loaded and require optional dependencies (`xlsx` and
58
95
  `jspdf`) only when those formats are used.
59
96
 
97
+ ## Import
98
+
99
+ `toChartData()` converts tab-delimited string input into chart JSON data.
100
+ It auto-detects grouped and normal (flat) table layouts.
101
+
102
+ ```typescript
103
+ import { toChartData } from '@internetstiftelsen/charts/utils';
104
+ import { XYChart } from '@internetstiftelsen/charts/xy-chart';
105
+
106
+ const data = toChartData(
107
+ '\t\tDaily\tWeekly\nAll users\tSegment A\t85%\t92%\n\tSegment B\t84%\t91%',
108
+ {
109
+ categoryKey: 'Category',
110
+ },
111
+ );
112
+
113
+ const chart = new XYChart({ data });
114
+ chart.render('#chart-container');
115
+ ```
116
+
117
+ The parser supports JSON-escaped string payloads and grouped carry-forward row
118
+ structure (blank first column on continuation rows).
119
+
60
120
  ## Documentation
61
121
 
62
122
  - [Getting Started](./docs/getting-started.md) - Installation, Vanilla JS, React integration
package/base-chart.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Selection } from 'd3';
2
- import type { ChartData, DataItem, ChartTheme, AxisScaleConfig, ExportFormat, ExportOptions, D3Scale, ExportHookContext, ExportRenderContext } from './types.js';
2
+ import type { ChartData, DataItem, ChartTheme, AxisScaleConfig, ExportFormat, ExportOptions, D3Scale, ExportHookContext, ExportRenderContext, LegendItem, LegendSeries, ResponsiveConfig, ResponsiveRenderContext, DeepPartial } 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';
@@ -14,10 +14,15 @@ type RenderDimensions = {
14
14
  height: number;
15
15
  svgHeightAttr: number | string;
16
16
  };
17
+ type ResponsiveOverrides = {
18
+ theme?: DeepPartial<ChartTheme>;
19
+ components: Map<ChartComponent, Record<string, unknown>>;
20
+ };
17
21
  export type BaseChartConfig = {
18
22
  data: ChartData;
19
23
  theme?: Partial<ChartTheme>;
20
24
  scales?: AxisScaleConfig;
25
+ responsive?: ResponsiveConfig;
21
26
  };
22
27
  /**
23
28
  * Base chart class that provides common functionality for all chart types
@@ -27,6 +32,7 @@ export declare abstract class BaseChart {
27
32
  protected sourceData: ChartData;
28
33
  protected readonly theme: ChartTheme;
29
34
  protected readonly scaleConfig: AxisScaleConfig;
35
+ protected readonly responsiveConfig?: ResponsiveConfig;
30
36
  protected width: number;
31
37
  protected height: number;
32
38
  protected xAxis: XAxis | null;
@@ -43,6 +49,7 @@ export declare abstract class BaseChart {
43
49
  protected resizeObserver: ResizeObserver | null;
44
50
  protected layoutManager: LayoutManager;
45
51
  protected plotArea: PlotAreaBounds | null;
52
+ private disconnectedLegendContainer;
46
53
  protected constructor(config: BaseChartConfig);
47
54
  /**
48
55
  * Adds a component (axis, grid, tooltip, etc.) to the chart
@@ -58,6 +65,13 @@ export declare abstract class BaseChart {
58
65
  */
59
66
  private performRender;
60
67
  protected resolveRenderDimensions(containerRect: DOMRect): RenderDimensions;
68
+ protected resolveResponsiveContext(context: {
69
+ width: number;
70
+ height: number;
71
+ }): ResponsiveRenderContext;
72
+ private resolveBreakpointName;
73
+ private resolveRenderTheme;
74
+ private applyThemeOverride;
61
75
  /**
62
76
  * Get layout-aware components in order
63
77
  * Override in subclasses to provide chart-specific components
@@ -65,7 +79,11 @@ export declare abstract class BaseChart {
65
79
  protected getLayoutComponents(): LayoutAwareComponent[];
66
80
  protected getExportComponents(): ChartComponent[];
67
81
  protected collectExportOverrides(context: ExportRenderContext): Map<ChartComponent, Record<string, unknown>>;
82
+ protected collectResponsiveOverrides(context: ResponsiveRenderContext): ResponsiveOverrides;
68
83
  protected runExportHooks(context: ExportHookContext): void;
84
+ private mergeComponentOverrideMaps;
85
+ private createOverrideComponents;
86
+ protected applyComponentOverrides(overrides: Map<ChartComponent, ChartComponent>): () => void;
69
87
  private renderExportChart;
70
88
  protected prepareLayout(): void;
71
89
  /**
@@ -77,6 +95,13 @@ export declare abstract class BaseChart {
77
95
  */
78
96
  protected abstract renderChart(): void;
79
97
  protected abstract createExportChart(): BaseChart;
98
+ protected getLegendSeries(): LegendSeries[];
99
+ getLegendItems(): LegendItem[];
100
+ isLegendSeriesVisible(dataKey: string): boolean;
101
+ setLegendSeriesVisible(dataKey: string, visible: boolean): this;
102
+ toggleLegendSeries(dataKey: string): this;
103
+ setLegendVisibility(visibility: Record<string, boolean>): this;
104
+ onLegendChange(callback: () => void): () => void;
80
105
  /**
81
106
  * Updates the chart with new data
82
107
  */
@@ -85,6 +110,9 @@ export declare abstract class BaseChart {
85
110
  * Destroys the chart and cleans up resources
86
111
  */
87
112
  destroy(): void;
113
+ private renderDisconnectedLegend;
114
+ private resolveDisconnectedLegendHost;
115
+ private cleanupDisconnectedLegendContainer;
88
116
  protected parseValue(value: unknown): number;
89
117
  /**
90
118
  * Exports the chart in the specified format
package/base-chart.js CHANGED
@@ -7,6 +7,7 @@ import { exportRasterBlob } from './export-image.js';
7
7
  import { exportXLSXBlob } from './export-xlsx.js';
8
8
  import { exportPDFBlob } from './export-pdf.js';
9
9
  import { normalizeChartData } from './grouped-data.js';
10
+ import { mergeDeep } from './utils.js';
10
11
  /**
11
12
  * Base chart class that provides common functionality for all chart types
12
13
  */
@@ -36,6 +37,12 @@ export class BaseChart {
36
37
  writable: true,
37
38
  value: void 0
38
39
  });
40
+ Object.defineProperty(this, "responsiveConfig", {
41
+ enumerable: true,
42
+ configurable: true,
43
+ writable: true,
44
+ value: void 0
45
+ });
39
46
  Object.defineProperty(this, "width", {
40
47
  enumerable: true,
41
48
  configurable: true,
@@ -132,6 +139,12 @@ export class BaseChart {
132
139
  writable: true,
133
140
  value: null
134
141
  });
142
+ Object.defineProperty(this, "disconnectedLegendContainer", {
143
+ enumerable: true,
144
+ configurable: true,
145
+ writable: true,
146
+ value: null
147
+ });
135
148
  const normalized = normalizeChartData(config.data);
136
149
  ChartValidator.validateData(normalized.data);
137
150
  this.sourceData = config.data;
@@ -140,6 +153,7 @@ export class BaseChart {
140
153
  this.width = this.theme.width;
141
154
  this.height = this.theme.height;
142
155
  this.scaleConfig = config.scales || {};
156
+ this.responsiveConfig = config.responsive;
143
157
  this.layoutManager = new LayoutManager(this.theme);
144
158
  }
145
159
  /**
@@ -147,6 +161,7 @@ export class BaseChart {
147
161
  */
148
162
  render(target) {
149
163
  const container = this.resolveContainer(target);
164
+ this.cleanupDisconnectedLegendContainer();
150
165
  this.container = container;
151
166
  container.innerHTML = '';
152
167
  // Perform initial render
@@ -178,27 +193,45 @@ export class BaseChart {
178
193
  const dimensions = this.resolveRenderDimensions(this.container.getBoundingClientRect());
179
194
  this.width = dimensions.width;
180
195
  this.height = dimensions.height;
181
- // Clear and setup SVG
182
- this.container.innerHTML = '';
183
- this.svg = create('svg')
184
- .attr('width', '100%')
185
- .attr('height', dimensions.svgHeightAttr)
186
- .style('display', 'block');
187
- this.container.appendChild(this.svg.node());
188
- this.prepareLayout();
189
- // Calculate layout
190
- const layoutTheme = {
191
- ...this.theme,
196
+ const sizeContext = {
192
197
  width: this.width,
193
198
  height: this.height,
194
199
  };
195
- this.layoutManager = new LayoutManager(layoutTheme);
196
- const components = this.getLayoutComponents();
197
- this.plotArea = this.layoutManager.calculateLayout(components);
198
- // Create plot group
199
- this.plotGroup = this.svg.append('g').attr('class', 'chart-plot');
200
- // Render chart content
201
- this.renderChart();
200
+ const responsiveContext = this.resolveResponsiveContext(sizeContext);
201
+ const responsiveOverrides = this.collectResponsiveOverrides(responsiveContext);
202
+ const mergedComponentOverrides = this.mergeComponentOverrideMaps(responsiveOverrides.components);
203
+ const renderTheme = this.resolveRenderTheme(responsiveOverrides);
204
+ const overrideComponents = this.createOverrideComponents(mergedComponentOverrides);
205
+ const restoreComponents = this.applyComponentOverrides(overrideComponents);
206
+ const restoreTheme = this.applyThemeOverride(renderTheme);
207
+ try {
208
+ // Clear and setup SVG
209
+ this.container.innerHTML = '';
210
+ this.svg = create('svg')
211
+ .attr('width', '100%')
212
+ .attr('height', dimensions.svgHeightAttr)
213
+ .style('display', 'block');
214
+ this.container.appendChild(this.svg.node());
215
+ this.prepareLayout();
216
+ // Calculate layout
217
+ const layoutTheme = {
218
+ ...this.theme,
219
+ width: this.width,
220
+ height: this.height,
221
+ };
222
+ this.layoutManager = new LayoutManager(layoutTheme);
223
+ const components = this.getLayoutComponents();
224
+ this.plotArea = this.layoutManager.calculateLayout(components);
225
+ // Create plot group
226
+ this.plotGroup = this.svg.append('g').attr('class', 'chart-plot');
227
+ // Render chart content
228
+ this.renderChart();
229
+ this.renderDisconnectedLegend();
230
+ }
231
+ finally {
232
+ restoreComponents();
233
+ restoreTheme();
234
+ }
202
235
  }
203
236
  resolveRenderDimensions(containerRect) {
204
237
  const width = containerRect.width || this.theme.width;
@@ -209,6 +242,44 @@ export class BaseChart {
209
242
  svgHeightAttr: '100%',
210
243
  };
211
244
  }
245
+ resolveResponsiveContext(context) {
246
+ return {
247
+ ...context,
248
+ breakpoint: this.resolveBreakpointName(context.width),
249
+ };
250
+ }
251
+ resolveBreakpointName(width) {
252
+ const breakpoints = this.responsiveConfig?.breakpoints;
253
+ if (!breakpoints) {
254
+ return null;
255
+ }
256
+ const sorted = Object.entries(breakpoints)
257
+ .filter(([, minWidth]) => Number.isFinite(minWidth))
258
+ .sort((a, b) => a[1] - b[1]);
259
+ let active = null;
260
+ sorted.forEach(([name, minWidth]) => {
261
+ if (width >= minWidth) {
262
+ active = name;
263
+ }
264
+ });
265
+ return active;
266
+ }
267
+ resolveRenderTheme(responsiveOverrides) {
268
+ if (!responsiveOverrides.theme) {
269
+ return this.theme;
270
+ }
271
+ return mergeDeep(this.theme, responsiveOverrides.theme);
272
+ }
273
+ applyThemeOverride(theme) {
274
+ if (theme === this.theme) {
275
+ return () => { };
276
+ }
277
+ const originalTheme = this.theme;
278
+ this.theme = theme;
279
+ return () => {
280
+ this.theme = originalTheme;
281
+ };
282
+ }
212
283
  /**
213
284
  * Get layout-aware components in order
214
285
  * Override in subclasses to provide chart-specific components
@@ -224,7 +295,7 @@ export class BaseChart {
224
295
  if (this.yAxis) {
225
296
  components.push(this.yAxis);
226
297
  }
227
- if (this.legend) {
298
+ if (this.legend?.isInlineMode()) {
228
299
  components.push(this.legend);
229
300
  }
230
301
  return components;
@@ -266,12 +337,120 @@ export class BaseChart {
266
337
  });
267
338
  return overrides;
268
339
  }
340
+ collectResponsiveOverrides(context) {
341
+ const beforeRender = this.responsiveConfig?.beforeRender;
342
+ const components = this.getExportComponents();
343
+ const componentOverrides = new Map();
344
+ if (!beforeRender) {
345
+ return {
346
+ components: componentOverrides,
347
+ };
348
+ }
349
+ const snapshots = components.map((component, index) => {
350
+ const exportable = component;
351
+ const currentConfig = exportable.getExportConfig?.() ?? {};
352
+ const dataKey = component.dataKey;
353
+ return {
354
+ index,
355
+ type: component.type,
356
+ dataKey: typeof dataKey === 'string' ? dataKey : undefined,
357
+ currentConfig,
358
+ };
359
+ });
360
+ const result = beforeRender(context, {
361
+ theme: this.theme,
362
+ components: snapshots,
363
+ });
364
+ if (!result || typeof result !== 'object') {
365
+ return {
366
+ components: componentOverrides,
367
+ };
368
+ }
369
+ result.components?.forEach((entry) => {
370
+ const match = entry.match ?? {};
371
+ const override = entry.override;
372
+ if (!override || typeof override !== 'object') {
373
+ return;
374
+ }
375
+ components.forEach((component, index) => {
376
+ const dataKey = component.dataKey;
377
+ const matchesIndex = match.index === undefined || match.index === index;
378
+ const matchesType = match.type === undefined || match.type === component.type;
379
+ const matchesDataKey = match.dataKey === undefined ||
380
+ (typeof dataKey === 'string' && dataKey === match.dataKey);
381
+ if (!matchesIndex || !matchesType || !matchesDataKey) {
382
+ return;
383
+ }
384
+ const existing = componentOverrides.get(component);
385
+ componentOverrides.set(component, existing ? mergeDeep(existing, override) : { ...override });
386
+ });
387
+ });
388
+ return {
389
+ theme: result.theme,
390
+ components: componentOverrides,
391
+ };
392
+ }
269
393
  runExportHooks(context) {
270
394
  const components = this.getExportComponents();
271
395
  components.forEach((component) => {
272
396
  component.exportHooks?.before?.call(component, context);
273
397
  });
274
398
  }
399
+ mergeComponentOverrideMaps(...maps) {
400
+ const merged = new Map();
401
+ maps.forEach((map) => {
402
+ map.forEach((override, component) => {
403
+ const existing = merged.get(component);
404
+ merged.set(component, existing ? mergeDeep(existing, override) : { ...override });
405
+ });
406
+ });
407
+ return merged;
408
+ }
409
+ createOverrideComponents(overrides) {
410
+ const overrideComponents = new Map();
411
+ overrides.forEach((override, component) => {
412
+ const exportable = component;
413
+ if (!exportable.createExportComponent) {
414
+ return;
415
+ }
416
+ overrideComponents.set(component, exportable.createExportComponent(override));
417
+ });
418
+ return overrideComponents;
419
+ }
420
+ applyComponentOverrides(overrides) {
421
+ if (overrides.size === 0) {
422
+ return () => { };
423
+ }
424
+ const previousState = {
425
+ title: this.title,
426
+ grid: this.grid,
427
+ xAxis: this.xAxis,
428
+ yAxis: this.yAxis,
429
+ tooltip: this.tooltip,
430
+ legend: this.legend,
431
+ };
432
+ const resolve = (component) => {
433
+ if (!component) {
434
+ return component;
435
+ }
436
+ const override = overrides.get(component);
437
+ return override ?? component;
438
+ };
439
+ this.title = resolve(this.title);
440
+ this.grid = resolve(this.grid);
441
+ this.xAxis = resolve(this.xAxis);
442
+ this.yAxis = resolve(this.yAxis);
443
+ this.tooltip = resolve(this.tooltip);
444
+ this.legend = resolve(this.legend);
445
+ return () => {
446
+ this.title = previousState.title;
447
+ this.grid = previousState.grid;
448
+ this.xAxis = previousState.xAxis;
449
+ this.yAxis = previousState.yAxis;
450
+ this.tooltip = previousState.tooltip;
451
+ this.legend = previousState.legend;
452
+ };
453
+ }
275
454
  renderExportChart(chart, width, height) {
276
455
  const container = document.createElement('div');
277
456
  const containerId = `chart-export-${Math.random()
@@ -310,6 +489,45 @@ export class BaseChart {
310
489
  this.resizeObserver = new ResizeObserver(() => this.performRender());
311
490
  this.resizeObserver.observe(this.container);
312
491
  }
492
+ getLegendSeries() {
493
+ return [];
494
+ }
495
+ getLegendItems() {
496
+ if (!this.legend) {
497
+ return [];
498
+ }
499
+ return this.getLegendSeries().map((series) => {
500
+ return {
501
+ dataKey: series.dataKey,
502
+ color: series.stroke || series.fill || '#8884d8',
503
+ visible: this.legend.isSeriesVisible(series.dataKey),
504
+ };
505
+ });
506
+ }
507
+ isLegendSeriesVisible(dataKey) {
508
+ if (!this.legend) {
509
+ return true;
510
+ }
511
+ return this.legend.isSeriesVisible(dataKey);
512
+ }
513
+ setLegendSeriesVisible(dataKey, visible) {
514
+ this.legend?.setSeriesVisible(dataKey, visible);
515
+ return this;
516
+ }
517
+ toggleLegendSeries(dataKey) {
518
+ this.legend?.toggleSeries(dataKey);
519
+ return this;
520
+ }
521
+ setLegendVisibility(visibility) {
522
+ this.legend?.setVisibilityMap(visibility);
523
+ return this;
524
+ }
525
+ onLegendChange(callback) {
526
+ if (!this.legend) {
527
+ return () => { };
528
+ }
529
+ return this.legend.subscribe(callback);
530
+ }
313
531
  /**
314
532
  * Updates the chart with new data
315
533
  */
@@ -335,12 +553,82 @@ export class BaseChart {
335
553
  if (this.container) {
336
554
  this.container.innerHTML = '';
337
555
  }
556
+ this.cleanupDisconnectedLegendContainer();
338
557
  this.svg = null;
339
558
  this.plotGroup = null;
340
559
  this.plotArea = null;
341
560
  this.x = null;
342
561
  this.y = null;
343
562
  }
563
+ renderDisconnectedLegend() {
564
+ if (!this.legend || !this.svg || !this.container) {
565
+ this.cleanupDisconnectedLegendContainer();
566
+ return;
567
+ }
568
+ if (!this.legend.isDisconnectedMode()) {
569
+ this.cleanupDisconnectedLegendContainer();
570
+ return;
571
+ }
572
+ const series = this.getLegendSeries();
573
+ if (!series.length) {
574
+ this.cleanupDisconnectedLegendContainer();
575
+ return;
576
+ }
577
+ const legendHost = this.resolveDisconnectedLegendHost();
578
+ if (!legendHost) {
579
+ this.cleanupDisconnectedLegendContainer();
580
+ return;
581
+ }
582
+ legendHost.innerHTML = '';
583
+ const legendSvg = create('svg')
584
+ .attr('width', '100%')
585
+ .style('display', 'block');
586
+ legendHost.appendChild(legendSvg.node());
587
+ const svgNode = legendSvg.node();
588
+ if (!svgNode) {
589
+ return;
590
+ }
591
+ this.legend.estimateLayoutSpace(series, this.theme, this.width, svgNode);
592
+ const legendHeight = Math.max(1, this.legend.getMeasuredHeight());
593
+ legendSvg.attr('height', legendHeight);
594
+ this.legend.render(legendSvg, series, this.theme, this.width);
595
+ }
596
+ resolveDisconnectedLegendHost() {
597
+ if (!this.legend || !this.container) {
598
+ return null;
599
+ }
600
+ const customTarget = this.legend.getDisconnectedTarget();
601
+ if (customTarget instanceof HTMLElement) {
602
+ this.disconnectedLegendContainer = customTarget;
603
+ return customTarget;
604
+ }
605
+ if (typeof customTarget === 'string') {
606
+ const target = document.querySelector(customTarget);
607
+ if (target instanceof HTMLElement) {
608
+ this.disconnectedLegendContainer = target;
609
+ return target;
610
+ }
611
+ }
612
+ if (!this.disconnectedLegendContainer) {
613
+ const autoContainer = document.createElement('div');
614
+ autoContainer.className = 'chart-disconnected-legend';
615
+ this.container.insertAdjacentElement('afterend', autoContainer);
616
+ this.disconnectedLegendContainer = autoContainer;
617
+ }
618
+ return this.disconnectedLegendContainer;
619
+ }
620
+ cleanupDisconnectedLegendContainer() {
621
+ if (!this.disconnectedLegendContainer) {
622
+ return;
623
+ }
624
+ if (this.disconnectedLegendContainer.classList.contains('chart-disconnected-legend')) {
625
+ this.disconnectedLegendContainer.remove();
626
+ }
627
+ else {
628
+ this.disconnectedLegendContainer.innerHTML = '';
629
+ }
630
+ this.disconnectedLegendContainer = null;
631
+ }
344
632
  parseValue(value) {
345
633
  if (typeof value === 'string') {
346
634
  return parseFloat(value);
@@ -387,7 +675,9 @@ export class BaseChart {
387
675
  */
388
676
  downloadContent(content, format, options) {
389
677
  const mimeType = this.getMimeType(format);
390
- const blob = content instanceof Blob ? content : new Blob([content], { type: mimeType });
678
+ const blob = content instanceof Blob
679
+ ? content
680
+ : new Blob([content], { type: mimeType });
391
681
  const filename = options.filename || `chart.${format}`;
392
682
  const url = URL.createObjectURL(blob);
393
683
  const link = document.createElement('a');
@@ -434,7 +724,8 @@ export class BaseChart {
434
724
  async exportImage(format, options) {
435
725
  const { width, height } = this.exportSize(options);
436
726
  const svg = this.exportSVG(options, format);
437
- const backgroundColor = options?.backgroundColor ?? (format === 'jpg' ? '#ffffff' : undefined);
727
+ const backgroundColor = options?.backgroundColor ??
728
+ (format === 'jpg' ? '#ffffff' : undefined);
438
729
  return exportRasterBlob({
439
730
  format,
440
731
  svg,
@@ -67,7 +67,8 @@ export class DonutCenterContent {
67
67
  const style = this.config.mainValueStyle;
68
68
  elements.push({
69
69
  text: this.mainValue,
70
- fontSize: (style?.fontSize ?? defaults.mainValue.fontSize) * fontScale,
70
+ fontSize: (style?.fontSize ?? defaults.mainValue.fontSize) *
71
+ fontScale,
71
72
  fontWeight: style?.fontWeight ?? defaults.mainValue.fontWeight,
72
73
  fontFamily: style?.fontFamily ??
73
74
  defaults.mainValue.fontFamily ??
package/donut-chart.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { DataItem } from './types.js';
1
+ import type { DataItem, LegendSeries } from './types.js';
2
2
  import { BaseChart, type BaseChartConfig } from './base-chart.js';
3
3
  import type { ChartComponent, LayoutAwareComponent } from './chart-interface.js';
4
4
  export type DonutConfig = {
@@ -28,9 +28,10 @@ export declare class DonutChart extends BaseChart {
28
28
  protected getLayoutComponents(): LayoutAwareComponent[];
29
29
  protected prepareLayout(): void;
30
30
  protected createExportChart(): BaseChart;
31
+ protected applyComponentOverrides(overrides: Map<ChartComponent, ChartComponent>): () => void;
31
32
  protected renderChart(): void;
32
33
  private resolveFontScale;
33
- private getLegendSeries;
34
+ protected getLegendSeries(): LegendSeries[];
34
35
  private positionTooltip;
35
36
  private buildTooltipContent;
36
37
  private renderSegments;