From: Dominik Csapak <d.csapak@proxmox.com>
To: yew-devel@lists.proxmox.com
Subject: [yew-devel] [PATCH yew-comp 15/20] rrd: refactor grid data computation
Date: Fri, 30 May 2025 14:21:57 +0200 [thread overview]
Message-ID: <20250530122202.2779300-16-d.csapak@proxmox.com> (raw)
In-Reply-To: <20250530122202.2779300-1-d.csapak@proxmox.com>
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
next prev parent reply other threads:[~2025-05-30 12:22 UTC|newest]
Thread overview: 21+ messages / expand[flat|nested] mbox.gz Atom feed top
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 ` Dominik Csapak [this message]
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
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20250530122202.2779300-16-d.csapak@proxmox.com \
--to=d.csapak@proxmox.com \
--cc=yew-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal