From 3285d496b43ed8b0e2e4a19f36bf027fc3b8044a Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 11 Nov 2025 14:50:50 +0100 Subject: [PATCH] changed barchart to stacked bars to show individual feeding durations --- src/baby_monitor/static/feedings.html | 230 ++++++++++++++++++-------- 1 file changed, 165 insertions(+), 65 deletions(-) diff --git a/src/baby_monitor/static/feedings.html b/src/baby_monitor/static/feedings.html index 0337744..20471b5 100644 --- a/src/baby_monitor/static/feedings.html +++ b/src/baby_monitor/static/feedings.html @@ -306,6 +306,7 @@ let allChildren = []; let currentDaysRange = parseInt(localStorage.getItem("lastFeedingTimeRange")) || 7; let selectedChildId = null; // null means "All Children" + let barChartInstance = null; // Restore last selected child from localStorage const lastChildId = localStorage.getItem("lastFeedingChildFilter"); @@ -410,7 +411,7 @@
-

Feedings (Last ${currentDaysRange} Day${currentDaysRange !== 1 ? "s" : ""})

+

Total Feeding Time (Last ${currentDaysRange} Day${currentDaysRange !== 1 ? "s" : ""})

@@ -659,7 +660,8 @@ function prepareBarChartData(feedings) { const now = new Date(); const labels = []; - const data = []; + const durations = []; // Array of arrays, each containing individual feeding durations + const feedingCounts = []; // For reference // Filter by selected child if applicable let filteredFeedings = feedings; @@ -669,6 +671,9 @@ ); } + // Only use completed feedings + const completedFeedings = filteredFeedings.filter(f => f.end_time); + // If 24 hours selected, show 3-hour aggregates if (currentDaysRange === 1) { // Create 8 three-hour buckets @@ -689,13 +694,20 @@ }); labels.push(`${startLabel}-${endLabel}`); - // Count feedings for this 3-hour period - const count = filteredFeedings.filter((f) => { + // Get individual feeding durations for this 3-hour period (in minutes) + const periodFeedings = completedFeedings.filter((f) => { const feedingDate = new Date(f.start_time); return feedingDate >= periodStart && feedingDate < periodEnd; - }).length; - - data.push(count); + }); + + const feedingDurations = periodFeedings.map(feeding => { + const start = new Date(feeding.start_time); + const end = new Date(feeding.end_time); + return (end - start) / 60000; // Convert to minutes + }); + + durations.push(feedingDurations); + feedingCounts.push(periodFeedings.length); } } else if (currentDaysRange === 3) { // Show 8-hour aggregates for 3 days (9 buckets total) @@ -722,13 +734,20 @@ } labels.push(periodLabel); - // Count feedings for this 8-hour period - const count = filteredFeedings.filter((f) => { + // Get individual feeding durations for this 8-hour period (in minutes) + const periodFeedings = completedFeedings.filter((f) => { const feedingDate = new Date(f.start_time); return feedingDate >= periodStart && feedingDate < periodEnd; - }).length; - - data.push(count); + }); + + const feedingDurations = periodFeedings.map(feeding => { + const start = new Date(feeding.start_time); + const end = new Date(feeding.end_time); + return (end - start) / 60000; // Convert to minutes + }); + + durations.push(feedingDurations); + feedingCounts.push(periodFeedings.length); } } else { // Create days based on currentDaysRange (including today) @@ -743,43 +762,150 @@ }); labels.push(dateStr); - // Count feedings for this day + // Get individual feeding durations for this day (in minutes) const nextDay = new Date(date); nextDay.setDate(nextDay.getDate() + 1); - const count = filteredFeedings.filter((f) => { + const dayFeedings = completedFeedings.filter((f) => { const feedingDate = new Date(f.start_time); return feedingDate >= date && feedingDate < nextDay; - }).length; - - data.push(count); + }); + + const feedingDurations = dayFeedings.map(feeding => { + const start = new Date(feeding.start_time); + const end = new Date(feeding.end_time); + return (end - start) / 60000; // Convert to minutes + }); + + durations.push(feedingDurations); + feedingCounts.push(dayFeedings.length); } } - return { labels, data }; + return { labels, durations, feedingCounts }; } function renderBarChart(chartData) { const ctx = document.getElementById("feedingsChart").getContext("2d"); - new Chart(ctx, { + // Destroy existing chart if it exists + if (barChartInstance) { + barChartInstance.destroy(); + } + + // Find the maximum number of feedings in any period + const maxFeedings = Math.max(...chartData.durations.map(d => d.length), 0); + + // If no feedings at all, show empty chart + if (maxFeedings === 0) { + barChartInstance = new Chart(ctx, { + type: "bar", + data: { + labels: chartData.labels, + datasets: [{ + label: "No Data", + data: new Array(chartData.labels.length).fill(0), + backgroundColor: "rgba(102, 126, 234, 0.3)", + }], + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + beginAtZero: true, + max: 60, + title: { + display: true, + text: "Minutes", + }, + }, + }, + plugins: { + legend: { + display: false, + }, + }, + }, + }); + return; + } + + // Create color palette for different feedings + const colors = [ + 'rgba(102, 126, 234, 0.8)', + 'rgba(118, 75, 162, 0.8)', + 'rgba(255, 159, 64, 0.8)', + 'rgba(76, 175, 80, 0.8)', + 'rgba(244, 67, 54, 0.8)', + 'rgba(156, 39, 176, 0.8)', + 'rgba(102, 126, 234, 0.6)', + 'rgba(118, 75, 162, 0.6)', + 'rgba(255, 159, 64, 0.6)', + 'rgba(76, 175, 80, 0.6)', + ]; + + // Create datasets - one for each feeding position + const datasets = []; + for (let feedingIndex = 0; feedingIndex < maxFeedings; feedingIndex++) { + const dataForFeeding = chartData.durations.map(periodDurations => + periodDurations[feedingIndex] || 0 + ); + + datasets.push({ + label: `Feeding ${feedingIndex + 1}`, + data: dataForFeeding, + backgroundColor: colors[feedingIndex % colors.length], + borderColor: colors[feedingIndex % colors.length].replace('0.8', '1').replace('0.6', '1'), + borderWidth: 1, + }); + } + + barChartInstance = new Chart(ctx, { type: "bar", data: { labels: chartData.labels, - datasets: [ - { - label: "Feedings", - data: chartData.data, - backgroundColor: "rgba(102, 126, 234, 0.7)", - borderColor: "rgba(102, 126, 234, 1)", - borderWidth: 2, - borderRadius: 6, - }, - ], + datasets: datasets, }, options: { responsive: true, maintainAspectRatio: false, + scales: { + x: { + stacked: true, + ticks: { + font: { + size: 12, + }, + }, + grid: { + display: false, + }, + }, + y: { + stacked: true, + beginAtZero: true, + title: { + display: true, + text: "Minutes", + font: { + size: 13, + weight: "600", + }, + }, + ticks: { + font: { + size: 12, + }, + callback: function(value) { + return Math.round(value) + 'm'; + } + }, + grid: { + color: "rgba(0, 0, 0, 0.05)", + }, + }, + }, plugins: { legend: { display: false, @@ -794,43 +920,17 @@ size: 13, }, callbacks: { - label: function (context) { - const count = context.parsed.y; - return `${count} feeding${count !== 1 ? "s" : ""}`; + label: function(context) { + const minutes = Math.round(context.parsed.y); + return `${context.dataset.label}: ${minutes}m`; }, - }, - }, - }, - scales: { - y: { - beginAtZero: true, - title: { - display: true, - text: "Number of Feedings", - font: { - size: 13, - weight: "600", - }, - }, - ticks: { - stepSize: 1, - font: { - size: 12, - }, - }, - grid: { - color: "rgba(0, 0, 0, 0.05)", - }, - }, - x: { - ticks: { - font: { - size: 12, - }, - }, - grid: { - display: false, - }, + footer: function(tooltipItems) { + const periodIndex = tooltipItems[0].dataIndex; + const totalMinutes = chartData.durations[periodIndex].reduce((sum, val) => sum + val, 0); + const feedingCount = chartData.feedingCounts[periodIndex]; + return `Total: ${Math.round(totalMinutes)}m (${feedingCount} feeding${feedingCount !== 1 ? 's' : ''})`; + } + } }, }, },