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/diapers.html b/src/baby_monitor/static/diapers.html index be13c30..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); } @@ -365,7 +373,7 @@
-

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

+

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

@@ -627,36 +635,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 +811,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..20471b5 100644 --- a/src/baby_monitor/static/feedings.html +++ b/src/baby_monitor/static/feedings.html @@ -304,8 +304,15 @@ let allFeedings = []; let allChildren = []; - let currentDaysRange = 7; + 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"); + if (lastChildId && lastChildId !== "all") { + selectedChildId = parseInt(lastChildId); + } // Load feedings data async function loadFeedings() { @@ -348,12 +355,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); } @@ -402,7 +411,7 @@
-

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

+

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

@@ -651,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; @@ -661,54 +671,241 @@ ); } - // 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); + // Only use completed feedings + const completedFeedings = filteredFeedings.filter(f => f.end_time); - const dateStr = date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }); - labels.push(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); - // Count feedings for this day - const nextDay = new Date(date); - nextDay.setDate(nextDay.getDate() + 1); + const startLabel = periodStart.toLocaleTimeString("en-US", { + hour: "numeric", + hour12: true, + }); + const endLabel = periodEnd.toLocaleTimeString("en-US", { + hour: "numeric", + hour12: true, + }); + labels.push(`${startLabel}-${endLabel}`); - const count = filteredFeedings.filter((f) => { - const feedingDate = new Date(f.start_time); - return feedingDate >= date && feedingDate < nextDay; - }).length; + // 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; + }); + + 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) + 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); + + // 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; + }); + + 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) + 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); + + // Get individual feeding durations for this day (in minutes) + const nextDay = new Date(date); + nextDay.setDate(nextDay.getDate() + 1); + + const dayFeedings = completedFeedings.filter((f) => { + const feedingDate = new Date(f.start_time); + return feedingDate >= date && feedingDate < nextDay; + }); + + 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, @@ -723,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' : ''})`; + } + } }, }, }, 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; diff --git a/src/baby_monitor/static/sleep.html b/src/baby_monitor/static/sleep.html index 10df90c..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

-
-
- - -
-
- - -
+
+ + + + +
@@ -339,6 +334,8 @@ document .getElementById("timeRangeFilter") .addEventListener("change", function () { + // Save selection to localStorage + localStorage.setItem("lastSleepTimeRange", this.value); loadData(); }); @@ -374,6 +371,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: { @@ -480,7 +484,7 @@
-

Sleep Sessions per Day

+

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

@@ -536,41 +540,131 @@ return `${hours}h ${mins}m`; } - function prepareBarChartData(sleeps, timeRange) { - const days = {}; - const today = new Date(); - today.setHours(0, 0, 0, 0); + function prepareBarChartData(sleepSessions, timeRange) { + const now = new Date(); + const labels = []; + const durations = []; // Array of arrays, each containing individual session durations + const sessionCounts = []; // For reference - // 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; + // Only use completed sleep sessions + const completedSessions = sleepSessions.filter(s => s.end_time); + + // If 24 hours selected, show 3-hour aggregates + if (timeRange === 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}`); + + // 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; + }); + + 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) + 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); + + // 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); + + // 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; + }); + + 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) + for (let i = timeRange - 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); + + // Get individual session durations for this day (in hours) + const nextDay = new Date(date); + nextDay.setDate(nextDay.getDate() + 1); + + const daySessions = completedSessions.filter((session) => { + const startDate = new Date(session.start_time); + return startDate >= date && startDate < nextDay; + }); + + 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); + } } - // 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]++; + 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); } }); - - const labels = Object.keys(days).map((date) => { - const d = new Date(date); - return d.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }); - }); - - return { - labels, - data: Object.values(days), - }; + + return { labels, durations, sessionCounts }; } function prepareDurationDistributionData(sleeps) { @@ -599,30 +693,139 @@ }; } + 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); + + // 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: [ - { - label: "Sleep Sessions", - data: chartData.data, - backgroundColor: "rgba(79, 172, 254, 0.6)", - borderColor: "rgba(79, 172, 254, 1)", - borderWidth: 1, - }, - ], + datasets: finalDatasets, }, options: { responsive: true, maintainAspectRatio: false, scales: { + x: { + stacked: true, + }, y: { + stacked: true, beginAtZero: true, + title: { + display: true, + text: useMinutes ? "Minutes" : "Hours", + }, ticks: { - stepSize: 1, + callback: function(value) { + if (useMinutes) { + return Math.round(value) + 'm'; + } else { + return value.toFixed(1) + 'h'; + } + } }, }, }, @@ -630,6 +833,30 @@ legend: { display: false, }, + tooltip: { + callbacks: { + label: function(context) { + 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) { + 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]; + if (hours === 0) { + return `Total: ${minutes}m (${sessionCount} session${sessionCount !== 1 ? 's' : ''})`; + } + return `Total: ${hours}h ${minutes}m (${sessionCount} session${sessionCount !== 1 ? 's' : ''})`; + } + } + }, }, }, }); @@ -637,7 +864,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, @@ -668,6 +901,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); @@ -710,7 +948,7 @@ ); const ctx = document.getElementById("avgDurationChart"); - new Chart(ctx, { + avgDurationChartInstance = new Chart(ctx, { type: "line", data: { labels,