From bb27d4437ff6d47e1c4e93ee46bd7beaad596730 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 11 Nov 2025 13:45:54 +0100 Subject: [PATCH 1/7] changed overview graphs binning --- src/baby_monitor/static/diapers.html | 133 ++++++++++++++++++++------ src/baby_monitor/static/feedings.html | 101 +++++++++++++++---- src/baby_monitor/static/sleep.html | 123 ++++++++++++++++++------ 3 files changed, 281 insertions(+), 76 deletions(-) diff --git a/src/baby_monitor/static/diapers.html b/src/baby_monitor/static/diapers.html index be13c30..6e7e603 100644 --- a/src/baby_monitor/static/diapers.html +++ b/src/baby_monitor/static/diapers.html @@ -365,7 +365,7 @@
-

Diaper Changes Per Day (Last ${currentDaysRange} Day${currentDaysRange !== 1 ? "s" : ""})

+

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

@@ -627,36 +627,115 @@ ); } - // Create days based on currentDaysRange (including today) - for (let i = currentDaysRange - 1; i >= 0; i--) { - const date = new Date(now); - date.setDate(date.getDate() - i); - date.setHours(0, 0, 0, 0); + // If 24 hours selected, show 3-hour aggregates + if (currentDaysRange === 1) { + // Create 8 three-hour buckets + for (let i = 7; i >= 0; i--) { + const periodStart = new Date(now); + periodStart.setHours(periodStart.getHours() - (i * 3), 0, 0, 0); + + const periodEnd = new Date(periodStart); + periodEnd.setHours(periodEnd.getHours() + 3); - const dateStr = date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }); - labels.push(dateStr); + const startLabel = periodStart.toLocaleTimeString("en-US", { + hour: "numeric", + hour12: true, + }); + const endLabel = periodEnd.toLocaleTimeString("en-US", { + hour: "numeric", + hour12: true, + }); + labels.push(`${startLabel}-${endLabel}`); - // Count poop and pee diapers for this day - const nextDay = new Date(date); - nextDay.setDate(nextDay.getDate() + 1); + // Count poop and pee diapers for this 3-hour period + const periodChanges = filteredChanges.filter((dc) => { + const changeDate = new Date(dc.change_time); + return changeDate >= periodStart && changeDate < periodEnd; + }); - const dayChanges = filteredChanges.filter((dc) => { - const changeDate = new Date(dc.change_time); - return changeDate >= date && changeDate < nextDay; - }); + const poopCount = periodChanges.filter( + (dc) => dc.poop_amount !== null, + ).length; + const peeCount = periodChanges.filter( + (dc) => dc.pee_amount !== null, + ).length; - const poopCount = dayChanges.filter( - (dc) => dc.poop_amount !== null, - ).length; - const peeCount = dayChanges.filter( - (dc) => dc.pee_amount !== null, - ).length; + poopData.push(poopCount); + peeData.push(peeCount); + } + } else if (currentDaysRange === 3) { + // Show 8-hour aggregates for 3 days (9 buckets total) + for (let i = 8; i >= 0; i--) { + const periodStart = new Date(now); + periodStart.setHours(periodStart.getHours() - (i * 8), 0, 0, 0); + + const periodEnd = new Date(periodStart); + periodEnd.setHours(periodEnd.getHours() + 8); - poopData.push(poopCount); - peeData.push(peeCount); + // Format label based on the 8-hour period + const dayStr = periodStart.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + const startHour = periodStart.getHours(); + let periodLabel; + if (startHour >= 0 && startHour < 8) { + periodLabel = `${dayStr} Night`; + } else if (startHour >= 8 && startHour < 16) { + periodLabel = `${dayStr} Morning`; + } else { + periodLabel = `${dayStr} Evening`; + } + labels.push(periodLabel); + + // Count poop and pee diapers for this 8-hour period + const periodChanges = filteredChanges.filter((dc) => { + const changeDate = new Date(dc.change_time); + return changeDate >= periodStart && changeDate < periodEnd; + }); + + const poopCount = periodChanges.filter( + (dc) => dc.poop_amount !== null, + ).length; + const peeCount = periodChanges.filter( + (dc) => dc.pee_amount !== null, + ).length; + + poopData.push(poopCount); + peeData.push(peeCount); + } + } else { + // Create days based on currentDaysRange (including today) + for (let i = currentDaysRange - 1; i >= 0; i--) { + const date = new Date(now); + date.setDate(date.getDate() - i); + date.setHours(0, 0, 0, 0); + + const dateStr = date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + labels.push(dateStr); + + // Count poop and pee diapers for this day + const nextDay = new Date(date); + nextDay.setDate(nextDay.getDate() + 1); + + const dayChanges = filteredChanges.filter((dc) => { + const changeDate = new Date(dc.change_time); + return changeDate >= date && changeDate < nextDay; + }); + + const poopCount = dayChanges.filter( + (dc) => dc.poop_amount !== null, + ).length; + const peeCount = dayChanges.filter( + (dc) => dc.pee_amount !== null, + ).length; + + poopData.push(poopCount); + peeData.push(peeCount); + } } return { labels, poopData, peeData }; @@ -724,7 +803,7 @@ beginAtZero: true, title: { display: true, - text: "Number of Diapers", + text: "Number of Changes", font: { size: 13, weight: "600", diff --git a/src/baby_monitor/static/feedings.html b/src/baby_monitor/static/feedings.html index 9e38f5e..8da30d5 100644 --- a/src/baby_monitor/static/feedings.html +++ b/src/baby_monitor/static/feedings.html @@ -402,7 +402,7 @@
-

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

