@internetstiftelsen/charts 0.10.0 → 0.11.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 +65 -1
- package/dist/area.d.ts +11 -1
- package/dist/area.js +199 -55
- package/dist/bar.d.ts +26 -1
- package/dist/bar.js +425 -306
- package/dist/base-chart.d.ts +5 -0
- package/dist/base-chart.js +91 -67
- 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 +32 -32
- package/dist/gauge-chart.d.ts +23 -4
- package/dist/gauge-chart.js +235 -185
- package/dist/lazy-mount.d.ts +13 -0
- package/dist/lazy-mount.js +90 -0
- package/dist/legend.js +10 -9
- package/dist/line.d.ts +9 -1
- package/dist/line.js +144 -24
- package/dist/pie-chart.d.ts +3 -0
- package/dist/pie-chart.js +49 -47
- package/dist/radial-chart-base.d.ts +4 -3
- package/dist/radial-chart-base.js +27 -12
- package/dist/scatter.d.ts +5 -1
- package/dist/scatter.js +92 -9
- package/dist/theme.js +17 -0
- package/dist/tooltip.d.ts +55 -3
- package/dist/tooltip.js +968 -159
- package/dist/types.d.ts +23 -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-animation.d.ts +3 -0
- package/dist/xy-animation.js +2 -0
- package/dist/xy-chart.d.ts +35 -1
- package/dist/xy-chart.js +358 -153
- package/dist/xy-motion/config.d.ts +2 -0
- package/dist/xy-motion/config.js +177 -0
- package/dist/xy-motion/driver.d.ts +9 -0
- package/dist/xy-motion/driver.js +10 -0
- package/dist/xy-motion/helpers.d.ts +17 -0
- package/dist/xy-motion/helpers.js +105 -0
- package/dist/xy-motion/live-state.d.ts +8 -0
- package/dist/xy-motion/live-state.js +240 -0
- package/dist/xy-motion/noop-xy-motion-driver.d.ts +9 -0
- package/dist/xy-motion/noop-xy-motion-driver.js +15 -0
- package/dist/xy-motion/types.d.ts +85 -0
- package/dist/xy-motion/types.js +1 -0
- package/dist/xy-motion/xy-motion-driver.d.ts +19 -0
- package/dist/xy-motion/xy-motion-driver.js +130 -0
- package/dist/y-axis.d.ts +7 -2
- package/dist/y-axis.js +99 -10
- package/docs/components.md +50 -1
- package/docs/getting-started.md +35 -0
- package/docs/theming.md +14 -0
- package/docs/xy-chart.md +88 -7
- package/package.json +5 -4
package/dist/bar.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { getContrastTextColor, sanitizeForCSS, mergeDeep } from './utils.js';
|
|
2
2
|
import { getScalePosition } from './scale-utils.js';
|
|
3
|
+
import { buildXYDatumSnapshotKeys, createTransitionCompletionPromise, } from './xy-motion/helpers.js';
|
|
3
4
|
const LABEL_INSET_DEFAULT = 4;
|
|
4
5
|
const LABEL_INSET_STACKED = 6;
|
|
5
6
|
const LABEL_MIN_PADDING_DEFAULT = 8;
|
|
6
7
|
const LABEL_MIN_PADDING_STACKED = 16;
|
|
8
|
+
const LABEL_OUTSIDE_OFFSET = 4;
|
|
7
9
|
function getLabelSpacing(mode) {
|
|
8
10
|
const stacked = mode !== 'none';
|
|
9
11
|
return {
|
|
@@ -48,34 +50,50 @@ function getBarSlotLayout(bandwidth, mode, maxBarSize, totalSeries, seriesIndex,
|
|
|
48
50
|
function getBarValueRange(categoryKey, value, stackingContext) {
|
|
49
51
|
const mode = stackingContext?.mode ?? 'normal';
|
|
50
52
|
if (mode === 'none' || mode === 'layer') {
|
|
51
|
-
return
|
|
52
|
-
start: 0,
|
|
53
|
-
end: value,
|
|
54
|
-
};
|
|
53
|
+
return getUnstackedBarValueRange(value);
|
|
55
54
|
}
|
|
56
55
|
if (mode === 'percent') {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return { start: 0, end: 0 };
|
|
72
|
-
}
|
|
56
|
+
return getPercentBarValueRange(categoryKey, value, stackingContext);
|
|
57
|
+
}
|
|
58
|
+
return getStackedBarValueRange(categoryKey, value, stackingContext);
|
|
59
|
+
}
|
|
60
|
+
function getUnstackedBarValueRange(value) {
|
|
61
|
+
return {
|
|
62
|
+
start: 0,
|
|
63
|
+
end: value,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function getPercentBarValueRange(categoryKey, value, stackingContext) {
|
|
67
|
+
const isPositive = value >= 0;
|
|
68
|
+
const { cumulative, total } = getPercentBarRangeMetrics(categoryKey, isPositive, stackingContext);
|
|
69
|
+
if (total === 0) {
|
|
73
70
|
return {
|
|
74
|
-
start:
|
|
75
|
-
end:
|
|
76
|
-
100),
|
|
71
|
+
start: 0,
|
|
72
|
+
end: 0,
|
|
77
73
|
};
|
|
78
74
|
}
|
|
75
|
+
const start = (cumulative / total) * 100;
|
|
76
|
+
const end = ((cumulative + Math.abs(value)) / total) * 100;
|
|
77
|
+
return isPositive ? { start, end } : { start: -start, end: -end };
|
|
78
|
+
}
|
|
79
|
+
function getPercentBarRangeMetrics(categoryKey, isPositive, stackingContext) {
|
|
80
|
+
return isPositive
|
|
81
|
+
? getPositivePercentBarRangeMetrics(categoryKey, stackingContext)
|
|
82
|
+
: getNegativePercentBarRangeMetrics(categoryKey, stackingContext);
|
|
83
|
+
}
|
|
84
|
+
function getPositivePercentBarRangeMetrics(categoryKey, stackingContext) {
|
|
85
|
+
return {
|
|
86
|
+
cumulative: stackingContext?.positiveCumulativeData.get(categoryKey) ?? 0,
|
|
87
|
+
total: stackingContext?.positiveTotalData.get(categoryKey) ?? 0,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function getNegativePercentBarRangeMetrics(categoryKey, stackingContext) {
|
|
91
|
+
return {
|
|
92
|
+
cumulative: stackingContext?.negativeCumulativeData.get(categoryKey) ?? 0,
|
|
93
|
+
total: stackingContext?.negativeTotalData.get(categoryKey) ?? 0,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function getStackedBarValueRange(categoryKey, value, stackingContext) {
|
|
79
97
|
if (value >= 0) {
|
|
80
98
|
const cumulative = stackingContext?.positiveCumulativeData.get(categoryKey) ?? 0;
|
|
81
99
|
return {
|
|
@@ -181,13 +199,10 @@ export class Bar {
|
|
|
181
199
|
}
|
|
182
200
|
return value;
|
|
183
201
|
}
|
|
184
|
-
render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme, stackingContext, orientation = 'vertical') {
|
|
185
|
-
|
|
186
|
-
this.renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType, stackingContext)
|
|
187
|
-
|
|
188
|
-
else {
|
|
189
|
-
this.renderHorizontal(plotGroup, data, xKey, x, y, parseValue, xScaleType, stackingContext);
|
|
190
|
-
}
|
|
202
|
+
render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme, stackingContext, orientation = 'vertical', animation) {
|
|
203
|
+
const result = orientation === 'vertical'
|
|
204
|
+
? this.renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType, stackingContext, animation)
|
|
205
|
+
: this.renderHorizontal(plotGroup, data, xKey, x, y, parseValue, xScaleType, stackingContext, animation);
|
|
191
206
|
// Render value labels if enabled
|
|
192
207
|
if (this.valueLabel?.show && theme) {
|
|
193
208
|
if (orientation === 'vertical') {
|
|
@@ -197,8 +212,9 @@ export class Bar {
|
|
|
197
212
|
this.renderHorizontalValueLabels(plotGroup, data, xKey, x, y, parseValue, xScaleType, theme, stackingContext);
|
|
198
213
|
}
|
|
199
214
|
}
|
|
215
|
+
return result;
|
|
200
216
|
}
|
|
201
|
-
renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType, stackingContext) {
|
|
217
|
+
renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType, stackingContext, animation) {
|
|
202
218
|
const bandwidth = x.bandwidth ? x.bandwidth() : 20;
|
|
203
219
|
const mode = stackingContext?.mode ?? 'normal';
|
|
204
220
|
const { thickness: barWidth, offset: barOffset } = getBarSlotLayout(bandwidth, mode, this.maxBarSize, stackingContext?.totalSeries ?? 1, stackingContext?.seriesIndex ?? 0, stackingContext?.gap ?? 0.1);
|
|
@@ -208,29 +224,15 @@ export class Bar {
|
|
|
208
224
|
const { start, end } = getBarValueRange(categoryKey, value, stackingContext);
|
|
209
225
|
return getScaledValueRange(y, start, end);
|
|
210
226
|
};
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
.
|
|
216
|
-
.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const xPos = getScalePosition(x, d[xKey], xScaleType);
|
|
221
|
-
return xScaleType === 'band'
|
|
222
|
-
? xPos + barOffset
|
|
223
|
-
: xPos - barWidth / 2;
|
|
224
|
-
})
|
|
225
|
-
.attr('y', (d) => getVerticalBounds(d).min)
|
|
226
|
-
.attr('width', barWidth)
|
|
227
|
-
.attr('height', (d) => {
|
|
228
|
-
const bounds = getVerticalBounds(d);
|
|
229
|
-
return Math.abs(bounds.max - bounds.min);
|
|
230
|
-
})
|
|
231
|
-
.attr('fill', (d, i) => this.colorAdapter ? this.colorAdapter(d, i) : this.fill);
|
|
232
|
-
}
|
|
233
|
-
renderHorizontal(plotGroup, data, xKey, x, y, parseValue, yScaleType, stackingContext) {
|
|
227
|
+
const snapshotKeys = buildXYDatumSnapshotKeys(data, xKey);
|
|
228
|
+
const layoutData = this.createVerticalLayoutData(data, xKey, snapshotKeys, x, xScaleType, barWidth, barOffset, getVerticalBounds);
|
|
229
|
+
const animatedLayoutData = this.createAnimatedVerticalLayoutData(layoutData, animation);
|
|
230
|
+
return {
|
|
231
|
+
snapshot: this.createSnapshot(layoutData),
|
|
232
|
+
transitions: this.renderBars(plotGroup, layoutData, animatedLayoutData, animation),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
renderHorizontal(plotGroup, data, xKey, x, y, parseValue, yScaleType, stackingContext, animation) {
|
|
234
236
|
const bandwidth = y.bandwidth ? y.bandwidth() : 20;
|
|
235
237
|
const mode = stackingContext?.mode ?? 'normal';
|
|
236
238
|
const { thickness: barHeight, offset: barOffset } = getBarSlotLayout(bandwidth, mode, this.maxBarSize, stackingContext?.totalSeries ?? 1, stackingContext?.seriesIndex ?? 0, stackingContext?.gap ?? 0.1);
|
|
@@ -240,278 +242,395 @@ export class Bar {
|
|
|
240
242
|
const { start, end } = getBarValueRange(categoryKey, value, stackingContext);
|
|
241
243
|
return getScaledValueRange(x, start, end);
|
|
242
244
|
};
|
|
243
|
-
|
|
245
|
+
const snapshotKeys = buildXYDatumSnapshotKeys(data, xKey);
|
|
246
|
+
const layoutData = this.createHorizontalLayoutData(data, xKey, snapshotKeys, y, yScaleType, barHeight, barOffset, getHorizontalBounds);
|
|
247
|
+
const animatedLayoutData = this.createAnimatedHorizontalLayoutData(layoutData, animation);
|
|
248
|
+
return {
|
|
249
|
+
snapshot: this.createSnapshot(layoutData),
|
|
250
|
+
transitions: this.renderBars(plotGroup, layoutData, animatedLayoutData, animation),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
createVerticalLayoutData(data, xKey, snapshotKeys, x, xScaleType, barWidth, barOffset, getVerticalBounds) {
|
|
254
|
+
return data.map((entry, index) => {
|
|
255
|
+
const xPos = getScalePosition(x, entry[xKey], xScaleType);
|
|
256
|
+
const bounds = getVerticalBounds(entry);
|
|
257
|
+
return {
|
|
258
|
+
data: entry,
|
|
259
|
+
snapshotKey: snapshotKeys[index] ?? String(index),
|
|
260
|
+
x: xScaleType === 'band'
|
|
261
|
+
? xPos + barOffset
|
|
262
|
+
: xPos - barWidth / 2,
|
|
263
|
+
y: bounds.min,
|
|
264
|
+
width: barWidth,
|
|
265
|
+
height: Math.abs(bounds.max - bounds.min),
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
createHorizontalLayoutData(data, xKey, snapshotKeys, y, yScaleType, barHeight, barOffset, getHorizontalBounds) {
|
|
270
|
+
return data.map((entry, index) => {
|
|
271
|
+
const yPos = getScalePosition(y, entry[xKey], yScaleType);
|
|
272
|
+
const bounds = getHorizontalBounds(entry);
|
|
273
|
+
return {
|
|
274
|
+
data: entry,
|
|
275
|
+
snapshotKey: snapshotKeys[index] ?? String(index),
|
|
276
|
+
x: bounds.min,
|
|
277
|
+
y: yScaleType === 'band'
|
|
278
|
+
? yPos + barOffset
|
|
279
|
+
: yPos - barHeight / 2,
|
|
280
|
+
width: Math.abs(bounds.max - bounds.min),
|
|
281
|
+
height: barHeight,
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
createAnimatedVerticalLayoutData(layoutData, animation) {
|
|
286
|
+
return layoutData.map((entry) => {
|
|
287
|
+
return this.resolveAnimatedLayoutDatum(entry, animation, {
|
|
288
|
+
y: animation?.baselineValuePosition,
|
|
289
|
+
height: 0,
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
createAnimatedHorizontalLayoutData(layoutData, animation) {
|
|
294
|
+
return layoutData.map((entry) => {
|
|
295
|
+
return this.resolveAnimatedLayoutDatum(entry, animation, {
|
|
296
|
+
x: animation?.baselineValuePosition,
|
|
297
|
+
width: 0,
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
resolveAnimatedLayoutDatum(entry, animation, baselineOverride) {
|
|
302
|
+
if (!animation) {
|
|
303
|
+
return entry;
|
|
304
|
+
}
|
|
305
|
+
const previousSnapshot = animation.previousSnapshot?.get(entry.snapshotKey);
|
|
306
|
+
if (previousSnapshot) {
|
|
307
|
+
return {
|
|
308
|
+
...entry,
|
|
309
|
+
...previousSnapshot,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
...entry,
|
|
314
|
+
...baselineOverride,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
createSnapshot(layoutData) {
|
|
318
|
+
const snapshot = new Map();
|
|
319
|
+
layoutData.forEach((entry) => {
|
|
320
|
+
snapshot.set(entry.snapshotKey, {
|
|
321
|
+
x: entry.x,
|
|
322
|
+
y: entry.y,
|
|
323
|
+
width: entry.width,
|
|
324
|
+
height: entry.height,
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
return snapshot;
|
|
328
|
+
}
|
|
329
|
+
renderBars(plotGroup, layoutData, animatedLayoutData, animation) {
|
|
244
330
|
const sanitizedKey = sanitizeForCSS(this.dataKey);
|
|
245
|
-
plotGroup
|
|
331
|
+
const bars = plotGroup
|
|
246
332
|
.selectAll(`.bar-${sanitizedKey}`)
|
|
247
|
-
.data(
|
|
333
|
+
.data(layoutData)
|
|
248
334
|
.join('rect')
|
|
249
335
|
.attr('class', `bar-${sanitizedKey}`)
|
|
250
336
|
.attr('data-index', (_, i) => i)
|
|
251
|
-
.attr('x', (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
.attr('fill', (
|
|
337
|
+
.attr('x', (_, index) => (animation
|
|
338
|
+
? animatedLayoutData[index]?.x
|
|
339
|
+
: layoutData[index]?.x) ?? 0)
|
|
340
|
+
.attr('y', (_, index) => (animation
|
|
341
|
+
? animatedLayoutData[index]?.y
|
|
342
|
+
: layoutData[index]?.y) ?? 0)
|
|
343
|
+
.attr('width', (_, index) => (animation
|
|
344
|
+
? animatedLayoutData[index]?.width
|
|
345
|
+
: layoutData[index]?.width) ?? 0)
|
|
346
|
+
.attr('height', (_, index) => (animation
|
|
347
|
+
? animatedLayoutData[index]?.height
|
|
348
|
+
: layoutData[index]?.height) ?? 0)
|
|
349
|
+
.attr('fill', (entry, i) => this.colorAdapter
|
|
350
|
+
? this.colorAdapter(entry.data, i)
|
|
351
|
+
: this.fill);
|
|
352
|
+
if (!animation) {
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
const transition = bars
|
|
356
|
+
.transition()
|
|
357
|
+
.duration(animation.duration)
|
|
358
|
+
.ease(animation.easing)
|
|
359
|
+
.attr('x', (_, index) => layoutData[index]?.x ?? 0)
|
|
360
|
+
.attr('y', (_, index) => layoutData[index]?.y ?? 0)
|
|
361
|
+
.attr('width', (_, index) => layoutData[index]?.width ?? 0)
|
|
362
|
+
.attr('height', (_, index) => layoutData[index]?.height ?? 0);
|
|
363
|
+
return [createTransitionCompletionPromise(transition)];
|
|
364
|
+
}
|
|
365
|
+
resolveValueLabelConfig(theme) {
|
|
366
|
+
const config = this.valueLabel;
|
|
367
|
+
return {
|
|
368
|
+
...this.resolveValueLabelPlacement(config),
|
|
369
|
+
...this.resolveValueLabelStyle(config, theme),
|
|
370
|
+
formatter: config.formatter,
|
|
371
|
+
autoContrastInside: config.color === undefined,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
resolveValueLabelPlacement(config) {
|
|
375
|
+
return {
|
|
376
|
+
position: config.position ?? 'outside',
|
|
377
|
+
insidePosition: config.insidePosition ?? 'top',
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
resolveValueLabelStyle(config, theme) {
|
|
381
|
+
return {
|
|
382
|
+
fontSize: config.fontSize ?? theme.valueLabel.fontSize,
|
|
383
|
+
fontFamily: config.fontFamily ?? theme.valueLabel.fontFamily,
|
|
384
|
+
fontWeight: config.fontWeight ?? theme.valueLabel.fontWeight,
|
|
385
|
+
color: config.color ?? theme.valueLabel.color,
|
|
386
|
+
background: config.background ?? theme.valueLabel.background,
|
|
387
|
+
border: config.border ?? theme.valueLabel.border,
|
|
388
|
+
borderRadius: config.borderRadius ?? theme.valueLabel.borderRadius,
|
|
389
|
+
padding: config.padding ?? theme.valueLabel.padding,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
getValueLabelText(rawValue, data, config) {
|
|
393
|
+
return config.formatter
|
|
394
|
+
? config.formatter(this.dataKey, rawValue, data)
|
|
395
|
+
: String(rawValue);
|
|
396
|
+
}
|
|
397
|
+
getBarColor(data, index) {
|
|
398
|
+
return this.colorAdapter ? this.colorAdapter(data, index) : this.fill;
|
|
399
|
+
}
|
|
400
|
+
measureLabelBox(labelGroup, valueText, config) {
|
|
401
|
+
const tempText = labelGroup
|
|
402
|
+
.append('text')
|
|
403
|
+
.style('font-size', `${config.fontSize}px`)
|
|
404
|
+
.style('font-family', config.fontFamily)
|
|
405
|
+
.style('font-weight', config.fontWeight)
|
|
406
|
+
.text(valueText);
|
|
407
|
+
const textBox = tempText.node().getBBox();
|
|
408
|
+
tempText.remove();
|
|
409
|
+
return {
|
|
410
|
+
width: textBox.width + config.padding * 2,
|
|
411
|
+
height: textBox.height + config.padding * 2,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
getLabelColor(config, barColor) {
|
|
415
|
+
if (config.position === 'inside' && config.autoContrastInside) {
|
|
416
|
+
return getContrastTextColor(barColor);
|
|
417
|
+
}
|
|
418
|
+
return config.color;
|
|
419
|
+
}
|
|
420
|
+
appendValueLabel(labelGroup, valueText, placement, labelBox, config, labelColor) {
|
|
421
|
+
const group = labelGroup.append('g');
|
|
422
|
+
if (config.position === 'outside') {
|
|
423
|
+
group
|
|
424
|
+
.append('rect')
|
|
425
|
+
.attr('x', placement.x - labelBox.width / 2)
|
|
426
|
+
.attr('y', placement.y - labelBox.height / 2)
|
|
427
|
+
.attr('width', labelBox.width)
|
|
428
|
+
.attr('height', labelBox.height)
|
|
429
|
+
.attr('rx', config.borderRadius)
|
|
430
|
+
.attr('ry', config.borderRadius)
|
|
431
|
+
.attr('fill', config.background)
|
|
432
|
+
.attr('stroke', config.border)
|
|
433
|
+
.attr('stroke-width', 1);
|
|
434
|
+
}
|
|
435
|
+
group
|
|
436
|
+
.append('text')
|
|
437
|
+
.attr('x', placement.x)
|
|
438
|
+
.attr('y', placement.y)
|
|
439
|
+
.attr('text-anchor', 'middle')
|
|
440
|
+
.attr('dominant-baseline', 'central')
|
|
441
|
+
.style('font-size', `${config.fontSize}px`)
|
|
442
|
+
.style('font-family', config.fontFamily)
|
|
443
|
+
.style('font-weight', config.fontWeight)
|
|
444
|
+
.style('fill', labelColor)
|
|
445
|
+
.style('pointer-events', 'none')
|
|
446
|
+
.text(valueText);
|
|
447
|
+
}
|
|
448
|
+
getVerticalLabelPlacement(input) {
|
|
449
|
+
if (input.position === 'outside') {
|
|
450
|
+
return this.getVerticalOutsideLabelPlacement(input);
|
|
451
|
+
}
|
|
452
|
+
if (input.mode === 'layer' && input.insidePosition === 'bottom') {
|
|
453
|
+
return {
|
|
454
|
+
x: input.x,
|
|
455
|
+
y: (input.barTop + input.barBottom) / 2,
|
|
456
|
+
shouldRender: false,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
const { inset, minPadding } = getLabelSpacing(input.mode);
|
|
460
|
+
const y = this.getVerticalInsideLabelY(input.barTop, input.barBottom, input.labelBox.height, input.insidePosition, inset);
|
|
461
|
+
return {
|
|
462
|
+
x: input.x,
|
|
463
|
+
y,
|
|
464
|
+
shouldRender: input.labelBox.height + minPadding <= input.barHeight,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
getVerticalOutsideLabelPlacement(input) {
|
|
468
|
+
const y = input.isNegative
|
|
469
|
+
? input.barBottom + input.labelBox.height / 2 + LABEL_OUTSIDE_OFFSET
|
|
470
|
+
: input.barTop - input.labelBox.height / 2 - LABEL_OUTSIDE_OFFSET;
|
|
471
|
+
const shouldRender = input.isNegative
|
|
472
|
+
? y + input.labelBox.height / 2 <= input.plotBottom
|
|
473
|
+
: y - input.labelBox.height / 2 >= input.plotTop;
|
|
474
|
+
return {
|
|
475
|
+
x: input.x,
|
|
476
|
+
y,
|
|
477
|
+
shouldRender,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
getVerticalInsideLabelY(barTop, barBottom, labelHeight, insidePosition, inset) {
|
|
481
|
+
switch (insidePosition) {
|
|
482
|
+
case 'top':
|
|
483
|
+
return barTop + labelHeight / 2 + inset;
|
|
484
|
+
case 'middle':
|
|
485
|
+
return (barTop + barBottom) / 2;
|
|
486
|
+
case 'bottom':
|
|
487
|
+
return barBottom - labelHeight / 2 - inset;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
getHorizontalLabelPlacement(input) {
|
|
491
|
+
if (input.position === 'outside') {
|
|
492
|
+
return this.getHorizontalOutsideLabelPlacement(input);
|
|
493
|
+
}
|
|
494
|
+
if (input.mode === 'layer' && input.insidePosition === 'bottom') {
|
|
495
|
+
return {
|
|
496
|
+
x: (input.barLeft + input.barRight) / 2,
|
|
497
|
+
y: input.y,
|
|
498
|
+
shouldRender: false,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
const { inset, minPadding } = getLabelSpacing(input.mode);
|
|
502
|
+
const x = this.getHorizontalInsideLabelX(input.barLeft, input.barRight, input.labelBox.width, input.isNegative, input.insidePosition, inset);
|
|
503
|
+
const fitsBar = input.labelBox.width + minPadding <= input.barWidth;
|
|
504
|
+
const withinBounds = input.insidePosition === 'middle' ||
|
|
505
|
+
this.isHorizontalLabelWithinBounds(x, input.labelBox.width, input.barLeft, input.barRight);
|
|
506
|
+
return {
|
|
507
|
+
x,
|
|
508
|
+
y: input.y,
|
|
509
|
+
shouldRender: fitsBar && withinBounds,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
getHorizontalOutsideLabelPlacement(input) {
|
|
513
|
+
const x = input.isNegative
|
|
514
|
+
? input.barLeft - input.labelBox.width / 2 - LABEL_OUTSIDE_OFFSET
|
|
515
|
+
: input.barRight + input.labelBox.width / 2 + LABEL_OUTSIDE_OFFSET;
|
|
516
|
+
const shouldRender = input.isNegative
|
|
517
|
+
? x - input.labelBox.width / 2 >= input.plotLeft
|
|
518
|
+
: x + input.labelBox.width / 2 <= input.plotRight;
|
|
519
|
+
return {
|
|
520
|
+
x,
|
|
521
|
+
y: input.y,
|
|
522
|
+
shouldRender,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
getHorizontalInsideLabelX(barLeft, barRight, labelWidth, isNegative, insidePosition, inset) {
|
|
526
|
+
switch (insidePosition) {
|
|
527
|
+
case 'top':
|
|
528
|
+
return isNegative
|
|
529
|
+
? barRight - labelWidth / 2 - inset
|
|
530
|
+
: barLeft + labelWidth / 2 + inset;
|
|
531
|
+
case 'middle':
|
|
532
|
+
return (barLeft + barRight) / 2;
|
|
533
|
+
case 'bottom':
|
|
534
|
+
return isNegative
|
|
535
|
+
? barLeft + labelWidth / 2 + inset
|
|
536
|
+
: barRight - labelWidth / 2 - inset;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
isHorizontalLabelWithinBounds(labelX, labelWidth, barLeft, barRight) {
|
|
540
|
+
const labelLeft = labelX - labelWidth / 2;
|
|
541
|
+
const labelRight = labelX + labelWidth / 2;
|
|
542
|
+
return labelLeft >= barLeft + 1 && labelRight <= barRight - 1;
|
|
543
|
+
}
|
|
544
|
+
renderVerticalValueLabel(labelGroup, dataItem, index, xKey, x, y, parseValue, xScaleType, stackingContext, config, barWidth, barOffset, mode, plotTop, plotBottom) {
|
|
545
|
+
const categoryKey = String(dataItem[xKey]);
|
|
546
|
+
const rawValue = parseValue(dataItem[this.dataKey]);
|
|
547
|
+
const renderedValue = this.getRenderedValue(rawValue, 'vertical');
|
|
548
|
+
const valueText = this.getValueLabelText(rawValue, dataItem, config);
|
|
549
|
+
const xPos = getScalePosition(x, dataItem[xKey], xScaleType);
|
|
550
|
+
const barColor = this.getBarColor(dataItem, index);
|
|
551
|
+
const { start, end } = getBarValueRange(categoryKey, renderedValue, stackingContext);
|
|
552
|
+
const bounds = getScaledValueRange(y, start, end);
|
|
553
|
+
const barTop = bounds.min;
|
|
554
|
+
const barBottom = bounds.max;
|
|
555
|
+
const barHeight = Math.abs(barBottom - barTop);
|
|
556
|
+
const barCenterX = xPos +
|
|
557
|
+
(xScaleType === 'band' ? barOffset : -barWidth / 2) +
|
|
558
|
+
barWidth / 2;
|
|
559
|
+
const labelBox = this.measureLabelBox(labelGroup, valueText, config);
|
|
560
|
+
const placement = this.getVerticalLabelPlacement({
|
|
561
|
+
x: barCenterX,
|
|
562
|
+
barTop,
|
|
563
|
+
barBottom,
|
|
564
|
+
barHeight,
|
|
565
|
+
labelBox,
|
|
566
|
+
isNegative: renderedValue < 0,
|
|
567
|
+
mode,
|
|
568
|
+
position: config.position,
|
|
569
|
+
insidePosition: config.insidePosition,
|
|
570
|
+
plotTop,
|
|
571
|
+
plotBottom,
|
|
572
|
+
});
|
|
573
|
+
if (!placement.shouldRender) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
this.appendValueLabel(labelGroup, valueText, placement, labelBox, config, this.getLabelColor(config, barColor));
|
|
577
|
+
}
|
|
578
|
+
renderHorizontalValueLabel(labelGroup, dataItem, index, xKey, x, y, parseValue, yScaleType, stackingContext, config, barHeight, barOffset, mode, plotLeft, plotRight) {
|
|
579
|
+
const categoryKey = String(dataItem[xKey]);
|
|
580
|
+
const rawValue = parseValue(dataItem[this.dataKey]);
|
|
581
|
+
const renderedValue = this.getRenderedValue(rawValue, 'horizontal');
|
|
582
|
+
const valueText = this.getValueLabelText(rawValue, dataItem, config);
|
|
583
|
+
const yPos = getScalePosition(y, dataItem[xKey], yScaleType);
|
|
584
|
+
const barColor = this.getBarColor(dataItem, index);
|
|
585
|
+
const { start, end } = getBarValueRange(categoryKey, renderedValue, stackingContext);
|
|
586
|
+
const bounds = getScaledValueRange(x, start, end);
|
|
587
|
+
const barLeft = bounds.min;
|
|
588
|
+
const barRight = bounds.max;
|
|
589
|
+
const barWidth = Math.abs(barRight - barLeft);
|
|
590
|
+
const barCenterY = yPos +
|
|
591
|
+
(yScaleType === 'band' ? barOffset : -barHeight / 2) +
|
|
592
|
+
barHeight / 2;
|
|
593
|
+
const labelBox = this.measureLabelBox(labelGroup, valueText, config);
|
|
594
|
+
const placement = this.getHorizontalLabelPlacement({
|
|
595
|
+
y: barCenterY,
|
|
596
|
+
barLeft,
|
|
597
|
+
barRight,
|
|
598
|
+
barWidth,
|
|
599
|
+
labelBox,
|
|
600
|
+
isNegative: renderedValue < 0,
|
|
601
|
+
mode,
|
|
602
|
+
position: config.position,
|
|
603
|
+
insidePosition: config.insidePosition,
|
|
604
|
+
plotLeft,
|
|
605
|
+
plotRight,
|
|
606
|
+
});
|
|
607
|
+
if (!placement.shouldRender) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
this.appendValueLabel(labelGroup, valueText, placement, labelBox, config, this.getLabelColor(config, barColor));
|
|
264
611
|
}
|
|
265
612
|
renderVerticalValueLabels(plotGroup, data, xKey, x, y, parseValue, xScaleType, theme, stackingContext) {
|
|
266
613
|
const bandwidth = x.bandwidth ? x.bandwidth() : 20;
|
|
267
614
|
const mode = stackingContext?.mode ?? 'normal';
|
|
268
615
|
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;
|
|
616
|
+
const config = this.resolveValueLabelConfig(theme);
|
|
617
|
+
const plotTop = Math.min(...y.range());
|
|
618
|
+
const plotBottom = Math.max(...y.range());
|
|
280
619
|
const labelGroup = plotGroup
|
|
281
620
|
.append('g')
|
|
282
621
|
.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
|
-
});
|
|
622
|
+
data.forEach((dataItem, index) => this.renderVerticalValueLabel(labelGroup, dataItem, index, xKey, x, y, parseValue, xScaleType, stackingContext, config, barWidth, barOffset, mode, plotTop, plotBottom));
|
|
383
623
|
}
|
|
384
624
|
renderHorizontalValueLabels(plotGroup, data, xKey, x, y, parseValue, yScaleType, theme, stackingContext) {
|
|
385
625
|
const bandwidth = y.bandwidth ? y.bandwidth() : 20;
|
|
386
626
|
const mode = stackingContext?.mode ?? 'normal';
|
|
387
627
|
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;
|
|
628
|
+
const config = this.resolveValueLabelConfig(theme);
|
|
629
|
+
const plotLeft = Math.min(...x.range());
|
|
630
|
+
const plotRight = Math.max(...x.range());
|
|
399
631
|
const labelGroup = plotGroup
|
|
400
632
|
.append('g')
|
|
401
633
|
.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
|
-
});
|
|
634
|
+
data.forEach((dataItem, index) => this.renderHorizontalValueLabel(labelGroup, dataItem, index, xKey, x, y, parseValue, yScaleType, stackingContext, config, barHeight, barOffset, mode, plotLeft, plotRight));
|
|
516
635
|
}
|
|
517
636
|
}
|