v-uixy 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div ref="wrapperRef" class="relative w-full aspect-square">
|
|
3
|
+
<svg
|
|
4
|
+
ref="svgRef"
|
|
5
|
+
width="100%"
|
|
6
|
+
height="100%"
|
|
7
|
+
:viewBox="`0 0 ${size} ${size}`"
|
|
8
|
+
role="img"
|
|
9
|
+
aria-label="Pie chart"
|
|
10
|
+
class="block select-none pointer-events-none"
|
|
11
|
+
>
|
|
12
|
+
<motion.g
|
|
13
|
+
:initial="{ transform: `rotate(0, ${center}, ${center})` }"
|
|
14
|
+
:animate="{ transform: `rotate(0, ${center}, ${center})` }"
|
|
15
|
+
:transition="groupTransition"
|
|
16
|
+
>
|
|
17
|
+
<circle
|
|
18
|
+
:cx="center"
|
|
19
|
+
:cy="center"
|
|
20
|
+
:r="radius"
|
|
21
|
+
fill="none"
|
|
22
|
+
class="stroke-gray-300 dark:stroke-gray-800"
|
|
23
|
+
:stroke-width="thickness"
|
|
24
|
+
vector-effect="non-scaling-stroke"
|
|
25
|
+
shape-rendering="geometricPrecision"
|
|
26
|
+
/>
|
|
27
|
+
<template v-if="isReady">
|
|
28
|
+
<motion.circle
|
|
29
|
+
v-for="seg in segmentsTimed"
|
|
30
|
+
:key="seg.key"
|
|
31
|
+
:initial="{
|
|
32
|
+
strokeDasharray: `0 ${circumference}`,
|
|
33
|
+
strokeDashoffset: seg.offset,
|
|
34
|
+
opacity: 0.8,
|
|
35
|
+
}"
|
|
36
|
+
:animate="{
|
|
37
|
+
strokeDasharray: `${seg.length} ${circumference}`,
|
|
38
|
+
strokeDashoffset: seg.offset,
|
|
39
|
+
opacity: 1,
|
|
40
|
+
}"
|
|
41
|
+
:transition="transitionFor(seg)"
|
|
42
|
+
:cx="center"
|
|
43
|
+
:cy="center"
|
|
44
|
+
:r="radius"
|
|
45
|
+
fill="none"
|
|
46
|
+
:stroke="seg.color"
|
|
47
|
+
:stroke-width="thickness - 2"
|
|
48
|
+
stroke-linecap="butt"
|
|
49
|
+
vector-effect="non-scaling-stroke"
|
|
50
|
+
shape-rendering="geometricPrecision"
|
|
51
|
+
/>
|
|
52
|
+
</template>
|
|
53
|
+
</motion.g>
|
|
54
|
+
</svg>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<script setup lang="ts">
|
|
59
|
+
import { computed, onMounted, onUnmounted, ref, toRefs, watch } from "vue";
|
|
60
|
+
import { motion } from "motion-v";
|
|
61
|
+
import type { UixyPieChartProps } from "./PieChart.types";
|
|
62
|
+
|
|
63
|
+
type SegBase = { key: string; length: number; offset: number; color: string };
|
|
64
|
+
type SegTimed = SegBase & { dur: number; delay: number };
|
|
65
|
+
|
|
66
|
+
const INITIAL_TOTAL_DURATION = 1.2;
|
|
67
|
+
const UPDATE_DURATION = 0.45;
|
|
68
|
+
const OVERLAP = 0.02;
|
|
69
|
+
const SIZE_EPSILON = 0.5;
|
|
70
|
+
const EASING = "linear";
|
|
71
|
+
|
|
72
|
+
const props = withDefaults(defineProps<UixyPieChartProps>(), {
|
|
73
|
+
thickness: 18,
|
|
74
|
+
animate: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const wrapperRef = ref<HTMLElement | null>(null);
|
|
78
|
+
const svgRef = ref<SVGSVGElement | null>(null);
|
|
79
|
+
const measuredSize = ref<number>(180);
|
|
80
|
+
const roRef = ref<ResizeObserver | null>(null);
|
|
81
|
+
const isReady = ref(false);
|
|
82
|
+
const initialTimer = ref<ReturnType<typeof setTimeout> | null>(null);
|
|
83
|
+
|
|
84
|
+
const size = computed(() => measuredSize.value);
|
|
85
|
+
const center = computed(() => measuredSize.value / 2);
|
|
86
|
+
const radius = computed(() =>
|
|
87
|
+
Math.max(1, measuredSize.value / 2 - props.thickness / 2 - 2)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const circumference = computed(() => 2 * Math.PI * radius.value);
|
|
91
|
+
|
|
92
|
+
const groupTransition = computed(() =>
|
|
93
|
+
!isReady.value || isInitial.value
|
|
94
|
+
? { duration: 0 }
|
|
95
|
+
: { duration: 0.35, easing: EASING }
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const total = computed(() =>
|
|
99
|
+
props.data.reduce((acc, d) => acc + Math.max(0, d.value), 0)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const segmentsBase = computed<SegBase[]>(() => {
|
|
103
|
+
const totalVal = total.value || 1;
|
|
104
|
+
let acc = 0;
|
|
105
|
+
const list: SegBase[] = [];
|
|
106
|
+
|
|
107
|
+
props.data.forEach((d, i) => {
|
|
108
|
+
const frac = Math.max(0, d.value) / totalVal;
|
|
109
|
+
const baseLen = frac * circumference.value;
|
|
110
|
+
const drawLen = Math.max(0, baseLen);
|
|
111
|
+
|
|
112
|
+
if (drawLen > 0) {
|
|
113
|
+
list.push({
|
|
114
|
+
key: `seg-${i}-${d.color ?? "c"}`,
|
|
115
|
+
length: drawLen,
|
|
116
|
+
offset: -acc,
|
|
117
|
+
color: d.color || "#999999",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
acc += baseLen;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return list;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const segmentsTimed = computed<SegTimed[]>(() => {
|
|
128
|
+
const totalDur = Math.max(0.01, INITIAL_TOTAL_DURATION);
|
|
129
|
+
let running = 0;
|
|
130
|
+
|
|
131
|
+
return segmentsBase.value.map((s) => {
|
|
132
|
+
const dur = (s.length / Math.max(1e-6, circumference.value)) * totalDur;
|
|
133
|
+
const delay = Math.max(0, running - OVERLAP);
|
|
134
|
+
const segT: SegTimed = { ...s, dur, delay };
|
|
135
|
+
running += dur;
|
|
136
|
+
return segT;
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const isInitial = ref(true);
|
|
141
|
+
|
|
142
|
+
watch(
|
|
143
|
+
() => ({ ready: isReady.value }),
|
|
144
|
+
({ ready }) => {
|
|
145
|
+
if (!ready) return;
|
|
146
|
+
|
|
147
|
+
if (initialTimer.value) return;
|
|
148
|
+
|
|
149
|
+
const totalDur = INITIAL_TOTAL_DURATION;
|
|
150
|
+
|
|
151
|
+
initialTimer.value = setTimeout(() => {
|
|
152
|
+
isInitial.value = false;
|
|
153
|
+
if (initialTimer.value) {
|
|
154
|
+
clearTimeout(initialTimer.value);
|
|
155
|
+
initialTimer.value = null;
|
|
156
|
+
}
|
|
157
|
+
}, Math.ceil(totalDur * 1000) + 16);
|
|
158
|
+
},
|
|
159
|
+
{ immediate: true }
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
onMounted(() => {
|
|
163
|
+
startResizeObserver();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
onUnmounted(() => {
|
|
167
|
+
stopResizeObserver();
|
|
168
|
+
|
|
169
|
+
if (initialTimer.value) {
|
|
170
|
+
clearTimeout(initialTimer.value);
|
|
171
|
+
initialTimer.value = null;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const transitionFor = (seg: SegTimed) => {
|
|
176
|
+
if (isInitial.value)
|
|
177
|
+
return { delay: seg.delay, duration: seg.dur, easing: EASING } as const;
|
|
178
|
+
|
|
179
|
+
return { delay: 0, duration: UPDATE_DURATION, easing: EASING };
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const measure = () => {
|
|
183
|
+
const el = wrapperRef.value ?? svgRef.value;
|
|
184
|
+
|
|
185
|
+
if (!el) return;
|
|
186
|
+
|
|
187
|
+
const rect = el.getBoundingClientRect();
|
|
188
|
+
const next = Math.max(1, Math.min(rect.width || 0, rect.height || 0));
|
|
189
|
+
|
|
190
|
+
if (Math.abs(next - measuredSize.value) > SIZE_EPSILON)
|
|
191
|
+
measuredSize.value = next;
|
|
192
|
+
|
|
193
|
+
if (!isReady.value && next > 1) isReady.value = true;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const startResizeObserver = () => {
|
|
197
|
+
if (roRef.value) return;
|
|
198
|
+
|
|
199
|
+
roRef.value = new ResizeObserver(measure);
|
|
200
|
+
|
|
201
|
+
const el = wrapperRef.value ?? svgRef.value;
|
|
202
|
+
|
|
203
|
+
if (el) roRef.value.observe(el);
|
|
204
|
+
|
|
205
|
+
requestAnimationFrame(measure);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const stopResizeObserver = () => {
|
|
209
|
+
if (!roRef.value) return;
|
|
210
|
+
|
|
211
|
+
const el = wrapperRef.value ?? svgRef.value;
|
|
212
|
+
|
|
213
|
+
if (el) roRef.value.unobserve(el);
|
|
214
|
+
|
|
215
|
+
roRef.value.disconnect();
|
|
216
|
+
roRef.value = null;
|
|
217
|
+
};
|
|
218
|
+
</script>
|