@internetstiftelsen/charts 0.13.3 → 0.14.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/README.md CHANGED
@@ -10,8 +10,9 @@ A framework-agnostic, composable charting library built on D3.js with TypeScript
10
10
  - **Combined Chart Layouts** - `ChartGroup` composes existing charts into shared dashboards with one coordinated legend
11
11
  - **Divergent Bar Support** - Bar charts automatically render from zero and diverge around `0` for mixed positive/negative values
12
12
  - **Mirrored Bar Sides** - Horizontal bars can mirror a series to the left for population-pyramid style charts without changing source data
13
- - **Custom Value Labels** - XY, pie, and donut charts support optional on-chart labels with custom formatters
13
+ - **Custom Value Labels** - XY, pie, donut, and gauge charts support configurable labels with formatters, max-width overflow behavior, and forced rendering when labels would otherwise be hidden
14
14
  - **Optional XY Animation** - Animate XY series on first render and `chart.update(...)` with `animate`
15
+ - **Optional Radial Animation** - Animate pie and donut segments on first render and `chart.update(...)` with `animate`
15
16
  - **Optional Gauge Animation** - Animate gauge value transitions with `gauge.animate`
16
17
  - **Stacking Control** - Bar and area stacking modes with optional reversed visual series order
17
18
  - **Configurable Tooltips** - Shared or split tooltips with connectors, transitions, and default max-width wrapping
@@ -139,6 +140,8 @@ await chart.whenReady();
139
140
 
140
141
  Animation is off by default, applies to XY series marks only, and visual
141
142
  exports always render the final static state.
143
+ Preset easing values include `linear`, `ease-in`, `ease-out`, `ease-in-out`,
144
+ `bounce-out`, `elastic-out`, and `spring-out`.
142
145
 
143
146
  ## Lifecycle Events
144
147
 
