282 lines
5.6 KiB
Vue
282 lines
5.6 KiB
Vue
<template>
|
|
<!--<div class="todo-chart-container"> -->
|
|
<!-- Loading Overlay -->
|
|
<!--<div v-if="loading" class="loading-overlay">-->
|
|
<!-- <div class="spinner"></div>-->
|
|
<!-- <div class="loading-text">Loading chart data...</div>-->
|
|
<!--</div>-->
|
|
<!-- Chart Container -->
|
|
<div class="chart-wrapper">
|
|
<canvas ref="chartCanvas" class="chart-canvas" v-show="!loading"></canvas>
|
|
<!-- Center Data Display -->
|
|
<div class="center-data" v-if="centerData && !loading">
|
|
<div class="center-label">{{ centerData.label }}</div>
|
|
<div class="center-value">{{ centerData.value }}</div>
|
|
<div class="center-percentage" v-if="centerData.percentage">
|
|
{{ centerData.percentage }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!--</div> -->
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, watch, nextTick, computed, onUnmounted} from "vue";
|
|
import { Chart, registerables } from "chart.js";
|
|
|
|
// Register Chart.js components
|
|
Chart.register(...registerables);
|
|
|
|
const props = defineProps({
|
|
title: String,
|
|
todoNumber: Number,
|
|
completedNumber: Number,
|
|
loading: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
});
|
|
|
|
//Constants
|
|
const categories = ["To-do", "Completed"];
|
|
|
|
//Reactive data
|
|
const centerData = ref(null);
|
|
const hoveredSegment = ref(null);
|
|
const chartCanvas = ref(null);
|
|
const chartInstance = ref(null);
|
|
|
|
// Handle view changes
|
|
const handleViewChange = () => {
|
|
updateChart();
|
|
};
|
|
|
|
const getHoveredCategoryIndex = () => {
|
|
return hoveredSegment.value
|
|
}
|
|
|
|
const getCategoryValue = (categoryIndex) => {
|
|
if (categoryIndex === 0) {
|
|
return props.todoNumber
|
|
} else {
|
|
return props.completedNumber
|
|
}
|
|
}
|
|
|
|
const getChartData = () => {
|
|
const chartData = {
|
|
name: props.title,
|
|
datasets: [
|
|
{
|
|
label: "",
|
|
data: [props.todoNumber, props.completedNumber],
|
|
backgroundColor: ["#b22222", "#4caf50"]
|
|
},
|
|
]
|
|
};
|
|
return chartData;
|
|
};
|
|
|
|
|
|
const updateCenterData = () => {
|
|
const total = props.todoNumber + props.completedNumber;
|
|
const todos = props.todoNumber;
|
|
|
|
if (todos === 0 && total > 0) {
|
|
centerData.value = {
|
|
label: "Completed",
|
|
value: "0",
|
|
percentage: "100%",
|
|
};
|
|
return;
|
|
}
|
|
|
|
if (todos === 0 || isNaN(todos)){
|
|
centerData.value = {
|
|
label: "No To-Dos",
|
|
value: "0",
|
|
percentage: null,
|
|
};
|
|
return;
|
|
}
|
|
|
|
const hoveredCategoryIndex = getHoveredCategoryIndex()
|
|
if (hoveredCategoryIndex !== null) {
|
|
// Show specific segment data when hovered
|
|
const value = getCategoryValue(hoveredCategoryIndex);
|
|
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) + "%" : "0%";
|
|
|
|
centerData.value = {
|
|
label: categories[hoveredCategoryIndex],
|
|
value: value,
|
|
percentage: percentage,
|
|
};
|
|
} else {
|
|
centerData.value = {
|
|
label: "To-do",
|
|
value: props.todoNumber,
|
|
percentage: null,
|
|
};
|
|
}
|
|
};
|
|
|
|
// Chart options
|
|
const getChartOptions = () => {
|
|
return {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
cutout: "60%",
|
|
plugins: {
|
|
legend: {
|
|
position: "bottom",
|
|
labels: {
|
|
padding: 20,
|
|
usePointStyle: true,
|
|
font: { size: 12 },
|
|
},
|
|
},
|
|
tooltip: { enabled: false },
|
|
title: {
|
|
display: true,
|
|
text: props.title,
|
|
},
|
|
},
|
|
elements: {
|
|
arc: {
|
|
borderWidth: 2,
|
|
borderColor: "#ffffff",
|
|
},
|
|
},
|
|
animation: {
|
|
animateRotate: true,
|
|
animateScale: true,
|
|
duration: 1000,
|
|
easing: "easeOutQuart",
|
|
},
|
|
interaction: {
|
|
mode: "nearest",
|
|
intersect: true,
|
|
},
|
|
onHover: (event, elements) => {
|
|
const categoryIndex = getHoveredCategoryIndex();
|
|
const total = getCategoryValue(categoryIndex);
|
|
|
|
if (elements && elements.length > 0) {
|
|
const elementIndex = elements[0].index;
|
|
if (hoveredSegment.value !== elementIndex) {
|
|
hoveredSegment.value = elementIndex;
|
|
updateCenterData();
|
|
}
|
|
} else {
|
|
if (hoveredSegment.value !== null) {
|
|
hoveredSegment.value = null;
|
|
updateCenterData();
|
|
}
|
|
}
|
|
},
|
|
};
|
|
};
|
|
|
|
const createChart = () => {
|
|
if (!chartCanvas.value || props.loading) return;
|
|
|
|
console.log(`DEBUG: Creating chart for ${props.title}`);
|
|
console.log(props);
|
|
|
|
const ctx = chartCanvas.value.getContext("2d");
|
|
if (chartInstance.value) {
|
|
chartInstance.value.destroy();
|
|
}
|
|
const chart = new Chart(ctx, {
|
|
type: "doughnut",
|
|
data: getChartData(),
|
|
options: getChartOptions(),
|
|
});
|
|
// Don't let Vue mutate Chart members for reactivity
|
|
Object.seal(chart);
|
|
chartInstance.value = chart;
|
|
// Populate Chart display
|
|
updateCenterData();
|
|
}
|
|
|
|
// Update chart
|
|
const updateChart = () => {
|
|
if (props.loading || !chartInstance.value) {
|
|
return;
|
|
}
|
|
const newData = getChartData();
|
|
chartInstance.value.data = newData;
|
|
chartInstance.value.update("none");
|
|
updateCenterData();
|
|
};
|
|
|
|
onMounted(() => {
|
|
createChart();
|
|
});
|
|
|
|
watch(() => props.completedNumber, (newValue) => {
|
|
updateChart();
|
|
});
|
|
|
|
</script>
|
|
|
|
<style scoped>
|
|
/*.todo-chart-container {
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
position: relative;
|
|
min-height: 400px;
|
|
}*/
|
|
|
|
.chart-wrapper {
|
|
position: relative;
|
|
height: 200px;
|
|
width: 200px;
|
|
/*margin-top: 20px;*/
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0;
|
|
}
|
|
|
|
.chart-canvas {
|
|
max-height: 100%;
|
|
width: 100% !important;
|
|
height: 100% !important;
|
|
}
|
|
|
|
.center-data {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
text-align: center;
|
|
pointer-events: none;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.center-label {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #6b7280;
|
|
margin-bottom: 5px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.center-value {
|
|
font-size: 32px;
|
|
font-weight: bold;
|
|
color: #111827;
|
|
line-height: 1;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.center-percentage {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #3b82f6;
|
|
}
|
|
</style>
|