@internetstiftelsen/charts 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -8
  3. package/{area.d.ts → dist/area.d.ts} +1 -2
  4. package/{area.js → dist/area.js} +2 -19
  5. package/{bar.d.ts → dist/bar.d.ts} +3 -5
  6. package/{bar.js → dist/bar.js} +8 -33
  7. package/{base-chart.d.ts → dist/base-chart.d.ts} +75 -14
  8. package/{base-chart.js → dist/base-chart.js} +429 -122
  9. package/dist/chart-interface.d.ts +19 -0
  10. package/{donut-center-content.d.ts → dist/donut-center-content.d.ts} +1 -1
  11. package/dist/donut-chart.d.ts +51 -0
  12. package/dist/donut-chart.js +374 -0
  13. package/{gauge-chart.d.ts → dist/gauge-chart.d.ts} +19 -8
  14. package/{gauge-chart.js → dist/gauge-chart.js} +317 -106
  15. package/{grid.d.ts → dist/grid.d.ts} +1 -1
  16. package/{layout-manager.d.ts → dist/layout-manager.d.ts} +5 -5
  17. package/{legend.d.ts → dist/legend.d.ts} +3 -1
  18. package/{legend.js → dist/legend.js} +32 -0
  19. package/{line.d.ts → dist/line.d.ts} +1 -1
  20. package/{line.js → dist/line.js} +3 -25
  21. package/{pie-chart.d.ts → dist/pie-chart.d.ts} +10 -21
  22. package/{pie-chart.js → dist/pie-chart.js} +51 -172
  23. package/dist/radial-chart-base.d.ts +25 -0
  24. package/dist/radial-chart-base.js +79 -0
  25. package/dist/scale-utils.d.ts +3 -0
  26. package/dist/scale-utils.js +14 -0
  27. package/{theme.d.ts → dist/theme.d.ts} +2 -0
  28. package/{theme.js → dist/theme.js} +24 -29
  29. package/{title.d.ts → dist/title.d.ts} +1 -1
  30. package/{tooltip.d.ts → dist/tooltip.d.ts} +1 -1
  31. package/{tooltip.js → dist/tooltip.js} +239 -74
  32. package/{types.d.ts → dist/types.d.ts} +27 -10
  33. package/{utils.d.ts → dist/utils.d.ts} +7 -2
  34. package/{utils.js → dist/utils.js} +24 -5
  35. package/dist/word-cloud-chart.d.ts +32 -0
  36. package/dist/word-cloud-chart.js +201 -0
  37. package/{x-axis.d.ts → dist/x-axis.d.ts} +2 -1
  38. package/{x-axis.js → dist/x-axis.js} +18 -14
  39. package/{xy-chart.d.ts → dist/xy-chart.d.ts} +14 -9
  40. package/{xy-chart.js → dist/xy-chart.js} +107 -130
  41. package/{y-axis.d.ts → dist/y-axis.d.ts} +1 -1
  42. package/{y-axis.js → dist/y-axis.js} +4 -4
  43. package/package.json +39 -35
  44. package/chart-interface.d.ts +0 -13
  45. package/donut-chart.d.ts +0 -38
  46. package/donut-chart.js +0 -316
  47. /package/{chart-interface.js → dist/chart-interface.js} +0 -0
  48. /package/{donut-center-content.js → dist/donut-center-content.js} +0 -0
  49. /package/{export-image.d.ts → dist/export-image.d.ts} +0 -0
  50. /package/{export-image.js → dist/export-image.js} +0 -0
  51. /package/{export-pdf.d.ts → dist/export-pdf.d.ts} +0 -0
  52. /package/{export-pdf.js → dist/export-pdf.js} +0 -0
  53. /package/{export-tabular.d.ts → dist/export-tabular.d.ts} +0 -0
  54. /package/{export-tabular.js → dist/export-tabular.js} +0 -0
  55. /package/{export-xlsx.d.ts → dist/export-xlsx.d.ts} +0 -0
  56. /package/{export-xlsx.js → dist/export-xlsx.js} +0 -0
  57. /package/{grid.js → dist/grid.js} +0 -0
  58. /package/{grouped-data.d.ts → dist/grouped-data.d.ts} +0 -0
  59. /package/{grouped-data.js → dist/grouped-data.js} +0 -0
  60. /package/{grouped-tabular.d.ts → dist/grouped-tabular.d.ts} +0 -0
  61. /package/{grouped-tabular.js → dist/grouped-tabular.js} +0 -0
  62. /package/{layout-manager.js → dist/layout-manager.js} +0 -0
  63. /package/{title.js → dist/title.js} +0 -0
  64. /package/{types.js → dist/types.js} +0 -0
  65. /package/{validation.d.ts → dist/validation.d.ts} +0 -0
  66. /package/{validation.js → dist/validation.js} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { create } from 'd3';
