@internetstiftelsen/charts 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -1
- package/dist/base-chart.d.ts +50 -3
- package/dist/base-chart.js +188 -40
- package/dist/chart-group.d.ts +15 -2
- package/dist/chart-group.js +181 -45
- package/dist/chart-interface.d.ts +3 -3
- package/dist/grouped-data.d.ts +1 -0
- package/dist/grouped-data.js +80 -40
- package/dist/grouped-tabular.js +12 -3
- package/dist/layout-manager.js +4 -4
- package/dist/text.d.ts +40 -0
- package/dist/text.js +217 -0
- package/dist/theme.js +59 -3
- package/dist/title.d.ts +6 -12
- package/dist/title.js +29 -82
- package/dist/tooltip.d.ts +4 -2
- package/dist/tooltip.js +101 -77
- package/dist/types.d.ts +34 -1
- package/dist/xy-chart.js +1 -1
- package/docs/chart-group.md +24 -5
- package/docs/components.md +99 -15
- package/docs/donut-chart.md +2 -1
- package/docs/gauge-chart.md +2 -1
- package/docs/getting-started.md +25 -1
- package/docs/pie-chart.md +2 -1
- package/docs/theming.md +35 -0
- package/docs/word-cloud-chart.md +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -139,6 +139,19 @@ await chart.whenReady();
|
|
|
139
139
|
Animation is off by default, applies to XY series marks only, and visual
|
|
140
140
|
exports always render the final static state.
|
|
141
141
|
|
|
142
|
+
## Lifecycle Events
|
|
143
|
+
|
|
144
|
+
Charts expose `on()` and `off()` for lifecycle subscriptions.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
chart.on('ready', (event) => {
|
|
148
|
+
console.log(event.reason);
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
See [Getting Started](./docs/getting-started.md#lifecycle-events) for the
|
|
153
|
+
supported events and payloads.
|
|
154
|
+
|
|
142
155
|
## Lazy Loading
|
|
143
156
|
|
|
144
157
|
Use `mountChartWhenVisible` when you want the page to wait until a chart is
|
|
@@ -185,6 +198,7 @@ import { XYChart } from '@internetstiftelsen/charts/xy-chart';
|
|
|
185
198
|
import { Line } from '@internetstiftelsen/charts/line';
|
|
186
199
|
import { Bar } from '@internetstiftelsen/charts/bar';
|
|
187
200
|
import { Legend } from '@internetstiftelsen/charts/legend';
|
|
201
|
+
import { Text } from '@internetstiftelsen/charts/text';
|
|
188
202
|
import { Title } from '@internetstiftelsen/charts/title';
|
|
189
203
|
|
|
190
204
|
const lineChart = new XYChart({ data: lineData });
|
|
@@ -202,6 +216,14 @@ const group = new ChartGroup({
|
|
|
202
216
|
|
|
203
217
|
group
|
|
204
218
|
.addChild(new Title({ text: 'Revenue vs Expenses' }))
|
|
219
|
+
.addChild(
|
|
220
|
+
new Text({
|
|
221
|
+
text: 'Source: finance team',
|
|
222
|
+
position: 'bottom',
|
|
223
|
+
variant: 'caption',
|
|
224
|
+
align: 'left',
|
|
225
|
+
}),
|
|
226
|
+
)
|
|
205
227
|
.addChart(barChart)
|
|
206
228
|
.addChart(lineChart)
|
|
207
229
|
.addChild(new Legend());
|
|
@@ -413,7 +435,7 @@ Grouped parsing rules:
|
|
|
413
435
|
- [DonutChart](./docs/donut-chart.md) - Donut/pie charts API
|
|
414
436
|
- [PieChart](./docs/pie-chart.md) - Pie chart API
|
|
415
437
|
- [GaugeChart](./docs/gauge-chart.md) - Gauge chart API
|
|
416
|
-
- [Components](./docs/components.md) - Axes, Grid, Tooltip, Legend, Title
|
|
438
|
+
- [Components](./docs/components.md) - Axes, Grid, Tooltip, Legend, Text, Title
|
|
417
439
|
- [Theming](./docs/theming.md) - Colors, fonts, and styling
|
|
418
440
|
|
|
419
441
|
## Browser Support
|
package/dist/base-chart.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
|
-
import type { ChartData, DataItem, ChartTheme, ResolvedChartTheme, AxisScaleConfig, ExportFormat, ExportOptions, D3Scale, ExportHookContext, ExportRenderContext, LegendItem, LegendMode, LegendSeries, ResponsiveConfig, ResponsiveRenderContext, DeepPartial } from './types.js';
|
|
2
|
+
import type { ChartData, DataItem, ChartTheme, ResolvedChartTheme, AxisScaleConfig, ExportFormat, ExportOptions, D3Scale, ExportHookContext, ExportRenderContext, LegendItem, LegendMode, LegendSeries, ResponsiveConfig, ResponsiveRenderContext, DeepPartial, TextPosition } from './types.js';
|
|
3
3
|
import type { ChartComponentBase, LayoutAwareComponentBase } from './chart-interface.js';
|
|
4
4
|
import type { XAxis } from './x-axis.js';
|
|
5
5
|
import type { YAxis } from './y-axis.js';
|
|
@@ -34,6 +34,33 @@ type BaseExportComponentsOptions = {
|
|
|
34
34
|
tooltip?: boolean;
|
|
35
35
|
legend?: boolean;
|
|
36
36
|
};
|
|
37
|
+
export type ChartRenderReason = 'initial' | 'manual' | 'update' | 'resize' | 'legend' | 'component';
|
|
38
|
+
export type ChartRenderEventName = 'render:start' | 'render' | 'ready';
|
|
39
|
+
type ChartEventBase<TEventName extends string> = {
|
|
40
|
+
type: TEventName;
|
|
41
|
+
chart: BaseChart;
|
|
42
|
+
};
|
|
43
|
+
export type ChartRenderEvent<TEventName extends ChartRenderEventName> = ChartEventBase<TEventName> & {
|
|
44
|
+
reason: ChartRenderReason;
|
|
45
|
+
};
|
|
46
|
+
export type ChartDataEvent = ChartEventBase<'data'> & {
|
|
47
|
+
data: ChartData;
|
|
48
|
+
previousData: ChartData;
|
|
49
|
+
};
|
|
50
|
+
export type ChartLegendChangeEvent = ChartEventBase<'legend:change'>;
|
|
51
|
+
export type ChartDestroyEvent = ChartEventBase<'destroy'>;
|
|
52
|
+
export type ChartEventMap = {
|
|
53
|
+
'render:start': ChartRenderEvent<'render:start'>;
|
|
54
|
+
render: ChartRenderEvent<'render'>;
|
|
55
|
+
ready: ChartRenderEvent<'ready'>;
|
|
56
|
+
data: ChartDataEvent;
|
|
57
|
+
'legend:change': ChartLegendChangeEvent;
|
|
58
|
+
destroy: ChartDestroyEvent;
|
|
59
|
+
};
|
|
60
|
+
export type ChartEventName = keyof ChartEventMap;
|
|
61
|
+
export type ChartEventListener<TEventName extends ChartEventName> = (event: ChartEventMap[TEventName]) => void;
|
|
62
|
+
type ChartEventPayload<TEventName extends ChartEventName> = Omit<ChartEventMap[TEventName], 'type' | 'chart'>;
|
|
63
|
+
type ChartEventPayloadArgs<TEventName extends ChartEventName> = keyof ChartEventPayload<TEventName> extends never ? [payload?: ChartEventPayload<TEventName>] : [payload: ChartEventPayload<TEventName>];
|
|
37
64
|
export type BaseLayoutContext = {
|
|
38
65
|
svg: Selection<SVGSVGElement, undefined, null, undefined>;
|
|
39
66
|
svgNode: SVGSVGElement;
|
|
@@ -48,6 +75,13 @@ type ComponentSlot<TComponent extends ChartComponentBase = ChartComponentBase> =
|
|
|
48
75
|
set: (component: TComponent | null) => void;
|
|
49
76
|
onRegister?: (component: TComponent) => void;
|
|
50
77
|
};
|
|
78
|
+
type TextLayoutComponent = LayoutAwareComponentBase & ChartComponentBase & {
|
|
79
|
+
display: boolean;
|
|
80
|
+
text: string;
|
|
81
|
+
position: TextPosition;
|
|
82
|
+
variant: string;
|
|
83
|
+
render: (svg: Selection<SVGSVGElement, undefined, null, undefined>, theme: ChartTheme, width: number, x?: number, y?: number) => void;
|
|
84
|
+
};
|
|
51
85
|
export type BaseChartConfig = {
|
|
52
86
|
data: ChartData;
|
|
53
87
|
width?: number;
|
|
@@ -75,6 +109,7 @@ export declare abstract class BaseChart {
|
|
|
75
109
|
protected tooltip: Tooltip | null;
|
|
76
110
|
protected legend: Legend | null;
|
|
77
111
|
protected title: Title | null;
|
|
112
|
+
protected textComponents: TextLayoutComponent[];
|
|
78
113
|
protected svg: Selection<SVGSVGElement, undefined, null, undefined> | null;
|
|
79
114
|
protected plotGroup: Selection<SVGGElement, undefined, null, undefined> | null;
|
|
80
115
|
protected container: HTMLElement | null;
|
|
@@ -88,8 +123,12 @@ export declare abstract class BaseChart {
|
|
|
88
123
|
private disconnectedLegendContainer;
|
|
89
124
|
private renderThemeOverride;
|
|
90
125
|
private renderSizeOverride;
|
|
91
|
-
private readonly renderCallbacks;
|
|
92
126
|
private legendModeOverride;
|
|
127
|
+
private readonly eventListeners;
|
|
128
|
+
private renderId;
|
|
129
|
+
private readyEventVersion;
|
|
130
|
+
private isPerformingRender;
|
|
131
|
+
private currentRenderReason;
|
|
93
132
|
protected constructor(config: BaseChartConfig);
|
|
94
133
|
/**
|
|
95
134
|
* Adds a component (axis, grid, tooltip, etc.) to the chart
|
|
@@ -108,6 +147,7 @@ export declare abstract class BaseChart {
|
|
|
108
147
|
private pushIfIncluded;
|
|
109
148
|
private resolveAccessibleLabel;
|
|
110
149
|
private syncAccessibleLabelFromSvg;
|
|
150
|
+
private resolvePrimaryTextLabel;
|
|
111
151
|
protected resolveResponsiveContext(context: {
|
|
112
152
|
width: number;
|
|
113
153
|
height: number;
|
|
@@ -126,14 +166,19 @@ export declare abstract class BaseChart {
|
|
|
126
166
|
height?: number;
|
|
127
167
|
};
|
|
128
168
|
setLegendModeOverride(mode: LegendMode | null, rerender?: boolean): this;
|
|
169
|
+
on<TEventName extends ChartEventName>(eventName: TEventName, listener: ChartEventListener<TEventName>): this;
|
|
170
|
+
off<TEventName extends ChartEventName>(eventName: TEventName, listener: ChartEventListener<TEventName>): this;
|
|
129
171
|
onRender(callback: () => void): () => void;
|
|
172
|
+
protected emit<TEventName extends ChartEventName>(eventName: TEventName, ...[payload]: ChartEventPayloadArgs<TEventName>): void;
|
|
130
173
|
private notifyRendered;
|
|
174
|
+
private createRenderEventPayload;
|
|
131
175
|
/**
|
|
132
176
|
* Get layout-aware components in order
|
|
133
177
|
* Override in subclasses to provide chart-specific components
|
|
134
178
|
*/
|
|
135
179
|
protected getLayoutComponents(): LayoutAwareComponentBase[];
|
|
136
180
|
protected getBaseLayoutComponents(options: BaseLayoutComponentsOptions): LayoutAwareComponentBase[];
|
|
181
|
+
private getTextComponents;
|
|
137
182
|
protected getExportComponents(): ChartComponentBase[];
|
|
138
183
|
protected getOverrideableComponents(): ChartComponentBase[];
|
|
139
184
|
protected getBaseExportComponents(options: BaseExportComponentsOptions): ChartComponentBase[];
|
|
@@ -160,6 +205,7 @@ export declare abstract class BaseChart {
|
|
|
160
205
|
* Setup ResizeObserver for automatic resize handling
|
|
161
206
|
*/
|
|
162
207
|
private setupResizeObserver;
|
|
208
|
+
private notifyLegendChanged;
|
|
163
209
|
/**
|
|
164
210
|
* Subclasses must implement this method to define their rendering logic
|
|
165
211
|
*/
|
|
@@ -186,10 +232,11 @@ export declare abstract class BaseChart {
|
|
|
186
232
|
private resolveDisconnectedLegendHost;
|
|
187
233
|
private cleanupDisconnectedLegendContainer;
|
|
188
234
|
protected parseValue(value: unknown): number;
|
|
189
|
-
protected rerender(): void;
|
|
235
|
+
protected rerender(reason?: ChartRenderReason): void;
|
|
190
236
|
protected tryRegisterComponent(component: ChartComponentBase, slots: readonly ComponentSlot[]): boolean;
|
|
191
237
|
protected applySlotOverrides(overrides: Map<ChartComponentBase, ChartComponentBase>, slots: readonly ComponentSlot[]): () => void;
|
|
192
238
|
protected applyArrayComponentOverrides<TComponent extends ChartComponentBase>(components: TComponent[], overrides: Map<ChartComponentBase, ChartComponentBase>, isComponent: (component: ChartComponentBase) => component is TComponent): () => void;
|
|
239
|
+
private isTextComponent;
|
|
193
240
|
private getBaseComponentSlots;
|
|
194
241
|
/**
|
|
195
242
|
* Exports the chart in the specified format
|
package/dist/base-chart.js
CHANGED
|
@@ -135,6 +135,12 @@ export class BaseChart {
|
|
|
135
135
|
writable: true,
|
|
136
136
|
value: null
|
|
137
137
|
});
|
|
138
|
+
Object.defineProperty(this, "textComponents", {
|
|
139
|
+
enumerable: true,
|
|
140
|
+
configurable: true,
|
|
141
|
+
writable: true,
|
|
142
|
+
value: []
|
|
143
|
+
});
|
|
138
144
|
Object.defineProperty(this, "svg", {
|
|
139
145
|
enumerable: true,
|
|
140
146
|
configurable: true,
|
|
@@ -213,17 +219,41 @@ export class BaseChart {
|
|
|
213
219
|
writable: true,
|
|
214
220
|
value: null
|
|
215
221
|
});
|
|
216
|
-
Object.defineProperty(this, "
|
|
222
|
+
Object.defineProperty(this, "legendModeOverride", {
|
|
217
223
|
enumerable: true,
|
|
218
224
|
configurable: true,
|
|
219
225
|
writable: true,
|
|
220
|
-
value:
|
|
226
|
+
value: null
|
|
221
227
|
});
|
|
222
|
-
Object.defineProperty(this, "
|
|
228
|
+
Object.defineProperty(this, "eventListeners", {
|
|
223
229
|
enumerable: true,
|
|
224
230
|
configurable: true,
|
|
225
231
|
writable: true,
|
|
226
|
-
value:
|
|
232
|
+
value: new Map()
|
|
233
|
+
});
|
|
234
|
+
Object.defineProperty(this, "renderId", {
|
|
235
|
+
enumerable: true,
|
|
236
|
+
configurable: true,
|
|
237
|
+
writable: true,
|
|
238
|
+
value: 0
|
|
239
|
+
});
|
|
240
|
+
Object.defineProperty(this, "readyEventVersion", {
|
|
241
|
+
enumerable: true,
|
|
242
|
+
configurable: true,
|
|
243
|
+
writable: true,
|
|
244
|
+
value: 0
|
|
245
|
+
});
|
|
246
|
+
Object.defineProperty(this, "isPerformingRender", {
|
|
247
|
+
enumerable: true,
|
|
248
|
+
configurable: true,
|
|
249
|
+
writable: true,
|
|
250
|
+
value: false
|
|
251
|
+
});
|
|
252
|
+
Object.defineProperty(this, "currentRenderReason", {
|
|
253
|
+
enumerable: true,
|
|
254
|
+
configurable: true,
|
|
255
|
+
writable: true,
|
|
256
|
+
value: 'manual'
|
|
227
257
|
});
|
|
228
258
|
const normalized = normalizeChartData(config.data);
|
|
229
259
|
ChartValidator.validateData(normalized.data);
|
|
@@ -238,7 +268,8 @@ export class BaseChart {
|
|
|
238
268
|
this.responsiveConfig = config.responsive;
|
|
239
269
|
this.legendState = new LegendStateController();
|
|
240
270
|
this.legendState.subscribe(() => {
|
|
241
|
-
this.
|
|
271
|
+
this.notifyLegendChanged();
|
|
272
|
+
this.rerender('legend');
|
|
242
273
|
});
|
|
243
274
|
this.layoutManager = new LayoutManager(this.resolvedRenderTheme);
|
|
244
275
|
}
|
|
@@ -253,12 +284,13 @@ export class BaseChart {
|
|
|
253
284
|
* Renders the chart to the specified target element
|
|
254
285
|
*/
|
|
255
286
|
render(target) {
|
|
287
|
+
const reason = this.container ? 'manual' : 'initial';
|
|
256
288
|
const container = this.resolveContainer(target);
|
|
257
289
|
this.cleanupDisconnectedLegendContainer();
|
|
258
290
|
this.container = container;
|
|
259
291
|
container.innerHTML = '';
|
|
260
292
|
// Perform initial render
|
|
261
|
-
this.performRender();
|
|
293
|
+
this.performRender(reason);
|
|
262
294
|
// Set up ResizeObserver for automatic resize handling
|
|
263
295
|
this.setupResizeObserver();
|
|
264
296
|
return container;
|
|
@@ -279,11 +311,13 @@ export class BaseChart {
|
|
|
279
311
|
/**
|
|
280
312
|
* Performs the actual rendering logic
|
|
281
313
|
*/
|
|
282
|
-
performRender() {
|
|
314
|
+
performRender(reason) {
|
|
283
315
|
if (!this.container) {
|
|
284
316
|
return;
|
|
285
317
|
}
|
|
286
|
-
const
|
|
318
|
+
const container = this.container;
|
|
319
|
+
this.renderId += 1;
|
|
320
|
+
const dimensions = this.resolveRenderDimensions(container.getBoundingClientRect());
|
|
287
321
|
this.width = dimensions.width;
|
|
288
322
|
this.height = dimensions.height;
|
|
289
323
|
const sizeContext = {
|
|
@@ -297,17 +331,24 @@ export class BaseChart {
|
|
|
297
331
|
const overrideComponents = this.createOverrideComponents(mergedComponentOverrides);
|
|
298
332
|
const restoreComponents = this.applyComponentOverrides(overrideComponents);
|
|
299
333
|
const restoreTheme = this.applyRenderTheme(renderTheme);
|
|
334
|
+
let renderCompleted = false;
|
|
300
335
|
try {
|
|
336
|
+
this.currentRenderReason = reason;
|
|
337
|
+
this.isPerformingRender = true;
|
|
301
338
|
this.setReadyPromise(Promise.resolve());
|
|
339
|
+
this.svg = null;
|
|
340
|
+
this.plotGroup = null;
|
|
341
|
+
this.plotArea = null;
|
|
342
|
+
this.emit('render:start', this.createRenderEventPayload(reason));
|
|
302
343
|
// Clear and setup SVG
|
|
303
|
-
|
|
344
|
+
container.innerHTML = '';
|
|
304
345
|
this.svg = create('svg')
|
|
305
346
|
.attr('width', dimensions.svgWidthAttr)
|
|
306
347
|
.attr('height', dimensions.svgHeightAttr)
|
|
307
348
|
.attr('role', 'img')
|
|
308
349
|
.attr('aria-label', this.resolveAccessibleLabel())
|
|
309
350
|
.style('display', 'block');
|
|
310
|
-
|
|
351
|
+
container.appendChild(this.svg.node());
|
|
311
352
|
const svgNode = this.svg.node();
|
|
312
353
|
if (!svgNode) {
|
|
313
354
|
throw new Error('Failed to initialize chart SVG');
|
|
@@ -332,9 +373,14 @@ export class BaseChart {
|
|
|
332
373
|
plotArea,
|
|
333
374
|
});
|
|
334
375
|
this.renderDisconnectedLegend();
|
|
335
|
-
this.notifyRendered();
|
|
376
|
+
this.notifyRendered(reason);
|
|
377
|
+
renderCompleted = true;
|
|
336
378
|
}
|
|
337
379
|
finally {
|
|
380
|
+
if (!renderCompleted) {
|
|
381
|
+
this.readyEventVersion += 1;
|
|
382
|
+
}
|
|
383
|
+
this.isPerformingRender = false;
|
|
338
384
|
restoreComponents();
|
|
339
385
|
restoreTheme();
|
|
340
386
|
}
|
|
@@ -370,20 +416,31 @@ export class BaseChart {
|
|
|
370
416
|
}
|
|
371
417
|
}
|
|
372
418
|
resolveAccessibleLabel() {
|
|
373
|
-
const titleText = this.
|
|
419
|
+
const titleText = this.resolvePrimaryTextLabel();
|
|
374
420
|
if (titleText) {
|
|
375
421
|
return titleText;
|
|
376
422
|
}
|
|
377
423
|
return 'Chart';
|
|
378
424
|
}
|
|
379
425
|
syncAccessibleLabelFromSvg(svg) {
|
|
380
|
-
const titleText = svg
|
|
426
|
+
const titleText = svg
|
|
427
|
+
.querySelector('.title text, .text--title text')
|
|
428
|
+
?.textContent?.trim();
|
|
381
429
|
if (titleText) {
|
|
382
430
|
svg.setAttribute('aria-label', titleText);
|
|
383
431
|
return;
|
|
384
432
|
}
|
|
385
433
|
svg.setAttribute('aria-label', this.resolveAccessibleLabel());
|
|
386
434
|
}
|
|
435
|
+
resolvePrimaryTextLabel() {
|
|
436
|
+
return this.textComponents
|
|
437
|
+
.find((component) => {
|
|
438
|
+
return (component.display &&
|
|
439
|
+
(component.type === 'title' ||
|
|
440
|
+
component.variant === 'title'));
|
|
441
|
+
})
|
|
442
|
+
?.text.trim();
|
|
443
|
+
}
|
|
387
444
|
resolveResponsiveContext(context) {
|
|
388
445
|
const activeBreakpoints = this.resolveActiveBreakpoints(context.width).map(({ name }) => name);
|
|
389
446
|
return {
|
|
@@ -465,21 +522,61 @@ export class BaseChart {
|
|
|
465
522
|
}
|
|
466
523
|
this.legendModeOverride = mode;
|
|
467
524
|
if (rerender) {
|
|
468
|
-
this.rerender();
|
|
525
|
+
this.rerender('component');
|
|
526
|
+
}
|
|
527
|
+
return this;
|
|
528
|
+
}
|
|
529
|
+
on(eventName, listener) {
|
|
530
|
+
let listeners = this.eventListeners.get(eventName);
|
|
531
|
+
if (!listeners) {
|
|
532
|
+
listeners = new Set();
|
|
533
|
+
this.eventListeners.set(eventName, listeners);
|
|
534
|
+
}
|
|
535
|
+
listeners.add(listener);
|
|
536
|
+
return this;
|
|
537
|
+
}
|
|
538
|
+
off(eventName, listener) {
|
|
539
|
+
const listeners = this.eventListeners.get(eventName);
|
|
540
|
+
if (!listeners) {
|
|
541
|
+
return this;
|
|
542
|
+
}
|
|
543
|
+
listeners.delete(listener);
|
|
544
|
+
if (listeners.size === 0) {
|
|
545
|
+
this.eventListeners.delete(eventName);
|
|
469
546
|
}
|
|
470
547
|
return this;
|
|
471
548
|
}
|
|
472
549
|
onRender(callback) {
|
|
473
|
-
|
|
550
|
+
const listener = () => {
|
|
551
|
+
callback();
|
|
552
|
+
};
|
|
553
|
+
this.on('render', listener);
|
|
474
554
|
return () => {
|
|
475
|
-
this.
|
|
555
|
+
this.off('render', listener);
|
|
476
556
|
};
|
|
477
557
|
}
|
|
478
|
-
|
|
479
|
-
this.
|
|
480
|
-
|
|
558
|
+
emit(eventName, ...[payload]) {
|
|
559
|
+
const listeners = this.eventListeners.get(eventName);
|
|
560
|
+
if (!listeners) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const event = {
|
|
564
|
+
...(payload ?? {}),
|
|
565
|
+
type: eventName,
|
|
566
|
+
chart: this,
|
|
567
|
+
};
|
|
568
|
+
[...listeners].forEach((listener) => {
|
|
569
|
+
listener(event);
|
|
481
570
|
});
|
|
482
571
|
}
|
|
572
|
+
notifyRendered(reason) {
|
|
573
|
+
this.emit('render', this.createRenderEventPayload(reason));
|
|
574
|
+
}
|
|
575
|
+
createRenderEventPayload(reason) {
|
|
576
|
+
return {
|
|
577
|
+
reason,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
483
580
|
/**
|
|
484
581
|
* Get layout-aware components in order
|
|
485
582
|
* Override in subclasses to provide chart-specific components
|
|
@@ -494,8 +591,8 @@ export class BaseChart {
|
|
|
494
591
|
}
|
|
495
592
|
getBaseLayoutComponents(options) {
|
|
496
593
|
const components = [];
|
|
497
|
-
if (options.title
|
|
498
|
-
components.push(this.
|
|
594
|
+
if (options.title) {
|
|
595
|
+
components.push(...this.getTextComponents('top'));
|
|
499
596
|
}
|
|
500
597
|
if (options.xAxis && this.xAxis) {
|
|
501
598
|
components.push(this.xAxis);
|
|
@@ -506,8 +603,16 @@ export class BaseChart {
|
|
|
506
603
|
if (options.inlineLegend && this.legend && this.hasInlineLegend()) {
|
|
507
604
|
components.push(this.legend);
|
|
508
605
|
}
|
|
606
|
+
if (options.title) {
|
|
607
|
+
components.push(...this.getTextComponents('bottom'));
|
|
608
|
+
}
|
|
509
609
|
return components;
|
|
510
610
|
}
|
|
611
|
+
getTextComponents(position) {
|
|
612
|
+
return this.textComponents.filter((component) => {
|
|
613
|
+
return component.position === position;
|
|
614
|
+
});
|
|
615
|
+
}
|
|
511
616
|
getExportComponents() {
|
|
512
617
|
return this.getBaseExportComponents({
|
|
513
618
|
title: true,
|
|
@@ -523,7 +628,9 @@ export class BaseChart {
|
|
|
523
628
|
}
|
|
524
629
|
getBaseExportComponents(options) {
|
|
525
630
|
const components = [];
|
|
526
|
-
|
|
631
|
+
if (options.title) {
|
|
632
|
+
components.push(...this.textComponents);
|
|
633
|
+
}
|
|
527
634
|
this.pushIfIncluded(components, options.grid, this.grid);
|
|
528
635
|
this.pushIfIncluded(components, options.xAxis, this.xAxis);
|
|
529
636
|
this.pushIfIncluded(components, options.yAxis, this.yAxis);
|
|
@@ -532,6 +639,13 @@ export class BaseChart {
|
|
|
532
639
|
return components;
|
|
533
640
|
}
|
|
534
641
|
registerBaseComponent(component) {
|
|
642
|
+
if (this.isTextComponent(component)) {
|
|
643
|
+
this.textComponents.push(component);
|
|
644
|
+
if (component.type === 'title' && !this.title) {
|
|
645
|
+
this.title = component;
|
|
646
|
+
}
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
535
649
|
return this.tryRegisterComponent(component, this.getBaseComponentSlots());
|
|
536
650
|
}
|
|
537
651
|
collectExportOverrides(context) {
|
|
@@ -659,7 +773,18 @@ export class BaseChart {
|
|
|
659
773
|
return overrideComponents;
|
|
660
774
|
}
|
|
661
775
|
applyComponentOverrides(overrides) {
|
|
662
|
-
|
|
776
|
+
const previousTitle = this.title;
|
|
777
|
+
const restoreSlots = this.applySlotOverrides(overrides, this.getBaseComponentSlots());
|
|
778
|
+
const restoreText = this.applyArrayComponentOverrides(this.textComponents, overrides, this.isTextComponent);
|
|
779
|
+
this.title =
|
|
780
|
+
this.textComponents.find((component) => {
|
|
781
|
+
return component.type === 'title';
|
|
782
|
+
}) ?? null;
|
|
783
|
+
return () => {
|
|
784
|
+
restoreText();
|
|
785
|
+
restoreSlots();
|
|
786
|
+
this.title = previousTitle;
|
|
787
|
+
};
|
|
663
788
|
}
|
|
664
789
|
renderExportChart(chart, width, height) {
|
|
665
790
|
const container = document.createElement('div');
|
|
@@ -690,11 +815,10 @@ export class BaseChart {
|
|
|
690
815
|
});
|
|
691
816
|
}
|
|
692
817
|
renderTitle(svg) {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
this.title.render(svg, this.renderTheme, this.width, position.x, position.y);
|
|
818
|
+
this.textComponents.forEach((component) => {
|
|
819
|
+
const position = this.layoutManager.getComponentPosition(component);
|
|
820
|
+
component.render(svg, this.renderTheme, this.width, position.x, position.y);
|
|
821
|
+
});
|
|
698
822
|
}
|
|
699
823
|
renderInlineLegend(svg) {
|
|
700
824
|
if (!this.legend || !this.hasInlineLegend()) {
|
|
@@ -732,8 +856,9 @@ export class BaseChart {
|
|
|
732
856
|
* Setup ResizeObserver for automatic resize handling
|
|
733
857
|
*/
|
|
734
858
|
setupResizeObserver() {
|
|
735
|
-
if (!this.container)
|
|
859
|
+
if (!this.container) {
|
|
736
860
|
return;
|
|
861
|
+
}
|
|
737
862
|
if (this.resizeObserver) {
|
|
738
863
|
this.resizeObserver.disconnect();
|
|
739
864
|
}
|
|
@@ -746,12 +871,32 @@ export class BaseChart {
|
|
|
746
871
|
nextDimensions.height === this.height) {
|
|
747
872
|
return;
|
|
748
873
|
}
|
|
749
|
-
this.rerender();
|
|
874
|
+
this.rerender('resize');
|
|
750
875
|
});
|
|
751
876
|
this.resizeObserver.observe(this.container);
|
|
752
877
|
}
|
|
878
|
+
notifyLegendChanged() {
|
|
879
|
+
this.emit('legend:change');
|
|
880
|
+
}
|
|
753
881
|
setReadyPromise(promise) {
|
|
754
882
|
this.readyPromise = promise;
|
|
883
|
+
if (!this.isPerformingRender || !this.container) {
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const renderId = this.renderId;
|
|
887
|
+
const reason = this.currentRenderReason;
|
|
888
|
+
const container = this.container;
|
|
889
|
+
const readyVersion = ++this.readyEventVersion;
|
|
890
|
+
void promise
|
|
891
|
+
.then(() => {
|
|
892
|
+
if (readyVersion !== this.readyEventVersion ||
|
|
893
|
+
renderId !== this.renderId ||
|
|
894
|
+
container !== this.container) {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
this.emit('ready', this.createRenderEventPayload(reason));
|
|
898
|
+
})
|
|
899
|
+
.catch(() => undefined);
|
|
755
900
|
}
|
|
756
901
|
whenReady() {
|
|
757
902
|
return this.readyPromise;
|
|
@@ -796,16 +941,23 @@ export class BaseChart {
|
|
|
796
941
|
this.validateSourceData(data);
|
|
797
942
|
const normalized = normalizeChartData(data);
|
|
798
943
|
ChartValidator.validateData(normalized.data);
|
|
944
|
+
const previousSourceData = this.sourceData;
|
|
799
945
|
const previousData = this.data;
|
|
800
946
|
this.sourceData = data;
|
|
801
947
|
this.data = normalized.data;
|
|
802
948
|
this.syncDerivedState(previousData);
|
|
803
|
-
this.
|
|
949
|
+
this.emit('data', {
|
|
950
|
+
data: this.sourceData,
|
|
951
|
+
previousData: previousSourceData,
|
|
952
|
+
});
|
|
953
|
+
this.rerender('update');
|
|
804
954
|
}
|
|
805
955
|
/**
|
|
806
956
|
* Destroys the chart and cleans up resources
|
|
807
957
|
*/
|
|
808
958
|
destroy() {
|
|
959
|
+
this.readyEventVersion += 1;
|
|
960
|
+
this.emit('destroy');
|
|
809
961
|
this.tooltip?.cleanup();
|
|
810
962
|
if (this.resizeObserver) {
|
|
811
963
|
this.resizeObserver.disconnect();
|
|
@@ -900,11 +1052,11 @@ export class BaseChart {
|
|
|
900
1052
|
}
|
|
901
1053
|
return 0;
|
|
902
1054
|
}
|
|
903
|
-
rerender() {
|
|
1055
|
+
rerender(reason = 'manual') {
|
|
904
1056
|
if (!this.container) {
|
|
905
1057
|
return;
|
|
906
1058
|
}
|
|
907
|
-
this.performRender();
|
|
1059
|
+
this.performRender(reason);
|
|
908
1060
|
}
|
|
909
1061
|
tryRegisterComponent(component, slots) {
|
|
910
1062
|
const slot = slots.find((entry) => entry.type === component.type);
|
|
@@ -952,15 +1104,11 @@ export class BaseChart {
|
|
|
952
1104
|
components.splice(0, components.length, ...previousState);
|
|
953
1105
|
};
|
|
954
1106
|
}
|
|
1107
|
+
isTextComponent(component) {
|
|
1108
|
+
return component.type === 'text' || component.type === 'title';
|
|
1109
|
+
}
|
|
955
1110
|
getBaseComponentSlots() {
|
|
956
1111
|
return [
|
|
957
|
-
{
|
|
958
|
-
type: 'title',
|
|
959
|
-
get: () => this.title,
|
|
960
|
-
set: (component) => {
|
|
961
|
-
this.title = component;
|
|
962
|
-
},
|
|
963
|
-
},
|
|
964
1112
|
{
|
|
965
1113
|
type: 'grid',
|
|
966
1114
|
get: () => this.grid,
|
package/dist/chart-group.d.ts
CHANGED
|
@@ -56,7 +56,7 @@ export declare class ChartGroup {
|
|
|
56
56
|
private readonly warnedColorConflicts;
|
|
57
57
|
private container;
|
|
58
58
|
private legend;
|
|
59
|
-
private
|
|
59
|
+
private readonly textComponents;
|
|
60
60
|
private resizeObserver;
|
|
61
61
|
private readyPromise;
|
|
62
62
|
private childLegendSnapshot;
|
|
@@ -69,6 +69,7 @@ export declare class ChartGroup {
|
|
|
69
69
|
addChild(component: {
|
|
70
70
|
type: string;
|
|
71
71
|
}): this;
|
|
72
|
+
private isTextComponent;
|
|
72
73
|
render(target: string | HTMLElement): HTMLElement | null;
|
|
73
74
|
refresh(): HTMLElement | null;
|
|
74
75
|
whenReady(): Promise<void>;
|
|
@@ -79,6 +80,7 @@ export declare class ChartGroup {
|
|
|
79
80
|
toggleLegendSeries(dataKey: string): this;
|
|
80
81
|
setLegendVisibility(visibility: Record<string, boolean>): this;
|
|
81
82
|
onLegendChange(callback: () => void): () => void;
|
|
83
|
+
private resolveAccessibleLabel;
|
|
82
84
|
export(format: ChartGroupExportFormat, options?: ExportOptions): Promise<string | Blob | void>;
|
|
83
85
|
destroy(): void;
|
|
84
86
|
private bindChartRenderCallback;
|
|
@@ -105,15 +107,19 @@ export declare class ChartGroup {
|
|
|
105
107
|
private serializeChildLegendState;
|
|
106
108
|
private serializeChildYDomains;
|
|
107
109
|
private renderLegendIntoContainer;
|
|
108
|
-
private
|
|
110
|
+
private renderTextSvg;
|
|
111
|
+
private renderTextSvgs;
|
|
109
112
|
private renderLegendSvg;
|
|
110
113
|
private setupResizeObserver;
|
|
111
114
|
private exportSVG;
|
|
112
115
|
private resolveChartOptions;
|
|
113
116
|
private refreshIfMounted;
|
|
114
117
|
private prepareRenderState;
|
|
118
|
+
private sumRenderedTextHeight;
|
|
115
119
|
private createRenderHosts;
|
|
116
120
|
private appendRenderedSection;
|
|
121
|
+
private appendRenderedTextSections;
|
|
122
|
+
private resolveTextSectionClassName;
|
|
117
123
|
private renderLayoutItems;
|
|
118
124
|
private createChartHost;
|
|
119
125
|
private createReadyPromise;
|
|
@@ -122,9 +128,16 @@ export declare class ChartGroup {
|
|
|
122
128
|
private createRasterExportOptions;
|
|
123
129
|
private exportPdfContent;
|
|
124
130
|
private resolveExportLayoutState;
|
|
131
|
+
private createExportLayoutState;
|
|
132
|
+
private createExportRenderContext;
|
|
133
|
+
private resolveBaseExportHeight;
|
|
134
|
+
private createExportComponent;
|
|
135
|
+
private runExportHooks;
|
|
125
136
|
private resolveLiveChartHeightOverride;
|
|
126
137
|
private exportLayoutItems;
|
|
127
138
|
private composeExportSvg;
|
|
139
|
+
private syncAccessibleLabelFromSvg;
|
|
140
|
+
private appendExportTextSections;
|
|
128
141
|
private validateResponsiveConfig;
|
|
129
142
|
private validateItemResponsiveConfig;
|
|
130
143
|
private resolveResponsiveConfigForWidth;
|