@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.
- package/LICENSE +21 -0
- package/README.md +97 -8
- package/{area.d.ts → dist/area.d.ts} +1 -2
- package/{area.js → dist/area.js} +2 -19
- package/{bar.d.ts → dist/bar.d.ts} +3 -5
- package/{bar.js → dist/bar.js} +8 -33
- package/{base-chart.d.ts → dist/base-chart.d.ts} +75 -14
- package/{base-chart.js → dist/base-chart.js} +429 -122
- package/dist/chart-interface.d.ts +19 -0
- package/{donut-center-content.d.ts → dist/donut-center-content.d.ts} +1 -1
- package/dist/donut-chart.d.ts +51 -0
- package/dist/donut-chart.js +374 -0
- package/{gauge-chart.d.ts → dist/gauge-chart.d.ts} +19 -8
- package/{gauge-chart.js → dist/gauge-chart.js} +317 -106
- package/{grid.d.ts → dist/grid.d.ts} +1 -1
- package/{layout-manager.d.ts → dist/layout-manager.d.ts} +5 -5
- package/{legend.d.ts → dist/legend.d.ts} +3 -1
- package/{legend.js → dist/legend.js} +32 -0
- package/{line.d.ts → dist/line.d.ts} +1 -1
- package/{line.js → dist/line.js} +3 -25
- package/{pie-chart.d.ts → dist/pie-chart.d.ts} +10 -21
- package/{pie-chart.js → dist/pie-chart.js} +51 -172
- package/dist/radial-chart-base.d.ts +25 -0
- package/dist/radial-chart-base.js +79 -0
- package/dist/scale-utils.d.ts +3 -0
- package/dist/scale-utils.js +14 -0
- package/{theme.d.ts → dist/theme.d.ts} +2 -0
- package/{theme.js → dist/theme.js} +24 -29
- package/{title.d.ts → dist/title.d.ts} +1 -1
- package/{tooltip.d.ts → dist/tooltip.d.ts} +1 -1
- package/{tooltip.js → dist/tooltip.js} +239 -74
- package/{types.d.ts → dist/types.d.ts} +27 -10
- package/{utils.d.ts → dist/utils.d.ts} +7 -2
- package/{utils.js → dist/utils.js} +24 -5
- package/dist/word-cloud-chart.d.ts +32 -0
- package/dist/word-cloud-chart.js +201 -0
- package/{x-axis.d.ts → dist/x-axis.d.ts} +2 -1
- package/{x-axis.js → dist/x-axis.js} +18 -14
- package/{xy-chart.d.ts → dist/xy-chart.d.ts} +14 -9
- package/{xy-chart.js → dist/xy-chart.js} +107 -130
- package/{y-axis.d.ts → dist/y-axis.d.ts} +1 -1
- package/{y-axis.js → dist/y-axis.js} +4 -4
- package/package.json +39 -35
- package/chart-interface.d.ts +0 -13
- package/donut-chart.d.ts +0 -38
- package/donut-chart.js +0 -316
- /package/{chart-interface.js → dist/chart-interface.js} +0 -0
- /package/{donut-center-content.js → dist/donut-center-content.js} +0 -0
- /package/{export-image.d.ts → dist/export-image.d.ts} +0 -0
- /package/{export-image.js → dist/export-image.js} +0 -0
- /package/{export-pdf.d.ts → dist/export-pdf.d.ts} +0 -0
- /package/{export-pdf.js → dist/export-pdf.js} +0 -0
- /package/{export-tabular.d.ts → dist/export-tabular.d.ts} +0 -0
- /package/{export-tabular.js → dist/export-tabular.js} +0 -0
- /package/{export-xlsx.d.ts → dist/export-xlsx.d.ts} +0 -0
- /package/{export-xlsx.js → dist/export-xlsx.js} +0 -0
- /package/{grid.js → dist/grid.js} +0 -0
- /package/{grouped-data.d.ts → dist/grouped-data.d.ts} +0 -0
- /package/{grouped-data.js → dist/grouped-data.js} +0 -0
- /package/{grouped-tabular.d.ts → dist/grouped-tabular.d.ts} +0 -0
- /package/{grouped-tabular.js → dist/grouped-tabular.js} +0 -0
- /package/{layout-manager.js → dist/layout-manager.js} +0 -0
- /package/{title.js → dist/title.js} +0 -0
- /package/{types.js → dist/types.js} +0 -0
- /package/{validation.d.ts → dist/validation.d.ts} +0 -0
- /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.
|
|
153
|
-
this.
|
|
154
|
-
this.
|
|
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.
|
|
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.
|
|
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',
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
299
|
+
const plotArea = this.layoutManager.calculateLayout(components);
|
|
300
|
+
this.plotArea = plotArea;
|
|
225
301
|
// Create plot group
|
|
226
|
-
|
|
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 =
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
355
|
+
activeBreakpoints,
|
|
356
|
+
breakpoint: activeBreakpoints.length > 0
|
|
357
|
+
? activeBreakpoints[activeBreakpoints.length - 1]
|
|
358
|
+
: null,
|
|
249
359
|
};
|
|
250
360
|
}
|
|
251
|
-
|
|
361
|
+
resolveActiveBreakpoints(width) {
|
|
252
362
|
const breakpoints = this.responsiveConfig?.breakpoints;
|
|
253
363
|
if (!breakpoints) {
|
|
254
|
-
return
|
|
364
|
+
return [];
|
|
255
365
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
385
|
+
applyRenderTheme(theme) {
|
|
274
386
|
if (theme === this.theme) {
|
|
275
387
|
return () => { };
|
|
276
388
|
}
|
|
277
|
-
|
|
278
|
-
this.theme = theme;
|
|
389
|
+
this.renderThemeOverride = theme;
|
|
279
390
|
return () => {
|
|
280
|
-
this.
|
|
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 =
|
|
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.
|
|
343
|
-
const
|
|
487
|
+
const components = this.getOverrideableComponents();
|
|
488
|
+
const breakpointOverrides = this.collectResponsiveBreakpointOverrides(context, components);
|
|
344
489
|
if (!beforeRender) {
|
|
345
|
-
return
|
|
346
|
-
|
|
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
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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 =
|
|
385
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
469
|
-
|
|
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
|
-
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
renderTitle(svg) {
|
|
626
|
+
if (!this.title) {
|
|
627
|
+
return;
|
|
473
628
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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.
|
|
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
|
-
|
|
540
|
-
|
|
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.
|
|
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.
|
|
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 =
|
|
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() {
|