2
- import { defaultTheme } from './theme.js';
2
+ import { defaultTheme, DEFAULT_CHART_HEIGHT, DEFAULT_CHART_WIDTH, } from './theme.js';
3
3
  import { ChartValidator } from './validation.js';
4
4
  import { LayoutManager } from './layout-manager.js';
5
5
  import { serializeCSV } from './export-tabular.js';
@@ -8,6 +8,37 @@ import { exportXLSXBlob } from './export-xlsx.js';
8
8
  import { exportPDFBlob } from './export-pdf.js';
9
9
  import { normalizeChartData } from './grouped-data.js';
10
10
  import { mergeDeep } from './utils.js';
11
+ function normalizeChartDimension(value, name) {
12
+ if (value === undefined) {
13
+ return undefined;
14
+ }
15
+ if (!Number.isFinite(value) || value <= 0) {
16
+ throw new Error(`Chart ${name} must be a positive finite number`);
17
+ }
18
+ return value;
19
+ }
20
+ function normalizeResponsiveBreakpoint(definition) {
21
+ if (typeof definition === 'number') {
22
+ return Number.isFinite(definition) ? { minWidth: definition } : null;
23
+ }
24
+ if (!definition || typeof definition !== 'object') {
25
+ return null;
26
+ }
27
+ const minWidth = Number.isFinite(definition.minWidth)
28
+ ? definition.minWidth
29
+ : undefined;
30
+ const maxWidth = Number.isFinite(definition.maxWidth)
31
+ ? definition.maxWidth
32
+ : undefined;
33
+ if (minWidth === undefined && maxWidth === undefined) {
34
+ return null;
35
+ }
36
+ return {
37
+ ...definition,
38
+ minWidth,
39
+ maxWidth,
40
+ };
41
+ }
11
42
  /**
12
43
  * Base chart class that provides common functionality for all chart types
13
44
  */
@@ -25,6 +56,18 @@ export class BaseChart {
25
56
  writable: true,
26
57
  value: void 0
27
58
  });
59
+ Object.defineProperty(this, "configuredWidth", {
60
+ enumerable: true,
61
+ configurable: true,
62
+ writable: true,
63
+ value: void 0
64
+ });
65
+ Object.defineProperty(this, "configuredHeight", {
66
+ enumerable: true,
67
+ configurable: true,
68
+ writable: true,
69
+ value: void 0
70
+ });
28
71
  Object.defineProperty(this, "theme", {
29
72
  enumerable: true,
30
73
  configurable: true,
@@ -139,22 +182,49 @@ export class BaseChart {
139
182
  writable: true,
140
183
  value: null
141
184
  });
185
+ Object.defineProperty(this, "readyPromise", {
186
+ enumerable: true,
187
+ configurable: true,
188
+ writable: true,
189
+ value: Promise.resolve()
190
+ });
142
191
  Object.defineProperty(this, "disconnectedLegendContainer", {
143
192
  enumerable: true,
144
193
  configurable: true,
145
194
  writable: true,
146
195
  value: null
147
196
  });
197
+ Object.defineProperty(this, "renderThemeOverride", {
198
+ enumerable: true,
199
+ configurable: true,
200
+ writable: true,
201
+ value: null
202
+ });
203
+ Object.defineProperty(this, "renderSizeOverride", {
204
+ enumerable: true,
205
+ configurable: true,
206
+ writable: true,
207
+ value: null
208
+ });
148
209
  const normalized = normalizeChartData(config.data);
149
210
  ChartValidator.validateData(normalized.data);
150
211
  this.sourceData = config.data;
151
212
  this.data = normalized.data;
152
- this.theme = { ...defaultTheme, ...config.theme };
153
- this.width = this.theme.width;
154
- this.height = this.theme.height;
213
+ this.configuredWidth = normalizeChartDimension(config.width, 'width');
214
+ this.configuredHeight = normalizeChartDimension(config.height, 'height');
215
+ this.theme = mergeDeep(defaultTheme, config.theme);
216
+ this.width = this.configuredWidth ?? DEFAULT_CHART_WIDTH;
217
+ this.height = this.configuredHeight ?? DEFAULT_CHART_HEIGHT;
155
218
  this.scaleConfig = config.scales || {};
156
219
  this.responsiveConfig = config.responsive;
157
- this.layoutManager = new LayoutManager(this.theme);
220
+ this.layoutManager = new LayoutManager(this.resolvedRenderTheme);
221
+ }
222
+ /**
223
+ * Adds a component (axis, grid, tooltip, etc.) to the chart
224
+ */
225
+ addChild(component) {
226
+ this.registerBaseComponent(component);
227
+ return this;
158
228
  }
