Add calorie intake plot
This commit is contained in:
parent
e05437b198
commit
6c79af8213
186
src/main.rs
186
src/main.rs
@ -2,21 +2,26 @@
|
|||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::State,
|
extract::State,
|
||||||
http::{header, StatusCode, Uri},
|
http::{
|
||||||
|
// header,
|
||||||
|
StatusCode,
|
||||||
|
Uri,
|
||||||
|
},
|
||||||
response::{Html, IntoResponse},
|
response::{Html, IntoResponse},
|
||||||
routing::get,
|
routing::get,
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use image::ImageFormat;
|
// use image::ImageFormat;
|
||||||
use mysql::prelude::*;
|
// use mysql::prelude::*;
|
||||||
use mysql::*;
|
use mysql::*;
|
||||||
use plotters::prelude::*;
|
// use plotters::prelude::*;
|
||||||
use std::io::{BufWriter, Cursor};
|
// use std::io::{BufWriter, Cursor};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
mod math;
|
||||||
mod my_structs;
|
mod my_structs;
|
||||||
mod plotting;
|
mod plotting;
|
||||||
use crate::my_structs::MyError;
|
// use crate::my_structs::MyError;
|
||||||
use crate::plotting::Plotter;
|
// use crate::plotting::Plotter;
|
||||||
use std::{thread, time};
|
use std::{thread, time};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -49,7 +54,8 @@ async fn main() {
|
|||||||
.route("/html_plotter", get(html_plotter))
|
.route("/html_plotter", get(html_plotter))
|
||||||
.route("/image.png", get(png_image))
|
.route("/image.png", get(png_image))
|
||||||
.route("/image.svg", get(svg_image))
|
.route("/image.svg", get(svg_image))
|
||||||
.route("/image2.svg", get(svg_image2))
|
.route("/calorie_intake.svg", get(svg_calorie_intake))
|
||||||
|
.route("/calorie_intake", get(calorie_intake))
|
||||||
.with_state(shared_state)
|
.with_state(shared_state)
|
||||||
.fallback(not_found);
|
.fallback(not_found);
|
||||||
|
|
||||||
@ -59,43 +65,19 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn not_found(uri: Uri) -> (StatusCode, String) {
|
async fn not_found(uri: Uri) -> (StatusCode, String) {
|
||||||
(StatusCode::NOT_FOUND, format!("No route for {uri}"))
|
(StatusCode::NOT_FOUND, format!("404 not found: {uri}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_nutrition_rows(sql_pool: Pool) -> Result<Vec<my_structs::Nutrition>> {
|
async fn get_rows(State(state): State<Arc<AppState>>) -> Result<String, my_structs::MyError> {
|
||||||
let mut conn = sql_pool
|
let nutrition_val_promise =
|
||||||
.get_conn()
|
my_structs::Nutrition::get_nutrition_hashmap(state.sql_pool.clone());
|
||||||
.expect("Cannot establish database connection");
|
let purchases_val_promise = my_structs::Purchase::get_purchase_rows(state.sql_pool.clone());
|
||||||
|
|
||||||
let (nutrition_query, nutrition_closure) = my_structs::Nutrition::query_map_helper();
|
|
||||||
let nutrition_val: Vec<my_structs::Nutrition> = conn
|
|
||||||
.query_map(nutrition_query, nutrition_closure)
|
|
||||||
.expect("Data in database doesn't match the Nutrition class");
|
|
||||||
|
|
||||||
Ok(nutrition_val)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_purchases_rows(sql_pool: Pool) -> Result<Vec<my_structs::Purchase>> {
|
|
||||||
let mut conn = sql_pool
|
|
||||||
.get_conn()
|
|
||||||
.expect("Cannot establish database connection");
|
|
||||||
|
|
||||||
let (purchases_query, purchases_closure) = my_structs::Purchase::query_map_helper();
|
|
||||||
let purchases_val: Vec<my_structs::Purchase> = conn
|
|
||||||
.query_map(purchases_query, purchases_closure)
|
|
||||||
.expect("Data in database doesn't match the Purchase class");
|
|
||||||
|
|
||||||
Ok(purchases_val)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_rows(State(state): State<Arc<AppState>>) -> Result<String, MyError> {
|
|
||||||
let nutrition_val_promise = get_nutrition_rows(state.sql_pool.clone());
|
|
||||||
let purchases_val_promise = get_purchases_rows(state.sql_pool.clone());
|
|
||||||
|
|
||||||
let nutrition_val = nutrition_val_promise.await?;
|
let nutrition_val = nutrition_val_promise.await?;
|
||||||
let purchases_val = purchases_val_promise.await?;
|
let purchases_val = purchases_val_promise.await?;
|
||||||
|
|
||||||
println!("Serving: get_rows");
|
println!("Serving: get_rows");
|
||||||
|
// Ok("See print logs instead".to_string())
|
||||||
Ok(format!("{:#?} {:#?}", nutrition_val, purchases_val))
|
Ok(format!("{:#?} {:#?}", nutrition_val, purchases_val))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,122 +87,42 @@ async fn html_demo(State(_state): State<Arc<AppState>>) -> Html<String> {
|
|||||||
Html("<body><p>This is HTML</p></body>".to_string())
|
Html("<body><p>This is HTML</p></body>".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_bitmap_as_png(bitmap: Vec<u8>, width: u32, height: u32) -> Vec<u8> {
|
async fn svg_image(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||||
let image =
|
println!("Serving: svg_image");
|
||||||
image::ImageBuffer::<image::Rgb<u8>, Vec<u8>>::from_raw(width, height, bitmap).unwrap();
|
let fun = plotting::SVGPlotter::examplefun();
|
||||||
let mut buffer = BufWriter::new(Cursor::new(Vec::new()));
|
plotting::SVGPlotter::plot(640, 480, fun)
|
||||||
image.write_to(&mut buffer, ImageFormat::Png).unwrap();
|
|
||||||
let bytes: Vec<u8> = buffer.into_inner().unwrap().into_inner();
|
|
||||||
bytes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn plot_stuff_bitmap(image_buffer: &mut [u8], width: u32, height: u32) {
|
async fn svg_calorie_intake(
|
||||||
let root = BitMapBackend::with_buffer(image_buffer, (width, height)).into_drawing_area();
|
State(state): State<Arc<AppState>>,
|
||||||
root.fill(&WHITE).unwrap();
|
) -> Result<impl IntoResponse, my_structs::MyError> {
|
||||||
let mut chart = ChartBuilder::on(&root)
|
println!("Serving: svg_calorie_intake");
|
||||||
.caption("y=x^2", ("sans-serif", 50).into_font())
|
|
||||||
.margin(5)
|
|
||||||
.x_label_area_size(30)
|
|
||||||
.y_label_area_size(30)
|
|
||||||
.build_cartesian_2d(-1f32..1f32, -0.1f32..1f32)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
chart.configure_mesh().draw().unwrap();
|
let nutrition_val_promise =
|
||||||
|
my_structs::Nutrition::get_nutrition_hashmap(state.sql_pool.clone());
|
||||||
|
let purchases_val_promise = my_structs::Purchase::get_purchase_rows(state.sql_pool.clone());
|
||||||
|
|
||||||
chart
|
let nutrition_val = nutrition_val_promise.await?;
|
||||||
.draw_series(LineSeries::new(
|
let purchases_val = purchases_val_promise.await?;
|
||||||
(-50..=50).map(|x| x as f32 / 50.0).map(|x| (x, x * x)),
|
|
||||||
&RED,
|
|
||||||
))
|
|
||||||
.unwrap()
|
|
||||||
.label("y = x^2")
|
|
||||||
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED));
|
|
||||||
|
|
||||||
chart
|
let plotfun = math::plot_calories_per_day(nutrition_val, purchases_val);
|
||||||
.configure_series_labels()
|
Ok(plotting::SVGPlotter::plot(640, 480, plotfun))
|
||||||
.background_style(WHITE.mix(0.8))
|
|
||||||
.border_style(BLACK)
|
|
||||||
.draw()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
root.present().unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn png_image(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
async fn png_image(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||||
println!("Serving: png_image");
|
println!("Serving: png_image");
|
||||||
|
let fun = plotting::BitMapPlotter::examplefun();
|
||||||
let width: u32 = 640;
|
plotting::BitMapPlotter::plot(640, 480, fun, plotting::BitMapOutputFormat::PNG)
|
||||||
let height: u32 = 480;
|
|
||||||
let mut image_buffer = vec![0; (width as usize) * (height as usize) * 3];
|
|
||||||
plot_stuff_bitmap(image_buffer.as_mut_slice(), width, height);
|
|
||||||
|
|
||||||
let image = format_bitmap_as_png(image_buffer, width, height);
|
|
||||||
|
|
||||||
let headers = [
|
|
||||||
(header::CONTENT_TYPE, "image/png"),
|
|
||||||
(header::CONTENT_DISPOSITION, "inline"),
|
|
||||||
];
|
|
||||||
(headers, image).into_response()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn plot_stuff_svg(image_string: &mut String, width: u32, height: u32) {
|
|
||||||
let root = SVGBackend::with_string(image_string, (width, height)).into_drawing_area();
|
|
||||||
root.fill(&WHITE).unwrap();
|
|
||||||
let mut chart = ChartBuilder::on(&root)
|
|
||||||
.caption("y=x^2", ("sans-serif", 50).into_font())
|
|
||||||
.margin(5)
|
|
||||||
.x_label_area_size(30)
|
|
||||||
.y_label_area_size(30)
|
|
||||||
.build_cartesian_2d(-1f32..1f32, -0.1f32..1f32)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
chart.configure_mesh().draw().unwrap();
|
|
||||||
|
|
||||||
chart
|
|
||||||
.draw_series(LineSeries::new(
|
|
||||||
(-50..=50).map(|x| x as f32 / 50.0).map(|x| (x, x * x)),
|
|
||||||
&RED,
|
|
||||||
))
|
|
||||||
.unwrap()
|
|
||||||
.label("y = x^2")
|
|
||||||
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED));
|
|
||||||
|
|
||||||
chart
|
|
||||||
.configure_series_labels()
|
|
||||||
.background_style(WHITE.mix(0.8))
|
|
||||||
.border_style(BLACK)
|
|
||||||
.draw()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
root.present().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn svg_image(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
|
||||||
println!("Serving: svg_image");
|
|
||||||
|
|
||||||
let width: u32 = 640;
|
|
||||||
let height: u32 = 480;
|
|
||||||
let mut image_string = String::new();
|
|
||||||
plot_stuff_svg(&mut image_string, width, height);
|
|
||||||
|
|
||||||
let headers = [
|
|
||||||
(header::CONTENT_TYPE, "image/svg+xml"),
|
|
||||||
(header::CONTENT_DISPOSITION, "inline"),
|
|
||||||
];
|
|
||||||
(headers, image_string).into_response()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn svg_image2(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
|
||||||
println!("Serving: svg_image2");
|
|
||||||
let fun = plotting::SVGPlotter::examplefun();
|
|
||||||
plotting::SVGPlotter::plot(640, 480, fun)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn html_plotter(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn html_plotter(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
println!("Serving: html_plotter");
|
println!("Serving: html_plotter");
|
||||||
|
|
||||||
Html(
|
Html("<body><img src=/image.svg></img><img src=/calorie_intake.svg></img></body>".to_string())
|
||||||
"<body><p>This is HTML</p><img src=/image.png></img><img src=/image.svg></img></body>"
|
}
|
||||||
.to_string(),
|
|
||||||
)
|
async fn calorie_intake(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
|
println!("Serving: calorie_intake");
|
||||||
|
|
||||||
|
Html("<body><img src=/calorie_intake.svg></img></body>".to_string())
|
||||||
}
|
}
|
||||||
|
3
src/math.rs
Normal file
3
src/math.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// pub use self::calculations::calculate_calories_per_day;
|
||||||
|
pub use self::calculations::plot_calories_per_day;
|
||||||
|
mod calculations;
|
142
src/math/calculations.rs
Normal file
142
src/math/calculations.rs
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
use crate::my_structs;
|
||||||
|
use crate::plotting;
|
||||||
|
// use chrono::TimeZone;
|
||||||
|
use chrono::{DateTime, NaiveDate, Utc};
|
||||||
|
use plotters::prelude::*;
|
||||||
|
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 = 14;
|
||||||
|
|
||||||
|
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, my_structs::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] += 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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, my_structs::Nutrition>,
|
||||||
|
purchases: Vec<my_structs::Purchase>,
|
||||||
|
) -> 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 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)
|
||||||
|
.draw()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
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::UpperRight)
|
||||||
|
// .draw()
|
||||||
|
// .unwrap();
|
||||||
|
|
||||||
|
root.present().unwrap();
|
||||||
|
});
|
||||||
|
plotfun
|
||||||
|
}
|
@ -1,15 +1,18 @@
|
|||||||
|
use mysql::prelude::*;
|
||||||
|
use mysql::*;
|
||||||
use mysql_common::frunk::{hlist_pat, HList};
|
use mysql_common::frunk::{hlist_pat, HList};
|
||||||
|
use std::collections::HashMap;
|
||||||
use struct_field_names_as_array::FieldNamesAsArray;
|
use struct_field_names_as_array::FieldNamesAsArray;
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[derive(Debug, PartialEq, FieldNamesAsArray)]
|
#[derive(Debug, PartialEq, FieldNamesAsArray)]
|
||||||
pub struct Nutrition {
|
pub struct Nutrition {
|
||||||
id: i32,
|
id: u32,
|
||||||
name: String,
|
name: String,
|
||||||
manufacturer: String,
|
manufacturer: String,
|
||||||
barcode: String,
|
barcode: String,
|
||||||
amount: i32,
|
amount: u32,
|
||||||
divisor: i32,
|
divisor: u32,
|
||||||
kJ: f32,
|
kJ: f32,
|
||||||
kcal: f32,
|
kcal: f32,
|
||||||
saturated_fat: f32,
|
saturated_fat: f32,
|
||||||
@ -25,12 +28,12 @@ pub struct Nutrition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RowType = HList!(
|
type RowType = HList!(
|
||||||
i32,
|
u32,
|
||||||
Option<String>,
|
Option<String>,
|
||||||
Option<String>,
|
Option<String>,
|
||||||
Option<String>,
|
Option<String>,
|
||||||
Option<i32>,
|
Option<u32>,
|
||||||
Option<i32>,
|
Option<u32>,
|
||||||
Option<f32>,
|
Option<f32>,
|
||||||
Option<f32>,
|
Option<f32>,
|
||||||
Option<f32>,
|
Option<f32>,
|
||||||
@ -54,7 +57,7 @@ impl Nutrition {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn query_map_helper() -> (String, impl Fn(RowType) -> Nutrition) {
|
fn query_map_helper() -> (String, impl Fn(RowType) -> Nutrition) {
|
||||||
let sql_query = format!("SELECT {} from nutrition", Self::get_sql_fields());
|
let sql_query = format!("SELECT {} from nutrition", Self::get_sql_fields());
|
||||||
|
|
||||||
let construction_closure = |row: RowType| {
|
let construction_closure = |row: RowType| {
|
||||||
@ -102,4 +105,37 @@ impl Nutrition {
|
|||||||
|
|
||||||
(sql_query, construction_closure)
|
(sql_query, construction_closure)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_nutrition_rows(sql_pool: Pool) -> Result<Vec<Nutrition>> {
|
||||||
|
let mut conn = sql_pool
|
||||||
|
.get_conn()
|
||||||
|
.expect("Cannot establish database connection");
|
||||||
|
|
||||||
|
let (nutrition_query, nutrition_closure) = Self::query_map_helper();
|
||||||
|
let nutrition_val: Vec<Nutrition> = conn
|
||||||
|
.query_map(nutrition_query, nutrition_closure)
|
||||||
|
.expect("Data in database doesn't match the Nutrition class");
|
||||||
|
|
||||||
|
Ok(nutrition_val)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_nutrition_hashmap(sql_pool: Pool) -> Result<HashMap<u32, Nutrition>> {
|
||||||
|
let nutrition_vec = Self::get_nutrition_rows(sql_pool)?;
|
||||||
|
let nutrition_hashmap: HashMap<u32, Nutrition> =
|
||||||
|
nutrition_vec.into_iter().map(|n| (n.id, n)).collect();
|
||||||
|
Ok(nutrition_hashmap)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn total_kcal(&self) -> f32 {
|
||||||
|
match self.divisor {
|
||||||
|
0 => 0.,
|
||||||
|
_ => self.kcal * self.amount as f32 / self.divisor as f32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
// Comparing self.kcal with 0. should be okay, since it is initialized to 0. and if it is
|
||||||
|
// changed in any way, this function should return true
|
||||||
|
self.amount != 0 && self.divisor != 0 && self.kcal != 0.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use mysql::prelude::*;
|
||||||
|
use mysql::*;
|
||||||
use mysql_common::frunk::{hlist_pat, HList};
|
use mysql_common::frunk::{hlist_pat, HList};
|
||||||
use struct_field_names_as_array::FieldNamesAsArray;
|
use struct_field_names_as_array::FieldNamesAsArray;
|
||||||
use time::PrimitiveDateTime;
|
use time::PrimitiveDateTime;
|
||||||
@ -6,9 +8,9 @@ use time::PrimitiveDateTime;
|
|||||||
#[derive(Debug, PartialEq, FieldNamesAsArray)]
|
#[derive(Debug, PartialEq, FieldNamesAsArray)]
|
||||||
pub struct Purchase {
|
pub struct Purchase {
|
||||||
id: u32,
|
id: u32,
|
||||||
nutrition_id: u32,
|
pub nutrition_id: u32,
|
||||||
amount: u32,
|
pub amount: u32,
|
||||||
datetime: PrimitiveDateTime,
|
pub datetime: PrimitiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
type RowType = HList!(u32, u32, u32, PrimitiveDateTime);
|
type RowType = HList!(u32, u32, u32, PrimitiveDateTime);
|
||||||
@ -22,7 +24,7 @@ impl Purchase {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn query_map_helper() -> (String, impl Fn(RowType) -> Purchase) {
|
fn query_map_helper() -> (String, impl Fn(RowType) -> Purchase) {
|
||||||
let sql_query = format!("SELECT {} from purchases", Self::get_sql_fields());
|
let sql_query = format!("SELECT {} from purchases", Self::get_sql_fields());
|
||||||
println!("{}", sql_query);
|
println!("{}", sql_query);
|
||||||
|
|
||||||
@ -38,4 +40,17 @@ impl Purchase {
|
|||||||
|
|
||||||
(sql_query, construction_closure)
|
(sql_query, construction_closure)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_purchase_rows(sql_pool: Pool) -> Result<Vec<Purchase>> {
|
||||||
|
let mut conn = sql_pool
|
||||||
|
.get_conn()
|
||||||
|
.expect("Cannot establish database connection");
|
||||||
|
|
||||||
|
let (purchases_query, purchases_closure) = Self::query_map_helper();
|
||||||
|
let purchases_val: Vec<Purchase> = conn
|
||||||
|
.query_map(purchases_query, purchases_closure)
|
||||||
|
.expect("Data in database doesn't match the Purchase class");
|
||||||
|
|
||||||
|
Ok(purchases_val)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
pub use self::plotter::Plotter;
|
// pub use self::plotter::Plotter;
|
||||||
|
pub use self::bitmap_plotter::BitMapOutputFormat;
|
||||||
|
pub use self::bitmap_plotter::BitMapPlotFun;
|
||||||
|
pub use self::bitmap_plotter::BitMapPlotRoot;
|
||||||
|
pub use self::bitmap_plotter::BitMapPlotter;
|
||||||
|
pub use self::svg_plotter::SVGPlotFun;
|
||||||
|
pub use self::svg_plotter::SVGPlotRoot;
|
||||||
pub use self::svg_plotter::SVGPlotter;
|
pub use self::svg_plotter::SVGPlotter;
|
||||||
mod plotter;
|
// mod plotter;
|
||||||
|
mod bitmap_plotter;
|
||||||
mod svg_plotter;
|
mod svg_plotter;
|
||||||
|
91
src/plotting/bitmap_plotter.rs
Normal file
91
src/plotting/bitmap_plotter.rs
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
// use super::plotter::Plotter;
|
||||||
|
use axum::http::header;
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use image::ImageFormat;
|
||||||
|
use plotters::prelude::*;
|
||||||
|
use std::io::{BufWriter, Cursor};
|
||||||
|
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
|
pub enum BitMapOutputFormat {
|
||||||
|
PNG,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BitMapPlotter {}
|
||||||
|
pub type BitMapPlotRoot<'a> =
|
||||||
|
plotters::drawing::DrawingArea<plotters::backend::BitMapBackend<'a>, plotters::coord::Shift>;
|
||||||
|
pub type BitMapPlotFun = Box<dyn Fn(BitMapPlotRoot)>;
|
||||||
|
|
||||||
|
impl BitMapPlotter {
|
||||||
|
pub fn examplefun() -> BitMapPlotFun {
|
||||||
|
Box::new(|root: BitMapPlotRoot| {
|
||||||
|
root.fill(&WHITE).unwrap();
|
||||||
|
let mut chart = ChartBuilder::on(&root)
|
||||||
|
.caption("y=x^2", ("sans-serif", 50).into_font())
|
||||||
|
.margin(5)
|
||||||
|
.x_label_area_size(30)
|
||||||
|
.y_label_area_size(30)
|
||||||
|
.build_cartesian_2d(-1f32..1f32, -0.1f32..1f32)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
chart.configure_mesh().draw().unwrap();
|
||||||
|
|
||||||
|
chart
|
||||||
|
.draw_series(LineSeries::new(
|
||||||
|
(-50..=50).map(|x| x as f32 / 50.0).map(|x| (x, x * x)),
|
||||||
|
&RED,
|
||||||
|
))
|
||||||
|
.unwrap()
|
||||||
|
.label("y = x^2")
|
||||||
|
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED));
|
||||||
|
|
||||||
|
chart
|
||||||
|
.configure_series_labels()
|
||||||
|
.background_style(WHITE.mix(0.8))
|
||||||
|
.border_style(BLACK)
|
||||||
|
.draw()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
root.present().unwrap();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_bitmap_as_png(bitmap: Vec<u8>, width: u32, height: u32) -> Vec<u8> {
|
||||||
|
let image =
|
||||||
|
image::ImageBuffer::<image::Rgb<u8>, Vec<u8>>::from_raw(width, height, bitmap).unwrap();
|
||||||
|
let mut buffer = BufWriter::new(Cursor::new(Vec::new()));
|
||||||
|
image.write_to(&mut buffer, ImageFormat::Png).unwrap();
|
||||||
|
let bytes: Vec<u8> = buffer.into_inner().unwrap().into_inner();
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn plot(
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
plotting_fun: BitMapPlotFun,
|
||||||
|
format: BitMapOutputFormat,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let mut image_buffer = vec![0; (width as usize) * (height as usize) * 3];
|
||||||
|
|
||||||
|
{
|
||||||
|
let root =
|
||||||
|
// BitMapBackend::with_buffer(&mut image_string, (width, height)).into_drawing_area();
|
||||||
|
BitMapBackend::with_buffer(image_buffer.as_mut_slice(), (width, height)).into_drawing_area();
|
||||||
|
plotting_fun(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
let image = match format {
|
||||||
|
BitMapOutputFormat::PNG => Self::format_bitmap_as_png(image_buffer, width, height),
|
||||||
|
};
|
||||||
|
|
||||||
|
let headers = [
|
||||||
|
(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
match format {
|
||||||
|
BitMapOutputFormat::PNG => "image/png",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(header::CONTENT_DISPOSITION, "inline"),
|
||||||
|
];
|
||||||
|
(headers, image).into_response()
|
||||||
|
}
|
||||||
|
}
|
@ -1,21 +1,21 @@
|
|||||||
use axum::response::IntoResponse;
|
//use axum::response::IntoResponse;
|
||||||
use plotters::prelude::*;
|
//use plotters::prelude::*;
|
||||||
|
|
||||||
pub trait Plotter {
|
//pub trait Plotter {
|
||||||
//fn plot(width: u32, height: u32, plotting_fun: &dyn Fn(IntoDrawingArea)) -> impl IntoResponse;
|
// //fn plot(width: u32, height: u32, plotting_fun: &dyn Fn(IntoDrawingArea)) -> impl IntoResponse;
|
||||||
fn plot(
|
// fn plot(
|
||||||
width: u32,
|
// width: u32,
|
||||||
height: u32,
|
// height: u32,
|
||||||
//plotting_fun: &dyn Fn(Box<dyn IntoDrawingArea>),
|
// //plotting_fun: &dyn Fn(Box<dyn IntoDrawingArea>),
|
||||||
plotting_fun: Box<
|
// plotting_fun: Box<
|
||||||
dyn Fn(
|
// dyn Fn(
|
||||||
plotters::drawing::DrawingArea<
|
// plotters::drawing::DrawingArea<
|
||||||
plotters::backend::SVGBackend,
|
// plotters::backend::SVGBackend,
|
||||||
plotters::coord::Shift,
|
// plotters::coord::Shift,
|
||||||
>,
|
// >,
|
||||||
),
|
// ),
|
||||||
>,
|
// >,
|
||||||
) -> impl IntoResponse;
|
// ) -> impl IntoResponse;
|
||||||
//fn plot(width: u32, height: u32, plotting_fun: &dyn Fn()) -> impl IntoResponse;
|
// //fn plot(width: u32, height: u32, plotting_fun: &dyn Fn()) -> impl IntoResponse;
|
||||||
//fn plot(width: u32, height: u32) -> impl IntoResponse;
|
// //fn plot(width: u32, height: u32) -> impl IntoResponse;
|
||||||
}
|
//}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use super::plotter::Plotter;
|
// use super::plotter::Plotter;
|
||||||
use axum::http::header;
|
use axum::http::header;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use plotters::prelude::*;
|
use plotters::prelude::*;
|
||||||
@ -41,10 +41,8 @@ impl SVGPlotter {
|
|||||||
root.present().unwrap();
|
root.present().unwrap();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Plotter for SVGPlotter {
|
pub fn plot(width: u32, height: u32, plotting_fun: SVGPlotFun) -> impl IntoResponse {
|
||||||
fn plot(width: u32, height: u32, plotting_fun: SVGPlotFun) -> impl IntoResponse {
|
|
||||||
let mut image_string = String::new();
|
let mut image_string = String::new();
|
||||||
|
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user