From 8735b74ebfef62da8d4dd1303820134b9e02e8d1 Mon Sep 17 00:00:00 2001 From: Alexander Senier Date: Sat, 2 Nov 2024 17:21:14 +0100 Subject: [PATCH 1/4] Add option to set location of demo database --- tests/backend/cli_test.py | 10 ++++++++++ valens/cli.py | 31 +++++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/tests/backend/cli_test.py b/tests/backend/cli_test.py index 4e6f47c..0c31ccc 100644 --- a/tests/backend/cli_test.py +++ b/tests/backend/cli_test.py @@ -65,3 +65,13 @@ def test_main_demo_public(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(demo, "run", lambda x, y, z: demo_called.append(1)) assert cli.main() == 0 assert demo_called + + +def test_main_demo_db_exists(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + db_file = tmp_path / "db" + db_file.touch() + monkeypatch.setattr(sys, "argv", ["valens", "demo", "--database", str(db_file)]) + demo_called = [] + monkeypatch.setattr(demo, "run", lambda x, y, z: demo_called.append(1)) + assert cli.main() == 2 + assert not demo_called diff --git a/valens/cli.py b/valens/cli.py index 32587f4..6863337 100644 --- a/valens/cli.py +++ b/valens/cli.py @@ -47,6 +47,11 @@ def main() -> int: "demo", help="run app with random example data (all changes are non-persistent)" ) parser_demo.set_defaults(func=run_demo) + parser_demo.add_argument( + "--database", + type=Path, + help="path to the database file that will be created", + ) parser_demo.add_argument( "--public", action="store_true", @@ -66,30 +71,40 @@ def main() -> int: parser.print_usage() return 2 - args.func(args) - - return 0 + return args.func(args) -def create_config(args: argparse.Namespace) -> None: +def create_config(args: argparse.Namespace) -> int: config_file = config.create_config_file( args.directory, Path.home() / ".local/share/valens/valens.db" ) print(f"Created {config_file}") + return 0 -def upgrade(_: argparse.Namespace) -> None: +def upgrade(_: argparse.Namespace) -> int: with app.app_context(): config.check_config_file(os.environ.copy()) db.upgrade() + return 0 -def run(args: argparse.Namespace) -> None: +def run(args: argparse.Namespace) -> int: with app.app_context(): config.check_config_file(os.environ.copy()) app.run("0.0.0.0" if args.public else "127.0.0.1", args.port) + return 0 -def run_demo(args: argparse.Namespace) -> None: +def run_demo(args: argparse.Namespace) -> int: + if isinstance(args.database, Path) and args.database.exists(): + print(f'Database "{args.database}" already exists, exiting.', file=sys.stderr) + return 2 + with NamedTemporaryFile() as f: - demo.run(f"sqlite:///{f.name}", "0.0.0.0" if args.public else "127.0.0.1", args.port) + demo.run( + f"sqlite:///{args.database or f.name}", + "0.0.0.0" if args.public else "127.0.0.1", + args.port, + ) + return 0 From 2e0818a866e8d0342d8e35ed87a65d95d4d6a2b4 Mon Sep 17 00:00:00 2001 From: Alexander Senier Date: Sat, 2 Nov 2024 19:57:51 +0100 Subject: [PATCH 2/4] Allow setting configuration file in run target --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index a36969a..cf9d4de 100644 --- a/Makefile +++ b/Makefile @@ -127,8 +127,8 @@ $(addprefix frontend/dist/,$(FRONTEND_FILES)): third-party/bulma third-party/fon .PHONY: run run_frontend run_backend run: - tmux new-window $(MAKE) run_frontend - tmux new-window $(MAKE) run_backend + tmux new-window $(MAKE) CONFIG_FILE=$(CONFIG_FILE) run_frontend + tmux new-window $(MAKE) CONFIG_FILE=$(CONFIG_FILE) run_backend run_frontend: PATH=~/.cargo/bin:${PATH} trunk --config frontend/Trunk.toml serve --port 8000 From a8a34bd800b2d714f6bb65f4d287b98b80a43c4f Mon Sep 17 00:00:00 2001 From: Alexander Senier Date: Wed, 20 Nov 2024 17:19:20 +0100 Subject: [PATCH 3/4] Fix typos --- frontend/src/ui/page/body_fat.rs | 12 ++++++------ frontend/src/ui/page/exercise.rs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/ui/page/body_fat.rs b/frontend/src/ui/page/body_fat.rs index 4937a55..5df8bdb 100644 --- a/frontend/src/ui/page/body_fat.rs +++ b/frontend/src/ui/page/body_fat.rs @@ -96,7 +96,7 @@ pub enum Msg { DateChanged(String), ChestChanged(String), AbdominalChanged(String), - TighChanged(String), + ThighChanged(String), TricepChanged(String), SubscapularChanged(String), SuprailiacChanged(String), @@ -270,7 +270,7 @@ pub fn update( panic!(); } }, - Msg::TighChanged(thigh) => match model.dialog { + Msg::ThighChanged(thigh) => match model.dialog { Dialog::AddBodyFat(ref mut form) | Dialog::EditBodyFat(ref mut form) => { match thigh.parse::() { Ok(parsed_thigh) => { @@ -551,7 +551,7 @@ fn view_body_fat_dialog(dialog: &Dialog, loading: bool, sex: u8) -> Node { "Thigh", "Vertical fold midway between knee cap and top of thigh", &form.thigh, - Msg::TighChanged, + Msg::ThighChanged, save_disabled ), ] @@ -575,7 +575,7 @@ fn view_body_fat_dialog(dialog: &Dialog, loading: bool, sex: u8) -> Node { "Thigh", "Vertical fold midway between knee cap and top of thigh", &form.thigh, - Msg::TighChanged, + Msg::ThighChanged, save_disabled ), ] @@ -834,7 +834,7 @@ fn view_table(model: &Model, data_model: &data::Model) -> Node { nodes![ th!["Tricep (mm)"], th!["Suprailiac (mm)"], - th!["Tigh (mm)"], + th!["Thigh (mm)"], th!["Chest (mm)"], th!["Abdominal (mm)"], th!["Subscapular (mm)"], @@ -844,7 +844,7 @@ fn view_table(model: &Model, data_model: &data::Model) -> Node { nodes![ th!["Chest (mm)"], th!["Abdominal (mm)"], - th!["Tigh (mm)"], + th!["Thigh (mm)"], th!["Tricep (mm)"], th!["Subscapular (mm)"], th!["Suprailiac (mm)"], diff --git a/frontend/src/ui/page/exercise.rs b/frontend/src/ui/page/exercise.rs index e329025..d2d256c 100644 --- a/frontend/src/ui/page/exercise.rs +++ b/frontend/src/ui/page/exercise.rs @@ -513,7 +513,7 @@ pub fn view_charts( )]; if show_rpe { - labels.push(("+ Repetititions in reserve", common::COLOR_REPS_RIR)); + labels.push(("+ Repetitions in reserve", common::COLOR_REPS_RIR)); data.push(( reps_rpe .into_iter() From c5a4a0b548b4c8a6cb5f245b69d4b5e6f7d1319a Mon Sep 17 00:00:00 2001 From: Alexander Senier Date: Wed, 20 Nov 2024 15:26:23 +0100 Subject: [PATCH 4/4] Refactor chart plotting --- frontend/src/ui/common.rs | 437 ++++++++++-------------- frontend/src/ui/page/body_fat.rs | 35 +- frontend/src/ui/page/body_weight.rs | 22 +- frontend/src/ui/page/exercise.rs | 89 +++-- frontend/src/ui/page/menstrual_cycle.rs | 14 +- frontend/src/ui/page/muscles.rs | 10 +- frontend/src/ui/page/routine.rs | 36 +- frontend/src/ui/page/training.rs | 47 ++- 8 files changed, 322 insertions(+), 368 deletions(-) diff --git a/frontend/src/ui/common.rs b/frontend/src/ui/common.rs index ec8fde6..f00e625 100644 --- a/frontend/src/ui/common.rs +++ b/frontend/src/ui/common.rs @@ -1,4 +1,7 @@ -use std::collections::{BTreeMap, HashMap}; +use std::{ + borrow::BorrowMut, + collections::{BTreeMap, HashMap}, +}; use chrono::{prelude::*, Duration}; use plotters::prelude::*; @@ -25,6 +28,89 @@ pub const COLOR_REPS_RIR: usize = 4; pub const COLOR_WEIGHT: usize = 8; pub const COLOR_TIME: usize = 5; +#[derive(Clone)] +pub enum PlotType { + Circle(usize, u32), + Line(usize, u32), + Histogram(usize), +} + +pub fn plot_line_with_dots(color: usize) -> Vec { + [PlotType::Line(color, 2), PlotType::Circle(color, 2)].to_vec() +} + +#[derive(Default)] +pub struct PlotParams { + pub y_min_opt: Option, + pub y_max_opt: Option, + pub secondary: bool, +} + +impl PlotParams { + pub fn default() -> Self { + Self { + y_min_opt: None, + y_max_opt: None, + secondary: false, + } + } + + pub fn primary_range(min: f32, max: f32) -> Self { + Self { + y_min_opt: Some(min), + y_max_opt: Some(max), + secondary: false, + } + } + + pub const SECONDARY: Self = Self { + y_max_opt: None, + y_min_opt: None, + secondary: true, + }; +} + +pub struct PlotData { + pub values: Vec<(NaiveDate, f32)>, + pub plots: Vec, + pub params: PlotParams, +} + +#[derive(Clone, Copy, Default)] +pub struct Bounds { + min: f32, + max: f32, +} + +impl Bounds { + fn min_with_margin(self) -> f32 { + assert!(0. <= self.min); + assert!(self.min <= self.max); + + if self.min <= f32::EPSILON { + return self.min; + } + self.min - self.margin() + } + + fn max_with_margin(self) -> f32 { + assert!(0. <= self.min); + assert!(self.min <= self.max); + + self.max + self.margin() + } + + fn margin(self) -> f32 { + assert!(0. <= self.min); + assert!(self.min <= self.max); + + if (self.max - self.min).abs() > f32::EPSILON { + return (self.max - self.min) * 0.1; + } + 0.1 + } +} + pub struct Interval { pub first: NaiveDate, pub last: NaiveDate, @@ -647,25 +733,19 @@ pub fn view_chart( } } -pub fn plot_line_chart( - data: &[(Vec<(NaiveDate, f32)>, usize)], +pub fn plot_chart( + data: &[PlotData], x_min: NaiveDate, x_max: NaiveDate, - y_min_opt: Option, - y_max_opt: Option, theme: &data::Theme, ) -> Result, Box> { if all_zeros(data) { return Ok(None); } - let (y_min, y_max, y_margin) = determine_y_bounds( - data.iter() - .flat_map(|(s, _)| s.iter().map(|(_, y)| *y)) - .collect::>(), - y_min_opt, - y_max_opt, - ); + let (Some(primary_bounds), secondary_bounds) = determine_y_bounds(data) else { + return Ok(None); + }; let mut result = String::new(); @@ -681,196 +761,21 @@ pub fn plot_line_chart( .x_label_area_size(30f32) .y_label_area_size(40f32); - let mut chart = chart_builder.build_cartesian_2d( - x_min..x_max, - f32::max(0., y_min - y_margin)..y_max + y_margin, - )?; - - chart - .configure_mesh() - .disable_x_mesh() - .set_all_tick_mark_size(3u32) - .axis_style(color.mix(0.3)) - .bold_line_style(color.mix(0.05)) - .light_line_style(color.mix(0.0)) - .label_style(&color) - .x_labels(2) - .y_labels(6) - .draw()?; - - 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())), - )?; - } - - root.present()?; - } - - Ok(Some(result)) -} - -pub fn plot_dual_line_chart( - data: &[(Vec<(NaiveDate, f32)>, usize)], - secondary_data: &[(Vec<(NaiveDate, f32)>, usize)], - x_min: NaiveDate, - x_max: NaiveDate, - 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( - data.iter() - .flat_map(|(s, _)| s.iter().map(|(_, y)| *y)) - .collect::>(), - None, - None, - ); - let (y2_min, y2_max, y2_margin) = determine_y_bounds( - secondary_data - .iter() - .flat_map(|(s, _)| s.iter().map(|(_, y)| *y)) - .collect::>(), - None, - None, - ); - - let mut result = String::new(); - - { - let root = SVGBackend::with_string(&mut result, (chart_width(), 200)).into_drawing_area(); - let (color, background_color) = colors(theme); - - root.fill(&background_color)?; - let mut chart = ChartBuilder::on(&root) .margin(10f32) .x_label_area_size(30f32) .y_label_area_size(40f32) - .right_y_label_area_size(40f32) - .build_cartesian_2d(x_min..x_max, y1_min - y1_margin..y1_max + y1_margin)? - .set_secondary_coord(x_min..x_max, y2_min - y2_margin..y2_max + y2_margin); - - chart - .configure_mesh() - .disable_x_mesh() - .set_all_tick_mark_size(3u32) - .axis_style(color.mix(0.3)) - .bold_line_style(color.mix(0.05)) - .light_line_style(color.mix(0.0)) - .label_style(&color) - .x_labels(2) - .y_labels(6) - .draw()?; - - 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::>(); - 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), - ))?; - - chart.draw_secondary_series( - series - .iter() - .map(|(x, y)| Circle::new((*x, *y), 2, color.filled())), - )?; - } - - 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())), - )?; - } - - root.present()?; - } - - Ok(Some(result)) -} - -pub fn plot_bar_chart( - data: &[(Vec<(NaiveDate, f32)>, usize)], - secondary_data: &[(Vec<(NaiveDate, f32)>, usize)], - x_min: NaiveDate, - x_max: NaiveDate, - y_min_opt: Option, - y_max_opt: Option, - theme: &data::Theme, -) -> Result, Box> { - if all_zeros(data) && all_zeros(secondary_data) { - return Ok(None); - } - - let (y1_min, y1_max, _) = determine_y_bounds( - data.iter() - .flat_map(|(s, _)| s.iter().map(|(_, y)| *y)) - .collect::>(), - y_min_opt, - y_max_opt, - ); - let y1_margin = 0.; - let (y2_min, y2_max, y2_margin) = determine_y_bounds( - secondary_data - .iter() - .flat_map(|(s, _)| s.iter().map(|(_, y)| *y)) - .collect::>(), - None, - None, - ); - - let mut result = String::new(); - - { - let root = SVGBackend::with_string(&mut result, (chart_width(), 200)).into_drawing_area(); - let (color, background_color) = colors(theme); - - root.fill(&background_color)?; - - let mut chart = ChartBuilder::on(&root) - .margin(10f32) - .x_label_area_size(30f32) - .y_label_area_size(40f32) - .right_y_label_area_size(30f32) + .right_y_label_area_size(secondary_bounds.map_or_else(|| 0f32, |_| 40f32)) .build_cartesian_2d( - (x_min..x_max).into_segmented(), - y1_min - y1_margin..y1_max + y1_margin, + x_min..x_max, + primary_bounds.min_with_margin()..primary_bounds.max_with_margin(), )? - .set_secondary_coord(x_min..x_max, y2_min - y2_margin..y2_max + y2_margin); + .set_secondary_coord( + x_min..x_max, + secondary_bounds + .as_ref() + .map_or(0.0..0.0, |b| b.min_with_margin()..b.max_with_margin()), + ); chart .configure_mesh() @@ -884,40 +789,63 @@ pub fn plot_bar_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()?; - - 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).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))); - - chart.draw_series(histogram)?; + if secondary_bounds.is_some() { + 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::>(); + for plot_data in data { + let mut series = plot_data.values.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), - ))?; - - chart.draw_secondary_series( - series - .iter() - .map(|(x, y)| Circle::new((*x, *y), 2, color.filled())), - )?; + + for plot in &plot_data.plots { + match *plot { + PlotType::Circle(color, size) => { + let data = series + .iter() + .map(|(x, y)| { + Circle::new( + (*x, *y), + size, + Palette99::pick(color).mix(0.9).filled(), + ) + }) + .collect::>(); + if plot_data.params.secondary { + chart.draw_secondary_series(data)? + } else { + chart.draw_series(data)? + } + } + PlotType::Line(color, size) => { + let data = LineSeries::new( + series.iter().map(|(x, y)| (*x, *y)), + Palette99::pick(color).mix(0.9).stroke_width(size), + ); + if plot_data.params.secondary { + chart.draw_secondary_series(data)? + } else { + chart.draw_series(data)? + } + } + PlotType::Histogram(color) => { + let data = Histogram::vertical(&chart) + .style(Palette99::pick(color).mix(0.9).filled()) + .margin(0) // https://github.com/plotters-rs/plotters/issues/300 + .data(series.iter().map(|(x, y)| (*x, *y))); + + if plot_data.params.secondary { + chart.draw_secondary_series(data)? + } else { + chart.draw_series(data)? + } + } + }; + } } root.present()?; @@ -926,8 +854,11 @@ 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: &[PlotData]) -> bool { + data.iter() + .map(|v| v.values.iter().all(|(_, v)| *v == 0.0)) + .reduce(|l, r| l && r) + .unwrap_or(true) } fn colors(theme: &data::Theme) -> (RGBColor, RGBColor) { @@ -938,26 +869,36 @@ fn colors(theme: &data::Theme) -> (RGBColor, RGBColor) { } } -fn determine_y_bounds( - y: Vec, - y_min_opt: Option, - y_max_opt: Option, -) -> (f32, f32, f32) { - let y_min = f32::min( - y_min_opt.unwrap_or(f32::MAX), - y.clone().into_iter().reduce(f32::min).unwrap_or(0.), - ); - let y_max = f32::max( - y_max_opt.unwrap_or(0.), - y.into_iter().reduce(f32::max).unwrap_or(0.), - ); - let y_margin = if (y_max - y_min).abs() > f32::EPSILON || y_min == 0. { - (y_max - y_min) * 0.1 - } else { - 0.1 - }; +fn determine_y_bounds(data: &[PlotData]) -> (Option, Option) { + let mut primary_bounds: Option = None; + let mut secondary_bounds: Option = None; + + for plot in data.iter().filter(|plot| !plot.values.is_empty()) { + let min = plot + .values + .iter() + .map(|(_, v)| *v) + .fold(plot.params.y_min_opt.unwrap_or(f32::MAX), f32::min); + let max = plot + .values + .iter() + .map(|(_, v)| *v) + .fold(plot.params.y_max_opt.unwrap_or(0.), f32::max); + + assert!(min <= max, "min={min}, max={max}"); + + let b = if plot.params.secondary { + secondary_bounds.borrow_mut() + } else { + primary_bounds.borrow_mut() + } + .get_or_insert(Bounds { min, max }); + + b.min = f32::min(b.min, min); + b.max = f32::max(b.max, max); + } - (y_min, y_max, y_margin) + (primary_bounds, secondary_bounds) } fn chart_width() -> u32 { diff --git a/frontend/src/ui/page/body_fat.rs b/frontend/src/ui/page/body_fat.rs index 5df8bdb..7061d86 100644 --- a/frontend/src/ui/page/body_fat.rs +++ b/frontend/src/ui/page/body_fat.rs @@ -742,30 +742,33 @@ fn view_chart(model: &Model, data_model: &data::Model) -> Node { ("Weight (kg)", common::COLOR_BODY_WEIGHT), ] .as_slice(), - common::plot_dual_line_chart( + common::plot_chart( &[ - ( - body_fat + common::PlotData { + values: body_weight + .iter() + .map(|bw| (bw.date, bw.weight)) + .collect::>(), + plots: common::plot_line_with_dots(common::COLOR_BODY_WEIGHT), + params: common::PlotParams::SECONDARY, + }, + common::PlotData { + values: body_fat .iter() .filter_map(|bf| bf.jp3(sex).map(|jp3| (bf.date, jp3))) .collect::>(), - common::COLOR_BODY_FAT_JP3, - ), - ( - body_fat + plots: common::plot_line_with_dots(common::COLOR_BODY_FAT_JP3), + params: common::PlotParams::default(), + }, + common::PlotData { + values: body_fat .iter() .filter_map(|bf| bf.jp7(sex).map(|jp7| (bf.date, jp7))) .collect::>(), - common::COLOR_BODY_FAT_JP7, - ), + plots: common::plot_line_with_dots(common::COLOR_BODY_FAT_JP7), + params: common::PlotParams::default(), + }, ], - &[( - body_weight - .iter() - .map(|bw| (bw.date, bw.weight)) - .collect::>(), - common::COLOR_BODY_WEIGHT, - )], model.interval.first, model.interval.last, data_model.theme(), diff --git a/frontend/src/ui/page/body_weight.rs b/frontend/src/ui/page/body_weight.rs index a043f73..67182c5 100644 --- a/frontend/src/ui/page/body_weight.rs +++ b/frontend/src/ui/page/body_weight.rs @@ -361,10 +361,10 @@ fn view_chart(model: &Model, data_model: &data::Model) -> Node { ("Avg. weight (kg)", common::COLOR_AVG_BODY_WEIGHT), ] .as_slice(), - common::plot_line_chart( + common::plot_chart( &[ - ( - data_model + common::PlotData { + values: data_model .body_weight .values() .filter(|bw| { @@ -372,10 +372,11 @@ fn view_chart(model: &Model, data_model: &data::Model) -> Node { }) .map(|bw| (bw.date, bw.weight)) .collect::>(), - common::COLOR_BODY_WEIGHT, - ), - ( - data_model + plots: common::plot_line_with_dots(common::COLOR_BODY_WEIGHT), + params: common::PlotParams::default(), + }, + common::PlotData { + values: data_model .body_weight_stats .values() .filter(|bws| { @@ -383,13 +384,12 @@ fn view_chart(model: &Model, data_model: &data::Model) -> Node { }) .filter_map(|bws| bws.avg_weight.map(|avg_weight| (bws.date, avg_weight))) .collect::>(), - common::COLOR_AVG_BODY_WEIGHT, - ), + plots: common::plot_line_with_dots(common::COLOR_AVG_BODY_WEIGHT), + params: common::PlotParams::default(), + }, ], model.interval.first, model.interval.last, - None, - None, data_model.theme(), ), true, diff --git a/frontend/src/ui/page/exercise.rs b/frontend/src/ui/page/exercise.rs index d2d256c..b15cbaa 100644 --- a/frontend/src/ui/page/exercise.rs +++ b/frontend/src/ui/page/exercise.rs @@ -501,21 +501,22 @@ pub fn view_charts( let mut labels = vec![("Repetitions", common::COLOR_REPS)]; - let mut data = vec![( - reps_rpe + let mut data = vec![common::PlotData { + values: reps_rpe .iter() .map(|(date, (avg_reps, _))| { #[allow(clippy::cast_precision_loss)] (*date, avg_reps.iter().sum::() / avg_reps.len() as f32) }) .collect::>(), - common::COLOR_REPS, - )]; + plots: common::plot_line_with_dots(common::COLOR_REPS), + params: common::PlotParams::primary_range(0., 10.), + }]; if show_rpe { labels.push(("+ Repetitions in reserve", common::COLOR_REPS_RIR)); - data.push(( - reps_rpe + data.push(common::PlotData { + values: reps_rpe .into_iter() .filter_map(|(date, (avg_reps_values, avg_rpe_values))| { #[allow(clippy::cast_precision_loss)] @@ -530,37 +531,36 @@ pub fn view_charts( } }) .collect::>(), - common::COLOR_REPS_RIR, - )); + plots: common::plot_line_with_dots(common::COLOR_REPS_RIR), + params: common::PlotParams::primary_range(0., 10.), + }); } nodes![ common::view_chart( &[("Set volume", common::COLOR_SET_VOLUME)], - common::plot_line_chart( - &[( - set_volume.into_iter().collect::>(), - common::COLOR_SET_VOLUME, - )], + common::plot_chart( + &[common::PlotData { + values: set_volume.into_iter().collect::>(), + plots: common::plot_line_with_dots(common::COLOR_SET_VOLUME), + params: common::PlotParams::primary_range(0., 10.), + }], interval.first, interval.last, - Some(0.), - Some(10.), theme, ), false, ), common::view_chart( &[("Volume load", common::COLOR_VOLUME_LOAD)], - common::plot_line_chart( - &[( - volume_load.into_iter().collect::>(), - common::COLOR_VOLUME_LOAD, - )], + common::plot_chart( + &[common::PlotData { + values: volume_load.into_iter().collect::>(), + plots: common::plot_line_with_dots(common::COLOR_VOLUME_LOAD), + params: common::PlotParams::primary_range(0., 10.), + }], interval.first, interval.last, - Some(0.), - Some(10.), theme, ), false, @@ -568,12 +568,14 @@ pub fn view_charts( IF![show_tut => common::view_chart( &[("Time under tension (s)", common::COLOR_TUT)], - common::plot_line_chart( - &[(tut.into_iter().collect::>(), common::COLOR_TUT,)], + common::plot_chart( + &[common::PlotData { + values: tut.into_iter().collect::>(), + plots: common::plot_line_with_dots(common::COLOR_TUT), + params: common::PlotParams::primary_range(0., 10.), + }], interval.first, interval.last, - Some(0.), - Some(10.), theme, ), false, @@ -581,33 +583,25 @@ pub fn view_charts( ], common::view_chart( &labels, - common::plot_line_chart( - &data, - interval.first, - interval.last, - Some(0.), - Some(10.), - theme, - ), + common::plot_chart(&data, interval.first, interval.last, theme), false, ), common::view_chart( &[("Weight (kg)", common::COLOR_WEIGHT)], - common::plot_line_chart( - &[( - weight + common::plot_chart( + &[common::PlotData { + values: weight .into_iter() .map(|(date, values)| { #[allow(clippy::cast_precision_loss)] (date, values.iter().sum::() / values.len() as f32) }) .collect::>(), - common::COLOR_WEIGHT, - )], + plots: common::plot_line_with_dots(common::COLOR_WEIGHT), + params: common::PlotParams::primary_range(0., 10.), + }], interval.first, interval.last, - Some(0.), - Some(10.), theme, ), false, @@ -615,20 +609,19 @@ pub fn view_charts( IF![show_tut => common::view_chart( &[("Time (s)", common::COLOR_TIME)], - common::plot_line_chart( - &[( - time.into_iter() + common::plot_chart( + &[common::PlotData{ + values: time.into_iter() .map(|(date, values)| { #[allow(clippy::cast_precision_loss)] (date, values.iter().sum::() / values.len() as f32) }) .collect::>(), - common::COLOR_TIME, - )], + plots: common::plot_line_with_dots(common::COLOR_TIME), + params: common::PlotParams::primary_range(0., 10.) + }], interval.first, interval.last, - Some(0.), - Some(10.), theme, ), false, diff --git a/frontend/src/ui/page/menstrual_cycle.rs b/frontend/src/ui/page/menstrual_cycle.rs index f66e29c..79681fb 100644 --- a/frontend/src/ui/page/menstrual_cycle.rs +++ b/frontend/src/ui/page/menstrual_cycle.rs @@ -345,19 +345,17 @@ fn view_chart(model: &Model, data_model: &data::Model) -> Node { common::view_chart( vec![("Intensity", common::COLOR_PERIOD_INTENSITY)].as_slice(), - common::plot_bar_chart( - &[( - period + common::plot_chart( + &[common::PlotData { + values: period .iter() .map(|p| (p.date, f32::from(p.intensity))) .collect::>(), - common::COLOR_PERIOD_INTENSITY, - )], - &[], + plots: [common::PlotType::Histogram(common::COLOR_PERIOD_INTENSITY)].to_vec(), + params: common::PlotParams::primary_range(0., 4.), + }], model.interval.first, model.interval.last, - Some(0.), - Some(4.), data_model.theme(), ), true, diff --git a/frontend/src/ui/page/muscles.rs b/frontend/src/ui/page/muscles.rs index 4ca5e34..b88feec 100644 --- a/frontend/src/ui/page/muscles.rs +++ b/frontend/src/ui/page/muscles.rs @@ -97,12 +97,14 @@ pub fn view(model: &Model, data_model: &data::Model) -> Node { ], common::view_chart( &[("Set volume (weekly total)", common::COLOR_SET_VOLUME)], - common::plot_line_chart( - &[(set_volume, common::COLOR_SET_VOLUME)], + common::plot_chart( + &[common::PlotData { + values: set_volume, + plots: common::plot_line_with_dots(common::COLOR_SET_VOLUME), + params: common::PlotParams::primary_range(0., 10.), + }], model.interval.first, model.interval.last, - Some(0.), - Some(10.), data_model.theme() ), true, diff --git a/frontend/src/ui/page/routine.rs b/frontend/src/ui/page/routine.rs index 0bbfc04..93570fc 100644 --- a/frontend/src/ui/page/routine.rs +++ b/frontend/src/ui/page/routine.rs @@ -1338,27 +1338,28 @@ pub fn view_charts( nodes![ common::view_chart( &[("Load", common::COLOR_LOAD)], - common::plot_line_chart( - &[(load.into_iter().collect::>(), common::COLOR_LOAD)], + common::plot_chart( + &[common::PlotData { + values: load.into_iter().collect::>(), + plots: common::plot_line_with_dots(common::COLOR_LOAD), + params: common::PlotParams::primary_range(0., 10.), + }], interval.first, interval.last, - Some(0.), - Some(10.), theme, ), false, ), common::view_chart( &[("Set volume", common::COLOR_SET_VOLUME)], - common::plot_line_chart( - &[( - set_volume.into_iter().collect::>(), - common::COLOR_SET_VOLUME, - )], + common::plot_chart( + &[common::PlotData { + values: set_volume.into_iter().collect::>(), + plots: common::plot_line_with_dots(common::COLOR_SET_VOLUME), + params: common::PlotParams::primary_range(0., 10.), + }], interval.first, interval.last, - Some(0.), - Some(10.), theme, ), false, @@ -1367,9 +1368,9 @@ pub fn view_charts( show_rpe => common::view_chart( &[("RPE", common::COLOR_RPE)], - common::plot_line_chart( - &[( - rpe.into_iter() + common::plot_chart( + &[common::PlotData{ + values: rpe.into_iter() .map(|(date, values)| { #[allow(clippy::cast_precision_loss)] ( @@ -1382,12 +1383,11 @@ pub fn view_charts( ) }) .collect::>(), - common::COLOR_RPE, - )], + plots: common::plot_line_with_dots(common::COLOR_RPE), + params: common::PlotParams::primary_range(5., 10.) + }], interval.first, interval.last, - Some(5.), - Some(10.), theme, ), false, diff --git a/frontend/src/ui/page/training.rs b/frontend/src/ui/page/training.rs index 8049451..43b0806 100644 --- a/frontend/src/ui/page/training.rs +++ b/frontend/src/ui/page/training.rs @@ -528,29 +528,45 @@ pub fn view_charts( ("Short-term load", common::COLOR_LOAD), ("Long-term load", common::COLOR_LONG_TERM_LOAD) ], - common::plot_line_chart( + common::plot_chart( &[ - (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) + common::PlotData { + values: long_term_load_low, + plots: common::plot_line_with_dots(common::COLOR_LONG_TERM_LOAD_BOUNDS), + params: common::PlotParams::primary_range(0., 10.), + }, + common::PlotData { + values: long_term_load_high, + plots: common::plot_line_with_dots(common::COLOR_LONG_TERM_LOAD_BOUNDS), + params: common::PlotParams::primary_range(0., 10.), + }, + common::PlotData { + values: long_term_load, + plots: common::plot_line_with_dots(common::COLOR_LONG_TERM_LOAD), + params: common::PlotParams::primary_range(0., 10.), + }, + common::PlotData { + values: short_term_load, + plots: common::plot_line_with_dots(common::COLOR_LOAD), + params: common::PlotParams::primary_range(0., 10.), + } ], interval.first, interval.last, - Some(0.), - Some(10.), theme, ), false, ), common::view_chart( &[("Set volume (weekly total)", common::COLOR_SET_VOLUME)], - common::plot_line_chart( - &[(total_set_volume_per_week, common::COLOR_SET_VOLUME)], + common::plot_chart( + &[common::PlotData { + values: total_set_volume_per_week, + plots: common::plot_line_with_dots(common::COLOR_SET_VOLUME), + params: common::PlotParams::primary_range(0., 10.), + }], interval.first, interval.last, - Some(0.), - Some(10.), theme, ), false, @@ -559,12 +575,13 @@ pub fn view_charts( show_rpe => common::view_chart( &[("RPE (weekly average)", common::COLOR_RPE)], - common::plot_line_chart( - &[(avg_rpe_per_week, common::COLOR_RPE)], + common::plot_chart( + &[common::PlotData{values: avg_rpe_per_week, + plots: common::plot_line_with_dots(common::COLOR_RPE), + params: common::PlotParams::primary_range(5., 10.) + }], interval.first, interval.last, - Some(5.), - Some(10.), theme, ), false,