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:
parent
2ac4049cc8
commit
8f5d71f743
@ -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);
|
||||||
|
@ -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,
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user