[yew-devel] [PATCH yew-comp 15/20] rrd: refactor grid data computation
Dominik Csapak
d.csapak at proxmox.com
Fri May 30 14:21:57 CEST 2025
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 at 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
More information about the yew-devel
mailing list