159
229
  /**
160
230
  * Renders the chart to the specified target element
@@ -203,29 +273,41 @@ export class BaseChart {
203
273
  const renderTheme = this.resolveRenderTheme(responsiveOverrides);
204
274
  const overrideComponents = this.createOverrideComponents(mergedComponentOverrides);
205
275
  const restoreComponents = this.applyComponentOverrides(overrideComponents);
206
- const restoreTheme = this.applyThemeOverride(renderTheme);
276
+ const restoreTheme = this.applyRenderTheme(renderTheme);
207
277
  try {
278
+ this.setReadyPromise(Promise.resolve());
208
279
  // Clear and setup SVG
209
280
  this.container.innerHTML = '';
210
281
  this.svg = create('svg')
211
- .attr('width', '100%')
282
+ .attr('width', dimensions.svgWidthAttr)
212
283
  .attr('height', dimensions.svgHeightAttr)
284
+ .attr('role', 'img')
285
+ .attr('aria-label', this.resolveAccessibleLabel())
213
286
  .style('display', 'block');
214
287
  this.container.appendChild(this.svg.node());
215
- this.prepareLayout();
288
+ const svgNode = this.svg.node();
289
+ if (!svgNode) {
290
+ throw new Error('Failed to initialize chart SVG');
291
+ }
292
+ this.prepareLayout({
293
+ svg: this.svg,
294
+ svgNode,
295
+ });
216
296
  // Calculate layout
217
- const layoutTheme = {
218
- ...this.theme,
219
- width: this.width,
220
- height: this.height,
221
- };
222
- this.layoutManager = new LayoutManager(layoutTheme);
297
+ this.layoutManager = new LayoutManager(this.resolvedRenderTheme);
223
298
  const components = this.getLayoutComponents();
224
- this.plotArea = this.layoutManager.calculateLayout(components);
299
+ const plotArea = this.layoutManager.calculateLayout(components);
300
+ this.plotArea = plotArea;
225
301
  // Create plot group
226
- this.plotGroup = this.svg.append('g').attr('class', 'chart-plot');
302
+ const plotGroup = this.svg.append('g').attr('class', 'chart-plot');
303
+ this.plotGroup = plotGroup;
227
304
  // Render chart content
228
- this.renderChart();
305
+ this.renderChart({
306
+ svg: this.svg,
307
+ svgNode,
308
+ plotGroup,
309
+ plotArea,
310
+ });
229
311
  this.renderDisconnectedLegend();
230
312
  }
231
313
  finally {
@@ -234,35 +316,65 @@ export class BaseChart {
234
316
  }
235
317
  }
236
318
  resolveRenderDimensions(containerRect) {
237
- const width = containerRect.width || this.theme.width;
238
- const height = containerRect.height || this.theme.height;
319
+ const width = this.renderSizeOverride?.width ??
320
+ this.configuredWidth ??
321
+ (containerRect.width || DEFAULT_CHART_WIDTH);
322
+ const height = this.renderSizeOverride?.height ??
323
+ this.configuredHeight ??
324
+ (containerRect.height || DEFAULT_CHART_HEIGHT);
239
325
  return {
240
326
  width,
241
327
  height,
242
- svgHeightAttr: '100%',
328
+ svgWidthAttr: this.renderSizeOverride?.width ??
329
+ this.configuredWidth ??
330
+ '100%',
331
+ svgHeightAttr: this.renderSizeOverride?.height ??
332
+ this.configuredHeight ??
333
+ '100%',
243
334
  };
244
335
  }
336
+ resolveAccessibleLabel() {
337
+ const titleText = this.title?.text.trim();
338
+ if (titleText) {
339
+ return titleText;
340
+ }
341
+ return 'Chart';
342
+ }
343
+ syncAccessibleLabelFromSvg(svg) {
344
+ const titleText = svg.querySelector('.title text')?.textContent?.trim();
345
+ if (titleText) {
346
+ svg.setAttribute('aria-label', titleText);
347
+ return;
348
+ }
349
+ svg.setAttribute('aria-label', this.resolveAccessibleLabel());
350
+ }
245
351
  resolveResponsiveContext(context) {
352
+ const activeBreakpoints = this.resolveActiveBreakpoints(context.width).map(({ name }) => name);
246
353
  return {
247
354
  ...context,
248
- breakpoint: this.resolveBreakpointName(context.width),
355
+ activeBreakpoints,
356
+ breakpoint: activeBreakpoints.length > 0
357
+ ? activeBreakpoints[activeBreakpoints.length - 1]
358
+ : null,
249
359
  };
250
360
  }
251
- resolveBreakpointName(width) {
361
+ resolveActiveBreakpoints(width) {
252
362
  const breakpoints = this.responsiveConfig?.breakpoints;
253
363
  if (!breakpoints) {
254
- return null;
364
+ return [];
255
365
  }
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;
366
+ return Object.entries(breakpoints).flatMap(([name, definition]) => {
367
+ const config = normalizeResponsiveBreakpoint(definition);
368
+ if (!config) {
369
+ return [];
370
+ }
371
+ const matchesMinWidth = config.minWidth === undefined || width >= config.minWidth;
372
+ const matchesMaxWidth = config.maxWidth === undefined || width <= config.maxWidth;
373
+ if (!matchesMinWidth || !matchesMaxWidth) {
374
+ return [];
263
375
  }
376
+ return [{ name, config }];
264
377
  });
265
- return active;
266
378
  }
267
379
  resolveRenderTheme(responsiveOverrides) {
268
380
  if (!responsiveOverrides.theme) {
@@ -270,14 +382,23 @@ export class BaseChart {
270
382
  }
271
383
  return mergeDeep(this.theme, responsiveOverrides.theme);
272
384
  }
273
- applyThemeOverride(theme) {
385
+ applyRenderTheme(theme) {
274
386
  if (theme === this.theme) {
275
387
  return () => { };
276
388
  }
277
- const originalTheme = this.theme;
278
- this.theme = theme;
389
+ this.renderThemeOverride = theme;
279
390
  return () => {
280
- this.theme = originalTheme;
391
+ this.renderThemeOverride = null;
392
+ };
393
+ }
394
+ get renderTheme() {
395
+ return this.renderThemeOverride ?? this.theme;
396
+ }
397
+ get resolvedRenderTheme() {
398
+ return {
399
+ ...this.renderTheme,
400
+ width: this.width,
401
+ height: this.height,
281
402
  };
282
403
  }
283
404
  /**
@@ -285,50 +406,74 @@ export class BaseChart {
285
406
  * Override in subclasses to provide chart-specific components
286
407
  */