+

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

@@ -661,28 +661,91 @@ ); } - // Create days based on currentDaysRange (including today) - for (let i = currentDaysRange - 1; i >= 0; i--) { - const date = new Date(now); - date.setDate(date.getDate() - i); - date.setHours(0, 0, 0, 0); + // If 24 hours selected, show 3-hour aggregates + if (currentDaysRange === 1) { + // Create 8 three-hour buckets + for (let i = 7; i >= 0; i--) { + const periodStart = new Date(now); + periodStart.setHours(periodStart.getHours() - (i * 3), 0, 0, 0); + + const periodEnd = new Date(periodStart); + periodEnd.setHours(periodEnd.getHours() + 3); - const dateStr = date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }); - labels.push(dateStr); + const startLabel = periodStart.toLocaleTimeString("en-US", { + hour: "numeric", + hour12: true, + }); + const endLabel = periodEnd.toLocaleTimeString("en-US", { + hour: "numeric", + hour12: true, + }); + labels.push(`${startLabel}-${endLabel}`); - // Count feedings for this day - const nextDay = new Date(date); - nextDay.setDate(nextDay.getDate() + 1); + // Count feedings for this 3-hour period + const count = filteredFeedings.filter((f) => { + const feedingDate = new Date(f.start_time); + return feedingDate >= periodStart && feedingDate < periodEnd; + }).length; - const count = filteredFeedings.filter((f) => { - const feedingDate = new Date(f.start_time); - return feedingDate >= date && feedingDate < nextDay; - }).length; + data.push(count); + } + } else if (currentDaysRange === 3) { + // Show 8-hour aggregates for 3 days (9 buckets total) + for (let i = 8; i >= 0; i--) { + const periodStart = new Date(now); + periodStart.setHours(periodStart.getHours() - (i * 8), 0, 0, 0); + + const periodEnd = new Date(periodStart); + periodEnd.setHours(periodEnd.getHours() + 8); - data.push(count); + // Format label based on the 8-hour period + const dayStr = periodStart.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + const startHour = periodStart.getHours(); + let periodLabel; + if (startHour >= 0 && startHour < 8) { + periodLabel = `${dayStr} Night`; + } else if (startHour >= 8 && startHour < 16) { + periodLabel = `${dayStr} Morning`; + } else { + periodLabel = `${dayStr} Evening`; + } + labels.push(periodLabel); + + // Count feedings for this 8-hour period + const count = filteredFeedings.filter((f) => { + const feedingDate = new Date(f.start_time); + return feedingDate >= periodStart && feedingDate < periodEnd; + }).length; + + data.push(count); + } + } else { + // Create days based on currentDaysRange (including today) + for (let i = currentDaysRange - 1; i >= 0; i--) { + const date = new Date(now); + date.setDate(date.getDate() - i); + date.setHours(0, 0, 0, 0); + + const dateStr = date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + labels.push(dateStr); + + // Count feedings for this day + const nextDay = new Date(date); + nextDay.setDate(nextDay.getDate() + 1); + + const count = filteredFeedings.filter((f) => { + const feedingDate = new Date(f.start_time); + return feedingDate >= date && feedingDate < nextDay; + }).length; + + data.push(count); + } } return { labels, data }; diff --git a/src/baby_monitor/static/sleep.html b/src/baby_monitor/static/sleep.html index 10df90c..823b50a 100644 --- a/src/baby_monitor/static/sleep.html +++ b/src/baby_monitor/static/sleep.html @@ -536,42 +536,105 @@ return `${hours}h ${mins}m`; } - function prepareBarChartData(sleeps, timeRange) { - const days = {}; - const today = new Date(); - today.setHours(0, 0, 0, 0); + function prepareBarChartData(sleepSessions) { + const now = new Date(); + const labels = []; + const data = []; - // Initialize all days in range - for (let i = timeRange - 1; i >= 0; i--) { - const date = new Date(today); - date.setDate(date.getDate() - i); - const dateStr = date.toISOString().split("T")[0]; - days[dateStr] = 0; + // Filter by selected child if applicable + let filteredSessions = sleepSessions; + if (selectedChildId !== null) { + filteredSessions = sleepSessions.filter( + (session) => session.child_id === selectedChildId, + ); } - // Count sleeps per day - sleeps.forEach((sleep) => { - const date = new Date(sleep.start_time); - date.setHours(0, 0, 0, 0); - const dateStr = date.toISOString().split("T")[0]; - if (days.hasOwnProperty(dateStr)) { - days[dateStr]++; + // If 24 hours selected, show 3-hour aggregates + if (currentDaysRange === 1) { + // Create 8 three-hour buckets + for (let i = 7; i >= 0; i--) { + const periodStart = new Date(now); + periodStart.setHours(periodStart.getHours() - (i * 3), 0, 0, 0); + + const periodEnd = new Date(periodStart); + periodEnd.setHours(periodEnd.getHours() + 3); + + const startLabel = periodStart.toLocaleTimeString("en-US", { + hour: "numeric", + hour12: true, + }); + const endLabel = periodEnd.toLocaleTimeString("en-US", { + hour: "numeric", + hour12: true, + }); + labels.push(`${startLabel}-${endLabel}`); + + // Count sleep sessions for this 3-hour period + const count = filteredSessions.filter((session) => { + const startDate = new Date(session.start_time); + return startDate >= periodStart && startDate < periodEnd; + }).length; + + data.push(count); } - }); + } else if (currentDaysRange === 3) { + // Show 8-hour aggregates for 3 days (9 buckets total) + for (let i = 8; i >= 0; i--) { + const periodStart = new Date(now); + periodStart.setHours(periodStart.getHours() - (i * 8), 0, 0, 0); + + const periodEnd = new Date(periodStart); + periodEnd.setHours(periodEnd.getHours() + 8); - const labels = Object.keys(days).map((date) => { - const d = new Date(date); - return d.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }); - }); + // Format label based on the 8-hour period + const dayStr = periodStart.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + const startHour = periodStart.getHours(); + let periodLabel; + if (startHour >= 0 && startHour < 8) { + periodLabel = `${dayStr} Night`; + } else if (startHour >= 8 && startHour < 16) { + periodLabel = `${dayStr} Morning`; + } else { + periodLabel = `${dayStr} Evening`; + } + labels.push(periodLabel); - return { - labels, - data: Object.values(days), - }; - } + // Count sleep sessions for this 8-hour period + const count = filteredSessions.filter((session) => { + const startDate = new Date(session.start_time); + return startDate >= periodStart && startDate < periodEnd; + }).length; + + data.push(count); + } + } else { + // Create days based on currentDaysRange (including today) + for (let i = currentDaysRange - 1; i >= 0; i--) { + const date = new Date(now); + date.setDate(date.getDate() - i); + date.setHours(0, 0, 0, 0); + + const dateStr = date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + labels.push(dateStr); + + // Count sleep sessions for this day + const nextDay = new Date(date); + nextDay.setDate(nextDay.getDate() + 1); + + const count = filteredSessions.filter((session) => { + const startDate = new Date(session.start_time); + return startDate >= date && startDate < nextDay; + }).length; + + data.push(count); + } + } function prepareDurationDistributionData(sleeps) { const ranges = { From 954e5a9f6f671c9b128c98eee49e959bf0235d3e Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 11 Nov 2025 13:58:04 +0100 Subject: [PATCH 2/7] fixed bug with sleeping overview barchart --- src/baby_monitor/static/sleep.html | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/baby_monitor/static/sleep.html b/src/baby_monitor/static/sleep.html index 823b50a..6f2f57b 100644 --- a/src/baby_monitor/static/sleep.html +++ b/src/baby_monitor/static/sleep.html @@ -536,21 +536,13 @@ return `${hours}h ${mins}m`; } - function prepareBarChartData(sleepSessions) { + function prepareBarChartData(sleepSessions, timeRange) { const now = new Date(); const labels = []; const data = []; - // Filter by selected child if applicable - let filteredSessions = sleepSessions; - if (selectedChildId !== null) { - filteredSessions = sleepSessions.filter( - (session) => session.child_id === selectedChildId, - ); - } - // If 24 hours selected, show 3-hour aggregates - if (currentDaysRange === 1) { + if (timeRange === 1) { // Create 8 three-hour buckets for (let i = 7; i >= 0; i--) { const periodStart = new Date(now); @@ -570,14 +562,14 @@ labels.push(`${startLabel}-${endLabel}`); // Count sleep sessions for this 3-hour period - const count = filteredSessions.filter((session) => { + const count = sleepSessions.filter((session) => { const startDate = new Date(session.start_time); return startDate >= periodStart && startDate < periodEnd; }).length; data.push(count); } - } else if (currentDaysRange === 3) { + } else if (timeRange === 3) { // Show 8-hour aggregates for 3 days (9 buckets total) for (let i = 8; i >= 0; i--) { const periodStart = new Date(now); @@ -603,7 +595,7 @@ labels.push(periodLabel); // Count sleep sessions for this 8-hour period - const count = filteredSessions.filter((session) => { + const count = sleepSessions.filter((session) => { const startDate = new Date(session.start_time); return startDate >= periodStart && startDate < periodEnd; }).length; @@ -611,8 +603,8 @@ data.push(count); } } else { - // Create days based on currentDaysRange (including today) - for (let i = currentDaysRange - 1; i >= 0; i--) { + // Create days based on timeRange (including today) + for (let i = timeRange - 1; i >= 0; i--) { const date = new Date(now); date.setDate(date.getDate() - i); date.setHours(0, 0, 0, 0); @@ -627,7 +619,7 @@ const nextDay = new Date(date); nextDay.setDate(nextDay.getDate() + 1); - const count = filteredSessions.filter((session) => { + const count = sleepSessions.filter((session) => { const startDate = new Date(session.start_time); return startDate >= date && startDate < nextDay; }).length; @@ -636,6 +628,9 @@ } } + return { labels, data }; + } + function prepareDurationDistributionData(sleeps) { const ranges = { "0-30m": 0, From b1fae46f28e1355a7eebe392e4e2e9dfc36d51ae Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 11 Nov 2025 14:19:41 +0100 Subject: [PATCH 3/7] made stacked barchart to show individual sleep session lengths --- src/baby_monitor/static/sleep.html | 211 ++++++++++++++++++++++++----- 1 file changed, 180 insertions(+), 31 deletions(-) diff --git a/src/baby_monitor/static/sleep.html b/src/baby_monitor/static/sleep.html index 6f2f57b..7839437 100644 --- a/src/baby_monitor/static/sleep.html +++ b/src/baby_monitor/static/sleep.html @@ -480,7 +480,7 @@
-

