From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: <yew-devel-bounces@lists.proxmox.com> Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 3A3551FF1CD for <inbox@lore.proxmox.com>; Fri, 30 May 2025 14:22:20 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 3A8792E74F; Fri, 30 May 2025 14:22:36 +0200 (CEST) From: Dominik Csapak <d.csapak@proxmox.com> To: yew-devel@lists.proxmox.com Date: Fri, 30 May 2025 14:21:57 +0200 Message-Id: <20250530122202.2779300-16-d.csapak@proxmox.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20250530122202.2779300-1-d.csapak@proxmox.com> References: <20250530122202.2779300-1-d.csapak@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.022 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [yew-devel] [PATCH yew-comp 15/20] rrd: refactor grid data computation X-BeenThere: yew-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Yew framework devel list at Proxmox <yew-devel.lists.proxmox.com> List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/yew-devel>, <mailto:yew-devel-request@lists.proxmox.com?subject=unsubscribe> List-Archive: <http://lists.proxmox.com/pipermail/yew-devel/> List-Post: <mailto:yew-devel@lists.proxmox.com> List-Help: <mailto:yew-devel-request@lists.proxmox.com?subject=help> List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/yew-devel>, <mailto:yew-devel-request@lists.proxmox.com?subject=subscribe> Reply-To: Yew framework devel list at Proxmox <yew-devel@lists.proxmox.com> Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: yew-devel-bounces@lists.proxmox.com Sender: "yew-devel" <yew-devel-bounces@lists.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