@internetstiftelsen/charts 0.9.0 → 0.9.2

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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/{area.d.ts → dist/area.d.ts} +1 -1
  4. package/{bar.d.ts → dist/bar.d.ts} +3 -4
  5. package/{bar.js → dist/bar.js} +3 -11
  6. package/{base-chart.d.ts → dist/base-chart.d.ts} +30 -18
  7. package/{base-chart.js → dist/base-chart.js} +170 -50
  8. package/dist/chart-interface.d.ts +19 -0
  9. package/{donut-center-content.d.ts → dist/donut-center-content.d.ts} +1 -1
  10. package/{donut-chart.d.ts → dist/donut-chart.d.ts} +19 -4
  11. package/{donut-chart.js → dist/donut-chart.js} +140 -2
  12. package/{gauge-chart.d.ts → dist/gauge-chart.d.ts} +2 -2
  13. package/{gauge-chart.js → dist/gauge-chart.js} +2 -0
  14. package/{grid.d.ts → dist/grid.d.ts} +4 -4
  15. package/{grid.js → dist/grid.js} +15 -9
  16. package/{layout-manager.d.ts → dist/layout-manager.d.ts} +5 -5
  17. package/{legend.d.ts → dist/legend.d.ts} +3 -1
  18. package/{legend.js → dist/legend.js} +32 -0
  19. package/{line.d.ts → dist/line.d.ts} +1 -1
  20. package/{pie-chart.d.ts → dist/pie-chart.d.ts} +4 -11
  21. package/{pie-chart.js → dist/pie-chart.js} +23 -21
  22. package/{radial-chart-base.js → dist/radial-chart-base.js} +3 -1
  23. package/{theme.d.ts → dist/theme.d.ts} +2 -0
  24. package/{theme.js → dist/theme.js} +24 -29
  25. package/{title.d.ts → dist/title.d.ts} +1 -1
  26. package/{tooltip.d.ts → dist/tooltip.d.ts} +1 -1
  27. package/{tooltip.js → dist/tooltip.js} +239 -74
  28. package/{types.d.ts → dist/types.d.ts} +29 -12
  29. package/{utils.d.ts → dist/utils.d.ts} +0 -2
  30. package/{utils.js → dist/utils.js} +0 -5
  31. package/{word-cloud-chart.d.ts → dist/word-cloud-chart.d.ts} +1 -1
  32. package/{word-cloud-chart.js → dist/word-cloud-chart.js} +2 -0
  33. package/{x-axis.d.ts → dist/x-axis.d.ts} +2 -1
  34. package/{x-axis.js → dist/x-axis.js} +18 -14
  35. package/{xy-chart.d.ts → dist/xy-chart.d.ts} +8 -5
  36. package/{xy-chart.js → dist/xy-chart.js} +32 -6
  37. package/{y-axis.d.ts → dist/y-axis.d.ts} +1 -1
  38. package/{y-axis.js → dist/y-axis.js} +4 -4
  39. package/package.json +38 -36
  40. package/chart-interface.d.ts +0 -13
  41. /package/{area.js → dist/area.js} +0 -0
  42. /package/{chart-interface.js → dist/chart-interface.js} +0 -0
  43. /package/{donut-center-content.js → dist/donut-center-content.js} +0 -0
  44. /package/{export-image.d.ts → dist/export-image.d.ts} +0 -0
  45. /package/{export-image.js → dist/export-image.js} +0 -0
  46. /package/{export-pdf.d.ts → dist/export-pdf.d.ts} +0 -0
  47. /package/{export-pdf.js → dist/export-pdf.js} +0 -0
  48. /package/{export-tabular.d.ts → dist/export-tabular.d.ts} +0 -0
  49. /package/{export-tabular.js → dist/export-tabular.js} +0 -0
  50. /package/{export-xlsx.d.ts → dist/export-xlsx.d.ts} +0 -0
  51. /package/{export-xlsx.js → dist/export-xlsx.js} +0 -0
  52. /package/{grouped-data.d.ts → dist/grouped-data.d.ts} +0 -0
  53. /package/{grouped-data.js → dist/grouped-data.js} +0 -0
  54. /package/{grouped-tabular.d.ts → dist/grouped-tabular.d.ts} +0 -0
  55. /package/{grouped-tabular.js → dist/grouped-tabular.js} +0 -0
  56. /package/{layout-manager.js → dist/layout-manager.js} +0 -0
  57. /package/{line.js → dist/line.js} +0 -0
  58. /package/{radial-chart-base.d.ts → dist/radial-chart-base.d.ts} +0 -0
  59. /package/{scale-utils.d.ts → dist/scale-utils.d.ts} +0 -0
  60. /package/{scale-utils.js → dist/scale-utils.js} +0 -0
  61. /package/{title.js → dist/title.js} +0 -0
  62. /package/{types.js → dist/types.js} +0 -0
  63. /package/{validation.d.ts → dist/validation.d.ts} +0 -0
  64. /package/{validation.js → dist/validation.js} +0 -0
