mirror of
https://github.com/sstent/foodplanner.git
synced 2026-01-25 11:11:36 +00:00
281 lines
14 KiB
HTML
281 lines
14 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Daily Calories Chart{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container mt-4 d-flex flex-column min-vh-100">
|
|
<h2><i class="bi bi-graph-up"></i> Daily Calories Chart</h2>
|
|
|
|
<div class="row mb-4">
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center flex-wrap gap-2">
|
|
<label for="daysSelect" class="form-label mb-0 me-2">Select Time Period:</label>
|
|
<select class="form-select w-auto" id="daysSelect">
|
|
<option value="7">Past 7 Days</option>
|
|
<option value="30">Past 30 Days</option>
|
|
</select>
|
|
<button class="btn btn-primary" id="loadChartBtn">Load Chart</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row flex-grow-1">
|
|
<div class="col-md-12 h-100">
|
|
<div class="card h-100">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">Calories Consumed</h5>
|
|
</div>
|
|
<div class="card-body p-2 h-100" id="chartContainer">
|
|
<div class="position-relative h-100">
|
|
<canvas id="caloriesChart" class="w-100 h-100"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0"></script>
|
|
<script>
|
|
let chart;
|
|
const MACRO_KEYS = ['net_carbs', 'fat', 'protein'];
|
|
|
|
function resizeChart() {
|
|
const container = document.getElementById('chartContainer');
|
|
if (container) {
|
|
const rect = container.getBoundingClientRect();
|
|
container.style.height = (window.innerHeight - rect.top) + 'px';
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// Register the plugin
|
|
if (typeof ChartDataLabels !== 'undefined') {
|
|
Chart.register(ChartDataLabels);
|
|
}
|
|
|
|
const daysSelect = document.getElementById('daysSelect');
|
|
const loadBtn = document.getElementById('loadChartBtn');
|
|
const ctx = document.getElementById('caloriesChart').getContext('2d');
|
|
|
|
// Resize chart container to fit viewport
|
|
resizeChart();
|
|
window.addEventListener('resize', resizeChart);
|
|
|
|
// Default load for 7 days
|
|
loadChart(7);
|
|
|
|
loadBtn.addEventListener('click', function () {
|
|
const selectedDays = parseInt(daysSelect.value);
|
|
loadChart(selectedDays);
|
|
});
|
|
|
|
function loadChart(days) {
|
|
fetch(`/api/charts?person=Sarah&days=${days}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// Sort data by date ascending for chart
|
|
data.sort((a, b) => new Date(a.date) - new Date(b.date));
|
|
|
|
const labels = data.map(item => item.date);
|
|
|
|
// Calculate calories from macros
|
|
// Protein: 4 cals/g
|
|
// Fat: 9 cals/g
|
|
// Net Carbs: 4 cals/g
|
|
const proteinCals = data.map(item => item.protein * 4);
|
|
const fatCals = data.map(item => item.fat * 9);
|
|
const netCarbsCals = data.map(item => item.net_carbs * 4);
|
|
|
|
if (chart) {
|
|
chart.destroy();
|
|
}
|
|
|
|
// Resize container before creating chart
|
|
resizeChart();
|
|
|
|
chart = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [
|
|
{
|
|
type: 'line',
|
|
label: 'Weight (lbs)',
|
|
data: data.map(item => item.weight_lbs),
|
|
borderColor: '#0d6efd', // Bootstrap primary (Blue)
|
|
backgroundColor: '#0d6efd',
|
|
borderWidth: 2,
|
|
pointRadius: function (context) {
|
|
const index = context.dataIndex;
|
|
const item = data[index]; // Access data array from outer scope
|
|
|
|
// Show dot if it's a real weight measurement
|
|
if (item.weight_is_real) return 4;
|
|
|
|
// "Or the first point if no datapoints in the view"
|
|
// Check if ANY point in the view is real
|
|
const anyReal = data.some(d => d.weight_is_real);
|
|
if (!anyReal) {
|
|
// Make sure we only show ONE dot (the first one / oldest date)
|
|
// Data is sorted by date ascending in frontend (index 0 is oldest)
|
|
if (index === 0 && item.weight_lbs !== null) return 4;
|
|
}
|
|
|
|
return 0; // Hide dot for inferred points
|
|
},
|
|
yAxisID: 'y1',
|
|
datalabels: {
|
|
display: true,
|
|
align: 'top',
|
|
formatter: function (value, context) {
|
|
// Only show label if radius > 0
|
|
const index = context.dataIndex;
|
|
const item = data[index];
|
|
|
|
// Same logic as pointRadius
|
|
let show = false;
|
|
if (item.weight_is_real) show = true;
|
|
else {
|
|
const anyReal = data.some(d => d.weight_is_real);
|
|
if (!anyReal && index === 0 && item.weight_lbs !== null) show = true;
|
|
}
|
|
|
|
return show ? (value ? value + ' lbs' : '') : '';
|
|
},
|
|
color: '#0d6efd',
|
|
font: { weight: 'bold' }
|
|
},
|
|
spanGaps: true
|
|
},
|
|
{
|
|
label: 'Net Carbs',
|
|
data: netCarbsCals,
|
|
backgroundColor: 'rgba(255, 193, 7, 0.8)', // Bootstrap warning (Yellow)
|
|
borderColor: '#ffc107',
|
|
borderWidth: 1,
|
|
yAxisID: 'y'
|
|
},
|
|
{
|
|
label: 'Fat',
|
|
data: fatCals,
|
|
backgroundColor: 'rgba(220, 53, 69, 0.8)', // Bootstrap danger (Red)
|
|
borderColor: '#dc3545',
|
|
borderWidth: 1,
|
|
yAxisID: 'y'
|
|
},
|
|
{
|
|
label: 'Protein',
|
|
data: proteinCals,
|
|
backgroundColor: 'rgba(25, 135, 84, 0.8)', // Bootstrap success (Green)
|
|
borderColor: '#198754',
|
|
borderWidth: 1,
|
|
yAxisID: 'y'
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
stacked: true,
|
|
title: {
|
|
display: true,
|
|
text: 'Calories'
|
|
}
|
|
},
|
|
y1: {
|
|
type: 'linear',
|
|
display: true,
|
|
position: 'right',
|
|
title: {
|
|
display: true,
|
|
text: 'Weight (lbs)'
|
|
},
|
|
grid: {
|
|
drawOnChartArea: false // only want the grid lines for one axis to show up
|
|
}
|
|
},
|
|
x: {
|
|
stacked: true,
|
|
title: {
|
|
display: true,
|
|
text: 'Date'
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function (context) {
|
|
let label = context.dataset.label || '';
|
|
if (label) {
|
|
label += ': ';
|
|
}
|
|
if (context.parsed.y !== null) {
|
|
if (context.dataset.type === 'line') {
|
|
return label + context.parsed.y + ' lbs';
|
|
}
|
|
const dayData = data[context.dataIndex];
|
|
const macroKey = MACRO_KEYS[context.datasetIndex - 1]; // Offset by 1 due to weight dataset
|
|
const grams = dayData[macroKey];
|
|
label += Math.round(context.parsed.y) + ' cals (' + Math.round(grams) + 'g)';
|
|
}
|
|
return label;
|
|
}
|
|
}
|
|
},
|
|
datalabels: {
|
|
color: 'white',
|
|
font: {
|
|
weight: 'bold',
|
|
size: 11
|
|
},
|
|
display: function (context) {
|
|
if (context.dataset.type === 'line') return false; // Handled separately
|
|
|
|
const dayData = data[context.dataIndex];
|
|
const pC = dayData.protein * 4;
|
|
const fC = dayData.fat * 9;
|
|
const ncC = dayData.net_carbs * 4;
|
|
const calcTotal = pC + fC + ncC;
|
|
|
|
const value = context.dataset.data[context.dataIndex];
|
|
return calcTotal > 0 && (value / calcTotal) > 0.05;
|
|
},
|
|
formatter: function (value, context) {
|
|
if (context.dataset.type === 'line') return '';
|
|
|
|
const dayData = data[context.dataIndex];
|
|
const pC = dayData.protein * 4;
|
|
const fC = dayData.fat * 9;
|
|
const ncC = dayData.net_carbs * 4;
|
|
const calcTotal = pC + fC + ncC;
|
|
|
|
const totalCals = calcTotal || 1;
|
|
const percent = Math.round((value / totalCals) * 100);
|
|
const macroKey = MACRO_KEYS[context.datasetIndex - 1]; // Offset by 1
|
|
const grams = Math.round(dayData[macroKey]);
|
|
|
|
return grams + 'g\n' + percent + '%';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading chart data:', error);
|
|
alert('Failed to load chart data');
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %} |