@internetstiftelsen/charts 0.13.3 → 0.14.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.
@@ -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[];
@@ -1,4 +1,5 @@
1
1
  import { arc, easeBounceOut, easeCubicIn, easeCubicInOut, easeCubicOut, easeElasticOut, easeLinear, interpolateNumber, } from 'd3';
2
+ import { createCubicBezierEasing } from './easing.js';
2
3
  import { DEFAULT_COLOR_PALETTE } from './theme.js';
3
4
  import { ChartValidator } from './validation.js';
4
5
  import { RadialChartBase } from './radial-chart-base.js';
@@ -37,6 +38,7 @@ const DEFAULT_TICK_LABEL_OFFSET = 12;
37
38
  const DEFAULT_TICK_LABEL_FONT_SIZE = 11;
38
39
  const DEFAULT_TICK_LABEL_FONT_WEIGHT = 'normal';
39
40
  const DEFAULT_TICK_LABEL_COLOR = '#4b5563';
41
+ const DEFAULT_LABEL_OVERSIZED_BEHAVIOR = 'truncate';
40
42
  const DEFAULT_VALUE_LABEL_FONT_SIZE = 28;
41
43
  const DEFAULT_VALUE_LABEL_FONT_WEIGHT = '700';
42
44
  const DEFAULT_VALUE_LABEL_COLOR = '#111827';
@@ -49,6 +51,7 @@ const DEFAULT_PROGRESS_RADIUS_INSET = 2;
49
51
  const MIN_PROGRESS_BAND_THICKNESS = 1;
50
52
  const DEFAULT_HALF_CIRCLE_VALUE_TEXT_Y = 32;
51
53
  const DEFAULT_FULL_CIRCLE_VALUE_TEXT_MIN_Y = 22;
