added chart

This commit is contained in:
2025-09-30 06:04:19 -07:00
parent d034c7349d
commit 6de9c580a5
5 changed files with 278 additions and 1 deletions

49
app/api/routes/charts.py Normal file
View File

@@ -0,0 +1,49 @@
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from datetime import date, timedelta
from typing import List
from app.database import get_db, TrackedDay, TrackedMeal, calculate_day_nutrition_tracked
router = APIRouter(tags=["charts"])
@router.get("/charts", response_class=HTMLResponse)
async def charts_page(request: Request, person: str = "Sarah", db: Session = Depends(get_db)):
"""Render the charts page"""
from main import templates
return templates.TemplateResponse("charts.html", {
"request": request,
"person": person
})
@router.get("/api/charts", response_model=List[dict])
async def get_charts_data(
person: str = Query(..., description="Person name (e.g., Sarah)"),
days: int = Query(7, description="Number of past days to fetch data for", ge=1, le=30),
db: Session = Depends(get_db)
):
"""
Get daily calorie data for the last N days for a person.
Returns list of {"date": "YYYY-MM-DD", "calories": float} sorted by date descending.
"""
end_date = date.today()
start_date = end_date - timedelta(days=days - 1)
tracked_days = db.query(TrackedDay).filter(
TrackedDay.person == person,
TrackedDay.date >= start_date,
TrackedDay.date <= end_date
).order_by(TrackedDay.date.desc()).all()
chart_data = []
for tracked_day in tracked_days:
tracked_meals = db.query(TrackedMeal).filter(
TrackedMeal.tracked_day_id == tracked_day.id
).all()
day_totals = calculate_day_nutrition_tracked(tracked_meals, db)
chart_data.append({
"date": tracked_day.date.isoformat(),
"calories": round(day_totals.get("calories", 0), 2)
})
return chart_data

View File

@@ -50,7 +50,7 @@ async def lifespan(app: FastAPI):
app = FastAPI(title="Meal Planner", lifespan=lifespan)
templates = Jinja2Templates(directory="templates")
from app.api.routes import foods, meals, plans, templates as templates_router, weekly_menu, tracker, admin, export
from app.api.routes import foods, meals, plans, templates as templates_router, weekly_menu, tracker, admin, export, charts
app.include_router(foods.router, tags=["foods"])
app.include_router(meals.router, tags=["meals"])
@@ -60,6 +60,7 @@ app.include_router(weekly_menu.router, tags=["weekly_menu"])
app.include_router(tracker.router, tags=["tracker"])
app.include_router(admin.router, tags=["admin"])
app.include_router(export.router, tags=["export"])
app.include_router(charts.router, tags=["charts"])
# Add a logging middleware to see incoming requests
@app.middleware("http")

View File

@@ -88,6 +88,11 @@
<i class="bi bi-calendar-check"></i> Tracker
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" onclick="location.href='/charts'">
<i class="bi bi-graph-up"></i> Charts
</button>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" href="/admin">
<i class="bi bi-gear"></i> Admin

109
templates/charts.html Normal file
View File

@@ -0,0 +1,109 @@
{% extends "base.html" %}
{% block title %}Daily Calories Chart{% endblock %}
{% block content %}
<div class="container mt-4">
<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">
<label for="daysSelect" class="form-label">Select Time Period:</label>
<select class="form-select" id="daysSelect">
<option value="7">Past 7 Days</option>
<option value="30">Past 30 Days</option>
</select>
<button class="btn btn-primary mt-2" id="loadChartBtn">Load Chart</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Calories Consumed</h5>
</div>
<div class="card-body">
<canvas id="caloriesChart" width="400" height="200"></canvas>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
let chart;
document.addEventListener('DOMContentLoaded', function() {
const daysSelect = document.getElementById('daysSelect');
const loadBtn = document.getElementById('loadChartBtn');
const ctx = document.getElementById('caloriesChart').getContext('2d');
// 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);
const calories = data.map(item => item.calories);
if (chart) {
chart.destroy();
}
chart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Calories',
data: calories,
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.1,
fill: true
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Calories'
}
},
x: {
title: {
display: true,
text: 'Date'
}
}
}
}
});
})
.catch(error => {
console.error('Error loading chart data:', error);
alert('Failed to load chart data');
});
}
});
</script>
{% endblock %}

113
tests/test_charts.py Normal file
View File

@@ -0,0 +1,113 @@
import pytest
from datetime import date, timedelta
from app.database import TrackedDay, TrackedMeal, Meal, MealFood, Food, calculate_day_nutrition_tracked
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture
def sample_chart_data(db_session):
"""Create sample tracked data for chart testing"""
# Create sample food
food = Food(
name="Sample Food",
serving_size="100g",
serving_unit="g",
calories=100.0,
protein=10.0,
carbs=20.0,
fat=5.0
)
db_session.add(food)
db_session.commit()
db_session.refresh(food)
# Create sample meal
meal = Meal(
name="Sample Meal",
meal_type="breakfast",
meal_time="Breakfast"
)
db_session.add(meal)
db_session.commit()
db_session.refresh(meal)
# Link meal to food
meal_food = MealFood(
meal_id=meal.id,
food_id=food.id,
quantity=1.0
)
db_session.add(meal_food)
db_session.commit()
# Create tracked days
person = "Sarah"
today = date.today()
tracked_days = []
for i in range(3): # Last 3 days
tracked_date = today - timedelta(days=i)
tracked_day = TrackedDay(
person=person,
date=tracked_date,
is_modified=False
)
db_session.add(tracked_day)
db_session.commit()
db_session.refresh(tracked_day)
# Add a tracked meal
tracked_meal = TrackedMeal(
tracked_day_id=tracked_day.id,
meal_id=meal.id,
meal_time="Breakfast",
quantity=1.0
)
db_session.add(tracked_meal)
db_session.commit()
tracked_days.append(tracked_day)
return tracked_days, meal
def test_get_charts_data(client, db_session, sample_chart_data):
"""Test the charts data endpoint returns correct calorie data"""
tracked_days, meal = sample_chart_data
# Expected calories: 100 per day
expected_data = [
{
"date": tracked_days[0].date.isoformat(),
"calories": 100.0
},
{
"date": tracked_days[1].date.isoformat(),
"calories": 100.0
},
{
"date": tracked_days[2].date.isoformat(),
"calories": 100.0
}
]
response = client.get("/api/charts?person=Sarah&days=3")
assert response.status_code == 200
data = response.json()
# Sort by date descending
data_sorted = sorted(data, key=lambda x: x["date"], reverse=True)
assert data_sorted == expected_data
def test_get_charts_data_default_days(client, db_session, sample_chart_data):
"""Test default days parameter"""
response = client.get("/api/charts?person=Sarah")
assert response.status_code == 200
data = response.json()
assert len(data) == 3 # Should return last 3 days
def test_get_charts_data_no_data(client, db_session):
"""Test endpoint when no tracked data exists"""
response = client.get("/api/charts?person=Sarah&days=7")
assert response.status_code == 200
data = response.json()
assert data == [] # Empty list if no data