Added a TodoChart component, adding a reactive Todo/Completed items tracker.
This commit is contained in:
parent
d4545d753a
commit
d154c28ed2
2 changed files with 552 additions and 157 deletions
280
frontend/src/components/common/TodoChart.vue
Normal file
280
frontend/src/components/common/TodoChart.vue
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
<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: 300px;
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue