From 1d1bba51e7ccc0550e058b5b0dd222aa67b689dc Mon Sep 17 00:00:00 2001 From: Alexander Senier Date: Sun, 10 Nov 2024 15:30:40 +0100 Subject: [PATCH] WIP: Try alternative graph layouts --- frontend/src/ui/common.rs | 258 ++++++++++++++---------- frontend/src/ui/data.rs | 59 ++---- frontend/src/ui/page/body_fat.rs | 7 +- frontend/src/ui/page/body_weight.rs | 44 ++-- frontend/src/ui/page/exercise.rs | 52 +++-- frontend/src/ui/page/menstrual_cycle.rs | 2 +- frontend/src/ui/page/muscles.rs | 38 ++-- frontend/src/ui/page/routine.rs | 35 ++-- frontend/src/ui/page/training.rs | 37 ++-- 9 files changed, 286 insertions(+), 246 deletions(-) diff --git a/frontend/src/ui/common.rs b/frontend/src/ui/common.rs index cf5c466..45560b9 100644 --- a/frontend/src/ui/common.rs +++ b/frontend/src/ui/common.rs @@ -8,20 +8,20 @@ use crate::{domain, ui::data}; pub const ENTER_KEY: u32 = 13; -pub const COLOR_BODY_WEIGHT: usize = 1; -pub const COLOR_AVG_BODY_WEIGHT: usize = 2; +pub const COLOR_BODY_WEIGHT: usize = 19; +pub const COLOR_AVG_BODY_WEIGHT: usize = 1; pub const COLOR_BODY_FAT_JP3: usize = 4; pub const COLOR_BODY_FAT_JP7: usize = 0; pub const COLOR_PERIOD_INTENSITY: usize = 0; -pub const COLOR_LOAD: usize = 1; -pub const COLOR_LONG_TERM_LOAD: usize = 2; +pub const COLOR_LOAD: usize = 19; +pub const COLOR_LONG_TERM_LOAD: usize = 1; pub const COLOR_LONG_TERM_LOAD_BOUNDS: usize = 13; -pub const COLOR_RPE: usize = 0; -pub const COLOR_RPE_7DAY: usize = 2; -pub const COLOR_SET_VOLUME: usize = 3; -pub const COLOR_SET_VOLUME_7DAY: usize = 2; -pub const COLOR_VOLUME_LOAD: usize = 6; -pub const COLOR_VOLUME_LOAD_7DAY: usize = 2; +pub const COLOR_RPE: usize = 19; +pub const COLOR_RPE_7DAY: usize = 0; +pub const COLOR_SET_VOLUME: usize = 19; +pub const COLOR_SET_VOLUME_7DAY: usize = 3; +pub const COLOR_VOLUME_LOAD: usize = 19; +pub const COLOR_VOLUME_LOAD_7DAY: usize = 6; pub const COLOR_TUT: usize = 2; pub const COLOR_REPS: usize = 3; pub const COLOR_REPS_RIR: usize = 4; @@ -651,7 +651,7 @@ pub fn view_chart( } pub fn plot_line_chart( - data: &[(Vec<(NaiveDate, f32)>, usize)], + data: &[(Vec<(NaiveDate, Option)>, usize)], interval: &Interval, y_min_opt: Option, y_max_opt: Option, @@ -663,7 +663,7 @@ pub fn plot_line_chart( let (y_min, y_max, y_margin) = determine_y_bounds( data.iter() - .flat_map(|(s, _)| s.iter().map(|(_, y)| *y)) + .flat_map(|(s, _)| s.iter().filter_map(|(_, y)| *y)) .collect::>(), y_min_opt, y_max_opt, @@ -706,15 +706,9 @@ pub fn plot_line_chart( let color = Palette99::pick(*color_idx).mix(0.9); chart.draw_series(LineSeries::new( - series.iter().map(|(x, y)| (*x, *y)), + series.iter().map(|(x, y)| (*x, y.unwrap_or(0.0))), color.stroke_width(2), ))?; - - chart.draw_series( - series - .iter() - .map(|(x, y)| Circle::new((*x, *y), 2, color.filled())), - )?; } root.present()?; @@ -724,18 +718,19 @@ pub fn plot_line_chart( } pub fn plot_dual_line_chart( - data: &[(Vec<(NaiveDate, f32)>, usize)], - secondary_data: &[(Vec<(NaiveDate, f32)>, usize)], + data: &[(Vec<(NaiveDate, Option)>, usize)], + secondary_data: &[(Vec<(NaiveDate, Option)>, usize)], interval: &Interval, + single_y_domain: bool, theme: &data::Theme, ) -> Result, Box> { if all_zeros(data) && all_zeros(secondary_data) { return Ok(None); } - let (y1_min, y1_max, y1_margin) = determine_y_bounds( + let (mut y1_min, mut y1_max, y1_margin) = determine_y_bounds( data.iter() - .flat_map(|(s, _)| s.iter().map(|(_, y)| *y)) + .flat_map(|(s, _)| s.iter().filter_map(|(_, y)| *y)) .collect::>(), None, None, @@ -743,12 +738,17 @@ pub fn plot_dual_line_chart( let (y2_min, y2_max, y2_margin) = determine_y_bounds( secondary_data .iter() - .flat_map(|(s, _)| s.iter().map(|(_, y)| *y)) + .flat_map(|(s, _)| s.iter().map(|(_, y)| y.unwrap_or(0.0))) .collect::>(), None, None, ); + if single_y_domain { + y1_min = f32::min(y1_min, y2_min); + y1_max = f32::max(y1_max, y2_max); + } + let mut result = String::new(); { @@ -783,12 +783,14 @@ pub fn plot_dual_line_chart( .y_labels(6) .draw()?; - chart - .configure_secondary_axes() - .set_all_tick_mark_size(3u32) - .axis_style(color.mix(0.3)) - .label_style(&color) - .draw()?; + if !single_y_domain { + chart + .configure_secondary_axes() + .set_all_tick_mark_size(3u32) + .axis_style(color.mix(0.3)) + .label_style(&color) + .draw()?; + } for (series, color_idx) in secondary_data { let mut series = series.iter().collect::>(); @@ -796,31 +798,31 @@ pub fn plot_dual_line_chart( let color = Palette99::pick(*color_idx).mix(0.9); chart.draw_secondary_series(LineSeries::new( - series.iter().map(|(x, y)| (*x, *y)), + series.iter().map(|(x, y)| (*x, y.unwrap_or(0.0))), color.stroke_width(2), ))?; - - chart.draw_secondary_series( - series - .iter() - .map(|(x, y)| Circle::new((*x, *y), 2, color.filled())), - )?; } + let size = match interval + .last + .signed_duration_since(interval.first) + .abs() + .num_days() + { + i if i < 31 => 3, + i if i < 190 => 2, + _ => 1, + }; + for (series, color_idx) in data { let mut series = series.iter().collect::>(); series.sort_by_key(|e| e.0); let color = Palette99::pick(*color_idx).mix(0.9); - chart.draw_series(LineSeries::new( - series.iter().map(|(x, y)| (*x, *y)), - color.stroke_width(2), - ))?; - chart.draw_series( series .iter() - .map(|(x, y)| Circle::new((*x, *y), 2, color.filled())), + .map(|(x, y)| Circle::new((*x, y.unwrap_or(0.0)), size, color.filled())), )?; } @@ -831,8 +833,8 @@ pub fn plot_dual_line_chart( } pub fn plot_bar_chart( - data: &[(Vec<(NaiveDate, f32)>, usize)], - secondary_data: &[(Vec<(NaiveDate, f32)>, usize)], + data: &[(Vec<(NaiveDate, Option)>, usize)], + secondary_data: &[(Vec<(NaiveDate, Option)>, usize)], interval: &Interval, y_min_opt: Option, y_max_opt: Option, @@ -845,7 +847,7 @@ pub fn plot_bar_chart( let (mut y1_min, mut y1_max, _) = determine_y_bounds( data.iter() - .flat_map(|(s, _)| s.iter().map(|(_, y)| *y)) + .flat_map(|(s, _)| s.iter().filter_map(|(_, y)| *y)) .collect::>(), y_min_opt, y_max_opt, @@ -854,7 +856,7 @@ pub fn plot_bar_chart( let (y2_min, y2_max, mut y2_margin) = determine_y_bounds( secondary_data .iter() - .flat_map(|(s, _)| s.iter().map(|(_, y)| *y)) + .flat_map(|(s, _)| s.iter().filter_map(|(_, y)| *y)) .collect::>(), y_min_opt, y_max_opt, @@ -909,33 +911,52 @@ pub fn plot_bar_chart( .draw()?; } - for (series, color_idx) in data { + for (series, color_idx) in secondary_data { let mut series = series.iter().collect::>(); series.sort_by_key(|e| e.0); - let color = Palette99::pick(*color_idx).mix(0.9).filled(); - let histogram = Histogram::vertical(&chart) - .style(color) - .margin(0) // https://github.com/plotters-rs/plotters/issues/300 - .data(series.iter().map(|(x, y)| (*x, *y))); + let color = Palette99::pick(*color_idx).mix(0.9); - chart.draw_series(histogram)?; + // Plot separate series for every continuous (i.e. not interspersed by None) series of data points + for data in series.iter().fold( + Vec::new(), + |mut result: Vec>, (date, item)| { + match item { + Some(val) => match result.len() { + 0 => result.push(Vec::new()), + len => result[len - 1].push((*date, *val)), + }, + None => result.push(Vec::new()), + }; + result + }, + ) { + chart.draw_secondary_series(LineSeries::new( + data.iter().copied(), + color.stroke_width(2), + ))?; + } } - for (series, color_idx) in secondary_data { + let size = match interval + .last + .signed_duration_since(interval.first) + .abs() + .num_days() + { + i if i < 31 => 3, + i if i < 190 => 2, + _ => 1, + }; + + for (series, color_idx) in data { let mut series = series.iter().collect::>(); series.sort_by_key(|e| e.0); - let color = Palette99::pick(*color_idx).mix(0.9); - - chart.draw_secondary_series(LineSeries::new( - series.iter().map(|(x, y)| (*x, *y)), - color.stroke_width(2), - ))?; + let color = Palette99::pick(*color_idx).mix(0.9).filled(); - chart.draw_secondary_series( - series - .iter() - .map(|(x, y)| Circle::new((*x, *y), 2, color.filled())), - )?; + chart.draw_secondary_series(series.iter().filter_map(|(x, y)| { + y.as_ref() + .map(|y| Circle::new((*x, *y), size, color.filled())) + }))?; } root.present()?; @@ -944,8 +965,10 @@ pub fn plot_bar_chart( Ok(Some(result)) } -fn all_zeros(data: &[(Vec<(NaiveDate, f32)>, usize)]) -> bool { - return data.iter().all(|p| p.0.iter().all(|s| s.1 == 0.0)); +fn all_zeros(data: &[(Vec<(NaiveDate, Option)>, usize)]) -> bool { + return data + .iter() + .all(|p| p.0.iter().all(|s| s.1.is_none() || s.1 == Some(0.0))); } fn colors(theme: &data::Theme) -> (RGBColor, RGBColor) { @@ -1187,23 +1210,28 @@ pub fn post_message_to_service_worker(message: &ServiceWorkerMessage) -> Result< } pub fn centered_moving_grouping( - data: &Vec<(NaiveDate, f32)>, + data: &Vec<(NaiveDate, Option)>, interval: &Interval, radius: u64, - group_day: impl Fn(Vec) -> f32, - group_range: impl Fn(Vec) -> f32, -) -> Vec<(NaiveDate, f32)> { + group_day: impl Fn(Vec) -> Option, + group_range: impl Fn(Vec) -> Option, +) -> Vec<(NaiveDate, Option)> { let mut date_map: BTreeMap<&NaiveDate, Vec> = BTreeMap::new(); for elem in data { - date_map.entry(&elem.0).or_default().push(elem.1); + if let (date, Some(value)) = elem { + date_map.entry(date).or_default().push(*value); + } } - let mut grouped: BTreeMap<&NaiveDate, f32> = BTreeMap::new(); - - for elem in date_map { - grouped.insert(elem.0, group_day(elem.1)); - } + let grouped: BTreeMap<&NaiveDate, f32> = + date_map.iter().filter_map(|(date, value)| { + if let (d, Some(v)) = (*date, group_day(value.clone())) { + Some((d, v)) + } else { + None + } + }).collect::>(); interval .first @@ -1232,24 +1260,36 @@ pub fn centered_moving_grouping( } pub fn centered_moving_total( - data: &Vec<(NaiveDate, f32)>, + data: &Vec<(NaiveDate, Option)>, interval: &Interval, radius: u64, -) -> Vec<(NaiveDate, f32)> { +) -> Vec<(NaiveDate, Option)> { centered_moving_grouping( data, interval, radius, - |d| d.iter().sum(), - |d| d.iter().sum(), + |d| { + if d.is_empty() { + None + } else { + Some(d.iter().sum()) + } + }, + |d| { + if d.is_empty() { + None + } else { + Some(d.iter().sum()) + } + }, ) } pub fn centered_moving_average( - data: &Vec<(NaiveDate, f32)>, + data: &Vec<(NaiveDate, Option)>, interval: &Interval, radius: u64, -) -> Vec<(NaiveDate, f32)> { +) -> Vec<(NaiveDate, Option)> { #[allow(clippy::cast_precision_loss)] centered_moving_grouping( data, @@ -1257,16 +1297,16 @@ pub fn centered_moving_average( radius, |d| { if d.is_empty() { - 0.0 + None } else { - d.iter().sum::() / d.len() as f32 + Some(d.iter().sum::() / d.len() as f32) } }, |d| { if d.is_empty() { - 0.0 + None } else { - d.iter().sum::() / d.len() as f32 + Some(d.iter().sum::() / d.len() as f32) } }, ) @@ -1412,20 +1452,21 @@ mod tests { } #[rstest] - #[case((2020, 2, 3), (2020, 2, 5), 0, &[], &[(2020, 2, 3, 0.0), (2020, 2, 4, 0.0), (2020, 2, 5, 0.0)])] - #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, 1.0)], &[(2020, 2, 3, 1.0), (2020, 2, 4, 0.0), (2020, 2, 5, 0.0)])] - #[case((2020, 3, 3), (2020, 3, 5), 0, &[(2020, 2, 3, 1.0)], &[(2020, 3, 3, 0.0), (2020, 3, 4, 0.0), (2020, 3, 5, 0.0)])] - #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, 1.0), (2020, 2, 4, 1.0), (2020, 2, 5, 1.0)], &[(2020, 2, 3, 1.0), (2020, 2, 4, 1.0), (2020, 2, 5, 1.0)])] - #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, 1.0), (2020, 2, 4, 1.0), (2020, 2, 5, 1.0), (2020, 2, 3, 3.0)], &[(2020, 2, 3, 2.0), (2020, 2, 4, 1.0), (2020, 2, 5, 1.0)])] - #[case((2020, 2, 3), (2020, 2, 5), 1, &[(2020, 2, 3, 1.0), (2020, 2, 4, 2.0), (2020, 2, 5, 3.0)], &[(2020, 2, 3, 1.5), (2020, 2, 4, 2.0), (2020, 2, 5, 2.5)])] - #[case((2020, 2, 2), (2020, 2, 6), 1, &[(2020, 2, 3, 1.0), (2020, 2, 4, 2.0), (2020, 2, 5, 3.0)], &[(2020, 2, 2, 1.0), (2020, 2, 3, 1.5), (2020, 2, 4, 2.0), (2020, 2, 5, 2.5), (2020, 2, 6, 3.0)])] - #[case((2020, 2, 3), (2020, 2, 7), 1, &[(2020, 2, 3, 1.0), (2020, 2, 7, 1.0)], &[(2020, 2, 3, 1.0), (2020, 2, 4, 1.0), (2020, 2, 5, 0.0), (2020, 2, 6, 1.0), (2020, 2, 7, 1.0)])] + #[case((2020, 2, 3), (2020, 2, 5), 0, &[], &[(2020, 2, 3, None), (2020, 2, 4, None), (2020, 2, 5, None)])] + #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, Some(1.0))], &[(2020, 2, 3, Some(1.0)), (2020, 2, 4, None), (2020, 2, 5, None)])] + #[case((2020, 3, 3), (2020, 3, 5), 0, &[(2020, 2, 3, Some(1.0))], &[(2020, 3, 3, None), (2020, 3, 4, None), (2020, 3, 5, None)])] + #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, Some(1.0)), (2020, 2, 4, Some(1.0)), (2020, 2, 5, Some(1.0))], &[(2020, 2, 3, Some(1.0)), (2020, 2, 4, Some(1.0)), (2020, 2, 5, Some(1.0))])] + #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, Some(1.0)), (2020, 2, 4, Some(1.0)), (2020, 2, 4, Some(3.0)), (2020, 2, 5, Some(1.0))], &[(2020, 2, 3, Some(1.0)), (2020, 2, 4, Some(2.0)), (2020, 2, 5, Some(1.0))])] + #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, Some(1.0)), (2020, 2, 4, Some(1.0)), (2020, 2, 5, Some(1.0)), (2020, 2, 3, Some(3.0))], &[(2020, 2, 3, Some(2.0)), (2020, 2, 4, Some(1.0)), (2020, 2, 5, Some(1.0))])] + #[case((2020, 2, 3), (2020, 2, 5), 1, &[(2020, 2, 3, Some(1.0)), (2020, 2, 4, Some(2.0)), (2020, 2, 5, Some(3.0))], &[(2020, 2, 3, Some(1.5)), (2020, 2, 4, Some(2.0)), (2020, 2, 5, Some(2.5))])] + #[case((2020, 2, 2), (2020, 2, 6), 1, &[(2020, 2, 3, Some(1.0)), (2020, 2, 4, Some(2.0)), (2020, 2, 5, Some(3.0))], &[(2020, 2, 2, Some(1.0)), (2020, 2, 3, Some(1.5)), (2020, 2, 4, Some(2.0)), (2020, 2, 5, Some(2.5)), (2020, 2, 6, Some(3.0))])] + #[case((2020, 2, 3), (2020, 2, 7), 1, &[(2020, 2, 3, Some(1.0)), (2020, 2, 7, Some(1.0))], &[(2020, 2, 3, Some(1.0)), (2020, 2, 4, Some(1.0)), (2020, 2, 5, None), (2020, 2, 6, Some(1.0)), (2020, 2, 7, Some(1.0))])] fn centered_moving_average( #[case] start: (i32, u32, u32), #[case] end: (i32, u32, u32), #[case] radius: u64, - #[case] input: &[(i32, u32, u32, f32)], - #[case] expected: &[(i32, u32, u32, f32)], + #[case] input: &[(i32, u32, u32, Option)], + #[case] expected: &[(i32, u32, u32, Option)], ) { assert_eq!( super::centered_moving_average( @@ -1447,20 +1488,21 @@ mod tests { } #[rstest] - #[case((2020, 2, 3), (2020, 2, 5), 0, &[], &[(2020, 2, 3, 0.0), (2020, 2, 4, 0.0), (2020, 2, 5, 0.0)])] - #[case((2020, 3, 3), (2020, 3, 5), 0, &[(2020, 2, 3, 1.0)], &[(2020, 3, 3, 0.0), (2020, 3, 4, 0.0), (2020, 3, 5, 0.0)])] - #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, 1.0)], &[(2020, 2, 3, 1.0), (2020, 2, 4, 0.0), (2020, 2, 5, 0.0)])] - #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, 1.0), (2020, 2, 4, 2.0), (2020, 2, 5, 3.0)], &[(2020, 2, 3, 1.0), (2020, 2, 4, 2.0), (2020, 2, 5, 3.0)])] - #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, 1.0), (2020, 2, 4, 2.0), (2020, 2, 5, 3.0), (2020, 2, 3, 1.0)], &[(2020, 2, 3, 2.0), (2020, 2, 4, 2.0), (2020, 2, 5, 3.0)])] - #[case((2020, 2, 3), (2020, 2, 5), 1, &[(2020, 2, 3, 1.0), (2020, 2, 4, 2.0), (2020, 2, 5, 3.0)], &[(2020, 2, 3, 3.0), (2020, 2, 4, 6.0), (2020, 2, 5, 5.0)])] - #[case((2020, 2, 2), (2020, 2, 6), 1, &[(2020, 2, 3, 1.0), (2020, 2, 4, 2.0), (2020, 2, 5, 3.0)], &[(2020, 2, 2, 1.0), (2020, 2, 3, 3.0), (2020, 2, 4, 6.0), (2020, 2, 5, 5.0), (2020, 2, 6, 3.0)])] - #[case((2020, 2, 3), (2020, 2, 7), 1, &[(2020, 2, 3, 1.0), (2020, 2, 7, 1.0)], &[(2020, 2, 3, 1.0), (2020, 2, 4, 1.0), (2020, 2, 5, 0.0), (2020, 2, 6, 1.0), (2020, 2, 7, 1.0)])] + #[case((2020, 2, 3), (2020, 2, 5), 0, &[], &[(2020, 2, 3, None), (2020, 2, 4, None), (2020, 2, 5, None)])] + #[case((2020, 3, 3), (2020, 3, 5), 0, &[(2020, 2, 3, Some(1.0))], &[(2020, 3, 3, None), (2020, 3, 4, None), (2020, 3, 5, None)])] + #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, Some(1.0))], &[(2020, 2, 3, Some(1.0)), (2020, 2, 4, None), (2020, 2, 5, None)])] + #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, Some(1.0)), (2020, 2, 4, Some(2.0)), (2020, 2, 5, Some(3.0))], &[(2020, 2, 3, Some(1.0)), (2020, 2, 4, Some(2.0)), (2020, 2, 5, Some(3.0))])] + #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, Some(1.0)), (2020, 2, 4, Some(2.0)), (2020, 2, 4, Some(2.0)), (2020, 2, 5, Some(3.0))], &[(2020, 2, 3, Some(1.0)), (2020, 2, 4, Some(4.0)), (2020, 2, 5, Some(3.0))])] + #[case((2020, 2, 3), (2020, 2, 5), 0, &[(2020, 2, 3, Some(1.0)), (2020, 2, 4, Some(2.0)), (2020, 2, 5, Some(3.0)), (2020, 2, 3, Some(1.0))], &[(2020, 2, 3, Some(2.0)), (2020, 2, 4, Some(2.0)), (2020, 2, 5, Some(3.0))])] + #[case((2020, 2, 3), (2020, 2, 5), 1, &[(2020, 2, 3, Some(1.0)), (2020, 2, 4, Some(2.0)), (2020, 2, 5, Some(3.0))], &[(2020, 2, 3, Some(3.0)), (2020, 2, 4, Some(6.0)), (2020, 2, 5, Some(5.0))])] + #[case((2020, 2, 2), (2020, 2, 6), 1, &[(2020, 2, 3, Some(1.0)), (2020, 2, 4, Some(2.0)), (2020, 2, 5, Some(3.0))], &[(2020, 2, 2, Some(1.0)), (2020, 2, 3, Some(3.0)), (2020, 2, 4, Some(6.0)), (2020, 2, 5, Some(5.0)), (2020, 2, 6, Some(3.0))])] + #[case((2020, 2, 3), (2020, 2, 7), 1, &[(2020, 2, 3, Some(1.0)), (2020, 2, 7, Some(1.0))], &[(2020, 2, 3, Some(1.0)), (2020, 2, 4, Some(1.0)), (2020, 2, 5, None), (2020, 2, 6, Some(1.0)), (2020, 2, 7, Some(1.0))])] fn centered_moving_total( #[case] start: (i32, u32, u32), #[case] end: (i32, u32, u32), #[case] radius: u64, - #[case] input: &[(i32, u32, u32, f32)], - #[case] expected: &[(i32, u32, u32, f32)], + #[case] input: &[(i32, u32, u32, Option)], + #[case] expected: &[(i32, u32, u32, Option)], ) { assert_eq!( super::centered_moving_total( diff --git a/frontend/src/ui/data.rs b/frontend/src/ui/data.rs index 2560380..86f4b5e 100644 --- a/frontend/src/ui/data.rs +++ b/frontend/src/ui/data.rs @@ -68,7 +68,6 @@ pub fn init(url: Url, _orders: &mut impl Orders) -> Model { training_stats: TrainingStats { short_term_load: Vec::new(), long_term_load: Vec::new(), - stimulus_per_muscle: BTreeMap::new(), }, settings, ongoing_training_session, @@ -270,9 +269,8 @@ pub struct CycleStats { } pub struct TrainingStats { - pub short_term_load: Vec<(NaiveDate, f32)>, - pub long_term_load: Vec<(NaiveDate, f32)>, - pub stimulus_per_muscle: BTreeMap>, + pub short_term_load: Vec<(NaiveDate, Option)>, + pub long_term_load: Vec<(NaiveDate, Option)>, } impl TrainingStats { @@ -280,9 +278,15 @@ impl TrainingStats { pub const LOAD_RATIO_HIGH: f32 = 1.5; pub fn load_ratio(&self) -> Option { - let long_term_load = self.long_term_load.last().map_or(0., |(_, l)| *l); + let long_term_load = self + .long_term_load + .last() + .map_or(0., |(_, l)| l.map_or(0., |v| v)); if long_term_load > 0. { - let short_term_load = self.short_term_load.last().map_or(0., |(_, l)| *l); + let short_term_load = self + .short_term_load + .last() + .map_or(0., |(_, l)| l.map_or(0., |v| v)); Some(short_term_load / long_term_load) } else { None @@ -878,14 +882,13 @@ fn calculate_training_stats( TrainingStats { short_term_load, long_term_load, - stimulus_per_muscle: calculate_daily_stimulus_per_muscle(training_sessions, exercises), } } fn calculate_weighted_sum_of_load( training_sessions: &[&TrainingSession], window_size: usize, -) -> Vec<(NaiveDate, f32)> { +) -> Vec<(NaiveDate, Option)> { let mut result: BTreeMap = BTreeMap::new(); let today = Local::now().date_naive(); @@ -916,56 +919,32 @@ fn calculate_weighted_sum_of_load( window[0] = load; ( date, - zip(&window, &weighting) - .map(|(load, weight)| load * weight) - .sum(), + Some( + zip(&window, &weighting) + .map(|(load, weight)| load * weight) + .sum(), + ), ) }) .collect() } fn calculate_average_weighted_sum_of_load( - weighted_sum_of_load: &[(NaiveDate, f32)], + weighted_sum_of_load: &[(NaiveDate, Option)], window_size: usize, -) -> Vec<(NaiveDate, f32)> { +) -> Vec<(NaiveDate, Option)> { #[allow(clippy::cast_precision_loss)] weighted_sum_of_load .windows(window_size) .map(|window| { ( window.last().unwrap().0, - window.iter().map(|(_, l)| l).sum::() / window_size as f32, + Some(window.iter().filter_map(|(_, l)| *l).sum::() / window_size as f32), ) }) .collect::>() } -fn calculate_daily_stimulus_per_muscle( - training_sessions: &[&TrainingSession], - exercises: &BTreeMap, -) -> BTreeMap> { - let mut result: BTreeMap> = BTreeMap::new(); - - for m in domain::Muscle::iter() { - #[allow(clippy::cast_precision_loss)] - result.insert( - domain::Muscle::id(*m), - training_sessions - .iter() - .map(|s| { - ( - s.date, - *s.stimulus_per_muscle(exercises) - .get(&domain::Muscle::id(*m)) - .unwrap_or(&0) as f32, - ) - }) - .collect(), - ); - } - result -} - // ------ ------ // Update // ------ ------ diff --git a/frontend/src/ui/page/body_fat.rs b/frontend/src/ui/page/body_fat.rs index a71f8f8..979ea79 100644 --- a/frontend/src/ui/page/body_fat.rs +++ b/frontend/src/ui/page/body_fat.rs @@ -747,14 +747,14 @@ fn view_chart(model: &Model, data_model: &data::Model) -> Node { ( body_fat .iter() - .filter_map(|bf| bf.jp3(sex).map(|jp3| (bf.date, jp3))) + .filter_map(|bf| bf.jp3(sex).map(|jp3| (bf.date, Some(jp3)))) .collect::>(), common::COLOR_BODY_FAT_JP3, ), ( body_fat .iter() - .filter_map(|bf| bf.jp7(sex).map(|jp7| (bf.date, jp7))) + .filter_map(|bf| bf.jp7(sex).map(|jp7| (bf.date, Some(jp7)))) .collect::>(), common::COLOR_BODY_FAT_JP7, ), @@ -762,11 +762,12 @@ fn view_chart(model: &Model, data_model: &data::Model) -> Node { &[( body_weight .iter() - .map(|bw| (bw.date, bw.weight)) + .map(|bw| (bw.date, Some(bw.weight))) .collect::>(), common::COLOR_BODY_WEIGHT, )], &model.interval, + false, data_model.theme(), ), true, diff --git a/frontend/src/ui/page/body_weight.rs b/frontend/src/ui/page/body_weight.rs index 44c9d65..0fe8241 100644 --- a/frontend/src/ui/page/body_weight.rs +++ b/frontend/src/ui/page/body_weight.rs @@ -355,40 +355,34 @@ fn view_body_weight_dialog(dialog: &Dialog, loading: bool) -> Node { } fn view_chart(model: &Model, data_model: &data::Model) -> Node { + let body_weight = data_model + .body_weight + .values() + .filter(|bw| bw.date >= model.interval.first && bw.date <= model.interval.last) + .map(|bw| (bw.date, Some(bw.weight))) + .collect::>(); + let body_weight_7day_avg = common::centered_moving_average( + &data_model + .body_weight + .values() + .map(|bw| (bw.date, Some(bw.weight))) + .collect::>(), + &model.interval, + 3, + ); common::view_chart( vec![ ("Weight (kg)", common::COLOR_BODY_WEIGHT), ("Avg. weight (kg)", common::COLOR_AVG_BODY_WEIGHT), ] .as_slice(), - common::plot_line_chart( - &[ - ( - data_model - .body_weight - .values() - .filter(|bw| { - bw.date >= model.interval.first && bw.date <= model.interval.last - }) - .map(|bw| (bw.date, bw.weight)) - .collect::>(), - common::COLOR_BODY_WEIGHT, - ), - ( - data_model - .body_weight_stats - .values() - .filter(|bws| { - bws.date >= model.interval.first && bws.date <= model.interval.last - }) - .filter_map(|bws| bws.avg_weight.map(|avg_weight| (bws.date, avg_weight))) - .collect::>(), - common::COLOR_AVG_BODY_WEIGHT, - ), - ], + common::plot_bar_chart( + &[(body_weight, common::COLOR_BODY_WEIGHT)], + &[(body_weight_7day_avg, common::COLOR_AVG_BODY_WEIGHT)], &model.interval, None, None, + true, data_model.theme(), ), true, diff --git a/frontend/src/ui/page/exercise.rs b/frontend/src/ui/page/exercise.rs index 349bc9c..e78342c 100644 --- a/frontend/src/ui/page/exercise.rs +++ b/frontend/src/ui/page/exercise.rs @@ -454,27 +454,10 @@ pub fn view_charts( show_rpe: bool, show_tut: bool, ) -> Vec> { - let mut set_volume: BTreeMap = BTreeMap::new(); - let mut volume_load: BTreeMap = BTreeMap::new(); - let mut tut: BTreeMap = BTreeMap::new(); let mut reps_rpe: BTreeMap, Vec)> = BTreeMap::new(); let mut weight: BTreeMap> = BTreeMap::new(); let mut time: BTreeMap> = BTreeMap::new(); for training_session in training_sessions { - #[allow(clippy::cast_precision_loss)] - set_volume - .entry(training_session.date) - .and_modify(|e| *e += training_session.set_volume() as f32) - .or_insert(training_session.set_volume() as f32); - #[allow(clippy::cast_precision_loss)] - volume_load - .entry(training_session.date) - .and_modify(|e| *e += training_session.volume_load() as f32) - .or_insert(training_session.volume_load() as f32); - #[allow(clippy::cast_precision_loss)] - tut.entry(training_session.date) - .and_modify(|e| *e += training_session.tut().unwrap_or(0) as f32) - .or_insert(training_session.tut().unwrap_or(0) as f32); if let Some(avg_reps) = training_session.avg_reps() { reps_rpe .entry(training_session.date) @@ -498,11 +481,38 @@ pub fn view_charts( .or_insert(vec![avg_time]); } } + #[allow(clippy::cast_precision_loss)] + let tut = common::centered_moving_total( + &training_sessions + .iter() + .map(|s| (s.date, s.tut().map(|v| v as f32))) + .collect::>(), + interval, + 0, + ); + #[allow(clippy::cast_precision_loss)] + let set_volume = common::centered_moving_total( + &training_sessions + .iter() + .map(|s| (s.date, Some(s.set_volume() as f32))) + .collect::>(), + interval, + 0, + ); let set_volume_7day_total = common::centered_moving_total( &set_volume.clone().into_iter().collect::>(), interval, 3, ); + #[allow(clippy::cast_precision_loss)] + let volume_load = common::centered_moving_total( + &training_sessions + .iter() + .map(|s| (s.date, Some(s.volume_load() as f32))) + .collect::>(), + interval, + 0, + ); let volume_load_7day_avg = common::centered_moving_average( &volume_load.clone().into_iter().collect::>(), interval, @@ -516,7 +526,7 @@ pub fn view_charts( .iter() .map(|(date, (avg_reps, _))| { #[allow(clippy::cast_precision_loss)] - (*date, avg_reps.iter().sum::() / avg_reps.len() as f32) + (*date, Some(avg_reps.iter().sum::() / avg_reps.len() as f32)) }) .collect::>(), common::COLOR_REPS, @@ -536,7 +546,7 @@ pub fn view_charts( if avg_rpe_values.is_empty() { None } else { - Some((date, avg_reps + 10.0 - avg_rpe)) + Some((date, Some(avg_reps + 10.0 - avg_rpe))) } }) .collect::>(), @@ -615,7 +625,7 @@ pub fn view_charts( .into_iter() .map(|(date, values)| { #[allow(clippy::cast_precision_loss)] - (date, values.iter().sum::() / values.len() as f32) + (date, Some(values.iter().sum::() / values.len() as f32)) }) .collect::>(), common::COLOR_WEIGHT, @@ -635,7 +645,7 @@ pub fn view_charts( time.into_iter() .map(|(date, values)| { #[allow(clippy::cast_precision_loss)] - (date, values.iter().sum::() / values.len() as f32) + (date, Some(values.iter().sum::() / values.len() as f32)) }) .collect::>(), common::COLOR_TIME, diff --git a/frontend/src/ui/page/menstrual_cycle.rs b/frontend/src/ui/page/menstrual_cycle.rs index 87af870..5418936 100644 --- a/frontend/src/ui/page/menstrual_cycle.rs +++ b/frontend/src/ui/page/menstrual_cycle.rs @@ -349,7 +349,7 @@ fn view_chart(model: &Model, data_model: &data::Model) -> Node { &[( period .iter() - .map(|p| (p.date, f32::from(p.intensity))) + .map(|p| (p.date, Some(f32::from(p.intensity)))) .collect::>(), common::COLOR_PERIOD_INTENSITY, )], diff --git a/frontend/src/ui/page/muscles.rs b/frontend/src/ui/page/muscles.rs index 76e15a0..0803136 100644 --- a/frontend/src/ui/page/muscles.rs +++ b/frontend/src/ui/page/muscles.rs @@ -69,30 +69,22 @@ pub fn view(model: &Model, data_model: &data::Model) -> Node { Msg::ChangeInterval ), domain::Muscle::iter().map(|m| { - let set_volume = data_model - .training_stats - .stimulus_per_muscle - .get(&domain::Muscle::id(*m)) - .map_or(Vec::new(), std::clone::Clone::clone) - .into_iter() - .filter(|(date, _)| { - *date >= model.interval.first && *date <= model.interval.last + #[allow(clippy::cast_precision_loss)] + let set_volume_data = &data_model + .training_sessions + .values() + .map(|s| { + ( + s.date, + s.stimulus_per_muscle(&data_model.exercises) + .get(&domain::Muscle::id(*m)) + .map(|v| (*v as f32) / 100.0), + ) }) - .map(|(date, stimulus)| (date, stimulus / 100.0)) - .collect(); - - let set_volume_7day_total = common::centered_moving_total( - &data_model - .training_stats - .stimulus_per_muscle - .get(&domain::Muscle::id(*m)) - .unwrap_or(&Vec::new()) - .iter() - .map(|(date, stimulus)| (*date, *stimulus / 100.0)) - .collect(), - &model.interval, - 3, - ); + .collect::>(); + let set_volume = common::centered_moving_total(set_volume_data, &model.interval, 0); + let set_volume_7day_total = + common::centered_moving_total(set_volume_data, &model.interval, 3); div![ common::view_title(&span![domain::Muscle::name(*m)], 1), diff --git a/frontend/src/ui/page/routine.rs b/frontend/src/ui/page/routine.rs index 5e010b5..402ae24 100644 --- a/frontend/src/ui/page/routine.rs +++ b/frontend/src/ui/page/routine.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeSet; use chrono::prelude::*; use seed::{prelude::*, *}; @@ -1316,18 +1316,29 @@ pub fn view_charts( theme: &data::Theme, show_rpe: bool, ) -> Vec> { - let mut load: BTreeMap = BTreeMap::new(); - for training_session in training_sessions { - #[allow(clippy::cast_precision_loss)] - load.entry(training_session.date) - .and_modify(|e| *e += training_session.load() as f32) - .or_insert(training_session.load() as f32); - } + #[allow(clippy::cast_precision_loss)] + let load = common::centered_moving_total( + &training_sessions + .iter() + .map(|s| { + ( + s.date, + if s.load() == 0 { + None + } else { + Some(s.load() as f32) + }, + ) + }) + .collect(), + interval, + 0, + ); #[allow(clippy::cast_precision_loss)] let set_volume = common::centered_moving_total( &training_sessions .iter() - .map(|s| (s.date, s.set_volume() as f32)) + .map(|s| (s.date, Some(s.set_volume() as f32))) .collect(), interval, 0, @@ -1336,7 +1347,7 @@ pub fn view_charts( let set_volume_7day_total = common::centered_moving_total( &training_sessions .iter() - .map(|s| (s.date, s.set_volume() as f32)) + .map(|s| (s.date, Some(s.set_volume() as f32))) .collect(), interval, 3, @@ -1344,7 +1355,7 @@ pub fn view_charts( let rpe = common::centered_moving_average( &training_sessions .iter() - .filter_map(|s| s.avg_rpe().map(|avg_rpe| (s.date, avg_rpe))) + .filter_map(|s| s.avg_rpe().map(|avg_rpe| (s.date, Some(avg_rpe)))) .collect(), interval, 0, @@ -1352,7 +1363,7 @@ pub fn view_charts( let rpe_7day_avg = common::centered_moving_average( &training_sessions .iter() - .filter_map(|s| s.avg_rpe().map(|avg_rpe| (s.date, avg_rpe))) + .filter_map(|s| s.avg_rpe().map(|avg_rpe| (s.date, Some(avg_rpe)))) .collect(), interval, 3, diff --git a/frontend/src/ui/page/training.rs b/frontend/src/ui/page/training.rs index 9d8f05b..41e9679 100644 --- a/frontend/src/ui/page/training.rs +++ b/frontend/src/ui/page/training.rs @@ -484,8 +484,8 @@ fn view_training_sessions_dialog( pub fn view_charts( training_sessions: &[&data::TrainingSession], - short_term_load: Vec<(NaiveDate, f32)>, - long_term_load: Vec<(NaiveDate, f32)>, + short_term_load: Vec<(NaiveDate, Option)>, + long_term_load: Vec<(NaiveDate, Option)>, interval: &common::Interval, theme: &data::Theme, show_rpe: bool, @@ -493,18 +493,28 @@ pub fn view_charts( let long_term_load_high = long_term_load .iter() .copied() - .map(|(d, l)| (d, l * data::TrainingStats::LOAD_RATIO_HIGH)) + .map(|(d, l)| { + ( + d, + l.map(|v| v * data::TrainingStats::LOAD_RATIO_HIGH), + ) + }) .collect::>(); let long_term_load_low = long_term_load .iter() .copied() - .map(|(d, l)| (d, l * data::TrainingStats::LOAD_RATIO_LOW)) + .map(|(d, l)| { + ( + d, + l.map(|v| v * data::TrainingStats::LOAD_RATIO_LOW), + ) + }) .collect::>(); #[allow(clippy::cast_precision_loss)] let set_volume = common::centered_moving_total( &training_sessions .iter() - .map(|s| (s.date, s.set_volume() as f32)) + .map(|s| (s.date, Some(s.set_volume() as f32))) .collect::>(), interval, 0, @@ -513,15 +523,15 @@ pub fn view_charts( let set_volume_7day_total = common::centered_moving_total( &training_sessions .iter() - .map(|s| (s.date, s.set_volume() as f32)) + .map(|s| (s.date, Some(s.set_volume() as f32))) .collect::>(), interval, 3, ); - let rpe = common::centered_moving_total( + let rpe = common::centered_moving_average( &training_sessions .iter() - .filter_map(|s| s.avg_rpe().map(|avg_rpe| (s.date, avg_rpe))) + .filter_map(|s| s.avg_rpe().map(|avg_rpe| (s.date, Some(avg_rpe)))) .collect::>(), interval, 0, @@ -529,7 +539,7 @@ pub fn view_charts( let rpe_7day_avg = common::centered_moving_average( &training_sessions .iter() - .filter_map(|s| s.avg_rpe().map(|avg_rpe| (s.date, avg_rpe))) + .filter_map(|s| s.avg_rpe().map(|avg_rpe| (s.date, Some(avg_rpe)))) .collect::>(), interval, 3, @@ -540,16 +550,17 @@ pub fn view_charts( ("Short-term load", common::COLOR_LOAD), ("Long-term load", common::COLOR_LONG_TERM_LOAD) ], - common::plot_line_chart( + common::plot_dual_line_chart( + &[ + (short_term_load, common::COLOR_LOAD) + ], &[ (long_term_load_low, common::COLOR_LONG_TERM_LOAD_BOUNDS), (long_term_load_high, common::COLOR_LONG_TERM_LOAD_BOUNDS), (long_term_load, common::COLOR_LONG_TERM_LOAD), - (short_term_load, common::COLOR_LOAD) ], interval, - Some(0.), - Some(10.), + true, theme, ), false,