Sleep Sessions per Day

+

Total Sleep Time

@@ -539,7 +539,11 @@ function prepareBarChartData(sleepSessions, timeRange) { const now = new Date(); const labels = []; - const data = []; + const durations = []; // Array of arrays, each containing individual session durations + const sessionCounts = []; // For reference + + // Only use completed sleep sessions + const completedSessions = sleepSessions.filter(s => s.end_time); // If 24 hours selected, show 3-hour aggregates if (timeRange === 1) { @@ -561,13 +565,18 @@ }); labels.push(`${startLabel}-${endLabel}`); - // Count sleep sessions for this 3-hour period - const count = sleepSessions.filter((session) => { + // Get individual session durations for this 3-hour period (in hours) + const periodSessions = completedSessions.filter((session) => { const startDate = new Date(session.start_time); return startDate >= periodStart && startDate < periodEnd; - }).length; - - data.push(count); + }); + + const sessionDurations = periodSessions.map(session => + calculateDuration(session.start_time, session.end_time) / 60 + ); + + durations.push(sessionDurations); + sessionCounts.push(periodSessions.length); } } else if (timeRange === 3) { // Show 8-hour aggregates for 3 days (9 buckets total) @@ -594,13 +603,18 @@ } labels.push(periodLabel); - // Count sleep sessions for this 8-hour period - const count = sleepSessions.filter((session) => { + // Get individual session durations for this 8-hour period (in hours) + const periodSessions = completedSessions.filter((session) => { const startDate = new Date(session.start_time); return startDate >= periodStart && startDate < periodEnd; - }).length; - - data.push(count); + }); + + const sessionDurations = periodSessions.map(session => + calculateDuration(session.start_time, session.end_time) / 60 + ); + + durations.push(sessionDurations); + sessionCounts.push(periodSessions.length); } } else { // Create days based on timeRange (including today) @@ -615,20 +629,38 @@ }); labels.push(dateStr); - // Count sleep sessions for this day + // Get individual session durations for this day (in hours) const nextDay = new Date(date); nextDay.setDate(nextDay.getDate() + 1); - const count = sleepSessions.filter((session) => { + const daySessions = completedSessions.filter((session) => { const startDate = new Date(session.start_time); return startDate >= date && startDate < nextDay; - }).length; - - data.push(count); + }); + + const sessionDurations = daySessions.map(session => { + const dur = calculateDuration(session.start_time, session.end_time) / 60; + console.log(` Session: ${session.start_time} to ${session.end_time}, duration: ${dur} hours (${calculateDuration(session.start_time, session.end_time)} minutes)`); + return dur; + }); + + console.log(`${dateStr}: ${daySessions.length} sessions, durations:`, sessionDurations); + + durations.push(sessionDurations); + sessionCounts.push(daySessions.length); } } - return { labels, data }; + console.log('Final prepareBarChartData result:', { labels, durations, sessionCounts }); + + // Log the actual duration values for debugging + durations.forEach((periodDurations, idx) => { + if (periodDurations.length > 0) { + console.log(` ${labels[idx]}: durations =`, periodDurations); + } + }); + + return { labels, durations, sessionCounts }; } function prepareDurationDistributionData(sleeps) { @@ -657,30 +689,119 @@ }; } + let barChartInstance = null; + let durationChartInstance = null; + let avgDurationChartInstance = null; + function renderBarChart(chartData) { const ctx = document.getElementById("barChart"); - new Chart(ctx, { + + // Destroy existing chart if it exists + if (barChartInstance) { + barChartInstance.destroy(); + } + + // Debug: Log the data + console.log('Bar chart data:', chartData); + + // Find the maximum number of sessions in any period + const maxSessions = Math.max(...chartData.durations.map(d => d.length), 0); + + console.log('Max sessions:', maxSessions); + + // If no sessions at all, show empty chart + if (maxSessions === 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(79, 172, 254, 0.3)", + }], + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + beginAtZero: true, + max: 10, + title: { + display: true, + text: "Hours", + }, + }, + }, + plugins: { + legend: { + display: false, + }, + }, + }, + }); + return; + } + + // Create color palette for different sessions + const colors = [ + 'rgba(79, 172, 254, 0.8)', + 'rgba(0, 242, 254, 0.8)', + 'rgba(58, 155, 232, 0.8)', + 'rgba(32, 137, 220, 0.8)', + 'rgba(21, 119, 208, 0.8)', + 'rgba(11, 101, 196, 0.8)', + 'rgba(79, 172, 254, 0.6)', + 'rgba(0, 242, 254, 0.6)', + 'rgba(58, 155, 232, 0.6)', + 'rgba(32, 137, 220, 0.6)', + ]; + + // Create datasets - one for each session position + const datasets = []; + for (let sessionIndex = 0; sessionIndex < maxSessions; sessionIndex++) { + const dataForSession = chartData.durations.map(periodDurations => + periodDurations[sessionIndex] || 0 + ); + + console.log(`Session ${sessionIndex + 1} data:`, dataForSession); + + datasets.push({ + label: `Session ${sessionIndex + 1}`, + data: dataForSession, + backgroundColor: colors[sessionIndex % colors.length], + borderColor: colors[sessionIndex % colors.length].replace('0.8', '1').replace('0.6', '1'), + borderWidth: 1, + }); + } + + console.log('Datasets:', datasets); + + barChartInstance = new Chart(ctx, { type: "bar", data: { labels: chartData.labels, - datasets: [ - { - label: "Sleep Sessions", - data: chartData.data, - backgroundColor: "rgba(79, 172, 254, 0.6)", - borderColor: "rgba(79, 172, 254, 1)", - borderWidth: 1, - }, - ], + datasets: datasets, }, options: { responsive: true, maintainAspectRatio: false, scales: { + x: { + stacked: true, + }, y: { + stacked: true, beginAtZero: true, + title: { + display: true, + text: "Hours", + }, ticks: { - stepSize: 1, + callback: function(value) { + return value.toFixed(1) + 'h'; + } }, }, }, @@ -688,6 +809,23 @@ legend: { display: false, }, + tooltip: { + callbacks: { + label: function(context) { + const hours = Math.floor(context.parsed.y); + const minutes = Math.round((context.parsed.y - hours) * 60); + return `${context.dataset.label}: ${hours}h ${minutes}m`; + }, + footer: function(tooltipItems) { + const periodIndex = tooltipItems[0].dataIndex; + const totalHours = chartData.durations[periodIndex].reduce((sum, val) => sum + val, 0); + const hours = Math.floor(totalHours); + const minutes = Math.round((totalHours - hours) * 60); + const sessionCount = chartData.sessionCounts[periodIndex]; + return `Total: ${hours}h ${minutes}m (${sessionCount} session${sessionCount !== 1 ? 's' : ''})`; + } + } + }, }, }, }); @@ -695,7 +833,13 @@ function renderDurationChart(chartData) { const ctx = document.getElementById("durationChart"); - new Chart(ctx, { + + // Destroy existing chart if it exists + if (durationChartInstance) { + durationChartInstance.destroy(); + } + + durationChartInstance = new Chart(ctx, { type: "pie", data: { labels: chartData.labels, @@ -726,6 +870,11 @@ } function renderAvgDurationChart(sleeps, timeRange) { + // Destroy existing chart if it exists + if (avgDurationChartInstance) { + avgDurationChartInstance.destroy(); + } + const days = {}; const today = new Date(); today.setHours(0, 0, 0, 0); @@ -768,7 +917,7 @@ ); const ctx = document.getElementById("avgDurationChart"); - new Chart(ctx, { + avgDurationChartInstance = new Chart(ctx, { type: "line", data: { labels, From dea3b5d3b5f15bb699c319bd293bfe0395402391 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 11 Nov 2025 14:29:43 +0100 Subject: [PATCH 4/7] persisted selection of child and time range when switching between pages. Also added automatic total sleep time scaling on vertical axis --- src/baby_monitor/static/diapers.html | 10 +++++- src/baby_monitor/static/feedings.html | 10 +++++- src/baby_monitor/static/sleep.html | 46 ++++++++++++++++++++++++--- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/baby_monitor/static/diapers.html b/src/baby_monitor/static/diapers.html index 6e7e603..3398923 100644 --- a/src/baby_monitor/static/diapers.html +++ b/src/baby_monitor/static/diapers.html @@ -267,9 +267,15 @@ let allDiaperChanges = []; let allChildren = []; - let currentDaysRange = 7; + let currentDaysRange = parseInt(localStorage.getItem("lastDiaperTimeRange")) || 7; let selectedChildId = null; // null means "All Children" + // Restore last selected child from localStorage + const lastChildId = localStorage.getItem("lastDiaperChildFilter"); + if (lastChildId && lastChildId !== "all") { + selectedChildId = parseInt(lastChildId); + } + // Load diaper changes data async function loadDiaperChanges() { try { @@ -311,12 +317,14 @@ function onDaysRangeChange(event) { currentDaysRange = parseInt(event.target.value); + localStorage.setItem("lastDiaperTimeRange", currentDaysRange); displayDiaperChanges(allDiaperChanges); } function onChildChange(event) { selectedChildId = event.target.value === "all" ? null : parseInt(event.target.value); + localStorage.setItem("lastDiaperChildFilter", event.target.value); displayDiaperChanges(allDiaperChanges); } diff --git a/src/baby_monitor/static/feedings.html b/src/baby_monitor/static/feedings.html index 8da30d5..0337744 100644 --- a/src/baby_monitor/static/feedings.html +++ b/src/baby_monitor/static/feedings.html @@ -304,9 +304,15 @@ let allFeedings = []; let allChildren = []; - let currentDaysRange = 7; + let currentDaysRange = parseInt(localStorage.getItem("lastFeedingTimeRange")) || 7; let selectedChildId = null; // null means "All Children" + // Restore last selected child from localStorage + const lastChildId = localStorage.getItem("lastFeedingChildFilter"); + if (lastChildId && lastChildId !== "all") { + selectedChildId = parseInt(lastChildId); + } + // Load feedings data async function loadFeedings() { try { @@ -348,12 +354,14 @@ function onDaysRangeChange(event) { currentDaysRange = parseInt(event.target.value); + localStorage.setItem("lastFeedingTimeRange", currentDaysRange); displayFeedings(allFeedings); } function onChildChange(event) { selectedChildId = event.target.value === "all" ? null : parseInt(event.target.value); + localStorage.setItem("lastFeedingChildFilter", event.target.value); displayFeedings(allFeedings); } diff --git a/src/baby_monitor/static/sleep.html b/src/baby_monitor/static/sleep.html index 7839437..3210345 100644 --- a/src/baby_monitor/static/sleep.html +++ b/src/baby_monitor/static/sleep.html @@ -339,6 +339,8 @@ document .getElementById("timeRangeFilter") .addEventListener("change", function () { + // Save selection to localStorage + localStorage.setItem("lastSleepTimeRange", this.value); loadData(); }); @@ -374,6 +376,13 @@ selectedChildId = lastChildId; } + // Restore last selected time range from localStorage + const timeRangeFilter = document.getElementById("timeRangeFilter"); + const lastTimeRange = localStorage.getItem("lastSleepTimeRange"); + if (lastTimeRange) { + timeRangeFilter.value = lastTimeRange; + } + // Fetch sleep logs const sleepResponse = await fetch("/api/sleep", { headers: { @@ -778,11 +787,27 @@ console.log('Datasets:', datasets); + // Determine the maximum total duration across all periods (in hours) + const maxDuration = Math.max(...chartData.durations.map(periodDurations => + periodDurations.reduce((sum, val) => sum + val, 0) + ), 0); + + console.log('Max duration (hours):', maxDuration); + + // Decide whether to use minutes or hours based on max duration + const useMinutes = maxDuration < 2; // Use minutes if max is less than 2 hours + + // Convert data to minutes if needed + const finalDatasets = useMinutes ? datasets.map(dataset => ({ + ...dataset, + data: dataset.data.map(hours => hours * 60) // Convert hours to minutes + })) : datasets; + barChartInstance = new Chart(ctx, { type: "bar", data: { labels: chartData.labels, - datasets: datasets, + datasets: finalDatasets, }, options: { responsive: true, @@ -796,11 +821,15 @@ beginAtZero: true, title: { display: true, - text: "Hours", + text: useMinutes ? "Minutes" : "Hours", }, ticks: { callback: function(value) { - return value.toFixed(1) + 'h'; + if (useMinutes) { + return Math.round(value) + 'm'; + } else { + return value.toFixed(1) + 'h'; + } } }, }, @@ -812,8 +841,12 @@ tooltip: { callbacks: { label: function(context) { - const hours = Math.floor(context.parsed.y); - const minutes = Math.round((context.parsed.y - hours) * 60); + const value = useMinutes ? context.parsed.y / 60 : context.parsed.y; + const hours = Math.floor(value); + const minutes = Math.round((value - hours) * 60); + if (hours === 0) { + return `${context.dataset.label}: ${minutes}m`; + } return `${context.dataset.label}: ${hours}h ${minutes}m`; }, footer: function(tooltipItems) { @@ -822,6 +855,9 @@ const hours = Math.floor(totalHours); const minutes = Math.round((totalHours - hours) * 60); const sessionCount = chartData.sessionCounts[periodIndex]; + if (hours === 0) { + return `Total: ${minutes}m (${sessionCount} session${sessionCount !== 1 ? 's' : ''})`; + } return `Total: ${hours}h ${minutes}m (${sessionCount} session${sessionCount !== 1 ? 's' : ''})`; } } From c745ab1b7a7d9c6681bdf3401018a0168f9d50a9 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 11 Nov 2025 14:39:59 +0100 Subject: [PATCH 5/7] added skip add-child for new users to allow for adding a shared child --- src/baby_monitor/static/add-child.html | 19 +++++++++++++++++ src/baby_monitor/static/index.html | 28 ++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/baby_monitor/static/add-child.html b/src/baby_monitor/static/add-child.html index 7fde252..2bbcfa6 100644 --- a/src/baby_monitor/static/add-child.html +++ b/src/baby_monitor/static/add-child.html @@ -192,6 +192,15 @@ Cancel + +
diff --git a/src/baby_monitor/static/index.html b/src/baby_monitor/static/index.html index 6b12842..53df444 100644 --- a/src/baby_monitor/static/index.html +++ b/src/baby_monitor/static/index.html @@ -275,9 +275,9 @@ }) .then((response) => response.json()) .then((children) => { - // If no children, redirect to add-child page + // If no children, show welcome message with option to add if (children.length === 0) { - window.location.href = "/add-child.html"; + showNoChildrenMessage(); return; } @@ -290,6 +290,30 @@ }); } + function showNoChildrenMessage() { + const content = document.getElementById("content"); + content.className = ""; + content.innerHTML = ` +

👶 Welcome to Baby Monitor

+
+
🍼
+

No Children Added Yet

+

+ You can add a child to start tracking or redeem a child share code
+ from another user to access their child's information. +

+
+ + ➕ Add Your Child + + + 🔗 Redeem Share Code + +
+
+ `; + } + let activeFeeding = null; let userChildren = []; let lastFeeding = null; From 3285d496b43ed8b0e2e4a19f36bf027fc3b8044a Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 11 Nov 2025 14:50:50 +0100 Subject: [PATCH 6/7] 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' : ''})`; + } + } }, }, }, From 752aee1148d02a8cb5b66a8548a7cf672b2e9ec2 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 11 Nov 2025 14:51:04 +0100 Subject: [PATCH 7/7] corrected filter placement and barchart title --- src/baby_monitor/static/sleep.html | 67 ++++++++++++++---------------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/src/baby_monitor/static/sleep.html b/src/baby_monitor/static/sleep.html index 3210345..a73554b 100644 --- a/src/baby_monitor/static/sleep.html +++ b/src/baby_monitor/static/sleep.html @@ -231,33 +231,31 @@ background: #3a9be8; } - .filters { - display: flex; - gap: 15px; + .controls { margin-bottom: 30px; - flex-wrap: wrap; - align-items: center; - } - - .filter-group { display: flex; - flex-direction: column; - gap: 5px; + align-items: center; + gap: 15px; + flex-wrap: wrap; } - .filter-group label { - color: #666; - font-size: 14px; + .controls label { font-weight: 600; + color: #333; } - .filter-group select { - padding: 8px 12px; + .controls select { + padding: 8px 16px; border: 1px solid #ddd; - border-radius: 4px; + border-radius: 6px; font-size: 14px; - background: white; cursor: pointer; + background: white; + } + + .controls select:focus { + outline: none; + border-color: #4facfe; } .badge { @@ -280,24 +278,21 @@

😴 Sleep Tracking

Monitor your baby's sleep patterns

-
-
- - -
-
- - -
+
+ + + + +
@@ -489,7 +484,7 @@
-

Total Sleep Time

+

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