@internetstiftelsen/charts 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bar.js CHANGED
@@ -4,6 +4,7 @@ const LABEL_INSET_DEFAULT = 4;
4
4
  const LABEL_INSET_STACKED = 6;
5
5
  const LABEL_MIN_PADDING_DEFAULT = 8;
6
6
  const LABEL_MIN_PADDING_STACKED = 16;
7
+ const LABEL_OUTSIDE_OFFSET = 4;
7
8
  function getLabelSpacing(mode) {
8
9
  const stacked = mode !== 'none';
9
10
  return {
@@ -48,34 +49,50 @@ function getBarSlotLayout(bandwidth, mode, maxBarSize, totalSeries, seriesIndex,
48
49
  function getBarValueRange(categoryKey, value, stackingContext) {
49
50
  const mode = stackingContext?.mode ?? 'normal';
50
51
  if (mode === 'none' || mode === 'layer') {
51
- return {
52
- start: 0,
53
- end: value,
54
- };
52
+ return getUnstackedBarValueRange(value);
55
53
  }
56
54
  if (mode === 'percent') {
57
- if (value >= 0) {
58
- const cumulative = stackingContext?.positiveCumulativeData.get(categoryKey) ?? 0;
59
- const total = stackingContext?.positiveTotalData.get(categoryKey) ?? 0;
60
- if (total === 0) {
61
- return { start: 0, end: 0 };
62
- }
63
- return {
64
- start: (cumulative / total) * 100,
65
- end: ((cumulative + value) / total) * 100,
66
- };
67
- }
68
- const cumulativeMagnitude = stackingContext?.negativeCumulativeData.get(categoryKey) ?? 0;
69
- const totalMagnitude = stackingContext?.negativeTotalData.get(categoryKey) ?? 0;
70
- if (totalMagnitude === 0) {
71
- return { start: 0, end: 0 };
72
- }
55
+ return getPercentBarValueRange(categoryKey, value, stackingContext);
56
+ }
57
+ return getStackedBarValueRange(categoryKey, value, stackingContext);
58
+ }
59
+ function getUnstackedBarValueRange(value) {
60
+ return {
61
+ start: 0,
62
+ end: value,
63
+ };
64
+ }
65
+ function getPercentBarValueRange(categoryKey, value, stackingContext) {
66
+ const isPositive = value >= 0;
67
+ const { cumulative, total } = getPercentBarRangeMetrics(categoryKey, isPositive, stackingContext);
68
+ if (total === 0) {
73
69
  return {
74
- start: -((cumulativeMagnitude / totalMagnitude) * 100),
75
- end: -(((cumulativeMagnitude + Math.abs(value)) / totalMagnitude) *
76
- 100),
70
+ start: 0,
71
+ end: 0,
77
72
  };
78
73
  }
74
+ const start = (cumulative / total) * 100;
75
+ const end = ((cumulative + Math.abs(value)) / total) * 100;
76
+ return isPositive ? { start, end } : { start: -start, end: -end };
77
+ }
78
+ function getPercentBarRangeMetrics(categoryKey, isPositive, stackingContext) {
79
+ return isPositive
80
+ ? getPositivePercentBarRangeMetrics(categoryKey, stackingContext)
81
+ : getNegativePercentBarRangeMetrics(categoryKey, stackingContext);
82
+ }
83
+ function getPositivePercentBarRangeMetrics(categoryKey, stackingContext) {
84
+ return {
85
+ cumulative: stackingContext?.positiveCumulativeData.get(categoryKey) ?? 0,
86
+ total: stackingContext?.positiveTotalData.get(categoryKey) ?? 0,
87
+ };
88
+ }
89
+ function getNegativePercentBarRangeMetrics(categoryKey, stackingContext) {
90
+ return {
91
+ cumulative: stackingContext?.negativeCumulativeData.get(categoryKey) ?? 0,
92
+ total: stackingContext?.negativeTotalData.get(categoryKey) ?? 0,
93
+ };
94
+ }
95
+ function getStackedBarValueRange(categoryKey, value, stackingContext) {
79
96
  if (value >= 0) {
80
97
  const cumulative = stackingContext?.positiveCumulativeData.get(categoryKey) ?? 0;
81
98
  return {
@@ -262,256 +279,275 @@ export class Bar {
262
279
  .attr('height', barHeight)
263
280
  .attr('fill', (d, i) => this.colorAdapter ? this.colorAdapter(d, i) : this.fill);
264
281
  }
282
+ resolveValueLabelConfig(theme) {
283
+ const config = this.valueLabel;
284
+ return {
285
+ ...this.resolveValueLabelPlacement(config),
286
+ ...this.resolveValueLabelStyle(config, theme),
287
+ formatter: config.formatter,
288
+ autoContrastInside: config.color === undefined,
289
+ };
290
+ }
291
+ resolveValueLabelPlacement(config) {
292
+ return {
293
+ position: config.position ?? 'outside',
294
+ insidePosition: config.insidePosition ?? 'top',
295
+ };
296
+ }
297
+ resolveValueLabelStyle(config, theme) {
298
+ return {
299
+ fontSize: config.fontSize ?? theme.valueLabel.fontSize,
300
+ fontFamily: config.fontFamily ?? theme.valueLabel.fontFamily,
301
+ fontWeight: config.fontWeight ?? theme.valueLabel.fontWeight,
302
+ color: config.color ?? theme.valueLabel.color,
303
+ background: config.background ?? theme.valueLabel.background,
304
+ border: config.border ?? theme.valueLabel.border,
305
+ borderRadius: config.borderRadius ?? theme.valueLabel.borderRadius,
306
+ padding: config.padding ?? theme.valueLabel.padding,
307
+ };
308
+ }
309
+ getValueLabelText(rawValue, data, config) {
310
+ return config.formatter
311
+ ? config.formatter(this.dataKey, rawValue, data)
312
+ : String(rawValue);
313
+ }
314
+ getBarColor(data, index) {
315
+ return this.colorAdapter ? this.colorAdapter(data, index) : this.fill;
316
+ }
317
+ measureLabelBox(labelGroup, valueText, config) {
318
+ const tempText = labelGroup
319
+ .append('text')
320
+ .style('font-size', `${config.fontSize}px`)
321
+ .style('font-family', config.fontFamily)
322
+ .style('font-weight', config.fontWeight)
323
+ .text(valueText);
324
+ const textBox = tempText.node().getBBox();
325
+ tempText.remove();
326
+ return {
327
+ width: textBox.width + config.padding * 2,
328
+ height: textBox.height + config.padding * 2,
329
+ };
330
+ }
331
+ getLabelColor(config, barColor) {
332
+ if (config.position === 'inside' && config.autoContrastInside) {
333
+ return getContrastTextColor(barColor);
334
+ }
335
+ return config.color;
336
+ }
337
+ appendValueLabel(labelGroup, valueText, placement, labelBox, config, labelColor) {
338
+ const group = labelGroup.append('g');
339
+ if (config.position === 'outside') {
340
+ group
341
+ .append('rect')
342
+ .attr('x', placement.x - labelBox.width / 2)
343
+ .attr('y', placement.y - labelBox.height / 2)
344
+ .attr('width', labelBox.width)
345
+ .attr('height', labelBox.height)
346
+ .attr('rx', config.borderRadius)
347
+ .attr('ry', config.borderRadius)
348
+ .attr('fill', config.background)
349
+ .attr('stroke', config.border)
350
+ .attr('stroke-width', 1);
351
+ }
352
+ group
353
+ .append('text')
354
+ .attr('x', placement.x)
355
+ .attr('y', placement.y)
356
+ .attr('text-anchor', 'middle')
357
+ .attr('dominant-baseline', 'central')
358
+ .style('font-size', `${config.fontSize}px`)
359
+ .style('font-family', config.fontFamily)
360
+ .style('font-weight', config.fontWeight)
361
+ .style('fill', labelColor)
362
+ .style('pointer-events', 'none')
363
+ .text(valueText);
364
+ }
365
+ getVerticalLabelPlacement(input) {
366
+ if (input.position === 'outside') {
367
+ return this.getVerticalOutsideLabelPlacement(input);
368
+ }
369
+ if (input.mode === 'layer' && input.insidePosition === 'bottom') {
370
+ return {
371
+ x: input.x,
372
+ y: (input.barTop + input.barBottom) / 2,
373
+ shouldRender: false,
374
+ };
375
+ }
376
+ const { inset, minPadding } = getLabelSpacing(input.mode);
377
+ const y = this.getVerticalInsideLabelY(input.barTop, input.barBottom, input.labelBox.height, input.insidePosition, inset);
378
+ return {
379
+ x: input.x,
380
+ y,
381
+ shouldRender: input.labelBox.height + minPadding <= input.barHeight,
382
+ };
383
+ }
384
+ getVerticalOutsideLabelPlacement(input) {
385
+ const y = input.isNegative
386
+ ? input.barBottom + input.labelBox.height / 2 + LABEL_OUTSIDE_OFFSET
387
+ : input.barTop - input.labelBox.height / 2 - LABEL_OUTSIDE_OFFSET;
388
+ const shouldRender = input.isNegative
389
+ ? y + input.labelBox.height / 2 <= input.plotBottom
390
+ : y - input.labelBox.height / 2 >= input.plotTop;
391
+ return {
392
+ x: input.x,
393
+ y,
394
+ shouldRender,
395
+ };
396
+ }
397
+ getVerticalInsideLabelY(barTop, barBottom, labelHeight, insidePosition, inset) {
398
+ switch (insidePosition) {
399
+ case 'top':
400
+ return barTop + labelHeight / 2 + inset;
401
+ case 'middle':
402
+ return (barTop + barBottom) / 2;
403
+ case 'bottom':
404
+ return barBottom - labelHeight / 2 - inset;
405
+ }
406
+ }
407
+ getHorizontalLabelPlacement(input) {
408
+ if (input.position === 'outside') {
409
+ return this.getHorizontalOutsideLabelPlacement(input);
410
+ }
411
+ if (input.mode === 'layer' && input.insidePosition === 'bottom') {
412
+ return {
413
+ x: (input.barLeft + input.barRight) / 2,
414
+ y: input.y,
415
+ shouldRender: false,
416
+ };
417
+ }
418
+ const { inset, minPadding } = getLabelSpacing(input.mode);
419
+ const x = this.getHorizontalInsideLabelX(input.barLeft, input.barRight, input.labelBox.width, input.isNegative, input.insidePosition, inset);
420
+ const fitsBar = input.labelBox.width + minPadding <= input.barWidth;
421
+ const withinBounds = input.insidePosition === 'middle' ||
422
+ this.isHorizontalLabelWithinBounds(x, input.labelBox.width, input.barLeft, input.barRight);
423
+ return {
424
+ x,
425
+ y: input.y,
426
+ shouldRender: fitsBar && withinBounds,
427
+ };
428
+ }
429
+ getHorizontalOutsideLabelPlacement(input) {
430
+ const x = input.isNegative
431
+ ? input.barLeft - input.labelBox.width / 2 - LABEL_OUTSIDE_OFFSET
432
+ : input.barRight + input.labelBox.width / 2 + LABEL_OUTSIDE_OFFSET;
433
+ const shouldRender = input.isNegative
434
+ ? x - input.labelBox.width / 2 >= input.plotLeft
435
+ : x + input.labelBox.width / 2 <= input.plotRight;
436
+ return {
437
+ x,
438
+ y: input.y,
439
+ shouldRender,
440
+ };
441
+ }
442
+ getHorizontalInsideLabelX(barLeft, barRight, labelWidth, isNegative, insidePosition, inset) {
443
+ switch (insidePosition) {
444
+ case 'top':
445
+ return isNegative
446
+ ? barRight - labelWidth / 2 - inset
447
+ : barLeft + labelWidth / 2 + inset;
448
+ case 'middle':
449
+ return (barLeft + barRight) / 2;
450
+ case 'bottom':
451
+ return isNegative
452
+ ? barLeft + labelWidth / 2 + inset
453
+ : barRight - labelWidth / 2 - inset;
454
+ }
455
+ }
456
+ isHorizontalLabelWithinBounds(labelX, labelWidth, barLeft, barRight) {
457
+ const labelLeft = labelX - labelWidth / 2;
458
+ const labelRight = labelX + labelWidth / 2;
459
+ return labelLeft >= barLeft + 1 && labelRight <= barRight - 1;
460
+ }
461
+ renderVerticalValueLabel(labelGroup, dataItem, index, xKey, x, y, parseValue, xScaleType, stackingContext, config, barWidth, barOffset, mode, plotTop, plotBottom) {
462
+ const categoryKey = String(dataItem[xKey]);
463
+ const rawValue = parseValue(dataItem[this.dataKey]);
464
+ const renderedValue = this.getRenderedValue(rawValue, 'vertical');
465
+ const valueText = this.getValueLabelText(rawValue, dataItem, config);
466
+ const xPos = getScalePosition(x, dataItem[xKey], xScaleType);
467
+ const barColor = this.getBarColor(dataItem, index);
468
+ const { start, end } = getBarValueRange(categoryKey, renderedValue, stackingContext);
469
+ const bounds = getScaledValueRange(y, start, end);
470
+ const barTop = bounds.min;
471
+ const barBottom = bounds.max;
472
+ const barHeight = Math.abs(barBottom - barTop);
473
+ const barCenterX = xPos +
474
+ (xScaleType === 'band' ? barOffset : -barWidth / 2) +
475
+ barWidth / 2;
476
+ const labelBox = this.measureLabelBox(labelGroup, valueText, config);
477
+ const placement = this.getVerticalLabelPlacement({
478
+ x: barCenterX,
479
+ barTop,
480
+ barBottom,
481
+ barHeight,
482
+ labelBox,
483
+ isNegative: renderedValue < 0,
484
+ mode,
485
+ position: config.position,
486
+ insidePosition: config.insidePosition,
487
+ plotTop,
488
+ plotBottom,
489
+ });
490
+ if (!placement.shouldRender) {
491
+ return;
492
+ }
493
+ this.appendValueLabel(labelGroup, valueText, placement, labelBox, config, this.getLabelColor(config, barColor));
494
+ }
495
+ renderHorizontalValueLabel(labelGroup, dataItem, index, xKey, x, y, parseValue, yScaleType, stackingContext, config, barHeight, barOffset, mode, plotLeft, plotRight) {
496
+ const categoryKey = String(dataItem[xKey]);
497
+ const rawValue = parseValue(dataItem[this.dataKey]);
498
+ const renderedValue = this.getRenderedValue(rawValue, 'horizontal');
499
+ const valueText = this.getValueLabelText(rawValue, dataItem, config);
500
+ const yPos = getScalePosition(y, dataItem[xKey], yScaleType);
501
+ const barColor = this.getBarColor(dataItem, index);
502
+ const { start, end } = getBarValueRange(categoryKey, renderedValue, stackingContext);
503
+ const bounds = getScaledValueRange(x, start, end);
504
+ const barLeft = bounds.min;
505
+ const barRight = bounds.max;
506
+ const barWidth = Math.abs(barRight - barLeft);
507
+ const barCenterY = yPos +
508
+ (yScaleType === 'band' ? barOffset : -barHeight / 2) +
509
+ barHeight / 2;
510
+ const labelBox = this.measureLabelBox(labelGroup, valueText, config);
511
+ const placement = this.getHorizontalLabelPlacement({
512
+ y: barCenterY,
513
+ barLeft,
514
+ barRight,
515
+ barWidth,
516
+ labelBox,
517
+ isNegative: renderedValue < 0,
518
+ mode,
519
+ position: config.position,
520
+ insidePosition: config.insidePosition,
521
+ plotLeft,
522
+ plotRight,
523
+ });
524
+ if (!placement.shouldRender) {
525
+ return;
526
+ }
527
+ this.appendValueLabel(labelGroup, valueText, placement, labelBox, config, this.getLabelColor(config, barColor));
528
+ }
265
529
  renderVerticalValueLabels(plotGroup, data, xKey, x, y, parseValue, xScaleType, theme, stackingContext) {
266
530
  const bandwidth = x.bandwidth ? x.bandwidth() : 20;
267
531
  const mode = stackingContext?.mode ?? 'normal';
268
532
  const { thickness: barWidth, offset: barOffset } = getBarSlotLayout(bandwidth, mode, this.maxBarSize, stackingContext?.totalSeries ?? 1, stackingContext?.seriesIndex ?? 0, stackingContext?.gap ?? 0.1);
269
- const config = this.valueLabel;
270
- const position = config.position || 'outside';
271
- const insidePosition = config.insidePosition || 'top';
272
- const fontSize = config.fontSize ?? theme.valueLabel.fontSize;
273
- const fontFamily = config.fontFamily ?? theme.valueLabel.fontFamily;
274
- const fontWeight = config.fontWeight ?? theme.valueLabel.fontWeight;
275
- const defaultLabelColor = config.color ?? theme.valueLabel.color;
276
- const background = config.background ?? theme.valueLabel.background;
277
- const border = config.border ?? theme.valueLabel.border;
278
- const borderRadius = config.borderRadius ?? theme.valueLabel.borderRadius;
279
- const padding = config.padding ?? theme.valueLabel.padding;
533
+ const config = this.resolveValueLabelConfig(theme);
534
+ const plotTop = Math.min(...y.range());
535
+ const plotBottom = Math.max(...y.range());
280
536
  const labelGroup = plotGroup
281
537
  .append('g')
282
538
  .attr('class', `bar-value-labels-${sanitizeForCSS(this.dataKey)}`);
283
- data.forEach((d, i) => {
284
- const categoryKey = String(d[xKey]);
285
- const rawValue = parseValue(d[this.dataKey]);
286
- const renderedValue = this.getRenderedValue(rawValue, 'vertical');
287
- const valueText = String(rawValue);
288
- const xPos = getScalePosition(x, d[xKey], xScaleType);
289
- const barColor = this.colorAdapter
290
- ? this.colorAdapter(d, i)
291
- : this.fill;
292
- const { start, end } = getBarValueRange(categoryKey, renderedValue, stackingContext);
293
- const bounds = getScaledValueRange(y, start, end);
294
- const barTop = bounds.min;
295
- const barBottom = bounds.max;
296
- const isNegative = renderedValue < 0;
297
- const barHeight = Math.abs(barBottom - barTop);
298
- const barCenterX = xPos +
299
- (xScaleType === 'band' ? barOffset : -barWidth / 2) +
300
- barWidth / 2;
301
- // Create temporary text to measure dimensions
302
- const tempText = labelGroup
303
- .append('text')
304
- .style('font-size', `${fontSize}px`)
305
- .style('font-family', fontFamily)
306
- .style('font-weight', fontWeight)
307
- .text(valueText);
308
- const textBBox = tempText.node().getBBox();
309
- const boxWidth = textBBox.width + padding * 2;
310
- const boxHeight = textBBox.height + padding * 2;
311
- const labelX = barCenterX;
312
- let labelY = (barTop + barBottom) / 2; // Default to middle
313
- let shouldRender = true;
314
- if (position === 'outside') {
315
- const plotTop = y.range()[1];
316
- const plotBottom = y.range()[0];
317
- labelY = isNegative
318
- ? barBottom + boxHeight / 2 + 4
319
- : barTop - boxHeight / 2 - 4;
320
- if ((!isNegative && labelY - boxHeight / 2 < plotTop) ||
321
- (isNegative && labelY + boxHeight / 2 > plotBottom)) {
322
- shouldRender = false;
323
- }
324
- }
325
- else {
326
- if (mode === 'layer' && insidePosition === 'bottom') {
327
- // Bottom labels in layer mode are visually ambiguous and often hidden by overlap.
328
- shouldRender = false;
329
- }
330
- else {
331
- const { inset, minPadding } = getLabelSpacing(mode);
332
- switch (insidePosition) {
333
- case 'top':
334
- labelY = barTop + boxHeight / 2 + inset;
335
- break;
336
- case 'middle':
337
- labelY = (barTop + barBottom) / 2;
338
- break;
339
- case 'bottom':
340
- labelY = barBottom - boxHeight / 2 - inset;
341
- break;
342
- }
343
- if (boxHeight + minPadding > barHeight) {
344
- shouldRender = false;
345
- }
346
- }
347
- }
348
- tempText.remove();
349
- if (shouldRender) {
350
- const labelColor = position === 'inside' && config.color === undefined
351
- ? getContrastTextColor(barColor)
352
- : defaultLabelColor;
353
- const group = labelGroup.append('g');
354
- if (position === 'outside') {
355
- // Draw rounded rectangle background
356
- group
357
- .append('rect')
358
- .attr('x', labelX - boxWidth / 2)
359
- .attr('y', labelY - boxHeight / 2)
360
- .attr('width', boxWidth)
361
- .attr('height', boxHeight)
362
- .attr('rx', borderRadius)
363
- .attr('ry', borderRadius)
364
- .attr('fill', background)
365
- .attr('stroke', border)
366
- .attr('stroke-width', 1);
367
- }
368
- // Draw text
369
- group
370
- .append('text')
371
- .attr('x', labelX)
372
- .attr('y', labelY)
373
- .attr('text-anchor', 'middle')
374
- .attr('dominant-baseline', 'central')
375
- .style('font-size', `${fontSize}px`)
376
- .style('font-family', fontFamily)
377
- .style('font-weight', fontWeight)
378
- .style('fill', labelColor)
379
- .style('pointer-events', 'none')
380
- .text(valueText);
381
- }
382
- });
539
+ data.forEach((dataItem, index) => this.renderVerticalValueLabel(labelGroup, dataItem, index, xKey, x, y, parseValue, xScaleType, stackingContext, config, barWidth, barOffset, mode, plotTop, plotBottom));
383
540
  }
384
541
  renderHorizontalValueLabels(plotGroup, data, xKey, x, y, parseValue, yScaleType, theme, stackingContext) {
385
542
  const bandwidth = y.bandwidth ? y.bandwidth() : 20;
386
543
  const mode = stackingContext?.mode ?? 'normal';
387
544
  const { thickness: barHeight, offset: barOffset } = getBarSlotLayout(bandwidth, mode, this.maxBarSize, stackingContext?.totalSeries ?? 1, stackingContext?.seriesIndex ?? 0, stackingContext?.gap ?? 0.1);
388
- const config = this.valueLabel;
389
- const position = config.position || 'outside';
390
- const insidePosition = config.insidePosition || 'top';
391
- const fontSize = config.fontSize ?? theme.valueLabel.fontSize;
392
- const fontFamily = config.fontFamily ?? theme.valueLabel.fontFamily;
393
- const fontWeight = config.fontWeight ?? theme.valueLabel.fontWeight;
394
- const defaultLabelColor = config.color ?? theme.valueLabel.color;
395
- const background = config.background ?? theme.valueLabel.background;
396
- const border = config.border ?? theme.valueLabel.border;
397
- const borderRadius = config.borderRadius ?? theme.valueLabel.borderRadius;
398
- const padding = config.padding ?? theme.valueLabel.padding;
545
+ const config = this.resolveValueLabelConfig(theme);
546
+ const plotLeft = Math.min(...x.range());
547
+ const plotRight = Math.max(...x.range());
399
548
  const labelGroup = plotGroup
400
549
  .append('g')
401
550
  .attr('class', `bar-value-labels-${sanitizeForCSS(this.dataKey)}`);
402
- data.forEach((d, i) => {
403
- const categoryKey = String(d[xKey]);
404
- const rawValue = parseValue(d[this.dataKey]);
405
- const renderedValue = this.getRenderedValue(rawValue, 'horizontal');
406
- const valueText = String(rawValue);
407
- const yPos = getScalePosition(y, d[xKey], yScaleType);
408
- const barColor = this.colorAdapter
409
- ? this.colorAdapter(d, i)
410
- : this.fill;
411
- const { start, end } = getBarValueRange(categoryKey, renderedValue, stackingContext);
412
- const bounds = getScaledValueRange(x, start, end);
413
- const barLeft = bounds.min;
414
- const barRight = bounds.max;
415
- const isNegative = renderedValue < 0;
416
- const barWidth = Math.abs(barRight - barLeft);
417
- const barCenterY = yPos +
418
- (yScaleType === 'band' ? barOffset : -barHeight / 2) +
419
- barHeight / 2;
420
- // Create temporary text to measure dimensions
421
- const tempText = labelGroup
422
- .append('text')
423
- .style('font-size', `${fontSize}px`)
424
- .style('font-family', fontFamily)
425
- .style('font-weight', fontWeight)
426
- .text(valueText);
427
- const textBBox = tempText.node().getBBox();
428
- const boxWidth = textBBox.width + padding * 2;
429
- const boxHeight = textBBox.height + padding * 2;
430
- let labelX = (barLeft + barRight) / 2; // Default to middle
431
- const labelY = barCenterY;
432
- let shouldRender = true;
433
- if (position === 'outside') {
434
- const plotLeft = Math.min(...x.range());
435
- const plotRight = Math.max(...x.range());
436
- labelX = isNegative
437
- ? barLeft - boxWidth / 2 - 4
438
- : barRight + boxWidth / 2 + 4;
439
- if ((!isNegative && labelX + boxWidth / 2 > plotRight) ||
440
- (isNegative && labelX - boxWidth / 2 < plotLeft)) {
441
- shouldRender = false;
442
- }
443
- }
444
- else {
445
- if (mode === 'layer' && insidePosition === 'bottom') {
446
- // Bottom labels in layer mode are visually ambiguous and often hidden by overlap.
447
- shouldRender = false;
448
- }
449
- else {
450
- const { inset, minPadding } = getLabelSpacing(mode);
451
- switch (insidePosition) {
452
- case 'top':
453
- labelX = isNegative
454
- ? barRight - boxWidth / 2 - inset
455
- : barLeft + boxWidth / 2 + inset;
456
- break;
457
- case 'middle':
458
- labelX = (barLeft + barRight) / 2;
459
- break;
460
- case 'bottom':
461
- labelX = isNegative
462
- ? barLeft + boxWidth / 2 + inset
463
- : barRight - boxWidth / 2 - inset;
464
- break;
465
- }
466
- if (boxWidth + minPadding > barWidth) {
467
- shouldRender = false;
468
- }
469
- if (shouldRender &&
470
- position === 'inside' &&
471
- insidePosition !== 'middle') {
472
- const labelLeft = labelX - boxWidth / 2;
473
- const labelRight = labelX + boxWidth / 2;
474
- if (labelLeft < barLeft + 1 ||
475
- labelRight > barRight - 1) {
476
- shouldRender = false;
477
- }
478
- }
479
- }
480
- }
481
- tempText.remove();
482
- if (shouldRender) {
483
- const labelColor = position === 'inside' && config.color === undefined
484
- ? getContrastTextColor(barColor)
485
- : defaultLabelColor;
486
- const group = labelGroup.append('g');
487
- if (position === 'outside') {
488
- // Draw rounded rectangle background
489
- group
490
- .append('rect')
491
- .attr('x', labelX - boxWidth / 2)
492
- .attr('y', labelY - boxHeight / 2)
493
- .attr('width', boxWidth)
494
- .attr('height', boxHeight)
495
- .attr('rx', borderRadius)
496
- .attr('ry', borderRadius)
497
- .attr('fill', background)
498
- .attr('stroke', border)
499
- .attr('stroke-width', 1);
500
- }
501
- // Draw text
502
- group
503
- .append('text')
504
- .attr('x', labelX)
505
- .attr('y', labelY)
506
- .attr('text-anchor', 'middle')
507
- .attr('dominant-baseline', 'central')
508
- .style('font-size', `${fontSize}px`)
509
- .style('font-family', fontFamily)
510
- .style('font-weight', fontWeight)
511
- .style('fill', labelColor)
512
- .style('pointer-events', 'none')
513
- .text(valueText);
514
- }
515
- });
551
+ data.forEach((dataItem, index) => this.renderHorizontalValueLabel(labelGroup, dataItem, index, xKey, x, y, parseValue, yScaleType, stackingContext, config, barHeight, barOffset, mode, plotLeft, plotRight));
516
552
  }
517
553
  }
@@ -105,6 +105,7 @@ export declare abstract class BaseChart {
105
105
  */
106
106
  private performRender;
107
107
  protected resolveRenderDimensions(containerRect: DOMRect): RenderDimensions;
108
+ private pushIfIncluded;
108
109
  private resolveAccessibleLabel;
109
110
  private syncAccessibleLabelFromSvg;
110
111
  protected resolveResponsiveContext(context: {
@@ -208,6 +209,10 @@ export declare abstract class BaseChart {
208
209
  private exportImage;
209
210
  private exportPDF;
210
211
  protected exportSVG(options?: ExportOptions, formatForHooks?: VisualExportFormat): Promise<string>;
212
+ private requireRenderedSvg;
213
+ private resolveExportContext;
214
+ private createExportSvgClone;
215
+ private populateExportChart;
211
216
  protected exportJSON(): string;
212
217
  }
213
218
  export {};