* [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code
@ 2025-05-30 12:21 Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 01/20] remove old rrd uplot code Dominik Csapak
` (19 more replies)
0 siblings, 20 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:21 UTC (permalink / raw)
To: yew-devel
This series aims to make the code of the rrd graphs more readable and
maintanable, by:
* moving code into it's own directory/file structure
* move code/structs together that belong together thematically
* factour out calculation code from graph component construction
* documenting relevant functions
* removing unnecessary computations
While the code now sometimes iterates over the data multiple times to
1. create the baseline data
2. create the actual components
the first part is now only done when necessary (when data/time changes,
when the user selects a new range) and not every time the graph is
redrawn, e.g. when the user moves the curser over the data.
So all in all this should do less work than before.
Also this fixes some panics when dealing with edge cases (e.g. clicking
on empty graphs, having a range selected then changing the underlying
data so that the range is not there anymore, ect.)
While at it, remove the old rrd graph code that used uplot. This is not
in use anywhere anymore but can be easily restored if necessary.
Future work could be to include more than two series. This can be done
later though, since we don't require that at the moment.
Dominik Csapak (20):
remove old rrd uplot code
rrd: refactor code for compute_min_max
rrd: move into own module
rrd: move unit calculation to own module
rrd: units: add tests
rrd: units: simplify calculations for get_grid_unit_base
rrd: remove unnecessary `no_data` field
rrd: align tooltip directly to pointer position
rrd: use 'cross_pos' state instead of 'draw_cross'
rrd: give all elements in svg keys
rrd: simplify toggle Msg
rrd: remove wrongly annotated lifetime
rrd: refactor series related struct and functions into own module
rrd: clamp view range when time_data changes
rrd: refactor grid data computation
rrd: introduce GraphSpace struct and use it to precalculate graph data
rrd: precalculate the grid line and label positions
rrd: calculate series svg data only when necessary
rrd: refactor selection rectangle calculation
rrd: refactor the cross position calculation
js-helper-module.js | 18 -
src/lib.rs | 19 +-
src/rrd/graph.rs | 726 ++++++++++++++++++++++++++++
src/rrd/graph_space.rs | 147 ++++++
src/rrd/grid.rs | 147 ++++++
src/rrd/mod.rs | 11 +
src/rrd/series.rs | 96 ++++
src/rrd/units.rs | 326 +++++++++++++
src/rrd_graph.rs | 169 -------
src/rrd_graph_new.rs | 1038 ----------------------------------------
10 files changed, 1455 insertions(+), 1242 deletions(-)
create mode 100644 src/rrd/graph.rs
create mode 100644 src/rrd/graph_space.rs
create mode 100644 src/rrd/grid.rs
create mode 100644 src/rrd/mod.rs
create mode 100644 src/rrd/series.rs
create mode 100644 src/rrd/units.rs
delete mode 100644 src/rrd_graph.rs
delete mode 100644 src/rrd_graph_new.rs
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 01/20] remove old rrd uplot code
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
@ 2025-05-30 12:21 ` Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 02/20] rrd: refactor code for compute_min_max Dominik Csapak
` (18 subsequent siblings)
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:21 UTC (permalink / raw)
To: yew-devel
it's not in use (not even included) so let's just remove it
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
js-helper-module.js | 18 -----
src/lib.rs | 15 ----
src/rrd_graph.rs | 169 --------------------------------------------
3 files changed, 202 deletions(-)
delete mode 100644 src/rrd_graph.rs
diff --git a/js-helper-module.js b/js-helper-module.js
index 45ccbb7..9f0bd96 100644
--- a/js-helper-module.js
+++ b/js-helper-module.js
@@ -17,27 +17,9 @@ function get_cookie() {
return document.cookie;
}
-function uplot(opts, data, node) {
- return new uPlot(opts, data, node);
-}
-
-function uplot_set_data(uplot, data) {
- uplot.setData(data);
-}
-
-function uplot_set_size(uplot, width, height) {
- uplot.setSize({
- width: width,
- height: height,
- });
-}
-
export {
async_sleep,
get_cookie,
set_cookie,
clear_auth_cookie,
- uplot,
- uplot_set_data,
- uplot_set_size,
};
diff --git a/src/lib.rs b/src/lib.rs
index 091cb72..ff3044a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -217,11 +217,6 @@ extern "C" {
pub fn get_cookie() -> String;
pub fn set_cookie(value: &str);
pub fn clear_auth_cookie(name: &str);
-
- // uPlot binding
- pub fn uplot(opts: &JsValue, data: &JsValue, node: web_sys::Node) -> JsValue;
- pub fn uplot_set_data(uplot: &JsValue, data: &JsValue);
- pub fn uplot_set_size(uplot: &JsValue, width: usize, height: usize);
}
// Create wrapper which panics if called from target_arch!=wasm32
@@ -230,7 +225,6 @@ extern "C" {
pub use panic_wrapper::*;
#[cfg(not(target_arch = "wasm32"))]
mod panic_wrapper {
- use wasm_bindgen::JsValue;
pub fn async_sleep(_ms: i32) -> js_sys::Promise {
unreachable!()
}
@@ -243,15 +237,6 @@ mod panic_wrapper {
pub fn clear_auth_cookie(_name: &str) {
unreachable!()
}
- pub fn uplot(_opts: &JsValue, _data: &JsValue, _node: web_sys::Node) -> JsValue {
- unreachable!()
- }
- pub fn uplot_set_data(_uplot: &JsValue, _data: &JsValue) {
- unreachable!()
- }
- pub fn uplot_set_size(_uplot: &JsValue, _width: usize, _height: usize) {
- unreachable!()
- }
}
pub fn store_csrf_token(crsf_token: &str) {
diff --git a/src/rrd_graph.rs b/src/rrd_graph.rs
deleted file mode 100644
index 9ced377..0000000
--- a/src/rrd_graph.rs
+++ /dev/null
@@ -1,169 +0,0 @@
-use std::rc::Rc;
-
-use serde_json::json;
-use wasm_bindgen::JsValue;
-
-use yew::html::IntoPropValue;
-use yew::prelude::*;
-use yew::virtual_dom::{VComp, VNode};
-
-use pwt::dom::DomSizeObserver;
-use pwt::prelude::*;
-use pwt::widget::Panel;
-
-#[derive(Clone, PartialEq, Properties)]
-pub struct RRDGraph {
- #[prop_or_default]
- pub title: Option<AttrValue>,
- // Legend Label
- #[prop_or_default]
- pub label: Option<String>,
- #[prop_or_default]
- pub class: Classes,
-
- pub data: Rc<(Vec<i64>, Vec<f64>)>,
-}
-
-impl RRDGraph {
- pub fn new(data: Rc<(Vec<i64>, Vec<f64>)>) -> Self {
- yew::props!(RRDGraph { data })
- }
-
- pub fn title(mut self, title: impl IntoPropValue<Option<AttrValue>>) -> Self {
- self.set_title(title);
- self
- }
-
- pub fn set_title(&mut self, title: impl IntoPropValue<Option<AttrValue>>) {
- self.title = title.into_prop_value();
- }
-
- pub fn label(mut self, label: impl Into<String>) -> Self {
- self.label = Some(label.into());
- self
- }
-
- /// Builder style method to add a html class
- pub fn class(mut self, class: impl Into<Classes>) -> Self {
- self.add_class(class);
- self
- }
-
- /// Method to add a html class
- pub fn add_class(&mut self, class: impl Into<Classes>) {
- self.class.push(class);
- }
-}
-
-pub enum Msg {
- Reload,
- ViewportResize(f64, f64),
-}
-
-pub struct PwtRRDGraph {
- node_ref: NodeRef,
- size_observer: Option<DomSizeObserver>,
- width: usize,
- uplot: Option<JsValue>,
-}
-
-const DEFAULT_RRD_WIDTH: usize = 800;
-const DEFAULT_RRD_HEIGHT: usize = 250;
-
-impl Component for PwtRRDGraph {
- type Message = Msg;
- type Properties = RRDGraph;
-
- fn create(ctx: &Context<Self>) -> Self {
- ctx.link().send_message(Msg::Reload);
- Self {
- node_ref: NodeRef::default(),
- size_observer: None,
- width: DEFAULT_RRD_WIDTH,
- uplot: None,
- }
- }
-
- fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
- match msg {
- Msg::Reload => true,
- Msg::ViewportResize(width, _height) => {
- self.width = width as usize;
- true
- }
- }
- }
-
- fn view(&self, ctx: &Context<Self>) -> Html {
- let props = ctx.props();
-
- Panel::new()
- .title(props.title.clone())
- .class(props.class.clone())
- .with_child(
- Container::new()
- .padding(2)
- .with_child(html! {<div ref={self.node_ref.clone()}>}),
- )
- .into()
- }
-
- fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
- if let Some(uplot) = &self.uplot {
- let data = pwt::to_js_value(ctx.props().data.as_ref()).unwrap();
- crate::uplot_set_size(uplot, self.width, DEFAULT_RRD_HEIGHT);
- crate::uplot_set_data(uplot, &data);
- }
- true
- }
-
- fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
- if first_render {
- if let Some(el) = self.node_ref.cast::<web_sys::Element>() {
- let link = ctx.link().clone();
- let size_observer = DomSizeObserver::new(&el, move |(width, height)| {
- link.send_message(Msg::ViewportResize(width, height));
- });
-
- self.size_observer = Some(size_observer);
- }
-
- let props = ctx.props();
-
- let mut serie1 = json!({
- // initial toggled state (optional)
- "show": true,
-
- "spanGaps": false,
-
- // series style
- "stroke": "#94ae0a",
- "fill": "#94ae0a80",
- "width": 1,
- });
-
- if let Some(ref label) = props.label {
- serie1["label"] = label.as_str().into();
- }
-
- let opts = json!({
- "width": self.width,
- "height": DEFAULT_RRD_HEIGHT,
- "series": [ {}, serie1 ],
- });
-
- let opts = pwt::to_js_value(&opts).unwrap();
-
- let data = pwt::to_js_value(props.data.as_ref()).unwrap();
-
- self.uplot = Some(crate::uplot(&opts, &data, self.node_ref.get().unwrap()));
- }
- }
-}
-
-impl Into<VNode> for RRDGraph {
- fn into(self) -> VNode {
- let comp = VComp::new::<PwtRRDGraph>(Rc::new(self), None);
- VNode::from(comp)
- }
-}
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 02/20] rrd: refactor code for compute_min_max
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 01/20] remove old rrd uplot code Dominik Csapak
@ 2025-05-30 12:21 ` Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 03/20] rrd: move into own module Dominik Csapak
` (17 subsequent siblings)
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:21 UTC (permalink / raw)
To: yew-devel
using some simple combinators makes the code a bit shorter and more
readable. Using +/- INFINITY as marker for having no values, reduces the
code further since we don't have to handle the options in the loop.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/rrd_graph_new.rs | 35 ++++++++++-------------------------
1 file changed, 10 insertions(+), 25 deletions(-)
diff --git a/src/rrd_graph_new.rs b/src/rrd_graph_new.rs
index 2c77a38..bfa7d1e 100644
--- a/src/rrd_graph_new.rs
+++ b/src/rrd_graph_new.rs
@@ -311,34 +311,19 @@ fn render_value(props: &RRDGraph, v: f64) -> String {
}
fn compute_min_max(props: &RRDGraph, data1: &[f64], data2: &[f64]) -> (f64, f64, f64) {
- let mut min_data: Option<f64> = None;
- let mut max_data: Option<f64> = None;
+ let mut min_data: f64 = f64::INFINITY;
+ let mut max_data: f64 = -f64::INFINITY;
- for data in [data1, data2] {
- for v in data.iter() {
- if !v.is_finite() {
- continue; // NaN, INFINITY
- }
- if let Some(min) = min_data {
- if *v < min {
- min_data = Some(*v);
- }
- } else {
- min_data = Some(*v);
- }
-
- if let Some(max) = max_data {
- if *v > max {
- max_data = Some(*v);
- }
- } else {
- max_data = Some(*v);
- }
- }
+ for v in data1.iter().chain(data2).filter(|v| v.is_finite()) {
+ min_data = min_data.min(*v);
+ max_data = max_data.max(*v);
}
- let mut max_data = max_data.unwrap_or(1.0);
- let mut min_data = min_data.unwrap_or(0.0);
+ // if one is infinite, the other must be too
+ if min_data.is_infinite() || max_data.is_infinite() {
+ min_data = 0.0;
+ max_data = 1.0;
+ }
if props.include_zero {
max_data = max_data.max(0.0);
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 03/20] rrd: move into own module
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 01/20] remove old rrd uplot code Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 02/20] rrd: refactor code for compute_min_max Dominik Csapak
@ 2025-05-30 12:21 ` Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 04/20] rrd: move unit calculation to " Dominik Csapak
` (16 subsequent siblings)
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:21 UTC (permalink / raw)
To: yew-devel
we'll further divide this to make it more maintainable
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/lib.rs | 4 ++--
src/{rrd_graph_new.rs => rrd/graph.rs} | 0
src/rrd/mod.rs | 2 ++
3 files changed, 4 insertions(+), 2 deletions(-)
rename src/{rrd_graph_new.rs => rrd/graph.rs} (100%)
create mode 100644 src/rrd/mod.rs
diff --git a/src/lib.rs b/src/lib.rs
index ff3044a..ba3dac0 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -95,9 +95,9 @@ mod role_selector;
pub use role_selector::RoleSelector;
#[cfg(feature = "rrd")]
-mod rrd_graph_new;
+mod rrd;
#[cfg(feature = "rrd")]
-pub use rrd_graph_new::{RRDGraph, Series};
+pub use rrd::{RRDGraph, Series};
#[cfg(feature = "rrd")]
mod rrd_grid;
diff --git a/src/rrd_graph_new.rs b/src/rrd/graph.rs
similarity index 100%
rename from src/rrd_graph_new.rs
rename to src/rrd/graph.rs
diff --git a/src/rrd/mod.rs b/src/rrd/mod.rs
new file mode 100644
index 0000000..87bc07f
--- /dev/null
+++ b/src/rrd/mod.rs
@@ -0,0 +1,2 @@
+mod graph;
+pub use graph::*;
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 04/20] rrd: move unit calculation to own module
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
` (2 preceding siblings ...)
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 03/20] rrd: move into own module Dominik Csapak
@ 2025-05-30 12:21 ` Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 05/20] rrd: units: add tests Dominik Csapak
` (15 subsequent siblings)
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:21 UTC (permalink / raw)
To: yew-devel
makes it easier to test and makes the main rrd graph part smaller
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/rrd/graph.rs | 96 +-----------------------------------------------
src/rrd/mod.rs | 2 +
src/rrd/units.rs | 95 +++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 98 insertions(+), 95 deletions(-)
create mode 100644 src/rrd/units.rs
diff --git a/src/rrd/graph.rs b/src/rrd/graph.rs
index bfa7d1e..8b31103 100644
--- a/src/rrd/graph.rs
+++ b/src/rrd/graph.rs
@@ -170,101 +170,7 @@ impl Default for LayoutProps {
use pwt::widget::canvas::{Canvas, Circle, Group, Path, Rect, SvgLength, Text};
-fn get_grid_unit_base10(min: f64, max: f64) -> f64 {
- let range = max - min;
-
- if range == 0.0 {
- panic!("get_grid_unit_base10: got zero range - internal error");
- }
-
- let mut l = range.log10() as i32;
-
- while (range / 10.0_f64.powi(l)) < 2.0 {
- l -= 1;
- }
-
- let mut res = 10.0_f64.powi(l);
-
- let count = range / res;
-
- if count > 15.0 {
- res *= 5.0;
- } else if count > 10.0 {
- res *= 2.0;
- }
-
- res
-}
-
-fn get_grid_unit_base2(min: f64, max: f64) -> f64 {
- let range = max - min;
-
- if range == 0.0 {
- panic!("get_grid_unit_base2: got zero range - internal error");
- }
-
- let mut l = range.log2() as i32;
-
- while (range / 2.0_f64.powi(l)) < 4.0 {
- l -= 1;
- }
-
- let mut res = 2.0_f64.powi(l);
-
- let count = range / res;
-
- if count > 15.0 {
- res *= 2.0;
- }
-
- res
-}
-
-fn get_time_grid_unit(min: i64, max: i64) -> i64 {
- let range = max - min;
-
- if range < 10 {
- // should not happen
- return 1;
- }
-
- let units = [
- 3600 * 24,
- 3600 * 12,
- 3600 * 6,
- 3600 * 4,
- 3600 * 2,
- 60 * 60,
- 60 * 30,
- 60 * 15,
- 60 * 10,
- 60 * 5,
- 60 * 2,
- 60,
- 30,
- 15,
- 10,
- 5,
- 2,
- 1,
- ];
-
- let mut l = *units.first().unwrap();
- for unit in units {
- if (range / unit) > 5 {
- l = unit;
- break;
- }
- }
-
- while (l >= *units.first().unwrap()) && (range / l) > 10 {
- l *= 2;
- }
-
- //log::info!("TIMERANG {l}");
-
- l
-}
+use super::units::{get_grid_unit_base10, get_grid_unit_base2, get_time_grid_unit};
fn format_date_time(t: i64) -> String {
let (time, date) = format_time(t);
diff --git a/src/rrd/mod.rs b/src/rrd/mod.rs
index 87bc07f..7931053 100644
--- a/src/rrd/mod.rs
+++ b/src/rrd/mod.rs
@@ -1,2 +1,4 @@
mod graph;
pub use graph::*;
+
+pub(crate) mod units;
diff --git a/src/rrd/units.rs b/src/rrd/units.rs
new file mode 100644
index 0000000..b29a238
--- /dev/null
+++ b/src/rrd/units.rs
@@ -0,0 +1,95 @@
+pub(crate) fn get_grid_unit_base10(min: f64, max: f64) -> f64 {
+ let range = max - min;
+
+ if range == 0.0 {
+ panic!("get_grid_unit_base10: got zero range - internal error");
+ }
+
+ let mut l = range.log10() as i32;
+
+ while (range / 10.0_f64.powi(l)) < 2.0 {
+ l -= 1;
+ }
+
+ let mut res = 10.0_f64.powi(l);
+
+ let count = range / res;
+
+ if count > 15.0 {
+ res *= 5.0;
+ } else if count > 10.0 {
+ res *= 2.0;
+ }
+
+ res
+}
+
+pub(crate) fn get_grid_unit_base2(min: f64, max: f64) -> f64 {
+ let range = max - min;
+
+ if range == 0.0 {
+ panic!("get_grid_unit_base2: got zero range - internal error");
+ }
+
+ let mut l = range.log2() as i32;
+
+ while (range / 2.0_f64.powi(l)) < 4.0 {
+ l -= 1;
+ }
+
+ let mut res = 2.0_f64.powi(l);
+
+ let count = range / res;
+
+ if count > 15.0 {
+ res *= 2.0;
+ }
+
+ res
+}
+
+pub(crate) fn get_time_grid_unit(min: i64, max: i64) -> i64 {
+ let range = max - min;
+
+ if range < 10 {
+ // should not happen
+ return 1;
+ }
+
+ let units = [
+ 3600 * 24,
+ 3600 * 12,
+ 3600 * 6,
+ 3600 * 4,
+ 3600 * 2,
+ 60 * 60,
+ 60 * 30,
+ 60 * 15,
+ 60 * 10,
+ 60 * 5,
+ 60 * 2,
+ 60,
+ 30,
+ 15,
+ 10,
+ 5,
+ 2,
+ 1,
+ ];
+
+ let mut l = *units.first().unwrap();
+ for unit in units {
+ if (range / unit) > 5 {
+ l = unit;
+ break;
+ }
+ }
+
+ while (l >= *units.first().unwrap()) && (range / l) > 10 {
+ l *= 2;
+ }
+
+ //log::info!("TIMERANG {l}");
+
+ l
+}
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 05/20] rrd: units: add tests
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
` (3 preceding siblings ...)
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 04/20] rrd: move unit calculation to " Dominik Csapak
@ 2025-05-30 12:21 ` Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 06/20] rrd: units: simplify calculations for get_grid_unit_base Dominik Csapak
` (14 subsequent siblings)
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:21 UTC (permalink / raw)
To: yew-devel
so we can argue about code changes more easily, also expand the range
check for negative ranges, since that should not happen.
this also adds some comments what the functions should do
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/rrd/units.rs | 149 +++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 145 insertions(+), 4 deletions(-)
diff --git a/src/rrd/units.rs b/src/rrd/units.rs
index b29a238..ad72565 100644
--- a/src/rrd/units.rs
+++ b/src/rrd/units.rs
@@ -1,16 +1,21 @@
+// calculates the distance of the x axis labels + grid for base10 units
+// The distance calculated is always between 1/2 and 1/10 of the range
pub(crate) fn get_grid_unit_base10(min: f64, max: f64) -> f64 {
let range = max - min;
- if range == 0.0 {
- panic!("get_grid_unit_base10: got zero range - internal error");
+ if range <= 0.0 {
+ panic!("get_grid_unit_base10: got zero or negative range - internal error");
}
let mut l = range.log10() as i32;
+ // the count can be between 1 and 10
while (range / 10.0_f64.powi(l)) < 2.0 {
l -= 1;
}
+ // now it must be between 2 and 20
+
let mut res = 10.0_f64.powi(l);
let count = range / res;
@@ -20,15 +25,18 @@ pub(crate) fn get_grid_unit_base10(min: f64, max: f64) -> f64 {
} else if count > 10.0 {
res *= 2.0;
}
+ // here the count must be between 2 and 10
res
}
+// calculates the distance of the x axis labels + grid for base2 units
+// The distance calculated is always smaller than 1/4 of the range
pub(crate) fn get_grid_unit_base2(min: f64, max: f64) -> f64 {
let range = max - min;
- if range == 0.0 {
- panic!("get_grid_unit_base2: got zero range - internal error");
+ if range <= 0.0 {
+ panic!("get_grid_unit_base2: got zero or negative range - internal error");
}
let mut l = range.log2() as i32;
@@ -93,3 +101,136 @@ pub(crate) fn get_time_grid_unit(min: i64, max: i64) -> i64 {
l
}
+
+#[cfg(test)]
+mod test {
+ use std::panic;
+
+ use crate::rrd::units::get_time_grid_unit;
+
+ use super::{get_grid_unit_base10, get_grid_unit_base2};
+
+ const DELTA: f64 = 0.0000001;
+
+ #[test]
+ fn test_grid_unit_base2() {
+ // min, max, result
+ let test_data = [
+ // normal range tests
+ (0.0, 0.01, Some(2.0_f64.powi(-9))),
+ (0.0, 2.0, Some(2.0_f64.powi(-1))),
+ (0.0, 0.00001, Some(2.0_f64.powi(-19))),
+ (0.0, 100.0, Some(2.0_f64.powi(4))),
+ (0.0, 1_000_000.0, Some(2.0_f64.powi(17))),
+ (0.0, f64::MAX, Some(2.0_f64.powi(1021))),
+ (
+ 10.0 * 1024.0 * 1024.0,
+ 12.5 * 1024.0 * 1024.0,
+ Some(2.0_f64.powi(19)),
+ ),
+ // ranges with negative data
+ (-500.0, -100.0, Some(2.0_f64.powi(6))),
+ (-500.0, 100.0, Some(2.0_f64.powi(7))),
+ // panic tests
+ (0.0, 0.0, None),
+ (100.0, 0.01, None),
+ ];
+
+ for (min, max, expected) in test_data {
+ match (
+ panic::catch_unwind(|| get_grid_unit_base2(min, max)).ok(),
+ expected,
+ ) {
+ (Some(result), Some(expected)) => {
+ let diff = result - expected;
+ assert_eq!(
+ diff < DELTA,
+ diff > -DELTA,
+ "{min} .. {max} ->\n {expected} \n {result}"
+ )
+ }
+ (None, Some(expected)) => {
+ panic!("panic'ed when it shouldn't: {min} .. {max} -> {expected}")
+ }
+ (Some(result), None) => {
+ panic!("result when it should have panic'ed: {min} .. {max} -> {result}")
+ }
+ (None, None) => {}
+ }
+ }
+ }
+
+ #[test]
+ fn test_grid_unit_base10() {
+ // min, max, result
+ let test_data = [
+ // normal range tests
+ (0.0, 0.01, Some(0.001)),
+ (0.0, 2.0, Some(1.0)),
+ (0.0, 0.00001, Some(0.000002)),
+ (0.0, 100.0, Some(10.0)),
+ (0.0, 1_000_000.0, Some(100_000.0)),
+ (
+ 0.0,
+ f64::MAX,
+ Some(5000000000000002.0 * 10.0_f64.powf(292.0)),
+ ),
+ (
+ 10.0 * 1024.0 * 1024.0,
+ 12.5 * 1024.0 * 1024.0,
+ Some(1_000_000.0),
+ ),
+ // ranges with negative data
+ (-500.0, -100.0, Some(100.0)),
+ (-500.0, 100.0, Some(100.0)),
+ // panic tests
+ (0.0, 0.0, None),
+ (100.0, 0.01, None),
+ ];
+
+ for (min, max, expected) in test_data {
+ match (
+ panic::catch_unwind(|| get_grid_unit_base10(min, max)).ok(),
+ expected,
+ ) {
+ (Some(result), Some(expected)) => {
+ let diff = result - expected;
+ assert_eq!(
+ diff < DELTA,
+ diff > -DELTA,
+ "{min} .. {max} ->\n {expected} \n {result}"
+ )
+ }
+ (None, Some(expected)) => {
+ panic!("panic'ed when it shouldn't: {min} .. {max} -> {expected}")
+ }
+ (Some(result), None) => {
+ panic!("result when it should have panic'ed: {min} .. {max} -> {result}")
+ }
+ (None, None) => {}
+ }
+ }
+ }
+
+ #[test]
+ fn test_time_grid_unit() {
+ // min max result
+ let test_data = [
+ (0, 10, 1),
+ (0, 100, 15),
+ (0, 1_000_000, 172800),
+ (0, i64::MAX, 1519964874237542400),
+ (-1000, 1_000_000, 172800),
+ (0, 0, 1),
+ (1, 0, 1),
+ ];
+
+ for (min, max, expected) in test_data {
+ assert_eq!(
+ get_time_grid_unit(min, max),
+ expected,
+ "{min}..{max} -> {expected}"
+ )
+ }
+ }
+}
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 06/20] rrd: units: simplify calculations for get_grid_unit_base
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
` (4 preceding siblings ...)
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 05/20] rrd: units: add tests Dominik Csapak
@ 2025-05-30 12:21 ` Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 07/20] rrd: remove unnecessary `no_data` field Dominik Csapak
` (13 subsequent siblings)
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:21 UTC (permalink / raw)
To: yew-devel
in get_grid_unit_base2, when resolving the mathematical calculations,
one can see that we always subtract 2 from the log2 to get to the
result. (with the exception of ranges smaller than 2 and very big
numbers where the floating point operation does not work properly
anymore in 64 bits, e.g. 2.0^1024 -> Infinity). This saves us the loop
and at least 2 calculations for the condition.
The second condition could never be reached, since doubling a number <
4.0 can never be > 15.0, so we can just remove it.
In get_grid_unit_base10, we can at least change the while loop to an if,
since that will only ever run once.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/rrd/units.rs | 16 ++++------------
1 file changed, 4 insertions(+), 12 deletions(-)
diff --git a/src/rrd/units.rs b/src/rrd/units.rs
index ad72565..a15eb2d 100644
--- a/src/rrd/units.rs
+++ b/src/rrd/units.rs
@@ -10,7 +10,7 @@ pub(crate) fn get_grid_unit_base10(min: f64, max: f64) -> f64 {
let mut l = range.log10() as i32;
// the count can be between 1 and 10
- while (range / 10.0_f64.powi(l)) < 2.0 {
+ if (range / 10.0_f64.powi(l)) < 2.0 {
l -= 1;
}
@@ -39,21 +39,13 @@ pub(crate) fn get_grid_unit_base2(min: f64, max: f64) -> f64 {
panic!("get_grid_unit_base2: got zero or negative range - internal error");
}
- let mut l = range.log2() as i32;
+ let mut l = range.log2() as i32 - 2;
- while (range / 2.0_f64.powi(l)) < 4.0 {
+ if range / 2.0_f64.powi(l) < 4.0 {
l -= 1;
}
- let mut res = 2.0_f64.powi(l);
-
- let count = range / res;
-
- if count > 15.0 {
- res *= 2.0;
- }
-
- res
+ 2.0_f64.powi(l)
}
pub(crate) fn get_time_grid_unit(min: i64, max: i64) -> i64 {
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 07/20] rrd: remove unnecessary `no_data` field
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
` (5 preceding siblings ...)
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 06/20] rrd: units: simplify calculations for get_grid_unit_base Dominik Csapak
@ 2025-05-30 12:21 ` Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 08/20] rrd: align tooltip directly to pointer position Dominik Csapak
` (12 subsequent siblings)
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:21 UTC (permalink / raw)
To: yew-devel
just use slices, then we can return an empty slice here.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/rrd/graph.rs | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/src/rrd/graph.rs b/src/rrd/graph.rs
index 8b31103..e1eb9c9 100644
--- a/src/rrd/graph.rs
+++ b/src/rrd/graph.rs
@@ -143,7 +143,6 @@ pub struct PwtRRDGraph {
datapoint_ref: NodeRef,
align_options: AlignOptions,
y_label_ref: NodeRef,
- no_data: Vec<f64>,
serie0_visible: bool,
serie1_visible: bool,
}
@@ -359,12 +358,12 @@ impl PwtRRDGraph {
let time_data = &props.time_data;
let serie0_data = match (self.serie0_visible, &props.serie0) {
- (true, Some(serie)) => &serie.data,
- _ => &self.no_data,
+ (true, Some(serie)) => &serie.data[..],
+ _ => &[],
};
let serie1_data = match (self.serie1_visible, &props.serie1) {
- (true, Some(serie)) => &serie.data,
- _ => &self.no_data,
+ (true, Some(serie)) => &serie.data[..],
+ _ => &[],
};
if let Some((start, end)) = self.view_range {
@@ -697,7 +696,6 @@ impl Component for PwtRRDGraph {
datapoint_ref: NodeRef::default(),
align_options,
y_label_ref: NodeRef::default(),
- no_data: Vec::new(),
serie0_visible: true,
serie1_visible: true,
}
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 08/20] rrd: align tooltip directly to pointer position
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
` (6 preceding siblings ...)
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 07/20] rrd: remove unnecessary `no_data` field Dominik Csapak
@ 2025-05-30 12:21 ` Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 09/20] rrd: use 'cross_pos' state instead of 'draw_cross' Dominik Csapak
` (11 subsequent siblings)
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:21 UTC (permalink / raw)
To: yew-devel
instead of drawing an invisible circle into the graph that is only there
for aligning the toolip, directly save the pointer position (+ offset)
as position for the the tooltip
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/rrd/graph.rs | 37 ++++++++++++++-----------------------
1 file changed, 14 insertions(+), 23 deletions(-)
diff --git a/src/rrd/graph.rs b/src/rrd/graph.rs
index e1eb9c9..7f53c41 100644
--- a/src/rrd/graph.rs
+++ b/src/rrd/graph.rs
@@ -6,7 +6,7 @@ use yew::html::IntoPropValue;
use yew::prelude::*;
use yew::virtual_dom::{VComp, VNode};
-use pwt::dom::align::{align_to, AlignOptions};
+use pwt::dom::align::align_to_xy;
use pwt::dom::DomSizeObserver;
use pwt::prelude::*;
use pwt::props::{IntoOptionalTextRenderFn, TextRenderFn};
@@ -121,7 +121,7 @@ pub enum Msg {
AdjustLeftOffset(usize),
StartSelection(i32, i32),
EndSelection(i32),
- PointerMove(i32, i32),
+ PointerMove(i32, i32, i32, i32),
PointerEnter,
PointerLeave,
ClearViewRange,
@@ -139,9 +139,8 @@ pub struct PwtRRDGraph {
captured_pointer_id: Option<i32>,
draw_cross: bool,
cross_pos: Option<(i32, i32)>,
+ tooltip_pos: Option<(f64, f64)>,
tooltip_ref: NodeRef,
- datapoint_ref: NodeRef,
- align_options: AlignOptions,
y_label_ref: NodeRef,
serie0_visible: bool,
serie1_visible: bool,
@@ -595,17 +594,6 @@ impl PwtRRDGraph {
.d(format!("M {x} 0 L {x} {max_y} M {min_x} {y} L {max_x} {y}"))
.into(),
);
-
- // Add invisible circle to position the tooltip
- children.push(
- Circle::new()
- .node_ref(self.datapoint_ref.clone())
- .fill("none")
- .stroke("none")
- .position(x, y)
- .r(1)
- .into(),
- );
}
}
@@ -634,7 +622,12 @@ impl PwtRRDGraph {
.callback(|event: PointerEvent| Msg::EndSelection(event.offset_x())),
)
.onpointermove(ctx.link().callback(|event: PointerEvent| {
- Msg::PointerMove(event.offset_x(), event.offset_y())
+ Msg::PointerMove(
+ event.offset_x(),
+ event.offset_y(),
+ event.client_x(),
+ event.client_y(),
+ )
}))
.into()
}
@@ -680,8 +673,6 @@ impl Component for PwtRRDGraph {
fn create(ctx: &Context<Self>) -> Self {
ctx.link().send_message(Msg::Reload);
- let align_options = AlignOptions::default().offset(20.0, 20.0);
-
Self {
node_ref: NodeRef::default(),
size_observer: None,
@@ -692,9 +683,8 @@ impl Component for PwtRRDGraph {
captured_pointer_id: None,
draw_cross: false,
cross_pos: None,
+ tooltip_pos: None,
tooltip_ref: NodeRef::default(),
- datapoint_ref: NodeRef::default(),
- align_options,
y_label_ref: NodeRef::default(),
serie0_visible: true,
serie1_visible: true,
@@ -751,8 +741,9 @@ impl Component for PwtRRDGraph {
self.selection = Some((start_index, start_index));
true
}
- Msg::PointerMove(x, y) => {
+ Msg::PointerMove(x, y, client_x, client_y) => {
self.cross_pos = Some((x, y));
+ self.tooltip_pos = Some(((client_x + 20) as f64, (client_y + 20) as f64));
self.selection = match self.selection {
Some((start, _)) => {
let (data0, _, _) = self.get_view_data(ctx);
@@ -903,9 +894,9 @@ impl Component for PwtRRDGraph {
self.size_observer = Some(size_observer);
}
}
- if let Some(content_node) = self.datapoint_ref.get() {
+ if let Some(pos) = self.tooltip_pos {
if let Some(tooltip_node) = self.tooltip_ref.get() {
- let _ = align_to(content_node, tooltip_node, Some(self.align_options.clone()));
+ let _ = align_to_xy(tooltip_node, pos, pwt::dom::align::Point::TopStart);
}
}
if let Some(el) = self.y_label_ref.cast::<web_sys::SvgsvgElement>() {
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 09/20] rrd: use 'cross_pos' state instead of 'draw_cross'
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
` (7 preceding siblings ...)
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 08/20] rrd: align tooltip directly to pointer position Dominik Csapak
@ 2025-05-30 12:21 ` Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 10/20] rrd: give all elements in svg keys Dominik Csapak
` (10 subsequent siblings)
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:21 UTC (permalink / raw)
To: yew-devel
reset the state of 'cross_pos' when we would reset 'draw_cross' then we
can use `is_some()` instead of the draw_cross boolean, which saves us a
property plus some indentation
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/rrd/graph.rs | 114 ++++++++++++++++++++++-------------------------
1 file changed, 54 insertions(+), 60 deletions(-)
diff --git a/src/rrd/graph.rs b/src/rrd/graph.rs
index 7f53c41..e2ac767 100644
--- a/src/rrd/graph.rs
+++ b/src/rrd/graph.rs
@@ -137,7 +137,6 @@ pub struct PwtRRDGraph {
selection: Option<(usize, usize)>,
view_range: Option<(usize, usize)>,
captured_pointer_id: Option<i32>,
- draw_cross: bool,
cross_pos: Option<(i32, i32)>,
tooltip_pos: Option<(f64, f64)>,
tooltip_ref: NodeRef,
@@ -548,53 +547,51 @@ impl PwtRRDGraph {
}
}
- if self.draw_cross {
- if let Some((x, y)) = self.cross_pos {
- let idx = self.offset_to_time_index(x, data0);
-
- if let Some(t) = data0.get(idx) {
- if let Some(v) = data1.get(idx) {
- if v.is_finite() {
- let px = compute_x(*t) as f32;
- let py = compute_y(*v) as f32;
- children.push(
- Circle::new()
- .class("pwt-rrd-selected-datapoint")
- .position(px, py)
- .r(5)
- .into(),
- );
- }
+ if let Some((x, y)) = self.cross_pos {
+ let idx = self.offset_to_time_index(x, data0);
+
+ if let Some(t) = data0.get(idx) {
+ if let Some(v) = data1.get(idx) {
+ if v.is_finite() {
+ let px = compute_x(*t) as f32;
+ let py = compute_y(*v) as f32;
+ children.push(
+ Circle::new()
+ .class("pwt-rrd-selected-datapoint")
+ .position(px, py)
+ .r(5)
+ .into(),
+ );
}
- if let Some(v) = data2.get(idx) {
- if v.is_finite() {
- let px = compute_x(*t) as f32;
- let py = compute_y(*v) as f32;
- children.push(
- Circle::new()
- .class("pwt-rrd-selected-datapoint")
- .position(px, py)
- .r(5)
- .into(),
- );
- }
+ }
+ if let Some(v) = data2.get(idx) {
+ if v.is_finite() {
+ let px = compute_x(*t) as f32;
+ let py = compute_y(*v) as f32;
+ children.push(
+ Circle::new()
+ .class("pwt-rrd-selected-datapoint")
+ .position(px, py)
+ .r(5)
+ .into(),
+ );
}
}
+ }
- let max_y = compute_y(min_data);
- let min_x = self.layout.left_offset + self.layout.grid_border;
- let max_x = self.layout.width - self.layout.grid_border;
+ let max_y = compute_y(min_data);
+ let min_x = self.layout.left_offset + self.layout.grid_border;
+ let max_x = self.layout.width - self.layout.grid_border;
- let x = x.max(min_x as i32).min(max_x as i32);
- let y = y.min(max_y as i32);
+ let x = x.max(min_x as i32).min(max_x as i32);
+ let y = y.min(max_y as i32);
- children.push(
- Path::new()
- .class("pwt-rrd-cross")
- .d(format!("M {x} 0 L {x} {max_y} M {min_x} {y} L {max_x} {y}"))
- .into(),
- );
- }
+ children.push(
+ Path::new()
+ .class("pwt-rrd-cross")
+ .d(format!("M {x} 0 L {x} {max_y} M {min_x} {y} L {max_x} {y}"))
+ .into(),
+ );
}
Canvas::new()
@@ -681,7 +678,6 @@ impl Component for PwtRRDGraph {
selection: None,
view_range: None,
captured_pointer_id: None,
- draw_cross: false,
cross_pos: None,
tooltip_pos: None,
tooltip_ref: NodeRef::default(),
@@ -724,11 +720,11 @@ impl Component for PwtRRDGraph {
true
}
Msg::PointerEnter => {
- self.draw_cross = true;
+ self.cross_pos = None;
true
}
Msg::PointerLeave => {
- self.draw_cross = false;
+ self.cross_pos = None;
true
}
Msg::StartSelection(x, pointer_id) => {
@@ -800,21 +796,19 @@ impl Component for PwtRRDGraph {
let mut serie0_value = None;
let mut serie1_value = None;
- if self.draw_cross {
- if let Some((x, _)) = self.cross_pos {
- let (data0, data1, data2) = self.get_view_data(ctx);
- let idx = self.offset_to_time_index(x, data0);
- if let Some(t) = data0.get(idx) {
- data_time = Some(format_date_time(*t));
- if let Some(v) = data1.get(idx) {
- if v.is_finite() {
- serie0_value = Some(render_value(props, *v));
- }
+ if let Some((x, _)) = self.cross_pos {
+ let (data0, data1, data2) = self.get_view_data(ctx);
+ let idx = self.offset_to_time_index(x, data0);
+ if let Some(t) = data0.get(idx) {
+ data_time = Some(format_date_time(*t));
+ if let Some(v) = data1.get(idx) {
+ if v.is_finite() {
+ serie0_value = Some(render_value(props, *v));
}
- if let Some(v) = data2.get(idx) {
- if v.is_finite() {
- serie1_value = Some(render_value(props, *v));
- }
+ }
+ if let Some(v) = data2.get(idx) {
+ if v.is_finite() {
+ serie1_value = Some(render_value(props, *v));
}
}
}
@@ -824,7 +818,7 @@ impl Component for PwtRRDGraph {
.node_ref(self.tooltip_ref.clone())
.attribute("role", "tooltip")
.attribute("aria-live", "polite")
- .attribute("data-show", (self.draw_cross && data_time.is_some()).then_some(""))
+ .attribute("data-show", (self.cross_pos.is_some() && data_time.is_some()).then_some(""))
.class("pwt-tooltip")
.class("pwt-tooltip-rich")
.with_optional_child(match (self.serie0_visible, &props.serie0) {
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 10/20] rrd: give all elements in svg keys
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
` (8 preceding siblings ...)
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 09/20] rrd: use 'cross_pos' state instead of 'draw_cross' Dominik Csapak
@ 2025-05-30 12:21 ` Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 11/20] rrd: simplify toggle Msg Dominik Csapak
` (9 subsequent siblings)
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:21 UTC (permalink / raw)
To: yew-devel
otherwise adding the selection rectangle (or others in the future)
redraws the whole svg, which messes up the events, for example we then
get a pointerenter event for the svg but no pointerleave, even though
the mouse did not move
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/rrd/graph.rs | 42 ++++++++++++++++++++++++++++++++----------
1 file changed, 32 insertions(+), 10 deletions(-)
diff --git a/src/rrd/graph.rs b/src/rrd/graph.rs
index e2ac767..7f02274 100644
--- a/src/rrd/graph.rs
+++ b/src/rrd/graph.rs
@@ -480,14 +480,22 @@ impl PwtRRDGraph {
}
let mut children: Vec<Html> = Vec::new();
- children.push(Path::new().class("pwt-rrd-grid").d(grid_path).into());
- children.extend(time_labels);
-
- let y_label_group = Group::new()
- .node_ref(self.y_label_ref.clone())
- .children(value_labels);
-
- children.push(y_label_group.into());
+ children.push(
+ Path::new()
+ .key("grid")
+ .class("pwt-rrd-grid")
+ .d(grid_path)
+ .into(),
+ );
+ children.push(Group::new().key("time-labels").children(time_labels).into());
+
+ children.push(
+ Group::new()
+ .key("value-labels")
+ .node_ref(self.y_label_ref.clone())
+ .children(value_labels)
+ .into(),
+ );
if self.serie0_visible && props.serie0.is_some() {
let path = compute_outline_path(data0, data1, compute_x, compute_y);
@@ -495,8 +503,13 @@ impl PwtRRDGraph {
compute_fill_path(data0, data1, min_data, max_data, compute_x, compute_y);
children.extend(vec![
- Path::new().class("pwt-rrd-outline-path1").d(path).into(),
Path::new()
+ .key("series0-path")
+ .class("pwt-rrd-outline-path1")
+ .d(path)
+ .into(),
+ Path::new()
+ .key("series0-fill")
.class("pwt-rrd-fill-path1")
.d(pos_fill_path)
.into(),
@@ -509,8 +522,13 @@ impl PwtRRDGraph {
compute_fill_path(data0, data2, min_data, max_data, compute_x, compute_y);
children.extend(vec![
- Path::new().class("pwt-rrd-outline-path2").d(path).into(),
Path::new()
+ .key("series1-path")
+ .class("pwt-rrd-outline-path2")
+ .d(path)
+ .into(),
+ Path::new()
+ .key("series1-fill")
.class("pwt-rrd-fill-path2")
.d(pos_fill_path)
.into(),
@@ -535,6 +553,7 @@ impl PwtRRDGraph {
children.push(
Rect::new()
+ .key("selection-rect")
.class("pwt-rrd-selection")
.position(start_x as f32, end_y as f32)
.width((end_x - start_x) as f32)
@@ -557,6 +576,7 @@ impl PwtRRDGraph {
let py = compute_y(*v) as f32;
children.push(
Circle::new()
+ .key("selection-circle1")
.class("pwt-rrd-selected-datapoint")
.position(px, py)
.r(5)
@@ -570,6 +590,7 @@ impl PwtRRDGraph {
let py = compute_y(*v) as f32;
children.push(
Circle::new()
+ .key("selection-circle2")
.class("pwt-rrd-selected-datapoint")
.position(px, py)
.r(5)
@@ -588,6 +609,7 @@ impl PwtRRDGraph {
children.push(
Path::new()
+ .key("cross")
.class("pwt-rrd-cross")
.d(format!("M {x} 0 L {x} {max_y} M {min_x} {y} L {max_x} {y}"))
.into(),
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 11/20] rrd: simplify toggle Msg
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
` (9 preceding siblings ...)
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 10/20] rrd: give all elements in svg keys Dominik Csapak
@ 2025-05-30 12:21 ` Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 12/20] rrd: remove wrongly annotated lifetime Dominik Csapak
` (8 subsequent siblings)
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:21 UTC (permalink / raw)
To: yew-devel
by just accepting an idx instead of doing both series separately.
This also prepares that part of the code for more than 2 series.
While at it, fix the typo (Toogle -> Toggle).
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/rrd/graph.rs | 29 ++++++++++++++---------------
1 file changed, 14 insertions(+), 15 deletions(-)
diff --git a/src/rrd/graph.rs b/src/rrd/graph.rs
index 7f02274..e011c21 100644
--- a/src/rrd/graph.rs
+++ b/src/rrd/graph.rs
@@ -125,8 +125,7 @@ pub enum Msg {
PointerEnter,
PointerLeave,
ClearViewRange,
- ToogleSerie0,
- ToogleSerie1,
+ ToggleSeries(u32), // index
}
pub struct PwtRRDGraph {
@@ -719,17 +718,17 @@ impl Component for PwtRRDGraph {
}
true
}
- Msg::ToogleSerie0 => {
- self.serie0_visible = !self.serie0_visible;
- if !(self.serie0_visible || self.serie1_visible) {
- self.serie1_visible = true;
- }
- true
- }
- Msg::ToogleSerie1 => {
- self.serie1_visible = !self.serie1_visible;
- if !(self.serie0_visible || self.serie1_visible) {
- self.serie0_visible = true;
+ Msg::ToggleSeries(idx) => {
+ if idx == 0 {
+ self.serie0_visible = !self.serie0_visible;
+ if !(self.serie0_visible || self.serie1_visible) {
+ self.serie1_visible = true;
+ }
+ } else if idx == 1 {
+ self.serie1_visible = !self.serie1_visible;
+ if !(self.serie0_visible || self.serie1_visible) {
+ self.serie0_visible = true;
+ }
}
true
}
@@ -880,7 +879,7 @@ impl Component for PwtRRDGraph {
Button::new(serie0.label.clone())
.class("pwt-button-elevated")
.icon_class(icon_class0)
- .onclick(ctx.link().callback(|_| Msg::ToogleSerie0)),
+ .onclick(ctx.link().callback(|_| Msg::ToggleSeries(0))),
);
let icon_class1 = classes!(
"pwt-rrd-legend-marker1",
@@ -892,7 +891,7 @@ impl Component for PwtRRDGraph {
Button::new(serie1.label.clone())
.class("pwt-button-elevated")
.icon_class(icon_class1)
- .onclick(ctx.link().callback(|_| Msg::ToogleSerie1)),
+ .onclick(ctx.link().callback(|_| Msg::ToggleSeries(1))),
);
}
}
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 12/20] rrd: remove wrongly annotated lifetime
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
` (10 preceding siblings ...)
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 11/20] rrd: simplify toggle Msg Dominik Csapak
@ 2025-05-30 12:21 ` Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 13/20] rrd: refactor series related struct and functions into own module Dominik Csapak
` (7 subsequent siblings)
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:21 UTC (permalink / raw)
To: yew-devel
the data returned from get_view_data does not depend on self, but the
way the lifetimes are annotated implies this. This can be a problem when
we want to borrow self mutably, but want to keep the data returned from
this method. By simply removing the lifetime annotation from &self, we
tell the compiler that we don't have to borrow &self immutably
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/rrd/graph.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/rrd/graph.rs b/src/rrd/graph.rs
index e011c21..b58bd58 100644
--- a/src/rrd/graph.rs
+++ b/src/rrd/graph.rs
@@ -350,7 +350,7 @@ fn compute_fill_path(
}
impl PwtRRDGraph {
- fn get_view_data<'a>(&'a self, ctx: &'a Context<Self>) -> (&'a [i64], &'a [f64], &'a [f64]) {
+ fn get_view_data<'a>(&self, ctx: &'a Context<Self>) -> (&'a [i64], &'a [f64], &'a [f64]) {
let props = ctx.props();
let time_data = &props.time_data;
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 13/20] rrd: refactor series related struct and functions into own module
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
` (11 preceding siblings ...)
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 12/20] rrd: remove wrongly annotated lifetime Dominik Csapak
@ 2025-05-30 12:21 ` Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 14/20] rrd: clamp view range when time_data changes Dominik Csapak
` (6 subsequent siblings)
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:21 UTC (permalink / raw)
To: yew-devel
makes the main graph module a bit smaller. While at it, document the
compute_*_path functions.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/rrd/graph.rs | 101 +------------------------------------------
src/rrd/mod.rs | 3 ++
src/rrd/series.rs | 107 ++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 112 insertions(+), 99 deletions(-)
create mode 100644 src/rrd/series.rs
diff --git a/src/rrd/graph.rs b/src/rrd/graph.rs
index b58bd58..161269f 100644
--- a/src/rrd/graph.rs
+++ b/src/rrd/graph.rs
@@ -15,20 +15,6 @@ use pwt::widget::{Button, Container, Panel};
use pwt_macros::builder;
-pub struct Series {
- pub label: AttrValue,
- pub data: Vec<f64>,
-}
-
-impl Series {
- pub fn new(label: impl Into<AttrValue>, data: Vec<f64>) -> Self {
- Self {
- label: label.into(),
- data,
- }
- }
-}
-
#[derive(Derivative)]
#[derivative(Clone, PartialEq)]
#[derive(Properties)]
@@ -166,7 +152,9 @@ impl Default for LayoutProps {
use pwt::widget::canvas::{Canvas, Circle, Group, Path, Rect, SvgLength, Text};
+use super::series::{compute_fill_path, compute_outline_path};
use super::units::{get_grid_unit_base10, get_grid_unit_base2, get_time_grid_unit};
+use super::Series;
fn format_date_time(t: i64) -> String {
let (time, date) = format_time(t);
@@ -264,91 +252,6 @@ fn compute_min_max(props: &RRDGraph, data1: &[f64], data2: &[f64]) -> (f64, f64,
(min_data, max_data, grid_unit)
}
-fn compute_outline_path(
- time_data: &[i64],
- values: &[f64],
- compute_x: impl Fn(i64) -> f64,
- compute_y: impl Fn(f64) -> f64,
-) -> String {
- let mut path = String::new();
- let mut last_undefined = true;
- for (i, t) in time_data.iter().enumerate() {
- let value = *values.get(i).unwrap_or(&f64::NAN);
- let x = compute_x(*t);
-
- if last_undefined {
- if value.is_nan() {
- continue;
- }
- last_undefined = false;
- let y = compute_y(value);
- path.push_str(&format!(" M {:.1} {:.1}", x, y));
- } else {
- if value.is_nan() {
- last_undefined = true;
- continue;
- }
- let y = compute_y(value);
- path.push_str(&format!(" L {:.1} {:.1}", x, y));
- }
- }
- path
-}
-
-fn compute_fill_path(
- time_data: &[i64],
- values: &[f64],
- min_data: f64,
- max_data: f64,
- compute_x: impl Fn(i64) -> f64,
- compute_y: impl Fn(f64) -> f64,
-) -> String {
- let mut y0 = compute_y(0.0);
- if min_data > 0.0 {
- y0 = compute_y(min_data)
- }
- if max_data < 0.0 {
- y0 = compute_y(max_data)
- }
- let mut path = String::new();
- let mut last_undefined = true;
- for i in 0..time_data.len() {
- let t = time_data[i];
- let value = *values.get(i).unwrap_or(&f64::NAN);
-
- let x = compute_x(t);
-
- if last_undefined {
- if value.is_nan() {
- continue;
- }
- last_undefined = false;
- path.push_str(&format!(" M {:.1} {:.1}", x, y0));
- } else if value.is_nan() {
- last_undefined = true;
- let x = if i > 0 {
- compute_x(time_data[i - 1])
- } else {
- x
- };
- path.push_str(&format!(" L {:.1} {:.1}", x, y0));
-
- continue;
- }
- let y = compute_y(value);
- path.push_str(&format!(" L {:.1} {:.1}", x, y));
- }
-
- if let Some(t) = time_data.last() {
- if !last_undefined {
- let x = compute_x(*t);
- path.push_str(&format!(" L {:.1} {:.1}", x, y0));
- }
- }
-
- path
-}
-
impl PwtRRDGraph {
fn get_view_data<'a>(&self, ctx: &'a Context<Self>) -> (&'a [i64], &'a [f64], &'a [f64]) {
let props = ctx.props();
diff --git a/src/rrd/mod.rs b/src/rrd/mod.rs
index 7931053..fe63ff5 100644
--- a/src/rrd/mod.rs
+++ b/src/rrd/mod.rs
@@ -1,4 +1,7 @@
mod graph;
pub use graph::*;
+pub(crate) mod series;
+pub use series::Series;
+
pub(crate) mod units;
diff --git a/src/rrd/series.rs b/src/rrd/series.rs
new file mode 100644
index 0000000..8807598
--- /dev/null
+++ b/src/rrd/series.rs
@@ -0,0 +1,107 @@
+use yew::AttrValue;
+
+/// Represents a series of data for an [`crate::RRDGraph`]
+pub struct Series {
+ pub label: AttrValue,
+ pub data: Vec<f64>,
+}
+
+impl Series {
+ pub fn new(label: impl Into<AttrValue>, data: Vec<f64>) -> Self {
+ Self {
+ label: label.into(),
+ data,
+ }
+ }
+}
+
+/// Calculate the outline path of a series of [`f64`] data for [`i64`] points in time.
+///
+/// The line will not be drawn for points that are missing
+pub fn compute_outline_path(
+ time_data: &[i64],
+ values: &[f64],
+ compute_x: impl Fn(i64) -> f64,
+ compute_y: impl Fn(f64) -> f64,
+) -> String {
+ let mut path = String::new();
+ let mut last_undefined = true;
+ for (i, t) in time_data.iter().enumerate() {
+ let value = *values.get(i).unwrap_or(&f64::NAN);
+ let x = compute_x(*t);
+
+ if last_undefined {
+ if value.is_nan() {
+ continue;
+ }
+ last_undefined = false;
+ let y = compute_y(value);
+ path.push_str(&format!(" M {:.1} {:.1}", x, y));
+ } else {
+ if value.is_nan() {
+ last_undefined = true;
+ continue;
+ }
+ let y = compute_y(value);
+ path.push_str(&format!(" L {:.1} {:.1}", x, y));
+ }
+ }
+ path
+}
+
+/// Calculate the fill path for a series of [`f64`] points for [`i64`] points in time.
+///
+/// The area will not be filled for points that are missing
+pub fn compute_fill_path(
+ time_data: &[i64],
+ values: &[f64],
+ min_data: f64,
+ max_data: f64,
+ compute_x: impl Fn(i64) -> f64,
+ compute_y: impl Fn(f64) -> f64,
+) -> String {
+ let mut y0 = compute_y(0.0);
+ if min_data > 0.0 {
+ y0 = compute_y(min_data)
+ }
+ if max_data < 0.0 {
+ y0 = compute_y(max_data)
+ }
+ let mut path = String::new();
+ let mut last_undefined = true;
+ for i in 0..time_data.len() {
+ let t = time_data[i];
+ let value = *values.get(i).unwrap_or(&f64::NAN);
+
+ let x = compute_x(t);
+
+ if last_undefined {
+ if value.is_nan() {
+ continue;
+ }
+ last_undefined = false;
+ path.push_str(&format!(" M {:.1} {:.1}", x, y0));
+ } else if value.is_nan() {
+ last_undefined = true;
+ let x = if i > 0 {
+ compute_x(time_data[i - 1])
+ } else {
+ x
+ };
+ path.push_str(&format!(" L {:.1} {:.1}", x, y0));
+
+ continue;
+ }
+ let y = compute_y(value);
+ path.push_str(&format!(" L {:.1} {:.1}", x, y));
+ }
+
+ if let Some(t) = time_data.last() {
+ if !last_undefined {
+ let x = compute_x(*t);
+ path.push_str(&format!(" L {:.1} {:.1}", x, y0));
+ }
+ }
+
+ path
+}
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 14/20] rrd: clamp view range when time_data changes
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
` (12 preceding siblings ...)
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 13/20] rrd: refactor series related struct and functions into own module Dominik Csapak
@ 2025-05-30 12:21 ` Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 15/20] rrd: refactor grid data computation Dominik Csapak
` (5 subsequent siblings)
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:21 UTC (permalink / raw)
To: yew-devel
otherwise it can happen that the selected range is outside the new
range. While it would be possible to handle that when drawing, it's less
costly to do it once here when we update the component.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/rrd/graph.rs | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/src/rrd/graph.rs b/src/rrd/graph.rs
index 161269f..b4e7a37 100644
--- a/src/rrd/graph.rs
+++ b/src/rrd/graph.rs
@@ -802,6 +802,23 @@ impl Component for PwtRRDGraph {
panel.into()
}
+ fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
+ let props = ctx.props();
+
+ // clamp view range to the new time data range
+ if let Some((start, end)) = self.view_range {
+ if props.time_data.len() < 10 {
+ self.view_range = None;
+ } else {
+ let end = end.min(props.time_data.len() - 1);
+ let start = start.min(end - 10);
+ self.view_range = Some((start, end));
+ }
+ }
+
+ true
+ }
+
fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
if first_render {
if let Some(el) = self.node_ref.cast::<web_sys::Element>() {
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 15/20] rrd: refactor grid data computation
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
` (13 preceding siblings ...)
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 14/20] rrd: clamp view range when time_data changes Dominik Csapak
@ 2025-05-30 12:21 ` Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 16/20] rrd: introduce GraphSpace struct and use it to precalculate graph data Dominik Csapak
` (4 subsequent siblings)
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:21 UTC (permalink / raw)
To: yew-devel
namely the data/time min/max, interval and ranges. This makes the main
graph module a bit smaller, since we can move the compute_min_max
functionality out.
The grid unit calculation can then be private in the units module.
Rename the variables so they are consistent (*_min, *_max, *_interval,
etc.)
The check for start/end time now can be simplified to data0.is_empty(),
because we don't need to extract the first and last time a second time.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/rrd/graph.rs | 175 +++++++++++++++++------------------------------
src/rrd/units.rs | 102 ++++++++++++++++++++++++++-
2 files changed, 162 insertions(+), 115 deletions(-)
diff --git a/src/rrd/graph.rs b/src/rrd/graph.rs
index b4e7a37..5edab66 100644
--- a/src/rrd/graph.rs
+++ b/src/rrd/graph.rs
@@ -153,7 +153,7 @@ impl Default for LayoutProps {
use pwt::widget::canvas::{Canvas, Circle, Group, Path, Rect, SvgLength, Text};
use super::series::{compute_fill_path, compute_outline_path};
-use super::units::{get_grid_unit_base10, get_grid_unit_base2, get_time_grid_unit};
+use super::units::GraphKeyData;
use super::Series;
fn format_date_time(t: i64) -> String {
@@ -200,58 +200,6 @@ fn render_value(props: &RRDGraph, v: f64) -> String {
}
}
-fn compute_min_max(props: &RRDGraph, data1: &[f64], data2: &[f64]) -> (f64, f64, f64) {
- let mut min_data: f64 = f64::INFINITY;
- let mut max_data: f64 = -f64::INFINITY;
-
- for v in data1.iter().chain(data2).filter(|v| v.is_finite()) {
- min_data = min_data.min(*v);
- max_data = max_data.max(*v);
- }
-
- // if one is infinite, the other must be too
- if min_data.is_infinite() || max_data.is_infinite() {
- min_data = 0.0;
- max_data = 1.0;
- }
-
- if props.include_zero {
- max_data = max_data.max(0.0);
- min_data = min_data.min(0.0);
- }
-
- if (max_data - min_data) < 0.0005 {
- if min_data > 0.0003 {
- max_data += 0.0002;
- min_data -= 0.0003;
- } else {
- max_data += 0.0005;
- }
- }
-
- let grid_unit = if props.binary {
- get_grid_unit_base2(min_data, max_data)
- } else {
- get_grid_unit_base10(min_data, max_data)
- };
-
- let snapped = (((min_data / grid_unit) as i64) as f64) * grid_unit;
- if snapped > min_data {
- min_data = snapped - grid_unit;
- } else {
- min_data = snapped;
- }
-
- let snapped = (((max_data / grid_unit) as i64) as f64) * grid_unit;
- if snapped < max_data {
- max_data = snapped + grid_unit;
- } else {
- max_data = snapped;
- }
-
- (min_data, max_data, grid_unit)
-}
-
impl PwtRRDGraph {
fn get_view_data<'a>(&self, ctx: &'a Context<Self>) -> (&'a [i64], &'a [f64], &'a [f64]) {
let props = ctx.props();
@@ -288,19 +236,23 @@ impl PwtRRDGraph {
let (data0, data1, data2) = self.get_view_data(ctx);
- let start_time = data0.first().unwrap_or(&0);
- let end_time = data0.last().unwrap_or(&0);
-
- let (min_data, max_data, grid_unit) = compute_min_max(props, data1, data2);
-
- let time_span = (end_time - start_time) as f64;
- let data_range = max_data - min_data;
+ let GraphKeyData {
+ data_min,
+ data_max,
+ data_interval,
+ data_range,
+ time_min,
+ time_max,
+ time_interval,
+ start_time,
+ time_range,
+ } = GraphKeyData::new(data0, &[data1, data2], props.include_zero, props.binary);
let compute_x = {
let width = (layout.width - layout.left_offset - layout.grid_border * 2) as f64;
move |t: i64| -> f64 {
(layout.left_offset + layout.grid_border) as f64
- + ((t - start_time) as f64 * width) / time_span
+ + ((t - time_min) as f64 * width) / time_range as f64
}
};
@@ -308,7 +260,7 @@ impl PwtRRDGraph {
let height = (layout.height - layout.bottom_offset - layout.grid_border * 2) as f64;
move |value: f64| -> f64 {
(layout.height - layout.grid_border - layout.bottom_offset) as f64
- - ((value - min_data) * height) / data_range
+ - ((value - data_min) * height) / data_range
}
};
@@ -317,67 +269,64 @@ impl PwtRRDGraph {
let mut value_labels: Vec<Html> = Vec::new();
let mut time_labels: Vec<Html> = Vec::new();
- if let Some(start) = data0.first() {
- if let Some(end) = data0.last() {
- let x0 = compute_x(*start) - (layout.grid_border as f64);
- let x1 = compute_x(*end) + (layout.grid_border as f64);
-
- let mut v = min_data;
- while v <= max_data {
- let y = compute_y(v);
- grid_path.push_str(&format!("M {:.1} {:.1} L {:.1} {:.1}", x0, y, x1, y));
+ if !data0.is_empty() {
+ let x0 = compute_x(time_min) - (layout.grid_border as f64);
+ let x1 = compute_x(time_max) + (layout.grid_border as f64);
+
+ let mut v = data_min;
+ while v <= data_max {
+ let y = compute_y(v);
+ grid_path.push_str(&format!("M {:.1} {:.1} L {:.1} {:.1}", x0, y, x1, y));
+
+ let label = render_value(props, v);
+ value_labels.push(
+ Text::new(label)
+ .class("pwt-rrd-label-text")
+ .position(x0 as f32, y as f32)
+ .dy(SvgLength::Px(4.0))
+ .dx(SvgLength::Px(-4.0))
+ .attribute("text-anchor", "end")
+ .into(),
+ );
- let label = render_value(props, v);
- value_labels.push(
- Text::new(label)
- .class("pwt-rrd-label-text")
- .position(x0 as f32, y as f32)
- .dy(SvgLength::Px(4.0))
- .dx(SvgLength::Px(-4.0))
- .attribute("text-anchor", "end")
- .into(),
- );
+ v += data_interval;
+ }
- v += grid_unit;
- }
+ let mut t = start_time;
+ let ymax = compute_y(data_max) - (layout.grid_border as f64);
+ let ymin = compute_y(data_min) + (layout.grid_border as f64);
- let time_grid_unit = get_time_grid_unit(*start, *end);
- let mut t = ((*start + time_grid_unit - 1) / time_grid_unit) * time_grid_unit;
- let ymax = compute_y(max_data) - (layout.grid_border as f64);
- let ymin = compute_y(min_data) + (layout.grid_border as f64);
+ let mut last_date = String::new();
- let mut last_date = String::new();
+ while t <= time_max {
+ let x = compute_x(t);
+ grid_path.push_str(&format!("M {:.1} {:.1} L {:.1} {:.1}", x, ymin, x, ymax));
- while t <= *end {
- let x = compute_x(t);
- grid_path.push_str(&format!("M {:.1} {:.1} L {:.1} {:.1}", x, ymin, x, ymax));
+ let (time, date) = format_time(t);
- let (time, date) = format_time(t);
+ time_labels.push(
+ Text::new(time)
+ .class("pwt-rrd-label-text")
+ .position(x as f32, ymin as f32)
+ .dy(SvgLength::Px(10.0))
+ .attribute("text-anchor", "middle")
+ .into(),
+ );
+ if date != last_date {
time_labels.push(
- Text::new(time)
+ Text::new(date.clone())
.class("pwt-rrd-label-text")
.position(x as f32, ymin as f32)
- .dy(SvgLength::Px(10.0))
+ .dy(SvgLength::Px(10.0 + 16.0))
.attribute("text-anchor", "middle")
.into(),
);
- if date != last_date {
- time_labels.push(
- Text::new(date.clone())
- .class("pwt-rrd-label-text")
- .position(x as f32, ymin as f32)
- .dy(SvgLength::Px(10.0 + 16.0))
- .attribute("text-anchor", "middle")
- .into(),
- );
-
- last_date = date;
- }
-
- t += time_grid_unit;
+ last_date = date;
}
+
+ t += time_interval;
}
}
let mut children: Vec<Html> = Vec::new();
@@ -402,7 +351,7 @@ impl PwtRRDGraph {
if self.serie0_visible && props.serie0.is_some() {
let path = compute_outline_path(data0, data1, compute_x, compute_y);
let pos_fill_path =
- compute_fill_path(data0, data1, min_data, max_data, compute_x, compute_y);
+ compute_fill_path(data0, data1, data_min, data_max, compute_x, compute_y);
children.extend(vec![
Path::new()
@@ -421,7 +370,7 @@ impl PwtRRDGraph {
if self.serie1_visible && props.serie1.is_some() {
let path = compute_outline_path(data0, data2, compute_x, compute_y);
let pos_fill_path =
- compute_fill_path(data0, data2, min_data, max_data, compute_x, compute_y);
+ compute_fill_path(data0, data2, data_min, data_max, compute_x, compute_y);
children.extend(vec![
Path::new()
@@ -450,8 +399,8 @@ impl PwtRRDGraph {
std::mem::swap(&mut start_x, &mut end_x);
}
- let start_y = compute_y(min_data);
- let end_y = compute_y(max_data);
+ let start_y = compute_y(data_min);
+ let end_y = compute_y(data_max);
children.push(
Rect::new()
@@ -502,7 +451,7 @@ impl PwtRRDGraph {
}
}
- let max_y = compute_y(min_data);
+ let max_y = compute_y(data_min);
let min_x = self.layout.left_offset + self.layout.grid_border;
let max_x = self.layout.width - self.layout.grid_border;
diff --git a/src/rrd/units.rs b/src/rrd/units.rs
index a15eb2d..83b304f 100644
--- a/src/rrd/units.rs
+++ b/src/rrd/units.rs
@@ -1,6 +1,6 @@
// calculates the distance of the x axis labels + grid for base10 units
// The distance calculated is always between 1/2 and 1/10 of the range
-pub(crate) fn get_grid_unit_base10(min: f64, max: f64) -> f64 {
+fn get_grid_unit_base10(min: f64, max: f64) -> f64 {
let range = max - min;
if range <= 0.0 {
@@ -32,7 +32,7 @@ pub(crate) fn get_grid_unit_base10(min: f64, max: f64) -> f64 {
// calculates the distance of the x axis labels + grid for base2 units
// The distance calculated is always smaller than 1/4 of the range
-pub(crate) fn get_grid_unit_base2(min: f64, max: f64) -> f64 {
+fn get_grid_unit_base2(min: f64, max: f64) -> f64 {
let range = max - min;
if range <= 0.0 {
@@ -94,6 +94,104 @@ pub(crate) fn get_time_grid_unit(min: i64, max: i64) -> i64 {
l
}
+#[derive(Clone, Default, Debug, PartialEq)]
+pub struct GraphKeyData {
+ pub data_min: f64,
+ pub data_max: f64,
+ pub data_interval: f64,
+ pub data_range: f64,
+
+ pub time_min: i64,
+ pub time_max: i64,
+ pub time_interval: i64,
+ pub start_time: i64,
+ pub time_range: i64,
+}
+
+impl GraphKeyData {
+ pub fn new(time_data: &[i64], data: &[&[f64]], include_zero: bool, binary: bool) -> Self {
+ let (data_min, data_max, data_interval) = Self::data_parameters(data, include_zero, binary);
+ let (time_min, time_max, time_interval, start_time) = Self::time_parameters(time_data);
+
+ Self {
+ data_min,
+ data_max,
+ data_interval,
+ data_range: data_max - data_min,
+ time_min,
+ time_max,
+ time_interval,
+ start_time,
+ time_range: time_max - time_min,
+ }
+ }
+
+ fn data_parameters(data: &[&[f64]], include_zero: bool, binary: bool) -> (f64, f64, f64) {
+ let mut min_data: f64 = f64::INFINITY;
+ let mut max_data: f64 = -f64::INFINITY;
+
+ for v in data.iter().flat_map(|d| d.iter()).filter(|v| v.is_finite()) {
+ min_data = min_data.min(*v);
+ max_data = max_data.max(*v);
+ }
+
+ // if one is infinite, the other must be too
+ if min_data.is_infinite() || max_data.is_infinite() {
+ min_data = 0.0;
+ max_data = 1.0;
+ }
+
+ if include_zero {
+ max_data = max_data.max(0.0);
+ min_data = min_data.min(0.0);
+ }
+
+ // stretch to at least 0.0005 difference
+ if (max_data - min_data) < 0.0005 {
+ if min_data > 0.0003 {
+ max_data += 0.0002;
+ min_data -= 0.0003;
+ } else {
+ max_data += 0.0005;
+ }
+ }
+
+ let interval = if binary {
+ get_grid_unit_base2(min_data, max_data)
+ } else {
+ get_grid_unit_base10(min_data, max_data)
+ };
+
+ let snapped = (((min_data / interval) as i64) as f64) * interval;
+ if snapped > min_data {
+ min_data = snapped - interval;
+ } else {
+ min_data = snapped;
+ }
+
+ let snapped = (((max_data / interval) as i64) as f64) * interval;
+ if snapped < max_data {
+ max_data = snapped + interval;
+ } else {
+ max_data = snapped;
+ }
+
+ (min_data, max_data, interval)
+ }
+
+ fn time_parameters(time_data: &[i64]) -> (i64, i64, i64, i64) {
+ let min_time = *time_data.first().unwrap_or(&0);
+ let max_time = *time_data.last().unwrap_or(&0);
+
+ let interval = get_time_grid_unit(min_time, max_time);
+
+ // snap the start time point to the interval
+ let start_time = ((min_time + interval - 1) / interval) * interval;
+
+ (min_time, max_time, interval, start_time)
+ }
+}
+
#[cfg(test)]
mod test {
use std::panic;
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 16/20] rrd: introduce GraphSpace struct and use it to precalculate graph data
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
` (14 preceding siblings ...)
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 15/20] rrd: refactor grid data computation Dominik Csapak
@ 2025-05-30 12:21 ` Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 17/20] rrd: precalculate the grid line and label positions Dominik Csapak
` (3 subsequent siblings)
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:21 UTC (permalink / raw)
To: yew-devel
This contains all information about the data and the svg graph that is
needed to convert coordinates between data and svg space.
This replaces the LayoutProps struct on the graph, and provides the
necessary interfaces.
It's data is updated whenever the grid parameters are changed so that we
need to recalculate them, e.g. when the data changes, or the width of
the grid. Since this is now only done when necessary, it should be much
faster when unrelated redraws occur, e.g. when moving the mouse cursor
over the data.
We need to derive PartialEq for the Series struct to check if they have
changed.
Also change the interfaces of compute_*_path, so that they only take
a GraphSpace struct anymore instead of the min/max data and compute
functions (all that is provided by the struct).
`compute_fill_path` calculation of y0 is changed slightly to accommodate
the fact that the GraphSpace only takes values inside the min and max
data ranges, but the end result should be the same.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/rrd/graph.rs | 144 ++++++++++++++++------------------------
src/rrd/graph_space.rs | 147 +++++++++++++++++++++++++++++++++++++++++
src/rrd/mod.rs | 2 +
src/rrd/series.rs | 45 +++++--------
4 files changed, 223 insertions(+), 115 deletions(-)
create mode 100644 src/rrd/graph_space.rs
diff --git a/src/rrd/graph.rs b/src/rrd/graph.rs
index 5edab66..a02e086 100644
--- a/src/rrd/graph.rs
+++ b/src/rrd/graph.rs
@@ -118,7 +118,7 @@ pub struct PwtRRDGraph {
node_ref: NodeRef,
size_observer: Option<DomSizeObserver>,
canvas_ref: NodeRef,
- layout: LayoutProps,
+ graph_space: GraphSpace,
selection: Option<(usize, usize)>,
view_range: Option<(usize, usize)>,
captured_pointer_id: Option<i32>,
@@ -130,28 +130,9 @@ pub struct PwtRRDGraph {
serie1_visible: bool,
}
-pub struct LayoutProps {
- width: usize,
- height: usize,
- grid_border: usize,
- left_offset: usize,
- bottom_offset: usize,
-}
-
-impl Default for LayoutProps {
- fn default() -> Self {
- Self {
- width: 800,
- height: 250,
- grid_border: 10,
- left_offset: 50,
- bottom_offset: 30,
- }
- }
-}
-
use pwt::widget::canvas::{Canvas, Circle, Group, Path, Rect, SvgLength, Text};
+use super::graph_space::{CoordinateRange, GraphSpace};
use super::series::{compute_fill_path, compute_outline_path};
use super::units::GraphKeyData;
use super::Series;
@@ -201,6 +182,13 @@ fn render_value(props: &RRDGraph, v: f64) -> String {
}
impl PwtRRDGraph {
+ fn update_grid_content(&mut self, ctx: &Context<Self>) {
+ let props = ctx.props();
+ let (time_data, data1, data2) = self.get_view_data(ctx);
+ self.graph_space
+ .update(time_data, &[data1, data2], props.include_zero, props.binary);
+ }
+
fn get_view_data<'a>(&self, ctx: &'a Context<Self>) -> (&'a [i64], &'a [f64], &'a [f64]) {
let props = ctx.props();
@@ -232,37 +220,17 @@ impl PwtRRDGraph {
fn create_graph(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
- let layout = &self.layout;
-
let (data0, data1, data2) = self.get_view_data(ctx);
let GraphKeyData {
data_min,
data_max,
data_interval,
- data_range,
- time_min,
time_max,
time_interval,
start_time,
- time_range,
- } = GraphKeyData::new(data0, &[data1, data2], props.include_zero, props.binary);
-
- let compute_x = {
- let width = (layout.width - layout.left_offset - layout.grid_border * 2) as f64;
- move |t: i64| -> f64 {
- (layout.left_offset + layout.grid_border) as f64
- + ((t - time_min) as f64 * width) / time_range as f64
- }
- };
-
- let compute_y = {
- let height = (layout.height - layout.bottom_offset - layout.grid_border * 2) as f64;
- move |value: f64| -> f64 {
- (layout.height - layout.grid_border - layout.bottom_offset) as f64
- - ((value - data_min) * height) / data_range
- }
- };
+ ..
+ } = self.graph_space.graph_data;
let mut grid_path = String::new();
@@ -270,12 +238,11 @@ impl PwtRRDGraph {
let mut time_labels: Vec<Html> = Vec::new();
if !data0.is_empty() {
- let x0 = compute_x(time_min) - (layout.grid_border as f64);
- let x1 = compute_x(time_max) + (layout.grid_border as f64);
+ let (x0, x1) = self.graph_space.get_x_range(CoordinateRange::OutsideBorder);
let mut v = data_min;
while v <= data_max {
- let y = compute_y(v);
+ let y = self.graph_space.compute_y(v);
grid_path.push_str(&format!("M {:.1} {:.1} L {:.1} {:.1}", x0, y, x1, y));
let label = render_value(props, v);
@@ -293,13 +260,12 @@ impl PwtRRDGraph {
}
let mut t = start_time;
- let ymax = compute_y(data_max) - (layout.grid_border as f64);
- let ymin = compute_y(data_min) + (layout.grid_border as f64);
+ let (ymin, ymax) = self.graph_space.get_y_range(CoordinateRange::OutsideBorder);
let mut last_date = String::new();
while t <= time_max {
- let x = compute_x(t);
+ let x = self.graph_space.compute_x(t);
grid_path.push_str(&format!("M {:.1} {:.1} L {:.1} {:.1}", x, ymin, x, ymax));
let (time, date) = format_time(t);
@@ -349,9 +315,8 @@ impl PwtRRDGraph {
);
if self.serie0_visible && props.serie0.is_some() {
- let path = compute_outline_path(data0, data1, compute_x, compute_y);
- let pos_fill_path =
- compute_fill_path(data0, data1, data_min, data_max, compute_x, compute_y);
+ let path = compute_outline_path(data0, data1, &self.graph_space);
+ let pos_fill_path = compute_fill_path(data0, data1, &self.graph_space);
children.extend(vec![
Path::new()
@@ -368,9 +333,8 @@ impl PwtRRDGraph {
}
if self.serie1_visible && props.serie1.is_some() {
- let path = compute_outline_path(data0, data2, compute_x, compute_y);
- let pos_fill_path =
- compute_fill_path(data0, data2, data_min, data_max, compute_x, compute_y);
+ let path = compute_outline_path(data0, data2, &self.graph_space);
+ let pos_fill_path = compute_fill_path(data0, data2, &self.graph_space);
children.extend(vec![
Path::new()
@@ -392,15 +356,15 @@ impl PwtRRDGraph {
match (data0.get(start), data0.get(end)) {
(Some(start_data), Some(end_data)) => {
- let mut start_x = compute_x(*start_data);
- let mut end_x = compute_x(*end_data);
+ let mut start_x = self.graph_space.compute_x(*start_data);
+ let mut end_x = self.graph_space.compute_x(*end_data);
if start_x > end_x {
std::mem::swap(&mut start_x, &mut end_x);
}
- let start_y = compute_y(data_min);
- let end_y = compute_y(data_max);
+ let (start_y, end_y) =
+ self.graph_space.get_y_range(CoordinateRange::InsideBorder);
children.push(
Rect::new()
@@ -423,8 +387,8 @@ impl PwtRRDGraph {
if let Some(t) = data0.get(idx) {
if let Some(v) = data1.get(idx) {
if v.is_finite() {
- let px = compute_x(*t) as f32;
- let py = compute_y(*v) as f32;
+ let px = self.graph_space.compute_x(*t) as f32;
+ let py = self.graph_space.compute_y(*v) as f32;
children.push(
Circle::new()
.key("selection-circle1")
@@ -437,8 +401,8 @@ impl PwtRRDGraph {
}
if let Some(v) = data2.get(idx) {
if v.is_finite() {
- let px = compute_x(*t) as f32;
- let py = compute_y(*v) as f32;
+ let px = self.graph_space.compute_x(*t) as f32;
+ let py = self.graph_space.compute_y(*v) as f32;
children.push(
Circle::new()
.key("selection-circle2")
@@ -451,18 +415,17 @@ impl PwtRRDGraph {
}
}
- let max_y = compute_y(data_min);
- let min_x = self.layout.left_offset + self.layout.grid_border;
- let max_x = self.layout.width - self.layout.grid_border;
+ let (min_y, _) = self.graph_space.get_y_range(CoordinateRange::InsideBorder);
+ let (min_x, max_x) = self.graph_space.get_x_range(CoordinateRange::InsideBorder);
let x = x.max(min_x as i32).min(max_x as i32);
- let y = y.min(max_y as i32);
+ let y = y.min(min_y as i32);
children.push(
Path::new()
.key("cross")
.class("pwt-rrd-cross")
- .d(format!("M {x} 0 L {x} {max_y} M {min_x} {y} L {max_x} {y}"))
+ .d(format!("M {x} 0 L {x} {min_y} M {min_x} {y} L {max_x} {y}"))
.into(),
);
}
@@ -470,8 +433,8 @@ impl PwtRRDGraph {
Canvas::new()
.node_ref(self.canvas_ref.clone())
.class("pwt-rrd-svg")
- .width(layout.width)
- .height(layout.height)
+ .width(self.graph_space.get_width())
+ .height(self.graph_space.get_height())
.children(children)
.ondblclick(ctx.link().callback(|_| Msg::ClearViewRange))
.onpointerenter(ctx.link().callback(|_| Msg::PointerEnter))
@@ -503,16 +466,7 @@ impl PwtRRDGraph {
}
fn offset_to_time_index(&self, x: i32, data0: &[i64]) -> usize {
- let layout = &self.layout;
- let width = (layout.width - layout.left_offset - layout.grid_border * 2) as f64;
-
- let start_time: i64 = *data0.first().unwrap_or(&0);
- let end_time: i64 = *data0.last().unwrap_or(&0);
- let time_span: i64 = end_time - start_time;
-
- let fraction: f64 = ((x - (layout.left_offset + layout.grid_border) as i32) as f64) / width;
-
- let t: i64 = ((fraction * (time_span as f64)) as i64) + start_time;
+ let t = self.graph_space.original_x(x as f64);
let start_index = data0.partition_point(|&x| x < t);
// Select nearest point
@@ -543,11 +497,11 @@ impl Component for PwtRRDGraph {
fn create(ctx: &Context<Self>) -> Self {
ctx.link().send_message(Msg::Reload);
- Self {
+ let mut this = Self {
node_ref: NodeRef::default(),
size_observer: None,
canvas_ref: NodeRef::default(),
- layout: LayoutProps::default(),
+ graph_space: GraphSpace::default(),
selection: None,
view_range: None,
captured_pointer_id: None,
@@ -557,7 +511,10 @@ impl Component for PwtRRDGraph {
y_label_ref: NodeRef::default(),
serie0_visible: true,
serie1_visible: true,
- }
+ };
+
+ this.update_grid_content(ctx);
+ this
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
@@ -566,7 +523,8 @@ impl Component for PwtRRDGraph {
Msg::Reload => true,
Msg::ViewportResize(width, _height) => {
if width > 0.0 {
- self.layout.width = width as usize;
+ self.graph_space.set_width(width as usize);
+ self.update_grid_content(ctx);
}
true
}
@@ -582,14 +540,17 @@ impl Component for PwtRRDGraph {
self.serie0_visible = true;
}
}
+ self.update_grid_content(ctx);
true
}
Msg::ClearViewRange => {
self.view_range = None;
+ self.update_grid_content(ctx);
true
}
Msg::AdjustLeftOffset(offset) => {
- self.layout.left_offset = offset;
+ self.graph_space.set_left_offset(offset);
+ self.update_grid_content(ctx);
true
}
Msg::PointerEnter => {
@@ -656,6 +617,7 @@ impl Component for PwtRRDGraph {
}
None => None,
};
+ self.update_grid_content(ctx);
true
}
@@ -751,7 +713,7 @@ impl Component for PwtRRDGraph {
panel.into()
}
- fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
+ fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
let props = ctx.props();
// clamp view range to the new time data range
@@ -765,6 +727,14 @@ impl Component for PwtRRDGraph {
}
}
+ // we need to recalculate the grid content when the series or time data changes
+ if props.serie0 != old_props.serie0
+ || props.serie1 != old_props.serie1
+ || props.time_data != old_props.time_data
+ {
+ self.update_grid_content(ctx);
+ }
+
true
}
@@ -786,7 +756,7 @@ impl Component for PwtRRDGraph {
if let Some(el) = self.y_label_ref.cast::<web_sys::SvgsvgElement>() {
if let Ok(bbox) = el.get_b_box() {
let offset = (bbox.width() + 10.0) as usize;
- if self.layout.left_offset != offset {
+ if self.graph_space.get_left_offset() != offset {
ctx.link().send_message(Msg::AdjustLeftOffset(offset));
}
}
diff --git a/src/rrd/graph_space.rs b/src/rrd/graph_space.rs
new file mode 100644
index 0000000..cd75177
--- /dev/null
+++ b/src/rrd/graph_space.rs
@@ -0,0 +1,147 @@
+use super::units::GraphKeyData;
+
+// Holds the basic data necessary for the SVG Layout
+struct LayoutProps {
+ width: usize,
+ height: usize,
+ grid_border: usize,
+ left_offset: usize,
+ bottom_offset: usize,
+ inner_width: usize,
+ inner_height: usize,
+}
+
+impl Default for LayoutProps {
+ fn default() -> Self {
+ Self {
+ width: 800,
+ height: 250,
+ grid_border: 10,
+ left_offset: 50,
+ bottom_offset: 30,
+ inner_width: 0,
+ inner_height: 0,
+ }
+ }
+}
+
+// maps value in range 0.0..=1.0 to SVG coordinates on the y axis
+fn map_relative_to_y(input: f64, layout: &LayoutProps) -> f64 {
+ #[cfg(debug_assertions)]
+ assert!((0.0..=1.0).contains(&input), "input: {input}");
+ layout.inner_height as f64 * (1.0 - input) + layout.grid_border as f64
+}
+
+// maps value in range 0.0..=1.0 to SVG coordinates on the x axis
+fn map_relative_to_x(input: f64, layout: &LayoutProps) -> f64 {
+ #[cfg(debug_assertions)]
+ assert!((0.0..=1.0).contains(&input), "input: {input}");
+ layout.inner_width as f64 * input + (layout.left_offset + layout.grid_border) as f64
+}
+
+/// Holds all necessary information to calculate between data space and svg space
+#[derive(Default)]
+pub struct GraphSpace {
+ layout: LayoutProps,
+ pub graph_data: GraphKeyData,
+}
+
+/// Options for getting the boundaries of the SVG coordinates
+pub enum CoordinateRange {
+ /// Coordinates will be inside the border
+ InsideBorder,
+ /// Coordinates can include the border
+ OutsideBorder,
+}
+
+impl GraphSpace {
+ /// Update the graph space with new graph data
+ pub fn update(&mut self, time_data: &[i64], data: &[&[f64]], include_zero: bool, binary: bool) {
+ self.graph_data = GraphKeyData::new(time_data, data, include_zero, binary);
+ }
+
+ /// Converts from data space to svg space on the x axis
+ pub fn compute_x(&self, x: i64) -> f64 {
+ map_relative_to_x(
+ (x - self.graph_data.time_min) as f64 / self.graph_data.time_range as f64,
+ &self.layout,
+ )
+ }
+
+ /// Converts from data space to svg space on the y axis
+ pub fn compute_y(&self, y: f64) -> f64 {
+ map_relative_to_y(
+ (y - self.graph_data.data_min) / self.graph_data.data_range,
+ &self.layout,
+ )
+ }
+
+ /// Returns the minimum and maximum coordinates for the x axis
+ pub fn get_x_range(&self, opts: CoordinateRange) -> (f64, f64) {
+ let mut min = map_relative_to_x(0.0, &self.layout);
+ if let CoordinateRange::OutsideBorder = opts {
+ min -= self.layout.grid_border as f64;
+ }
+ let mut max = map_relative_to_x(1.0, &self.layout);
+ if let CoordinateRange::OutsideBorder = opts {
+ max += self.layout.grid_border as f64;
+ }
+ (min, max)
+ }
+
+ /// Returns the minimum and maximum coordinates for the y axis
+ pub fn get_y_range(&self, opts: CoordinateRange) -> (f64, f64) {
+ let mut min = map_relative_to_y(0.0, &self.layout);
+ if let CoordinateRange::OutsideBorder = opts {
+ min += self.layout.grid_border as f64;
+ }
+ let mut max = map_relative_to_y(1.0, &self.layout);
+ if let CoordinateRange::OutsideBorder = opts {
+ max -= self.layout.grid_border as f64;
+ }
+ (min, max)
+ }
+
+ /// Converts back from svg space to data space for the x axis
+ pub fn original_x(&self, x: f64) -> i64 {
+ let layout = &self.layout;
+ let width = layout.inner_width as f64;
+ let fraction: f64 = (x - (layout.left_offset + layout.grid_border) as f64) / width;
+
+ ((fraction * (self.graph_data.time_range as f64)) as i64) + self.graph_data.time_min
+ }
+
+ /// Returns the complete current width of the graph
+ pub fn get_width(&self) -> usize {
+ self.layout.width
+ }
+
+ /// Returns the complete current height of the graph
+ pub fn get_height(&self) -> usize {
+ self.layout.height
+ }
+
+ fn update_inner_size(&mut self) {
+ let layout = &mut self.layout;
+ layout.inner_width = layout.width - layout.left_offset - layout.grid_border * 2;
+ layout.inner_height = layout.height - layout.bottom_offset - layout.grid_border * 2;
+ }
+
+ /// Updates the width of the layout, recalculates all necessary fields
+ pub fn set_width(&mut self, width: usize) {
+ self.layout.width = width;
+ self.update_inner_size();
+ }
+
+ /// Updates the left offset of the layout, recalculates all necessary fields
+ pub fn set_left_offset(&mut self, offset: usize) {
+ self.layout.left_offset = offset;
+ self.update_inner_size();
+ }
+
+ /// Returns the left offset of the graph. This is useful for dynamically
+ /// updating the space for the value labels
+ pub fn get_left_offset(&self) -> usize {
+ self.layout.left_offset
+ }
+}
diff --git a/src/rrd/mod.rs b/src/rrd/mod.rs
index fe63ff5..b55ac3f 100644
--- a/src/rrd/mod.rs
+++ b/src/rrd/mod.rs
@@ -1,6 +1,8 @@
mod graph;
pub use graph::*;
+pub(crate) mod graph_space;
+
pub(crate) mod series;
pub use series::Series;
diff --git a/src/rrd/series.rs b/src/rrd/series.rs
index 8807598..a210a42 100644
--- a/src/rrd/series.rs
+++ b/src/rrd/series.rs
@@ -1,5 +1,8 @@
use yew::AttrValue;
+use super::graph_space::GraphSpace;
+
+#[derive(PartialEq)]
/// Represents a series of data for an [`crate::RRDGraph`]
pub struct Series {
pub label: AttrValue,
@@ -18,31 +21,26 @@ impl Series {
/// Calculate the outline path of a series of [`f64`] data for [`i64`] points in time.
///
/// The line will not be drawn for points that are missing
-pub fn compute_outline_path(
- time_data: &[i64],
- values: &[f64],
- compute_x: impl Fn(i64) -> f64,
- compute_y: impl Fn(f64) -> f64,
-) -> String {
+pub fn compute_outline_path(time_data: &[i64], values: &[f64], graph_space: &GraphSpace) -> String {
let mut path = String::new();
let mut last_undefined = true;
for (i, t) in time_data.iter().enumerate() {
let value = *values.get(i).unwrap_or(&f64::NAN);
- let x = compute_x(*t);
+ let x = graph_space.compute_x(*t);
if last_undefined {
if value.is_nan() {
continue;
}
last_undefined = false;
- let y = compute_y(value);
+ let y = graph_space.compute_y(value);
path.push_str(&format!(" M {:.1} {:.1}", x, y));
} else {
if value.is_nan() {
last_undefined = true;
continue;
}
- let y = compute_y(value);
+ let y = graph_space.compute_y(value);
path.push_str(&format!(" L {:.1} {:.1}", x, y));
}
}
@@ -52,28 +50,19 @@ pub fn compute_outline_path(
/// Calculate the fill path for a series of [`f64`] points for [`i64`] points in time.
///
/// The area will not be filled for points that are missing
-pub fn compute_fill_path(
- time_data: &[i64],
- values: &[f64],
- min_data: f64,
- max_data: f64,
- compute_x: impl Fn(i64) -> f64,
- compute_y: impl Fn(f64) -> f64,
-) -> String {
- let mut y0 = compute_y(0.0);
- if min_data > 0.0 {
- y0 = compute_y(min_data)
- }
- if max_data < 0.0 {
- y0 = compute_y(max_data)
- }
+pub fn compute_fill_path(time_data: &[i64], values: &[f64], graph_space: &GraphSpace) -> String {
+ let y0 = graph_space.compute_y(
+ 0.0_f64
+ .max(graph_space.graph_data.data_min)
+ .min(graph_space.graph_data.data_max),
+ );
let mut path = String::new();
let mut last_undefined = true;
for i in 0..time_data.len() {
let t = time_data[i];
let value = *values.get(i).unwrap_or(&f64::NAN);
- let x = compute_x(t);
+ let x = graph_space.compute_x(t);
if last_undefined {
if value.is_nan() {
@@ -84,7 +73,7 @@ pub fn compute_fill_path(
} else if value.is_nan() {
last_undefined = true;
let x = if i > 0 {
- compute_x(time_data[i - 1])
+ graph_space.compute_x(time_data[i - 1])
} else {
x
};
@@ -92,13 +81,13 @@ pub fn compute_fill_path(
continue;
}
- let y = compute_y(value);
+ let y = graph_space.compute_y(value);
path.push_str(&format!(" L {:.1} {:.1}", x, y));
}
if let Some(t) = time_data.last() {
if !last_undefined {
- let x = compute_x(*t);
+ let x = graph_space.compute_x(*t);
path.push_str(&format!(" L {:.1} {:.1}", x, y0));
}
}
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 17/20] rrd: precalculate the grid line and label positions
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
` (15 preceding siblings ...)
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 16/20] rrd: introduce GraphSpace struct and use it to precalculate graph data Dominik Csapak
@ 2025-05-30 12:21 ` Dominik Csapak
2025-05-30 12:22 ` [yew-devel] [PATCH yew-comp 18/20] rrd: calculate series svg data only when necessary Dominik Csapak
` (2 subsequent siblings)
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:21 UTC (permalink / raw)
To: yew-devel
Introduce a new `RrdGrid` struct, that calculates the correct positions
of the grid lines and label positions with the help of a `GraphSpace`
struct.
Since the calculation is now split off from the creation of the
component, the code becomes much clearer, and the code in the main graph
module becomes much smaller.
It saves only the base data, not the actual component themselves, they
are generated via helper methods on the fly.
This should speed up all operations on the graph that don't need to
update the grid lines, like moving the mouse cursor over the data.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/rrd/graph.rs | 98 +++++--------------------------
src/rrd/grid.rs | 147 +++++++++++++++++++++++++++++++++++++++++++++++
src/rrd/mod.rs | 2 +
3 files changed, 164 insertions(+), 83 deletions(-)
create mode 100644 src/rrd/grid.rs
diff --git a/src/rrd/graph.rs b/src/rrd/graph.rs
index a02e086..7845dc3 100644
--- a/src/rrd/graph.rs
+++ b/src/rrd/graph.rs
@@ -128,13 +128,14 @@ pub struct PwtRRDGraph {
y_label_ref: NodeRef,
serie0_visible: bool,
serie1_visible: bool,
+ grid: RrdGrid,
}
-use pwt::widget::canvas::{Canvas, Circle, Group, Path, Rect, SvgLength, Text};
+use pwt::widget::canvas::{Canvas, Circle, Group, Path, Rect};
use super::graph_space::{CoordinateRange, GraphSpace};
+use super::grid::RrdGrid;
use super::series::{compute_fill_path, compute_outline_path};
-use super::units::GraphKeyData;
use super::Series;
fn format_date_time(t: i64) -> String {
@@ -187,6 +188,7 @@ impl PwtRRDGraph {
let (time_data, data1, data2) = self.get_view_data(ctx);
self.graph_space
.update(time_data, &[data1, data2], props.include_zero, props.binary);
+ self.grid = RrdGrid::new(&self.graph_space);
}
fn get_view_data<'a>(&self, ctx: &'a Context<Self>) -> (&'a [i64], &'a [f64], &'a [f64]) {
@@ -222,88 +224,14 @@ impl PwtRRDGraph {
let (data0, data1, data2) = self.get_view_data(ctx);
- let GraphKeyData {
- data_min,
- data_max,
- data_interval,
- time_max,
- time_interval,
- start_time,
- ..
- } = self.graph_space.graph_data;
-
- let mut grid_path = String::new();
-
- let mut value_labels: Vec<Html> = Vec::new();
- let mut time_labels: Vec<Html> = Vec::new();
-
- if !data0.is_empty() {
- let (x0, x1) = self.graph_space.get_x_range(CoordinateRange::OutsideBorder);
-
- let mut v = data_min;
- while v <= data_max {
- let y = self.graph_space.compute_y(v);
- grid_path.push_str(&format!("M {:.1} {:.1} L {:.1} {:.1}", x0, y, x1, y));
-
- let label = render_value(props, v);
- value_labels.push(
- Text::new(label)
- .class("pwt-rrd-label-text")
- .position(x0 as f32, y as f32)
- .dy(SvgLength::Px(4.0))
- .dx(SvgLength::Px(-4.0))
- .attribute("text-anchor", "end")
- .into(),
- );
-
- v += data_interval;
- }
-
- let mut t = start_time;
- let (ymin, ymax) = self.graph_space.get_y_range(CoordinateRange::OutsideBorder);
-
- let mut last_date = String::new();
-
- while t <= time_max {
- let x = self.graph_space.compute_x(t);
- grid_path.push_str(&format!("M {:.1} {:.1} L {:.1} {:.1}", x, ymin, x, ymax));
-
- let (time, date) = format_time(t);
-
- time_labels.push(
- Text::new(time)
- .class("pwt-rrd-label-text")
- .position(x as f32, ymin as f32)
- .dy(SvgLength::Px(10.0))
- .attribute("text-anchor", "middle")
- .into(),
- );
-
- if date != last_date {
- time_labels.push(
- Text::new(date.clone())
- .class("pwt-rrd-label-text")
- .position(x as f32, ymin as f32)
- .dy(SvgLength::Px(10.0 + 16.0))
- .attribute("text-anchor", "middle")
- .into(),
- );
-
- last_date = date;
- }
-
- t += time_interval;
- }
- }
let mut children: Vec<Html> = Vec::new();
- children.push(
- Path::new()
- .key("grid")
- .class("pwt-rrd-grid")
- .d(grid_path)
- .into(),
- );
+ // draw grid and labels
+ let (value_labels, time_labels) = self
+ .grid
+ .to_label_list(|x| render_value(props, x), format_time);
+
+ children.push(self.grid.to_path().key("grid").into());
children.push(Group::new().key("time-labels").children(time_labels).into());
children.push(
@@ -497,11 +425,14 @@ impl Component for PwtRRDGraph {
fn create(ctx: &Context<Self>) -> Self {
ctx.link().send_message(Msg::Reload);
+ let graph_space = GraphSpace::default();
+ let grid = RrdGrid::new(&graph_space);
+
let mut this = Self {
node_ref: NodeRef::default(),
size_observer: None,
canvas_ref: NodeRef::default(),
- graph_space: GraphSpace::default(),
+ graph_space,
selection: None,
view_range: None,
captured_pointer_id: None,
@@ -511,6 +442,7 @@ impl Component for PwtRRDGraph {
y_label_ref: NodeRef::default(),
serie0_visible: true,
serie1_visible: true,
+ grid,
};
this.update_grid_content(ctx);
diff --git a/src/rrd/grid.rs b/src/rrd/grid.rs
new file mode 100644
index 0000000..dbad2c5
--- /dev/null
+++ b/src/rrd/grid.rs
@@ -0,0 +1,147 @@
+use pwt::{
+ props::WidgetBuilder,
+ widget::canvas::{Path, SvgLength, Text},
+};
+use yew::Html;
+
+use super::graph_space::{CoordinateRange, GraphSpace};
+
+/// Holds the coordinates and path for the rrd grid and labels, so we don't have to recalculate
+/// them too often
+pub(crate) struct RrdGrid {
+ x_points: XPoints,
+ y_points: YPoints,
+ grid_path: String,
+
+ x0: f64,
+ y0: f64,
+}
+
+pub type XPoints = Vec<(f64, i64)>;
+pub type YPoints = Vec<(f64, f64)>;
+
+impl RrdGrid {
+ /// Calculates the correct points for the grid and labels with help from [`GraphSpace`]
+ pub fn new(graph_space: &GraphSpace) -> Self {
+ let (x_points, y_points) = Self::calculate_grid_points(graph_space);
+ let grid_path = Self::create_svg_path(&x_points, &y_points, graph_space);
+
+ let (x0, _) = graph_space.get_x_range(CoordinateRange::OutsideBorder);
+ let (y0, _) = graph_space.get_y_range(CoordinateRange::OutsideBorder);
+
+ Self {
+ x_points,
+ y_points,
+ grid_path,
+ x0,
+ y0,
+ }
+ }
+
+ /// returns a [`Path`] object for the RRD grid
+ pub fn to_path(&self) -> Path {
+ Path::new().class("pwt-rrd-grid").d(self.grid_path.clone())
+ }
+
+ /// Returns a list of labels (using a [`Text`] component) for the values (y axis) and points in time (x axis)
+ pub fn to_label_list<R, T>(&self, render_value: R, format_time: T) -> (Vec<Html>, Vec<Html>)
+ where
+ R: Fn(f64) -> String,
+ T: Fn(i64) -> (String, String),
+ {
+ let mut value_labels: Vec<Html> = Vec::new();
+ let mut time_labels: Vec<Html> = Vec::new();
+
+ for (y, v) in &self.y_points {
+ let label = render_value(*v);
+ value_labels.push(
+ Text::new(label)
+ .class("pwt-rrd-label-text")
+ .position(self.x0 as f32, *y as f32)
+ .dy(SvgLength::Px(4.0))
+ .dx(SvgLength::Px(-4.0))
+ .attribute("text-anchor", "end")
+ .into(),
+ );
+ }
+
+ let mut last_date = String::new();
+ for (x, t) in &self.x_points {
+ let (time, date) = format_time(*t);
+
+ time_labels.push(
+ Text::new(time)
+ .class("pwt-rrd-label-text")
+ .position(*x as f32, self.y0 as f32)
+ .dy(SvgLength::Px(10.0))
+ .attribute("text-anchor", "middle")
+ .into(),
+ );
+
+ if date != last_date {
+ time_labels.push(
+ Text::new(date.clone())
+ .class("pwt-rrd-label-text")
+ .position(*x as f32, self.y0 as f32)
+ .dy(SvgLength::Px(10.0 + 16.0))
+ .attribute("text-anchor", "middle")
+ .into(),
+ );
+
+ last_date = date;
+ }
+ }
+
+ (value_labels, time_labels)
+ }
+
+ // maps the coordinates of the grid points from data space to svg space
+ fn calculate_grid_points(graph_space: &GraphSpace) -> (XPoints, YPoints) {
+ let mut x_points = Vec::new();
+ let mut y_points = Vec::new();
+
+ let parameters = &graph_space.graph_data;
+
+ let mut v = parameters.data_min;
+ if parameters.data_range > 0.0 {
+ while v <= parameters.data_max {
+ let y = graph_space.compute_y(v);
+ y_points.push((y, v));
+ v += parameters.data_interval;
+ }
+ }
+
+ let mut t = parameters.start_time;
+
+ if parameters.time_range > 0 {
+ while t <= parameters.time_max {
+ let x = graph_space.compute_x(t);
+ x_points.push((x, t));
+ t += parameters.time_interval;
+ }
+ }
+ (x_points, y_points)
+ }
+
+ // creates the svg path for the grid lines
+ fn create_svg_path(
+ x_points: &[(f64, i64)],
+ y_points: &[(f64, f64)],
+ graph_space: &GraphSpace,
+ ) -> String {
+ let mut grid_path = String::new();
+
+ let (x0, x1) = graph_space.get_x_range(CoordinateRange::OutsideBorder);
+ let (y0, y1) = graph_space.get_y_range(CoordinateRange::OutsideBorder);
+
+ for (y, _) in y_points {
+ grid_path.push_str(&format!("M {:.1} {:.1} L {:.1} {:.1}", x0, y, x1, y));
+ }
+
+ for (x, _) in x_points {
+ grid_path.push_str(&format!("M {:.1} {:.1} L {:.1} {:.1}", x, y0, x, y1));
+ }
+
+ grid_path
+ }
+}
diff --git a/src/rrd/mod.rs b/src/rrd/mod.rs
index b55ac3f..5bcc9be 100644
--- a/src/rrd/mod.rs
+++ b/src/rrd/mod.rs
@@ -3,6 +3,8 @@ pub use graph::*;
pub(crate) mod graph_space;
+pub(crate) mod grid;
+
pub(crate) mod series;
pub use series::Series;
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 18/20] rrd: calculate series svg data only when necessary
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
` (16 preceding siblings ...)
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 17/20] rrd: precalculate the grid line and label positions Dominik Csapak
@ 2025-05-30 12:22 ` Dominik Csapak
2025-05-30 12:22 ` [yew-devel] [PATCH yew-comp 19/20] rrd: refactor selection rectangle calculation Dominik Csapak
2025-05-30 12:22 ` [yew-devel] [PATCH yew-comp 20/20] rrd: refactor the cross position calculation Dominik Csapak
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:22 UTC (permalink / raw)
To: yew-devel
namely when the grid data changes. This change also prepares the code a
bit for having more than two series, by being able to draw any amount of
precalculated series paths.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/rrd/graph.rs | 61 ++++++++++++++++++++++++++----------------------
1 file changed, 33 insertions(+), 28 deletions(-)
diff --git a/src/rrd/graph.rs b/src/rrd/graph.rs
index 7845dc3..c54d288 100644
--- a/src/rrd/graph.rs
+++ b/src/rrd/graph.rs
@@ -129,6 +129,7 @@ pub struct PwtRRDGraph {
serie0_visible: bool,
serie1_visible: bool,
grid: RrdGrid,
+ series_paths: Vec<Option<(String, String)>>, //outline path, fill path
}
use pwt::widget::canvas::{Canvas, Circle, Group, Path, Rect};
@@ -189,6 +190,24 @@ impl PwtRRDGraph {
self.graph_space
.update(time_data, &[data1, data2], props.include_zero, props.binary);
self.grid = RrdGrid::new(&self.graph_space);
+
+ let mut paths = Vec::new();
+ if self.serie0_visible {
+ let outline_path = compute_outline_path(time_data, data1, &self.graph_space);
+ let fill_path = compute_fill_path(time_data, data1, &self.graph_space);
+ paths.push(Some((outline_path, fill_path)));
+ } else {
+ paths.push(None);
+ }
+ if self.serie1_visible {
+ let outline_path = compute_outline_path(time_data, data2, &self.graph_space);
+ let fill_path = compute_fill_path(time_data, data2, &self.graph_space);
+ paths.push(Some((outline_path, fill_path)));
+ } else {
+ paths.push(None);
+ }
+
+ self.series_paths = paths;
}
fn get_view_data<'a>(&self, ctx: &'a Context<Self>) -> (&'a [i64], &'a [f64], &'a [f64]) {
@@ -242,38 +261,23 @@ impl PwtRRDGraph {
.into(),
);
- if self.serie0_visible && props.serie0.is_some() {
- let path = compute_outline_path(data0, data1, &self.graph_space);
- let pos_fill_path = compute_fill_path(data0, data1, &self.graph_space);
-
- children.extend(vec![
- Path::new()
- .key("series0-path")
- .class("pwt-rrd-outline-path1")
- .d(path)
- .into(),
- Path::new()
- .key("series0-fill")
- .class("pwt-rrd-fill-path1")
- .d(pos_fill_path)
- .into(),
- ]);
- }
-
- if self.serie1_visible && props.serie1.is_some() {
- let path = compute_outline_path(data0, data2, &self.graph_space);
- let pos_fill_path = compute_fill_path(data0, data2, &self.graph_space);
-
+ // draw series
+ for (idx, series) in self.series_paths.iter().enumerate() {
+ let idx = idx + 1;
+ let (outline_path, fill_path) = match series {
+ Some(res) => res,
+ None => continue,
+ };
children.extend(vec![
Path::new()
- .key("series1-path")
- .class("pwt-rrd-outline-path2")
- .d(path)
+ .key(format!("series{idx}-path"))
+ .class(format!("pwt-rrd-outline-path{idx}"))
+ .d(outline_path.to_string())
.into(),
Path::new()
- .key("series1-fill")
- .class("pwt-rrd-fill-path2")
- .d(pos_fill_path)
+ .key(format!("series{idx}-fill"))
+ .class(format!("pwt-rrd-fill-path{idx}"))
+ .d(fill_path.to_string())
.into(),
]);
}
@@ -443,6 +447,7 @@ impl Component for PwtRRDGraph {
serie0_visible: true,
serie1_visible: true,
grid,
+ series_paths: Vec::new(),
};
this.update_grid_content(ctx);
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 19/20] rrd: refactor selection rectangle calculation
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
` (17 preceding siblings ...)
2025-05-30 12:22 ` [yew-devel] [PATCH yew-comp 18/20] rrd: calculate series svg data only when necessary Dominik Csapak
@ 2025-05-30 12:22 ` Dominik Csapak
2025-05-30 12:22 ` [yew-devel] [PATCH yew-comp 20/20] rrd: refactor the cross position calculation Dominik Csapak
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:22 UTC (permalink / raw)
To: yew-devel
makes the view method a bit easier to read
we can omit the subtraction for start and end here, since the selection
must be int he correct range anyway.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/rrd/graph.rs | 42 +++++++++++++++++++++++++-----------------
1 file changed, 25 insertions(+), 17 deletions(-)
diff --git a/src/rrd/graph.rs b/src/rrd/graph.rs
index c54d288..5ffc29d 100644
--- a/src/rrd/graph.rs
+++ b/src/rrd/graph.rs
@@ -282,29 +282,18 @@ impl PwtRRDGraph {
]);
}
+ // draw selection rectangle
if let Some((start, end)) = &self.selection {
- let start = (*start).min(data0.len() - 1);
- let end = (*end).min(data0.len() - 1);
-
- match (data0.get(start), data0.get(end)) {
+ match (data0.get(*start), data0.get(*end)) {
(Some(start_data), Some(end_data)) => {
- let mut start_x = self.graph_space.compute_x(*start_data);
- let mut end_x = self.graph_space.compute_x(*end_data);
-
- if start_x > end_x {
- std::mem::swap(&mut start_x, &mut end_x);
- }
-
- let (start_y, end_y) =
- self.graph_space.get_y_range(CoordinateRange::InsideBorder);
-
+ let (x, y, width, height) = self.get_selection_rect(*start_data, *end_data);
children.push(
Rect::new()
.key("selection-rect")
.class("pwt-rrd-selection")
- .position(start_x as f32, end_y as f32)
- .width((end_x - start_x) as f32)
- .height((start_y - end_y) as f32)
+ .position(x, y)
+ .width(width)
+ .height(height)
.into(),
);
}
@@ -397,6 +386,25 @@ impl PwtRRDGraph {
.into()
}
+ // returns x, y, width, height
+ fn get_selection_rect(&self, start: i64, end: i64) -> (f32, f32, f32, f32) {
+ let mut start_x = self.graph_space.compute_x(start);
+ let mut end_x = self.graph_space.compute_x(end);
+
+ if start_x > end_x {
+ std::mem::swap(&mut start_x, &mut end_x);
+ }
+
+ let (start_y, end_y) = self.graph_space.get_y_range(CoordinateRange::InsideBorder);
+
+ (
+ start_x as f32,
+ end_y as f32,
+ (end_x - start_x) as f32,
+ (start_y - end_y) as f32,
+ )
+ }
+
fn offset_to_time_index(&self, x: i32, data0: &[i64]) -> usize {
let t = self.graph_space.original_x(x as f64);
let start_index = data0.partition_point(|&x| x < t);
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
* [yew-devel] [PATCH yew-comp 20/20] rrd: refactor the cross position calculation
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
` (18 preceding siblings ...)
2025-05-30 12:22 ` [yew-devel] [PATCH yew-comp 19/20] rrd: refactor selection rectangle calculation Dominik Csapak
@ 2025-05-30 12:22 ` Dominik Csapak
19 siblings, 0 replies; 21+ messages in thread
From: Dominik Csapak @ 2025-05-30 12:22 UTC (permalink / raw)
To: yew-devel
makes the view method a bit easier to read.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/rrd/graph.rs | 85 ++++++++++++++++++++++++++----------------------
1 file changed, 47 insertions(+), 38 deletions(-)
diff --git a/src/rrd/graph.rs b/src/rrd/graph.rs
index 5ffc29d..4c43039 100644
--- a/src/rrd/graph.rs
+++ b/src/rrd/graph.rs
@@ -302,51 +302,25 @@ impl PwtRRDGraph {
}
}
+ // draw cross and data circles
if let Some((x, y)) = self.cross_pos {
- let idx = self.offset_to_time_index(x, data0);
-
- if let Some(t) = data0.get(idx) {
- if let Some(v) = data1.get(idx) {
- if v.is_finite() {
- let px = self.graph_space.compute_x(*t) as f32;
- let py = self.graph_space.compute_y(*v) as f32;
- children.push(
- Circle::new()
- .key("selection-circle1")
- .class("pwt-rrd-selected-datapoint")
- .position(px, py)
- .r(5)
- .into(),
- );
- }
- }
- if let Some(v) = data2.get(idx) {
- if v.is_finite() {
- let px = self.graph_space.compute_x(*t) as f32;
- let py = self.graph_space.compute_y(*v) as f32;
- children.push(
- Circle::new()
- .key("selection-circle2")
- .class("pwt-rrd-selected-datapoint")
- .position(px, py)
- .r(5)
- .into(),
- );
- }
- }
+ let (path, circles) = self.get_cross_positions(data0, &[data1, data2], x, y);
+ for (idx, (px, py)) in circles.into_iter().enumerate() {
+ children.push(
+ Circle::new()
+ .key(format!("selection-circle{idx}"))
+ .class("pwt-rrd-selected-datapoint")
+ .position(px, py)
+ .r(5)
+ .into(),
+ );
}
- let (min_y, _) = self.graph_space.get_y_range(CoordinateRange::InsideBorder);
- let (min_x, max_x) = self.graph_space.get_x_range(CoordinateRange::InsideBorder);
-
- let x = x.max(min_x as i32).min(max_x as i32);
- let y = y.min(min_y as i32);
-
children.push(
Path::new()
.key("cross")
.class("pwt-rrd-cross")
- .d(format!("M {x} 0 L {x} {min_y} M {min_x} {y} L {max_x} {y}"))
+ .d(path)
.into(),
);
}
@@ -405,6 +379,41 @@ impl PwtRRDGraph {
)
}
+ // returns the path for the cross and the positions of the circles for the series data points
+ fn get_cross_positions(
+ &self,
+ data0: &[i64],
+ series: &[&[f64]],
+ x: i32,
+ y: i32,
+ ) -> (String, Vec<(f32, f32)>) {
+ let idx = self.offset_to_time_index(x, data0);
+
+ let mut children = Vec::new();
+
+ if let Some(t) = data0.get(idx) {
+ for data in series {
+ if let Some(v) = data.get(idx) {
+ if v.is_finite() {
+ let px = self.graph_space.compute_x(*t) as f32;
+ let py = self.graph_space.compute_y(*v) as f32;
+ children.push((px, py));
+ }
+ }
+ }
+ }
+
+ let (min_y, _) = self.graph_space.get_y_range(CoordinateRange::InsideBorder);
+ let (min_x, max_x) = self.graph_space.get_x_range(CoordinateRange::InsideBorder);
+
+ let x = x.max(min_x as i32).min(max_x as i32);
+ let y = y.min(min_y as i32);
+
+ let path = format!("M {x} 0 L {x} {min_y} M {min_x} {y} L {max_x} {y}");
+
+ (path, children)
+ }
+
fn offset_to_time_index(&self, x: i32, data0: &[i64]) -> usize {
let t = self.graph_space.original_x(x as f64);
let start_index = data0.partition_point(|&x| x < t);
--
2.39.5
_______________________________________________
yew-devel mailing list
yew-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel
^ permalink raw reply [flat|nested] 21+ messages in thread
end of thread, other threads:[~2025-05-30 12:22 UTC | newest]
Thread overview: 21+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-05-30 12:21 [yew-devel] [PATCH yew-comp 00/20] refactor and improve rrd graph code Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 01/20] remove old rrd uplot code Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 02/20] rrd: refactor code for compute_min_max Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 03/20] rrd: move into own module Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 04/20] rrd: move unit calculation to " Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 05/20] rrd: units: add tests Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 06/20] rrd: units: simplify calculations for get_grid_unit_base Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 07/20] rrd: remove unnecessary `no_data` field Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 08/20] rrd: align tooltip directly to pointer position Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 09/20] rrd: use 'cross_pos' state instead of 'draw_cross' Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 10/20] rrd: give all elements in svg keys Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 11/20] rrd: simplify toggle Msg Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 12/20] rrd: remove wrongly annotated lifetime Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 13/20] rrd: refactor series related struct and functions into own module Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 14/20] rrd: clamp view range when time_data changes Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 15/20] rrd: refactor grid data computation Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 16/20] rrd: introduce GraphSpace struct and use it to precalculate graph data Dominik Csapak
2025-05-30 12:21 ` [yew-devel] [PATCH yew-comp 17/20] rrd: precalculate the grid line and label positions Dominik Csapak
2025-05-30 12:22 ` [yew-devel] [PATCH yew-comp 18/20] rrd: calculate series svg data only when necessary Dominik Csapak
2025-05-30 12:22 ` [yew-devel] [PATCH yew-comp 19/20] rrd: refactor selection rectangle calculation Dominik Csapak
2025-05-30 12:22 ` [yew-devel] [PATCH yew-comp 20/20] rrd: refactor the cross position calculation Dominik Csapak
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal