@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/README.md +1 -1
- package/dist/area.d.ts +2 -0
- package/dist/area.js +39 -31
- package/dist/bar.d.ts +17 -0
- package/dist/bar.js +295 -259
- package/dist/base-chart.d.ts +5 -0
- package/dist/base-chart.js +80 -66
- package/dist/chart-group.d.ts +16 -0
- package/dist/chart-group.js +201 -143
- package/dist/donut-center-content.d.ts +1 -0
- package/dist/donut-center-content.js +21 -38
- package/dist/donut-chart.js +29 -14
- package/dist/gauge-chart.d.ts +20 -0
- package/dist/gauge-chart.js +228 -132
- package/dist/legend.js +10 -9
- package/dist/line.js +3 -1
- package/dist/pie-chart.d.ts +3 -0
- package/dist/pie-chart.js +44 -18
- package/dist/scatter.js +3 -1
- package/dist/tooltip.js +18 -22
- package/dist/types.d.ts +3 -1
- package/dist/utils.js +11 -19
- package/dist/x-axis.d.ts +10 -0
- package/dist/x-axis.js +190 -149
- package/dist/xy-chart.d.ts +24 -0
- package/dist/xy-chart.js +251 -143
- package/dist/y-axis.d.ts +7 -2
- package/dist/y-axis.js +99 -10
- package/docs/components.md +14 -1
- package/docs/xy-chart.md +21 -6
- package/package.json +5 -4
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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:
|
|
75
|
-
end:
|
|
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.
|
|
270
|
-
const
|
|
271
|
-
const
|
|
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((
|
|
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.
|
|
389
|
-
const
|
|
390
|
-
const
|
|
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((
|
|
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
|
}
|
package/dist/base-chart.d.ts
CHANGED
|
@@ -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 {};
|