11<script setup>
2- import { ref , computed , onMounted } from " vue" ;
2+ import { ref , computed , onMounted , nextTick } from " vue" ;
33import { useConfig } from " ../useConfig" ;
44import {
55 adaptColorToBackground ,
@@ -78,6 +78,7 @@ const chartSlicer = ref(null);
7878const slicerStep = ref (0 );
7979const isFullscreen = ref (false );
8080const trapIndex = ref (null );
81+ const isLoaded = ref (false );
8182
8283onMounted (() => {
8384 if (objectIsEmpty (props .dataset )) {
@@ -101,6 +102,9 @@ onMounted(() => {
101102 })
102103 })
103104 }
105+ setTimeout (() => {
106+ isLoaded .value = true ;
107+ }, 10 )
104108})
105109
106110const FINAL_CONFIG = computed (() => {
@@ -150,10 +154,11 @@ const customPalette = computed(() => {
150154});
151155
152156const resizeObserver = ref (null );
153-
157+ const to = ref ( null )
154158onMounted (() => {
155159 if (FINAL_CONFIG .value .responsive ) {
156160 const handleResize = throttle (() => {
161+ isLoaded .value = false ; // unset rect transitions as it looks janky during resizing process
157162 const { width , height } = useResponsive ({
158163 chart: stackbarChart .value ,
159164 title: FINAL_CONFIG .value .style .chart .title .text ? chartTitle .value : null ,
@@ -162,6 +167,10 @@ onMounted(() => {
162167 });
163168 defaultSizes .value .width = width;
164169 defaultSizes .value .height = height;
170+ clearTimeout (to .value );
171+ to .value = setTimeout (() => {
172+ isLoaded .value = true ;
173+ },10 )
165174 });
166175
167176 resizeObserver .value = new ResizeObserver (handleResize);
@@ -189,7 +198,7 @@ const drawingArea = computed(() => {
189198 bottom,
190199 left,
191200 width,
192- height,
201+ height: height < 0 ? 0 : height ,
193202 }
194203});
195204
@@ -198,6 +207,12 @@ const unmutableDataset = computed(() => {
198207 const color = convertColorToHex (ds .color ) || customPalette .value [i] || palette[i] || palette[i % palette .length ];
199208 return {
200209 ... ds,
210+ // In distributed mode, all values are converted to positive
211+ series: JSON .parse (JSON .stringify (ds .series )).map (v => {
212+ return FINAL_CONFIG .value .style .chart .bars .distributed ? Math .abs (v) : v
213+ }),
214+ // Store signs to manage display of neg values in distributed mode
215+ signedSeries: ds .series .map (v => v >= 0 ? 1 : - 1 ),
201216 absoluteIndex: i,
202217 id: createUid (),
203218 color
@@ -209,7 +224,6 @@ const maxSeries = computed(() => {
209224 return Math .max (... unmutableDataset .value .filter (ds => ! segregated .value .includes (ds .id )).map (ds => ds .series .length ))
210225});
211226
212-
213227const slicer = ref ({
214228 start: 0 ,
215229 end: Math .max (... props .dataset .map (ds => ds .series .length ))
@@ -232,12 +246,33 @@ const datasetTotals = computed(() => {
232246 return sumSeries (unmutableDataset .value .filter (ds => ! segregated .value .includes (ds .id ))).slice (slicer .value .start , slicer .value .end );
233247});
234248
249+ const datasetSignedTotals = computed (() => {
250+ const src = unmutableDataset .value .filter (ds => ! segregated .value .includes (ds .id ))
251+ return {
252+ positive: sumSeries (src .map (s => {
253+ return {
254+ ... s,
255+ series: s .series .slice (slicer .value .start , slicer .value .end ).map (v => v >= 0 ? v : 0 )
256+ }
257+ })),
258+ negative: sumSeries (src .map (s => {
259+ return {
260+ ... s,
261+ series: s .series .slice (slicer .value .start , slicer .value .end ).map (v => v < 0 ? v : 0 )
262+ }
263+ }))
264+ }
265+ });
266+
235267const yLabels = computed (() => {
236- const MAX = Math .max (... datasetTotals .value );
237- const scale = calculateNiceScale (0 , MAX , FINAL_CONFIG .value .style .chart .grid .scale .ticks );
268+ const MAX = Math .max (... datasetSignedTotals .value .positive );
269+ const workingMin = Math .min (... datasetSignedTotals .value .negative );
270+ const MIN = [- Infinity , Infinity , NaN , undefined , null ].includes (workingMin) ? 0 : workingMin;
271+ const scale = calculateNiceScale ((MIN > 0 ? 0 : MIN ), MAX < 0 ? 0 : MAX , FINAL_CONFIG .value .style .chart .grid .scale .ticks );
238272 return scale .ticks .map (t => {
239273 return {
240- y: drawingArea .value .bottom - (drawingArea .value .height * (t / scale .max )),
274+ zero: drawingArea .value .bottom - (drawingArea .value .height * ((Math .abs (scale .min )) / (scale .max + Math .abs (scale .min )))),
275+ y: drawingArea .value .bottom - (drawingArea .value .height * ((t + Math .abs (scale .min )) / ((scale .max ) + Math .abs (scale .min )))),
241276 x: drawingArea .value .left - 8 ,
242277 value: t
243278 }
@@ -251,48 +286,78 @@ const timeLabels = computed(() => {
251286
252287const formattedDataset = computed (() => {
253288 if (! isDataset .value ) return [];
289+
254290 let cumulativeY = Array (maxSeries .value ).fill (0 );
291+ let cumulativeNegY = Array (maxSeries .value ).fill (0 );
292+
293+ const premax = Math .max (... datasetSignedTotals .value .positive ) || 0 ;
294+ const workingMin = Math .min (... datasetSignedTotals .value .negative );
295+ const premin = [- Infinity , Infinity , NaN , undefined , null ].includes (workingMin) ? 0 : workingMin;
296+
297+ const scale = calculateNiceScale ((premin > 0 ? 0 : premin), premax < 0 ? 0 : premax, FINAL_CONFIG .value .style .chart .grid .scale .ticks );
298+ const { min: MIN , max: MAX } = scale;
299+
300+ const maxTotal = (MAX + (MIN >= 0 ? 0 : Math .abs (MIN ))) || 1
301+
255302 const totalHeight = drawingArea .value .height ;
256- const MAX = Math .max (... datasetTotals .value );
257- const scale = calculateNiceScale (0 , MAX , FINAL_CONFIG .value .style .chart .grid .scale .ticks );
258- const maxTotal = scale .max ;
303+ const ZERO_POSITION = yLabels .value [0 ] ? yLabels .value [0 ].zero : drawingArea .value .bottom ;
259304
260305 return unmutableDataset .value
261306 .filter (ds => ! segregated .value .includes (ds .id ))
262307 .map (ds => {
263308 const slicedSeries = ds .series .slice (slicer .value .start , slicer .value .end );
309+ const signedSliced = ds .signedSeries .slice (slicer .value .start , slicer .value .end );
310+
311+ const x = slicedSeries .map ((_dp , i ) => {
312+ return drawingArea .value .left + (barSlot .value * i) + (barSlot .value * FINAL_CONFIG .value .style .chart .bars .gapRatio / 4 );
313+ });
314+
315+ const y = slicedSeries .map ((dp , i ) => {
316+ const proportion = FINAL_CONFIG .value .style .chart .bars .distributed
317+ ? (dp || 0 ) / datasetTotals .value [i]
318+ : (dp || 0 ) / maxTotal;
319+
320+ let currentY, height;
321+ if (dp > 0 ) {
322+ height = totalHeight * proportion;
323+ currentY = ZERO_POSITION - height - cumulativeY[i];
324+ cumulativeY[i] += height;
325+ } else {
326+ height = totalHeight * proportion;
327+ currentY = ZERO_POSITION + cumulativeNegY[i]
328+ cumulativeNegY[i] += Math .abs (height)
329+ }
330+ return currentY;
331+ });
332+
333+ const height = slicedSeries .map ((dp , i ) => {
334+ const proportion = FINAL_CONFIG .value .style .chart .bars .distributed
335+ ? (dp || 0 ) / datasetTotals .value [i]
336+ : (dp || 0 ) / maxTotal;
337+
338+ if (dp > 0 ) {
339+ return totalHeight * proportion
340+ } else {
341+ return totalHeight * Math .abs (proportion)
342+ }
343+ });
344+
345+ const absoluteTotal = slicedSeries .map (v => Math .abs (v)).reduce ((a , b ) => a + b, 0 );
264346
265347 return {
266348 ... ds,
267349 proportions: slicedSeries .map ((dp , i ) => {
268350 if (FINAL_CONFIG .value .style .chart .bars .distributed ) {
269351 return (dp || 0 ) / datasetTotals .value [i];
270352 } else {
271- return (dp || 0 ) / maxTotal ;
353+ return (dp || 0 ) / absoluteTotal ;
272354 }
273355 }),
274356 series: slicedSeries,
275- x: slicedSeries .map ((_dp , i ) => {
276- return drawingArea .value .left + (barSlot .value * i) + (barSlot .value * FINAL_CONFIG .value .style .chart .bars .gapRatio / 4 );
277- }),
278- y: slicedSeries .map ((dp , i ) => {
279- const proportion = FINAL_CONFIG .value .style .chart .bars .distributed
280- ? (dp || 0 ) / datasetTotals .value [i]
281- : (dp || 0 ) / maxTotal;
282-
283- const height = totalHeight * proportion;
284- const currentY = totalHeight - height - cumulativeY[i];
285- cumulativeY[i] += height;
286-
287- return currentY + drawingArea .value .top ;
288- }),
289- height: slicedSeries .map ((dp , i ) => {
290- const proportion = FINAL_CONFIG .value .style .chart .bars .distributed
291- ? (dp || 0 ) / datasetTotals .value [i]
292- : (dp || 0 ) / maxTotal;
293-
294- return totalHeight * proportion <= 0 ? 0 : totalHeight * proportion;
295- }),
357+ signedSeries: signedSliced,
358+ x,
359+ y,
360+ height,
296361 };
297362 });
298363});
@@ -311,13 +376,15 @@ const totalLabels = computed(() => {
311376});
312377
313378
314- function barDataLabel (val , datapoint , index , dpIndex ) {
379+ function barDataLabel (val , datapoint , index , dpIndex , signed ) {
380+
381+ const appliedValue = FINAL_CONFIG .value .style .chart .bars .distributed ? signed === 1 ? val : - val : val;
315382 return applyDataLabel (
316383 FINAL_CONFIG .value .style .chart .bars .dataLabels .formatter ,
317- val ,
384+ appliedValue ,
318385 dataLabel ({
319386 p: FINAL_CONFIG .value .style .chart .bars .dataLabels .prefix ,
320- v: val ,
387+ v: appliedValue ,
321388 s: FINAL_CONFIG .value .style .chart .bars .dataLabels .suffix ,
322389 r: FINAL_CONFIG .value .style .chart .bars .dataLabels .rounding ,
323390 }),
@@ -368,7 +435,7 @@ function useTooltip(seriesIndex) {
368435 }
369436 });
370437
371- const sum = datapoint .map (d => d .value ).reduce ((a , b ) => a + b, 0 );
438+ const sum = datapoint .map (d => Math . abs ( d .value ) ).reduce ((a , b ) => a + b, 0 );
372439
373440 if (isFunction (customFormat) && functionReturnsString (() => customFormat ({
374441 seriesIndex,
@@ -412,7 +479,7 @@ function useTooltip(seriesIndex) {
412479 s: FINAL_CONFIG .value .style .chart .bars .dataLabels .suffix ,
413480 r: roundingValue,
414481 }) : ' ' } ${ parenthesis[0 ]}${ showPercentage ? dataLabel ({
415- v: isNaN (ds .value / sum) ? 0 : ds .value / sum * 100 ,
482+ 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.
416483 s: ' %' ,
417484 r: roundingPercentage,
418485 }) : ' ' }${ parenthesis[1 ]}
@@ -670,28 +737,21 @@ defineExpose({
670737 < / template>
671738
672739 <!-- STACKED BARS -->
673- < g v- for = " (dp, i) in formattedDataset" >
674- < path
740+ < g v- for = " (dp, i) in formattedDataset" >
741+ < rect
675742 v- for = " (rect, j) in dp.x"
676- : d= " (i === formattedDataset.length - 1 && formattedDataset.at(-1).series[j]) || (!formattedDataset.at(-1).series[j] && i < formattedDataset.length - 1 && formattedDataset[i].series[j] && !formattedDataset[i+1].series[j])
677- ? `M ${rect},${dp.y[j]}
678- h ${barSlot * (1 - FINAL_CONFIG.style.chart.bars.gapRatio / 2) - FINAL_CONFIG.style.chart.bars.borderRadius}
679- a ${FINAL_CONFIG.style.chart.bars.borderRadius},${FINAL_CONFIG.style.chart.bars.borderRadius} 0 0 1 ${FINAL_CONFIG.style.chart.bars.borderRadius},${FINAL_CONFIG.style.chart.bars.borderRadius}
680- v ${dp.height[j] - FINAL_CONFIG.style.chart.bars.borderRadius < 0 ? 0 : dp.height[j] - FINAL_CONFIG.style.chart.bars.borderRadius}
681- h -${barSlot * (1 - FINAL_CONFIG.style.chart.bars.gapRatio / 2)}
682- v -${dp.height[j] - FINAL_CONFIG.style.chart.bars.borderRadius < 0 ? 0 : dp.height[j] - FINAL_CONFIG.style.chart.bars.borderRadius}
683- a ${FINAL_CONFIG.style.chart.bars.borderRadius},${FINAL_CONFIG.style.chart.bars.borderRadius} 0 0 1 ${FINAL_CONFIG.style.chart.bars.borderRadius},-${FINAL_CONFIG.style.chart.bars.borderRadius} z`
684- : `M ${rect},${dp.y[j]}
685- h ${barSlot * (1 - FINAL_CONFIG.style.chart.bars.gapRatio / 2)}
686- v ${dp.height[j] < 0 ? 0 : dp.height[j]}
687- h -${barSlot * (1 - FINAL_CONFIG.style.chart.bars.gapRatio / 2)} z`"
743+ : x= " rect"
744+ : y= " dp.y[j] < 0 ? 0 : dp.y[j]"
745+ : height= " dp.height[j] < 0 ? 0.0001 : dp.height[j]"
746+ : 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 "
747+ : width= " barSlot * (1 - FINAL_CONFIG.style.chart.bars.gapRatio / 2)"
688748 : fill= " FINAL_CONFIG.style.chart.bars.gradient.show ? `url(#gradient_${dp.id})` : dp.color"
689749 : stroke= " FINAL_CONFIG.style.chart.backgroundColor"
690750 : stroke- width= " FINAL_CONFIG.style.chart.bars.strokeWidth"
691751 stroke- linecap= " round"
692752 stroke- linejoin= " round"
693- : class = " { 'vue-data-ui-bar-animated': FINAL_CONFIG.useCssAnimation }"
694- / >
753+ : class = " { 'vue-data-ui-bar-animated': FINAL_CONFIG.useCssAnimation, 'vue-data-ui-bar-transition': isLoaded }"
754+ / >
695755 < / g>
696756
697757 <!-- X AXIS -->
@@ -751,13 +811,15 @@ defineExpose({
751811 < text
752812 v- for = " (rect, j) in dp.x"
753813 : x= " rect + (barSlot * (1 - FINAL_CONFIG.style.chart.bars.gapRatio / 2) / 2)"
754- : y= " dp.y[j] + ( dp.proportions [j] * drawingArea.height / 2) + FINAL_CONFIG.style.chart.bars.dataLabels.fontSize / 3"
814+ : y= " dp.y[j] + dp.height [j] / 2 + FINAL_CONFIG.style.chart.bars.dataLabels.fontSize / 3"
755815 : font- size= " FINAL_CONFIG.style.chart.bars.dataLabels.fontSize"
756816 : fill= " FINAL_CONFIG.style.chart.bars.dataLabels.adaptColorToBackground ? adaptColorToBackground(dp.color) : FINAL_CONFIG.style.chart.bars.dataLabels.color"
757817 : font- weight= " FINAL_CONFIG.style.chart.bars.dataLabels.bold ? 'bold' : 'normal'"
758818 text- anchor= " middle"
759819 >
760- {{ FINAL_CONFIG .style .chart .bars .showDistributedPercentage && FINAL_CONFIG .style .chart .bars .distributed ? barDataLabelPercentage (dp .proportions [j] * 100 , dp, i, j) : barDataLabel (dp .series [j], dp, i, j) }}
820+ {{ FINAL_CONFIG .style .chart .bars .showDistributedPercentage && FINAL_CONFIG .style .chart .bars .distributed ?
821+ barDataLabelPercentage (dp .proportions [j] * 100 , dp, i, j) :
822+ barDataLabel (dp .series [j], dp, i, j, dp .signedSeries [j]) }}
761823 < / text>
762824 < / g>
763825
@@ -766,7 +828,7 @@ defineExpose({
766828 < text
767829 v- for = " (total, i) in totalLabels"
768830 : x= " drawingArea.left + (barSlot * i) + barSlot / 2"
769- : y= " (FINAL_CONFIG.style.chart.bars.distributed ? drawingArea.top - FINAL_CONFIG.style.chart.bars.totalValues.fontSize / 3 : total.y) + FINAL_CONFIG.style.chart.bars.totalValues.offsetY "
831+ : y= " drawingArea.top - FINAL_CONFIG.style.chart.bars.totalValues.fontSize / 3"
770832 text- anchor= " middle"
771833 : font- size= " FINAL_CONFIG.style.chart.bars.totalValues.fontSize"
772834 : font- weight= " FINAL_CONFIG.style.chart.bars.totalValues.bold ? 'bold' : 'normal'"
@@ -827,7 +889,7 @@ defineExpose({
827889 : x= " drawingArea.left + (i * barSlot)"
828890 : y= " drawingArea.top"
829891 : width= " barSlot"
830- : height= " drawingArea.height"
892+ : height= " drawingArea.height < 0 ? 0 : drawingArea.height "
831893 @click= " selectDatapoint(i)"
832894 @mouseenter= " useTooltip(i)"
833895 @mouseleave= " trapIndex = null; isTooltip = false"
@@ -976,6 +1038,10 @@ defineExpose({
9761038 transform- origin: center;
9771039}
9781040
1041+ .vue - data- ui- bar- transition {
1042+ transition: all 0 .2s ease- in - out ! important;
1043+ }
1044+
9791045@keyframes vueDataUiBarAnimation {
9801046 0 % {
9811047 transform: scale (0.9 ,0.9 );
0 commit comments