From a4578c004fec9667df2372aaa115c827d229fd83 Mon Sep 17 00:00:00 2001 From: Knyffen Date: Mon, 11 Nov 2024 14:39:59 +0100 Subject: [PATCH] Plot weight loss --- server/src/main.rs | 22 +++++- server/src/math.rs | 1 + server/src/math/calculations.rs | 116 ++++++++++++++++++++++++++++++++ server/src/my_structs.rs | 2 + server/src/my_structs/weight.rs | 54 +++++++++++++++ 5 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 server/src/my_structs/weight.rs diff --git a/server/src/main.rs b/server/src/main.rs index f09eaaa..7b2e6d3 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -21,6 +21,7 @@ mod math; mod my_structs; use my_structs::MyError; use my_structs::Purchase; +use my_structs::Weight; mod plotting; // use crate::plotting::Plotter; use std::{thread, time}; @@ -68,6 +69,7 @@ async fn main() { .route("/image.png", get(png_image)) .route("/image.svg", get(svg_image)) .route("/calorie_intake.svg", get(svg_calorie_intake)) + .route("/weight_loss.svg", get(svg_weight_loss)) .route("/calorie_intake", get(calorie_intake)) .route("/wasm_test", get(wasm_test)) .route("/camera_test", get(camera_test)) @@ -139,6 +141,21 @@ async fn svg_calorie_intake( Ok(plotting::SVGPlotter::plot(640, 480, plotfun)) } +async fn svg_weight_loss(State(state): State>) -> Result { + println!("Serving: svg_weight_loss"); + + let nutrition_val_promise = Nutrition::get_nutrition_hashmap(state.sql_pool.clone()); + let purchases_val_promise = Purchase::get_purchase_rows(state.sql_pool.clone()); + let weight_val_promise = Weight::get_weight_rows(state.sql_pool.clone()); + + let nutrition_val = nutrition_val_promise.await?; + let purchases_val = purchases_val_promise.await?; + let weight_val = weight_val_promise.await?; + + let plotfun = math::plot_weight_loss(nutrition_val, purchases_val, weight_val); + Ok(plotting::SVGPlotter::plot(640, 480, plotfun)) +} + async fn png_image(State(_state): State>) -> impl IntoResponse { println!("Serving: png_image"); let fun = plotting::BitMapPlotter::examplefun(); @@ -154,7 +171,10 @@ async fn html_plotter(State(_state): State>) -> Html { async fn calorie_intake(State(_state): State>) -> Html { println!("Serving: calorie_intake"); - Html("".to_string()) + Html( + "
" + .to_string(), + ) } fn construct_js(path: &str) -> Result { diff --git a/server/src/math.rs b/server/src/math.rs index 64d6a85..5aa21ad 100644 --- a/server/src/math.rs +++ b/server/src/math.rs @@ -1,3 +1,4 @@ // pub use self::calculations::calculate_calories_per_day; pub use self::calculations::plot_calories_per_day; +pub use self::calculations::plot_weight_loss; mod calculations; diff --git a/server/src/math/calculations.rs b/server/src/math/calculations.rs index 5cbb5b7..3ece96a 100644 --- a/server/src/math/calculations.rs +++ b/server/src/math/calculations.rs @@ -153,3 +153,119 @@ pub fn plot_calories_per_day( }); plotfun } + +pub fn plot_weight_loss( + nutrition_map: HashMap, + purchases: Vec, + weight: Vec, +) -> plotting::SVGPlotFun { + let (mindate, calorie_days) = calculate_calories_per_day(nutrition_map, purchases); + let calories_burned_per_day: f32 = 2200.; + let deficit_days: Vec = calorie_days + .iter() + .scan(0., |deficit, &calories| { + *deficit += calories - calories_burned_per_day; + 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., + ) + }), + 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 +} diff --git a/server/src/my_structs.rs b/server/src/my_structs.rs index 14603ff..9067c05 100644 --- a/server/src/my_structs.rs +++ b/server/src/my_structs.rs @@ -1,4 +1,6 @@ pub use self::myerror::MyError; pub use self::purchases::Purchase; +pub use self::weight::Weight; mod myerror; mod purchases; +mod weight; diff --git a/server/src/my_structs/weight.rs b/server/src/my_structs/weight.rs new file mode 100644 index 0000000..baedf52 --- /dev/null +++ b/server/src/my_structs/weight.rs @@ -0,0 +1,54 @@ +use mysql::prelude::*; +use mysql::*; +use mysql_common::frunk::{hlist_pat, HList}; +use struct_field_names_as_array::FieldNamesAsArray; +use time::PrimitiveDateTime; + +#[allow(non_snake_case)] +#[derive(Debug, PartialEq, FieldNamesAsArray)] +pub struct Weight { + id: u32, + pub weight: f32, + pub datetime: PrimitiveDateTime, +} + +type RowType = HList!(u32, f32, PrimitiveDateTime); + +impl Weight { + fn get_sql_fields() -> String { + Weight::FIELD_NAMES_AS_ARRAY + .iter() + .cloned() + .intersperse(", ") + .collect() + } + + fn query_map_helper() -> (String, impl Fn(RowType) -> Weight) { + let sql_query = format!("SELECT {} from weight", Self::get_sql_fields()); + println!("{}", sql_query); + + let construction_closure = |row: RowType| { + let hlist_pat![id, weight, datetime] = row; + Weight { + id, + weight, + datetime, + } + }; + + (sql_query, construction_closure) + } + + pub async fn get_weight_rows(sql_pool: Pool) -> Result> { + let mut conn = sql_pool + .get_conn() + .expect("Cannot establish database connection"); + + let (weight_query, weight_closure) = Self::query_map_helper(); + let weight_val: Vec = conn + .query_map(weight_query, weight_closure) + .expect("Data in database doesn't match the Weight class"); + + Ok(weight_val) + } +}