@internetstiftelsen/charts 0.6.0 → 0.6.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/donut-chart.d.ts CHANGED
@@ -26,9 +26,11 @@ export declare class DonutChart extends BaseChart {
26
26
  protected getExportComponents(): ChartComponent[];
27
27
  update(data: DataItem[]): void;
28
28
  protected getLayoutComponents(): LayoutAwareComponent[];
29
+ protected prepareLayout(): void;
29
30
  protected createExportChart(): BaseChart;
30
31
  protected renderChart(): void;
31
32
  private resolveFontScale;
33
+ private getLegendSeries;
32
34
  private positionTooltip;
33
35
  private buildTooltipContent;
34
36
  private renderSegments;
package/donut-chart.js CHANGED
@@ -129,6 +129,12 @@ export class DonutChart extends BaseChart {
129
129
  }
130
130
  return components;
131
131
  }
132
+ prepareLayout() {
133
+ const svgNode = this.svg?.node();
134
+ if (svgNode && this.legend) {
135
+ this.legend.estimateLayoutSpace(this.getLegendSeries(), this.theme, this.width, svgNode);
136
+ }
137
+ }
132
138
  createExportChart() {
133
139
  return new DonutChart({
134
140
  data: this.data,
@@ -167,11 +173,7 @@ export class DonutChart extends BaseChart {
167
173
  }
168
174
  if (this.legend) {
169
175
  const pos = this.layoutManager.getComponentPosition(this.legend);
170
- const legendSeries = this.segments.map((seg) => ({
171
- dataKey: seg.label,
172
- fill: seg.color,
173
- }));
174
- this.legend.render(this.svg, legendSeries, this.theme, this.width, pos.x, pos.y);
176
+ this.legend.render(this.svg, this.getLegendSeries(), this.theme, this.width, pos.x, pos.y);
175
177
  }
176
178
  }
177
179
  resolveFontScale(outerRadius) {
@@ -180,6 +182,14 @@ export class DonutChart extends BaseChart {
180
182
  const rawScale = outerRadius / referenceRadius;
181
183
  return Math.max(0.5, Math.min(1, rawScale));
182
184
  }
185
+ getLegendSeries() {
186
+ return this.segments.map((segment) => {
187
+ return {
188
+ dataKey: segment.label,
189
+ fill: segment.color,
190
+ };
191
+ });
192
+ }
183
193
  positionTooltip(event, tooltipDiv) {
184
194
  const node = tooltipDiv.node();
185
195
  if (!node)
package/gauge-chart.d.ts CHANGED
@@ -112,6 +112,7 @@ export declare class GaugeChart extends BaseChart {
112
112
  protected getExportComponents(): ChartComponent[];
113
113
  update(data: DataItem[]): void;
114
114
  protected getLayoutComponents(): LayoutAwareComponent[];
115
+ protected prepareLayout(): void;
115
116
  protected createExportChart(): BaseChart;
116
117
  protected renderChart(): void;
117
118
  private buildAriaLabel;
@@ -135,4 +136,5 @@ export declare class GaugeChart extends BaseChart {
135
136
  private buildTooltipContent;
136
137
  private positionTooltip;
137
138
  private renderLegend;
139
+ private getLegendSeries;
138
140
  }
package/gauge-chart.js CHANGED
@@ -544,6 +544,12 @@ export class GaugeChart extends BaseChart {
544
544
  }
545
545
  return components;
546
546
  }
547
+ prepareLayout() {
548
+ const svgNode = this.svg?.node();
549
+ if (svgNode && this.legend) {
550
+ this.legend.estimateLayoutSpace(this.getLegendSeries(), this.theme, this.width, svgNode);
551
+ }
552
+ }
547
553
  createExportChart() {
548
554
  return new GaugeChart({
549
555
  data: this.data,
@@ -1032,10 +1038,14 @@ export class GaugeChart extends BaseChart {
1032
1038
  return;
1033
1039
  }
1034
1040
  const legendPosition = this.layoutManager.getComponentPosition(this.legend);
1035
- const legendSeries = this.segments.map((segment) => ({
1036
- dataKey: segment.legendLabel,
1037
- fill: segment.color,
1038
- }));
1039
- this.legend.render(this.svg, legendSeries, this.theme, this.width, legendPosition.x, legendPosition.y);
1041
+ this.legend.render(this.svg, this.getLegendSeries(), this.theme, this.width, legendPosition.x, legendPosition.y);
1042
+ }
1043
+ getLegendSeries() {
1044
+ return this.segments.map((segment) => {
1045
+ return {
1046
+ dataKey: segment.legendLabel,
1047
+ fill: segment.color,
1048
+ };
1049
+ });
1040
1050
  }
1041
1051
  }
package/legend.d.ts CHANGED
@@ -7,18 +7,32 @@ export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
7
7
  readonly exportHooks?: ExportHooks<LegendConfigBase>;
8
8
  private readonly marginTop;
9
9
  private readonly marginBottom;
10
- private readonly itemHeight;
10
+ private readonly paddingX?;
11
+ private readonly itemSpacingX?;
12
+ private readonly itemSpacingY?;
13
+ private readonly gapBetweenBoxAndText;
11
14
  private visibilityState;
12
15
  private onToggleCallback?;
16
+ private estimatedLayout;
17
+ private estimatedLayoutSignature;
13
18
  constructor(config?: LegendConfig);
14
19
  getExportConfig(): LegendConfigBase;
15
20
  createExportComponent(override?: Partial<LegendConfigBase>): LayoutAwareComponent;
16
21
  setToggleCallback(callback: () => void): void;
17
22
  isSeriesVisible(dataKey: string): boolean;
23
+ estimateLayoutSpace(series: LegendSeries[], theme: ChartTheme, width: number, svg: SVGSVGElement): void;
18
24
  private getCheckmarkPath;
19
25
  /**
20
26
  * Returns the space required by the legend
21
27
  */
22
28
  getRequiredSpace(): ComponentSpace;
23
29
  render(svg: Selection<SVGSVGElement, undefined, null, undefined>, series: LegendSeries[], theme: ChartTheme, width: number, _x?: number, y?: number): void;
30
+ private computeLayout;
31
+ private resolveLayoutSettings;
32
+ private buildLegendItems;
33
+ private measureLegendItemWidths;
34
+ private buildRows;
35
+ private positionRows;
36
+ private getLayoutSignature;
37
+ private getFallbackRowHeight;
24
38
  }
package/legend.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { select } from 'd3';
1
2
  import { getSeriesColor } from './types.js';
2
3
  import { getContrastTextColor, mergeDeep } from './utils.js';
3
4
  export class Legend {
@@ -32,11 +33,29 @@ export class Legend {
32
33
  writable: true,
33
34
  value: void 0
34
35
  });
35
- Object.defineProperty(this, "itemHeight", {
36
+ Object.defineProperty(this, "paddingX", {
36
37
  enumerable: true,
37
38
  configurable: true,
38
39
  writable: true,
39
- value: 15
40
+ value: void 0
41
+ });
42
+ Object.defineProperty(this, "itemSpacingX", {
43
+ enumerable: true,
44
+ configurable: true,
45
+ writable: true,
46
+ value: void 0
47
+ });
48
+ Object.defineProperty(this, "itemSpacingY", {
49
+ enumerable: true,
50
+ configurable: true,
51
+ writable: true,
52
+ value: void 0
53
+ });
54
+ Object.defineProperty(this, "gapBetweenBoxAndText", {
55
+ enumerable: true,
56
+ configurable: true,
57
+ writable: true,
58
+ value: 8
40
59
  });
41
60
  Object.defineProperty(this, "visibilityState", {
42
61
  enumerable: true,
@@ -50,9 +69,24 @@ export class Legend {
50
69
  writable: true,
51
70
  value: void 0
52
71
  });
72
+ Object.defineProperty(this, "estimatedLayout", {
73
+ enumerable: true,
74
+ configurable: true,
75
+ writable: true,
76
+ value: null
77
+ });
78
+ Object.defineProperty(this, "estimatedLayoutSignature", {
79
+ enumerable: true,
80
+ configurable: true,
81
+ writable: true,
82
+ value: null
83
+ });
53
84
  this.position = config?.position || 'bottom';
54
85
  this.marginTop = config?.marginTop ?? 20;
55
86
  this.marginBottom = config?.marginBottom ?? 10;
87
+ this.paddingX = config?.paddingX;
88
+ this.itemSpacingX = config?.itemSpacingX;
89
+ this.itemSpacingY = config?.itemSpacingY;
56
90
  this.exportHooks = config?.exportHooks;
57
91
  }
58
92
  getExportConfig() {
@@ -60,6 +94,9 @@ export class Legend {
60
94
  position: this.position,
61
95
  marginTop: this.marginTop,
62
96
  marginBottom: this.marginBottom,
97
+ paddingX: this.paddingX,
98
+ itemSpacingX: this.itemSpacingX,
99
+ itemSpacingY: this.itemSpacingY,
63
100
  };
64
101
  }
65
102
  createExportComponent(override) {
@@ -77,6 +114,11 @@ export class Legend {
77
114
  isSeriesVisible(dataKey) {
78
115
  return this.visibilityState.get(dataKey) ?? true;
79
116
  }
117
+ estimateLayoutSpace(series, theme, width, svg) {
118
+ const signature = this.getLayoutSignature(series, width, theme);
119
+ this.estimatedLayout = this.computeLayout(series, theme, width, svg);
120
+ this.estimatedLayoutSignature = signature;
121
+ }
80
122
  getCheckmarkPath(size) {
81
123
  const scale = (size / 24) * 0.7;
82
124
  const offsetX = size * 0.15;
@@ -89,56 +131,32 @@ export class Legend {
89
131
  getRequiredSpace() {
90
132
  return {
91
133
  width: 0, // Legend spans full width
92
- height: this.marginTop + this.itemHeight + this.marginBottom,
134
+ height: this.estimatedLayout?.requiredHeight ??
135
+ this.marginTop + this.marginBottom,
93
136
  position: 'bottom',
94
137
  };
95
138
  }
96
139
  render(svg, series, theme, width, _x = 0, y = 0) {
97
- const boxSize = theme.legend.boxSize;
98
- const gapBetweenBoxAndText = 8;
99
- const itemSpacing = 20; // Space between legend items
100
- const legendItems = series.map((s) => ({
101
- label: s.dataKey,
102
- color: getSeriesColor(s),
103
- dataKey: s.dataKey,
104
- }));
105
- legendItems.forEach((item) => {
106
- if (!this.visibilityState.has(item.dataKey)) {
107
- this.visibilityState.set(item.dataKey, true);
108
- }
109
- });
110
- // Create temporary text elements to measure widths
111
- const tempSvg = svg.append('g').style('visibility', 'hidden');
112
- const itemWidths = legendItems.map((item) => {
113
- const textElem = tempSvg
114
- .append('text')
115
- .attr('font-size', `${theme.legend.fontSize}px`)
116
- .attr('font-family', theme.axis.fontFamily)
117
- .text(item.label);
118
- const textWidth = textElem.node()?.getBBox().width || 0;
119
- textElem.remove();
120
- return boxSize + gapBetweenBoxAndText + textWidth;
121
- });
122
- tempSvg.remove();
123
- // Calculate positions for each item
124
- const itemPositions = [];
125
- let currentX = 0;
126
- itemWidths.forEach((itemWidth) => {
127
- itemPositions.push(currentX);
128
- currentX += itemWidth + itemSpacing;
129
- });
130
- const totalLegendWidth = currentX - itemSpacing;
131
- const legendX = (width - totalLegendWidth) / 2;
140
+ const svgNode = svg.node();
141
+ if (!svgNode) {
142
+ return;
143
+ }
144
+ const signature = this.getLayoutSignature(series, width, theme);
145
+ const layout = this.estimatedLayout && this.estimatedLayoutSignature === signature
146
+ ? this.estimatedLayout
147
+ : this.computeLayout(series, theme, width, svgNode);
148
+ this.estimatedLayout = layout;
149
+ this.estimatedLayoutSignature = signature;
132
150
  const legendY = y + this.marginTop;
133
151
  const legend = svg
134
152
  .append('g')
135
153
  .attr('class', 'legend')
136
- .attr('transform', `translate(${legendX}, ${legendY})`);
154
+ .attr('transform', `translate(0, ${legendY})`);
137
155
  const legendGroups = legend
138
156
  .selectAll('g')
139
- .data(legendItems)
157
+ .data(layout.positionedItems)
140
158
  .join('g')
141
- .attr('transform', (_d, i) => `translate(${itemPositions[i]}, 0)`)
159
+ .attr('transform', (d) => `translate(${d.x}, ${d.y})`)
142
160
  .style('cursor', 'pointer')
143
161
  .on('click', (_event, d) => {
144
162
  const currentState = this.visibilityState.get(d.dataKey) ?? true;
@@ -150,8 +168,8 @@ export class Legend {
150
168
  // Add checkbox rect
151
169
  legendGroups
152
170
  .append('rect')
153
- .attr('width', boxSize)
154
- .attr('height', boxSize)
171
+ .attr('width', theme.legend.boxSize)
172
+ .attr('height', theme.legend.boxSize)
155
173
  .attr('fill', (d) => {
156
174
  const isVisible = this.visibilityState.get(d.dataKey) ?? true;
157
175
  return isVisible ? d.color : theme.legend.uncheckedColor;
@@ -160,7 +178,7 @@ export class Legend {
160
178
  // Add checkmark when visible
161
179
  legendGroups
162
180
  .append('path')
163
- .attr('d', this.getCheckmarkPath(boxSize))
181
+ .attr('d', this.getCheckmarkPath(theme.legend.boxSize))
164
182
  .attr('fill', 'none')
165
183
  .attr('stroke', (d) => getContrastTextColor(d.color))
166
184
  .attr('stroke-width', 2)
@@ -173,10 +191,149 @@ export class Legend {
173
191
  // Add label text
174
192
  legendGroups
175
193
  .append('text')
176
- .attr('x', boxSize + gapBetweenBoxAndText)
177
- .attr('y', boxSize / 2 + 4)
194
+ .attr('x', theme.legend.boxSize + this.gapBetweenBoxAndText)
195
+ .attr('y', theme.legend.boxSize / 2 + 4)
178
196
  .attr('font-size', `${theme.legend.fontSize}px`)
179
197
  .attr('font-family', theme.axis.fontFamily)
180
198
  .text((d) => d.label);
181
199
  }
200
+ computeLayout(series, theme, width, svg) {
201
+ const settings = this.resolveLayoutSettings(theme);
202
+ const legendItems = this.buildLegendItems(series);
203
+ legendItems.forEach((item) => {
204
+ if (!this.visibilityState.has(item.dataKey)) {
205
+ this.visibilityState.set(item.dataKey, true);
206
+ }
207
+ });
208
+ const measuredItems = this.measureLegendItemWidths(legendItems, theme, svg);
209
+ const rows = this.buildRows(measuredItems, width, settings);
210
+ const positionedItems = this.positionRows(rows, width, settings);
211
+ const rowCount = Math.max(rows.length, 1);
212
+ const rowsHeight = rows.length > 0
213
+ ? rows.reduce((sum, row) => {
214
+ return sum + row.height;
215
+ }, 0)
216
+ : this.getFallbackRowHeight(theme);
217
+ const requiredHeight = this.marginTop +
218
+ rowsHeight +
219
+ Math.max(0, rowCount - 1) * settings.itemSpacingY +
220
+ this.marginBottom;
221
+ return {
222
+ positionedItems,
223
+ requiredHeight,
224
+ };
225
+ }
226
+ resolveLayoutSettings(theme) {
227
+ return {
228
+ paddingX: this.paddingX ?? theme.legend.paddingX,
229
+ itemSpacingX: this.itemSpacingX ?? theme.legend.itemSpacingX,
230
+ itemSpacingY: this.itemSpacingY ?? theme.legend.itemSpacingY,
231
+ };
232
+ }
233
+ buildLegendItems(series) {
234
+ return series.map((item) => ({
235
+ label: item.dataKey,
236
+ color: getSeriesColor(item),
237
+ dataKey: item.dataKey,
238
+ width: 0,
239
+ height: 0,
240
+ }));
241
+ }
242
+ measureLegendItemWidths(legendItems, theme, svg) {
243
+ const tempSvg = select(svg).append('g').style('visibility', 'hidden');
244
+ const measuredItems = legendItems.map((item) => {
245
+ const textElem = tempSvg
246
+ .append('text')
247
+ .attr('font-size', `${theme.legend.fontSize}px`)
248
+ .attr('font-family', theme.axis.fontFamily)
249
+ .text(item.label);
250
+ const textBBox = textElem.node()?.getBBox();
251
+ const textWidth = textBBox?.width || 0;
252
+ const textHeight = textBBox?.height || theme.legend.fontSize;
253
+ textElem.remove();
254
+ return {
255
+ ...item,
256
+ width: theme.legend.boxSize +
257
+ this.gapBetweenBoxAndText +
258
+ textWidth,
259
+ height: Math.max(theme.legend.boxSize, textHeight),
260
+ };
261
+ });
262
+ tempSvg.remove();
263
+ return measuredItems;
264
+ }
265
+ buildRows(legendItems, width, settings) {
266
+ const rows = [];
267
+ const availableWidth = Math.max(0, width - settings.paddingX * 2);
268
+ let currentRow = {
269
+ items: [],
270
+ width: 0,
271
+ height: 0,
272
+ };
273
+ legendItems.forEach((item) => {
274
+ const nextWidth = currentRow.items.length === 0
275
+ ? item.width
276
+ : currentRow.width + settings.itemSpacingX + item.width;
277
+ if (currentRow.items.length > 0 &&
278
+ nextWidth > availableWidth) {
279
+ rows.push(currentRow);
280
+ currentRow = {
281
+ items: [item],
282
+ width: item.width,
283
+ height: item.height,
284
+ };
285
+ return;
286
+ }
287
+ currentRow.items.push(item);
288
+ currentRow.width = nextWidth;
289
+ currentRow.height = Math.max(currentRow.height, item.height);
290
+ });
291
+ if (currentRow.items.length > 0) {
292
+ rows.push(currentRow);
293
+ }
294
+ return rows;
295
+ }
296
+ positionRows(rows, width, settings) {
297
+ const positionedItems = [];
298
+ const availableWidth = Math.max(0, width - settings.paddingX * 2);
299
+ let accumulatedRowHeight = 0;
300
+ rows.forEach((row, rowIndex) => {
301
+ const rowX = settings.paddingX +
302
+ Math.max(0, (availableWidth - row.width) / 2);
303
+ const rowY = accumulatedRowHeight + rowIndex * settings.itemSpacingY;
304
+ let currentX = rowX;
305
+ row.items.forEach((item, itemIndex) => {
306
+ const centeredY = rowY + (row.height - item.height) / 2;
307
+ positionedItems.push({
308
+ ...item,
309
+ x: currentX,
310
+ y: centeredY,
311
+ });
312
+ currentX += item.width;
313
+ if (itemIndex < row.items.length - 1) {
314
+ currentX += settings.itemSpacingX;
315
+ }
316
+ });
317
+ accumulatedRowHeight += row.height;
318
+ });
319
+ return positionedItems;
320
+ }
321
+ getLayoutSignature(series, width, theme) {
322
+ return [
323
+ width,
324
+ theme.legend.boxSize,
325
+ theme.legend.fontSize,
326
+ theme.axis.fontFamily,
327
+ theme.legend.paddingX,
328
+ theme.legend.itemSpacingX,
329
+ theme.legend.itemSpacingY,
330
+ this.paddingX ?? '',
331
+ this.itemSpacingX ?? '',
332
+ this.itemSpacingY ?? '',
333
+ series.map((item) => item.dataKey).join('|'),
334
+ ].join(':');
335
+ }
336
+ getFallbackRowHeight(theme) {
337
+ return Math.max(theme.legend.boxSize, theme.legend.fontSize);
338
+ }
182
339
  }
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.6.0",
2
+ "version": "0.6.1",
3
3
  "name": "@internetstiftelsen/charts",
4
4
  "type": "module",
5
5
  "sideEffects": false,
@@ -18,6 +18,7 @@
18
18
  "dev": "vite",
19
19
  "build": "tsc -b && vite build",
20
20
  "lint": "eslint .",
21
+ "format": "prettier --write ./src",
21
22
  "preview": "vite preview",
22
23
  "test": "vitest",
23
24
  "test:run": "vitest run",
package/pie-chart.d.ts CHANGED
@@ -59,9 +59,11 @@ export declare class PieChart extends BaseChart {
59
59
  protected getExportComponents(): ChartComponent[];
60
60
  update(data: DataItem[]): void;
61
61
  protected getLayoutComponents(): LayoutAwareComponent[];
62
+ protected prepareLayout(): void;
62
63
  protected createExportChart(): BaseChart;
63
64
  protected renderChart(): void;
64
65
  private resolveFontScale;
66
+ private getLegendSeries;
65
67
  private resolveSortComparator;
66
68
  private renderSegments;
67
69
  private handleSegmentKeyNavigation;
package/pie-chart.js CHANGED
@@ -218,6 +218,12 @@ export class PieChart extends BaseChart {
218
218
  }
219
219
  return components;
220
220
  }
221
+ prepareLayout() {
222
+ const svgNode = this.svg?.node();
223
+ if (svgNode && this.legend) {
224
+ this.legend.estimateLayoutSpace(this.getLegendSeries(), this.theme, this.width, svgNode);
225
+ }
226
+ }
221
227
  createExportChart() {
222
228
  return new PieChart({
223
229
  data: this.data,
@@ -264,11 +270,7 @@ export class PieChart extends BaseChart {
264
270
  }
265
271
  if (this.legend) {
266
272
  const pos = this.layoutManager.getComponentPosition(this.legend);
267
- const legendSeries = this.segments.map((seg) => ({
268
- dataKey: seg.label,
269
- fill: seg.color,
270
- }));
271
- this.legend.render(this.svg, legendSeries, this.theme, this.width, pos.x, pos.y);
273
+ this.legend.render(this.svg, this.getLegendSeries(), this.theme, this.width, pos.x, pos.y);
272
274
  }
273
275
  }
274
276
  resolveFontScale(outerRadius) {
@@ -277,6 +279,14 @@ export class PieChart extends BaseChart {
277
279
  const rawScale = outerRadius / referenceRadius;
278
280
  return Math.max(0.5, Math.min(1, rawScale));
279
281
  }
282
+ getLegendSeries() {
283
+ return this.segments.map((segment) => {
284
+ return {
285
+ dataKey: segment.label,
286
+ fill: segment.color,
287
+ };
288
+ });
289
+ }
280
290
  resolveSortComparator() {
281
291
  if (typeof this.sort === 'function') {
282
292
  return this.sort;
package/theme.js CHANGED
@@ -36,6 +36,9 @@ export const defaultTheme = {
36
36
  boxSize: 24,
37
37
  uncheckedColor: '#d0d0d0',
38
38
  fontSize: 14,
39
+ paddingX: 0,
40
+ itemSpacingX: 20,
41
+ itemSpacingY: 8,
39
42
  },
40
43
  line: {
41
44
  strokeWidth: 4,
@@ -115,6 +118,9 @@ export const newspaperTheme = {
115
118
  boxSize: 18,
116
119
  uncheckedColor: '#d3d3d3',
117
120
  fontSize: 13,
121
+ paddingX: 0,
122
+ itemSpacingX: 20,
123
+ itemSpacingY: 8,
118
124
  },
119
125
  line: {
120
126
  strokeWidth: 2.5,
package/types.d.ts CHANGED
@@ -63,6 +63,9 @@ export type ChartTheme = {
63
63
  boxSize: number;
64
64
  uncheckedColor: string;
65
65
  fontSize: number;
66
+ paddingX: number;
67
+ itemSpacingX: number;
68
+ itemSpacingY: number;
66
69
  };
67
70
  line: {
68
71
  strokeWidth: number;
@@ -227,6 +230,9 @@ export type LegendConfigBase = {
227
230
  position?: 'bottom';
228
231
  marginTop?: number;
229
232
  marginBottom?: number;
233
+ paddingX?: number;
234
+ itemSpacingX?: number;
235
+ itemSpacingY?: number;
230
236
  };
231
237
  export type LegendConfig = LegendConfigBase & {
232
238
  exportHooks?: ExportHooks<LegendConfigBase>;
package/xy-chart.d.ts CHANGED
@@ -18,6 +18,7 @@ export declare class XYChart extends BaseChart {
18
18
  protected prepareLayout(): void;
19
19
  protected renderChart(): void;
20
20
  private getXKey;
21
+ private getLegendSeries;
21
22
  private getCategoryScaleType;
22
23
  private getVisibleSeries;
23
24
  private setupScales;
package/xy-chart.js CHANGED
@@ -129,19 +129,22 @@ export class XYChart extends BaseChart {
129
129
  this.update(this.sourceData);
130
130
  }
131
131
  prepareLayout() {
132
+ const svgNode = this.svg?.node();
132
133
  this.xAxis?.clearEstimatedSpace?.();
133
- if (!this.xAxis || !this.svg) {
134
- return;
134
+ if (svgNode && this.xAxis) {
135
+ const xKey = this.getXKey();
136
+ const labelKey = this.xAxis.labelKey;
137
+ const labels = this.data.map((item) => {
138
+ if (labelKey) {
139
+ return item[labelKey];
140
+ }
141
+ return item[xKey];
142
+ });
143
+ this.xAxis.estimateLayoutSpace?.(labels, this.theme, svgNode);
144
+ }
145
+ if (svgNode && this.legend) {
146
+ this.legend.estimateLayoutSpace(this.getLegendSeries(), this.theme, this.width, svgNode);
135
147
  }
136
- const xKey = this.getXKey();
137
- const labelKey = this.xAxis.labelKey;
138
- const labels = this.data.map((item) => {
139
- if (labelKey) {
140
- return item[labelKey];
141
- }
142
- return item[xKey];
143
- });
144
- this.xAxis.estimateLayoutSpace?.(labels, this.theme, this.svg.node());
145
148
  }
146
149
  renderChart() {
147
150
  if (!this.plotArea) {
@@ -198,24 +201,7 @@ export class XYChart extends BaseChart {
198
201
  }
199
202
  if (this.legend) {
200
203
  const legendPos = this.layoutManager.getComponentPosition(this.legend);
201
- this.legend.render(this.svg, this.series.map((series) => {
202
- if (series.type === 'line') {
203
- return {
204
- dataKey: series.dataKey,
205
- stroke: series.stroke,
206
- };
207
- }
208
- if (series.type === 'bar') {
209
- return {
210
- dataKey: series.dataKey,
211
- fill: series.fill,
212
- };
213
- }
214
- return {
215
- dataKey: series.dataKey,
216
- fill: series.fill,
217
- };
218
- }), this.theme, this.width, legendPos.x, legendPos.y);
204
+ this.legend.render(this.svg, this.getLegendSeries(), this.theme, this.width, legendPos.x, legendPos.y);
219
205
  }
220
206
  }
221
207
  getXKey() {
@@ -224,6 +210,20 @@ export class XYChart extends BaseChart {
224
210
  }
225
211
  return (Object.keys(this.data[0]).find((key) => !this.series.some((s) => s.dataKey === key)) || 'column');
226
212
  }
213
+ getLegendSeries() {
214
+ return this.series.map((series) => {
215
+ if (series.type === 'line') {
216
+ return {
217
+ dataKey: series.dataKey,
218
+ stroke: series.stroke,
219
+ };
220
+ }
221
+ return {
222
+ dataKey: series.dataKey,
223
+ fill: series.fill,
224
+ };
225
+ });
226
+ }
227
227
  getCategoryScaleType() {
228
228
  return this.scaleConfig.x?.type || 'band';
229
229
  }