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