Skip to content

Commit ec90d22

Browse files
committed
VueUiNestedDonuts added smooth animation on series segregation
1 parent 69ad843 commit ec90d22

File tree

5 files changed

+141
-29
lines changed

5 files changed

+141
-29
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "vue-data-ui",
33
"private": false,
4-
"version": "2.1.29",
4+
"version": "2.1.30",
55
"type": "module",
66
"description": "A user-empowering data visualization Vue 3 components library for eloquent data storytelling",
77
"keywords": [

src/App.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3801,7 +3801,7 @@ const pillConfig = ref({
38013801
</template>
38023802
</Box>
38033803

3804-
<Box @copy="copyConfig(PROD_CONFIG.vue_ui_nested_donuts)">
3804+
<Box open @copy="copyConfig(PROD_CONFIG.vue_ui_nested_donuts)">
38053805
<template #title>
38063806
<BaseIcon name="chartNestedDonuts"/>
38073807
VueUiNestedDonuts
@@ -4442,7 +4442,7 @@ const pillConfig = ref({
44424442
</template>
44434443
</Box>
44444444

4445-
<Box open @copy="copyConfig(PROD_CONFIG.vue_ui_scatter)">
4445+
<Box @copy="copyConfig(PROD_CONFIG.vue_ui_scatter)">
44464446
<template #title>
44474447
<BaseIcon name="chartScatter" />
44484448
VueUiScatter
@@ -4555,7 +4555,7 @@ const pillConfig = ref({
45554555
</template>
45564556
</Box>
45574557

4558-
<Box open @copy="copyConfig(PROD_CONFIG.vue_ui_onion)">
4558+
<Box @copy="copyConfig(PROD_CONFIG.vue_ui_onion)">
45594559
<template #title>
45604560
<BaseIcon name="chartOnion" />
45614561
VueUiOnion

src/components/vue-ui-nested-donuts.vue

Lines changed: 134 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,6 @@ const svg = computed(() => {
9595
9696
const emit = defineEmits(['selectLegend', 'selectDatapoint']);
9797
98-
function segregate(legend) {
99-
if(segregated.value.includes(legend.id)) {
100-
segregated.value = segregated.value.filter(s => s !== legend.id)
101-
} else {
102-
segregated.value.push(legend.id)
103-
}
104-
emit('selectLegend', legend)
105-
}
106-
10798
function selectDatapoint({ datapoint, index }) {
10899
emit('selectDatapoint', { datapoint, index })
109100
}
@@ -180,11 +171,11 @@ const immutableDataset = computed(() => {
180171
})
181172
}
182173
})
183-
})
174+
});
184175
185176
const donutSize = computed(() => donutConfig.value.style.chart.layout.donut.strokeWidth)
186177
187-
const mutableDataset = computed(() => {
178+
const md = computed(() => {
188179
return [...immutableDataset.value].map((ds, i) => {
189180
const sizeRatio = i * donutSize.value / immutableDataset.value.length;
190181
@@ -201,8 +192,125 @@ const mutableDataset = computed(() => {
201192
proportion: s.value / total
202193
}
203194
}),
195+
}
196+
})
197+
});
198+
199+
function checkSegregation(sourceArray, n, targetArray) {
200+
let count = 0;
201+
for (let i = 0; i < sourceArray.length; i += 1) {
202+
if (targetArray.includes(sourceArray[i])) {
203+
count += 1;
204+
}
205+
}
206+
return count < n;
207+
}
208+
209+
const mutableDataset = ref(md.value);
210+
const rafUp = ref(null);
211+
const rafDown = ref(null);
212+
213+
function segregateDonut(item) {
214+
emit('selectLegend', item);
215+
const target = immutableDataset.value.flatMap(d => d.series).find(el => el.id === item.id);
216+
const source = mutableDataset.value.flatMap(d => d.series).find(el => el.id === item.id).value;
217+
let initVal = source;
218+
219+
const allParentDonutIds = immutableDataset.value.find(el => el.id === target.arcOfId).series.map(s => s.id);
220+
const canSegregate = checkSegregation(allParentDonutIds, allParentDonutIds.length - 1, segregated.value);
221+
222+
if(segregated.value.includes(item.id)) {
223+
segregated.value = segregated.value.filter(s => s !== item.id);
224+
function animUp() {
225+
if(initVal > target.value) {
226+
cancelAnimationFrame(animUp);
227+
mutableDataset.value = mutableDataset.value.map(ds => {
228+
return {
229+
...ds,
230+
series: ds.series.map(s => {
231+
if(s.id == item.id) {
232+
return {
233+
...s,
234+
value: target.value
235+
}
236+
} else {
237+
return s;
238+
}
239+
})
240+
}
241+
});
242+
} else {
243+
initVal += (target.value * 0.025);
244+
mutableDataset.value = mutableDataset.value.map(ds => {
245+
return {
246+
...ds,
247+
series: ds.series.map(s => {
248+
if(s.id === item.id) {
249+
return {
250+
...s,
251+
value: initVal
252+
}
253+
} else {
254+
return s;
255+
}
256+
})
257+
}
258+
});
259+
rafUp.value = requestAnimationFrame(animUp);
260+
}
261+
}
262+
animUp();
263+
} else if(canSegregate) {
264+
function animDown() {
265+
if(initVal < 0.1) {
266+
cancelAnimationFrame(animDown);
267+
segregated.value.push(item.id);
268+
mutableDataset.value = mutableDataset.value.map((ds, i) => {
269+
return {
270+
...ds,
271+
series: ds.series.map(s => {
272+
if(s.id === item.id) {
273+
return {
274+
...s,
275+
value: 0
276+
}
277+
} else {
278+
return s
279+
}
280+
})
281+
}
282+
});
283+
} else {
284+
initVal /= 1.1;
285+
mutableDataset.value = mutableDataset.value.map((ds, i) => {
286+
return {
287+
...ds,
288+
series: ds.series.map(s => {
289+
if(s.id === item.id) {
290+
return {
291+
...s,
292+
value: initVal
293+
}
294+
} else {
295+
return s
296+
}
297+
})
298+
}
299+
});
300+
rafDown.value = requestAnimationFrame(animDown);
301+
}
302+
}
303+
animDown();
304+
}
305+
}
306+
307+
const donuts = computed(() => {
308+
return mutableDataset.value.map((ds, i) => {
309+
const sizeRatio = i * donutSize.value / immutableDataset.value.length;
310+
return {
311+
...ds,
204312
donut: makeDonut(
205-
{ series: ds.series.filter(serie => !segregated.value.includes(serie.id))},
313+
{ series: ds.series },
206314
svg.value.width / 2,
207315
svg.value.height / 2,
208316
donutSize.value - sizeRatio,
@@ -216,7 +324,7 @@ const mutableDataset = computed(() => {
216324
)
217325
}
218326
})
219-
})
327+
});
220328
221329
const gradientSets = computed(() => {
222330
return [...immutableDataset.value].map((ds, i) => {
@@ -239,7 +347,7 @@ const gradientSets = computed(() => {
239347
)[0]
240348
}
241349
})
242-
})
350+
});
243351
244352
const selectedDonut = ref(null);
245353
const selectedDatapoint = ref(null);
@@ -366,7 +474,7 @@ const legendSets = computed(() => {
366474
return {
367475
...s,
368476
opacity: segregated.value.includes(s.id) ? 0.5 : 1,
369-
segregate: () => segregate(s),
477+
segregate: () => segregateDonut(s),
370478
isSegregated: segregated.value.includes(s.id)
371479
}
372480
})
@@ -557,7 +665,7 @@ defineExpose({
557665
558666
<svg :xmlns="XMLNS" v-if="isDataset" :class="{ 'vue-data-ui-fullscreen--on': isFullscreen, 'vue-data-ui-fulscreen--off': !isFullscreen }" :viewBox="`0 0 ${svg.width} ${svg.height}`" :style="`max-width:100%; overflow: visible; background:${donutConfig.style.chart.backgroundColor};color:${donutConfig.style.chart.color}`">
559667
<!-- NESTED DONUTS -->
560-
<g v-for="(item, i) in mutableDataset">
668+
<g v-for="(item, i) in donuts">
561669
<g v-for="(arc, j) in item.donut">
562670
<path
563671
class="vue-ui-donut-arc-path"
@@ -598,9 +706,10 @@ defineExpose({
598706
</g>
599707
600708
<g v-if="donutConfig.style.chart.layout.labels.dataLabels.showDonutName">
601-
<g v-for="(item, i) in mutableDataset">
709+
<g v-for="(item, i) in donuts">
602710
<g v-for="(arc, j) in item.donut">
603711
<text
712+
:class="{ 'animated': donutConfig.useCssAnimation }"
604713
v-if="j === 0"
605714
:x="svg.width / 2"
606715
:y="arc.startY - donutConfig.style.chart.layout.labels.dataLabels.fontSize + donutConfig.style.chart.layout.labels.dataLabels.donutNameOffsetY"
@@ -617,9 +726,10 @@ defineExpose({
617726
618727
<!-- DATALABELS -->
619728
<g v-if="donutConfig.style.chart.layout.labels.dataLabels.show">
620-
<g v-for="(item, i) in mutableDataset">
729+
<g v-for="(item, i) in donuts">
621730
<g v-for="(arc, j) in item.donut" :filter="getBlurFilter(arc, j)">
622731
<text
732+
:class="{ 'animated': donutConfig.useCssAnimation }"
623733
v-if="isArcBigEnough(arc) && mutableConfig.dataLabels.show && donutConfig.style.chart.layout.labels.dataLabels.showPercentage"
624734
:text-anchor="calcMarkerOffsetX(arc, true).anchor"
625735
:x="calcMarkerOffsetX(arc, false, donutConfig.style.chart.layout.labels.dataLabels.offsetX).x"
@@ -631,6 +741,7 @@ defineExpose({
631741
{{ dataLabel({ v: arc.proportion * 100, s: '%', r: donutConfig.style.chart.layout.labels.dataLabels.roundingPercentage }) }}
632742
</text>
633743
<text
744+
:class="{ 'animated': donutConfig.useCssAnimation }"
634745
v-if="isArcBigEnough(arc) && mutableConfig.dataLabels.show && donutConfig.style.chart.layout.labels.dataLabels.showPercentage && donutConfig.style.chart.layout.labels.dataLabels.showValue"
635746
:text-anchor="calcMarkerOffsetX(arc, true).anchor"
636747
:x="calcMarkerOffsetX(arc, false, donutConfig.style.chart.layout.labels.dataLabels.offsetX).x"
@@ -642,6 +753,7 @@ defineExpose({
642753
({{ dataLabel({ p: donutConfig.style.chart.layout.labels.dataLabels.prefix, v: arc.value, s: donutConfig.style.chart.layout.labels.dataLabels.suffix, r: donutConfig.style.chart.layout.labels.dataLabels.roundingValue }) }})
643754
</text>
644755
<text
756+
:class="{ 'animated': donutConfig.useCssAnimation }"
645757
v-if="isArcBigEnough(arc) && mutableConfig.dataLabels.show && !donutConfig.style.chart.layout.labels.dataLabels.showPercentage && donutConfig.style.chart.layout.labels.dataLabels.showValue"
646758
:text-anchor="calcMarkerOffsetX(arc, true).anchor"
647759
:x="calcMarkerOffsetX(arc, false, donutConfig.style.chart.layout.labels.dataLabels.offsetX).x"
@@ -657,7 +769,7 @@ defineExpose({
657769
</g>
658770
659771
<!-- TOOLTIP TRAPS -->
660-
<g v-for="(item, i) in mutableDataset">
772+
<g v-for="(item, i) in donuts">
661773
<g v-for="(arc, j) in item.donut">
662774
<path
663775
data-cy-donut-trap
@@ -712,15 +824,15 @@ defineExpose({
712824
v-for="legendSet in legendSets"
713825
:legendSet="legendSet"
714826
:config="legendConfig"
715-
@clickMarker="({ legend, index }) => segregate(legend, index)"
827+
@clickMarker="({ legend }) => segregateDonut(legend)"
716828
>
717829
<template #legendTitle="{ titleSet }">
718830
<div class="vue-ui-nested-donuts-legend-title" v-if="titleSet[0].arcOf">
719831
{{ titleSet[0].arcOf }}
720832
</div>
721833
</template>
722834
<template #item="{ legend, index }">
723-
<div :data-cy="`legend-item-${index}`" @click="segregate(legend, index)" :style="`opacity:${segregated.includes(legend.id) ? 0.5 : 1}`">
835+
<div :data-cy="`legend-item-${index}`" @click="segregateDonut(legend)" :style="`opacity:${segregated.includes(legend.id) ? 0.5 : 1}`">
724836
{{ legend.name }} : {{ dataLabel({p: donutConfig.style.chart.layout.labels.dataLabels.prefix, v: legend.value, s: donutConfig.style.chart.layout.labels.dataLabels.suffix, r: donutConfig.style.chart.legend.roundingValue}) }}
725837
<template v-if="!segregated.includes(legend.id)">
726838
({{ isNaN(legend.value / legend.total) ? '-' : dataLabel({ v: legend.value / legend.total * 100, s: '%', r: donutConfig.style.chart.legend.roundingPercentage }) }})
@@ -765,7 +877,7 @@ defineExpose({
765877
position: relative;
766878
}
767879
768-
path {
880+
.animated {
769881
animation: donut 0.5s ease-in-out;
770882
transform-origin: center;
771883
}

src/lib.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1000,7 +1000,7 @@ export function generateSpiralCoordinates({ points, a, b, angleStep, startX, sta
10001000

10011001
for (let i = 0; i < points; i++) {
10021002
const theta = angleStep * i;
1003-
const r = a + b * theta; // i * 0.007 for gradual slope increase, should be a var
1003+
const r = a + b * theta;
10041004
const x = r * Math.cos(theta) + startX;
10051005
const y = r * Math.sin(theta) + startY;
10061006
coordinates.push({ x, y });

0 commit comments

Comments
 (0)