Skip to content

Commit f3e1d7c

Browse files
committed
Improvement - VueUiStackbar - Show total label on top of bars #262; add config options for total in tooltip and chart frame
1 parent 234f4d4 commit f3e1d7c

File tree

4 files changed

+195
-26
lines changed

4 files changed

+195
-26
lines changed

TestingArena/ArenaVueUiStackbar.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,12 @@ const model = ref([
275275
{ key: 'style.chart.grid.y.axisLabels.bold', def: false, type: 'checkbox'},
276276
{ key: 'style.chart.grid.y.axisLabels.rounding', def: 2, type: 'number', min: 0, max: 6},
277277
278+
{ key: 'style.chart.grid.frame.show', def: true, type: 'checkbox' },
279+
{ key: 'style.chart.grid.frame.stroke', def: '#CCCCCC', type: 'color'},
280+
{ key: 'style.chart.grid.frame.strokeWidth', def: 4, type: 'number', min: 0, max: 12},
281+
{ key: 'style.chart.grid.frame.strokeLinecap', def: 'round', type: 'text'},
282+
{ key: 'style.chart.grid.frame.strokeLinejoin', def: 'round', type: 'text'},
283+
{ key: 'style.chart.grid.frame.strokeDasharray', def: 0, type: 'number', min: 0, max: 24 },
278284
279285
{ key: 'userOptions.showOnChartHover', def: true, type: 'checkbox'},
280286
{ key: 'userOptions.keepStateOnChartLeave', def: true, type: 'checkbox'},

src/components/vue-ui-stackbar.vue

Lines changed: 158 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -498,19 +498,55 @@ const updateOffsetRight = throttle((w) => {
498498
offsetRight.value = w + FINAL_CONFIG.value.style.chart.bars.totalValues.fontSize
499499
}, 100);
500500
501+
502+
function computeRightOverhang() {
503+
if (FINAL_CONFIG.value.orientation !== 'horizontal') return 0;
504+
505+
const group = sumRight.value;
506+
if (!group) return 0;
507+
508+
const texts = Array.from(group.querySelectorAll('text'));
509+
if (!texts.length) return 0;
510+
511+
let maxRight = -Infinity;
512+
for (const t of texts) {
513+
try {
514+
const box = t.getBBox();
515+
const right = box.x + box.width;
516+
if (right > maxRight) maxRight = right;
517+
} catch (_) {
518+
//
519+
}
520+
}
521+
522+
const overhang = Math.max(0, maxRight - (drawingArea.value?.right ?? 0));
523+
return overhang;
524+
}
525+
501526
watchEffect((onInvalidate) => {
502-
if (FINAL_CONFIG.value.orientation === 'vertical') return;
527+
if (FINAL_CONFIG.value.orientation !== 'horizontal') return;
503528
504529
const el = sumRight.value;
505-
if (!el) return
530+
if (!el) return;
506531
507-
const observer = new ResizeObserver(entries => {
508-
updateOffsetRight(entries[0].contentRect.width)
509-
})
510-
observer.observe(el)
511-
onInvalidate(() => observer.disconnect())
532+
const onChange = () => {
533+
const overhang = computeRightOverhang();
534+
updateOffsetRight(overhang);
535+
};
536+
537+
onChange();
538+
const resizeObserver = new ResizeObserver(onChange);
539+
resizeObserver.observe(el);
540+
const mutationObserver = new MutationObserver(onChange);
541+
mutationObserver.observe(el, { childList: true, subtree: true, characterData: true });
542+
543+
onInvalidate(() => {
544+
resizeObserver.disconnect();
545+
mutationObserver.disconnect();
546+
});
512547
});
513548
549+
514550
onBeforeUnmount(() => {
515551
labelsXHeight.value = 0;
516552
offsetRight.value = 0;
@@ -1182,6 +1218,7 @@ function useTooltip(seriesIndex) {
11821218
}
11831219
11841220
const sum = datapoint.map(d => Math.abs(d.value)).reduce((a, b) => a + b, 0);
1221+
const sumLabel = datapoint.map(d => forceValidValue(d.value)).reduce((a, b) => a + b, 0);
11851222
11861223
if (isFunction(customFormat) && functionReturnsString(() => customFormat({
11871224
seriesIndex,
@@ -1198,6 +1235,8 @@ function useTooltip(seriesIndex) {
11981235
} else {
11991236
const {
12001237
showValue,
1238+
showTotal,
1239+
totalTranslation,
12011240
showPercentage,
12021241
borderColor,
12031242
roundingValue,
@@ -1210,6 +1249,25 @@ function useTooltip(seriesIndex) {
12101249
html += `<div style="width:100%;text-align:center;border-bottom:1px solid ${borderColor};padding-bottom:6px;margin-bottom:3px;">${FINAL_CONFIG.value.style.chart.tooltip.useDefaultTimeFormat ? timeLabels.value[seriesIndex]?.text : preciseAllTimeLabelsTooltip.value[seriesIndex]?.text || allTimeLabels.value[seriesIndex]?.text || ''}</div>`;
12111250
}
12121251
1252+
if (showTotal) {
1253+
html += `<div class="vue-data-ui-tooltip-total" style="display:flex;flex-direction:row;align-items:center;gap:4px">
1254+
<span>${totalTranslation}:</span>
1255+
<span>
1256+
${applyDataLabel(
1257+
FINAL_CONFIG.value.style.chart.bars.dataLabels.formatter,
1258+
sumLabel,
1259+
dataLabel({
1260+
p: FINAL_CONFIG.value.style.chart.bars.dataLabels.prefix,
1261+
v: sumLabel,
1262+
s: FINAL_CONFIG.value.style.chart.bars.dataLabels.suffix,
1263+
r: roundingValue
1264+
}),
1265+
{ datapoint: { name: totalTranslation, value: sumLabel } }
1266+
)}
1267+
</span>
1268+
</div>`
1269+
}
1270+
12131271
const parenthesis = [
12141272
showValue && showPercentage ? '(' : '',
12151273
showValue && showPercentage ? ')' : '',
@@ -1219,12 +1277,16 @@ function useTooltip(seriesIndex) {
12191277
html += `
12201278
<div style="display:flex;flex-direction:row;align-items:center;gap:4px">
12211279
<svg viewBox="0 0 60 60" height="14" width="14"><rect rx="5" x="0" y="0" height="60" width="60" stroke="none" fill="${FINAL_CONFIG.value.style.chart.bars.gradient.show ? `url(#gradient_${ds.id})` : ds.color}"/>${slots.pattern ? `<rect rx="5" x="0" y="0" height="60" width="60" stroke="none" fill="url(#pattern_${uid.value}_${ds.absoluteIndex})"/>` : ''}</svg>
1222-
${ds.name}${showValue || showPercentage ? ':' : ''} ${showValue ? dataLabel({
1223-
p: FINAL_CONFIG.value.style.chart.bars.dataLabels.prefix,
1224-
v: ds.value,
1225-
s: FINAL_CONFIG.value.style.chart.bars.dataLabels.suffix,
1226-
r: roundingValue,
1227-
}) : ''} ${parenthesis[0]}${showPercentage ? dataLabel({
1280+
${ds.name}${showValue || showPercentage ? ':' : ''} ${showValue ? applyDataLabel(
1281+
FINAL_CONFIG.value.style.chart.bars.dataLabels.formatter,
1282+
ds.value,
1283+
dataLabel({
1284+
p: FINAL_CONFIG.value.style.chart.bars.dataLabels.prefix,
1285+
v: ds.value,
1286+
s: FINAL_CONFIG.value.style.chart.bars.dataLabels.suffix,
1287+
r: roundingValue,
1288+
}, { datapoint: ds })
1289+
) : ''} ${parenthesis[0]}${showPercentage ? dataLabel({
12281290
v: isNaN(ds.value / sum) ? 0 : Math.abs(ds.value) / sum * 100, // Negs are absed to show relative proportion to absolute total. It's opinionated.
12291291
s: '%',
12301292
r: roundingPercentage,
@@ -1616,6 +1678,67 @@ function selectX({ seriesIndex, datapoint }) {
16161678
})
16171679
}
16181680
1681+
function getZeroPositions() {
1682+
const y0 = yLabels.value?.[0]?.zero ?? drawingArea.value.bottom;
1683+
const x0 = yLabels.value?.[0]?.horizontal_zero ?? drawingArea.value.left;
1684+
return { y0, x0 };
1685+
}
1686+
1687+
function placeLabelTotalY(index) {
1688+
const { y0 } = getZeroPositions();
1689+
const cfg = FINAL_CONFIG.value.style.chart.bars.totalValues;
1690+
const pad = Math.max(2, (cfg.fontSize * 0.3) + cfg.offsetY);
1691+
1692+
let minY = Infinity;
1693+
let hasPos = false;
1694+
1695+
for (const dp of formattedDataset.value || []) {
1696+
const v = dp?.series?.[index] ?? 0;
1697+
const h = dp?.height?.[index] ?? 0;
1698+
const y = dp?.y?.[index];
1699+
if (v > 0 && h > 0 && Number.isFinite(y)) {
1700+
hasPos = true;
1701+
if (y < minY) minY = y;
1702+
}
1703+
}
1704+
1705+
const topY = hasPos && Number.isFinite(minY) ? minY : y0;
1706+
const rawY = topY - pad;
1707+
1708+
const clampedY = Math.min(
1709+
Math.max(rawY, 0),
1710+
drawingArea.value.bottom
1711+
);
1712+
1713+
return clampedY;
1714+
}
1715+
1716+
1717+
1718+
function placeLabelTotalX(index) {
1719+
const { x0 } = getZeroPositions();
1720+
const pad = Math.max(2, (FINAL_CONFIG.value.style.chart.bars.totalValues.fontSize * 0.3) + FINAL_CONFIG.value.style.chart.bars.totalValues.offsetX);
1721+
1722+
let rightMost = -Infinity;
1723+
let hasPos = false;
1724+
1725+
for (const dp of formattedDataset.value || []) {
1726+
const v = dp?.series?.[index] ?? 0;
1727+
const x = dp?.horizontal_x?.[index];
1728+
const wRaw = dp?.horizontal_width?.[index];
1729+
const w = Number.isFinite(wRaw) ? Math.max(0, wRaw) : 0;
1730+
if (!Number.isFinite(x)) continue;
1731+
1732+
if (v > 0 && w > 0) {
1733+
hasPos = true;
1734+
rightMost = Math.max(rightMost, x + w);
1735+
}
1736+
}
1737+
1738+
const baseX = hasPos && Number.isFinite(rightMost) ? rightMost : x0;
1739+
return baseX + pad;
1740+
}
1741+
16191742
defineExpose({
16201743
getData,
16211744
getImage,
@@ -1775,6 +1898,23 @@ defineExpose({
17751898
</linearGradient>
17761899
</defs>
17771900
1901+
<!-- FRAME -->
1902+
<rect
1903+
data-cy="frame"
1904+
v-if="FINAL_CONFIG.style.chart.grid.frame.show"
1905+
:style="{ pointerEvents: 'none', transition: 'none', animation: 'none !important' }"
1906+
:x="Math.max(0, drawingArea.left)"
1907+
:y="Math.max(0, drawingArea.top)"
1908+
:width="Math.max(0, drawingArea.width)"
1909+
:height="Math.max(0, drawingArea.height)"
1910+
fill="transparent"
1911+
:stroke="FINAL_CONFIG.style.chart.grid.frame.stroke"
1912+
:stroke-width="FINAL_CONFIG.style.chart.grid.frame.strokeWidth"
1913+
:stroke-linecap="FINAL_CONFIG.style.chart.grid.frame.strokeLinecap"
1914+
:stroke-linejoin="FINAL_CONFIG.style.chart.grid.frame.strokeLinejoin"
1915+
:stroke-dasharray="FINAL_CONFIG.style.chart.grid.frame.strokeDasharray"
1916+
/>
1917+
17781918
<!-- HORIZONTAL LINES (vertical mode) -->
17791919
<template v-if="FINAL_CONFIG.style.chart.grid.x.showHorizontalLines && FINAL_CONFIG.orientation === 'vertical'">
17801920
<line
@@ -1846,7 +1986,7 @@ defineExpose({
18461986
<rect
18471987
v-for="(rect, j) in dp.x"
18481988
:x="rect"
1849-
:y="dp.y[j] < 0 ? 0 : dp.y[j]"
1989+
:y="forceValidValue(dp.y[j])"
18501990
:height="dp.height[j] < 0 ? 0.0001 : dp.height[j] || 0"
18511991
:rx="FINAL_CONFIG.style.chart.bars.borderRadius > dp.height[j] / 2 ? (dp.height[j] < 0 ? 0 : dp.height[j]) / 2 : FINAL_CONFIG.style.chart.bars.borderRadius "
18521992
:width="barSlot * (1 - FINAL_CONFIG.style.chart.bars.gapRatio / 2)"
@@ -1861,7 +2001,7 @@ defineExpose({
18612001
<rect
18622002
v-for="(rect, j) in dp.x"
18632003
:x="rect"
1864-
:y="dp.y[j] < 0 ? 0 : dp.y[j]"
2004+
:y="forceValidValue(dp.y[j])"
18652005
:height="dp.height[j] < 0 ? 0.0001 : dp.height[j] || 0"
18662006
:rx="FINAL_CONFIG.style.chart.bars.borderRadius > dp.height[j] / 2 ? (dp.height[j] < 0 ? 0 : dp.height[j]) / 2 : FINAL_CONFIG.style.chart.bars.borderRadius "
18672007
:width="barSlot * (1 - FINAL_CONFIG.style.chart.bars.gapRatio / 2)"
@@ -1996,7 +2136,7 @@ defineExpose({
19962136
data-cy="label-total"
19972137
v-if="FINAL_CONFIG.style.chart.bars.dataLabels.hideEmptyValues ? total.value !== 0 : true"
19982138
:x="drawingArea.left + (barSlot * i) + barSlot / 2"
1999-
:y="FINAL_CONFIG.style.chart.bars.totalValues.fontSize"
2139+
:y="placeLabelTotalY(i)"
20002140
text-anchor="middle"
20012141
:font-size="FINAL_CONFIG.style.chart.bars.totalValues.fontSize"
20022142
:font-weight="FINAL_CONFIG.style.chart.bars.totalValues.bold ? 'bold' : 'normal'"
@@ -2035,8 +2175,8 @@ defineExpose({
20352175
<text
20362176
data-cy="label-total"
20372177
v-if="FINAL_CONFIG.style.chart.bars.dataLabels.hideEmptyValues ? total.value !== 0 : true"
2038-
:x="drawingArea.right + FINAL_CONFIG.style.chart.bars.totalValues.fontSize / 3"
2039-
:y="drawingArea.top + (barSlot * i) + barSlot / 2"
2178+
:x="placeLabelTotalX(i)"
2179+
:y="drawingArea.top + (barSlot * i) + barSlot / 2 + (FINAL_CONFIG.style.chart.bars.totalValues.fontSize / 3)"
20402180
text-anchor="start"
20412181
:font-size="FINAL_CONFIG.style.chart.bars.totalValues.fontSize"
20422182
:font-weight="FINAL_CONFIG.style.chart.bars.totalValues.bold ? 'bold' : 'normal'"

src/useConfig.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,8 @@ export function useConfig() {
332332
roundingValue: 0,
333333
roundingPercentage: 0,
334334
showTimeLabel: true,
335+
showTotal: false,
336+
totalTranslation: 'Total',
335337
useDefaultTimeFormat: true,
336338
timeFormat: 'yyyy-MM-dd HH:mm:ss' // When datetimeFormatter is enabled and useDefaultFormat is false
337339
},
@@ -352,6 +354,7 @@ export function useConfig() {
352354
totalValues: {
353355
show: true,
354356
offsetY: 0,
357+
offsetX: 0,
355358
fontSize: FONT._16,
356359
bold: false,
357360
color: COLOR_BLACK
@@ -378,6 +381,14 @@ export function useConfig() {
378381
scaleMin: null, // Force min scale (defaults to dataset's min)
379382
scaleMax: null, // Force max scale (defaults to dataset's max)
380383
},
384+
frame: {
385+
show: false,
386+
stroke: COLOR_GREY_LIGHT,
387+
strokeWidth: 2,
388+
strokeLinecap: 'round',
389+
strokeLinejoin: 'round',
390+
strokeDasharray: 0
391+
},
381392
x: {
382393
showAxis: true,
383394
showHorizontalLines: false,
@@ -2724,6 +2735,7 @@ export function useConfig() {
27242735
},
27252736
labels: {
27262737
value: {
2738+
show: true,
27272739
fontSize: FONT._14,
27282740
minFontSize: MIN_FONT_SIZE, // v3
27292741
color: COLOR_BLACK,
@@ -2735,13 +2747,15 @@ export function useConfig() {
27352747
formatter: null,
27362748
},
27372749
valueLabel: {
2750+
show: true,
27382751
fontSize: FONT._14,
27392752
minFontSize: MIN_FONT_SIZE, // v3
27402753
color: COLOR_BLACK,
27412754
bold: false,
27422755
rounding: 0,
27432756
},
27442757
timeLabel: {
2758+
show: true,
27452759
fontSize: FONT._12,
27462760
minFontSize: MIN_FONT_SIZE, // v3
27472761
color: COLOR_BLACK,

0 commit comments

Comments
 (0)