54
+ const easeSpringOut = createCubicBezierEasing(0.85, 0, 0.15, 1);
52
55
  const GAUGE_ANIMATION_EASING_PRESETS = {
53
56
  linear: easeLinear,
54
57
  'ease-in': easeCubicIn,
@@ -56,6 +59,7 @@ const GAUGE_ANIMATION_EASING_PRESETS = {
56
59
  'ease-in-out': easeCubicInOut,
57
60
  'bounce-out': easeBounceOut,
58
61
  'elastic-out': easeElasticOut,
62
+ 'spring-out': easeSpringOut,
59
63
  };
60
64
  const DUMMY_ARC_DATUM = {
61
65
  innerRadius: 0,
@@ -537,21 +541,31 @@ export class GaugeChart extends RadialChartBase {
537
541
  return lastPoint.value;
538
542
  }
539
543
  normalizeTickLabelStyle(config) {
540
- return {
541
- fontSize: config?.fontSize ?? DEFAULT_TICK_LABEL_FONT_SIZE,
542
- fontFamily: config?.fontFamily ?? this.theme.axis.fontFamily,
543
- fontWeight: config?.fontWeight ??
544
- this.theme.axis.fontWeight ??
545
- DEFAULT_TICK_LABEL_FONT_WEIGHT,
546
- color: config?.color ?? DEFAULT_TICK_LABEL_COLOR,
547
- };
544
+ return this.normalizeGaugeLabelStyle(config, {
545
+ fontSize: DEFAULT_TICK_LABEL_FONT_SIZE,
546
+ fontFamily: this.theme.axis.fontFamily,
547
+ fontWeight: this.theme.axis.fontWeight ?? DEFAULT_TICK_LABEL_FONT_WEIGHT,
548
+ color: DEFAULT_TICK_LABEL_COLOR,
549
+ });
548
550
  }
549
551
  normalizeValueLabelStyle(config) {
552
+ return this.normalizeGaugeLabelStyle(config, {
553
+ fontSize: DEFAULT_VALUE_LABEL_FONT_SIZE,
554
+ fontFamily: this.theme.axis.fontFamily,
555
+ fontWeight: DEFAULT_VALUE_LABEL_FONT_WEIGHT,
556
+ color: DEFAULT_VALUE_LABEL_COLOR,
557
+ });
558
+ }
559
+ normalizeGaugeLabelStyle(config, defaults) {
560
+ const resolvedConfig = config ?? {};
550
561
  return {
551
- fontSize: config?.fontSize ?? DEFAULT_VALUE_LABEL_FONT_SIZE,
552
- fontFamily: config?.fontFamily ?? this.theme.axis.fontFamily,
553
- fontWeight: config?.fontWeight ?? DEFAULT_VALUE_LABEL_FONT_WEIGHT,
554
- color: config?.color ?? DEFAULT_VALUE_LABEL_COLOR,
562
+ fontSize: resolveDefault(resolvedConfig.fontSize, defaults.fontSize),
563
+ fontFamily: resolveDefault(resolvedConfig.fontFamily, defaults.fontFamily),
564
+ fontWeight: resolveDefault(resolvedConfig.fontWeight, defaults.fontWeight),
565
+ color: resolveDefault(resolvedConfig.color, defaults.color),
566
+ maxLabelWidth: resolvedConfig.maxLabelWidth,
567
+ oversizedBehavior: resolveDefault(resolvedConfig.oversizedBehavior, DEFAULT_LABEL_OVERSIZED_BEHAVIOR),
568
+ forceVisible: resolvedConfig.forceVisible ?? false,
555
569
  };
556
570
  }
557
571
  validateGaugeConfig() {
@@ -1107,7 +1121,8 @@ export class GaugeChart extends RadialChartBase {
1107
1121
  : labelPoint.x > 0
1108
1122
  ? 'start'
1109
1123
  : 'end';
1110
- tickGroup
1124
+ const tickLabelText = this.ticks.formatter(value);
1125
+ const textElement = tickGroup
1111
1126
  .append('text')
1112
1127
  .attr('class', 'gauge-tick-label')
1113
1128
  .attr('x', labelPoint.x)
@@ -1117,8 +1132,8 @@ export class GaugeChart extends RadialChartBase {
1117
1132
  .attr('font-size', this.tickLabelStyle.fontSize)
1118
1133
  .attr('font-family', this.tickLabelStyle.fontFamily)
1119
1134
  .attr('font-weight', this.tickLabelStyle.fontWeight)
1120
- .attr('fill', this.tickLabelStyle.color)
1121
- .text(this.ticks.formatter(value));
1135
+ .attr('fill', this.tickLabelStyle.color);
1136
+ this.renderGaugeLabelText(textElement, tickLabelText, this.tickLabelStyle);
1122
1137
  }
1123
1138
  }
1124
1139
  renderTargetMarker(gaugeGroup, innerRadius, outerRadius) {
@@ -1243,8 +1258,8 @@ export class GaugeChart extends RadialChartBase {
1243
1258
  .attr('font-size', this.valueLabelStyle.fontSize)
1244
1259
  .attr('font-weight', this.valueLabelStyle.fontWeight)
1245
1260
  .attr('font-family', this.valueLabelStyle.fontFamily)
1246
- .attr('fill', this.valueLabelStyle.color)
1247
- .text(this.valueFormatter(initialValue));
1261
+ .attr('fill', this.valueLabelStyle.color);
1262
+ this.renderGaugeLabelText(valueText, this.valueFormatter(initialValue), this.valueLabelStyle, 'baseline');
1248
1263
  if (shouldAnimate) {
1249
1264
  valueText
1250
1265
  .transition()
@@ -1253,11 +1268,21 @@ export class GaugeChart extends RadialChartBase {
1253
1268
  .tween('text', () => {
1254
1269
  return (progress) => {
1255
1270
  const currentValue = startValue + (this.value - startValue) * progress;
1256
- valueText.text(this.valueFormatter(currentValue));
1271
+ this.renderGaugeLabelText(valueText, this.valueFormatter(currentValue), this.valueLabelStyle, 'baseline');
1257
1272
  };
1258
1273
  });
1259
1274
  }
1260
1275
  }
1276
+ renderGaugeLabelText(textElement, text, labelStyle, verticalAnchor = 'middle') {
1277
+ this.renderRadialLabelText(textElement, text, {
1278
+ maxLabelWidth: labelStyle.maxLabelWidth,
1279
+ oversizedBehavior: labelStyle.oversizedBehavior,
1280
+ forceVisible: labelStyle.forceVisible,
1281
+ fontSize: labelStyle.fontSize,
1282
+ fontFamily: labelStyle.fontFamily,
1283
+ fontWeight: labelStyle.fontWeight,
1284
+ }, verticalAnchor);
1285
+ }
1261
1286
  attachTooltipLayer(gaugeGroup, innerRadius, outerRadius, progressColor) {
1262
1287
  const interactionArc = arc()
1263
1288
  .innerRadius(Math.max(0, innerRadius - 20))
package/dist/line.js CHANGED
@@ -267,6 +267,7 @@ export class Line {
267
267
  const border = config.border ?? theme.valueLabel.border;
268
268
  const borderRadius = config.borderRadius ?? theme.valueLabel.borderRadius;
269
269
  const padding = config.padding ?? theme.valueLabel.padding;
270
+ const forceVisible = config.forceVisible === true;
270
271
  const labelGroup = plotGroup
271
272
  .append('g')
272
273
  .attr('class', `line-value-labels-${sanitizeForCSS(this.dataKey)}`);
@@ -299,7 +300,7 @@ export class Line {
299
300
  labelY = yPos + boxHeight / 2 + theme.line.point.size + 4;
300
301
  // Check if it fits below
301
302
  if (labelY + boxHeight / 2 > plotBottom - 4) {
302
- shouldRender = false;
303
+ shouldRender = forceVisible;
303
304
  }
304
305
  }
305
306
  tempText.remove();
@@ -1,7 +1,10 @@
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 PieAnimationConfig = RadialAnimationConfig;
7
+ export type PieAnimationEasingPreset = RadialAnimationEasingPreset;
5
8
  export type PieSort = 'none' | 'ascending' | 'descending' | ((a: PieSegmentData, b: PieSegmentData) => number);
6
9
  export type PieConfig = {
7
10
  innerRadius?: number;
@@ -12,6 +15,7 @@ export type PieConfig = {
12
15
  sort?: PieSort;
13
16
  };
14
17
  export type PieValueLabelPosition = 'inside' | 'outside' | 'auto';
18
+ export type PieValueLabelFormatter = (label: string, value: number, data: DataItem, percentage: number) => string;
15
19
  export type PieValueLabelConfig = {
16
20
  show?: boolean;
17
21
  position?: PieValueLabelPosition;
@@ -19,11 +23,18 @@ export type PieValueLabelConfig = {
19
23
  outsideOffset?: number;
20
24
  insideMargin?: number;
21
25
  minVerticalSpacing?: number;
22
- formatter?: (label: string, value: number, data: DataItem, percentage: number) => string;
26
+ maxLabelWidth?: number;
27
+ oversizedBehavior?: LabelOversizedBehavior;
28
+ forceVisible?: boolean;
29
+ labelFormatter?: PieValueLabelFormatter;
30
+ valueFormatter?: PieValueLabelFormatter;
31
+ separator?: string;
32
+ formatter?: PieValueLabelFormatter;
23
33
  };
24
34
  export type PieChartConfig = BaseChartConfig & {
25
35
  pie?: PieConfig;
26
36
  valueLabel?: PieValueLabelConfig;
37
+ animate?: boolean | PieAnimationConfig;
27
38
  valueKey?: string;
28
39
  labelKey?: string;
29
40
  };
@@ -43,6 +54,7 @@ export declare class PieChart extends RadialChartBase {
43
54
  private readonly valueKey;
44
55
  private readonly labelKey;
45
56
  private readonly valueLabel;
57
+ private readonly motionController;
46
58
  private segments;
47
59
  constructor(config: PieChartConfig);
48
60
  private validatePieData;
@@ -53,7 +65,9 @@ export declare class PieChart extends RadialChartBase {
53
65
  private warnOnTinySlices;
54
66
  protected getExportComponents(): ChartComponentBase[];
55
67
  update(data: DataItem[]): void;
56
- private formatValueLabelText;
68
+ protected prepareForLegendChange(): void;
69
+ private resolveValueLabelText;
70
+ private getValueLabelPercentage;
57
71
  protected createExportChart(): RadialChartBase;
58
72
  protected syncDerivedState(): void;
59
73
  protected renderChart({ svg, plotGroup, plotArea, }: BaseRenderContext): void;
@@ -69,6 +83,8 @@ export declare class PieChart extends RadialChartBase {
69
83
  private resolveValueLabelPlacement;
70
84
  private canFitInsideLabel;
71
85
  private resolveOutsideLabel;
86
+ private measureValueLabelDimensions;
72
87
  private adjustOutsideLabelPositions;
88
+ private getOutsideLabelSpacing;
73
89
  }
74
90
  export {};