355 lines
12 KiB
Rust
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
|
|
}
|