sensor_core/
graph_renderer.rs1use std::io::{BufWriter, Cursor};
2
3use image::{ImageBuffer, Rgba, RgbaImage};
4
5use crate::{hex_to_rgba, GraphConfig, GraphType};
6
7pub fn render(graph_config: &GraphConfig) -> Vec<u8> {
13 let width = graph_config.width;
14 let height = graph_config.height;
15
16 let graph_data = prepare_graph_data(width, &graph_config.sensor_values);
18
19 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 if !graph_config.border_color.ends_with("00") {
27 draw_border(&mut image, &graph_config.border_color, width, height);
28 }
29
30 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
39fn 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
58fn prepare_graph_data(width: u32, sensor_values: &Vec<f64>) -> Vec<f64> {
61 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 let mut plot_data: Vec<f64> = vec![0.0; (width) as usize];
70
71 plot_data.splice(
73 (width - sensor_values.len() as u32) as usize..,
74 sensor_values,
75 );
76 plot_data
77}
78
79fn 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 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 let img_line_start = current_value_normalized * height as f64;
102 let img_line_end = next_value_normalized * height as f64;
103
104 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 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
125fn 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 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 let img_line_start = current_value_normalized * height as f64;
149 let img_line_end = next_value_normalized * height as f64;
150
151 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 let mut y = y0;
161 while y < height {
162 image.put_pixel(x0 as u32, y, fill_color);
163 y += 1;
164 }
165
166 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
180fn 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
191fn 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}