Merge pull request 'improve overview graphs' (#22) from 24-hour-graphs-should-show-individual-hours into main
Build and Push Docker Image / build-and-push (push) Successful in 25s
Python Code Quality / python-code-quality (push) Successful in 10s
Python Test / python-test (push) Successful in 17s

Reviewed-on: #22
This commit was merged in pull request #22.
This commit is contained in:
2025-11-11 14:55:59 +01:00
5 changed files with 717 additions and 178 deletions
+19
View File
@@ -192,6 +192,15 @@
Cancel
</button>
</form>
<div id="skipOption" style="text-align: center; margin-top: 20px; display: none;">
<p style="color: #666; font-size: 14px; margin-bottom: 10px;">
Want to accept a child share invitation first?
</p>
<button type="button" class="button secondary" onclick="skipToHome()" style="margin: 0;">
Skip for now
</button>
</div>
</div>
<script>
@@ -205,11 +214,17 @@
const urlParams = new URLSearchParams(window.location.search);
const childId = urlParams.get("id");
const isEditMode = !!childId;
const fromHome = urlParams.get("from") === "home"; // Check if redirected from home
const form = document.getElementById("addChildForm");
const messageDiv = document.getElementById("message");
const submitBtn = document.getElementById("submitBtn");
// Show skip option if user was redirected from home (has no children)
if (fromHome && !isEditMode) {
document.getElementById("skipOption").style.display = "block";
}
// Update page title and button text if editing
if (isEditMode) {
document.querySelector("h1").textContent = "✏️ Edit Child";
@@ -317,6 +332,10 @@
function goBack() {
window.location.href = "/";
}
function skipToHome() {
window.location.href = "/";
}
</script>
</body>
</html>
+115 -28
View File
@@ -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 @@
</div>
<div class="chart-container">
<h2>Diaper Changes Per Day (Last ${currentDaysRange} Day${currentDaysRange !== 1 ? "s" : ""})</h2>
<h2>Diaper Changes (Last ${currentDaysRange} Day${currentDaysRange !== 1 ? "s" : ""})</h2>
<div class="chart-wrapper">
<canvas id="diaperChart"></canvas>
</div>
@@ -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",
+240 -69
View File
@@ -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 @@
</div>
<div class="chart-container">
<h2>Feedings Per Day (Last ${currentDaysRange} Day${currentDaysRange !== 1 ? "s" : ""})</h2>
<h2>Total Feeding Time (Last ${currentDaysRange} Day${currentDaysRange !== 1 ? "s" : ""})</h2>
<div class="chart-wrapper">
<canvas id="feedingsChart"></canvas>
</div>
@@ -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' : ''})`;
}
}
},
},
},
+26 -2
View File
@@ -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 = `
<h1>👶 Welcome to Baby Monitor</h1>
<div style="text-align: center; padding: 40px 20px;">
<div style="font-size: 64px; margin-bottom: 20px;">🍼</div>
<h2 style="color: #333; margin-bottom: 15px;">No Children Added Yet</h2>
<p style="color: #666; margin-bottom: 30px; line-height: 1.6;">
You can add a child to start tracking or redeem a child share code<br>
from another user to access their child's information.
</p>
<div style="display: flex; gap: 15px; justify-content: center; flex-wrap: wrap;">
<a href="/add-child.html" class="button" style="text-decoration: none; display: inline-block;">
Add Your Child
</a>
<a href="/settings.html" class="button secondary" style="text-decoration: none; display: inline-block;">
🔗 Redeem Share Code
</a>
</div>
</div>
`;
}
let activeFeeding = null;
let userChildren = [];
let lastFeeding = null;
+317 -79
View File
@@ -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 @@
<h1>😴 Sleep Tracking</h1>
<p class="subtitle">Monitor your baby's sleep patterns</p>
<div class="filters">
<div class="filter-group">
<label for="childFilter">Child</label>
<select id="childFilter">
<option value="">All Children</option>
</select>
</div>
<div class="filter-group">
<label for="timeRangeFilter">Time Range</label>
<select id="timeRangeFilter">
<option value="1">Last 24 hours</option>
<option value="3">Last 3 days</option>
<option value="7" selected>Last 7 days</option>
<option value="14">Last 14 days</option>
<option value="30">Last 30 days</option>
<option value="90">Last 90 days</option>
</select>
</div>
<div class="controls">
<label for="childFilter">Child:</label>
<select id="childFilter">
<option value="">All Children</option>
</select>
<label for="timeRangeFilter">Time Range:</label>
<select id="timeRangeFilter">
<option value="1">Last 24 hours</option>
<option value="3">Last 3 days</option>
<option value="7" selected>Last 7 days</option>
<option value="14">Last 14 days</option>
<option value="30">Last 30 days</option>
<option value="90">Last 90 days</option>
</select>
</div>
<div id="content" class="loading">
@@ -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 @@
</div>
<div class="chart-container">
<h2>Sleep Sessions per Day</h2>
<h2>Total Sleep Time (Last ${timeRange} Day${timeRange !== 1 ? "s" : ""})</h2>
<div class="chart-wrapper">
<canvas id="barChart"></canvas>
</div>
@@ -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,