2
0
mirror of https://github.com/frappe/books.git synced 2024-12-24 11:55:46 +00:00

fix: refactor donutchart

- factor in edge cases
- z-index on dropdown
This commit is contained in:
18alantom 2022-01-18 12:12:37 +05:30
parent 2ac4049cc8
commit 8f5d71f743
3 changed files with 61 additions and 54 deletions

View File

@ -1,6 +1,10 @@
<template> <template>
<div> <div>
<svg version="1.1" viewBox="0 0 100 100" @mouseleave="active = null"> <svg
version="1.1"
viewBox="0 0 100 100"
@mouseleave="$emit('change', null)"
>
<defs> <defs>
<clipPath id="donut-hole"> <clipPath id="donut-hole">
<circle <circle
@ -13,52 +17,52 @@
</clipPath> </clipPath>
</defs> </defs>
<circle <circle
v-if="sectors.length === 1 || sectors.length === 0" v-if="thetasAndStarts.length === 1 || thetasAndStarts.length === 0"
clip-path="url(#donut-hole)" clip-path="url(#donut-hole)"
:cx="cx" :cx="cx"
:cy="cy" :cy="cy"
:r="radius" :r="radius"
@mouseover="active = sectors.length === 1 ? 0 : null" @mouseover="
:stroke-width=" $emit(
thickness + (active === 0 || externalActive === 0 ? 4 : 0) 'change',
thetasAndStarts.length === 1 ? thetasAndStarts[0][0] : null
)
" "
:stroke="(sectors[0] && sectors[0].color) || '#f4f4f6'" :stroke-width="
:class="sectors.length >= 1 ? 'sector' : ''" thickness +
(hasNonZeroValues && active === thetasAndStarts[0][0] ? 4 : 0)
"
:stroke="
hasNonZeroValues ? sectors[thetasAndStarts[0][0]].color : '#f4f4f6'
"
:class="hasNonZeroValues ? 'sector' : ''"
:style="{ transformOrigin: `${cx}px ${cy}px` }" :style="{ transformOrigin: `${cx}px ${cy}px` }"
fill="transparent" fill="transparent"
/> />
<template v-if="sectors.length > 1"> <template v-if="thetasAndStarts.length > 1">
<path <path
clip-path="url(#donut-hole)" clip-path="url(#donut-hole)"
v-for="([theta, start_], i) in sectorsToStarts()" v-for="[i, theta, start_] in thetasAndStarts"
:key="i" :key="i"
:d="getArcPath(cx, cy, radius, start_, theta)" :d="getArcPath(cx, cy, radius, start_, theta)"
:stroke="sectors[i].color" :stroke="sectors[i].color"
:stroke-width=" :stroke-width="thickness + (active === i ? 4 : 0)"
thickness + (active === i || externalActive === i ? 4 : 0)
"
:style="{ transformOrigin: `${cx}px ${cy}px` }" :style="{ transformOrigin: `${cx}px ${cy}px` }"
class="sector" class="sector"
fill="transparent" fill="transparent"
@mouseover="active = i" @mouseover="$emit('change', i)"
/> />
</template> </template>
</svg> </svg>
<div class="relative" style="top: -50%"> <div class="relative" style="top: -50%">
<div class="text-base text-center font-semibold grid justify-center"> <div class="text-base text-center font-semibold grid justify-center">
<p class="text-xs text-gray-600 w-32"> <p class="text-xs text-gray-600 w-32">
{{ {{ active !== null ? sectors[active].label : totalLabel }}
active !== null || externalActive !== null
? sectors[active !== null ? active : externalActive].label
: totalLabel
}}
</p> </p>
<p class="w-32"> <p class="w-32">
{{ {{
valueFormatter( valueFormatter(
active !== null || externalActive !== null active !== null ? sectors[active].value : totalValue,
? sectors[active !== null ? active : externalActive].value
: getTotalValue(),
'Currency' 'Currency'
) )
}} }}
@ -77,45 +81,52 @@ export default {
}, },
totalLabel: { default: 'Total', type: String }, totalLabel: { default: 'Total', type: String },
radius: { default: 36, type: Number }, radius: { default: 36, type: Number },
startAngle: { default: Math.PI, type: Number },
thickness: { default: 10, type: Number }, thickness: { default: 10, type: Number },
externalActive: { default: null, type: Number }, active: { default: null, type: Number },
valueFormatter: { default: (v) => v.toString(), Function }, valueFormatter: { default: (v) => v.toString(), Function },
}, },
data() { data() {
return { return {
cx: 50, cx: 50,
cy: 50, cy: 50,
width: 8,
active: null,
start: Math.PI,
}; };
}, },
methods: { computed: {
getTotalValue() { totalValue() {
return this.sectors.map(({ value }) => value).reduce((a, b) => a + b, 0); return this.sectors.map(({ value }) => value).reduce((a, b) => a + b, 0);
}, },
sectorsToRadians() { thetasAndStarts() {
const totalValue = this.getTotalValue(); const thetas = this.sectors
return this.sectors.map( .map(({ value }, i) => ({
({ value }) => (2 * Math.PI * value) / totalValue value: (2 * Math.PI * value) / this.totalValue,
); filterOut: value !== 0,
}, i,
sectorsToStarts() { }))
const theta = this.sectorsToRadians(); .filter(({ filterOut }) => filterOut);
const starts = [...theta];
starts.forEach((e, i) => { const starts = [...thetas.map(({ value }) => value)];
starts.forEach(({ value }, i) => {
starts[i] += starts[i - 1] ?? 0; starts[i] += starts[i - 1] ?? 0;
}); });
starts.unshift(0); starts.unshift(0);
starts.pop(); starts.pop();
return theta.map((t, i) => [t, starts[i]]); return thetas.map((t, i) => [t.i, t.value, starts[i]]);
}, },
hasNonZeroValues() {
return (
this.thetasAndStarts.length > 0 &&
this.thetasAndStarts.some((t) => this.sectors[t[0]].value !== 0)
);
},
},
methods: {
getArcPath(...args) { getArcPath(...args) {
let [cx, cy, r, start, theta] = args.map(parseFloat); let [cx, cy, r, start, theta] = args.map(parseFloat);
start += parseFloat(this.start);
start += parseFloat(this.startAngle);
const startX = cx + r * Math.cos(start); const startX = cx + r * Math.cos(start);
const startY = cy + r * Math.sin(start); const startY = cy + r * Math.sin(start);
const endX = cx + r * Math.cos(start + theta); const endX = cx + r * Math.cos(start + theta);

View File

@ -28,10 +28,11 @@
</div> </div>
<DonutChart <DonutChart
class="w-1/2" class="w-1/2"
:external-active="active" :active="active"
:sectors="sectors" :sectors="sectors"
:value-formatter="(value) => frappe.format(value, 'Currency')" :value-formatter="(value) => frappe.format(value, 'Currency')"
:total-label="_('Total Spending')" :total-label="_('Total Spending')"
@change="(value) => (active = value)"
/> />
</div> </div>
<div v-if="expenses.length === 0" class="flex-1 w-full h-full flex-center"> <div v-if="expenses.length === 0" class="flex-1 w-full h-full flex-center">
@ -60,13 +61,6 @@ export default {
data: () => ({ data: () => ({
period: 'This Year', period: 'This Year',
active: null, active: null,
sectors: [
{
value: 1,
label: frappe._('No Entries'),
color: theme.backgroundColor.gray['100'],
},
],
expenses: [], expenses: [],
}), }),
mounted() { mounted() {
@ -80,7 +74,14 @@ export default {
return this.expenses.reduce((sum, expense) => sum + expense.total, 0); return this.expenses.reduce((sum, expense) => sum + expense.total, 0);
}, },
hasData() { hasData() {
return this.totalExpense > 0; return this.expenses.length > 0;
},
sectors() {
return this.expenses.map(({ account, color, total }) => ({
color,
label: account,
value: total,
}));
}, },
}, },
methods: { methods: {
@ -121,11 +122,6 @@ export default {
}); });
this.expenses = topExpenses; this.expenses = topExpenses;
this.sectors = topExpenses.map(({ account, color, total }) => ({
color,
label: account,
value: total,
}));
}, },
}, },
}; };

View File

@ -1,5 +1,5 @@
<template> <template>
<Dropdown ref="dropdown" class="text-sm" :items="periodOptions" right> <Dropdown ref="dropdown" class="text-sm z-10" :items="periodOptions" right>
<template <template
v-slot="{ v-slot="{
toggleDropdown, toggleDropdown,