@@ -1,14 +1,23 @@
1
1
  import type { DataItem, LegendSeries } from './types.js';
2
2
  import { type BaseChartConfig, type BaseRenderContext } from './base-chart.js';
3
- import type { ChartComponent } from './chart-interface.js';
3
+ import type { ChartComponentBase } from './chart-interface.js';
4
4
  import { RadialChartBase } from './radial-chart-base.js';
5
5
  export type DonutConfig = {
6
6
  innerRadius?: number;
7
7
  padAngle?: number;
8
8
  cornerRadius?: number;
9
9
  };
10
+ export type DonutValueLabelPosition = 'outside' | 'auto';
11
+ export type DonutValueLabelConfig = {
12
+ show?: boolean;
13
+ position?: DonutValueLabelPosition;
14
+ outsideOffset?: number;
15
+ minVerticalSpacing?: number;
16
+ formatter?: (label: string, value: number, data: DataItem, percentage: number) => string;
17
+ };
10
18
  export type DonutChartConfig = BaseChartConfig & {
11
19
  donut?: DonutConfig;
20
+ valueLabel?: DonutValueLabelConfig;
12
21
  valueKey?: string;
13
22
  labelKey?: string;
14
23
  };
@@ -18,19 +27,25 @@ export declare class DonutChart extends RadialChartBase {
18
27
  private readonly cornerRadius;
19
28
  private readonly valueKey;
20
29
  private readonly labelKey;
30
+ private readonly valueLabel;
21
31
  private segments;
22
32
  private centerContent;
23
33
  constructor(config: DonutChartConfig);
24
34
  private validateDonutData;
25
35
  private prepareSegments;
26
- addChild(component: ChartComponent): this;
27
- protected getExportComponents(): ChartComponent[];
36
+ addChild(component: ChartComponentBase): this;
37
+ protected getExportComponents(): ChartComponentBase[];
28
38
  update(data: DataItem[]): void;
29
39
  protected createExportChart(): RadialChartBase;
30
- protected applyComponentOverrides(overrides: Map<ChartComponent, ChartComponent>): () => void;
40
+ protected applyComponentOverrides(overrides: Map<ChartComponentBase, ChartComponentBase>): () => void;
31
41
  protected syncDerivedState(): void;
32
42
  protected renderChart({ svg, plotGroup, plotArea, }: BaseRenderContext): void;
33
43
  protected getLegendSeries(): LegendSeries[];
34
44
  private buildTooltipContent;
45
+ private formatValueLabelText;
35
46
  private renderSegments;
47
+ private renderLabels;
48
+ private getArcPoint;
49
+ private resolveOutsideLabel;
50
+ private adjustOutsideLabelPositions;
36
51
  }
@@ -4,6 +4,8 @@ import { ChartValidator } from './validation.js';
4
4
  import { RadialChartBase } from './radial-chart-base.js';
5
5
  const HOVER_EXPAND_PX = 8;
6
6
  const ANIMATION_DURATION_MS = 150;
