@internetstiftelsen/charts 0.8.0 → 0.9.0

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.js CHANGED
@@ -1,12 +1,10 @@
1
1
  import { arc, pie, select } from 'd3';
2
- import { BaseChart } from './base-chart.js';
3
2
  import { sanitizeForCSS } from './utils.js';
4
3
  import { ChartValidator } from './validation.js';
4
+ import { RadialChartBase } from './radial-chart-base.js';
5
5
  const HOVER_EXPAND_PX = 8;
6
6
  const ANIMATION_DURATION_MS = 150;
7
- const TOOLTIP_OFFSET_PX = 12;
8
- const EDGE_MARGIN_PX = 10;
9
- export class DonutChart extends BaseChart {
7
+ export class DonutChart extends RadialChartBase {
10
8
  constructor(config) {
11
9
  super(config);
12
10
  Object.defineProperty(this, "innerRadiusRatio", {
@@ -58,8 +56,7 @@ export class DonutChart extends BaseChart {
58
56
  this.cornerRadius = donut.cornerRadius ?? this.theme.donut.cornerRadius;
59
57
  this.valueKey = config.valueKey ?? 'value';
60
58
  this.labelKey = config.labelKey ?? 'name';
61
- this.validateDonutData();
62
- this.prepareSegments();
59
+ this.initializeDataState();
63
60
  }
64
61
  validateDonutData() {
65
62
  ChartValidator.validateDataKey(this.data, this.labelKey, 'DonutChart');
@@ -81,65 +78,34 @@ export class DonutChart extends BaseChart {
81
78
  }));
82
79
  }
83
80
  addChild(component) {
84
- const type = component.type;
85
- if (type === 'tooltip') {
86
- this.tooltip = component;
87
- }
88
- else if (type === 'legend') {
89
- this.legend = component;
90
- this.legend.setToggleCallback(() => {
91
- if (!this.container) {
92
- return;
93
- }
94
- this.update(this.data);
95
- });
96
- }
97
- else if (type === 'title') {
98
- this.title = component;
99
- }
100
- else if (type === 'donutCenterContent') {
101
- this.centerContent = component;
81
+ if (this.tryRegisterComponent(component, [
82
+ {
83
+ type: 'donutCenterContent',
84
+ get: () => this.centerContent,
85
+ set: (entry) => {
86
+ this.centerContent = entry;
87
+ },
88
+ },
89
+ ])) {
90
+ return this;
102
91
  }
103
- return this;
92
+ return super.addChild(component);
104
93
  }
105
94
  getExportComponents() {
106
- const components = [];
107
- if (this.title) {
108
- components.push(this.title);
109
- }
110
- if (this.centerContent) {
111
- components.push(this.centerContent);
112
- }
113
- if (this.tooltip) {
114
- components.push(this.tooltip);
115
- }
116
- if (this.legend?.isInlineMode()) {
117
- components.push(this.legend);
118
- }
119
- return components;
95
+ return [
96
+ ...this.getBaseExportComponents({
97
+ title: true,
98
+ }),
99
+ ...(this.centerContent ? [this.centerContent] : []),
100
+ ...this.getBaseExportComponents({
101
+ tooltip: true,
102
+ legend: this.legend?.isInlineMode(),
103
+ }),
104
+ ];
120
105
  }
121
106
  update(data) {
122
- this.data = data;
123
- this.validateDonutData();
124
- this.prepareSegments();
125
107
  super.update(data);
126
108
  }
127
- getLayoutComponents() {
128
- const components = [];
129
- if (this.title) {
130
- components.push(this.title);
131
- }
132
- if (this.legend) {
133
- components.push(this.legend);
134
- }
135
- return components;
136
- }
137
- prepareLayout() {
138
- const svgNode = this.svg?.node();
139
- if (svgNode && this.legend?.isInlineMode()) {
140
- this.legend.estimateLayoutSpace(this.getLegendSeries(), this.theme, this.width, svgNode);
141
- }
142
- }
143
109
  createExportChart() {
144
110
  return new DonutChart({
145
111
  data: this.data,
@@ -156,81 +122,37 @@ export class DonutChart extends BaseChart {
156
122
  }
157
123
  applyComponentOverrides(overrides) {
158
124
  const restoreBase = super.applyComponentOverrides(overrides);
159
- if (overrides.size === 0) {
160
- return restoreBase;
161
- }
162
- const previousCenterContent = this.centerContent;
163
- if (this.centerContent) {
164
- const override = overrides.get(this.centerContent);
165
- if (override && override.type === 'donutCenterContent') {
166
- this.centerContent = override;
167
- }
168
- }
125
+ const restoreCenterContent = this.applySlotOverrides(overrides, [
126
+ {
127
+ type: 'donutCenterContent',
128
+ get: () => this.centerContent,
129
+ set: (component) => {
130
+ this.centerContent = component;
131
+ },
132
+ },
133
+ ]);
169
134
  return () => {
170
- this.centerContent = previousCenterContent;
135
+ restoreCenterContent();
171
136
  restoreBase();
172
137
  };
173
138
  }
174
- renderChart() {
175
- if (!this.plotArea || !this.svg || !this.plotGroup) {
176
- throw new Error('Plot area not calculated');
177
- }
178
- if (this.title) {
179
- const pos = this.layoutManager.getComponentPosition(this.title);
180
- this.title.render(this.svg, this.theme, this.width, pos.x, pos.y);
181
- }
182
- const visibleSegments = this.legend
183
- ? this.segments.filter((seg) => this.legend.isSeriesVisible(seg.label))
184
- : this.segments;
185
- const cx = this.plotArea.left + this.plotArea.width / 2;
186
- const cy = this.plotArea.top + this.plotArea.height / 2;
187
- const outerRadius = Math.min(this.plotArea.width, this.plotArea.height) / 2;
188
- const innerRadius = outerRadius * this.innerRadiusRatio;
189
- const fontScale = this.resolveFontScale(outerRadius);
190
- if (this.tooltip) {
191
- this.tooltip.initialize(this.theme);
192
- }
193
- this.renderSegments(visibleSegments, cx, cy, innerRadius, outerRadius);
139
+ syncDerivedState() {
140
+ this.validateDonutData();
141
+ this.prepareSegments();
142
+ }
143
+ renderChart({ svg, plotGroup, plotArea, }) {
144
+ this.renderTitle(svg);
145
+ const visibleSegments = this.getVisibleRadialItems(this.segments);
146
+ const { cx, cy, outerRadius, innerRadius, fontScale } = this.getRadialLayout(plotArea, this.innerRadiusRatio);
147
+ this.initializeTooltip();
148
+ this.renderSegments(plotGroup, visibleSegments, cx, cy, innerRadius, outerRadius);
194
149
  if (this.centerContent) {
195
- this.centerContent.render(this.svg, cx, cy, this.theme, fontScale);
196
- }
197
- if (this.legend?.isInlineMode()) {
198
- const pos = this.layoutManager.getComponentPosition(this.legend);
199
- this.legend.render(this.svg, this.getLegendSeries(), this.theme, this.width, pos.x, pos.y);
150
+ this.centerContent.render(svg, cx, cy, this.renderTheme, fontScale);
200
151
  }
201
- }
202
- resolveFontScale(outerRadius) {
203
- const plotHeight = Math.max(1, this.theme.height -
204
- this.theme.margins.top -
205
- this.theme.margins.bottom);
206
- const referenceRadius = Math.max(1, plotHeight / 2);
207
- const rawScale = outerRadius / referenceRadius;
208
- return Math.max(0.5, Math.min(1, rawScale));
152
+ this.renderInlineLegend(svg);
209
153
  }
210
154
  getLegendSeries() {
211
- return this.segments.map((segment) => {
212
- return {
213
- dataKey: segment.label,
214
- fill: segment.color,
215
- };
216
- });
217
- }
218
- positionTooltip(event, tooltipDiv) {
219
- const node = tooltipDiv.node();
220
- if (!node)
221
- return;
222
- const rect = node.getBoundingClientRect();
223
- let x = event.pageX + TOOLTIP_OFFSET_PX;
224
- let y = event.pageY - rect.height / 2;
225
- if (x + rect.width > window.innerWidth - EDGE_MARGIN_PX) {
226
- x = event.pageX - rect.width - TOOLTIP_OFFSET_PX;
227
- }
228
- x = Math.max(EDGE_MARGIN_PX, x);
229
- y = Math.max(EDGE_MARGIN_PX, Math.min(y, window.innerHeight +
230
- window.scrollY -
231
- rect.height -
232
- EDGE_MARGIN_PX));
233
- tooltipDiv.style('left', `${x}px`).style('top', `${y}px`);
155
+ return this.getRadialLegendSeries(this.segments);
234
156
  }
235
157
  buildTooltipContent(d, segments) {
236
158
  const total = segments.reduce((sum, s) => sum + s.value, 0);
@@ -246,9 +168,7 @@ export class DonutChart extends BaseChart {
246
168
  }
247
169
  return `<strong>${d.data.label}</strong><br/>${d.data.value} (${percentage}%)`;
248
170
  }
249
- renderSegments(segments, cx, cy, innerRadius, outerRadius) {
250
- if (!this.plotGroup || !this.svg)
251
- return;
171
+ renderSegments(plotGroup, segments, cx, cy, innerRadius, outerRadius) {
252
172
  const pieGenerator = pie()
253
173
  .value((d) => d.value)
254
174
  .padAngle(this.padAngle)
@@ -262,7 +182,7 @@ export class DonutChart extends BaseChart {
262
182
  .outerRadius(outerRadius + HOVER_EXPAND_PX)
263
183
  .cornerRadius(this.cornerRadius);
264
184
  const pieData = pieGenerator(segments);
265
- const segmentGroup = this.plotGroup
185
+ const segmentGroup = plotGroup
266
186
  .append('g')
267
187
  .attr('class', 'donut-segments')
268
188
  .attr('transform', `translate(${cx}, ${cy})`);
@@ -292,13 +212,13 @@ export class DonutChart extends BaseChart {
292
212
  tooltipDiv
293
213
  .style('visibility', 'visible')
294
214
  .html(this.buildTooltipContent(d, segments));
295
- this.positionTooltip(event, tooltipDiv);
215
+ this.positionTooltipFromPointer(event, tooltipDiv);
296
216
  }
297
217
  })
298
218
  .on('mousemove', (event) => {
299
219
  const tooltipDiv = resolveTooltipDiv();
300
220
  if (tooltipDiv && !tooltipDiv.empty()) {
301
- this.positionTooltip(event, tooltipDiv);
221
+ this.positionTooltipFromPointer(event, tooltipDiv);
302
222
  }
303
223
  })
304
224
  .on('mouseleave', (event, d) => {
package/gauge-chart.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { DataItem, LegendSeries } from './types.js';
2
- import { BaseChart, type BaseChartConfig } from './base-chart.js';
3
- import type { ChartComponent, LayoutAwareComponent } from './chart-interface.js';
2
+ import { BaseChart, type BaseChartConfig, type BaseRenderContext } from './base-chart.js';
3
+ import type { ChartComponent } from './chart-interface.js';
4
4
  export type GaugeSegment = {
5
5
  from: number;
6
6
  to: number;
@@ -35,10 +35,17 @@ export type GaugeLabelStyle = {
35
35
  fontWeight?: number | string;
36
36
  color?: string;
37
37
  };
38
+ export type GaugeAnimationEasingPreset = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce-out' | 'elastic-out';
39
+ export type GaugeAnimationConfig = {
40
+ show?: boolean;
41
+ duration?: number;
42
+ easing?: GaugeAnimationEasingPreset | `linear(${string})` | ((progress: number) => number);
43
+ };
38
44
  export type GaugeConfig = {
39
45
  value?: number;
40
46
  min?: number;
41
47
  max?: number;
48
+ animate?: boolean | GaugeAnimationConfig;
42
49
  halfCircle?: boolean;
43
50
  startAngle?: number;
44
51
  endAngle?: number;
@@ -71,6 +78,7 @@ export declare class GaugeChart extends BaseChart {
71
78
  private readonly targetValueKey;
72
79
  private readonly minValue;
73
80
  private readonly maxValue;
81
+ private readonly animation;
74
82
  private readonly halfCircle;
75
83
  private readonly startAngle;
76
84
  private readonly endAngle;
@@ -91,11 +99,15 @@ export declare class GaugeChart extends BaseChart {
91
99
  private segments;
92
100
  private value;
93
101
  private targetValue;
102
+ private lastRenderedValue;
94
103
  constructor(config: GaugeChartConfig);
95
104
  private normalizeNeedleConfig;
96
105
  private normalizeMarkerConfig;
97
106
  private getThemePaletteColor;
98
107
  private normalizeTickConfig;
108
+ private normalizeAnimationConfig;
109
+ private resolveAnimationEasing;
110
+ private parseCssLinearEasing;
99
111
  private normalizeTickLabelStyle;
100
112
  private normalizeValueLabelStyle;
101
113
  private validateGaugeConfig;
@@ -108,13 +120,13 @@ export declare class GaugeChart extends BaseChart {
108
120
  private clampToDomain;
109
121
  private prepareSegments;
110
122
  private defaultFormat;
111
- addChild(component: ChartComponent): this;
112
123
  protected getExportComponents(): ChartComponent[];
113
124
  update(data: DataItem[]): void;
114
- protected getLayoutComponents(): LayoutAwareComponent[];
115
- protected prepareLayout(): void;
116
125
  protected createExportChart(): BaseChart;
117
- protected renderChart(): void;
126
+ protected syncDerivedState(): void;
127
+ protected renderChart({ svg, plotGroup, plotArea, }: BaseRenderContext): void;
128
+ private resolveAnimationStartValue;
129
+ private shouldAnimateTransition;
118
130
  private buildAriaLabel;
119
131
  private findSegmentStatusLabel;
120
132
  private getVisibleSegments;
@@ -135,6 +147,5 @@ export declare class GaugeChart extends BaseChart {
135
147
  private resolveTooltipDiv;
136
148
  private buildTooltipContent;
137
149
  private positionTooltip;
138
- private renderLegend;
139
150
  protected getLegendSeries(): LegendSeries[];
140
151
  }