Merge pull request 'improve overview graphs' (#22) from 24-hour-graphs-should-show-individual-hours into main
Reviewed-on: #22
This commit was merged in pull request #22.
This commit is contained in:
@@ -192,6 +192,15 @@
|
|||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -205,11 +214,17 @@
|
|||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const childId = urlParams.get("id");
|
const childId = urlParams.get("id");
|
||||||
const isEditMode = !!childId;
|
const isEditMode = !!childId;
|
||||||
|
const fromHome = urlParams.get("from") === "home"; // Check if redirected from home
|
||||||
|
|
||||||
const form = document.getElementById("addChildForm");
|
const form = document.getElementById("addChildForm");
|
||||||
const messageDiv = document.getElementById("message");
|
const messageDiv = document.getElementById("message");
|
||||||
const submitBtn = document.getElementById("submitBtn");
|
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
|
// Update page title and button text if editing
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
document.querySelector("h1").textContent = "✏️ Edit Child";
|
document.querySelector("h1").textContent = "✏️ Edit Child";
|
||||||
@@ -317,6 +332,10 @@
|
|||||||
function goBack() {
|
function goBack() {
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function skipToHome() {
|
||||||
|
window.location.href = "/";
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -267,9 +267,15 @@
|
|||||||
|
|
||||||
let allDiaperChanges = [];
|
let allDiaperChanges = [];
|
||||||
let allChildren = [];
|
let allChildren = [];
|
||||||
let currentDaysRange = 7;
|
let currentDaysRange = parseInt(localStorage.getItem("lastDiaperTimeRange")) || 7;
|
||||||
let selectedChildId = null; // null means "All Children"
|
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
|
// Load diaper changes data
|
||||||
async function loadDiaperChanges() {
|
async function loadDiaperChanges() {
|
||||||
try {
|
try {
|
||||||
@@ -311,12 +317,14 @@
|
|||||||
|
|
||||||
function onDaysRangeChange(event) {
|
function onDaysRangeChange(event) {
|
||||||
currentDaysRange = parseInt(event.target.value);
|
currentDaysRange = parseInt(event.target.value);
|
||||||
|
localStorage.setItem("lastDiaperTimeRange", currentDaysRange);
|
||||||
displayDiaperChanges(allDiaperChanges);
|
displayDiaperChanges(allDiaperChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChildChange(event) {
|
function onChildChange(event) {
|
||||||
selectedChildId =
|
selectedChildId =
|
||||||
event.target.value === "all" ? null : parseInt(event.target.value);
|
event.target.value === "all" ? null : parseInt(event.target.value);
|
||||||
|
localStorage.setItem("lastDiaperChildFilter", event.target.value);
|
||||||
displayDiaperChanges(allDiaperChanges);
|
displayDiaperChanges(allDiaperChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +373,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chart-container">
|
<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">
|
<div class="chart-wrapper">
|
||||||
<canvas id="diaperChart"></canvas>
|
<canvas id="diaperChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
@@ -627,36 +635,115 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create days based on currentDaysRange (including today)
|
// If 24 hours selected, show 3-hour aggregates
|
||||||
for (let i = currentDaysRange - 1; i >= 0; i--) {
|
if (currentDaysRange === 1) {
|
||||||
const date = new Date(now);
|
// Create 8 three-hour buckets
|
||||||
date.setDate(date.getDate() - i);
|
for (let i = 7; i >= 0; i--) {
|
||||||
date.setHours(0, 0, 0, 0);
|
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", {
|
const startLabel = periodStart.toLocaleTimeString("en-US", {
|
||||||
month: "short",
|
hour: "numeric",
|
||||||
day: "numeric",
|
hour12: true,
|
||||||
});
|
});
|
||||||
labels.push(dateStr);
|
const endLabel = periodEnd.toLocaleTimeString("en-US", {
|
||||||
|
hour: "numeric",
|
||||||
|
hour12: true,
|
||||||
|
});
|
||||||
|
labels.push(`${startLabel}-${endLabel}`);
|
||||||
|
|
||||||
// Count poop and pee diapers for this day
|
// Count poop and pee diapers for this 3-hour period
|
||||||
const nextDay = new Date(date);
|
const periodChanges = filteredChanges.filter((dc) => {
|
||||||
nextDay.setDate(nextDay.getDate() + 1);
|
const changeDate = new Date(dc.change_time);
|
||||||
|
return changeDate >= periodStart && changeDate < periodEnd;
|
||||||
|
});
|
||||||
|
|
||||||
const dayChanges = filteredChanges.filter((dc) => {
|
const poopCount = periodChanges.filter(
|
||||||
const changeDate = new Date(dc.change_time);
|
(dc) => dc.poop_amount !== null,
|
||||||
return changeDate >= date && changeDate < nextDay;
|
).length;
|
||||||
});
|
const peeCount = periodChanges.filter(
|
||||||
|
(dc) => dc.pee_amount !== null,
|
||||||
|
).length;
|
||||||
|
|
||||||
const poopCount = dayChanges.filter(
|
poopData.push(poopCount);
|
||||||
(dc) => dc.poop_amount !== null,
|
peeData.push(peeCount);
|
||||||
).length;
|
}
|
||||||
const peeCount = dayChanges.filter(
|
} else if (currentDaysRange === 3) {
|
||||||
(dc) => dc.pee_amount !== null,
|
// Show 8-hour aggregates for 3 days (9 buckets total)
|
||||||
).length;
|
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);
|
// Format label based on the 8-hour period
|
||||||
peeData.push(peeCount);
|
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 };
|
return { labels, poopData, peeData };
|
||||||
@@ -724,7 +811,7 @@
|
|||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: "Number of Diapers",
|
text: "Number of Changes",
|
||||||
font: {
|
font: {
|
||||||
size: 13,
|
size: 13,
|
||||||
weight: "600",
|
weight: "600",
|
||||||
|
|||||||
@@ -304,8 +304,15 @@
|
|||||||
|
|
||||||
let allFeedings = [];
|
let allFeedings = [];
|
||||||
let allChildren = [];
|
let allChildren = [];
|
||||||
let currentDaysRange = 7;
|
let currentDaysRange = parseInt(localStorage.getItem("lastFeedingTimeRange")) || 7;
|
||||||
let selectedChildId = null; // null means "All Children"
|
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
|
// Load feedings data
|
||||||
async function loadFeedings() {
|
async function loadFeedings() {
|
||||||
@@ -348,12 +355,14 @@
|
|||||||
|
|
||||||
function onDaysRangeChange(event) {
|
function onDaysRangeChange(event) {
|
||||||
currentDaysRange = parseInt(event.target.value);
|
currentDaysRange = parseInt(event.target.value);
|
||||||
|
localStorage.setItem("lastFeedingTimeRange", currentDaysRange);
|
||||||
displayFeedings(allFeedings);
|
displayFeedings(allFeedings);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChildChange(event) {
|
function onChildChange(event) {
|
||||||
selectedChildId =
|
selectedChildId =
|
||||||
event.target.value === "all" ? null : parseInt(event.target.value);
|
event.target.value === "all" ? null : parseInt(event.target.value);
|
||||||
|
localStorage.setItem("lastFeedingChildFilter", event.target.value);
|
||||||
displayFeedings(allFeedings);
|
displayFeedings(allFeedings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,7 +411,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chart-container">
|
<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">
|
<div class="chart-wrapper">
|
||||||
<canvas id="feedingsChart"></canvas>
|
<canvas id="feedingsChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
@@ -651,7 +660,8 @@
|
|||||||
function prepareBarChartData(feedings) {
|
function prepareBarChartData(feedings) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const labels = [];
|
const labels = [];
|
||||||
const data = [];
|
const durations = []; // Array of arrays, each containing individual feeding durations
|
||||||
|
const feedingCounts = []; // For reference
|
||||||
|
|
||||||
// Filter by selected child if applicable
|
// Filter by selected child if applicable
|
||||||
let filteredFeedings = feedings;
|
let filteredFeedings = feedings;
|
||||||
@@ -661,54 +671,241 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create days based on currentDaysRange (including today)
|
// Only use completed feedings
|
||||||
for (let i = currentDaysRange - 1; i >= 0; i--) {
|
const completedFeedings = filteredFeedings.filter(f => f.end_time);
|
||||||
const date = new Date(now);
|
|
||||||
date.setDate(date.getDate() - i);
|
|
||||||
date.setHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
const dateStr = date.toLocaleDateString("en-US", {
|
// If 24 hours selected, show 3-hour aggregates
|
||||||
month: "short",
|
if (currentDaysRange === 1) {
|
||||||
day: "numeric",
|
// Create 8 three-hour buckets
|
||||||
});
|
for (let i = 7; i >= 0; i--) {
|
||||||
labels.push(dateStr);
|
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 startLabel = periodStart.toLocaleTimeString("en-US", {
|
||||||
const nextDay = new Date(date);
|
hour: "numeric",
|
||||||
nextDay.setDate(nextDay.getDate() + 1);
|
hour12: true,
|
||||||
|
});
|
||||||
|
const endLabel = periodEnd.toLocaleTimeString("en-US", {
|
||||||
|
hour: "numeric",
|
||||||
|
hour12: true,
|
||||||
|
});
|
||||||
|
labels.push(`${startLabel}-${endLabel}`);
|
||||||
|
|
||||||
const count = filteredFeedings.filter((f) => {
|
// Get individual feeding durations for this 3-hour period (in minutes)
|
||||||
const feedingDate = new Date(f.start_time);
|
const periodFeedings = completedFeedings.filter((f) => {
|
||||||
return feedingDate >= date && feedingDate < nextDay;
|
const feedingDate = new Date(f.start_time);
|
||||||
}).length;
|
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) {
|
function renderBarChart(chartData) {
|
||||||
const ctx = document.getElementById("feedingsChart").getContext("2d");
|
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",
|
type: "bar",
|
||||||
data: {
|
data: {
|
||||||
labels: chartData.labels,
|
labels: chartData.labels,
|
||||||
datasets: [
|
datasets: datasets,
|
||||||
{
|
|
||||||
label: "Feedings",
|
|
||||||
data: chartData.data,
|
|
||||||
backgroundColor: "rgba(102, 126, 234, 0.7)",
|
|
||||||
borderColor: "rgba(102, 126, 234, 1)",
|
|
||||||
borderWidth: 2,
|
|
||||||
borderRadius: 6,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
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: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: false,
|
display: false,
|
||||||
@@ -723,43 +920,17 @@
|
|||||||
size: 13,
|
size: 13,
|
||||||
},
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: function (context) {
|
label: function(context) {
|
||||||
const count = context.parsed.y;
|
const minutes = Math.round(context.parsed.y);
|
||||||
return `${count} feeding${count !== 1 ? "s" : ""}`;
|
return `${context.dataset.label}: ${minutes}m`;
|
||||||
},
|
},
|
||||||
},
|
footer: function(tooltipItems) {
|
||||||
},
|
const periodIndex = tooltipItems[0].dataIndex;
|
||||||
},
|
const totalMinutes = chartData.durations[periodIndex].reduce((sum, val) => sum + val, 0);
|
||||||
scales: {
|
const feedingCount = chartData.feedingCounts[periodIndex];
|
||||||
y: {
|
return `Total: ${Math.round(totalMinutes)}m (${feedingCount} feeding${feedingCount !== 1 ? 's' : ''})`;
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -275,9 +275,9 @@
|
|||||||
})
|
})
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((children) => {
|
.then((children) => {
|
||||||
// If no children, redirect to add-child page
|
// If no children, show welcome message with option to add
|
||||||
if (children.length === 0) {
|
if (children.length === 0) {
|
||||||
window.location.href = "/add-child.html";
|
showNoChildrenMessage();
|
||||||
return;
|
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 activeFeeding = null;
|
||||||
let userChildren = [];
|
let userChildren = [];
|
||||||
let lastFeeding = null;
|
let lastFeeding = null;
|
||||||
|
|||||||
@@ -231,33 +231,31 @@
|
|||||||
background: #3a9be8;
|
background: #3a9be8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters {
|
.controls {
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-group {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-group label {
|
.controls label {
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-group select {
|
.controls select {
|
||||||
padding: 8px 12px;
|
padding: 8px 16px;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
background: white;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #4facfe;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
@@ -280,24 +278,21 @@
|
|||||||
<h1>😴 Sleep Tracking</h1>
|
<h1>😴 Sleep Tracking</h1>
|
||||||
<p class="subtitle">Monitor your baby's sleep patterns</p>
|
<p class="subtitle">Monitor your baby's sleep patterns</p>
|
||||||
|
|
||||||
<div class="filters">
|
<div class="controls">
|
||||||
<div class="filter-group">
|
<label for="childFilter">Child:</label>
|
||||||
<label for="childFilter">Child</label>
|
<select id="childFilter">
|
||||||
<select id="childFilter">
|
<option value="">All Children</option>
|
||||||
<option value="">All Children</option>
|
</select>
|
||||||
</select>
|
|
||||||
</div>
|
<label for="timeRangeFilter">Time Range:</label>
|
||||||
<div class="filter-group">
|
<select id="timeRangeFilter">
|
||||||
<label for="timeRangeFilter">Time Range</label>
|
<option value="1">Last 24 hours</option>
|
||||||
<select id="timeRangeFilter">
|
<option value="3">Last 3 days</option>
|
||||||
<option value="1">Last 24 hours</option>
|
<option value="7" selected>Last 7 days</option>
|
||||||
<option value="3">Last 3 days</option>
|
<option value="14">Last 14 days</option>
|
||||||
<option value="7" selected>Last 7 days</option>
|
<option value="30">Last 30 days</option>
|
||||||
<option value="14">Last 14 days</option>
|
<option value="90">Last 90 days</option>
|
||||||
<option value="30">Last 30 days</option>
|
</select>
|
||||||
<option value="90">Last 90 days</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="content" class="loading">
|
<div id="content" class="loading">
|
||||||
@@ -339,6 +334,8 @@
|
|||||||
document
|
document
|
||||||
.getElementById("timeRangeFilter")
|
.getElementById("timeRangeFilter")
|
||||||
.addEventListener("change", function () {
|
.addEventListener("change", function () {
|
||||||
|
// Save selection to localStorage
|
||||||
|
localStorage.setItem("lastSleepTimeRange", this.value);
|
||||||
loadData();
|
loadData();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -374,6 +371,13 @@
|
|||||||
selectedChildId = lastChildId;
|
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
|
// Fetch sleep logs
|
||||||
const sleepResponse = await fetch("/api/sleep", {
|
const sleepResponse = await fetch("/api/sleep", {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -480,7 +484,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chart-container">
|
<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">
|
<div class="chart-wrapper">
|
||||||
<canvas id="barChart"></canvas>
|
<canvas id="barChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
@@ -536,41 +540,131 @@
|
|||||||
return `${hours}h ${mins}m`;
|
return `${hours}h ${mins}m`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareBarChartData(sleeps, timeRange) {
|
function prepareBarChartData(sleepSessions, timeRange) {
|
||||||
const days = {};
|
const now = new Date();
|
||||||
const today = new Date();
|
const labels = [];
|
||||||
today.setHours(0, 0, 0, 0);
|
const durations = []; // Array of arrays, each containing individual session durations
|
||||||
|
const sessionCounts = []; // For reference
|
||||||
|
|
||||||
// Initialize all days in range
|
// Only use completed sleep sessions
|
||||||
for (let i = timeRange - 1; i >= 0; i--) {
|
const completedSessions = sleepSessions.filter(s => s.end_time);
|
||||||
const date = new Date(today);
|
|
||||||
date.setDate(date.getDate() - i);
|
// If 24 hours selected, show 3-hour aggregates
|
||||||
const dateStr = date.toISOString().split("T")[0];
|
if (timeRange === 1) {
|
||||||
days[dateStr] = 0;
|
// 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
|
console.log('Final prepareBarChartData result:', { labels, durations, sessionCounts });
|
||||||
sleeps.forEach((sleep) => {
|
|
||||||
const date = new Date(sleep.start_time);
|
// Log the actual duration values for debugging
|
||||||
date.setHours(0, 0, 0, 0);
|
durations.forEach((periodDurations, idx) => {
|
||||||
const dateStr = date.toISOString().split("T")[0];
|
if (periodDurations.length > 0) {
|
||||||
if (days.hasOwnProperty(dateStr)) {
|
console.log(` ${labels[idx]}: durations =`, periodDurations);
|
||||||
days[dateStr]++;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const labels = Object.keys(days).map((date) => {
|
return { labels, durations, sessionCounts };
|
||||||
const d = new Date(date);
|
|
||||||
return d.toLocaleDateString("en-US", {
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
labels,
|
|
||||||
data: Object.values(days),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareDurationDistributionData(sleeps) {
|
function prepareDurationDistributionData(sleeps) {
|
||||||
@@ -599,30 +693,139 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let barChartInstance = null;
|
||||||
|
let durationChartInstance = null;
|
||||||
|
let avgDurationChartInstance = null;
|
||||||
|
|
||||||
function renderBarChart(chartData) {
|
function renderBarChart(chartData) {
|
||||||
const ctx = document.getElementById("barChart");
|
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",
|
type: "bar",
|
||||||
data: {
|
data: {
|
||||||
labels: chartData.labels,
|
labels: chartData.labels,
|
||||||
datasets: [
|
datasets: finalDatasets,
|
||||||
{
|
|
||||||
label: "Sleep Sessions",
|
|
||||||
data: chartData.data,
|
|
||||||
backgroundColor: "rgba(79, 172, 254, 0.6)",
|
|
||||||
borderColor: "rgba(79, 172, 254, 1)",
|
|
||||||
borderWidth: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
scales: {
|
scales: {
|
||||||
|
x: {
|
||||||
|
stacked: true,
|
||||||
|
},
|
||||||
y: {
|
y: {
|
||||||
|
stacked: true,
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: useMinutes ? "Minutes" : "Hours",
|
||||||
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
stepSize: 1,
|
callback: function(value) {
|
||||||
|
if (useMinutes) {
|
||||||
|
return Math.round(value) + 'm';
|
||||||
|
} else {
|
||||||
|
return value.toFixed(1) + 'h';
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -630,6 +833,30 @@
|
|||||||
legend: {
|
legend: {
|
||||||
display: false,
|
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) {
|
function renderDurationChart(chartData) {
|
||||||
const ctx = document.getElementById("durationChart");
|
const ctx = document.getElementById("durationChart");
|
||||||
new Chart(ctx, {
|
|
||||||
|
// Destroy existing chart if it exists
|
||||||
|
if (durationChartInstance) {
|
||||||
|
durationChartInstance.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
durationChartInstance = new Chart(ctx, {
|
||||||
type: "pie",
|
type: "pie",
|
||||||
data: {
|
data: {
|
||||||
labels: chartData.labels,
|
labels: chartData.labels,
|
||||||
@@ -668,6 +901,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAvgDurationChart(sleeps, timeRange) {
|
function renderAvgDurationChart(sleeps, timeRange) {
|
||||||
|
// Destroy existing chart if it exists
|
||||||
|
if (avgDurationChartInstance) {
|
||||||
|
avgDurationChartInstance.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
const days = {};
|
const days = {};
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
@@ -710,7 +948,7 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
const ctx = document.getElementById("avgDurationChart");
|
const ctx = document.getElementById("avgDurationChart");
|
||||||
new Chart(ctx, {
|
avgDurationChartInstance = new Chart(ctx, {
|
||||||
type: "line",
|
type: "line",
|
||||||
data: {
|
data: {
|
||||||
labels,
|
labels,
|
||||||
|
|||||||
Reference in New Issue
Block a user