calories/server/src/math/calculations.rs

355 lines
12 KiB
Rust

use crate::my_structs;
use crate::plotting;
// use chrono::TimeZone;
use chrono::{DateTime, NaiveDate, Utc};
use interp::{interp_slice, InterpMode};
use plotters::prelude::*;
use shared::structs::Nutrition;
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
use time::Date;
use time::*;
const MEALS_PER_DAY: f32 = 2.3;
const SMOOTHING_DAYS: usize = 21;
const CALORIES_BURNED_PER_DAY: f32 = 2270.;
const CALORIES_BURNED_PER_DAY_BASE_WEIGHT: f32 = 83.;
const CALORIES_BURNED_LESS_PER_DAY_PER_KG: f32 = 12.5;
fn time_date_to_chrono_naive_date(d: Date) -> chrono::NaiveDate {
chrono::NaiveDate::from_ymd_opt(d.year(), d.month() as u32, d.day() as u32).unwrap()
}
fn calculate_calories_per_day(
nutrition_map: HashMap<u32, Nutrition>,
purchases: Vec<my_structs::Purchase>,
) -> (Date, Vec<f32>) {
if purchases.is_empty() {
return (Date::MIN, vec![]); // TODO: vec![] or vec![0.]?
}
// let mindate = purchases.into_iter(); //.map(|x| x.datetime.date()).min();
let mindate: Date = purchases.iter().map(|x| x.datetime.date()).min().unwrap();
let maxdate: Date = purchases.iter().map(|x| x.datetime.date()).max().unwrap();
let date_range: usize = ((maxdate - mindate).whole_days() + 1).try_into().unwrap();
let mut calorie_purchases = vec![0.; date_range];
let mut mystery_meals: Vec<u32> = vec![0; date_range];
purchases.iter().for_each(|p| {
let dateindex: usize = (p.datetime.date() - mindate)
.whole_days()
.try_into()
.unwrap();
let nutrition = &nutrition_map[&p.nutrition_id];
if nutrition.is_valid() {
let calories = nutrition_map[&p.nutrition_id].total_kcal() * p.amount as f32;
calorie_purchases[dateindex] += calories;
} else {
mystery_meals[dateindex] += p.amount;
}
});
let mut calorie_days = vec![0.; date_range + SMOOTHING_DAYS];
calorie_purchases
.iter()
.enumerate()
.for_each(|(i, calories)| {
let calories_smoothed = calories / SMOOTHING_DAYS as f32;
for item in &mut calorie_days[i..i + SMOOTHING_DAYS] {
*item += calories_smoothed;
}
});
mystery_meals.iter().enumerate().for_each(|(i, amount)| {
let calories = calorie_days[i] / MEALS_PER_DAY * *amount as f32;
let calories_smoothed = calories / SMOOTHING_DAYS as f32;
for item in &mut calorie_days[i..i + SMOOTHING_DAYS] {
*item += calories_smoothed;
}
});
// Cut plot at today
// let today_chrono = DateTime::<Utc>::from(SystemTime::now()).date_naive();
// let mindate_chrono = time_date_to_chrono_naive_date(mindate);
// let days_since_mindate = (today_chrono - mindate_chrono).num_days() + 1;
// calorie_days.resize(days_since_mindate as usize, 0.);
// println!("{:?}", calorie_purchases);
// println!("{:?}", mystery_meals);
// println!("{:?}", calorie_days);
(mindate, calorie_days)
}
pub fn plot_calories_per_day(
nutrition_map: HashMap<u32, Nutrition>,
purchases: Vec<my_structs::Purchase>,
weight: Vec<my_structs::Weight>,
) -> plotting::SVGPlotFun {
let (mindate, calorie_days) = calculate_calories_per_day(nutrition_map, purchases);
let maxdate = mindate
.checked_add(Duration::days(calorie_days.len().try_into().unwrap()))
.unwrap();
let mindate_chrono = time_date_to_chrono_naive_date(mindate);
let maxdate_chrono = time_date_to_chrono_naive_date(maxdate);
let today_chrono = DateTime::<Utc>::from(SystemTime::now()).date_naive();
let max_calories = calorie_days
.clone()
.into_iter()
.reduce(|a, b| a.max(b))
.unwrap();
let weight_dates: Vec<f32> = weight
.iter()
.map(|w| (w.datetime.date() - mindate).whole_days() as f32)
.collect();
let weight_values: Vec<f32> = weight.iter().map(|w| w.weight).collect();
let weight_days = interp_slice(
&weight_dates,
&weight_values,
&(0..calorie_days.len())
.map(|x| x as f32)
.collect::<Vec<f32>>(),
&InterpMode::FirstLast,
);
let calories_burned_per_day_days: Vec<f32> = weight_days
.iter()
.map(|weight| {
CALORIES_BURNED_PER_DAY
+ CALORIES_BURNED_LESS_PER_DAY_PER_KG
* (weight - CALORIES_BURNED_PER_DAY_BASE_WEIGHT)
})
.collect();
let plotfun = Box::new(move |root: plotting::SVGPlotRoot| {
root.fill(&WHITE).unwrap();
let mut chart = ChartBuilder::on(&root)
.caption("Calorie intake", ("sans-serif", 50).into_font())
// .margin(20)
.x_label_area_size(30)
.y_label_area_size(50)
.build_cartesian_2d(mindate_chrono..maxdate_chrono, 0f32..max_calories + 200.)
.unwrap();
chart
.configure_mesh()
.y_desc("kcal")
.y_max_light_lines(4)
.x_max_light_lines(6)
.draw()
.unwrap();
chart
.draw_series(LineSeries::new(
vec![(today_chrono, 0.), (today_chrono, max_calories + 200.)],
BLACK.stroke_width(2),
))
.unwrap()
.label("today")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], BLACK));
chart
.draw_series(LineSeries::new(
calories_burned_per_day_days
.iter()
.enumerate()
.map(|(i, cal)| {
(
mindate_chrono
.checked_add_days(chrono::Days::new(i.try_into().unwrap()))
.unwrap(),
*cal,
)
}),
BLACK.stroke_width(2),
))
.unwrap()
.label("calories burned per day")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], BLACK));
chart
.draw_series(LineSeries::new(
calorie_days.iter().enumerate().map(|(i, cal)| {
(
mindate_chrono
.checked_add_days(chrono::Days::new(i.try_into().unwrap()))
.unwrap(),
*cal,
)
}),
RED.stroke_width(3),
))
.unwrap()
.label("kcal")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED));
// Show labels
chart
.configure_series_labels()
.background_style(WHITE.mix(0.8))
.border_style(BLACK)
.position(SeriesLabelPosition::LowerRight)
.draw()
.unwrap();
root.present().unwrap();
});
plotfun
}
pub fn plot_weight_loss(
nutrition_map: HashMap<u32, Nutrition>,
purchases: Vec<my_structs::Purchase>,
weight: Vec<my_structs::Weight>,
running: Vec<my_structs::Running>,
) -> plotting::SVGPlotFun {
let (mindate, calorie_days) = calculate_calories_per_day(nutrition_map, purchases);
// Based on the Caloie Burned by Distance Calculator
// https://www.calculator.net/calories-burned-calculator.html
// if running between 5 minutes per km, and 7 minutes per km,
// we get the approximate formula: kcal = (0.94*kg + 10.01)*km
let weight_dates: Vec<f32> = weight
.iter()
.map(|w| (w.datetime.date() - mindate).whole_days() as f32)
.collect();
let weight_values: Vec<f32> = weight.iter().map(|w| w.weight).collect();
let weight_days = interp_slice(
&weight_dates,
&weight_values,
&(0..calorie_days.len())
.map(|x| x as f32)
.collect::<Vec<f32>>(),
&InterpMode::FirstLast,
);
let mut running_days: Vec<f32> = vec![0.; calorie_days.len()];
running.into_iter().for_each(|r| {
let idx: usize = (r.datetime.date() - mindate)
.whole_days()
.try_into()
.unwrap();
running_days[idx] += (0.94 * weight_days[idx] + 10.01) * r.distance;
});
let deficit_days: Vec<f32> = calorie_days
.iter()
.enumerate()
.scan(0., |deficit, (i, &calories)| {
*deficit += calories
- (CALORIES_BURNED_PER_DAY
+ CALORIES_BURNED_LESS_PER_DAY_PER_KG
* (weight_days[i] - CALORIES_BURNED_PER_DAY_BASE_WEIGHT))
- running_days[i];
Some(*deficit)
})
.collect();
let maxdate = mindate
.checked_add(Duration::days(deficit_days.len().try_into().unwrap()))
.unwrap();
let mindate_chrono = time_date_to_chrono_naive_date(mindate);
let maxdate_chrono = time_date_to_chrono_naive_date(maxdate);
let today_chrono = DateTime::<Utc>::from(SystemTime::now()).date_naive();
let max_weight = weight
.iter()
.map(|w| w.weight)
.reduce(|a, b| a.max(b))
.unwrap();
let min_weight = weight
.iter()
.map(|w| w.weight)
.reduce(|a, b| a.min(b))
.unwrap();
let plotfun = Box::new(move |root: plotting::SVGPlotRoot| {
root.fill(&WHITE).unwrap();
let mut chart = ChartBuilder::on(&root)
.caption("Weight loss", ("sans-serif", 50).into_font())
// .margin(20)
.x_label_area_size(30)
.y_label_area_size(50)
.right_y_label_area_size(50)
.build_cartesian_2d(
mindate_chrono..maxdate_chrono,
(min_weight - 2.)..(max_weight + 2.),
)
.unwrap()
.set_secondary_coord(
mindate_chrono..maxdate_chrono,
(min_weight - max_weight - 2.)..2.,
);
chart
.configure_mesh()
.y_desc("weight [kg]")
.y_max_light_lines(4)
.x_max_light_lines(6)
.draw()
.unwrap();
chart
.configure_secondary_axes()
.y_desc("fat burned [kg]")
.draw()
.unwrap();
chart
.draw_series(LineSeries::new(
vec![
(today_chrono, min_weight - 2.),
(today_chrono, max_weight + 2.),
],
BLACK.stroke_width(2),
))
.unwrap()
.label("today")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], BLACK));
chart
.draw_series(LineSeries::new(
deficit_days.iter().enumerate().map(|(i, cal)| {
(
mindate_chrono
.checked_add_days(chrono::Days::new(i.try_into().unwrap()))
.unwrap(),
*cal / 7000. + (weight[0].weight + 1.), // the non-calories values is an
// offset to make the two plotted
// lines fit
)
}),
RED.stroke_width(3),
))
.unwrap()
.label("fat burned")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED));
chart
.draw_series(LineSeries::new(
weight
.iter()
.map(|w| (time_date_to_chrono_naive_date(w.datetime.date()), w.weight)),
BLUE.stroke_width(3),
))
.unwrap()
.label("weight")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], BLUE));
// Show labels
chart
.configure_series_labels()
.background_style(WHITE.mix(0.8))
.border_style(BLACK)
.position(SeriesLabelPosition::LowerRight)
.draw()
.unwrap();
root.present().unwrap();
});
plotfun
}