@internetstiftelsen/charts 0.3.1 → 0.3.3
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/base-chart.d.ts +2 -0
- package/base-chart.js +20 -3
- package/package.json +1 -1
- package/x-axis.d.ts +4 -1
- package/x-axis.js +62 -0
- package/xy-chart.d.ts +1 -0
- package/xy-chart.js +8 -0
package/base-chart.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export declare abstract class BaseChart {
|
|
|
21
21
|
protected readonly theme: ChartTheme;
|
|
22
22
|
protected readonly scaleConfig: AxisScaleConfig;
|
|
23
23
|
protected width: number;
|
|
24
|
+
protected height: number;
|
|
24
25
|
protected xAxis: XAxis | null;
|
|
25
26
|
protected yAxis: YAxis | null;
|
|
26
27
|
protected grid: Grid | null;
|
|
@@ -53,6 +54,7 @@ export declare abstract class BaseChart {
|
|
|
53
54
|
* Override in subclasses to provide chart-specific components
|
|
54
55
|
*/
|
|
55
56
|
protected getLayoutComponents(): LayoutAwareComponent[];
|
|
57
|
+
protected prepareLayout(): void;
|
|
56
58
|
/**
|
|
57
59
|
* Setup ResizeObserver for automatic resize handling
|
|
58
60
|
*/
|
package/base-chart.js
CHANGED
|
@@ -31,6 +31,12 @@ export class BaseChart {
|
|
|
31
31
|
writable: true,
|
|
32
32
|
value: void 0
|
|
33
33
|
}); // Current rendering width
|
|
34
|
+
Object.defineProperty(this, "height", {
|
|
35
|
+
enumerable: true,
|
|
36
|
+
configurable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
value: void 0
|
|
39
|
+
}); // Current rendering height
|
|
34
40
|
Object.defineProperty(this, "xAxis", {
|
|
35
41
|
enumerable: true,
|
|
36
42
|
configurable: true,
|
|
@@ -120,6 +126,7 @@ export class BaseChart {
|
|
|
120
126
|
this.data = config.data;
|
|
121
127
|
this.theme = { ...defaultTheme, ...config.theme };
|
|
122
128
|
this.width = this.theme.width;
|
|
129
|
+
this.height = this.theme.height;
|
|
123
130
|
this.scaleConfig = config.scales || {};
|
|
124
131
|
this.layoutManager = new LayoutManager(this.theme);
|
|
125
132
|
}
|
|
@@ -148,15 +155,22 @@ export class BaseChart {
|
|
|
148
155
|
// Calculate current width
|
|
149
156
|
this.width =
|
|
150
157
|
this.container.getBoundingClientRect().width || this.theme.width;
|
|
158
|
+
this.height =
|
|
159
|
+
this.container.getBoundingClientRect().height || this.theme.height;
|
|
151
160
|
// Clear and setup SVG
|
|
152
161
|
this.container.innerHTML = '';
|
|
153
162
|
this.svg = create('svg')
|
|
154
163
|
.attr('width', '100%')
|
|
155
|
-
.attr('height', this.
|
|
164
|
+
.attr('height', this.height)
|
|
156
165
|
.style('display', 'block');
|
|
157
166
|
this.container.appendChild(this.svg.node());
|
|
167
|
+
this.prepareLayout();
|
|
158
168
|
// Calculate layout
|
|
159
|
-
const layoutTheme = {
|
|
169
|
+
const layoutTheme = {
|
|
170
|
+
...this.theme,
|
|
171
|
+
width: this.width,
|
|
172
|
+
height: this.height,
|
|
173
|
+
};
|
|
160
174
|
this.layoutManager = new LayoutManager(layoutTheme);
|
|
161
175
|
const components = this.getLayoutComponents();
|
|
162
176
|
this.plotArea = this.layoutManager.calculateLayout(components);
|
|
@@ -181,6 +195,9 @@ export class BaseChart {
|
|
|
181
195
|
components.push(this.legend);
|
|
182
196
|
return components;
|
|
183
197
|
}
|
|
198
|
+
// Hook for subclasses to update component layout estimates before layout calc
|
|
199
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
200
|
+
prepareLayout() { }
|
|
184
201
|
/**
|
|
185
202
|
* Setup ResizeObserver for automatic resize handling
|
|
186
203
|
*/
|
|
@@ -267,7 +284,7 @@ export class BaseChart {
|
|
|
267
284
|
const clone = this.svg.node().cloneNode(true);
|
|
268
285
|
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
269
286
|
clone.setAttribute('width', String(this.width));
|
|
270
|
-
clone.setAttribute('height', String(this.
|
|
287
|
+
clone.setAttribute('height', String(this.height));
|
|
271
288
|
return clone.outerHTML;
|
|
272
289
|
}
|
|
273
290
|
exportJSON() {
|
package/package.json
CHANGED
package/x-axis.d.ts
CHANGED
|
@@ -6,11 +6,12 @@ export declare class XAxis implements LayoutAwareComponent {
|
|
|
6
6
|
readonly dataKey?: string;
|
|
7
7
|
private readonly rotatedLabels;
|
|
8
8
|
private readonly tickPadding;
|
|
9
|
-
private
|
|
9
|
+
private fontSize;
|
|
10
10
|
private readonly maxLabelWidth?;
|
|
11
11
|
private readonly oversizedBehavior;
|
|
12
12
|
private readonly tickFormat;
|
|
13
13
|
private wrapLineCount;
|
|
14
|
+
private estimatedHeight;
|
|
14
15
|
private readonly autoHideOverlapping;
|
|
15
16
|
private readonly minLabelGap;
|
|
16
17
|
private readonly preserveEndLabels;
|
|
@@ -19,6 +20,8 @@ export declare class XAxis implements LayoutAwareComponent {
|
|
|
19
20
|
* Returns the space required by the x-axis
|
|
20
21
|
*/
|
|
21
22
|
getRequiredSpace(): ComponentSpace;
|
|
23
|
+
estimateLayoutSpace(labels: unknown[], theme: ChartTheme, svg: SVGSVGElement): void;
|
|
24
|
+
clearEstimatedSpace(): void;
|
|
22
25
|
render(svg: Selection<SVGSVGElement, undefined, null, undefined>, x: D3Scale, theme: ChartTheme, yPosition: number): void;
|
|
23
26
|
private applyLabelConstraints;
|
|
24
27
|
private wrapTextElement;
|
package/x-axis.js
CHANGED
|
@@ -57,6 +57,12 @@ export class XAxis {
|
|
|
57
57
|
writable: true,
|
|
58
58
|
value: 1
|
|
59
59
|
});
|
|
60
|
+
Object.defineProperty(this, "estimatedHeight", {
|
|
61
|
+
enumerable: true,
|
|
62
|
+
configurable: true,
|
|
63
|
+
writable: true,
|
|
64
|
+
value: null
|
|
65
|
+
});
|
|
60
66
|
Object.defineProperty(this, "autoHideOverlapping", {
|
|
61
67
|
enumerable: true,
|
|
62
68
|
configurable: true,
|
|
@@ -88,6 +94,13 @@ export class XAxis {
|
|
|
88
94
|
* Returns the space required by the x-axis
|
|
89
95
|
*/
|
|
90
96
|
getRequiredSpace() {
|
|
97
|
+
if (this.estimatedHeight !== null) {
|
|
98
|
+
return {
|
|
99
|
+
width: 0,
|
|
100
|
+
height: this.estimatedHeight,
|
|
101
|
+
position: 'bottom',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
91
104
|
// Height = tick padding + font size + some extra space for descenders
|
|
92
105
|
// Rotated labels need more vertical space (roughly 2.5x for -45deg rotation)
|
|
93
106
|
const baseHeight = this.tickPadding + this.fontSize + 5;
|
|
@@ -104,6 +117,55 @@ export class XAxis {
|
|
|
104
117
|
position: 'bottom',
|
|
105
118
|
};
|
|
106
119
|
}
|
|
120
|
+
estimateLayoutSpace(labels, theme, svg) {
|
|
121
|
+
if (!labels.length) {
|
|
122
|
+
this.estimatedHeight = null;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const parsedFontSize = typeof theme.axis.fontSize === 'string'
|
|
126
|
+
? parseFloat(theme.axis.fontSize)
|
|
127
|
+
: theme.axis.fontSize;
|
|
128
|
+
this.fontSize = Number.isFinite(parsedFontSize)
|
|
129
|
+
? parsedFontSize
|
|
130
|
+
: this.fontSize;
|
|
131
|
+
const fontSize = this.fontSize;
|
|
132
|
+
const fontFamily = theme.axis.fontFamily;
|
|
133
|
+
const fontWeight = theme.axis.fontWeight || 'normal';
|
|
134
|
+
let maxWidth = 0;
|
|
135
|
+
let maxLines = 1;
|
|
136
|
+
for (const label of labels) {
|
|
137
|
+
const text = String(label ?? '');
|
|
138
|
+
if (!text)
|
|
139
|
+
continue;
|
|
140
|
+
const textWidth = measureTextWidth(text, fontSize, fontFamily, fontWeight, svg);
|
|
141
|
+
if (this.maxLabelWidth && this.oversizedBehavior === 'wrap') {
|
|
142
|
+
const lines = wrapText(text, this.maxLabelWidth, fontSize, fontFamily, fontWeight, svg);
|
|
143
|
+
maxLines = Math.max(maxLines, lines.length || 1);
|
|
144
|
+
maxWidth = Math.max(maxWidth, this.maxLabelWidth);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const effectiveWidth = this.maxLabelWidth
|
|
148
|
+
? Math.min(textWidth, this.maxLabelWidth)
|
|
149
|
+
: textWidth;
|
|
150
|
+
maxWidth = Math.max(maxWidth, effectiveWidth);
|
|
151
|
+
}
|
|
152
|
+
const lineHeight = fontSize * 1.2;
|
|
153
|
+
const textHeight = lineHeight * maxLines;
|
|
154
|
+
if (this.rotatedLabels) {
|
|
155
|
+
const radians = Math.PI / 4;
|
|
156
|
+
const verticalFootprint = Math.sin(radians) * maxWidth +
|
|
157
|
+
Math.cos(radians) * textHeight;
|
|
158
|
+
this.estimatedHeight =
|
|
159
|
+
this.tickPadding + verticalFootprint + 5;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
this.estimatedHeight = this.tickPadding + textHeight + 5;
|
|
163
|
+
}
|
|
164
|
+
this.wrapLineCount = Math.max(this.wrapLineCount, maxLines);
|
|
165
|
+
}
|
|
166
|
+
clearEstimatedSpace() {
|
|
167
|
+
this.estimatedHeight = null;
|
|
168
|
+
}
|
|
107
169
|
render(svg, x, theme, yPosition) {
|
|
108
170
|
const axisGenerator = axisBottom(x)
|
|
109
171
|
.tickSizeOuter(0)
|
package/xy-chart.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare class XYChart extends BaseChart {
|
|
|
11
11
|
constructor(config: XYChartConfig);
|
|
12
12
|
addChild(component: ChartComponent): this;
|
|
13
13
|
private rerender;
|
|
14
|
+
protected prepareLayout(): void;
|
|
14
15
|
protected renderChart(): void;
|
|
15
16
|
private getXKey;
|
|
16
17
|
private setupScales;
|
package/xy-chart.js
CHANGED
|
@@ -71,6 +71,14 @@ export class XYChart extends BaseChart {
|
|
|
71
71
|
rerender() {
|
|
72
72
|
this.update(this.data);
|
|
73
73
|
}
|
|
74
|
+
prepareLayout() {
|
|
75
|
+
this.xAxis?.clearEstimatedSpace?.();
|
|
76
|
+
if (!this.xAxis || !this.svg)
|
|
77
|
+
return;
|
|
78
|
+
const xKey = this.getXKey();
|
|
79
|
+
const labels = this.data.map((item) => item[xKey]);
|
|
80
|
+
this.xAxis.estimateLayoutSpace?.(labels, this.theme, this.svg.node());
|
|
81
|
+
}
|
|
74
82
|
renderChart() {
|
|
75
83
|
if (!this.plotArea) {
|
|
76
84
|
throw new Error('Plot area not calculated');
|