sensor_core/
graph_renderer.rs

1use std::io::{BufWriter, Cursor};
2
3use image::{ImageBuffer, Rgba, RgbaImage};
4
5use crate::{hex_to_rgba, GraphConfig, GraphType};
6
7/// Renders a graph based on the given config
8/// # Returns
9/// A vector of bytes containing the RGB8 png image
10/// # Arguments
11/// * `graph_config` - The config for the graph
12pub fn render(graph_config: &GraphConfig) -> Vec<u8> {
13    let width = graph_config.width;
14    let height = graph_config.height;
15
16    // Prepare the data for the graph
17    let graph_data = prepare_graph_data(width, &graph_config.sensor_values);
18
19    // Render the graph
20    let mut image = match graph_config.graph_type {
21        GraphType::Line => render_line_chart(&graph_data, graph_config),
22        GraphType::LineFill => render_line_chart_filled(&graph_data, graph_config),
23    };
24
25    // Draw border if border is visible
26    if !graph_config.border_color.ends_with("00") {
27        draw_border(&mut image, &graph_config.border_color, width, height);
28    }
29
30    // Encode to png and return encoded bytes
31    let mut writer = BufWriter::new(Cursor::new(Vec::new()));
32    image
33        .write_to(&mut writer, image::ImageOutputFormat::Png)
34        .unwrap();
35
36    writer.into_inner().unwrap().into_inner()
37}
38
39/// Draws a border around the specified image
40fn draw_border(
41    image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
42    border_color: &str,
43    width: u32,
44    height: u32,
45) {
46    let border_color = hex_to_rgba(border_color);
47    let border_x: i32 = 0;
48    let border_y: i32 = 0;
49    let border_width: u32 = width;
50    let border_height: u32 = height;
51    imageproc::drawing::draw_hollow_rect_mut(
52        image,
53        imageproc::rect::Rect::at(border_x, border_y).of_size(border_width, border_height),
54        border_color,
55    );
56}
57
58/// Prepares the plot data for the graph.
59/// Aligns the sensor values to the width of the desired graph width.
60fn prepare_graph_data(width: u32, sensor_values: &Vec<f64>) -> Vec<f64> {
61    // Ensure that sensor values does not exceed the width, if so cut them and keep the last values
62    let sensor_values = if sensor_values.len() > width as usize {
63        sensor_values[(sensor_values.len() - width as usize)..].to_vec()
64    } else {
65        sensor_values.to_vec()
66    };
67
68    // Create a new vector for the width of the image, initialize with 0
69    let mut plot_data: Vec<f64> = vec![0.0; (width) as usize];
70
71    // Set the gen values to the end of plat data
72    plot_data.splice(
73        (width - sensor_values.len() as u32) as usize..,
74        sensor_values,
75    );
76    plot_data
77}
78
79/// Renders a graph based on the given config
80fn render_line_chart(numbers: &[f64], config: &GraphConfig) -> RgbaImage {
81    let width = config.width;
82    let height = config.height;
83    let min_value = config.min_sensor_value.unwrap_or(get_min(numbers));
84    let max_value = config.max_sensor_value.unwrap_or(get_max(numbers));
85    let line_width = config.graph_stroke_width;
86    let line_color = hex_to_rgba(&config.graph_color);
87    let background_color = hex_to_rgba(&config.background_color);
88
89    let mut image = RgbaImage::from_pixel(width, height, background_color);
90    let half_line_width = (line_width / 2) as f32;
91
92    for i in 0..numbers.len() - 1 {
93        let current_value = numbers[i];
94        let next_value = numbers[i + 1];
95
96        // First move value between 0 and 1, where min_value is the lower bound and max_value the upper bound
97        let current_value_normalized = (current_value - min_value) / (max_value - min_value);
98        let next_value_normalized = (next_value - min_value) / (max_value - min_value);
99
100        // Then move the value between 0 and height
101        let img_line_start = current_value_normalized * height as f64;
102        let img_line_end = next_value_normalized * height as f64;
103
104        // Render line on image
105        let x0 = i;
106        let y0 = height as f64 - img_line_start;
107
108        let x1 = i + 1;
109        let y1 = height as f64 - img_line_end;
110
111        // Draw graph line
112        for offset in -half_line_width as i32..=half_line_width as i32 {
113            imageproc::drawing::draw_line_segment_mut(
114                &mut image,
115                ((x0 as f32) + offset as f32, y0 as f32),
116                ((x1 as f32) + offset as f32, y1 as f32),
117                line_color,
118            );
119        }
120    }
121
122    image
123}
124
125/// Renders a graph based on the given config
126fn render_line_chart_filled(numbers: &[f64], config: &GraphConfig) -> RgbaImage {
127    let width = config.width;
128    let height = config.height;
129    let min_value = config.min_sensor_value.unwrap_or(get_min(numbers));
130    let max_value = config.max_sensor_value.unwrap_or(get_max(numbers));
131    let line_width = config.graph_stroke_width;
132    let line_color = hex_to_rgba(&config.graph_color);
133    let fill_color = line_color;
134    let background_color = hex_to_rgba(&config.background_color);
135
136    let mut image = RgbaImage::from_pixel(width, height, background_color);
137    let half_line_width = (line_width / 2) as f32;
138
139    for i in 0..numbers.len() - 1 {
140        let current_value = numbers[i];
141        let next_value = numbers[i + 1];
142
143        // First move value between 0 and 1, where min_value is the lower bound and max_value the upper bound
144        let current_value_normalized = (current_value - min_value) / (max_value - min_value);
145        let next_value_normalized = (next_value - min_value) / (max_value - min_value);
146
147        // Then move the value between 0 and height
148        let img_line_start = current_value_normalized * height as f64;
149        let img_line_end = next_value_normalized * height as f64;
150
151        // Render line on image
152        // Avoid attempt to subtract with overflow by using saturating_sub
153        let x0 = i;
154        let y0 = height.saturating_sub(img_line_start as u32);
155
156        let x1 = i + 1;
157        let y1 = height.saturating_sub(img_line_end as u32);
158
159        // Fill the area under the line until image bottom, respect the line width
160        let mut y = y0;
161        while y < height {
162            image.put_pixel(x0 as u32, y, fill_color);
163            y += 1;
164        }
165
166        // Draw graph line
167        for offset in -half_line_width as i32..=half_line_width as i32 {
168            imageproc::drawing::draw_line_segment_mut(
169                &mut image,
170                ((x0 as f32) + offset as f32, y0 as f32),
171                ((x1 as f32) + offset as f32, y1 as f32),
172                line_color,
173            );
174        }
175    }
176
177    image
178}
179
180/// Returns the minimum value of the given vector
181fn get_min(values: &[f64]) -> f64 {
182    let mut min = values[0];
183    for value in values {
184        if *value < min {
185            min = *value;
186        }
187    }
188    min
189}
190
191/// Returns the maximum value of the given vector
192fn get_max(values: &[f64]) -> f64 {
193    let mut max = values[0];
194    for value in values {
195        if *value > max {
196            max = *value;
197        }
198    }
199    max
200}