7
+ const OUTSIDE_LABEL_TEXT_OFFSET_PX = 10;
8
+ const OUTSIDE_LABEL_LINE_INSET_PX = 4;
7
9
  export class DonutChart extends RadialChartBase {
8
10
  constructor(config) {
9
11
  super(config);
@@ -37,6 +39,12 @@ export class DonutChart extends RadialChartBase {
37
39
  writable: true,
38
40
  value: void 0
39
41
  });
42
+ Object.defineProperty(this, "valueLabel", {
43
+ enumerable: true,
44
+ configurable: true,
45
+ writable: true,
46
+ value: void 0
47
+ });
40
48
  Object.defineProperty(this, "segments", {
41
49
  enumerable: true,
42
50
  configurable: true,
@@ -56,6 +64,14 @@ export class DonutChart extends RadialChartBase {
56
64
  this.cornerRadius = donut.cornerRadius ?? this.theme.donut.cornerRadius;
57
65
  this.valueKey = config.valueKey ?? 'value';
58
66
  this.labelKey = config.labelKey ?? 'name';
67
+ this.valueLabel = {
68
+ show: config.valueLabel?.show ?? false,
69
+ position: config.valueLabel?.position ?? 'auto',
70
+ outsideOffset: config.valueLabel?.outsideOffset ?? 16,
71
+ minVerticalSpacing: config.valueLabel?.minVerticalSpacing ?? 14,
72
+ formatter: config.valueLabel?.formatter ??
73
+ ((label, value, _data, _percentage) => `${label}: ${value}`),
74
+ };
59
75
  this.initializeDataState();
60
76
  }
61
77
  validateDonutData() {
@@ -68,13 +84,20 @@ export class DonutChart extends RadialChartBase {
68
84
  throw new Error(`DonutChart: data item at index ${index} has negative value '${item[this.valueKey]}' for key '${this.valueKey}'`);
69
85
  }
70
86
  }
87
+ if (this.valueLabel.outsideOffset < 0) {
88
+ throw new Error(`DonutChart: valueLabel.outsideOffset must be >= 0, received '${this.valueLabel.outsideOffset}'`);
89
+ }
90
+ if (this.valueLabel.minVerticalSpacing < 0) {
91
+ throw new Error(`DonutChart: valueLabel.minVerticalSpacing must be >= 0, received '${this.valueLabel.minVerticalSpacing}'`);
92
+ }
71
93
  }
72
94
  prepareSegments() {
73
95
  this.segments = this.data.map((item, index) => ({
74
96
  label: String(item[this.labelKey]),
75
97
  value: this.parseValue(item[this.valueKey]),
76
- color: item.color ||
98
+ color: (typeof item['color'] === 'string' ? item['color'] : null) ||
77
99
  this.theme.colorPalette[index % this.theme.colorPalette.length],
100
+ source: item,
78
101
  }));
79
102
  }
80
103
  addChild(component) {
@@ -109,6 +132,8 @@ export class DonutChart extends RadialChartBase {
109
132
  createExportChart() {
110
133
  return new DonutChart({
111
134
  data: this.data,
135
+ width: this.configuredWidth,
136
+ height: this.configuredHeight,
112
137
  theme: this.theme,
113
138
  responsive: this.responsiveConfig,
114
139
  donut: {
@@ -116,6 +141,7 @@ export class DonutChart extends RadialChartBase {
116
141
  padAngle: this.padAngle,
117
142
  cornerRadius: this.cornerRadius,
118
143
  },
144
+ valueLabel: this.valueLabel,
119
145
  valueKey: this.valueKey,
120
146
  labelKey: this.labelKey,
121
147
  });
@@ -145,7 +171,12 @@ export class DonutChart extends RadialChartBase {
145
171
  const visibleSegments = this.getVisibleRadialItems(this.segments);
146
172
  const { cx, cy, outerRadius, innerRadius, fontScale } = this.getRadialLayout(plotArea, this.innerRadiusRatio);
147
173
  this.initializeTooltip();
148
- this.renderSegments(plotGroup, visibleSegments, cx, cy, innerRadius, outerRadius);
174
+ const { segmentGroup, pieData } = this.renderSegments(plotGroup, visibleSegments, cx, cy, innerRadius, outerRadius);
175
+ if (this.valueLabel.show && visibleSegments.length > 0) {
176
+ this.renderLabels(segmentGroup, pieData, outerRadius, visibleSegments.reduce((sum, segment) => {
177
+ return sum + segment.value;
178
+ }, 0), fontScale);
179
+ }
149
180
  if (this.centerContent) {
150
181
  this.centerContent.render(svg, cx, cy, this.renderTheme, fontScale);
151
182
  }
@@ -168,6 +199,9 @@ export class DonutChart extends RadialChartBase {
168
199
  }
169
200
  return `<strong>${d.data.label}</strong><br/>${d.data.value} (${percentage}%)`;
170
201
  }
202
+ formatValueLabelText(segment, total) {
203
+ return this.valueLabel.formatter(segment.label, segment.value, segment.source, total > 0 ? (segment.value / total) * 100 : 0);
204
+ }
171
205
  renderSegments(plotGroup, segments, cx, cy, innerRadius, outerRadius) {
172
206
  const pieGenerator = pie()
173
207
  .value((d) => d.value)
@@ -232,5 +266,109 @@ export class DonutChart extends RadialChartBase {
232
266
  tooltipDiv.style('visibility', 'hidden');
233
267
  }
234
268
  });
269
+ return {
270
+ segmentGroup,
271
+ pieData,
272
+ };
273
+ }
274
+ renderLabels(segmentGroup, pieData, outerRadius, total, fontScale) {
275
+ if (pieData.length === 0) {
276
+ return;
277
+ }
278
+ const labelGroup = segmentGroup
279
+ .append('g')
280
+ .attr('class', 'donut-labels');
281
+ const fontSize = this.renderTheme.valueLabel.fontSize * fontScale;
282
+ const fontFamily = this.renderTheme.valueLabel.fontFamily;
283
+ const fontWeight = this.renderTheme.valueLabel.fontWeight;
284
+ const outsideLabels = pieData.map((datum) => this.resolveOutsideLabel(datum, outerRadius));
285
+ const adjustedOutsideLabels = this.adjustOutsideLabelPositions(outsideLabels, outerRadius);
286
+ const linesGroup = labelGroup
287
+ .append('g')
288
+ .attr('class', 'donut-label-leader-lines');
289
+ adjustedOutsideLabels.forEach((outsideLabel) => {
290
+ const midAngle = (outsideLabel.datum.startAngle + outsideLabel.datum.endAngle) /
291
+ 2;
292
+ const anchorX = outerRadius + this.valueLabel.outsideOffset;
293
+ const textX = outsideLabel.side === 'right'
294
+ ? anchorX + OUTSIDE_LABEL_TEXT_OFFSET_PX
295
+ : -anchorX - OUTSIDE_LABEL_TEXT_OFFSET_PX;
296
+ const lineEndX = outsideLabel.side === 'right'
297
+ ? textX - OUTSIDE_LABEL_TEXT_OFFSET_PX / 2
298
+ : textX + OUTSIDE_LABEL_TEXT_OFFSET_PX / 2;
299
+ const start = this.getArcPoint(midAngle, outerRadius + OUTSIDE_LABEL_LINE_INSET_PX);
300
+ const elbow = this.getArcPoint(midAngle, outerRadius + this.valueLabel.outsideOffset * 0.65);
301
+ const elbowY = outsideLabel.y;
302
+ linesGroup
303
+ .append('path')
304
+ .attr('class', `donut-label-line donut-label-line-${sanitizeForCSS(outsideLabel.datum.data.label)}`)
305
+ .attr('d', `M ${start.x},${start.y} L ${elbow.x},${elbowY} L ${lineEndX},${outsideLabel.y}`)
306
+ .attr('fill', 'none')
307
+ .attr('stroke', this.renderTheme.valueLabel.border)
308
+ .attr('stroke-width', 1);
309
+ labelGroup
310
+ .append('text')
311
+ .attr('class', `donut-label donut-label--outside donut-label-${sanitizeForCSS(outsideLabel.datum.data.label)}`)
312
+ .attr('x', textX)
313
+ .attr('y', outsideLabel.y)
314
+ .attr('text-anchor', outsideLabel.textAnchor)
315
+ .attr('dominant-baseline', 'middle')
316
+ .attr('font-family', fontFamily)
317
+ .attr('font-size', `${fontSize}px`)
318
+ .attr('font-weight', fontWeight)
319
+ .attr('fill', this.renderTheme.valueLabel.color)
320
+ .text(this.formatValueLabelText(outsideLabel.datum.data, total));
321
+ });
322
+ }
323
+ getArcPoint(angle, radius) {
324
+ return {
325
+ x: Math.sin(angle) * radius,
326
+ y: -Math.cos(angle) * radius,
327
+ };
328
+ }
329
+ resolveOutsideLabel(datum, outerRadius) {
330
+ const midAngle = (datum.startAngle + datum.endAngle) / 2;
331
+ const point = this.getArcPoint(midAngle, outerRadius + this.valueLabel.outsideOffset);
332
+ const side = point.x >= 0 ? 'right' : 'left';
333
+ return {
334
+ datum,
335
+ y: point.y,
336
+ side,
337
+ textAnchor: side === 'right' ? 'start' : 'end',
338
+ };
339
+ }
340
+ adjustOutsideLabelPositions(labels, outerRadius) {
341
+ const adjustForSide = (side) => {
342
+ const sideLabels = labels
343
+ .filter((label) => label.side === side)
344
+ .sort((a, b) => a.y - b.y);
345
+ if (sideLabels.length === 0) {
346
+ return [];
347
+ }
348
+ const topLimit = -outerRadius;
349
+ const bottomLimit = outerRadius;
350
+ sideLabels[0].y = Math.max(topLimit, sideLabels[0].y);
351
+ for (let i = 1; i < sideLabels.length; i++) {
352
+ const minY = sideLabels[i - 1].y + this.valueLabel.minVerticalSpacing;
353
+ sideLabels[i].y = Math.max(sideLabels[i].y, minY);
354
+ }
355
+ const overflow = sideLabels[sideLabels.length - 1].y - bottomLimit;
356
+ if (overflow > 0) {
357
+ sideLabels[sideLabels.length - 1].y -= overflow;
358
+ for (let i = sideLabels.length - 2; i >= 0; i--) {
359
+ const maxY = sideLabels[i + 1].y -
360
+ this.valueLabel.minVerticalSpacing;
361
+ sideLabels[i].y = Math.min(sideLabels[i].y, maxY);
362
+ }
363
+ const underflow = topLimit - sideLabels[0].y;
364
+ if (underflow > 0) {
365
+ sideLabels.forEach((label) => {
366
+ label.y += underflow;
367
+ });
368
+ }
369
+ }
370
+ return sideLabels;
371
+ };
372
+ return [...adjustForSide('left'), ...adjustForSide('right')];
235
373
  }
236
374
  }
@@ -1,6 +1,6 @@
1
1
  import type { DataItem, LegendSeries } from './types.js';
2
2
  import { BaseChart, type BaseChartConfig, type BaseRenderContext } from './base-chart.js';
3
- import type { ChartComponent } from './chart-interface.js';
3
+ import type { ChartComponentBase } from './chart-interface.js';
4
4
  export type GaugeSegment = {
5
5
  from: number;
6
6
  to: number;
@@ -120,7 +120,7 @@ export declare class GaugeChart extends BaseChart {
120
120
  private clampToDomain;
121
121
  private prepareSegments;
122
122
  private defaultFormat;
123
- protected getExportComponents(): ChartComponent[];
123
+ protected getExportComponents(): ChartComponentBase[];
124
124
  update(data: DataItem[]): void;
125
125
  protected createExportChart(): BaseChart;
126
126
  protected syncDerivedState(): void;
@@ -685,6 +685,8 @@ export class GaugeChart extends BaseChart {
685
685
  createExportChart() {
686
686
  return new GaugeChart({
687
687
  data: this.data,
688
+ width: this.configuredWidth,
689
+ height: this.configuredHeight,
688
690
  theme: this.theme,
689
691
  responsive: this.responsiveConfig,
690
692
  valueKey: this.valueKey,
@@ -3,11 +3,11 @@ import type { GridConfig, ChartTheme, D3Scale, ExportHooks, GridConfigBase } fro
3
3
  import type { ChartComponent } from './chart-interface.js';
4
4
  export declare class Grid implements ChartComponent<GridConfigBase> {
5
5
  readonly type: "grid";
6
- readonly horizontal: boolean;
7
- readonly vertical: boolean;
6
+ readonly value: boolean;
7
+ readonly category: boolean;
8
8
  readonly exportHooks?: ExportHooks<GridConfigBase>;
9
9
  constructor(config?: GridConfig);
10
10
  getExportConfig(): GridConfigBase;
11
- createExportComponent(override?: Partial<GridConfigBase>): ChartComponent;
12
- render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, x: D3Scale, y: D3Scale, theme: ChartTheme): void;
11
+ createExportComponent(override?: Partial<GridConfigBase>): ChartComponent<GridConfigBase>;
12
+ render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, x: D3Scale, y: D3Scale, theme: ChartTheme, isHorizontalOrientation: boolean): void;
13
13
  }
@@ -8,13 +8,13 @@ export class Grid {
8
8
  writable: true,
9
9
  value: 'grid'
10
10
  });
11
- Object.defineProperty(this, "horizontal", {
11
+ Object.defineProperty(this, "value", {
12
12
  enumerable: true,
13
13
  configurable: true,
14
14
  writable: true,
15
15
  value: void 0
16
16
  });
17
- Object.defineProperty(this, "vertical", {
17
+ Object.defineProperty(this, "category", {
18
18
  enumerable: true,
19
19
  configurable: true,
20
20
  writable: true,
@@ -26,14 +26,14 @@ export class Grid {
26
26
  writable: true,
27
27
  value: void 0
28
28
  });
29
- this.horizontal = config?.horizontal ?? true;
30
- this.vertical = config?.vertical ?? true;
29
+ this.value = config?.value ?? true;
30
+ this.category = config?.category ?? true;
31
31
  this.exportHooks = config?.exportHooks;
32
32
  }
33
33
  getExportConfig() {
34
34
  return {
35
- horizontal: this.horizontal,
36
- vertical: this.vertical,
35
+ value: this.value,
36
+ category: this.category,
37
37
  };
38
38
  }
39
39
  createExportComponent(override) {
@@ -43,13 +43,19 @@ export class Grid {
43
43
  exportHooks: this.exportHooks,
44
44
  });
45
45
  }
46
- render(plotGroup, x, y, theme) {
46
+ render(plotGroup, x, y, theme, isHorizontalOrientation) {
47
47
  // Get plot area dimensions from the scale ranges
48
48
  const xRange = x.range();
49
49
  const yRange = y.range();
50
50
  const plotWidth = xRange[1] - xRange[0];
51
51
  const plotHeight = yRange[0] - yRange[1];
52
- if (this.horizontal) {
52
+ const showHorizontalLines = isHorizontalOrientation
53
+ ? this.category
54
+ : this.value;
55
+ const showVerticalLines = isHorizontalOrientation
56
+ ? this.value
57
+ : this.category;
58
+ if (showHorizontalLines) {
53
59
  plotGroup
54
60
  .append('g')
55
61
  .attr('class', 'grid-lines horizontal')
@@ -65,7 +71,7 @@ export class Grid {
65
71
  .selectAll('.domain')
66
72
  .remove();
67
73
  }
68
- if (this.vertical) {
74
+ if (showVerticalLines) {
69
75
  plotGroup
70
76
  .append('g')
71
77
  .attr('class', 'grid-lines vertical')
@@ -1,5 +1,5 @@
1
- import type { LayoutAwareComponent } from './chart-interface.js';
2
- import type { ChartTheme } from './types.js';
1
+ import type { LayoutAwareComponentBase } from './chart-interface.js';
2
+ import type { ResolvedChartTheme } from './types.js';
3
3
  export type PlotAreaBounds = {
4
4
  left: number;
5
5
  right: number;
@@ -20,16 +20,16 @@ export declare class LayoutManager {
20
20
  private theme;
21
21
  private plotBounds;
22
22
  private componentPositions;
23
- constructor(theme: ChartTheme);
23
+ constructor(theme: ResolvedChartTheme);
24
24
  /**
25
25
  * Calculate layout based on registered components
26
26
  * Returns the plot area bounds
27
27
  */
28
- calculateLayout(components: LayoutAwareComponent[]): PlotAreaBounds;
28
+ calculateLayout(components: LayoutAwareComponentBase[]): PlotAreaBounds;
29
29
  /**
30
30
  * Get the position for a specific component
31
31
  */
32
- getComponentPosition(component: LayoutAwareComponent): ComponentPosition;
32
+ getComponentPosition(component: LayoutAwareComponentBase): ComponentPosition;
33
33
  /**
34
34
  * Calculate positions for all components based on their space requirements
35
35
  * Components are positioned in registration order, stacking outward from the plot area
@@ -20,7 +20,7 @@ export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
20
20
  private estimatedLayoutSignature;
21
21
  constructor(config?: LegendConfig);
22
22
  getExportConfig(): LegendConfigBase;
23
- createExportComponent(override?: Partial<LegendConfigBase>): LayoutAwareComponent;
23
+ createExportComponent(override?: Partial<LegendConfigBase>): LayoutAwareComponent<LegendConfigBase>;
24
24
  setToggleCallback(callback: () => void): void;
25
25
  isInlineMode(): boolean;
26
26
  isDisconnectedMode(): boolean;
@@ -40,6 +40,8 @@ export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
40
40
  getRequiredSpace(): ComponentSpace;
41
41
  getMeasuredHeight(): number;
42
42
  render(svg: Selection<SVGSVGElement, undefined, null, undefined>, series: LegendSeries[], theme: ChartTheme, width: number, _x?: number, y?: number): void;
43
+ private isLegendItemVisible;
44
+ private isToggleActivationKey;
43
45
  private computeLayout;
44
46
  private resolveLayoutSettings;
45
47
  private buildLegendItems;
@@ -232,10 +232,36 @@ export class Legend {
232
232
  .selectAll('g')
233
233
  .data(layout.positionedItems)
234
234
  .join('g')
235
+ .attr('class', 'legend-item')
235
236
  .attr('transform', (d) => `translate(${d.x}, ${d.y})`)
237
+ .attr('tabindex', 0)
238
+ .attr('role', 'checkbox')
239
+ .attr('aria-label', (d) => `Toggle series ${d.label}`)
240
+ .attr('aria-checked', (d) => {
241
+ return String(this.isLegendItemVisible(d.dataKey));
242
+ })
236
243
  .style('cursor', 'pointer')
237
244
  .on('click', (_event, d) => {
238
245
  this.toggleSeries(d.dataKey);
246
+ })
247
+ .on('keydown', (event, d) => {
248
+ if (!this.isToggleActivationKey(event.key)) {
249
+ return;
250
+ }
251
+ event.preventDefault();
252
+ this.toggleSeries(d.dataKey);
253
+ })
254
+ .on('focus', function () {
255
+ select(this)
256
+ .select('rect')
257
+ .attr('stroke', '#111827')
258
+ .attr('stroke-width', 2);
259
+ })
260
+ .on('blur', function () {
261
+ select(this)
262
+ .select('rect')
263
+ .attr('stroke', null)
264
+ .attr('stroke-width', null);
239
265
  });
240
266
  // Add checkbox rect
241
267
  legendGroups
@@ -269,6 +295,12 @@ export class Legend {
269
295
  .attr('font-family', theme.axis.fontFamily)
270
296
  .text((d) => d.label);
271
297
  }
298
+ isLegendItemVisible(dataKey) {
299
+ return this.visibilityState.get(dataKey) ?? true;
300
+ }
301
+ isToggleActivationKey(key) {
302
+ return key === 'Enter' || key === ' ' || key === 'Spacebar';
303
+ }
272
304
  computeLayout(series, theme, width, svg) {
273
305
  const settings = this.resolveLayoutSettings(theme);
274
306
  const legendItems = this.buildLegendItems(series);
@@ -10,7 +10,7 @@ export declare class Line implements ChartComponent<LineConfigBase> {
10
10
  readonly exportHooks?: ExportHooks<LineConfigBase>;
11
11
  constructor(config: LineConfig);
12
12
  getExportConfig(): LineConfigBase;
13
- createExportComponent(override?: Partial<LineConfigBase>): ChartComponent;
13
+ createExportComponent(override?: Partial<LineConfigBase>): ChartComponent<LineConfigBase>;
14
14
  render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: D3Scale, y: D3Scale, parseValue: (value: unknown) => number, xScaleType: ScaleType | undefined, theme: ChartTheme): void;
15
15
  private renderValueLabels;
16
16
  }
@@ -1,6 +1,6 @@
1
1
  import type { DataItem, LegendSeries } from './types.js';
2
2
  import { type BaseChartConfig, type BaseRenderContext } from './base-chart.js';
3
- import type { ChartComponent } from './chart-interface.js';
3
+ import type { ChartComponentBase } from './chart-interface.js';
4
4
  import { RadialChartBase } from './radial-chart-base.js';
5
5
  export type PieSort = 'none' | 'ascending' | 'descending' | ((a: PieSegmentData, b: PieSegmentData) => number);
6
6
  export type PieConfig = {
@@ -19,19 +19,11 @@ export type PieValueLabelConfig = {
19
19
  outsideOffset?: number;
20
20
  insideMargin?: number;
21
21
  minVerticalSpacing?: number;
22
- };
23
- export type PieLabelsConfig = {
24
- show?: boolean;
25
- mode?: 'adaptive' | 'inside' | 'outside';
26
- minInsidePercentage?: number;
27
- outsideOffset?: number;
28
- minVerticalSpacing?: number;
22
+ formatter?: (label: string, value: number, data: DataItem, percentage: number) => string;
29
23
  };
30
24
  export type PieChartConfig = BaseChartConfig & {
31
25
  pie?: PieConfig;
32
26
  valueLabel?: PieValueLabelConfig;
33
- /** @deprecated Use valueLabel instead */
34
- labels?: PieLabelsConfig;
35
27
  valueKey?: string;
36
28
  labelKey?: string;
37
29
  };
@@ -56,8 +48,9 @@ export declare class PieChart extends RadialChartBase {
56
48
  private validatePieData;
57
49
  private prepareSegments;
58
50
  private warnOnTinySlices;
59
- protected getExportComponents(): ChartComponent[];
51
+ protected getExportComponents(): ChartComponentBase[];
60
52
  update(data: DataItem[]): void;
53
+ private formatValueLabelText;
61
54
  protected createExportChart(): RadialChartBase;
62
55
  protected syncDerivedState(): void;
63
56
  protected renderChart({ svg, plotGroup, plotArea, }: BaseRenderContext): void;
@@ -81,21 +81,15 @@ export class PieChart extends RadialChartBase {
81
81
  this.sort = pieConfig.sort ?? 'none';
82
82
  this.valueKey = config.valueKey ?? 'value';
83
83
  this.labelKey = config.labelKey ?? 'name';
84
- const legacyLabels = config.labels;
85
- const legacyPosition = legacyLabels?.mode === 'adaptive' ? 'auto' : legacyLabels?.mode;
86
84
  this.valueLabel = {
87
- show: config.valueLabel?.show ?? legacyLabels?.show ?? false,
88
- position: config.valueLabel?.position ?? legacyPosition ?? 'auto',
89
- minInsidePercentage: config.valueLabel?.minInsidePercentage ??
90
- legacyLabels?.minInsidePercentage ??
91
- 8,
92
- outsideOffset: config.valueLabel?.outsideOffset ??
93
- legacyLabels?.outsideOffset ??
94
- 16,
85
+ show: config.valueLabel?.show ?? false,
86
+ position: config.valueLabel?.position ?? 'auto',
87
+ minInsidePercentage: config.valueLabel?.minInsidePercentage ?? 8,
88
+ outsideOffset: config.valueLabel?.outsideOffset ?? 16,
95
89
  insideMargin: config.valueLabel?.insideMargin ?? 8,
96
- minVerticalSpacing: config.valueLabel?.minVerticalSpacing ??
97
- legacyLabels?.minVerticalSpacing ??
98
- 14,
90
+ minVerticalSpacing: config.valueLabel?.minVerticalSpacing ?? 14,
91
+ formatter: config.valueLabel?.formatter ??
92
+ ((label, value, _data, _percentage) => `${label}: ${value}`),
99
93
  };
100
94
  this.initializeDataState();
101
95
  }
@@ -148,7 +142,9 @@ export class PieChart extends RadialChartBase {
148
142
  nextSegments.push({
149
143
  label,
150
144
  value,
151
- color: item.color ||
145
+ color: (typeof item['color'] === 'string'
146
+ ? item['color']
147
+ : null) ||
152
148
  this.theme.colorPalette[nextSegments.length % this.theme.colorPalette.length],
153
149
  source: item,
154
150
  });
@@ -183,9 +179,14 @@ export class PieChart extends RadialChartBase {
183
179
  update(data) {
184
180
  super.update(data);
185
181
  }
182
+ formatValueLabelText(segment, total) {
183
+ return this.valueLabel.formatter(segment.label, segment.value, segment.source, total > 0 ? (segment.value / total) * 100 : 0);
184
+ }
186
185
  createExportChart() {
187
186
  return new PieChart({
188
187
  data: this.data,
188
+ width: this.configuredWidth,
189
+ height: this.configuredHeight,
189
190
  theme: this.theme,
190
191
  responsive: this.responsiveConfig,
191
192
  pie: {
@@ -416,7 +417,8 @@ export class PieChart extends RadialChartBase {
416
417
  const outsideLabels = [];
417
418
  pieData.forEach((d) => {
418
419
  const percentage = total > 0 ? (d.data.value / total) * 100 : 0;
419
- const placement = this.resolveValueLabelPlacement(d, percentage, innerRadius, outerRadius, insideLabelRadius, fontSize, fontWeight);
420
+ const labelText = this.formatValueLabelText(d.data, total);
421
+ const placement = this.resolveValueLabelPlacement(d, labelText, percentage, innerRadius, outerRadius, insideLabelRadius, fontSize, fontWeight);
420
422
  if (placement === 'inside') {
421
423
  const [x, y] = insideArc.centroid(d);
422
424
  labelGroup
@@ -430,7 +432,7 @@ export class PieChart extends RadialChartBase {
430
432
  .attr('font-size', `${fontSize}px`)
431
433
  .attr('font-weight', fontWeight)
432
434
  .attr('fill', getContrastTextColor(d.data.color))
433
- .text(d.data.label);
435
+ .text(labelText);
434
436
  return;
435
437
  }
436
438
  if (placement === 'outside') {
@@ -475,11 +477,11 @@ export class PieChart extends RadialChartBase {
475
477
  .attr('font-size', `${fontSize}px`)
476
478
  .attr('font-weight', fontWeight)
477
479
  .attr('fill', this.renderTheme.valueLabel.color)
478
- .text(outsideLabel.datum.data.label);
480
+ .text(this.formatValueLabelText(outsideLabel.datum.data, total));
479
481
  });
480
482
  }
481
- resolveValueLabelPlacement(datum, percentage, innerRadius, outerRadius, insideLabelRadius, fontSize, fontWeight) {
482
- const fitsInside = this.canFitInsideLabel(datum, innerRadius, outerRadius, insideLabelRadius, fontSize, fontWeight);
483
+ resolveValueLabelPlacement(datum, labelText, percentage, innerRadius, outerRadius, insideLabelRadius, fontSize, fontWeight) {
484
+ const fitsInside = this.canFitInsideLabel(datum, labelText, innerRadius, outerRadius, insideLabelRadius, fontSize, fontWeight);
483
485
  if (this.valueLabel.position === 'inside') {
484
486
  return fitsInside ? 'inside' : 'hidden';
485
487
  }
@@ -494,7 +496,7 @@ export class PieChart extends RadialChartBase {
494
496
  }
495
497
  return 'inside';
496
498
  }
497
- canFitInsideLabel(datum, innerRadius, outerRadius, insideLabelRadius, fontSize, fontWeight) {
499
+ canFitInsideLabel(datum, labelText, innerRadius, outerRadius, insideLabelRadius, fontSize, fontWeight) {
498
500
  if (!this.svg) {
499
501
  return false;
500
502
  }
@@ -502,7 +504,7 @@ export class PieChart extends RadialChartBase {
502
504
  if (!svgNode) {
503
505
  return false;
504
506
  }
505
- const textWidth = measureTextWidth(datum.data.label, fontSize, this.renderTheme.axis.fontFamily, fontWeight, svgNode);
507
+ const textWidth = measureTextWidth(labelText, fontSize, this.renderTheme.axis.fontFamily, fontWeight, svgNode);
506
508
  const angle = Math.max(0, datum.endAngle - datum.startAngle);
507
509
  const availableArcLength = angle * insideLabelRadius - this.valueLabel.insideMargin * 2;
508
510
  const availableRadialThickness = outerRadius - innerRadius - this.valueLabel.insideMargin * 2;
@@ -1,4 +1,5 @@
1
1
  import { BaseChart } from './base-chart.js';
2
+ import { DEFAULT_CHART_HEIGHT } from './theme.js';
2
3
  const TOOLTIP_OFFSET_PX = 12;
3
4
  const EDGE_MARGIN_PX = 10;
4
5
  export class RadialChartBase extends BaseChart {
@@ -69,7 +70,8 @@ export class RadialChartBase extends BaseChart {
69
70
  tooltipDiv.style('left', `${x}px`).style('top', `${y}px`);
70
71
  }
71
72
  resolveRadialFontScale(outerRadius, theme) {
72
- const plotHeight = Math.max(1, theme.height - theme.margins.top - theme.margins.bottom);
73
+ const referenceHeight = this.configuredHeight ?? DEFAULT_CHART_HEIGHT;
74
+ const plotHeight = Math.max(1, referenceHeight - theme.margins.top - theme.margins.bottom);
73
75
  const referenceRadius = Math.max(1, plotHeight / 2);
74
76
  const rawScale = outerRadius / referenceRadius;
75
77
  return Math.max(0.5, Math.min(1, rawScale));
@@ -1,5 +1,7 @@
1
1
  import { type ChartTheme, type ResponsiveConfig } from './types.js';
2
2
  export declare const DEFAULT_COLOR_PALETTE: string[];
3
+ export declare const DEFAULT_CHART_WIDTH = 928;
4
+ export declare const DEFAULT_CHART_HEIGHT = 600;
3
5
  export declare const defaultTheme: ChartTheme;
4
6
  export declare const newspaperTheme: ChartTheme;
5
7
  export declare const defaultResponsiveConfig: ResponsiveConfig;