287
408
  getLayoutComponents() {
409
+ return this.getBaseLayoutComponents({
410
+ title: true,
411
+ xAxis: true,
412
+ yAxis: true,
413
+ inlineLegend: true,
414
+ });
415
+ }
416
+ getBaseLayoutComponents(options) {
288
417
  const components = [];
289
- if (this.title) {
418
+ if (options.title && this.title) {
290
419
  components.push(this.title);
291
420
  }
292
- if (this.xAxis) {
421
+ if (options.xAxis && this.xAxis) {
293
422
  components.push(this.xAxis);
294
423
  }
295
- if (this.yAxis) {
424
+ if (options.yAxis && this.yAxis) {
296
425
  components.push(this.yAxis);
297
426
  }
298
- if (this.legend?.isInlineMode()) {
427
+ if (options.inlineLegend && this.legend?.isInlineMode()) {
299
428
  components.push(this.legend);
300
429
  }
301
430
  return components;
302
431
  }
303
432
  getExportComponents() {
433
+ return this.getBaseExportComponents({
434
+ title: true,
435
+ grid: true,
436
+ xAxis: true,
437
+ yAxis: true,
438
+ tooltip: true,
439
+ legend: true,
440
+ });
441
+ }
442
+ getOverrideableComponents() {
443
+ return this.getExportComponents();
444
+ }
445
+ getBaseExportComponents(options) {
304
446
  const components = [];
305
- if (this.title) {
447
+ if (options.title && this.title) {
306
448
  components.push(this.title);
307
449
  }
308
- if (this.grid) {
450
+ if (options.grid && this.grid) {
309
451
  components.push(this.grid);
310
452
  }
311
- if (this.xAxis) {
453
+ if (options.xAxis && this.xAxis) {
312
454
  components.push(this.xAxis);
313
455
  }
314
- if (this.yAxis) {
456
+ if (options.yAxis && this.yAxis) {
315
457
  components.push(this.yAxis);
316
458
  }
317
- if (this.tooltip) {
459
+ if (options.tooltip && this.tooltip) {
318
460
  components.push(this.tooltip);
319
461
  }
320
- if (this.legend) {
462
+ if (options.legend && this.legend) {
321
463
  components.push(this.legend);
322
464
  }
323
465
  return components;
324
466
  }
467
+ registerBaseComponent(component) {
468
+ return this.tryRegisterComponent(component, this.getBaseComponentSlots());
469
+ }
325
470
  collectExportOverrides(context) {
326
471
  const overrides = new Map();
327
472
  const components = this.getExportComponents();
328
473
  components.forEach((component) => {
329
474
  const exportable = component;
330
475
  const currentConfig = exportable.getExportConfig?.() ?? {};
331
- const result = component.exportHooks?.beforeRender?.call(component, context, currentConfig);
476
+ const result = exportable.exportHooks?.beforeRender?.call(component, context, currentConfig);
332
477
  if (result &&
333
478
  typeof result === 'object' &&
334
479
  exportable.createExportComponent) {
@@ -339,34 +484,66 @@ export class BaseChart {
339
484
  }
340
485
  collectResponsiveOverrides(context) {
341
486
  const beforeRender = this.responsiveConfig?.beforeRender;
342
- const components = this.getExportComponents();
343
- const componentOverrides = new Map();
487
+ const components = this.getOverrideableComponents();
488
+ const breakpointOverrides = this.collectResponsiveBreakpointOverrides(context, components);
344
489
  if (!beforeRender) {
345
- return {
346
- components: componentOverrides,
347
- };
490
+ return breakpointOverrides;
491
+ }
492
+ const effectiveTheme = breakpointOverrides.theme
493
+ ? mergeDeep(this.theme, breakpointOverrides.theme)
494
+ : this.theme;
495
+ const snapshots = this.createResponsiveComponentSnapshots(components, breakpointOverrides.components);
496
+ const result = beforeRender(context, {
497
+ theme: effectiveTheme,
498
+ components: snapshots,
499
+ });
500
+ if (!result || typeof result !== 'object') {
501
+ return breakpointOverrides;
348
502
  }
349
- const snapshots = components.map((component, index) => {
503
+ const componentOverrides = this.mergeComponentOverrideMaps(breakpointOverrides.components);
504
+ this.applyResponsiveComponentOverrideEntries(result.components, components, componentOverrides);
505
+ return {
506
+ theme: breakpointOverrides.theme
507
+ ? mergeDeep(breakpointOverrides.theme, result.theme)
508
+ : result.theme,
509
+ components: componentOverrides,
510
+ };
511
+ }
512
+ collectResponsiveBreakpointOverrides(context, components) {
513
+ const matchedBreakpoints = this.resolveActiveBreakpoints(context.width);
514
+ const componentOverrides = new Map();
515
+ let themeOverride;
516
+ matchedBreakpoints.forEach(({ config }) => {
517
+ if (config.theme) {
518
+ themeOverride = themeOverride
519
+ ? mergeDeep(themeOverride, config.theme)
520
+ : config.theme;
521
+ }
522
+ this.applyResponsiveComponentOverrideEntries(config.components, components, componentOverrides);
523
+ });
524
+ return {
525
+ theme: themeOverride,
526
+ components: componentOverrides,
527
+ };
528
+ }
529
+ createResponsiveComponentSnapshots(components, overrides) {
530
+ return components.map((component, index) => {
350
531
  const exportable = component;
351
532
  const currentConfig = exportable.getExportConfig?.() ?? {};
352
533
  const dataKey = component.dataKey;
534
+ const override = overrides.get(component);
353
535
  return {
354
536
  index,
355
537
  type: component.type,
356
538
  dataKey: typeof dataKey === 'string' ? dataKey : undefined,
357
- currentConfig,
539
+ currentConfig: override
540
+ ? mergeDeep(currentConfig, override)
541
+ : currentConfig,
358
542
  };
359
543
  });
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) => {
544
+ }
545
+ applyResponsiveComponentOverrideEntries(entries, components, overrides) {
546
+ entries?.forEach((entry) => {
370
547
  const match = entry.match ?? {};
371
548
  const override = entry.override;
372
549
  if (!override || typeof override !== 'object') {
@@ -381,19 +558,16 @@ export class BaseChart {
381
558
  if (!matchesIndex || !matchesType || !matchesDataKey) {
382
559
  return;
383
560
  }
384
- const existing = componentOverrides.get(component);
385
- componentOverrides.set(component, existing ? mergeDeep(existing, override) : { ...override });
561
+ const existing = overrides.get(component);
562
+ overrides.set(component, existing ? mergeDeep(existing, override) : { ...override });
386
563
  });
387
564
  });
388
- return {
389
- theme: result.theme,
390
- components: componentOverrides,
391
- };
392
565
  }
393
566
  runExportHooks(context) {
394
567
  const components = this.getExportComponents();
395
568
  components.forEach((component) => {
396
- component.exportHooks?.before?.call(component, context);
569
+ const exportable = component;
570
+ exportable.exportHooks?.before?.call(component, context);
397
571
  });
398
572
  }
399
573
  mergeComponentOverrideMaps(...maps) {
@@ -418,38 +592,7 @@ export class BaseChart {
418
592
  return overrideComponents;
419
593
  }
420
594
  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
- };
595
+ return this.applySlotOverrides(overrides, this.getBaseComponentSlots());
453
596
  }
454
597
  renderExportChart(chart, width, height) {
455
598
  const container = document.createElement('div');
@@ -465,18 +608,59 @@ export class BaseChart {
465
608
  container.style.visibility = 'hidden';
466
609
  document.body.appendChild(container);
467
610
  chart.render(`#${containerId}`);
468
- const svg = chart.svg?.node();
469
- if (!svg) {
611
+ return chart
612
+ .whenReady()
613
+ .then(() => {
614
+ const svg = chart.svg?.node();
615
+ if (!svg) {
616
+ throw new Error('Failed to render export SVG');
617
+ }
618
+ return svg;
619
+ })
620
+ .finally(() => {
470
621
  chart.destroy();
471
622
  document.body.removeChild(container);
472
- throw new Error('Failed to render export SVG');
623
+ });
624
+ }
625
+ renderTitle(svg) {
626
+ if (!this.title) {
627
+ return;
473
628
  }
474
- chart.destroy();
475
- document.body.removeChild(container);
476
- return svg;
629
+ const position = this.layoutManager.getComponentPosition(this.title);
630
+ this.title.render(svg, this.renderTheme, this.width, position.x, position.y);
631
+ }
632
+ renderInlineLegend(svg) {
633
+ if (!this.legend?.isInlineMode()) {
634
+ return;
635
+ }
636
+ const position = this.layoutManager.getComponentPosition(this.legend);
637
+ this.legend.render(svg, this.getLegendSeries(), this.renderTheme, this.width, position.x, position.y);
638
+ }
639
+ measureInlineLegend(svgNode) {
640
+ if (!this.legend?.isInlineMode()) {
641
+ return;
642
+ }
643
+ this.legend.estimateLayoutSpace(this.getLegendSeries(), this.renderTheme, this.width, svgNode);
644
+ }
645
+ filterVisibleItems(items, getDataKey) {
646
+ const { legend } = this;
647
+ if (!legend) {
648
+ return items;
649
+ }
650
+ return items.filter((item) => {
651
+ return legend.isSeriesVisible(getDataKey(item));
652
+ });
653
+ }
654
+ validateSourceData(_data) { }
655
+ syncDerivedState(_previousData) { }
656
+ initializeDataState() {
657
+ this.validateSourceData(this.sourceData);
658
+ this.syncDerivedState();
477
659
  }
478
660
  // Hook for subclasses to update component layout estimates before layout calc
479
- prepareLayout() { }
661
+ prepareLayout(context) {
662
+ this.measureInlineLegend(context.svgNode);
663
+ }
480
664
  /**
481
665
  * Setup ResizeObserver for automatic resize handling
482
666
  */
@@ -486,9 +670,15 @@ export class BaseChart {
486
670
  if (this.resizeObserver) {
487
671
  this.resizeObserver.disconnect();
488
672
  }
489
- this.resizeObserver = new ResizeObserver(() => this.performRender());
673
+ this.resizeObserver = new ResizeObserver(() => this.rerender());
490
674
  this.resizeObserver.observe(this.container);
491
675
  }
676
+ setReadyPromise(promise) {
677
+ this.readyPromise = promise;
678
+ }
679
+ whenReady() {
680
+ return this.readyPromise;
681
+ }
492
682
  getLegendSeries() {
493
683
  return [];
494
684
  }
@@ -532,14 +722,17 @@ export class BaseChart {
532
722
  * Updates the chart with new data
533
723
  */
534
724
  update(data) {
725
+ if (!this.container) {
726
+ throw new Error('Chart must be rendered before update()');
727
+ }
728
+ this.validateSourceData(data);
535
729
  const normalized = normalizeChartData(data);
536
730
  ChartValidator.validateData(normalized.data);
731
+ const previousData = this.data;
537
732
  this.sourceData = data;
538
733
  this.data = normalized.data;
539
- if (!this.container) {
540
- throw new Error('Chart must be rendered before update()');
541
- }
542
- this.performRender();
734
+ this.syncDerivedState(previousData);
735
+ this.rerender();
543
736
  }
544
737
  /**
545
738
  * Destroys the chart and cleans up resources
@@ -588,10 +781,10 @@ export class BaseChart {
588
781
  if (!svgNode) {
589
782
  return;
590
783
  }
591
- this.legend.estimateLayoutSpace(series, this.theme, this.width, svgNode);
784
+ this.legend.estimateLayoutSpace(series, this.renderTheme, this.width, svgNode);
592
785
  const legendHeight = Math.max(1, this.legend.getMeasuredHeight());
593
786
  legendSvg.attr('height', legendHeight);
594
- this.legend.render(legendSvg, series, this.theme, this.width);
787
+ this.legend.render(legendSvg, series, this.renderTheme, this.width);
595
788
  }
596
789
  resolveDisconnectedLegendHost() {
597
790
  if (!this.legend || !this.container) {
@@ -638,6 +831,109 @@ export class BaseChart {
638
831
  }
639
832
  return 0;
640
833
  }
834
+ rerender() {
835
+ if (!this.container) {
836
+ return;
837
+ }
838
+ this.performRender();
839
+ }
840
+ tryRegisterComponent(component, slots) {
841
+ const slot = slots.find((entry) => entry.type === component.type);
842
+ if (!slot) {
843
+ return false;
844
+ }
845
+ slot.set(component);
846
+ slot.onRegister?.(component);
847
+ return true;
848
+ }
849
+ applySlotOverrides(overrides, slots) {
850
+ if (overrides.size === 0) {
851
+ return () => { };
852
+ }
853
+ const previousState = new Map();
854
+ slots.forEach((slot) => {
855
+ previousState.set(slot, slot.get());
856
+ const current = slot.get();
857
+ if (!current) {
858
+ return;
859
+ }
860
+ const override = overrides.get(current);
861
+ if (override) {
862
+ slot.set(override);
863
+ }
864
+ });
865
+ return () => {
866
+ slots.forEach((slot) => {
867
+ slot.set(previousState.get(slot) ?? null);
868
+ });
869
+ };
870
+ }
871
+ applyArrayComponentOverrides(components, overrides, isComponent) {
872
+ if (overrides.size === 0) {
873
+ return () => { };
874
+ }
875
+ const previousState = [...components];
876
+ components.forEach((component, index) => {
877
+ const override = overrides.get(component);
878
+ if (override && isComponent(override)) {
879
+ components[index] = override;
880
+ }
881
+ });
882
+ return () => {
883
+ components.splice(0, components.length, ...previousState);
884
+ };
885
+ }
886
+ getBaseComponentSlots() {
887
+ return [
888
+ {
889
+ type: 'title',
890
+ get: () => this.title,
891
+ set: (component) => {
892
+ this.title = component;
893
+ },
894
+ },
895
+ {
896
+ type: 'grid',
897
+ get: () => this.grid,
898
+ set: (component) => {
899
+ this.grid = component;
900
+ },
901
+ },
902
+ {
903
+ type: 'xAxis',
904
+ get: () => this.xAxis,
905
+ set: (component) => {
906
+ this.xAxis = component;
907
+ },
908
+ },
909
+ {
910
+ type: 'yAxis',
911
+ get: () => this.yAxis,
912
+ set: (component) => {
913
+ this.yAxis = component;
914
+ },
915
+ },
916
+ {
917
+ type: 'tooltip',
918
+ get: () => this.tooltip,
919
+ set: (component) => {
920
+ this.tooltip = component;
921
+ },
922
+ },
923
+ {
924
+ type: 'legend',
925
+ get: () => this.legend,
926
+ set: (component) => {
927
+ this.legend = component;
928
+ },
929
+ onRegister: (component) => {
930
+ component.setToggleCallback(() => {
931
+ this.rerender();
932
+ });
933
+ },
934
+ },
935
+ ];
936
+ }
641
937
  /**
642
938
  * Exports the chart in the specified format
643
939
  * @param format - The export format
@@ -647,7 +943,7 @@ export class BaseChart {
647
943
  async export(format, options) {
648
944
  let content;
649
945
  if (format === 'svg') {
650
- content = this.exportSVG(options, 'svg');
946
+ content = await this.exportSVG(options, 'svg');
651
947
  }
652
948
  else if (format === 'json') {
653
949
  content = this.exportJSON();
@@ -723,7 +1019,7 @@ export class BaseChart {
723
1019
  }
724
1020
  async exportImage(format, options) {
725
1021
  const { width, height } = this.exportSize(options);
726
- const svg = this.exportSVG(options, format);
1022
+ const svg = await this.exportSVG(options, format);
727
1023
  const backgroundColor = options?.backgroundColor ??
728
1024
  (format === 'jpg' ? '#ffffff' : undefined);
729
1025
  return exportRasterBlob({
@@ -738,7 +1034,7 @@ export class BaseChart {
738
1034
  }
739
1035
  async exportPDF(options) {
740
1036
  const { width, height } = this.exportSize(options);
741
- const svg = this.exportSVG(options, 'pdf');
1037
+ const svg = await this.exportSVG(options, 'pdf');
742
1038
  const pngBlob = await exportRasterBlob({
743
1039
  format: 'png',
744
1040
  svg,
@@ -754,14 +1050,19 @@ export class BaseChart {
754
1050
  margin: options?.pdfMargin ?? 0,
755
1051
  });
756
1052
  }
757
- exportSVG(options, formatForHooks = 'svg') {
1053
+ async exportSVG(options, formatForHooks = 'svg') {
758
1054
  if (!this.svg) {
759
1055
  throw new Error('Chart must be rendered before export');
760
1056
  }
1057
+ await this.whenReady();
1058
+ const liveSvg = this.svg?.node();
1059
+ if (!liveSvg) {
1060
+ throw new Error('Chart must remain mounted until export completes');
1061
+ }
761
1062
  const exportWidth = options?.width ?? this.width;
762
1063
  const exportHeight = options?.height ?? this.height;
763
1064
  const requiresExportRender = exportWidth !== this.width || exportHeight !== this.height;
764
- const clone = this.svg.node().cloneNode(true);
1065
+ const clone = liveSvg.cloneNode(true);
765
1066
  clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
766
1067
  clone.setAttribute('width', String(exportWidth));
767
1068
  clone.setAttribute('height', String(exportHeight));
@@ -777,9 +1078,14 @@ export class BaseChart {
777
1078
  ...baseContext,
778
1079
  svg: clone,
779
1080
  });
1081
+ this.syncAccessibleLabelFromSvg(clone);
780
1082
  return clone.outerHTML;
781
1083
  }
782
1084
  const exportChart = this.createExportChart();
1085
+ exportChart.renderSizeOverride = {
1086
+ width: exportWidth,
1087
+ height: exportHeight,
1088
+ };
783
1089
  const components = this.getExportComponents();
784
1090
  components.forEach((component) => {
785
1091
  const exportable = component;
@@ -791,7 +1097,7 @@ export class BaseChart {
791
1097
  exportChart.addChild(component);
792
1098
  }
793
1099
  });
794
- const exportSvg = this.renderExportChart(exportChart, exportWidth, exportHeight);
1100
+ const exportSvg = await this.renderExportChart(exportChart, exportWidth, exportHeight);
795
1101
  exportSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
796
1102
  exportSvg.setAttribute('width', String(exportWidth));
797
1103
  exportSvg.setAttribute('height', String(exportHeight));
@@ -799,6 +1105,7 @@ export class BaseChart {
799
1105
  ...baseContext,
800
1106
  svg: exportSvg,
801
1107
  });
1108
+ this.syncAccessibleLabelFromSvg(exportSvg);
802
1109
  return exportSvg.outerHTML;
803
1110
  }
804
1111
  exportJSON() {