1616 />
1717 </clipPath >
1818 </defs >
19+
20+ <!-- Empty State -->
21+ <template v-if =" ! hasNonZeroValues " >
22+ <circle
23+ :cx =" cx"
24+ :cy =" cy"
25+ :r =" radius"
26+ :stroke-width =" thickness"
27+ stroke =" #f4f4f6"
28+ fill =" transparent"
29+ />
30+ <text
31+ :x =" cx"
32+ :y =" cy"
33+ text-anchor =" middle"
34+ style =" font-size : 5px ; fill : #a1abb4 "
35+ >
36+ No Expenses
37+ </text >
38+ </template >
39+
40+ <!-- Single Sector -->
1941 <circle
20- v-if =" thetasAndStarts.length === 1 || thetasAndStarts.length === 0 "
42+ v-else- if =" thetasAndStarts.length === 1"
2143 clip-path =" url(#donut-hole)"
2244 :cx =" cx"
2345 :cy =" cy"
2446 :r =" radius"
25- :stroke-width ="
26- thickness +
27- (hasNonZeroValues && active === thetasAndStarts[0][0] ? 4 : 0)
28- "
29- :stroke ="
30- hasNonZeroValues ? sectors[thetasAndStarts[0][0]].color : '#f4f4f6'
31- "
32- :class =" hasNonZeroValues ? 'sector' : ''"
47+ :stroke-width =" thickness + (active === thetasAndStarts[0][0] ? 4 : 0)"
48+ :stroke =" sectors[thetasAndStarts[0][0]].color"
49+ :class =" 'sector'"
3350 :style =" { transformOrigin: `${cx}px ${cy}px` }"
3451 fill =" transparent"
35- @mouseover ="
36- $emit(
37- 'change',
38- thetasAndStarts.length === 1 ? thetasAndStarts[0][0] : null
39- )
40- "
52+ @mouseover =" $emit('change', thetasAndStarts[0][0])"
4153 />
42- <template v-if =" thetasAndStarts .length > 1 " >
54+
55+ <!-- Multiple Sectors -->
56+ <template v-else >
4357 <path
4458 v-for =" [i, theta, start_] in thetasAndStarts"
4559 :key =" i"
5367 @mouseover =" $emit('change', i)"
5468 />
5569 </template >
70+
71+ <!-- Center Value -->
5672 <text
73+ v-if =" hasNonZeroValues"
5774 :x =" cx"
5875 :y =" cy"
5976 text-anchor =" middle"
6784 valueFormatter(
6885 active !== null && sectors.length !== 0
6986 ? sectors[active].value
70- : totalValue,
71- 'Currency'
87+ : totalValue
7288 )
7389 }}
7490 </text >
91+
92+ <!-- Center Label -->
7593 <text
94+ v-if =" hasNonZeroValues"
7695 :x =" cx"
7796 :y =" cy + 8"
7897 text-anchor =" middle"
91110<script >
92111export default {
93112 props: {
94- sectors: {
95- default : () => [],
96- type: Array ,
97- },
113+ sectors: { default : () => [], type: Array },
98114 totalLabel: { default: ' Total' , type: String },
99115 radius: { default: 36 , type: Number },
100116 startAngle: { default: Math .PI , type: Number },
101117 thickness: { default: 10 , type: Number },
102118 active: { default: null , type: Number },
103- valueFormatter: { default : (v ) => v .toString (), Function },
119+ valueFormatter: { default : (v ) => v .toString (), type : Function },
104120 offsetX: { default: 0 , type: Number },
105121 offsetY: { default: 0 , type: Number },
106- textOffsetX: { default: 0 , type: Number },
107- textOffsetY: { default: 0 , type: Number },
108122 darkMode: { type: Boolean , default: false },
109123 },
110124 emits: [' change' ],
@@ -116,51 +130,49 @@ export default {
116130 return 50 + this .offsetY ;
117131 },
118132 totalValue () {
119- return this .sectors .map (({ value }) => value).reduce ((a , b ) => a + b, 0 );
133+ const total = this .sectors .reduce (
134+ (sum , { value }) => sum + (value || 0 ),
135+ 0
136+ );
137+ return total > 0 ? total : 0 ;
120138 },
121139 thetasAndStarts () {
140+ if (this .totalValue === 0 ) return [];
122141 const thetas = this .sectors
123142 .map (({ value }, i ) => ({
124143 value: (2 * Math .PI * value) / this .totalValue ,
125- filterOut : value !== 0 ,
144+ valid : value > 0 ,
126145 i,
127146 }))
128- .filter (({ filterOut }) => filterOut );
147+ .filter (({ valid }) => valid );
129148
130- const starts = [ ... thetas .map (({ value }) => value)] ;
131- starts .forEach (({ value } , i ) => {
149+ const starts = thetas .map (({ value }) => value);
150+ starts .forEach ((_ , i ) => {
132151 starts[i] += starts[i - 1 ] ?? 0 ;
133152 });
134-
135153 starts .unshift (0 );
136154 starts .pop ();
137155
138156 return thetas .map ((t , i ) => [t .i , t .value , starts[i]]);
139157 },
140158 hasNonZeroValues () {
141- return this .thetasAndStarts . some (( t ) => this . sectors [t[ 0 ]]. value !== 0 ) ;
159+ return this .totalValue > 0 ;
142160 },
143161 },
144162 methods: {
145- getArcPath (... args ) {
146- let [cx, cy, r, start, theta] = args .map (parseFloat);
147-
148- start += parseFloat (this .startAngle );
163+ getArcPath (cx , cy , r , start , theta ) {
164+ start += this .startAngle ;
149165 const startX = cx + r * Math .cos (start);
150166 const startY = cy + r * Math .sin (start);
151167 const endX = cx + r * Math .cos (start + theta);
152168 const endY = cy + r * Math .sin (start + theta);
153169 const largeArcFlag = theta > Math .PI ? 1 : 0 ;
154- const sweepFlag = 1 ;
155-
156- return ` M ${ startX} ${ startY} A ${ r} ${ r} 0 ${ largeArcFlag} ${ sweepFlag} ${ endX} ${ endY} ` ;
170+ return ` M ${ startX} ${ startY} A ${ r} ${ r} 0 ${ largeArcFlag} 1 ${ endX} ${ endY} ` ;
157171 },
158172 getSectorColor (index ) {
159- if (this .darkMode ) {
160- return this .sectors [index].color .darkColor ;
161- } else {
162- return this .sectors [index].color .color ;
163- }
173+ return this .darkMode
174+ ? this .sectors [index].color .darkColor
175+ : this .sectors [index].color .color ;
164176 },
165177 },
166178};
0 commit comments