添加界面优化
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1738,6 +1738,7 @@ dependencies = [
|
|||||||
name = "eskin-data-analysis"
|
name = "eskin-data-analysis"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"dioxus",
|
"dioxus",
|
||||||
"futures",
|
"futures",
|
||||||
"polars",
|
"polars",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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]
|
||||||
|
anyhow = "1.0.102"
|
||||||
dioxus = { version = "0.7.1", features = ["router"] }
|
dioxus = { version = "0.7.1", features = ["router"] }
|
||||||
futures = "0.3.32"
|
futures = "0.3.32"
|
||||||
polars = { version="0.53.0", features = ["lazy", "csv"] }
|
polars = { version="0.53.0", features = ["lazy", "csv"] }
|
||||||
@@ -17,3 +18,4 @@ default = ["desktop"]
|
|||||||
web = ["dioxus/web"]
|
web = ["dioxus/web"]
|
||||||
desktop = ["dioxus/desktop"]
|
desktop = ["dioxus/desktop"]
|
||||||
mobile = ["dioxus/mobile"]
|
mobile = ["dioxus/mobile"]
|
||||||
|
|
||||||
|
|||||||
@@ -19,3 +19,7 @@ script = []
|
|||||||
# Javascript code file
|
# Javascript code file
|
||||||
# serve: [dev-server] only
|
# serve: [dev-server] only
|
||||||
script = []
|
script = []
|
||||||
|
|
||||||
|
[bundle]
|
||||||
|
identifier = "com.qunin"
|
||||||
|
publisher = "JOYSONQUNIN"
|
||||||
@@ -182,6 +182,9 @@
|
|||||||
max-width: 96rem;
|
max-width: 96rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.mr-3 {
|
||||||
|
margin-right: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
.mb-4 {
|
.mb-4 {
|
||||||
margin-bottom: calc(var(--spacing) * 4);
|
margin-bottom: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,92 @@
|
|||||||
use dioxus::{Ok, html::{div, script::r#async, text}, prelude::*};
|
use std::{fs::File, future::Future};
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use crate::guide_router::Route;
|
|
||||||
use rfd::AsyncFileDialog;
|
|
||||||
use polars::prelude::*;
|
use polars::prelude::*;
|
||||||
const RATE_LABELS: [i32; 11] = [60, 160, 260, 360, 460, 560, 660, 860, 1060, 1560, 2060];
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct FilterRateThresholdState {
|
pub struct FilterRateThresholdState {
|
||||||
pub thresholds: Signal<Vec<f32>>,
|
pub items: Signal<Vec<ThresholdItem>>,
|
||||||
|
pub next_id: Signal<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct ThresholdItem {
|
||||||
|
pub id: usize,
|
||||||
|
pub label: String,
|
||||||
|
pub value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn FilterRateThreshold() -> Element {
|
pub fn FilterRateThreshold() -> Element {
|
||||||
let mut state = use_context::<FilterRateThresholdState>();
|
let mut state = use_context::<FilterRateThresholdState>();
|
||||||
let mut navigator = use_navigator();
|
let navigator = use_navigator();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "section",
|
div { class: "section",
|
||||||
div { class: "container",
|
div { class: "container",
|
||||||
div { class: "card",
|
div { class: "card",
|
||||||
div { class: "card-header",
|
div { class: "card-header",
|
||||||
p { class: "card-header-title", "过滤阈值配置" }
|
p { class: "card-header-title", "阈值配置" }
|
||||||
}
|
}
|
||||||
|
|
||||||
div { class: "card-content",
|
div { class: "card-content",
|
||||||
p { class: "subtitle is-6 has-text-grey",
|
p { class: "subtitle is-6 has-text-grey",
|
||||||
"支持手动输入、CSV 导入,后续可扩展其他输入方式。"
|
"支持手动输入和 CSV 导入。手动输入时可以随时新增、删除和修改条目。"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 手动输入
|
|
||||||
div { class: "box",
|
div { class: "box",
|
||||||
h2 { class: "title is-6 mb-4", "手动输入" }
|
h2 { class: "title is-6 mb-4", "手动输入" }
|
||||||
|
|
||||||
div { class: "columns is-multiline",
|
div { class: "columns is-multiline",
|
||||||
for (index , label) in RATE_LABELS.iter().enumerate() {
|
for item in state.items.read().iter() {
|
||||||
div { class: "column is-half",
|
div { class: "column is-half",
|
||||||
ThresholdField { label: *label, index }
|
ThresholdField {
|
||||||
|
id: item.id,
|
||||||
|
label: item.label.clone(),
|
||||||
|
value: item.value.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div { class: "column is-full",
|
||||||
|
button {
|
||||||
|
class: "button is-primary mr-3",
|
||||||
|
onclick: move |_| {
|
||||||
|
let id = (state.next_id)();
|
||||||
|
state
|
||||||
|
.items
|
||||||
|
.write()
|
||||||
|
.push(ThresholdItem {
|
||||||
|
id,
|
||||||
|
label: String::new(),
|
||||||
|
value: String::new(),
|
||||||
|
});
|
||||||
|
state.next_id.with_mut(|next_id| *next_id += 1);
|
||||||
|
},
|
||||||
|
"添加"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
class: "button is-light",
|
||||||
|
onclick: move |_| {
|
||||||
|
// let id = (state.next_id)();
|
||||||
|
// state
|
||||||
|
// .items
|
||||||
|
// .write()
|
||||||
|
// .push(ThresholdItem {
|
||||||
|
// id,
|
||||||
|
// label: String::new(),
|
||||||
|
// value: String::new(),
|
||||||
|
// });
|
||||||
|
// state.next_id.with_mut(|next_id| *next_id += 1);
|
||||||
|
state.items.write().clear();
|
||||||
|
state.next_id.with_mut(|n| *n = 0);
|
||||||
|
},
|
||||||
|
"重置"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 数据导入
|
|
||||||
div { class: "box",
|
div { class: "box",
|
||||||
h2 { class: "title is-6 mb-4", "数据导入" }
|
h2 { class: "title is-6 mb-4", "数据导入" }
|
||||||
|
|
||||||
@@ -47,7 +94,7 @@ pub fn FilterRateThreshold() -> Element {
|
|||||||
button {
|
button {
|
||||||
class: "button is-primary",
|
class: "button is-primary",
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
load_csv_data(&mut state.thresholds);
|
load_csv_data(state.items);
|
||||||
},
|
},
|
||||||
span { class: "icon",
|
span { class: "icon",
|
||||||
i { class: "fas fa-file-import" }
|
i { class: "fas fa-file-import" }
|
||||||
@@ -55,39 +102,13 @@ pub fn FilterRateThreshold() -> Element {
|
|||||||
span { "导入 CSV" }
|
span { "导入 CSV" }
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
class: "button is-link is-light",
|
|
||||||
onclick: move |_| {
|
|
||||||
paste_data(state.thresholds);
|
|
||||||
},
|
|
||||||
"批量粘贴"
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
class: "button is-light",
|
class: "button is-light",
|
||||||
onclick: move |_| {
|
onclick: move |_| navigator.go_back(),
|
||||||
navigator.go_back();
|
|
||||||
}, // navigator.go_back();, // back();,
|
|
||||||
"返回"
|
"返回"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当前预览
|
|
||||||
div { class: "box",
|
|
||||||
h2 { class: "title is-6 mb-4", "当前预览" }
|
|
||||||
|
|
||||||
div { class: "content",
|
|
||||||
ul {
|
|
||||||
for (index , label) in RATE_LABELS.iter().enumerate() {
|
|
||||||
li {
|
|
||||||
strong { "{label}: " }
|
|
||||||
"{state.thresholds.read()[index]}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,102 +117,221 @@ pub fn FilterRateThreshold() -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ThresholdField(label: i32, index: usize) -> Element {
|
fn ThresholdField(id: usize, label: String, value: String) -> Element {
|
||||||
let mut state = use_context::<FilterRateThresholdState>();
|
let mut state = use_context::<FilterRateThresholdState>();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "field",
|
div { class: "box",
|
||||||
label { class: "label", "{label}" }
|
div { class: "columns is-vcentered",
|
||||||
div { class: "control",
|
div { class: "column",
|
||||||
|
input {
|
||||||
|
class: "input",
|
||||||
|
r#type: "text",
|
||||||
|
placeholder: "边界值",
|
||||||
|
value: label,
|
||||||
|
oninput: move |evt| {
|
||||||
|
let new_label = evt.value();
|
||||||
|
state
|
||||||
|
.items
|
||||||
|
.with_mut(|items| {
|
||||||
|
if let Some(item) = items.iter_mut().find(|item| item.id == id) {
|
||||||
|
item.label = new_label;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div { class: "column",
|
||||||
input {
|
input {
|
||||||
class: "input is-info",
|
class: "input is-info",
|
||||||
r#type: "text",
|
r#type: "text",
|
||||||
placeholder: "输入阈值",
|
placeholder: "阈值",
|
||||||
value: "{state.thresholds.read()[index]}",
|
value,
|
||||||
oninput: move |evt| {
|
oninput: move |evt| {
|
||||||
// state.thresholds.write()[index] = evt as i32;
|
let new_value = evt.value();
|
||||||
if let Ok(v) = evt.value().parse::<f32>() {
|
state
|
||||||
state.thresholds.write()[index] = v;
|
.items
|
||||||
|
.with_mut(|items| {
|
||||||
|
if let Some(item) = items.iter_mut().find(|item| item.id == id) {
|
||||||
|
item.value = new_value;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div { class: "column is-narrow",
|
||||||
|
button {
|
||||||
|
class: "delete is-medium",
|
||||||
|
onclick: move |_| {
|
||||||
|
state.items.with_mut(|items| items.retain(|item| item.id != id));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct SmoothRateConfig {
|
struct SmoothRateConfig {
|
||||||
thresholds: Vec<f32>,
|
ths: Vec<f32>,
|
||||||
rates: Vec<f32>,
|
rates: Vec<f32>,
|
||||||
file_path: Vec<String>,
|
file_path: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SmoothRateConfig {
|
impl Default for SmoothRateConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
SmoothRateConfig { thresholds: Vec::new(), rates: Vec::new(), file_path: Vec::new() }
|
SmoothRateConfig { ths: Vec::new(), rates: Vec::new(), file_path: Vec::new() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SmoothRateConfig {
|
impl SmoothRateConfig {
|
||||||
fn new(ths: Vec<f32>, files: Vec<String>) -> Self {
|
pub fn new(items: Vec<ThresholdItem>, files: Vec<String>) -> Self {
|
||||||
if ths.len() != 11 {
|
let mut points = items
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|item| {
|
||||||
|
let label = item.label.trim().parse::<usize>().ok()?;
|
||||||
|
let value = item.value.trim().parse::<f32>().ok()?;
|
||||||
|
Some((label, value))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
points.sort_by_key(|(label, _)| *label);
|
||||||
|
|
||||||
|
if points.len() < 2 {
|
||||||
return SmoothRateConfig::default();
|
return SmoothRateConfig::default();
|
||||||
}
|
}
|
||||||
let rates = ths.chunks(2)
|
|
||||||
.map(|ck| ck[1] - ck[0]).collect::<Vec<f32>>();
|
|
||||||
|
|
||||||
SmoothRateConfig { thresholds: ths, rates, file_path: files }
|
// let labels = points.iter().map(|(label, _)| *label).collect::<Vec<_>>();
|
||||||
|
let ths: Vec<f32> = points.iter().map(|(_, v)| *v).collect::<Vec<f32>>();
|
||||||
|
let rates = points
|
||||||
|
.windows(2)
|
||||||
|
.filter_map(|window| {
|
||||||
|
let (start_label, start_value) = window[0];
|
||||||
|
let (end_label, end_value) = window[1];
|
||||||
|
let distance = end_label.checked_sub(start_label)?;
|
||||||
|
if distance == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((end_value - start_value) / distance as f32)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if rates.len() + 1 != ths.len() {
|
||||||
|
return SmoothRateConfig::default();
|
||||||
|
}
|
||||||
|
println!("ths: {:?}", ths);
|
||||||
|
println!("rates: {:?}", rates);
|
||||||
|
|
||||||
|
SmoothRateConfig { ths, rates, file_path: files }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn analysis_file_once(file: &str) -> PolarsResult<()> {
|
pub fn analysis_all_files(&self) -> anyhow::Result<()> {
|
||||||
|
if self.file_path.is_empty() {
|
||||||
|
return anyhow::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut failed = vec![];
|
||||||
|
for file in &self.file_path {
|
||||||
|
match self.analysis_file_once(file) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(error) => failed.push((file.clone(), error.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !failed.is_empty() {
|
||||||
|
for (file, error) in failed {
|
||||||
|
println!("{file} -> {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn analysis_file_once(&self, file: &str) -> anyhow::Result<()> {
|
||||||
let df = LazyCsvReader::new(file.into())
|
let df = LazyCsvReader::new(file.into())
|
||||||
.with_has_header(true)
|
.with_has_header(true)
|
||||||
.finish()?
|
.finish()?
|
||||||
|
.with_columns(
|
||||||
|
(1..=84)
|
||||||
|
.map(|i| {
|
||||||
|
let name = format!("channel{i}");
|
||||||
|
SmoothRateConfig::analysis_col_once(&name, &self.ths, &self.rates)
|
||||||
|
.cast(DataType::Float32)
|
||||||
|
.alias(&name)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
.collect()?;
|
.collect()?;
|
||||||
|
|
||||||
let df = df.with_column(
|
let output = format!("{file}_processed.csv");
|
||||||
(1..=84).map(|i| {
|
let mut output_file = File::create(output)?;
|
||||||
let name = format!("channel{}", i);
|
CsvWriter::new(&mut output_file)
|
||||||
|
.include_header(true)
|
||||||
})
|
.finish(&mut df.clone())?;
|
||||||
)
|
|
||||||
|
|
||||||
|
debug!("analysis finished");
|
||||||
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn analysis_col_once(name: &str, thresholds: &[f32], rates: &[f32]) -> Expr {
|
fn analysis_col_once(name: &str, thresholds: &[f32], rates: &[f32]) -> Expr {
|
||||||
|
let mut expr: Expr =
|
||||||
|
col(name).cast(DataType::Float64) * lit(rates[rates.len() - 1] as f64);
|
||||||
|
|
||||||
|
for i in (0..rates.len()).rev() {
|
||||||
|
let t0 = thresholds[i] as f64;
|
||||||
|
let t1 = thresholds[i + 1] as f64;
|
||||||
|
let rate = rates[i] as f64;
|
||||||
|
|
||||||
|
expr = when(
|
||||||
|
col(name)
|
||||||
|
.cast(DataType::Float64)
|
||||||
|
.gt_eq(lit(t0))
|
||||||
|
.and(col(name).cast(DataType::Float64).lt(lit(t1))),
|
||||||
|
)
|
||||||
|
.then(col(name).cast(DataType::Float64) * lit(rate))
|
||||||
|
.otherwise(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
when(col(name).cast(DataType::Float64).lt(lit(thresholds[0] as f64)))
|
||||||
|
.then(lit(0.0))
|
||||||
|
.otherwise(expr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_csv_data(items: Signal<Vec<ThresholdItem>>) {
|
||||||
|
let current_items = items.read().clone();
|
||||||
|
|
||||||
|
|
||||||
fn load_csv_data(thresholds: &mut Signal<Vec<f32>>) {
|
|
||||||
let task = rfd::AsyncFileDialog::new()
|
let task = rfd::AsyncFileDialog::new()
|
||||||
.add_filter("CSV", &["csv"])
|
.add_filter("CSV", &["csv"])
|
||||||
.pick_files();
|
.pick_files();
|
||||||
execute(async {
|
|
||||||
let file_handles = task.await;
|
|
||||||
if let Some(files) = file_handles {
|
|
||||||
for file in files {
|
|
||||||
let path = file.path();
|
|
||||||
|
|
||||||
|
execute(async move {
|
||||||
|
let file_handles = task.await;
|
||||||
|
|
||||||
|
if let Some(files) = file_handles {
|
||||||
|
let file_path_vec = files
|
||||||
|
.iter()
|
||||||
|
.map(|file| file.path().to_string_lossy().to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let config = SmoothRateConfig::new(current_items, file_path_vec);
|
||||||
|
|
||||||
|
if let Err(error) = config.analysis_all_files() {
|
||||||
|
eprintln!("批量处理失败: {error}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paste_data(thresholds: Signal<Vec<f32>>) {
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
todo!()
|
fn execute<F: Future<Output = ()> + Send + 'static>(future: F) {
|
||||||
|
std::thread::spawn(move || futures::executor::block_on(future));
|
||||||
}
|
}
|
||||||
|
|
||||||
use std::{fs::FileTimes, future::Future};
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
fn execute<F: Future<Output = ()> + Send + 'static>(f: F) {
|
|
||||||
// this is stupid... use any executor of your choice instead
|
|
||||||
std::thread::spawn(move || futures::executor::block_on(f));
|
|
||||||
}
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
fn execute<F: Future<Output = ()> + 'static>(f: F) {
|
fn execute<F: Future<Output = ()> + 'static>(future: F) {
|
||||||
wasm_bindgen_futures::spawn_local(f);
|
wasm_bindgen_futures::spawn_local(future);
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod guide_router;
|
pub mod guide_router;
|
||||||
use dioxus::{html::{div, text}, prelude::*};
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
||||||
const MAIN_CSS: Asset = asset!("/assets/bulma.css");
|
const MAIN_CSS: Asset = asset!("/assets/bulma.css");
|
||||||
const HEADER_SVG: Asset = asset!("/assets/header.svg");
|
const HEADER_SVG: Asset = asset!("/assets/header.svg");
|
||||||
|
|
||||||
|
|
||||||
use crate::components::filters_rate_threshold::{FilterRateThreshold, FilterRateThresholdState};
|
use crate::components::filters_rate_threshold::FilterRateThresholdState;
|
||||||
use crate::guide_router::Route;
|
use crate::guide_router::Route;
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus::launch(App);
|
dioxus::launch(App);
|
||||||
@@ -15,8 +14,9 @@ fn main() {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn App() -> Element {
|
fn App() -> Element {
|
||||||
let thresholds = use_signal(|| vec![0.0; 11]);
|
let items = use_signal(Vec::new);
|
||||||
use_context_provider(|| FilterRateThresholdState { thresholds });
|
let next_id = use_signal(|| 0usize);
|
||||||
|
use_context_provider(|| FilterRateThresholdState { items, next_id });
|
||||||
rsx! {
|
rsx! {
|
||||||
document::Link { rel: "icon", href: FAVICON }
|
document::Link { rel: "icon", href: FAVICON }
|
||||||
document::Link { rel: "stylesheet", href: MAIN_CSS }
|
document::Link { rel: "stylesheet", href: MAIN_CSS }
|
||||||
|
|||||||
Reference in New Issue
Block a user