Skip to content

Commit 8e84b1b

Browse files
committed
Fix - VueUiQuickChart - Fix dasharray possible issues when animation is enabled and line is long
1 parent d2e98a5 commit 8e84b1b

File tree

2 files changed

+142
-21
lines changed

2 files changed

+142
-21
lines changed

TestingArena/ArenaVueUiQuickChart.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import { VueUiQuickChart as VueUiQuickChartTreeshaken } from "vue-data-ui/vue-ui
1111
function makeDs(m, n = 100) {
1212
const arr = [];
1313
for (let i = 0; i < m; i += 1) {
14-
arr.push(Math.random() * n)
14+
if (i > 20 && i < 40) {
15+
arr.push(null);
16+
} else {
17+
arr.push(Math.random() * n)
18+
}
1519
}
1620
return arr
1721
}

src/components/vue-ui-quick-chart.vue

Lines changed: 137 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ const segregated = ref([]);
8989
const step = ref(0);
9090
const slicerStep = ref(0);
9191
const readyTeleport = ref(false);
92+
const pathWrapper = ref(null);
93+
const pathTop = ref(null);
9294
9395
const timeLabelsEls = ref(null);
9496
const scaleLabels = ref(null);
@@ -104,6 +106,18 @@ const { loading, FINAL_DATASET, manualLoading } = useLoading({
104106
...toRefs(props),
105107
FINAL_CONFIG,
106108
prepareConfig,
109+
callback: () => {
110+
Promise.resolve().then(async () => {
111+
await nextTick();
112+
if (chartType.value === detector.chartType.LINE && FINAL_CONFIG.value.lineAnimated && !loading.value) {
113+
animateLineNow({
114+
pathDuration: 1000,
115+
pointDuration: 1200,
116+
labelDuration: 1200,
117+
});
118+
}
119+
})
120+
},
107121
skeletonDataset: [1, 2, 3, 5, 8, 13, 21, 34, 55, 89],
108122
skeletonConfig: treeShake({
109123
defaultConfig: FINAL_CONFIG.value,
@@ -287,7 +301,7 @@ watch(FINAL_CONFIG, () => {
287301
const resizeObserver = shallowRef(null);
288302
const observedEl = shallowRef(null);
289303
290-
onMounted(() => {
304+
onMounted(async () => {
291305
readyTeleport.value = true;
292306
prepareChart();
293307
})
@@ -817,7 +831,7 @@ const line = computed(() => {
817831
maxSeries: Math.max(...ds.map(d => d.values.length))
818832
};
819833
820-
const scale = calculateNiceScale(extremes.min < 0 ? extremes.min : 0, extremes.max < 0 ? 0 : extremes.max, FINAL_CONFIG.value.xyScaleSegments)
834+
const scale = extremes.max === extremes.min ? calculateNiceScale(extremes.min, extremes.min + 1, FINAL_CONFIG.value.xyScaleSegments) : calculateNiceScale(extremes.min < 0 ? extremes.min : 0, extremes.max < 0 ? 0 : extremes.max, FINAL_CONFIG.value.xyScaleSegments)
821835
const absoluteMin = extremes.min < 0 ? Math.abs(extremes.min) : 0;
822836
const absoluteZero = extremes.max < 0 ? drawingArea.top : drawingArea.bottom - (absoluteMin / (scale.max + absoluteMin) * drawingArea.height)
823837
const slotSize = drawingArea.width / extremes.maxSeries;
@@ -1018,7 +1032,7 @@ const bar = computed(() => {
10181032
maxSeries: Math.max(...ds.filter(d => !segregated.value.includes(d.id)).map(d => d.values.length))
10191033
}
10201034
1021-
const scale = calculateNiceScale(extremes.min < 0 ? extremes.min : 0, extremes.max, FINAL_CONFIG.value.xyScaleSegments)
1035+
const scale = extremes.min === extremes.max ? calculateNiceScale(extremes.min, extremes.min + 1, FINAL_CONFIG.value.xyScaleSegments) : calculateNiceScale(extremes.min < 0 ? extremes.min : 0, extremes.max, FINAL_CONFIG.value.xyScaleSegments)
10221036
const absoluteMin = scale.min < 0 ? Math.abs(scale.min) : 0;
10231037
const absoluteZero = drawingArea.bottom - (absoluteMin / (scale.max + absoluteMin) * drawingArea.height)
10241038
const slotSize = drawingArea.width / extremes.maxSeries;
@@ -1173,6 +1187,116 @@ const bar = computed(() => {
11731187
}
11741188
});
11751189
1190+
function primePath(p) {
1191+
if (!p) return;
1192+
const len = p.getTotalLength();
1193+
p.style.transition = 'none';
1194+
p.style.strokeDasharray = `${len}`;
1195+
p.style.strokeDashoffset = `${len}`;
1196+
}
1197+
1198+
function primeRevealables(els, { fromOpacity='0', fromScale='0.85' } = {}) {
1199+
els.forEach(el => {
1200+
el.style.animation = 'none';
1201+
el.style.transition = 'none';
1202+
el.style.opacity = fromOpacity;
1203+
el.style.transform = `scale(${fromScale})`;
1204+
el.style.transformBox = 'fill-box';
1205+
el.style.transformOrigin = '50% 50%';
1206+
});
1207+
}
1208+
1209+
function getXFromCircle(el) {
1210+
return el.cx?.baseVal?.value ?? parseFloat(el.getAttribute('cx'));
1211+
}
1212+
function getXFromText(el) {
1213+
const xAttr = el.getAttribute('x');
1214+
if (xAttr != null) return parseFloat(xAttr);
1215+
const ctm = el.getCTM?.();
1216+
return ctm ? ctm.e : 0;
1217+
}
1218+
1219+
function bucketByXTolerance(elems, getX) {
1220+
if (!elems.length) return [];
1221+
const withX = elems.map(el => ({ el, x: getX(el) })).filter(o => Number.isFinite(o.x));
1222+
withX.sort((a, b) => a.x - b.x);
1223+
1224+
let minGap = Infinity;
1225+
for (let i = 1; i < withX.length; i++) {
1226+
const d = withX[i].x - withX[i - 1].x;
1227+
if (d > 0 && d < minGap) minGap = d;
1228+
}
1229+
const tol = (minGap === Infinity ? 1 : minGap) / 2;
1230+
1231+
const buckets = [];
1232+
let current = { x: withX[0].x, items: [withX[0].el] };
1233+
for (let i = 1; i < withX.length; i++) {
1234+
const { x, el } = withX[i];
1235+
if (Math.abs(x - current.x) <= tol) {
1236+
current.items.push(el);
1237+
} else {
1238+
buckets.push(current);
1239+
current = { x, items: [el] };
1240+
}
1241+
}
1242+
buckets.push(current);
1243+
return buckets;
1244+
}
1245+
1246+
function animateLineNow({
1247+
pathDuration,
1248+
pathEasing = 'ease-in-out',
1249+
pointDuration,
1250+
labelDuration,
1251+
pointDelay = 0,
1252+
labelDelay = 0,
1253+
pointStep = 0,
1254+
labelStep = 0,
1255+
intraSeriesStep = 0
1256+
} = {}) {
1257+
const wrappers = Array.isArray(pathWrapper.value) ? pathWrapper.value : [pathWrapper.value].filter(Boolean);
1258+
const tops = Array.isArray(pathTop.value) ? pathTop.value : [pathTop.value].filter(Boolean);
1259+
const paths = [...wrappers, ...tops].filter(Boolean);
1260+
const root = quickChart.value;
1261+
const points = Array.from(root.querySelectorAll('.vue-ui-quick-chart-plot'));
1262+
const labels = Array.from(root.querySelectorAll('.vue-ui-quick-chart-label'));
1263+
paths.forEach(primePath);
1264+
primeRevealables(points, { fromOpacity:'0', fromScale:'0.75' });
1265+
primeRevealables(labels, { fromOpacity:'0', fromScale:'0.98' });
1266+
points.forEach(el => el.classList.remove('quick-animation'));
1267+
labels.forEach(el => el.classList.remove('quick-animation'));
1268+
void root.offsetWidth;
1269+
const pointCols = bucketByXTolerance(points, getXFromCircle);
1270+
const labelCols = bucketByXTolerance(labels, getXFromText);
1271+
1272+
requestAnimationFrame(() => {
1273+
requestAnimationFrame(() => {
1274+
paths.forEach(p => {
1275+
p.style.transition = `stroke-dashoffset ${pathDuration}ms ${pathEasing}`;
1276+
p.style.strokeDashoffset = '0';
1277+
});
1278+
1279+
pointCols.forEach((col, colIndex) => {
1280+
col.items.forEach((el, k) => {
1281+
const delay = pointDelay + colIndex * pointStep + k * intraSeriesStep;
1282+
el.style.transition = `opacity ${pointDuration}ms ease-out ${delay}ms, transform ${pointDuration}ms ease-out ${delay}ms`;
1283+
el.style.opacity = '1';
1284+
el.style.transform = 'scale(1)';
1285+
});
1286+
});
1287+
1288+
labelCols.forEach((col, colIndex) => {
1289+
col.items.forEach((el, k) => {
1290+
const delay = labelDelay + colIndex * labelStep + k * intraSeriesStep;
1291+
el.style.transition = `opacity ${labelDuration}ms ease-out ${delay}ms, transform ${labelDuration}ms ease-out ${delay}ms`;
1292+
el.style.opacity = '1';
1293+
el.style.transform = 'scale(1)';
1294+
});
1295+
});
1296+
});
1297+
});
1298+
}
1299+
11761300
const allMinimaps = computed(() => {
11771301
if (chartType.value === detector.chartType.LINE) {
11781302
return line.value.legend.map(ds => {
@@ -1678,29 +1802,32 @@ defineExpose({
16781802
<g class="line-plot-series">
16791803
<template v-if="FINAL_CONFIG.lineSmooth">
16801804
<path
1805+
ref="pathWrapper"
16811806
data-cy="datapoint-line-wrapper"
16821807
:d="`M ${createSmoothPath(ds.coordinates)}`"
16831808
:stroke="FINAL_CONFIG.backgroundColor"
16841809
:stroke-width="FINAL_CONFIG.lineStrokeWidth + 1"
16851810
stroke-linecap="round"
16861811
fill="none"
16871812
:class="{'quick-animation': !loading, 'vue-data-ui-line-animated': FINAL_CONFIG.lineAnimated && !loading }"
1688-
:style="{ transition: loading ? undefined : 'all 0.3s ease-in-out' }"
1813+
:style="{ transition: loading ? undefined : 'all 0.3s ease-in-out'}"
16891814
/>
16901815
<path
1816+
ref="pathTop"
16911817
data-cy="datapoint-line"
16921818
:d="`M ${createSmoothPath(ds.coordinates)}`"
16931819
:stroke="ds.color"
16941820
:stroke-width="FINAL_CONFIG.lineStrokeWidth"
16951821
stroke-linecap="round"
16961822
fill="none"
16971823
:class="{'quick-animation': !loading, 'vue-data-ui-line-animated': FINAL_CONFIG.lineAnimated && !loading }"
1698-
:style="{ transition: loading ? undefined : 'all 0.3s ease-in-out' }"
1824+
:style="{ transition: loading ? undefined : 'all 0.3s ease-in-out'}"
16991825
>
17001826
</path>
17011827
</template>
17021828
<template v-else>
17031829
<path
1830+
ref="pathWrapper"
17041831
data-cy="datapoint-line-wrapper"
17051832
:d="`M ${ds.linePath}`"
17061833
:stroke="FINAL_CONFIG.backgroundColor"
@@ -1711,14 +1838,15 @@ defineExpose({
17111838
:style="{ transition: loading ? undefined : 'all 0.3s ease-in-out' }"
17121839
/>
17131840
<path
1841+
ref="pathTop"
17141842
data-cy="datapoint-line"
17151843
:d="`M ${ds.linePath}`"
17161844
:stroke="ds.color"
17171845
:stroke-width="FINAL_CONFIG.lineStrokeWidth"
17181846
stroke-linecap="round"
17191847
fill="none"
17201848
:class="{'quick-animation': !loading, 'vue-data-ui-line-animated': FINAL_CONFIG.lineAnimated && !loading }"
1721-
:style="{ transition: loading ? undefined : 'all 0.3s ease-in-out' }"
1849+
:style="{ transition: loading ? undefined : 'all 0.3s ease-in-out'}"
17221850
/>
17231851
</template>
17241852
<template v-for="(plot, j) in ds.coordinates">
@@ -1730,7 +1858,7 @@ defineExpose({
17301858
:fill="ds.color"
17311859
:stroke="FINAL_CONFIG.backgroundColor"
17321860
stroke-width="0.5"
1733-
:class="{ 'quick-animation': !loading }"
1861+
:class="{ 'vue-ui-quick-chart-plot': true, 'quick-animation': !loading }"
17341862
:style="{ transition: loading ? undefined : 'all 0.3s ease-in-out' }"
17351863
/>
17361864
</template>
@@ -1747,7 +1875,7 @@ defineExpose({
17471875
:fill="ds.color"
17481876
:x="plot.x"
17491877
:y="checkNaN(plot.y) - FINAL_CONFIG.dataLabelFontSize / 2"
1750-
class="quick-animation"
1878+
:class="{ 'vue-ui-quick-chart-label': true, 'quick-animation': !loading }"
17511879
:style="{ transition: loading ? undefined : 'all 0.3s ease-in-out' }"
17521880
>
17531881
{{ applyDataLabel(
@@ -2276,6 +2404,7 @@ defineExpose({
22762404
animation: quick 0.5s ease-in-out;
22772405
transform-origin: center;
22782406
}
2407+
22792408
@keyframes quick {
22802409
0% {
22812410
transform: scale(0.9,0.9);
@@ -2291,18 +2420,6 @@ defineExpose({
22912420
}
22922421
}
22932422
2294-
.vue-data-ui-line-animated {
2295-
stroke-dasharray: 2000;
2296-
stroke-dashoffset: 2000;
2297-
animation: vueDataUiLineAnimation 0.5s cubic-bezier(0.790, 0.210, 0.790, 0.210) forwards;
2298-
}
2299-
2300-
@keyframes vueDataUiLineAnimation {
2301-
to {
2302-
stroke-dashoffset: 0;
2303-
}
2304-
}
2305-
23062423
.vue-data-ui-bar-animated {
23072424
animation: vueDataUiBarAnimation 0.5s cubic-bezier(0.790, 0.210, 0.790, 0.210) forwards;
23082425
}

0 commit comments

Comments
 (0)