Compare commits
3 Commits
61865bcb7a
...
3fbc0a2ff8
Author | SHA1 | Date |
---|---|---|
|
3fbc0a2ff8 | |
|
726dfefadf | |
|
a4c3b37019 |
|
@ -5,6 +5,7 @@
|
||||||
!/Cargo.lock
|
!/Cargo.lock
|
||||||
!/Cargo.toml
|
!/Cargo.toml
|
||||||
!/config.toml
|
!/config.toml
|
||||||
|
!/rust-toolchain.toml
|
||||||
|
|
||||||
!/Shots/
|
!/Shots/
|
||||||
!/src/
|
!/src/
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
20
Cargo.toml
|
@ -1,21 +1,19 @@
|
||||||
[package]
|
[package]
|
||||||
name = "beanconqueror"
|
authors = ["David Holland <info@dustvoice.de>"]
|
||||||
version = "0.4.0"
|
name = "rustybeans"
|
||||||
|
version = "0.5.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
calamine = "0.18.0"
|
calamine = "0.18.0"
|
||||||
chrono = "0.4.22"
|
chrono = { version = "0.4.23", features = ["wasmbind"] }
|
||||||
colourado = "0.2.0"
|
console_error_panic_hook = "0.1.7"
|
||||||
|
iced = { version = "0.4", features = ["pure"]}
|
||||||
fast-float = "0.2.0"
|
fast-float = "0.2.0"
|
||||||
palette = "0.6.1"
|
palette = "0.6.1"
|
||||||
plotters-canvas = "*"
|
plotly = { version = "0.8.1", features = ["wasm"] }
|
||||||
serde = "1.0.144"
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_derive = "1.0.144"
|
serde_json = "1.0"
|
||||||
toml = "0.5.9"
|
toml = "0.5.9"
|
||||||
wasm-bindgen-test = "0.2"
|
|
||||||
|
|
||||||
[dependencies.plotters]
|
|
||||||
git = "https://github.com/plotters-rs/plotters"
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
shot_dir = "Shots"
|
shot_dir = "shots"
|
||||||
|
brew_dir = "beanconqueror/brews"
|
||||||
output_dir = "images"
|
output_dir = "images"
|
||||||
|
main_json = "beanconqueror/Beanconqueror.json"
|
||||||
|
|
||||||
width = 1600
|
width = 1600
|
||||||
height = 900
|
height = 900
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
[toolchain]
|
||||||
|
channel = "nightly"
|
||||||
|
components = [ "rustfmt", "rust-analyzer" ]
|
|
@ -0,0 +1,48 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Shot {
|
||||||
|
pub filename: Option<String>,
|
||||||
|
pub json: Option<String>,
|
||||||
|
pub title: String,
|
||||||
|
pub cutoff: Option<f64>,
|
||||||
|
pub disable: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Chart {
|
||||||
|
pub title: String,
|
||||||
|
pub shots: Vec<u64>,
|
||||||
|
|
||||||
|
pub max_time: u64,
|
||||||
|
pub max_weight: u64,
|
||||||
|
pub max_flow: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub shots: HashMap<String, Shot>,
|
||||||
|
pub charts: HashMap<String, Chart>,
|
||||||
|
|
||||||
|
pub shot_dir: String,
|
||||||
|
pub brew_dir: String,
|
||||||
|
pub output_dir: String,
|
||||||
|
pub main_json: String,
|
||||||
|
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const WGHT_SHEET: usize = 0;
|
||||||
|
pub const FLOW_SHEET: usize = 1;
|
||||||
|
|
||||||
|
pub const TIME_COL: usize = 0;
|
||||||
|
pub const WGHT_COL: usize = 5;
|
||||||
|
pub const FLOW_COL: usize = 2;
|
||||||
|
|
||||||
|
pub struct Data {
|
||||||
|
pub weight: Vec<(f64, f64)>,
|
||||||
|
pub flowrate: Vec<(f64, f64)>,
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
/// Structs for handling the Beanconqueror database "Beanconqueror.json" export data
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct Database {
|
||||||
|
#[serde(rename = "BEANS")]
|
||||||
|
pub beans: Vec<Bean>,
|
||||||
|
|
||||||
|
#[serde(rename = "BREWS")]
|
||||||
|
pub brews: Vec<Brew>,
|
||||||
|
|
||||||
|
#[serde(rename = "MILL")]
|
||||||
|
pub mill: Vec<Mill>,
|
||||||
|
|
||||||
|
#[serde(rename = "PREPARATION")]
|
||||||
|
pub preparation: Vec<Preparation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct Bean {
|
||||||
|
pub name: String,
|
||||||
|
pub config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct Brew {
|
||||||
|
pub bean: String,
|
||||||
|
pub config: Config,
|
||||||
|
pub flow_profile: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct Mill {
|
||||||
|
pub name: String,
|
||||||
|
pub config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct Preparation {
|
||||||
|
pub name: String,
|
||||||
|
pub config: Config,
|
||||||
|
pub tools: Tools,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct Tools {
|
||||||
|
pub name: String,
|
||||||
|
pub config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct Config {
|
||||||
|
pub uuid: String,
|
||||||
|
pub unix_timestamp: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
pub fn from_file(path: &str) -> Database {
|
||||||
|
let file = fs::File::open(path)
|
||||||
|
.unwrap_or_else(|_| panic!("Cannot open file at path \"{}\"", path));
|
||||||
|
let database: Database = serde_json::from_reader(file)
|
||||||
|
.unwrap_or_else(|_| panic!("Cannot deserialize file at path \"{}\"", path));
|
||||||
|
|
||||||
|
database
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bean_with_uuid(&self, uuid: &str) -> Option<&Bean> {
|
||||||
|
self.beans.iter().find(|bean| bean.config.uuid == uuid.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn brew_with_uuid(&self, uuid: &str) -> Option<&Brew> {
|
||||||
|
self.brews.iter().find(|brew| brew.config.uuid == uuid.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bean_for_brew(&self, brew: &Brew) -> Option<&Bean> {
|
||||||
|
self.beans.iter().find(|bean| bean.config.uuid == brew.config.uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn brews_for_bean(&self, bean: &Bean) -> Vec<&Brew> {
|
||||||
|
self.brews.iter().filter(|brew| brew.bean == bean.config.uuid).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Preparation, Tools, maybe transiton to HashMaps?
|
||||||
|
}
|
|
@ -0,0 +1,163 @@
|
||||||
|
use crate::time::*;
|
||||||
|
|
||||||
|
use chrono::NaiveTime;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
/// Structs for handling the Beanconqueror "*_flow_profile.json" export data
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct FlowProfile {
|
||||||
|
pub weight: Vec<WeightData>,
|
||||||
|
#[serde(rename = "waterFlow")]
|
||||||
|
pub water_flow: Vec<FlowData>,
|
||||||
|
#[serde(rename = "realtimeFlow")]
|
||||||
|
pub realtime_flow: Vec<RtFlowData>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub cutoff: Option<f64>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub data_collection: Option<DataCollection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct WeightData {
|
||||||
|
pub timestamp: String,
|
||||||
|
pub brew_time: String,
|
||||||
|
pub actual_weight: f64,
|
||||||
|
pub old_weight: f64,
|
||||||
|
pub actual_smoothed_weight: f64,
|
||||||
|
pub old_smoothed_weight: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct FlowData {
|
||||||
|
pub brew_time: String,
|
||||||
|
pub timestamp: String,
|
||||||
|
pub value: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct RtFlowData {
|
||||||
|
pub brew_time: String,
|
||||||
|
pub timestamp: String,
|
||||||
|
pub smoothed_weight: f64,
|
||||||
|
pub flow_value: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct DataCollection {
|
||||||
|
pub reference_time: NaiveTime,
|
||||||
|
pub weight: Vec<(f64, f64)>,
|
||||||
|
pub flow: Vec<(f64, f64)>,
|
||||||
|
pub rt_flow: Vec<(f64, f64)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlowProfile {
|
||||||
|
pub fn from_file(path: &str, cutoff: Option<f64>) -> FlowProfile {
|
||||||
|
let file = fs::File::open(path)
|
||||||
|
.unwrap_or_else(|_| panic!("Cannot open file at path \"{}\"", path));
|
||||||
|
let mut brew: FlowProfile = serde_json::from_reader(file)
|
||||||
|
.unwrap_or_else(|_| panic!("Cannot deserialize file at path \"{}\"", path));
|
||||||
|
brew.cutoff = cutoff;
|
||||||
|
|
||||||
|
brew
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn preprocess_json_mut(&mut self) {
|
||||||
|
self.data_collection = self.process_json();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn preprocess_json(self) -> FlowProfile {
|
||||||
|
let data_collection = self.process_json();
|
||||||
|
FlowProfile {
|
||||||
|
weight: self.weight,
|
||||||
|
water_flow: self.water_flow,
|
||||||
|
realtime_flow: self.realtime_flow,
|
||||||
|
cutoff: None,
|
||||||
|
data_collection,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_json(&self) -> Option<DataCollection> {
|
||||||
|
let weight_starting_time: NaiveTime = str_to_naivetime(&(self.weight[0].timestamp));
|
||||||
|
let flow_starting_time: NaiveTime = str_to_naivetime(&(self.water_flow[0].timestamp));
|
||||||
|
let rt_flow_starting_time: NaiveTime = str_to_naivetime(&(self.realtime_flow[0].timestamp));
|
||||||
|
|
||||||
|
let reference_time: NaiveTime = weight_starting_time
|
||||||
|
.min(flow_starting_time)
|
||||||
|
.min(rt_flow_starting_time);
|
||||||
|
|
||||||
|
let mut weight_tuples: Vec<(f64, f64)> = self
|
||||||
|
.weight
|
||||||
|
.iter()
|
||||||
|
.map(
|
||||||
|
|WeightData {
|
||||||
|
ref timestamp,
|
||||||
|
ref actual_weight,
|
||||||
|
..
|
||||||
|
}| {
|
||||||
|
let deltatime = deltatime(str_to_naivetime(timestamp.as_str()), reference_time);
|
||||||
|
let std_duration = deltatime.to_std().unwrap();
|
||||||
|
|
||||||
|
(std_duration.as_secs_f64(), actual_weight.clone())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut flow_tuples: Vec<(f64, f64)> = self
|
||||||
|
.water_flow
|
||||||
|
.iter()
|
||||||
|
.map(
|
||||||
|
|FlowData {
|
||||||
|
ref timestamp,
|
||||||
|
ref value,
|
||||||
|
..
|
||||||
|
}| {
|
||||||
|
let deltatime = deltatime(str_to_naivetime(timestamp.as_str()), reference_time);
|
||||||
|
let std_duration = deltatime.to_std().unwrap();
|
||||||
|
|
||||||
|
(std_duration.as_secs_f64(), value.clone())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut rt_flow_tuples: Vec<(f64, f64)> = self
|
||||||
|
.realtime_flow
|
||||||
|
.iter()
|
||||||
|
.map(
|
||||||
|
|RtFlowData {
|
||||||
|
ref timestamp,
|
||||||
|
ref flow_value,
|
||||||
|
..
|
||||||
|
}| {
|
||||||
|
let deltatime = deltatime(str_to_naivetime(timestamp.as_str()), reference_time);
|
||||||
|
let std_duration = deltatime.to_std().unwrap();
|
||||||
|
|
||||||
|
(std_duration.as_secs_f64(), flow_value.clone())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if let Some(cutoff_val) = self.cutoff {
|
||||||
|
if cutoff_val != -1.0 {
|
||||||
|
weight_tuples.retain(|tuple| tuple.0 < cutoff_val);
|
||||||
|
flow_tuples.retain(|tuple| tuple.0 < cutoff_val);
|
||||||
|
rt_flow_tuples.retain(|tuple| tuple.0 < cutoff_val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !weight_tuples.is_empty() && !flow_tuples.is_empty() && !rt_flow_tuples.is_empty() {
|
||||||
|
Some(DataCollection {
|
||||||
|
reference_time,
|
||||||
|
weight: weight_tuples,
|
||||||
|
flow: flow_tuples,
|
||||||
|
rt_flow: rt_flow_tuples,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
use iced::pure::widget::{Button, Column, Container, Text};
|
||||||
|
use iced::pure::Sandbox;
|
||||||
|
// use iced::Settings;
|
||||||
|
|
||||||
|
pub struct Counter {
|
||||||
|
count: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum CounterMessage {
|
||||||
|
Increment,
|
||||||
|
Decrement,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sandbox for Counter {
|
||||||
|
type Message = CounterMessage;
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
Counter { count: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&self) -> String {
|
||||||
|
String::from("RustyBeans")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Self::Message) {
|
||||||
|
match message {
|
||||||
|
CounterMessage::Increment => self.count += 1,
|
||||||
|
CounterMessage::Decrement => self.count -= 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self) -> iced::pure::Element<'_, Self::Message> {
|
||||||
|
let label = Text::new(format!("Count: {}", self.count));
|
||||||
|
|
||||||
|
let incr = Button::new("Increment").on_press(CounterMessage::Increment);
|
||||||
|
let decr = Button::new("Decrement").on_press(CounterMessage::Decrement);
|
||||||
|
|
||||||
|
let col = Column::new().push(incr).push(label).push(decr);
|
||||||
|
|
||||||
|
Container::new(col)
|
||||||
|
.center_x()
|
||||||
|
.center_y()
|
||||||
|
.width(iced::Length::Fill)
|
||||||
|
.height(iced::Length::Fill)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
235
src/main.rs
235
src/main.rs
|
@ -1,223 +1,32 @@
|
||||||
use calamine::{open_workbook, Reader, Xlsx};
|
mod database;
|
||||||
use chrono::{Duration, NaiveTime};
|
mod config;
|
||||||
use colourado::{ColorPalette, PaletteType};
|
mod flow_profile;
|
||||||
use plotters::prelude::*;
|
mod iced;
|
||||||
use serde_derive::Deserialize;
|
mod plot;
|
||||||
|
mod sheets;
|
||||||
|
mod time;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use crate::flow_profile::FlowProfile;
|
||||||
use std::fs;
|
use crate::plot::generate_plots;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
extern crate console_error_panic_hook;
|
||||||
struct Shot {
|
use std::panic;
|
||||||
filename: String,
|
|
||||||
title: String,
|
|
||||||
cutoff: Option<f64>,
|
|
||||||
disable: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct Chart {
|
|
||||||
title: String,
|
|
||||||
shots: Vec<u64>,
|
|
||||||
|
|
||||||
max_time: u64,
|
|
||||||
max_weight: u64,
|
|
||||||
max_flow: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct Config {
|
|
||||||
shots: HashMap<String, Shot>,
|
|
||||||
charts: HashMap<String, Chart>,
|
|
||||||
|
|
||||||
shot_dir: String,
|
|
||||||
output_dir: String,
|
|
||||||
|
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
const WGHT_SHEET: usize = 0;
|
|
||||||
const FLOW_SHEET: usize = 1;
|
|
||||||
|
|
||||||
const TIME_COL: usize = 0;
|
|
||||||
const WGHT_COL: usize = 5;
|
|
||||||
const FLOW_COL: usize = 2;
|
|
||||||
|
|
||||||
struct Data {
|
|
||||||
weight: Vec<(f64, f64)>,
|
|
||||||
flowrate: Vec<(f64, f64)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn str_to_naivetime(unix_str: &str) -> NaiveTime {
|
|
||||||
NaiveTime::parse_from_str(unix_str, "%T%.3f").expect("Couldn't parse timestamp")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cell_to_naivetime(cell: Option<&str>) -> NaiveTime {
|
|
||||||
str_to_naivetime(cell.expect("Timestamp is not a string!"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deltatime(time: NaiveTime, start: NaiveTime) -> Duration {
|
|
||||||
time - start
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_sheet(
|
|
||||||
path: &str,
|
|
||||||
worksheet: usize,
|
|
||||||
time_col: usize,
|
|
||||||
data_col: usize,
|
|
||||||
) -> Vec<(f64, f64)> {
|
|
||||||
let mut workbook: Xlsx<_> =
|
|
||||||
open_workbook(path).unwrap_or_else(|_| panic!("Cannot open file at path \"{}\"", path));
|
|
||||||
|
|
||||||
if let Some(Ok(range)) = workbook.worksheet_range_at(worksheet) {
|
|
||||||
let starting_time: NaiveTime = cell_to_naivetime(range[(1, time_col)].get_string());
|
|
||||||
|
|
||||||
let time_range = range.range(
|
|
||||||
(1, time_col as u32),
|
|
||||||
(range.height() as u32 - 1, time_col as u32),
|
|
||||||
);
|
|
||||||
let weight_range = range.range(
|
|
||||||
(1, data_col as u32),
|
|
||||||
(range.height() as u32 - 1, data_col as u32),
|
|
||||||
);
|
|
||||||
|
|
||||||
// println!("time column cells: {:?}", time_range.cells().next());
|
|
||||||
// println!("time column strings: {:?}", time_range.cells().map(|c| c.2.get_string().unwrap()).collect::<Vec<&str>>());
|
|
||||||
|
|
||||||
let map_time_range = time_range.cells().map(|c| {
|
|
||||||
let timestamp = cell_to_naivetime(c.2.get_string());
|
|
||||||
let deltatime = deltatime(timestamp, starting_time);
|
|
||||||
let std_duration = deltatime.to_std().unwrap();
|
|
||||||
|
|
||||||
std_duration.as_secs_f32() as f64
|
|
||||||
});
|
|
||||||
|
|
||||||
let map_weight_range = weight_range
|
|
||||||
.cells()
|
|
||||||
.map(|c| {
|
|
||||||
c.2.get_float().unwrap_or_else(|| {
|
|
||||||
panic!(
|
|
||||||
"Can't get float value of weight column at position ({},{})",
|
|
||||||
c.0, c.1
|
|
||||||
)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
map_time_range.zip(map_weight_range).collect()
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_data(path: &str, cutoff: Option<f64>) -> Option<Data> {
|
|
||||||
let mut w = process_sheet(path, WGHT_SHEET, TIME_COL, WGHT_COL);
|
|
||||||
let mut fr = process_sheet(path, FLOW_SHEET, TIME_COL, FLOW_COL);
|
|
||||||
|
|
||||||
if let Some(cutoff_val) = cutoff {
|
|
||||||
if cutoff_val != -1.0 {
|
|
||||||
w.retain(|x| x.0 < cutoff_val);
|
|
||||||
fr.retain(|x| x.0 < cutoff_val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !w.is_empty() && !fr.is_empty() {
|
|
||||||
let data = Data {
|
|
||||||
weight: w,
|
|
||||||
flowrate: fr,
|
|
||||||
};
|
|
||||||
Some(data)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
//fn main() -> Result<(), iced::Error> {
|
||||||
fn main() {
|
fn main() {
|
||||||
let config_file = fs::read_to_string("config.toml").expect("Can't read config.toml");
|
panic::set_hook(Box::new(console_error_panic_hook::hook));
|
||||||
let config: Config = toml::from_str(&config_file).expect("Can't deserialize config.toml");
|
|
||||||
|
|
||||||
for chart in config.charts {
|
let mut brew = FlowProfile::from_file("brews/test.json", None);
|
||||||
// println!("Chart: {}\n", chart.1.title);
|
|
||||||
|
|
||||||
let title = format!("{}/{}.svg", config.output_dir, chart.1.title);
|
println!("Brew: {:?}\n", &brew);
|
||||||
let root_area = SVGBackend::new(&title, (config.width, config.height)).into_drawing_area();
|
println!("process_json(): {:?}\n", &brew.process_json());
|
||||||
root_area.fill(&WHITE).unwrap();
|
brew.preprocess_json_mut();
|
||||||
|
println!("preprocess_json_mut(): {:?}\n", &brew);
|
||||||
|
|
||||||
let mut ctx = ChartBuilder::on(&root_area)
|
brew = brew.preprocess_json();
|
||||||
.set_label_area_size(LabelAreaPosition::Left, 40)
|
println!("preprocess_json: {:?}\n", &brew);
|
||||||
.set_label_area_size(LabelAreaPosition::Bottom, 40)
|
|
||||||
.caption(&chart.1.title, ("Fira Code", 24))
|
|
||||||
.build_cartesian_2d(
|
|
||||||
0f64..(chart.1.max_time as f64),
|
|
||||||
0f64..(chart.1.max_weight as f64),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
ctx.configure_mesh()
|
generate_plots();
|
||||||
.label_style(("Fira Code", 12))
|
|
||||||
.draw()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let shot_count = chart.1.shots.len();
|
// Counter::run(Settings::default())
|
||||||
|
|
||||||
let palette = ColorPalette::new(shot_count as u32, PaletteType::Random, false);
|
|
||||||
let mut palette_iter = palette.colors.iter();
|
|
||||||
|
|
||||||
for shot_nr in chart.1.shots {
|
|
||||||
if let Some(shot) = config.shots.get(&shot_nr.to_string()) {
|
|
||||||
// println!("\tShot: {}n", shot.title);
|
|
||||||
|
|
||||||
if let Some(data) = load_data(
|
|
||||||
&format!("{}/{}", config.shot_dir, shot.filename),
|
|
||||||
shot.cutoff,
|
|
||||||
) {
|
|
||||||
if let Some(disable) = shot.disable {
|
|
||||||
if disable {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// println!("\ttweight: {:?}, flowrate: {:?}\n", data.weight, data.flowrate);
|
|
||||||
|
|
||||||
// ctx.draw_series(data.weight.iter().map(|x| {
|
|
||||||
// Pixel::new(*x, &RED)
|
|
||||||
// })).unwrap();
|
|
||||||
|
|
||||||
let f32_to_u8 = |color: f32| (color * 255.0) as u8;
|
|
||||||
|
|
||||||
let palette_color = palette_iter.next();
|
|
||||||
|
|
||||||
if let Some(color) = palette_color {
|
|
||||||
let color = RGBAColor(
|
|
||||||
f32_to_u8(color.red),
|
|
||||||
f32_to_u8(color.green),
|
|
||||||
f32_to_u8(color.blue),
|
|
||||||
1.0,
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.draw_series(LineSeries::new(data.weight, color))
|
|
||||||
.unwrap()
|
|
||||||
.label(&shot.title)
|
|
||||||
.legend(move |(x, y)| {
|
|
||||||
Rectangle::new(
|
|
||||||
[(x - 20, y + 2), (x, y - 2)],
|
|
||||||
ShapeStyle {
|
|
||||||
color,
|
|
||||||
filled: true,
|
|
||||||
stroke_width: 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.configure_series_labels()
|
|
||||||
.position(SeriesLabelPosition::UpperLeft)
|
|
||||||
.margin(25)
|
|
||||||
.legend_area_size(10)
|
|
||||||
.label_font(("Fira Code", 12))
|
|
||||||
.draw()
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::database::{Database};
|
||||||
|
use crate::flow_profile::FlowProfile;
|
||||||
|
use crate::sheets::load_data;
|
||||||
|
|
||||||
|
use plotly::{
|
||||||
|
common::{Mode, Title},
|
||||||
|
Layout, Plot, Scatter,
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
pub fn generate_plots() -> Vec<(String, Plot)> {
|
||||||
|
let config_file = fs::read_to_string("config.toml").expect("Can't read config.toml");
|
||||||
|
let config: Config = toml::from_str(&config_file).expect("Can't deserialize config.toml");
|
||||||
|
|
||||||
|
let mut result: Vec<(String, Plot)> = Vec::with_capacity(config.charts.len());
|
||||||
|
|
||||||
|
for chart in config.charts {
|
||||||
|
// println!("Chart: {}\n", chart.1.title);
|
||||||
|
|
||||||
|
let filename = format!("{}/{}.html", config.output_dir, &chart.1.title);
|
||||||
|
let mut plot = Plot::new();
|
||||||
|
|
||||||
|
let _shot_count = chart.1.shots.len();
|
||||||
|
|
||||||
|
for shot_nr in chart.1.shots {
|
||||||
|
if let Some(shot) = config.shots.get(&shot_nr.to_string()) {
|
||||||
|
// println!("\tShot: {}n", shot.title);
|
||||||
|
|
||||||
|
if let Some(shot_json) = &shot.json {
|
||||||
|
let brew = FlowProfile::from_file(
|
||||||
|
&format!("{}/{}_flow_profile.json", config.brew_dir, shot_json),
|
||||||
|
shot.cutoff,
|
||||||
|
)
|
||||||
|
.preprocess_json();
|
||||||
|
|
||||||
|
let (x, y): (Vec<_>, Vec<_>) = brew
|
||||||
|
.data_collection
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
panic!("No data_collection present for shot_json: {}", shot_json)
|
||||||
|
})
|
||||||
|
.weight
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.unzip();
|
||||||
|
let trace = Scatter::new(x, y).name(&shot.title).mode(Mode::Lines);
|
||||||
|
plot.add_trace(trace);
|
||||||
|
} else if let Some(shot_filename) = &shot.filename {
|
||||||
|
if let Some(data) = load_data(
|
||||||
|
&format!("{}/{}", config.shot_dir, shot_filename),
|
||||||
|
shot.cutoff,
|
||||||
|
) {
|
||||||
|
if let Some(disable) = shot.disable {
|
||||||
|
if disable {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (x, y): (Vec<_>, Vec<_>) = data.weight.into_iter().unzip();
|
||||||
|
let trace = Scatter::new(x, y).name(&shot.title).mode(Mode::Lines);
|
||||||
|
plot.add_trace(trace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout = Layout::new().title(Title::new(&chart.1.title));
|
||||||
|
plot.set_layout(layout);
|
||||||
|
|
||||||
|
plot.use_local_plotly();
|
||||||
|
plot.write_html(filename);
|
||||||
|
result.push((chart.1.title, plot));
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn database_plots() -> Vec<(String, Plot)> {
|
||||||
|
|
||||||
|
|
||||||
|
vec![]
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
use crate::config::{Data, FLOW_COL, FLOW_SHEET, TIME_COL, WGHT_COL, WGHT_SHEET};
|
||||||
|
use crate::time::{cell_to_naivetime, deltatime};
|
||||||
|
|
||||||
|
use calamine::{open_workbook, Reader, Xlsx};
|
||||||
|
|
||||||
|
use chrono::NaiveTime;
|
||||||
|
|
||||||
|
pub fn process_sheet(
|
||||||
|
path: &str,
|
||||||
|
worksheet: usize,
|
||||||
|
time_col: usize,
|
||||||
|
data_col: usize,
|
||||||
|
) -> Vec<(f64, f64)> {
|
||||||
|
let mut workbook: Xlsx<_> =
|
||||||
|
open_workbook(path).unwrap_or_else(|_| panic!("Cannot open file at path \"{}\"", path));
|
||||||
|
|
||||||
|
if let Some(Ok(range)) = workbook.worksheet_range_at(worksheet) {
|
||||||
|
let starting_time: NaiveTime = cell_to_naivetime(range[(1, time_col)].get_string());
|
||||||
|
|
||||||
|
let time_range = range.range(
|
||||||
|
(1, time_col as u32),
|
||||||
|
(range.height() as u32 - 1, time_col as u32),
|
||||||
|
);
|
||||||
|
let weight_range = range.range(
|
||||||
|
(1, data_col as u32),
|
||||||
|
(range.height() as u32 - 1, data_col as u32),
|
||||||
|
);
|
||||||
|
|
||||||
|
// println!("time column cells: {:?}", time_range.cells().next());
|
||||||
|
// println!("time column strings: {:?}", time_range.cells().map(|c| c.2.get_string().unwrap()).collect::<Vec<&str>>());
|
||||||
|
|
||||||
|
let map_time_range = time_range.cells().map(|c| {
|
||||||
|
let timestamp = cell_to_naivetime(c.2.get_string());
|
||||||
|
let deltatime = deltatime(timestamp, starting_time);
|
||||||
|
let std_duration = deltatime.to_std().unwrap();
|
||||||
|
|
||||||
|
std_duration.as_secs_f32() as f64
|
||||||
|
});
|
||||||
|
|
||||||
|
let map_weight_range = weight_range.cells().map(|c| {
|
||||||
|
c.2.get_float().unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"Can't get float value of weight column at position ({},{})",
|
||||||
|
c.0, c.1
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
map_time_range.zip(map_weight_range).collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_data(path: &str, cutoff: Option<f64>) -> Option<Data> {
|
||||||
|
let mut w = process_sheet(path, WGHT_SHEET, TIME_COL, WGHT_COL);
|
||||||
|
let mut fr = process_sheet(path, FLOW_SHEET, TIME_COL, FLOW_COL);
|
||||||
|
|
||||||
|
if let Some(cutoff_val) = cutoff {
|
||||||
|
if cutoff_val != -1.0 {
|
||||||
|
w.retain(|x| x.0 < cutoff_val);
|
||||||
|
fr.retain(|x| x.0 < cutoff_val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !w.is_empty() && !fr.is_empty() {
|
||||||
|
let data = Data {
|
||||||
|
weight: w,
|
||||||
|
flowrate: fr,
|
||||||
|
};
|
||||||
|
Some(data)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
:root {
|
||||||
|
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color: #0f0f0f;
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin: 0;
|
||||||
|
/* padding-top: 10vh; */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
color: #0f0f0f;
|
||||||
|
background-color: #ffffff;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
border-color: #396cd8;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
button {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
color: #f6f6f6;
|
||||||
|
background-color: #2f2f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #24c8db;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
button {
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #0f0f0f98;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
use chrono::{Duration, NaiveDateTime, NaiveTime};
|
||||||
|
|
||||||
|
pub fn str_to_naivetime(unix_str: &str) -> NaiveTime {
|
||||||
|
NaiveTime::parse_from_str(unix_str, "%T%.3f").expect("Couldn't parse timestamp")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cell_to_naivetime(cell: Option<&str>) -> NaiveTime {
|
||||||
|
str_to_naivetime(cell.expect("Timestamp is not a string!"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deltatime(time: NaiveTime, start: NaiveTime) -> Duration {
|
||||||
|
time - start
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unix_to_naivetime(unix_timestamp: i64) -> Option<NaiveTime> {
|
||||||
|
if let Some(date_time) = NaiveDateTime::from_timestamp_millis(unix_timestamp) {
|
||||||
|
Some(date_time.time())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_same_day(time_1: NaiveTime, time_2: NaiveTime) -> bool {
|
||||||
|
time_1.format("%Y:%m:%d").to_string() == time_2.format("%Y:%m:%d").to_string()
|
||||||
|
}
|
Loading…
Reference in New Issue