Skip to content

Commit 9fb2a8d

Browse files
committed
Improvement - VueUiDonut - Improve easing animation on serie segregation; update related config
1 parent 4ad77b3 commit 9fb2a8d

File tree

5 files changed

+120
-97
lines changed

5 files changed

+120
-97
lines changed

TestingArena/ArenaVueUiDonut.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ function alterDataset() {
6666
}
6767
6868
const model = ref([
69-
{ key: 'type', def: 'classic', type: 'select', options: ['classic', 'polar']},
70-
{ key: 'useSerieToggleAnimation', def: true, type: 'checkbox'},
69+
{ key: 'type', def: 'polar', type: 'select', options: ['classic', 'polar']},
70+
{ key: 'serieToggleAnimation.show', def: true, type: 'checkbox'},
71+
{ key: 'serieToggleAnimation.durationMs', def: 500, type: 'number', min: 0, max: 5000, step: 100},
7172
{ key: 'loadAnimation.show', def: true, type: 'checkbox'},
7273
{ key: 'loadAnimation.durationMs', def: 1000, type: 'number', min: 0, max: 5000, step: 500},
7374
{ key: 'loadAnimation.staggerMs', def: 50, type: 'number', min: 0, max: 1000, step: 25},

src/components/vue-ui-donut.vue

Lines changed: 104 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
createUid,
1313
dataLabel,
1414
downloadCsv,
15+
easeOutCubic,
1516
error,
1617
getMissingDatasetAttributes,
1718
isFunction,
@@ -171,10 +172,6 @@ const FINAL_CONFIG = computed({
171172
const isFirstLoad = ref(true);
172173
const animatedValues = ref([]);
173174
174-
function easeOutCubic(t) {
175-
return 1 - Math.pow(1 - t, 3);
176-
}
177-
178175
function animateWithGhost(finalValues, duration = 1000, stagger = 50) {
179176
return new Promise(resolve => {
180177
const N = finalValues.length;
@@ -280,7 +277,6 @@ const donutThickness = computed(() => {
280277
281278
const emit = defineEmits(['selectLegend', 'selectDatapoint'])
282279
283-
284280
const immutableSet = computed(() => {
285281
return props.dataset
286282
.map((serie, i) => {
@@ -317,106 +313,102 @@ const rafUp = ref(null);
317313
const rafDown = ref(null);
318314
const isAnimating = ref(false);
319315
316+
function animateValue({ from, to, duration, onUpdate, onDone, easing = easeOutCubic }) {
317+
const start = performance.now();
318+
function step(now) {
319+
const t = Math.min((now - start) / duration, 1);
320+
const eased = easing(t);
321+
const current = from + (to - from) * eased;
322+
onUpdate(current, t);
323+
if (t < 1) {
324+
requestAnimationFrame(step);
325+
} else {
326+
onUpdate(to, 1);
327+
if (onDone) onDone();
328+
}
329+
}
330+
requestAnimationFrame(step);
331+
}
332+
320333
function segregate(index) {
321-
const target = immutableSet.value.find((_, idx) => idx === index)
322-
const source = mutableSet.value.find((_, idx) => idx === index)
334+
const target = immutableSet.value.find((_, idx) => idx === index);
335+
const source = mutableSet.value.find((_, idx) => idx === index);
323336
let initVal = source.value;
337+
324338
if (segregated.value.includes(index)) {
325339
segregated.value = segregated.value.filter(s => s !== index);
326340
const targetVal = target.value;
327341
328342
function setFinalUpState() {
329-
mutableSet.value = mutableSet.value.map((ds, i) => {
330-
if (index === i) {
331-
return {
332-
...ds,
333-
value: targetVal
334-
}
335-
} else {
336-
return ds
337-
}
338-
});
343+
mutableSet.value = mutableSet.value.map((ds, i) =>
344+
index === i ? { ...ds, value: targetVal } : ds
345+
);
339346
}
340347
341-
function animUp() {
342-
if (initVal > targetVal) {
343-
cancelAnimationFrame(rafUp.value);
344-
setFinalUpState();
345-
isAnimating.value = false;
346-
} else {
347-
isAnimating.value = true;
348-
initVal += (targetVal * (FINAL_CONFIG.value.type === 'polar' ? 1 : 0.045));
349-
mutableSet.value = mutableSet.value.map((ds, i) => {
350-
if ((index === i)) {
351-
return {
352-
...ds,
353-
value: initVal
354-
}
355-
} else {
356-
return ds
357-
}
358-
})
359-
rafUp.value = requestAnimationFrame(animUp)
360-
}
348+
function doAnimUp() {
349+
isAnimating.value = true;
350+
animateValue({
351+
from: initVal,
352+
to: targetVal,
353+
duration: FINAL_CONFIG.value.serieToggleAnimation.durationMs,
354+
onUpdate: (val, t) => {
355+
mutableSet.value = mutableSet.value.map((ds, i) =>
356+
index === i ? { ...ds, value: val } : ds
357+
);
358+
},
359+
onDone: () => {
360+
setFinalUpState();
361+
isAnimating.value = false;
362+
}
363+
});
361364
}
362365
363-
if (FINAL_CONFIG.value.useSerieToggleAnimation) {
364-
animUp();
366+
if (FINAL_CONFIG.value.serieToggleAnimation.show && FINAL_CONFIG.value.type === 'classic') {
367+
doAnimUp();
365368
} else {
366369
setFinalUpState();
367370
}
368-
369371
} else if (segregated.value.length < immutableSet.value.length - 1) {
370372
function setFinalDownState() {
371373
segregated.value.push(index);
372-
mutableSet.value = mutableSet.value.map((ds, i) => {
373-
if (index === i) {
374-
return {
375-
...ds,
376-
value: 0,
377-
}
378-
} else {
379-
return ds;
380-
}
381-
});
374+
mutableSet.value = mutableSet.value.map((ds, i) =>
375+
index === i ? { ...ds, value: 0 } : ds
376+
);
382377
}
383378
384-
function animDown() {
385-
if (initVal < source.value / 100) {
386-
cancelAnimationFrame(rafDown.value);
387-
setFinalDownState();
388-
isAnimating.value = false;
389-
} else {
390-
isAnimating.value = true;
391-
initVal /= (FINAL_CONFIG.value.type === 'polar' ? 20 : 1.18);
392-
mutableSet.value = mutableSet.value.map((ds, i) => {
393-
if (index === i) {
394-
return {
395-
...ds,
396-
value: initVal
397-
}
398-
} else {
399-
return ds;
400-
}
401-
})
402-
rafDown.value = requestAnimationFrame(animDown);
403-
}
379+
function doAnimDown() {
380+
isAnimating.value = true;
381+
animateValue({
382+
from: initVal,
383+
to: 0,
384+
duration: FINAL_CONFIG.value.serieToggleAnimation.durationMs,
385+
onUpdate: (val, t) => {
386+
mutableSet.value = mutableSet.value.map((ds, i) =>
387+
index === i ? { ...ds, value: val } : ds
388+
);
389+
},
390+
onDone: () => {
391+
setFinalDownState();
392+
isAnimating.value = false;
393+
}
394+
});
404395
}
405-
if (FINAL_CONFIG.value.useSerieToggleAnimation) {
406-
animDown();
396+
397+
if (FINAL_CONFIG.value.serieToggleAnimation.show && FINAL_CONFIG.value.type === 'classic') {
398+
doAnimDown();
407399
} else {
408400
setFinalDownState();
409401
}
410402
}
411-
emit('selectLegend', donutSet.value.map(ds => {
412-
return {
413-
name: ds.name,
414-
color: ds.color,
415-
value: ds.value
416-
}
417-
}));
403+
404+
emit('selectLegend', donutSet.value.map(ds => ({
405+
name: ds.name,
406+
color: ds.color,
407+
value: ds.value
408+
})));
418409
}
419410
411+
420412
const _total = computed(() => props.dataset.reduce((sum, ds) => sum + ds.values.reduce((a, b) => a + b, 0), 0));
421413
422414
const donutSet = computed(() => {
@@ -922,14 +914,18 @@ defineExpose({
922914
</template>
923915
<template v-if="FINAL_CONFIG.type === 'polar'">
924916
<g v-for="(arc, i) in currentDonut.filter(el => !el.ghost)">
925-
<line data-cy="polar-datapoint" v-if="isArcBigEnough(arc) && mutableConfig.dataLabels.show"
926-
:x1="offsetFromCenterPoint({ initX: polarAreas[i].middlePoint.x, initY: polarAreas[i].middlePoint.y, offset: 24, centerX: svg.width / 2, centerY: svg.height / 2 }).x"
927-
:y1="offsetFromCenterPoint({ initX: polarAreas[i].middlePoint.x, initY: polarAreas[i].middlePoint.y, offset: 24, centerX: svg.width / 2, centerY: svg.height / 2 }).y"
928-
:x2="polarAreas[i].middlePoint.x" :y2="polarAreas[i].middlePoint.y" :stroke="arc.color"
929-
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" fill="none"
917+
<path
918+
data-cy="polar-datapoint"
919+
v-if="isArcBigEnough(arc) && mutableConfig.dataLabels.show"
920+
:d="`M ${offsetFromCenterPoint({ initX: polarAreas[i].middlePoint.x, initY: polarAreas[i].middlePoint.y, offset: 24, centerX: svg.width / 2, centerY: svg.height / 2 }).x},${offsetFromCenterPoint({ initX: polarAreas[i].middlePoint.x, initY: polarAreas[i].middlePoint.y, offset: 24, centerX: svg.width / 2, centerY: svg.height / 2 }).y} ${polarAreas[i].middlePoint.x},${polarAreas[i].middlePoint.y}`"
921+
:stroke="arc.color"
922+
stroke-width="1"
923+
stroke-linecap="round"
924+
stroke-linejoin="round"
925+
fill="none"
930926
:filter="getBlurFilter(i)"
931927
:style="{
932-
transition: isFirstLoad ? '' : 'all 0.1s ease-in-out'
928+
transition: isFirstLoad || !FINAL_CONFIG.serieToggleAnimation.show ? 'none' : `all ${FINAL_CONFIG.serieToggleAnimation.durationMs}ms ease-in-out`,
933929
}"
934930
/>
935931
</g>
@@ -969,7 +965,7 @@ defineExpose({
969965
<path v-for="(arc, i) in noGhostDonut" :stroke="FINAL_CONFIG.style.chart.backgroundColor"
970966
:d="polarAreas[i].path" fill="#FFFFFF"
971967
:style="{
972-
transition: isFirstLoad ? 'none' : 'all 0.1s ease-in-out'
968+
transition: isFirstLoad || !FINAL_CONFIG.serieToggleAnimation.show ? 'none' : `all ${FINAL_CONFIG.serieToggleAnimation.durationMs}ms ease-in-out`
973969
}"
974970
/>
975971
<g v-if="FINAL_CONFIG.style.chart.layout.donut.useShadow">
@@ -979,7 +975,7 @@ defineExpose({
979975
:stroke-width="FINAL_CONFIG.style.chart.layout.donut.borderWidth"
980976
:filter="`url(#drop_shadow_${uid})`"
981977
:style="{
982-
transition: isFirstLoad ? 'none' : 'all 0.1s ease-in-out'
978+
transition: isFirstLoad || !FINAL_CONFIG.serieToggleAnimation.show ? 'none' : `all ${FINAL_CONFIG.serieToggleAnimation.durationMs}ms ease-in-out`
983979
}"
984980
/>
985981
</g>
@@ -992,7 +988,7 @@ defineExpose({
992988
:stroke-width="FINAL_CONFIG.style.chart.layout.donut.borderWidth"
993989
:filter="getBlurFilter(i)"
994990
:style="{
995-
transition: isFirstLoad ? 'none' : 'all 0.1s ease-in-out'
991+
transition: isFirstLoad || !FINAL_CONFIG.serieToggleAnimation.show ? 'none' : `all ${FINAL_CONFIG.serieToggleAnimation.durationMs}ms ease-in-out`
996992
}"
997993
/>
998994
</g>
@@ -1002,7 +998,7 @@ defineExpose({
1002998
:stroke="FINAL_CONFIG.style.chart.backgroundColor"
1003999
:stroke-width="FINAL_CONFIG.style.chart.layout.donut.borderWidth" :filter="getBlurFilter(i)"
10041000
:style="{
1005-
transition: isFirstLoad ? 'none' : 'all 0.1s ease-in-out'
1001+
transition: isFirstLoad || !FINAL_CONFIG.serieToggleAnimation.show ? 'none' : `all ${FINAL_CONFIG.serieToggleAnimation.durationMs}ms ease-in-out`
10061002
}"
10071003
/>
10081004
</g>
@@ -1145,7 +1141,11 @@ defineExpose({
11451141
:fill="arc.color" :stroke="FINAL_CONFIG.style.chart.backgroundColor" :stroke-width="1"
11461142
:r="3"
11471143
:filter="!FINAL_CONFIG.useBlurOnHover || [null, undefined].includes(selectedSerie) || selectedSerie === i ? `` : `url(#blur_${uid})`"
1148-
@click="selectDatapoint(arc, i)" />
1144+
@click="selectDatapoint(arc, i)"
1145+
:style="{
1146+
transition: isFirstLoad || !FINAL_CONFIG.serieToggleAnimation.show ? 'none' : `all ${FINAL_CONFIG.serieToggleAnimation.durationMs}ms ease-in-out`
1147+
}"
1148+
/>
11491149
</template>
11501150
<template v-if="FINAL_CONFIG.type === 'classic'">
11511151
<text data-cy="donut-label-value" v-if="isArcBigEnough(arc) && mutableConfig.dataLabels.show"
@@ -1185,7 +1185,10 @@ defineExpose({
11851185
:y="offsetFromCenterPoint({ initX: polarAreas[i].middlePoint.x, initY: polarAreas[i].middlePoint.y, offset: 42, centerX: svg.width / 2, centerY: svg.height / 2 }).y"
11861186
:fill="FINAL_CONFIG.style.chart.layout.labels.percentage.color"
11871187
:font-size="FINAL_CONFIG.style.chart.layout.labels.percentage.fontSize"
1188-
:style="`transition: all 0.1s ease-in-out; font-weight:${FINAL_CONFIG.style.chart.layout.labels.percentage.bold ? 'bold' : ''}`"
1188+
:style="{
1189+
transition: isFirstLoad || !FINAL_CONFIG.serieToggleAnimation.show ? 'none' : `all ${FINAL_CONFIG.serieToggleAnimation.durationMs}ms ease-in-out`,
1190+
fontWeight: FINAL_CONFIG.style.chart.layout.labels.percentage.bold ? 'bold': 'normal'
1191+
}"
11891192
@click="selectDatapoint(arc, i)">
11901193
{{ displayArcPercentage(arc, noGhostDonut) }} {{
11911194
FINAL_CONFIG.style.chart.layout.labels.value.show ? `(${applyDataLabel(
@@ -1206,7 +1209,10 @@ defineExpose({
12061209
:y="offsetFromCenterPoint({ initX: polarAreas[i].middlePoint.x, initY: polarAreas[i].middlePoint.y, offset: 42, centerX: svg.width / 2, centerY: svg.height / 2 }).y + FINAL_CONFIG.style.chart.layout.labels.percentage.fontSize"
12071210
:fill="FINAL_CONFIG.style.chart.layout.labels.name.color"
12081211
:font-size="FINAL_CONFIG.style.chart.layout.labels.name.fontSize"
1209-
:style="`transition: all 0.1s ease-in-out; font-weight:${FINAL_CONFIG.style.chart.layout.labels.name.bold ? 'bold' : ''}`"
1212+
:style="{
1213+
transition: isFirstLoad || !FINAL_CONFIG.serieToggleAnimation.show ? 'none' : `all ${FINAL_CONFIG.serieToggleAnimation.durationMs}ms ease-in-out`,
1214+
fontWeight: FINAL_CONFIG.style.chart.layout.labels.name.bold ? 'bold': 'normal'
1215+
}"
12101216
@click="selectDatapoint(arc, i)">
12111217
{{ arc.name }}
12121218
</text>
@@ -1228,7 +1234,12 @@ defineExpose({
12281234
:x="FINAL_CONFIG.style.chart.comments.offsetX + (getPolarAnchor(polarAreas[i].middlePoint) === 'end' ? offsetFromCenterPoint({ initX: polarAreas[i].middlePoint.x, initY: polarAreas[i].middlePoint.y, offset: 42, centerX: svg.width / 2, centerY: svg.height / 2 }).x - FINAL_CONFIG.style.chart.comments.width : getPolarAnchor(polarAreas[i].middlePoint) === 'middle' ? offsetFromCenterPoint({ initX: polarAreas[i].middlePoint.x, initY: polarAreas[i].middlePoint.y, offset: 42, centerX: svg.width / 2, centerY: svg.height / 2 }).x - (FINAL_CONFIG.style.chart.comments.width / 2) : offsetFromCenterPoint({ initX: polarAreas[i].middlePoint.x, initY: polarAreas[i].middlePoint.y, offset: 42, centerX: svg.width / 2, centerY: svg.height / 2 }).x)"
12291235
:y="getPolarCommentY(polarAreas[i]) + FINAL_CONFIG.style.chart.comments.offsetY"
12301236
:width="FINAL_CONFIG.style.chart.comments.width" height="200"
1231-
style="overflow: visible; pointer-events: none">
1237+
:style="{
1238+
transition: isFirstLoad || !FINAL_CONFIG.serieToggleAnimation.show ? 'none' : `all ${FINAL_CONFIG.serieToggleAnimation.durationMs}ms ease-in-out`,
1239+
overflow: 'visible',
1240+
pointerEvents: 'none'
1241+
}"
1242+
>
12321243
<div>
12331244
<slot name="plot-comment"
12341245
:plot="{ ...arc, textAlign: getPolarAnchor(polarAreas[i].middlePoint), flexAlign: getPolarAnchor(polarAreas[i].middlePoint), isFirstLoad }" />

src/lib.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2506,6 +2506,10 @@ export function deepEmptyObjectToNull(value) {
25062506
return value;
25072507
}
25082508

2509+
export function easeOutCubic(t) {
2510+
return 1 - Math.pow(1 - t, 3);
2511+
}
2512+
25092513

25102514
const lib = {
25112515
XMLNS,
@@ -2550,6 +2554,7 @@ const lib = {
25502554
deepEmptyObjectToNull,
25512555
degreesToRadians,
25522556
downloadCsv,
2557+
easeOutCubic,
25532558
emptyObjectToNull,
25542559
error,
25552560
forceValidValue,

src/useConfig.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -622,7 +622,10 @@ export function useConfig() {
622622
theme: '',
623623
customPalette: [],
624624
useCssAnimation: true,
625-
useSerieToggleAnimation: true,
625+
serieToggleAnimation: {
626+
show: true,
627+
durationMs: 500,
628+
},
626629
startAnimation: {
627630
show: true,
628631
durationMs: 1000,

types/vue-data-ui.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2925,7 +2925,10 @@ declare module "vue-data-ui" {
29252925
customPalette?: string[];
29262926
useBlurOnHover?: boolean;
29272927
useCssAnimation?: boolean;
2928-
useSerieToggleAnimation?: boolean;
2928+
serieToggleAnimation?: {
2929+
show?: boolean;
2930+
durationMs?: number;
2931+
};
29292932
startAnimation?: {
29302933
show?: boolean;
29312934
durationMs?: number;

0 commit comments

Comments
 (0)