package/dist/area.js CHANGED
@@ -388,6 +388,7 @@ export class Area {
388
388
  const border = config.border ?? theme.valueLabel.border;
389
389
  const borderRadius = config.borderRadius ?? theme.valueLabel.borderRadius;
390
390
  const padding = config.padding ?? theme.valueLabel.padding;
391
+ const forceVisible = config.forceVisible === true;
391
392
  const labelGroup = plotGroup
392
393
  .append('g')
393
394
  .attr('class', `area-value-labels-${sanitizeForCSS(this.dataKey)}`);
@@ -422,7 +423,7 @@ export class Area {
422
423
  if (labelY - boxHeight / 2 < plotTop + 4) {
423
424
  labelY = yPos + boxHeight / 2 + theme.line.point.size + 4;
424
425
  if (labelY + boxHeight / 2 > plotBottom - 4) {
425
- shouldRender = false;
426
+ shouldRender = forceVisible;
426
427
  }
427
428
  }
428
429
  tempText.remove();
package/dist/bar.js CHANGED
@@ -369,6 +369,7 @@ export class Bar {
369
369
  ...this.resolveValueLabelStyle(config, theme),
370
370
  formatter: config.formatter,
371
371
  autoContrastInside: config.color === undefined,
372
+ forceVisible: config.forceVisible ?? false,
372
373
  };
373
374
  }
374
375
  resolveValueLabelPlacement(config) {
@@ -461,7 +462,8 @@ export class Bar {
461
462
  return {
462
463
  x: input.x,
463
464
  y,
464
- shouldRender: input.labelBox.height + minPadding <= input.barHeight,
465
+ shouldRender: input.forceVisible ||
466
+ input.labelBox.height + minPadding <= input.barHeight,
465
467
  };
466
468
  }
467
469
  getVerticalOutsideLabelPlacement(input) {
@@ -474,7 +476,7 @@ export class Bar {
474
476
  return {
475
477
  x: input.x,
476
478
  y,
477
- shouldRender,
479
+ shouldRender: input.forceVisible || shouldRender,
478
480
  };
479
481
  }
480
482
  getVerticalInsideLabelY(barTop, barBottom, labelHeight, insidePosition, inset) {
@@ -506,7 +508,7 @@ export class Bar {
506
508
  return {
507
509
  x,
508
510
  y: input.y,
509
- shouldRender: fitsBar && withinBounds,
511
+ shouldRender: input.forceVisible || (fitsBar && withinBounds),
510
512
  };
511
513
  }
512
514
  getHorizontalOutsideLabelPlacement(input) {
@@ -519,7 +521,7 @@ export class Bar {
519
521
  return {
520
522
  x,
521
523
  y: input.y,
522
- shouldRender,
524
+ shouldRender: input.forceVisible || shouldRender,
523
525
  };
524
526
  }
525
527
  getHorizontalInsideLabelX(barLeft, barRight, labelWidth, isNegative, insidePosition, inset) {
@@ -567,6 +569,7 @@ export class Bar {
567
569
  mode,
568
570
  position: config.position,
569
571
  insidePosition: config.insidePosition,
572
+ forceVisible: config.forceVisible,
570
573
  plotTop,
571
574
  plotBottom,
572
575
  });
@@ -601,6 +604,7 @@ export class Bar {
601
604
  mode,
602
605
  position: config.position,
603
606
  insidePosition: config.insidePosition,
607
+ forceVisible: config.forceVisible,
604
608
  plotLeft,
605
609
  plotRight,
606
610
  });
@@ -199,6 +199,7 @@ export declare abstract class BaseChart {
199
199
  protected filterVisibleItems<T>(items: T[], getDataKey: (item: T) => string): T[];
200
200
  protected validateSourceData(_data: ChartData): void;
201
201
  protected syncDerivedState(_previousData?: DataItem[]): void;
202
+ protected prepareForLegendChange(): void;
202
203
  protected initializeDataState(): void;
203
204
  protected prepareLayout(context: BaseLayoutContext): void;
204
205
  /**
@@ -268,6 +268,7 @@ export class BaseChart {
268
268
  this.responsiveConfig = config.responsive;
269
269
  this.legendState = new LegendStateController();
270
270
  this.legendState.subscribe(() => {
271
+ this.prepareForLegendChange();
271
272
  this.notifyLegendChanged();
272
273
  this.rerender('legend');
273
274
  });
@@ -844,6 +845,7 @@ export class BaseChart {
844
845
  }
845
846
  validateSourceData(_data) { }
846
847
  syncDerivedState(_previousData) { }
848
+ prepareForLegendChange() { }
847
849
  initializeDataState() {
848
850
  this.validateSourceData(this.sourceData);
849
851
  this.syncDerivedState();
@@ -1,23 +1,34 @@
1
- import type { DataItem, LegendSeries } from './types.js';
1
+ import type { DataItem, LabelOversizedBehavior, LegendSeries } from './types.js';
2
2
  import { type BaseChartConfig, type BaseRenderContext } from './base-chart.js';
3
3
  import type { ChartComponentBase } from './chart-interface.js';
4
4
  import { RadialChartBase } from './radial-chart-base.js';
5
+ import { type RadialAnimationConfig, type RadialAnimationEasingPreset } from './radial-animation.js';
6
+ export type DonutAnimationConfig = RadialAnimationConfig;
7
+ export type DonutAnimationEasingPreset = RadialAnimationEasingPreset;
5
8
  export type DonutConfig = {
6
9
  innerRadius?: number;
7
10
  padAngle?: number;
8
11
  cornerRadius?: number;
9
12
  };
10
13
  export type DonutValueLabelPosition = 'outside' | 'auto';
14
+ export type DonutValueLabelFormatter = (label: string, value: number, data: DataItem, percentage: number) => string;
11
15
  export type DonutValueLabelConfig = {
12
16
  show?: boolean;
13
17
  position?: DonutValueLabelPosition;
14
18
  outsideOffset?: number;
15
19
  minVerticalSpacing?: number;
16
- formatter?: (label: string, value: number, data: DataItem, percentage: number) => string;
20
+ maxLabelWidth?: number;
21
+ oversizedBehavior?: LabelOversizedBehavior;
22
+ forceVisible?: boolean;
23
+ labelFormatter?: DonutValueLabelFormatter;
24
+ valueFormatter?: DonutValueLabelFormatter;
25
+ separator?: string;
26
+ formatter?: DonutValueLabelFormatter;
17
27
  };
18
28
  export type DonutChartConfig = BaseChartConfig & {
19
29
  donut?: DonutConfig;
20
30
  valueLabel?: DonutValueLabelConfig;
31
+ animate?: boolean | DonutAnimationConfig;
21
32
  valueKey?: string;
22
33
  labelKey?: string;
23
34
  };
@@ -28,6 +39,7 @@ export declare class DonutChart extends RadialChartBase {
28
39
  private readonly valueKey;
29
40
  private readonly labelKey;
30
41
  private readonly valueLabel;
42
+ private readonly motionController;
31
43
  private segments;
32
44
  private centerContent;
33
45
  constructor(config: DonutChartConfig);
@@ -36,16 +48,20 @@ export declare class DonutChart extends RadialChartBase {
36
48
  addChild(component: ChartComponentBase): this;
37
49
  protected getExportComponents(): ChartComponentBase[];
38
50
  update(data: DataItem[]): void;
51
+ protected prepareForLegendChange(): void;
39
52
  protected createExportChart(): RadialChartBase;
40
53
  protected applyComponentOverrides(overrides: Map<ChartComponentBase, ChartComponentBase>): () => void;
41
54
  protected syncDerivedState(): void;
42
55
  protected renderChart({ svg, plotGroup, plotArea, }: BaseRenderContext): void;
43
56
  protected getLegendSeries(): LegendSeries[];
44
57
  private buildTooltipContent;
45
- private formatValueLabelText;
58
+ private resolveValueLabelText;
59
+ private getValueLabelPercentage;
46
60
  private renderSegments;
47
61
  private renderLabels;
48
62
  private getArcPoint;
49
63
  private resolveOutsideLabel;
64
+ private measureValueLabelDimensions;
50
65
  private adjustOutsideLabelPositions;
66
+ private getOutsideLabelSpacing;
51
67
  }
@@ -2,8 +2,9 @@ import { arc, pie, select } from 'd3';
2
2
  import { sanitizeForCSS } from './utils.js';
3
3
  import { ChartValidator } from './validation.js';
4
4
  import { RadialChartBase } from './radial-chart-base.js';
5
+ import { buildRadialAnimatedArcData, buildRadialExitTargetPieData, createTransitionCompletionPromise, interpolateRadialArcDatum, interpolateRadialArcShape, normalizeRadialAnimationConfig, renderRadialArcDatum, RadialMotionController, } from './radial-animation.js';
5
6
  const HOVER_EXPAND_PX = 8;
6
- const ANIMATION_DURATION_MS = 150;
7
+ const HOVER_ANIMATION_DURATION_MS = 150;
7
8
  const OUTSIDE_LABEL_TEXT_OFFSET_PX = 10;
8
9
  const OUTSIDE_LABEL_LINE_INSET_PX = 4;
9
10
  const DEFAULT_DONUT_VALUE_LABEL = {
@@ -11,9 +12,9 @@ const DEFAULT_DONUT_VALUE_LABEL = {
11
12
  position: 'auto',
12
13
  outsideOffset: 16,
13
14
  minVerticalSpacing: 14,
14
- formatter: (label, value, _data, _percentage) => {
15
- return `${label}: ${value}`;
16
- },
15
+ oversizedBehavior: 'truncate',
16
+ forceVisible: false,
17
+ separator: ': ',
17
18
  };
18
19
  export class DonutChart extends RadialChartBase {
19
20
  constructor(config) {
@@ -54,6 +55,12 @@ export class DonutChart extends RadialChartBase {
54
55
  writable: true,
55
56
  value: void 0
56
57
  });
58
+ Object.defineProperty(this, "motionController", {
59
+ enumerable: true,
60
+ configurable: true,
61
+ writable: true,
62
+ value: void 0
63
+ });
57
64
  Object.defineProperty(this, "segments", {
58
65
  enumerable: true,
59
66
  configurable: true,
@@ -87,6 +94,7 @@ export class DonutChart extends RadialChartBase {
87
94
  this.valueKey = resolvedConfig.valueKey;
88
95
  this.labelKey = resolvedConfig.labelKey;
89
96
  this.valueLabel = valueLabel;
97
+ this.motionController = new RadialMotionController(normalizeRadialAnimationConfig(config.animate, 'DonutChart'));
90
98
  this.initializeDataState();
91
99
  }
92
100
  validateDonutData() {
@@ -142,8 +150,12 @@ export class DonutChart extends RadialChartBase {
142
150
  ];
143
151
  }
144
152
  update(data) {
153
+ this.motionController.prepareForUpdate();
145
154
  super.update(data);
146
155
  }
156
+ prepareForLegendChange() {
157
+ this.motionController.prepareForUpdate();
158
+ }
147
159
  createExportChart() {
148
160
  return new DonutChart({
149
161
  data: this.data,
@@ -157,6 +169,7 @@ export class DonutChart extends RadialChartBase {
157
169
  cornerRadius: this.cornerRadius,
158
170
  },
159
171
  valueLabel: this.valueLabel,
172
+ animate: false,
160
173
  valueKey: this.valueKey,
161
174
  labelKey: this.labelKey,
162
175
  });
@@ -184,9 +197,10 @@ export class DonutChart extends RadialChartBase {
184
197
  renderChart({ svg, plotGroup, plotArea, }) {
185
198
  this.renderTitle(svg);
186
199
  const visibleSegments = this.getVisibleRadialItems(this.segments);
200
+ const animationContext = this.motionController.getAnimationContext();
187
201
  const { cx, cy, outerRadius, innerRadius, fontScale } = this.getRadialLayout(plotArea, this.innerRadiusRatio);
188
202
  this.initializeTooltip();
189
- const { segmentGroup, pieData } = this.renderSegments(plotGroup, visibleSegments, cx, cy, innerRadius, outerRadius);
203
+ const { segmentGroup, pieData, transitions } = this.renderSegments(plotGroup, visibleSegments, cx, cy, innerRadius, outerRadius, animationContext, this.segments);
190
204
  if (this.valueLabel.show && visibleSegments.length > 0) {
191
205
  this.renderLabels(segmentGroup, pieData, outerRadius, visibleSegments.reduce((sum, segment) => {
192
206
  return sum + segment.value;
@@ -195,6 +209,7 @@ export class DonutChart extends RadialChartBase {
195
209
  if (this.centerContent) {
196
210
  this.centerContent.render(svg, cx, cy, this.renderTheme, fontScale);
197
211
  }
212
+ this.setReadyPromise(this.motionController.completeRender(pieData, transitions));
198
213
  this.renderInlineLegend(svg);
199
214
  }
200
215
  getLegendSeries() {
@@ -214,10 +229,32 @@ export class DonutChart extends RadialChartBase {
214
229
  }
215
230
  return `<strong>${d.data.label}</strong><br/>${d.data.value} (${percentage}%)`;
216
231
  }
217
- formatValueLabelText(segment, total) {
218
- return this.valueLabel.formatter(segment.label, segment.value, segment.source, total > 0 ? (segment.value / total) * 100 : 0);
232
+ resolveValueLabelText(segment, total) {
233
+ const percentage = this.getValueLabelPercentage(segment, total);
234
+ if (this.valueLabel.formatter) {
235
+ const fullText = this.valueLabel.formatter(segment.label, segment.value, segment.source, percentage);
236
+ return {
237
+ label: fullText,
238
+ value: '',
239
+ separator: '',
240
+ fullText,
241
+ isCustom: true,
242
+ };
243
+ }
244
+ const label = this.valueLabel.labelFormatter?.(segment.label, segment.value, segment.source, percentage) ?? segment.label;
245
+ const value = this.valueLabel.valueFormatter?.(segment.label, segment.value, segment.source, percentage) ?? String(segment.value);
246
+ return {
247
+ label,
248
+ value,
249
+ separator: this.valueLabel.separator,
250
+ fullText: `${label}${this.valueLabel.separator}${value}`,
251
+ isCustom: false,
252
+ };
219
253
  }
220
- renderSegments(plotGroup, segments, cx, cy, innerRadius, outerRadius) {
254
+ getValueLabelPercentage(segment, total) {
255
+ return total > 0 ? (segment.value / total) * 100 : 0;
256
+ }
257
+ renderSegments(plotGroup, segments, cx, cy, innerRadius, outerRadius, animationContext, allSegments) {
221
258
  const pieGenerator = pie()
222
259
  .value((d) => d.value)
223
260
  .padAngle(this.padAngle)
@@ -231,26 +268,64 @@ export class DonutChart extends RadialChartBase {
231
268
  .outerRadius(outerRadius + HOVER_EXPAND_PX)
232
269
  .cornerRadius(this.cornerRadius);
233
270
  const pieData = pieGenerator(segments);
271
+ const exitTargetPieData = buildRadialExitTargetPieData(pieGenerator, segments, allSegments, animationContext, pieData);
272
+ const arcShape = {
273
+ innerRadius,
274
+ outerRadius,
275
+ cornerRadius: this.cornerRadius,
276
+ };
234
277
  const segmentGroup = plotGroup
235
278
  .append('g')
236
279
  .attr('class', 'donut-segments')
237
280
  .attr('transform', `translate(${cx}, ${cy})`);
238
- segmentGroup
281
+ const animatedArcData = buildRadialAnimatedArcData(pieData, allSegments, animationContext, arcShape, exitTargetPieData);
282
+ const animationByDatum = new Map(animatedArcData.map((entry) => [entry.datum, entry]));
283
+ const getAnimation = (datum) => {
284
+ return animationByDatum.get(datum);
285
+ };
286
+ const renderedArcData = animatedArcData.map((entry) => entry.datum);
287
+ const transitions = [];
288
+ const segmentSelection = segmentGroup
239
289
  .selectAll('.donut-segment')
240
- .data(pieData)
290
+ .data(renderedArcData, (d) => getAnimation(d).key)
241
291
  .join('path')
242
292
  .attr('class', (d) => `donut-segment segment-${sanitizeForCSS(d.data.label)}`)
243
- .attr('d', arcGenerator)
293
+ .attr('d', (d) => {
294
+ const animation = getAnimation(d);
295
+ return renderRadialArcDatum(animation.startDatum, animation.startShape);
296
+ })
244
297
  .attr('fill', (d) => d.data.color)
245
- .style('cursor', 'pointer')
246
- .style('transition', 'opacity 0.15s ease')
298
+ .attr('opacity', (d) => getAnimation(d).initialOpacity)
299
+ .attr('aria-hidden', (d) => getAnimation(d).interactive ? null : 'true')
300
+ .style('cursor', (d) => getAnimation(d).interactive ? 'pointer' : 'default')
301
+ .style('pointer-events', (d) => getAnimation(d).interactive ? 'all' : 'none')
302
+ .style('transition', 'opacity 0.15s ease');
303
+ if (animationContext) {
304
+ const transition = segmentSelection
305
+ .transition()
306
+ .duration(animationContext.duration)
307
+ .ease(animationContext.easing)
308
+ .attrTween('d', (d) => {
309
+ const animation = getAnimation(d);
310
+ const datumInterpolator = interpolateRadialArcDatum(animation.startDatum, animation.endDatum);
311
+ const shapeInterpolator = interpolateRadialArcShape(animation.startShape, animation.endShape);
312
+ return (progress) => {
313
+ return renderRadialArcDatum(datumInterpolator(progress), shapeInterpolator(progress));
314
+ };
315
+ })
316
+ .attr('opacity', (d) => getAnimation(d).finalOpacity);
317
+ transitions.push(createTransitionCompletionPromise(transition));
318
+ }
319
+ const interactiveSegmentSelection = segmentSelection.filter((d) => {
320
+ return getAnimation(d).interactive;
321
+ });
322
+ interactiveSegmentSelection
247
323
  .on('mouseenter', (event, d) => {
248
324
  select(event.currentTarget)
249
325
  .transition()
250
- .duration(ANIMATION_DURATION_MS)
326
+ .duration(HOVER_ANIMATION_DURATION_MS)
251
327
  .attr('d', hoverArcGenerator(d));
252
- segmentGroup
253
- .selectAll('.donut-segment')
328
+ interactiveSegmentSelection
254
329
  .filter((_, i, nodes) => nodes[i] !== event.currentTarget)
255
330
  .style('opacity', 0.5);
256
331
  this.showTooltipFromPointer(event, this.buildTooltipContent(d, segments));
@@ -261,14 +336,15 @@ export class DonutChart extends RadialChartBase {
261
336
  .on('mouseleave', (event, d) => {
262
337
  select(event.currentTarget)
263
338
  .transition()
264
- .duration(ANIMATION_DURATION_MS)
339
+ .duration(HOVER_ANIMATION_DURATION_MS)
265
340
  .attr('d', arcGenerator(d));
266
- segmentGroup.selectAll('.donut-segment').style('opacity', 1);
341
+ interactiveSegmentSelection.style('opacity', 1);
267
342
  this.hideTooltip();
268
343
  });
269
344
  return {
270
345
  segmentGroup,
271
346
  pieData,
347
+ transitions,
272
348
  };
273
349
  }
274
350
  renderLabels(segmentGroup, pieData, outerRadius, total, fontScale) {
@@ -281,7 +357,19 @@ export class DonutChart extends RadialChartBase {
281
357
  const fontSize = this.renderTheme.valueLabel.fontSize * fontScale;
282
358
  const fontFamily = this.renderTheme.valueLabel.fontFamily;
283
359
  const fontWeight = this.renderTheme.valueLabel.fontWeight;
284
- const outsideLabels = pieData.map((datum) => this.resolveOutsideLabel(datum, outerRadius));
360
+ const labelOverflowOptions = {
361
+ maxLabelWidth: this.valueLabel.maxLabelWidth,
362
+ oversizedBehavior: this.valueLabel.oversizedBehavior,
363
+ forceVisible: this.valueLabel.forceVisible,
364
+ fontSize,
365
+ fontFamily,
366
+ fontWeight,
367
+ };
368
+ const outsideLabels = pieData.map((datum) => {
369
+ const valueLabel = this.resolveValueLabelText(datum.data, total);
370
+ const dimensions = this.measureValueLabelDimensions(valueLabel, labelOverflowOptions);
371
+ return this.resolveOutsideLabel(datum, outerRadius, valueLabel, dimensions.height);
372
+ });
285
373
  const adjustedOutsideLabels = this.adjustOutsideLabelPositions(outsideLabels, outerRadius);
286
374
  const linesGroup = labelGroup
287
375
  .append('g')
@@ -306,7 +394,7 @@ export class DonutChart extends RadialChartBase {
306
394
  .attr('fill', 'none')
307
395
  .attr('stroke', this.renderTheme.valueLabel.border)
308
396
  .attr('stroke-width', 1);
309
- labelGroup
397
+ const textElement = labelGroup
310
398
  .append('text')
311
399
  .attr('class', `donut-label donut-label--outside donut-label-${sanitizeForCSS(outsideLabel.datum.data.label)}`)
312
400
  .attr('x', textX)
@@ -316,8 +404,12 @@ export class DonutChart extends RadialChartBase {
316
404
  .attr('font-family', fontFamily)
317
405
  .attr('font-size', `${fontSize}px`)
318
406
  .attr('font-weight', fontWeight)
319
- .attr('fill', this.renderTheme.valueLabel.color)
320
- .text(this.formatValueLabelText(outsideLabel.datum.data, total));
407
+ .attr('fill', this.renderTheme.valueLabel.color);
408
+ if (outsideLabel.valueLabel.isCustom) {
409
+ this.renderRadialLabelText(textElement, outsideLabel.valueLabel.fullText, labelOverflowOptions);
410
+ return;
411
+ }
412
+ this.renderRadialStructuredLabelText(textElement, outsideLabel.valueLabel.label, outsideLabel.valueLabel.value, outsideLabel.valueLabel.separator, labelOverflowOptions);
321
413
  });
322
414
  }
323
415
  getArcPoint(angle, radius) {
@@ -326,7 +418,7 @@ export class DonutChart extends RadialChartBase {
326
418
  y: -Math.cos(angle) * radius,
327
419
  };
328
420
  }
329
- resolveOutsideLabel(datum, outerRadius) {
421
+ resolveOutsideLabel(datum, outerRadius, valueLabel, height) {
330
422
  const midAngle = (datum.startAngle + datum.endAngle) / 2;
331
423
  const point = this.getArcPoint(midAngle, outerRadius + this.valueLabel.outsideOffset);
332
424
  const side = point.x >= 0 ? 'right' : 'left';
@@ -335,8 +427,16 @@ export class DonutChart extends RadialChartBase {
335
427
  y: point.y,
336
428
  side,
337
429
  textAnchor: side === 'right' ? 'start' : 'end',
430
+ valueLabel,
431
+ height,
338
432
  };
339
433
  }
434
+ measureValueLabelDimensions(valueLabel, labelOverflowOptions) {
435
+ if (valueLabel.isCustom) {
436
+ return this.measureRadialLabelDimensions(valueLabel.fullText, labelOverflowOptions);
437
+ }
438
+ return this.measureRadialStructuredLabelDimensions(valueLabel.label, valueLabel.value, valueLabel.separator, labelOverflowOptions);
439
+ }
340
440
  adjustOutsideLabelPositions(labels, outerRadius) {
341
441
  const adjustForSide = (side) => {
342
442
  const sideLabels = labels
@@ -349,7 +449,8 @@ export class DonutChart extends RadialChartBase {
349
449
  const bottomLimit = outerRadius;
350
450
  sideLabels[0].y = Math.max(topLimit, sideLabels[0].y);
351
451
  for (let i = 1; i < sideLabels.length; i++) {
352
- const minY = sideLabels[i - 1].y + this.valueLabel.minVerticalSpacing;
452
+ const minY = sideLabels[i - 1].y +
453
+ this.getOutsideLabelSpacing(sideLabels[i - 1], sideLabels[i]);
353
454
  sideLabels[i].y = Math.max(sideLabels[i].y, minY);
354
455
  }
355
456
  const overflow = sideLabels[sideLabels.length - 1].y - bottomLimit;
@@ -357,7 +458,7 @@ export class DonutChart extends RadialChartBase {
357
458
  sideLabels[sideLabels.length - 1].y -= overflow;
358
459
  for (let i = sideLabels.length - 2; i >= 0; i--) {
359
460
  const maxY = sideLabels[i + 1].y -
360
- this.valueLabel.minVerticalSpacing;
461
+ this.getOutsideLabelSpacing(sideLabels[i], sideLabels[i + 1]);
361
462
  sideLabels[i].y = Math.min(sideLabels[i].y, maxY);
362
463
  }
363
464
  const underflow = topLimit - sideLabels[0].y;
@@ -371,4 +472,7 @@ export class DonutChart extends RadialChartBase {
371
472
  };
372
473
  return [...adjustForSide('left'), ...adjustForSide('right')];
373
474
  }
475
+ getOutsideLabelSpacing(previousLabel, currentLabel) {
476
+ return Math.max(this.valueLabel.minVerticalSpacing, (previousLabel.height + currentLabel.height) / 2);
477
+ }
374
478
  }
@@ -0,0 +1 @@
1
+ export declare function createCubicBezierEasing(x1: number, y1: number, x2: number, y2: number): (progress: number) => number;
package/dist/easing.js ADDED
@@ -0,0 +1,30 @@
1
+ const CUBIC_BEZIER_SAMPLE_STEPS = 24;
2
+ export function createCubicBezierEasing(x1, y1, x2, y2) {
3
+ return (progress) => {
4
+ if (progress <= 0) {
5
+ return 0;
6
+ }
7
+ if (progress >= 1) {
8
+ return 1;
9
+ }
10
+ let lower = 0;
11
+ let upper = 1;
12
+ let parameter = progress;
13
+ for (let step = 0; step < CUBIC_BEZIER_SAMPLE_STEPS; step += 1) {
14
+ parameter = (lower + upper) / 2;
15
+ if (sampleCubicBezier(parameter, x1, x2) < progress) {
16
+ lower = parameter;
17
+ }
18
+ else {
19
+ upper = parameter;
20
+ }
21
+ }
22
+ return sampleCubicBezier(parameter, y1, y2);
23
+ };
24
+ }
25
+ function sampleCubicBezier(parameter, controlPoint1, controlPoint2) {
26
+ const inverseParameter = 1 - parameter;
27
+ return (3 * inverseParameter * inverseParameter * parameter * controlPoint1 +
28
+ 3 * inverseParameter * parameter * parameter * controlPoint2 +
29
+ parameter * parameter * parameter);
30
+ }
@@ -1,4 +1,4 @@
1
- import type { DataItem, LegendSeries } from './types.js';
1
+ import type { DataItem, LabelOversizedBehavior, LegendSeries } from './types.js';
2
2
  import type { BaseChart, BaseChartConfig, BaseRenderContext } from './base-chart.js';
3
3
  import type { ChartComponentBase } from './chart-interface.js';
4
4
  import { RadialChartBase } from './radial-chart-base.js';
@@ -35,8 +35,11 @@ export type GaugeLabelStyle = {
35
35
  fontFamily?: string;
36
36
  fontWeight?: number | string;
37
37
  color?: string;
38
+ maxLabelWidth?: number;
39
+ oversizedBehavior?: LabelOversizedBehavior;
40
+ forceVisible?: boolean;
38
41
  };
39
- export type GaugeAnimationEasingPreset = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce-out' | 'elastic-out';
42
+ export type GaugeAnimationEasingPreset = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bounce-out' | 'elastic-out' | 'spring-out';
40
43
  export type GaugeAnimationConfig = {
41
44
  show?: boolean;
42
45
  duration?: number;
@@ -116,6 +119,7 @@ export declare class GaugeChart extends RadialChartBase {
116
119
  private interpolateLinearEasing;
117
120
  private normalizeTickLabelStyle;
118
121
  private normalizeValueLabelStyle;
122
+ private normalizeGaugeLabelStyle;
119
123
  private validateGaugeConfig;
120
124
  private validateGaugeRange;
121
125
  private validateGaugeGeometry;
@@ -164,6 +168,7 @@ export declare class GaugeChart extends RadialChartBase {
164
168
  private renderNeedle;
165
169
  private renderCurrentValueMarker;
166
170
  private renderValueText;
171
+ private renderGaugeLabelText;
167
172
  private attachTooltipLayer;
168
173
  private buildTooltipContent;
169
174
  protected getLegendSeries(): LegendSeries[];