custom_ui/frontend/src/components/common/TodoChart.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>