[READ-ONLY] Mirror of https://github.com/graphieros/vue-data-ui. An open source user-empowering data visualization Vue 3 components library for eloquent data storytelling vue-data-ui.graphieros.com/
charts components-library dashboard data-storytelling data-visualization donut gauge heatmap quadrant radar rating-stars scatter screenshot skeleton-loader sparkline table vue-data-ui vue3 vuejs vuejs3
3

Configure Feed

Select the types of activity you want to include in your feed.

fix(VueUiStackBar, VueUiStackline): scale labels size reactivity issues

graphieros (Jun 30, 2026, 10:46 PM +0200) df29ad5e bf457c70

+179 -33
+1 -1
manual-testing/package-lock.json
··· 28 28 } 29 29 }, 30 30 "..": { 31 - "version": "3.22.0", 31 + "version": "3.22.2", 32 32 "dev": true, 33 33 "license": "MIT", 34 34 "devDependencies": {
+129 -32
src/components/vue-ui-stackbar.vue
··· 697 697 698 698 const labelsHeightRaf = ref(0); 699 699 700 + function setMeasuredLabelsHeight(value) { 701 + const nextValue = Number.isFinite(value) ? value : 0; 702 + if (measuredLabelsHeight.value === nextValue) return; 703 + measuredLabelsHeight.value = nextValue; 704 + } 705 + 700 706 function measureLabelsHeightNow() { 701 707 // What contributes to Y offset depends on orientation 702 708 const el = ··· 705 711 : scaleLabels.value; 706 712 707 713 if (!el) { 708 - measuredLabelsHeight.value = 0; 714 + setMeasuredLabelsHeight(0); 709 715 return; 710 716 } 711 717 712 718 try { 713 719 // SVG group bbox is the most reliable "rendered height" 714 720 const box = el.getBBox(); 715 - measuredLabelsHeight.value = Number.isFinite(box?.height) 716 - ? box.height 717 - : 0; 721 + setMeasuredLabelsHeight(box?.height); 718 722 } catch (_) { 719 - measuredLabelsHeight.value = 0; 723 + setMeasuredLabelsHeight(0); 720 724 } 721 725 } 722 726 ··· 883 887 const unmutableDataset = computed(() => { 884 888 return FINAL_DATASET.value.map((ds, i) => { 885 889 const id = `stackbar_serie_${i}`; 890 + const datasetKey = String(ds.key ?? ds.id ?? ds.name ?? id); 886 891 const color = 887 892 convertColorToHex(ds.color) || 888 893 customPalette.value[i] || ··· 890 895 palette[i % palette.length]; 891 896 return { 892 897 ...ds, 898 + datasetKey, 893 899 // In distributed mode, all values are converted to positive 894 900 series: JSON.parse(JSON.stringify(ds.series)).map((v) => { 895 901 return FINAL_CONFIG.value.style.chart.bars.distributed ··· 1096 1102 }; 1097 1103 }); 1098 1104 1099 - const yLabels = computed(() => { 1105 + const yScale = computed(() => { 1100 1106 const MAX = 1101 1107 FINAL_CONFIG.value.style.chart.grid.scale.scaleMax !== null && 1102 1108 !FINAL_CONFIG.value.style.chart.bars.distributed ··· 1111 1117 ? 0 1112 1118 : workingMin; 1113 1119 1114 - const scale = 1115 - !FINAL_CONFIG.value.style.chart.bars.distributed && 1120 + return !FINAL_CONFIG.value.style.chart.bars.distributed && 1116 1121 (FINAL_CONFIG.value.style.chart.grid.scale.scaleMax !== null || 1117 1122 FINAL_CONFIG.value.style.chart.grid.scale.scaleMin !== null) 1118 - ? calculateNiceScaleWithExactExtremes( 1119 - MIN > 0 ? 0 : MIN, 1120 - MAX < 0 ? 0 : MAX, 1121 - FINAL_CONFIG.value.style.chart.grid.scale.ticks, 1122 - ) 1123 - : calculateNiceScale( 1124 - MIN > 0 ? 0 : MIN, 1125 - MAX < 0 ? 0 : MAX, 1126 - FINAL_CONFIG.value.style.chart.grid.scale.ticks, 1127 - ); 1128 - return scale.ticks.map((t) => { 1123 + ? calculateNiceScaleWithExactExtremes( 1124 + MIN > 0 ? 0 : MIN, 1125 + MAX < 0 ? 0 : MAX, 1126 + FINAL_CONFIG.value.style.chart.grid.scale.ticks, 1127 + ) 1128 + : calculateNiceScale( 1129 + MIN > 0 ? 0 : MIN, 1130 + MAX < 0 ? 0 : MAX, 1131 + FINAL_CONFIG.value.style.chart.grid.scale.ticks, 1132 + ); 1133 + }); 1134 + 1135 + const yLabelValues = computed(() => yScale.value.ticks); 1136 + 1137 + const yLabels = computed(() => { 1138 + const scale = yScale.value; 1139 + const scaleRange = scale.max + Math.abs(scale.min); 1140 + 1141 + return yLabelValues.value.map((t) => { 1129 1142 return { 1130 1143 zero: 1131 1144 drawingArea.value.bottom - 1132 - drawingArea.value.height * 1133 - (Math.abs(scale.min) / (scale.max + Math.abs(scale.min))), 1145 + drawingArea.value.height * (Math.abs(scale.min) / scaleRange), 1134 1146 y: 1135 1147 drawingArea.value.bottom - 1136 1148 drawingArea.value.height * 1137 - ((t + Math.abs(scale.min)) / 1138 - (scale.max + Math.abs(scale.min))), 1149 + ((t + Math.abs(scale.min)) / scaleRange), 1139 1150 x: yAxisLabelsAreRight.value 1140 1151 ? drawingArea.value.right + 8 1141 1152 : drawingArea.value.left - 8, 1142 1153 horizontal_zero: 1143 1154 drawingArea.value.left + 1144 - drawingArea.value.width * 1145 - (Math.abs(scale.min) / (scale.max + Math.abs(scale.min))), 1155 + drawingArea.value.width * (Math.abs(scale.min) / scaleRange), 1146 1156 horizontal_x: 1147 1157 drawingArea.value.left + 1148 1158 drawingArea.value.width * 1149 - ((t + Math.abs(scale.min)) / 1150 - (scale.max + Math.abs(scale.min))), 1159 + ((t + Math.abs(scale.min)) / scaleRange), 1151 1160 horizontal_y: drawingArea.value.bottom - 8, 1152 1161 value: t, 1153 1162 }; 1154 1163 }); 1155 1164 }); 1156 1165 1166 + const formattedScaleLabelSignature = computed(() => { 1167 + const axisLabels = FINAL_CONFIG.value.style.chart.grid.y.axisLabels; 1168 + const { prefix, suffix } = FINAL_CONFIG.value.style.chart.bars.dataLabels; 1169 + 1170 + return yLabelValues.value 1171 + .map((value) => 1172 + String( 1173 + applyDataLabel( 1174 + axisLabels.formatter, 1175 + value, 1176 + dataLabel({ 1177 + p: prefix, 1178 + v: value, 1179 + s: suffix, 1180 + r: axisLabels.rounding, 1181 + }), 1182 + { datapoint: { value } }, 1183 + ) ?? '', 1184 + ), 1185 + ) 1186 + .join('|'); 1187 + }); 1188 + 1189 + watch( 1190 + () => [ 1191 + formattedScaleLabelSignature.value, 1192 + FINAL_CONFIG.value.orientation, 1193 + FINAL_CONFIG.value.style.chart.grid.y.axisLabels.show, 1194 + FINAL_CONFIG.value.style.chart.grid.y.axisLabels.fontSize, 1195 + FINAL_CONFIG.value.style.chart.grid.y.axisLabels.bold, 1196 + FINAL_CONFIG.value.style.chart.grid.y.position, 1197 + ], 1198 + () => { 1199 + if (!FINAL_CONFIG.value.style.chart.grid.y.axisLabels.show) return; 1200 + 1201 + runParentStableLayoutPass(); 1202 + }, 1203 + { flush: 'post' }, 1204 + ); 1205 + 1157 1206 const timeLabels = ref([]); 1158 1207 const allTimeLabels = ref([]); 1159 1208 ··· 1247 1296 const visibleTimeLabels = 1248 1297 displayedTimeLabels.value?.map((l) => l?.text ?? '').join('|') || 1249 1298 ''; 1250 - const scaleTicks = 1251 - yLabels.value?.map((l) => l?.value ?? '').join('|') || ''; 1299 + const scaleLabelsText = formattedScaleLabelSignature.value; 1252 1300 1253 1301 // Triggers on resize 1254 1302 const w = defaultSizes.value.width; ··· 1265 1313 void tlRotation; 1266 1314 void tlOffsetY; 1267 1315 void visibleTimeLabels; 1268 - void scaleTicks; 1316 + void scaleLabelsText; 1269 1317 void w; 1270 1318 void h; 1271 1319 void tlEl; ··· 1342 1390 })); 1343 1391 }); 1344 1392 1393 + const stackbarTimeLabelCounts = computed(() => { 1394 + const counts = new Map(); 1395 + const values = 1396 + FINAL_CONFIG.value.style.chart.grid.x.timeLabels.values || []; 1397 + values.forEach((value) => { 1398 + if ([null, undefined, ''].includes(value)) return; 1399 + const key = String(value); 1400 + counts.set(key, (counts.get(key) || 0) + 1); 1401 + }); 1402 + return counts; 1403 + }); 1404 + 1405 + function getStackbarDatapointKey(absoluteIndex) { 1406 + const values = 1407 + FINAL_CONFIG.value.style.chart.grid.x.timeLabels.values || []; 1408 + const value = values[absoluteIndex]; 1409 + if ([null, undefined, ''].includes(value)) { 1410 + return `fallback-${absoluteIndex}`; 1411 + } 1412 + const key = String(value); 1413 + if ((stackbarTimeLabelCounts.value.get(key) || 0) > 1) { 1414 + return `${key}-${absoluteIndex}`; 1415 + } 1416 + return key; 1417 + } 1418 + 1419 + function getStackbarRectKey(ds, absoluteIndex) { 1420 + return `${ds.datasetKey}:${getStackbarDatapointKey(absoluteIndex)}`; 1421 + } 1422 + 1345 1423 const formattedDataset = computed(() => { 1346 1424 if (!isDataset.value && !loading.value) return []; 1347 1425 ··· 1429 1507 slicer.value.end, 1430 1508 ); 1431 1509 1510 + const rectKeys = slicedSeries.map((_, i) => 1511 + getStackbarRectKey(ds, slicer.value.start + i), 1512 + ); 1513 + 1514 + const minimapRectKeys = fullSeries.map((_, absoluteIndex) => 1515 + getStackbarRectKey(ds, absoluteIndex), 1516 + ); 1517 + 1432 1518 const x = slicedSeries.map((_dp, i) => { 1433 1519 return ( 1434 1520 drawingArea.value.left + ··· 1613 1699 }), 1614 1700 series: slicedSeries, 1615 1701 signedSeries: signedSliced, 1702 + rectKeys, 1703 + minimapRectKeys, 1616 1704 x, 1617 1705 y, 1618 1706 height, ··· 3062 3150 /> 3063 3151 </template> 3064 3152 3065 - <g v-for="(dp, i) in formattedDataset"> 3153 + <g 3154 + v-for="(dp, i) in formattedDataset" 3155 + :key="`stackbar-group-${dp.datasetKey}`" 3156 + > 3066 3157 <defs v-if="$slots.pattern"> 3067 3158 <slot 3068 3159 name="pattern" ··· 3077 3168 <template v-if="FINAL_CONFIG.orientation === 'vertical'"> 3078 3169 <rect 3079 3170 v-for="(rect, j) in dp.x" 3171 + :key="`stackbar-rect-${dp.rectKeys[j]}`" 3080 3172 :x="rect" 3081 3173 :y="forceValidValue(dp.y[j])" 3082 3174 :height=" ··· 3112 3204 <g v-if="$slots.pattern"> 3113 3205 <rect 3114 3206 v-for="(rect, j) in dp.x" 3207 + :key="`stackbar-pattern-rect-${dp.rectKeys[j]}`" 3115 3208 :x="rect" 3116 3209 :y="forceValidValue(dp.y[j])" 3117 3210 :height=" ··· 3156 3249 <template v-else> 3157 3250 <rect 3158 3251 v-for="(rect, j) in dp.horizontal_x" 3252 + :key="`stackbar-rect-${dp.rectKeys[j]}`" 3159 3253 :x="forceValidValue(rect, drawingArea.left)" 3160 3254 :y="dp.horizontal_y[j] < 0 ? 0 : dp.horizontal_y[j]" 3161 3255 :width=" ··· 3195 3289 <g v-if="$slots.pattern"> 3196 3290 <rect 3197 3291 v-for="(rect, j) in dp.horizontal_x" 3292 + :key="`stackbar-pattern-rect-${dp.rectKeys[j]}`" 3198 3293 :x="forceValidValue(rect, drawingArea.left)" 3199 3294 :y=" 3200 3295 dp.horizontal_y[j] < 0 ··· 4079 4174 name="svg" 4080 4175 :svg="{ 4081 4176 drawingArea, 4177 + slicer, 4082 4178 data: formattedDataset, 4083 4179 isPrintingImg: 4084 4180 isPrinting || isImaging || isCallbackImaging, 4085 4181 isPrintingSvg: isCallbackSvg, 4182 + barWidth: barSlot, 4086 4183 }" 4087 4184 /> 4088 4185 </svg> ··· 4236 4333 <g v-for="dp in formattedDataset" :key="dp.id"> 4237 4334 <template 4238 4335 v-for="(x, j) in dp.xMinimap({ left: 0, unitW })" 4239 - :key="j" 4336 + :key="`minimap-rect-${dp.minimapRectKeys[j]}`" 4240 4337 > 4241 4338 <rect 4242 4339 :x="
+39
src/components/vue-ui-stackline.vue
··· 1839 1839 width: drawingArea.value.width, 1840 1840 })); 1841 1841 1842 + const formattedScaleLabelSignature = computed(() => { 1843 + const axisLabels = FINAL_CONFIG.value.style.chart.grid.y.axisLabels; 1844 + const { prefix, suffix } = FINAL_CONFIG.value.style.chart.lines.dataLabels; 1845 + 1846 + return yLabels.value 1847 + .map((yLabel) => 1848 + String( 1849 + applyDataLabel( 1850 + axisLabels.formatter, 1851 + yLabel.value, 1852 + dataLabel({ 1853 + p: prefix, 1854 + v: yLabel.value, 1855 + s: suffix, 1856 + r: axisLabels.rounding, 1857 + }), 1858 + { datapoint: yLabel }, 1859 + ) ?? '', 1860 + ), 1861 + ) 1862 + .join('|'); 1863 + }); 1864 + 1865 + watch( 1866 + [ 1867 + formattedScaleLabelSignature, 1868 + () => FINAL_CONFIG.value.style.chart.grid.y.axisLabels.show, 1869 + () => FINAL_CONFIG.value.style.chart.grid.y.axisLabels.fontSize, 1870 + () => FINAL_CONFIG.value.style.chart.grid.y.axisLabels.bold, 1871 + () => FINAL_CONFIG.value.style.chart.grid.y.position, 1872 + ], 1873 + () => { 1874 + if (!FINAL_CONFIG.value.style.chart.grid.y.axisLabels.show) return; 1875 + 1876 + runParentStableLayoutPass(); 1877 + }, 1878 + { flush: 'post' }, 1879 + ); 1880 + 1842 1881 const suppressChild = ref(false); 1843 1882 1844 1883 const WIDTH = computed(() => defaultSizes.value.width);
+10
types/vue-data-ui.d.ts
··· 13306 13306 }) => number[]; 13307 13307 y: number[]; 13308 13308 yMinimap: ({ minimapH }: { minimapH: number }) => number[]; 13309 + rectKeys: string[]; 13309 13310 }; 13310 13311 13311 13312 export type VueUiStackbarTimeLabelSlotProps = { ··· 13335 13336 }; 13336 13337 isPrintingImg: boolean; 13337 13338 isPrintingSvg: boolean; 13339 + slicer: { 13340 + start: number; 13341 + end: number; 13342 + }; 13343 + barWidth: number; 13338 13344 }; 13339 13345 }; 13340 13346 ··· 13780 13786 }; 13781 13787 isPrintingImg: boolean; 13782 13788 isPrintingSvg: boolean; 13789 + slicer: { 13790 + start: number; 13791 + end: number; 13792 + }; 13783 13793 }; 13784 13794 }; 13785 13795