@internetstiftelsen/charts 0.9.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 +44 -0
- package/{area.d.ts → dist/area.d.ts} +1 -1
- package/{bar.d.ts → dist/bar.d.ts} +3 -4
- package/{bar.js → dist/bar.js} +3 -11
- package/{base-chart.d.ts → dist/base-chart.d.ts} +30 -18
- package/{base-chart.js → dist/base-chart.js} +170 -50
- package/dist/chart-interface.d.ts +19 -0
- package/{donut-center-content.d.ts → dist/donut-center-content.d.ts} +1 -1
- package/{donut-chart.d.ts → dist/donut-chart.d.ts} +19 -4
- package/{donut-chart.js → dist/donut-chart.js} +140 -2
- package/{gauge-chart.d.ts → dist/gauge-chart.d.ts} +2 -2
- package/{gauge-chart.js → dist/gauge-chart.js} +2 -0
- 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/{pie-chart.d.ts → dist/pie-chart.d.ts} +4 -11
- package/{pie-chart.js → dist/pie-chart.js} +23 -21
- package/{radial-chart-base.js → dist/radial-chart-base.js} +3 -1
- 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} +0 -2
- package/{utils.js → dist/utils.js} +0 -5
- package/{word-cloud-chart.d.ts → dist/word-cloud-chart.d.ts} +1 -1
- package/{word-cloud-chart.js → dist/word-cloud-chart.js} +2 -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} +8 -5
- package/{xy-chart.js → dist/xy-chart.js} +31 -5
- 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 +38 -36
- package/chart-interface.d.ts +0 -13
- /package/{area.js → dist/area.js} +0 -0
- /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/{line.js → dist/line.js} +0 -0
- /package/{radial-chart-base.d.ts → dist/radial-chart-base.d.ts} +0 -0
- /package/{scale-utils.d.ts → dist/scale-utils.d.ts} +0 -0
- /package/{scale-utils.js → dist/scale-utils.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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Internetstiftelsen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -7,9 +7,11 @@ A framework-agnostic, composable charting library built on D3.js with TypeScript
|
|
|
7
7
|
- **Framework Agnostic** - Works with vanilla JS, React, Vue, Svelte, or any framework
|
|
8
8
|
- **Composable Architecture** - Build charts by composing components
|
|
9
9
|
- **Multiple Chart Types** - XYChart (lines, areas, bars), WordCloudChart, DonutChart, PieChart, and GaugeChart
|
|
10
|
+
- **Radial Value Labels** - Pie and donut charts support optional on-chart labels with custom formatters
|
|
10
11
|
- **Optional Gauge Animation** - Animate gauge value transitions with `gauge.animate`
|
|
11
12
|
- **Stacking Control** - Bar stacking modes with optional reversed visual series order
|
|
12
13
|
- **Flexible Scales** - Band, linear, time, and logarithmic scales
|
|
14
|
+
- **Explicit or Responsive Sizing** - Set top-level `width`/`height` or let the container drive size
|
|
13
15
|
- **Auto Resize** - Built-in ResizeObserver handles responsive behavior
|
|
14
16
|
- **Responsive Policy** - Chart-level container-query overrides for theme and components
|
|
15
17
|
- **Type Safe** - Written in TypeScript with full type definitions
|
|
@@ -82,6 +84,48 @@ chart
|
|
|
82
84
|
chart.render('#chart-container');
|
|
83
85
|
```
|
|
84
86
|
|
|
87
|
+
Use top-level `width` and `height` for fixed-size charts, or omit them to size
|
|
88
|
+
from the render container.
|
|
89
|
+
|
|
90
|
+
Theme overrides are deep-partial, so nested overrides like
|
|
91
|
+
`theme.axis.fontSize` preserve the rest of the theme defaults.
|
|
92
|
+
|
|
93
|
+
Responsive overrides are declarative and merge all matching breakpoints in
|
|
94
|
+
declaration order:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
const chart = new XYChart({
|
|
98
|
+
data,
|
|
99
|
+
responsive: {
|
|
100
|
+
breakpoints: {
|
|
101
|
+
sm: {
|
|
102
|
+
maxWidth: 640,
|
|
103
|
+
theme: {
|
|
104
|
+
axis: {
|
|
105
|
+
fontSize: 11,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
components: [
|
|
109
|
+
{
|
|
110
|
+
match: { type: 'xAxis' },
|
|
111
|
+
override: { display: false },
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
md: {
|
|
116
|
+
minWidth: 641,
|
|
117
|
+
maxWidth: 768,
|
|
118
|
+
theme: {
|
|
119
|
+
axis: {
|
|
120
|
+
fontSize: 12,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
85
129
|
## Word Cloud
|
|
86
130
|
|
|
87
131
|
```javascript
|
|
@@ -18,7 +18,7 @@ export declare class Area implements ChartComponent<AreaConfigBase> {
|
|
|
18
18
|
readonly exportHooks?: ExportHooks<AreaConfigBase>;
|
|
19
19
|
constructor(config: AreaConfig);
|
|
20
20
|
getExportConfig(): AreaConfigBase;
|
|
21
|
-
createExportComponent(override?: Partial<AreaConfigBase>): ChartComponent
|
|
21
|
+
createExportComponent(override?: Partial<AreaConfigBase>): ChartComponent<AreaConfigBase>;
|
|
22
22
|
private getStackValues;
|
|
23
23
|
render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: D3Scale, y: D3Scale, parseValue: (value: unknown) => number, xScaleType: ScaleType | undefined, theme: ChartTheme, stackingContext?: AreaStackingContext, valueLabelLayer?: Selection<SVGGElement, undefined, null, undefined>): void;
|
|
24
24
|
private renderValueLabels;
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import type { Selection } from 'd3';
|
|
2
|
-
import type { BarConfig, BarStackingContext, BarValueLabelConfig, ChartTheme, D3Scale, DataItem,
|
|
2
|
+
import type { BarConfig, BarStackingContext, BarValueLabelConfig, ChartTheme, D3Scale, DataItem, ScaleType, ExportHooks, BarConfigBase } from './types.js';
|
|
3
3
|
import type { ChartComponent } from './chart-interface.js';
|
|
4
4
|
export declare class Bar implements ChartComponent<BarConfigBase> {
|
|
5
5
|
readonly type: "bar";
|
|
6
6
|
readonly dataKey: string;
|
|
7
7
|
readonly fill: string;
|
|
8
8
|
readonly colorAdapter?: (data: DataItem, index: number) => string;
|
|
9
|
-
readonly orientation: Orientation;
|
|
10
9
|
readonly maxBarSize?: number;
|
|
11
10
|
readonly valueLabel?: BarValueLabelConfig;
|
|
12
11
|
readonly exportHooks?: ExportHooks<BarConfigBase>;
|
|
13
12
|
constructor(config: BarConfig);
|
|
14
13
|
getExportConfig(): BarConfigBase;
|
|
15
|
-
createExportComponent(override?: Partial<BarConfigBase>): ChartComponent
|
|
16
|
-
render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: D3Scale, y: D3Scale, parseValue: (value: unknown) => number, xScaleType?: ScaleType, theme?: ChartTheme, stackingContext?: BarStackingContext): void;
|
|
14
|
+
createExportComponent(override?: Partial<BarConfigBase>): ChartComponent<BarConfigBase>;
|
|
15
|
+
render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: D3Scale, y: D3Scale, parseValue: (value: unknown) => number, xScaleType?: ScaleType, theme?: ChartTheme, stackingContext?: BarStackingContext, orientation?: 'vertical' | 'horizontal'): void;
|
|
17
16
|
private renderVertical;
|
|
18
17
|
private renderHorizontal;
|
|
19
18
|
private renderVerticalValueLabels;
|
package/{bar.js → dist/bar.js}
RENAMED
|
@@ -40,12 +40,6 @@ export class Bar {
|
|
|
40
40
|
writable: true,
|
|
41
41
|
value: void 0
|
|
42
42
|
});
|
|
43
|
-
Object.defineProperty(this, "orientation", {
|
|
44
|
-
enumerable: true,
|
|
45
|
-
configurable: true,
|
|
46
|
-
writable: true,
|
|
47
|
-
value: void 0
|
|
48
|
-
});
|
|
49
43
|
Object.defineProperty(this, "maxBarSize", {
|
|
50
44
|
enumerable: true,
|
|
51
45
|
configurable: true,
|
|
@@ -67,7 +61,6 @@ export class Bar {
|
|
|
67
61
|
this.dataKey = config.dataKey;
|
|
68
62
|
this.fill = config.fill || '#8884d8';
|
|
69
63
|
this.colorAdapter = config.colorAdapter;
|
|
70
|
-
this.orientation = config.orientation || 'vertical';
|
|
71
64
|
this.maxBarSize = config.maxBarSize;
|
|
72
65
|
this.valueLabel = config.valueLabel;
|
|
73
66
|
this.exportHooks = config.exportHooks;
|
|
@@ -77,7 +70,6 @@ export class Bar {
|
|
|
77
70
|
dataKey: this.dataKey,
|
|
78
71
|
fill: this.fill,
|
|
79
72
|
colorAdapter: this.colorAdapter,
|
|
80
|
-
orientation: this.orientation,
|
|
81
73
|
maxBarSize: this.maxBarSize,
|
|
82
74
|
valueLabel: this.valueLabel,
|
|
83
75
|
};
|
|
@@ -89,8 +81,8 @@ export class Bar {
|
|
|
89
81
|
exportHooks: this.exportHooks,
|
|
90
82
|
});
|
|
91
83
|
}
|
|
92
|
-
render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme, stackingContext) {
|
|
93
|
-
if (
|
|
84
|
+
render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme, stackingContext, orientation = 'vertical') {
|
|
85
|
+
if (orientation === 'vertical') {
|
|
94
86
|
this.renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType, stackingContext);
|
|
95
87
|
}
|
|
96
88
|
else {
|
|
@@ -98,7 +90,7 @@ export class Bar {
|
|
|
98
90
|
}
|
|
99
91
|
// Render value labels if enabled
|
|
100
92
|
if (this.valueLabel?.show && theme) {
|
|
101
|
-
if (
|
|
93
|
+
if (orientation === 'vertical') {
|
|
102
94
|
this.renderVerticalValueLabels(plotGroup, data, xKey, x, y, parseValue, xScaleType, theme, stackingContext);
|
|
103
95
|
}
|
|
104
96
|
else {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
|
-
import type { ChartData, DataItem, ChartTheme, AxisScaleConfig, ExportFormat, ExportOptions, D3Scale, ExportHookContext, ExportRenderContext, LegendItem, LegendSeries, ResponsiveConfig, ResponsiveRenderContext, DeepPartial } from './types.js';
|
|
3
|
-
import type {
|
|
2
|
+
import type { ChartData, DataItem, ChartTheme, ResolvedChartTheme, AxisScaleConfig, ExportFormat, ExportOptions, D3Scale, ExportHookContext, ExportRenderContext, LegendItem, LegendSeries, ResponsiveConfig, ResponsiveRenderContext, DeepPartial } from './types.js';
|
|
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';
|
|
6
6
|
import type { Grid } from './grid.js';
|
|
@@ -12,11 +12,12 @@ type VisualExportFormat = 'svg' | 'png' | 'jpg' | 'pdf';
|
|
|
12
12
|
type RenderDimensions = {
|
|
13
13
|
width: number;
|
|
14
14
|
height: number;
|
|
15
|
+
svgWidthAttr: number | string;
|
|
15
16
|
svgHeightAttr: number | string;
|
|
16
17
|
};
|
|
17
18
|
type ResponsiveOverrides = {
|
|
18
19
|
theme?: DeepPartial<ChartTheme>;
|
|
19
|
-
components: Map<
|
|
20
|
+
components: Map<ChartComponentBase, Record<string, unknown>>;
|
|
20
21
|
};
|
|
21
22
|
type BaseLayoutComponentsOptions = {
|
|
22
23
|
title?: boolean;
|
|
@@ -40,7 +41,7 @@ export type BaseRenderContext = BaseLayoutContext & {
|
|
|
40
41
|
plotGroup: Selection<SVGGElement, undefined, null, undefined>;
|
|
41
42
|
plotArea: PlotAreaBounds;
|
|
42
43
|
};
|
|
43
|
-
type ComponentSlot<TComponent extends
|
|
44
|
+
type ComponentSlot<TComponent extends ChartComponentBase = ChartComponentBase> = {
|
|
44
45
|
type: TComponent['type'];
|
|
45
46
|
get: () => TComponent | null;
|
|
46
47
|
set: (component: TComponent | null) => void;
|
|
@@ -48,7 +49,9 @@ type ComponentSlot<TComponent extends ChartComponent = ChartComponent> = {
|
|
|
48
49
|
};
|
|
49
50
|
export type BaseChartConfig = {
|
|
50
51
|
data: ChartData;
|
|
51
|
-
|
|
52
|
+
width?: number;
|
|
53
|
+
height?: number;
|
|
54
|
+
theme?: DeepPartial<ChartTheme>;
|
|
52
55
|
scales?: AxisScaleConfig;
|
|
53
56
|
responsive?: ResponsiveConfig;
|
|
54
57
|
};
|
|
@@ -58,6 +61,8 @@ export type BaseChartConfig = {
|
|
|
58
61
|
export declare abstract class BaseChart {
|
|
59
62
|
protected data: DataItem[];
|
|
60
63
|
protected sourceData: ChartData;
|
|
64
|
+
protected readonly configuredWidth?: number;
|
|
65
|
+
protected readonly configuredHeight?: number;
|
|
61
66
|
protected readonly theme: ChartTheme;
|
|
62
67
|
protected readonly scaleConfig: AxisScaleConfig;
|
|
63
68
|
protected readonly responsiveConfig?: ResponsiveConfig;
|
|
@@ -80,11 +85,12 @@ export declare abstract class BaseChart {
|
|
|
80
85
|
private readyPromise;
|
|
81
86
|
private disconnectedLegendContainer;
|
|
82
87
|
private renderThemeOverride;
|
|
88
|
+
private renderSizeOverride;
|
|
83
89
|
protected constructor(config: BaseChartConfig);
|
|
84
90
|
/**
|
|
85
91
|
* Adds a component (axis, grid, tooltip, etc.) to the chart
|
|
86
92
|
*/
|
|
87
|
-
addChild(component:
|
|
93
|
+
addChild(component: ChartComponentBase): this;
|
|
88
94
|
/**
|
|
89
95
|
* Renders the chart to the specified target element
|
|
90
96
|
*/
|
|
@@ -95,30 +101,36 @@ export declare abstract class BaseChart {
|
|
|
95
101
|
*/
|
|
96
102
|
private performRender;
|
|
97
103
|
protected resolveRenderDimensions(containerRect: DOMRect): RenderDimensions;
|
|
104
|
+
private resolveAccessibleLabel;
|
|
105
|
+
private syncAccessibleLabelFromSvg;
|
|
98
106
|
protected resolveResponsiveContext(context: {
|
|
99
107
|
width: number;
|
|
100
108
|
height: number;
|
|
101
109
|
}): ResponsiveRenderContext;
|
|
102
|
-
private
|
|
110
|
+
private resolveActiveBreakpoints;
|
|
103
111
|
private resolveRenderTheme;
|
|
104
112
|
private applyRenderTheme;
|
|
105
113
|
protected get renderTheme(): ChartTheme;
|
|
114
|
+
protected get resolvedRenderTheme(): ResolvedChartTheme;
|
|
106
115
|
/**
|
|
107
116
|
* Get layout-aware components in order
|
|
108
117
|
* Override in subclasses to provide chart-specific components
|
|
109
118
|
*/
|
|
110
|
-
protected getLayoutComponents():
|
|
111
|
-
protected getBaseLayoutComponents(options: BaseLayoutComponentsOptions):
|
|
112
|
-
protected getExportComponents():
|
|
113
|
-
protected getOverrideableComponents():
|
|
114
|
-
protected getBaseExportComponents(options: BaseExportComponentsOptions):
|
|
115
|
-
protected registerBaseComponent(component:
|
|
116
|
-
protected collectExportOverrides(context: ExportRenderContext): Map<
|
|
119
|
+
protected getLayoutComponents(): LayoutAwareComponentBase[];
|
|
120
|
+
protected getBaseLayoutComponents(options: BaseLayoutComponentsOptions): LayoutAwareComponentBase[];
|
|
121
|
+
protected getExportComponents(): ChartComponentBase[];
|
|
122
|
+
protected getOverrideableComponents(): ChartComponentBase[];
|
|
123
|
+
protected getBaseExportComponents(options: BaseExportComponentsOptions): ChartComponentBase[];
|
|
124
|
+
protected registerBaseComponent(component: ChartComponentBase): boolean;
|
|
125
|
+
protected collectExportOverrides(context: ExportRenderContext): Map<ChartComponentBase, Record<string, unknown>>;
|
|
117
126
|
protected collectResponsiveOverrides(context: ResponsiveRenderContext): ResponsiveOverrides;
|
|
127
|
+
private collectResponsiveBreakpointOverrides;
|
|
128
|
+
private createResponsiveComponentSnapshots;
|
|
129
|
+
private applyResponsiveComponentOverrideEntries;
|
|
118
130
|
protected runExportHooks(context: ExportHookContext): void;
|
|
119
131
|
private mergeComponentOverrideMaps;
|
|
120
132
|
private createOverrideComponents;
|
|
121
|
-
protected applyComponentOverrides(overrides: Map<
|
|
133
|
+
protected applyComponentOverrides(overrides: Map<ChartComponentBase, ChartComponentBase>): () => void;
|
|
122
134
|
private renderExportChart;
|
|
123
135
|
protected renderTitle(svg: Selection<SVGSVGElement, undefined, null, undefined>): void;
|
|
124
136
|
protected renderInlineLegend(svg: Selection<SVGSVGElement, undefined, null, undefined>): void;
|
|
@@ -159,9 +171,9 @@ export declare abstract class BaseChart {
|
|
|
159
171
|
private cleanupDisconnectedLegendContainer;
|
|
160
172
|
protected parseValue(value: unknown): number;
|
|
161
173
|
protected rerender(): void;
|
|
162
|
-
protected tryRegisterComponent(component:
|
|
163
|
-
protected applySlotOverrides(overrides: Map<
|
|
164
|
-
protected applyArrayComponentOverrides<TComponent extends
|
|
174
|
+
protected tryRegisterComponent(component: ChartComponentBase, slots: readonly ComponentSlot[]): boolean;
|
|
175
|
+
protected applySlotOverrides(overrides: Map<ChartComponentBase, ChartComponentBase>, slots: readonly ComponentSlot[]): () => void;
|
|
176
|
+
protected applyArrayComponentOverrides<TComponent extends ChartComponentBase>(components: TComponent[], overrides: Map<ChartComponentBase, ChartComponentBase>, isComponent: (component: ChartComponentBase) => component is TComponent): () => void;
|
|
165
177
|
private getBaseComponentSlots;
|
|
166
178
|
/**
|
|
167
179
|
* Exports the chart in the specified format
|
|
@@ -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,
|
|
@@ -157,16 +200,24 @@ export class BaseChart {
|
|
|
157
200
|
writable: true,
|
|
158
201
|
value: null
|
|
159
202
|
});
|
|
203
|
+
Object.defineProperty(this, "renderSizeOverride", {
|
|
204
|
+
enumerable: true,
|
|
205
|
+
configurable: true,
|
|
206
|
+
writable: true,
|
|
207
|
+
value: null
|
|
208
|
+
});
|
|
160
209
|
const normalized = normalizeChartData(config.data);
|
|
161
210
|
ChartValidator.validateData(normalized.data);
|
|
162
211
|
this.sourceData = config.data;
|
|
163
212
|
this.data = normalized.data;
|
|
164
|
-
this.
|
|
165
|
-
this.
|
|
166
|
-
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;
|
|
167
218
|
this.scaleConfig = config.scales || {};
|
|
168
219
|
this.responsiveConfig = config.responsive;
|
|
169
|
-
this.layoutManager = new LayoutManager(this.
|
|
220
|
+
this.layoutManager = new LayoutManager(this.resolvedRenderTheme);
|
|
170
221
|
}
|
|
171
222
|
/**
|
|
172
223
|
* Adds a component (axis, grid, tooltip, etc.) to the chart
|
|
@@ -228,8 +279,10 @@ export class BaseChart {
|
|
|
228
279
|
// Clear and setup SVG
|
|
229
280
|
this.container.innerHTML = '';
|
|
230
281
|
this.svg = create('svg')
|
|
231
|
-
.attr('width',
|
|
282
|
+
.attr('width', dimensions.svgWidthAttr)
|
|
232
283
|
.attr('height', dimensions.svgHeightAttr)
|
|
284
|
+
.attr('role', 'img')
|
|
285
|
+
.attr('aria-label', this.resolveAccessibleLabel())
|
|
233
286
|
.style('display', 'block');
|
|
234
287
|
this.container.appendChild(this.svg.node());
|
|
235
288
|
const svgNode = this.svg.node();
|
|
@@ -241,12 +294,7 @@ export class BaseChart {
|
|
|
241
294
|
svgNode,
|
|
242
295
|
});
|
|
243
296
|
// Calculate layout
|
|
244
|
-
|
|
245
|
-
...this.renderTheme,
|
|
246
|
-
width: this.width,
|
|
247
|
-
height: this.height,
|
|
248
|
-
};
|
|
249
|
-
this.layoutManager = new LayoutManager(layoutTheme);
|
|
297
|
+
this.layoutManager = new LayoutManager(this.resolvedRenderTheme);
|
|
250
298
|
const components = this.getLayoutComponents();
|
|
251
299
|
const plotArea = this.layoutManager.calculateLayout(components);
|
|
252
300
|
this.plotArea = plotArea;
|
|
@@ -268,35 +316,65 @@ export class BaseChart {
|
|
|
268
316
|
}
|
|
269
317
|
}
|
|
270
318
|
resolveRenderDimensions(containerRect) {
|
|
271
|
-
const width =
|
|
272
|
-
|
|
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);
|
|
273
325
|
return {
|
|
274
326
|
width,
|
|
275
327
|
height,
|
|
276
|
-
|
|
328
|
+
svgWidthAttr: this.renderSizeOverride?.width ??
|
|
329
|
+
this.configuredWidth ??
|
|
330
|
+
'100%',
|
|
331
|
+
svgHeightAttr: this.renderSizeOverride?.height ??
|
|
332
|
+
this.configuredHeight ??
|
|
333
|
+
'100%',
|
|
277
334
|
};
|
|
278
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
|
+
}
|
|
279
351
|
resolveResponsiveContext(context) {
|
|
352
|
+
const activeBreakpoints = this.resolveActiveBreakpoints(context.width).map(({ name }) => name);
|
|
280
353
|
return {
|
|
281
354
|
...context,
|
|
282
|
-
|
|
355
|
+
activeBreakpoints,
|
|
356
|
+
breakpoint: activeBreakpoints.length > 0
|
|
357
|
+
? activeBreakpoints[activeBreakpoints.length - 1]
|
|
358
|
+
: null,
|
|
283
359
|
};
|
|
284
360
|
}
|
|
285
|
-
|
|
361
|
+
resolveActiveBreakpoints(width) {
|
|
286
362
|
const breakpoints = this.responsiveConfig?.breakpoints;
|
|
287
363
|
if (!breakpoints) {
|
|
288
|
-
return
|
|
364
|
+
return [];
|
|
289
365
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
sorted.forEach(([name, minWidth]) => {
|
|
295
|
-
if (width >= minWidth) {
|
|
296
|
-
active = name;
|
|
366
|
+
return Object.entries(breakpoints).flatMap(([name, definition]) => {
|
|
367
|
+
const config = normalizeResponsiveBreakpoint(definition);
|
|
368
|
+
if (!config) {
|
|
369
|
+
return [];
|
|
297
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 [];
|
|
375
|
+
}
|
|
376
|
+
return [{ name, config }];
|
|
298
377
|
});
|
|
299
|
-
return active;
|
|
300
378
|
}
|
|
301
379
|
resolveRenderTheme(responsiveOverrides) {
|
|
302
380
|
if (!responsiveOverrides.theme) {
|
|
@@ -316,6 +394,13 @@ export class BaseChart {
|
|
|
316
394
|
get renderTheme() {
|
|
317
395
|
return this.renderThemeOverride ?? this.theme;
|
|
318
396
|
}
|
|
397
|
+
get resolvedRenderTheme() {
|
|
398
|
+
return {
|
|
399
|
+
...this.renderTheme,
|
|
400
|
+
width: this.width,
|
|
401
|
+
height: this.height,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
319
404
|
/**
|
|
320
405
|
* Get layout-aware components in order
|
|
321
406
|
* Override in subclasses to provide chart-specific components
|
|
@@ -388,7 +473,7 @@ export class BaseChart {
|
|
|
388
473
|
components.forEach((component) => {
|
|
389
474
|
const exportable = component;
|
|
390
475
|
const currentConfig = exportable.getExportConfig?.() ?? {};
|
|
391
|
-
const result =
|
|
476
|
+
const result = exportable.exportHooks?.beforeRender?.call(component, context, currentConfig);
|
|
392
477
|
if (result &&
|
|
393
478
|
typeof result === 'object' &&
|
|
394
479
|
exportable.createExportComponent) {
|
|
@@ -400,33 +485,65 @@ export class BaseChart {
|
|
|
400
485
|
collectResponsiveOverrides(context) {
|
|
401
486
|
const beforeRender = this.responsiveConfig?.beforeRender;
|
|
402
487
|
const components = this.getOverrideableComponents();
|
|
403
|
-
const
|
|
488
|
+
const breakpointOverrides = this.collectResponsiveBreakpointOverrides(context, components);
|
|
404
489
|
if (!beforeRender) {
|
|
405
|
-
return
|
|
406
|
-
components: componentOverrides,
|
|
407
|
-
};
|
|
490
|
+
return breakpointOverrides;
|
|
408
491
|
}
|
|
409
|
-
const
|
|
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;
|
|
502
|
+
}
|
|
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) => {
|
|
410
531
|
const exportable = component;
|
|
411
532
|
const currentConfig = exportable.getExportConfig?.() ?? {};
|
|
412
533
|
const dataKey = component.dataKey;
|
|
534
|
+
const override = overrides.get(component);
|
|
413
535
|
return {
|
|
414
536
|
index,
|
|
415
537
|
type: component.type,
|
|
416
538
|
dataKey: typeof dataKey === 'string' ? dataKey : undefined,
|
|
417
|
-
currentConfig
|
|
539
|
+
currentConfig: override
|
|
540
|
+
? mergeDeep(currentConfig, override)
|
|
541
|
+
: currentConfig,
|
|
418
542
|
};
|
|
419
543
|
});
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
});
|
|
424
|
-
if (!result || typeof result !== 'object') {
|
|
425
|
-
return {
|
|
426
|
-
components: componentOverrides,
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
result.components?.forEach((entry) => {
|
|
544
|
+
}
|
|
545
|
+
applyResponsiveComponentOverrideEntries(entries, components, overrides) {
|
|
546
|
+
entries?.forEach((entry) => {
|
|
430
547
|
const match = entry.match ?? {};
|
|
431
548
|
const override = entry.override;
|
|
432
549
|
if (!override || typeof override !== 'object') {
|
|
@@ -441,19 +558,16 @@ export class BaseChart {
|
|
|
441
558
|
if (!matchesIndex || !matchesType || !matchesDataKey) {
|
|
442
559
|
return;
|
|
443
560
|
}
|
|
444
|
-
const existing =
|
|
445
|
-
|
|
561
|
+
const existing = overrides.get(component);
|
|
562
|
+
overrides.set(component, existing ? mergeDeep(existing, override) : { ...override });
|
|
446
563
|
});
|
|
447
564
|
});
|
|
448
|
-
return {
|
|
449
|
-
theme: result.theme,
|
|
450
|
-
components: componentOverrides,
|
|
451
|
-
};
|
|
452
565
|
}
|
|
453
566
|
runExportHooks(context) {
|
|
454
567
|
const components = this.getExportComponents();
|
|
455
568
|
components.forEach((component) => {
|
|
456
|
-
component
|
|
569
|
+
const exportable = component;
|
|
570
|
+
exportable.exportHooks?.before?.call(component, context);
|
|
457
571
|
});
|
|
458
572
|
}
|
|
459
573
|
mergeComponentOverrideMaps(...maps) {
|
|
@@ -964,9 +1078,14 @@ export class BaseChart {
|
|
|
964
1078
|
...baseContext,
|
|
965
1079
|
svg: clone,
|
|
966
1080
|
});
|
|
1081
|
+
this.syncAccessibleLabelFromSvg(clone);
|
|
967
1082
|
return clone.outerHTML;
|
|
968
1083
|
}
|
|
969
1084
|
const exportChart = this.createExportChart();
|
|
1085
|
+
exportChart.renderSizeOverride = {
|
|
1086
|
+
width: exportWidth,
|
|
1087
|
+
height: exportHeight,
|
|
1088
|
+
};
|
|
970
1089
|
const components = this.getExportComponents();
|
|
971
1090
|
components.forEach((component) => {
|
|
972
1091
|
const exportable = component;
|
|
@@ -986,6 +1105,7 @@ export class BaseChart {
|
|
|
986
1105
|
...baseContext,
|
|
987
1106
|
svg: exportSvg,
|
|
988
1107
|
});
|
|
1108
|
+
this.syncAccessibleLabelFromSvg(exportSvg);
|
|
989
1109
|
return exportSvg.outerHTML;
|
|
990
1110
|
}
|
|
991
1111
|
exportJSON() {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ExportHooks } from './types.js';
|
|
2
|
+
export type ChartComponentType = 'line' | 'area' | 'bar' | 'xAxis' | 'yAxis' | 'grid' | 'tooltip' | 'legend' | 'title' | 'donutCenterContent';
|
|
3
|
+
export interface ChartComponentBase {
|
|
4
|
+
type: ChartComponentType;
|
|
5
|
+
}
|
|
6
|
+
export interface ChartComponent<TConfig = unknown> extends ChartComponentBase {
|
|
7
|
+
type: ChartComponentType;
|
|
8
|
+
exportHooks?: ExportHooks<TConfig>;
|
|
9
|
+
}
|
|
10
|
+
export type ComponentSpace = {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
position: 'top' | 'bottom' | 'left' | 'right';
|
|
14
|
+
};
|
|
15
|
+
export interface LayoutAwareComponentBase extends ChartComponentBase {
|
|
16
|
+
getRequiredSpace(): ComponentSpace;
|
|
17
|
+
}
|
|
18
|
+
export interface LayoutAwareComponent<TConfig = unknown> extends ChartComponent<TConfig>, LayoutAwareComponentBase {
|
|
19
|
+
}
|
|
@@ -27,7 +27,7 @@ export declare class DonutCenterContent implements ChartComponent<DonutCenterCon
|
|
|
27
27
|
private readonly config;
|
|
28
28
|
constructor(config?: DonutCenterContentConfig);
|
|
29
29
|
getExportConfig(): DonutCenterContentConfigBase;
|
|
30
|
-
createExportComponent(override?: Partial<DonutCenterContentConfigBase>): ChartComponent
|
|
30
|
+
createExportComponent(override?: Partial<DonutCenterContentConfigBase>): ChartComponent<DonutCenterContentConfigBase>;
|
|
31
31
|
render(svg: Selection<SVGSVGElement, undefined, null, undefined>, cx: number, cy: number, theme: ChartTheme, fontScale?: number): void;
|
|
32
32
|
}
|
|
33
33
|
export {};
|