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; 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, purchases: Vec, ) -> (Date, Vec) { 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 = 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] += 1; } }); 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::::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, purchases: Vec, ) -> 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::::from(SystemTime::now()).date_naive(); let max_calories = calorie_days .clone() .into_iter() .reduce(|a, b| a.max(b)) .unwrap(); 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( 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, purchases: Vec, weight: Vec, running: Vec, ) -> plotting::SVGPlotFun { let (mindate, calorie_days) = calculate_calories_per_day(nutrition_map, purchases); let calories_burned_per_day: f32 = 2140.; // 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 = weight .iter() .map(|w| (w.datetime.date() - mindate).whole_days() as f32) .collect(); let weight_values: Vec = 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::>(), &InterpMode::FirstLast, ); let mut running_days: Vec = 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 = calorie_days .iter() .enumerate() .scan(0., |deficit, (i, &calories)| { *deficit += calories - calories_burned_per_day - 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::::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) .build_cartesian_2d( mindate_chrono..maxdate_chrono, (min_weight - 2.)..(max_weight + 2.), // 70_f32..90_f32, ) .unwrap(); chart .configure_mesh() .y_desc("kg fat burned") .y_max_light_lines(4) .x_max_light_lines(6) .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("kg 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()), // mindate_chrono // .checked_add_days(chrono::Days::new(i.try_into().unwrap())) // .unwrap(), 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 }