freya_components/
graph.rs

1use dioxus::prelude::*;
2use freya_core::{
3    custom_attributes::CanvasRunnerContext,
4    parsing::Parse,
5};
6use freya_elements as dioxus_elements;
7use freya_engine::prelude::*;
8use freya_hooks::{
9    use_applied_theme,
10    use_canvas_with_deps,
11    use_node_signal,
12    use_platform,
13    GraphTheme,
14    GraphThemeWith,
15};
16
17/// Data line for the [`Graph`] component.
18#[derive(Debug, PartialEq, Clone)]
19pub struct GraphLine {
20    color: String,
21    points: Vec<Option<i32>>,
22}
23
24impl GraphLine {
25    pub fn new(color: &str, points: Vec<Option<i32>>) -> Self {
26        Self {
27            color: color.to_string(),
28            points,
29        }
30    }
31}
32
33/// Properties for the [`Graph`] component.
34#[derive(Debug, Props, PartialEq, Clone)]
35pub struct GraphProps {
36    /// Theme override.
37    pub theme: Option<GraphThemeWith>,
38    /// X axis labels.
39    labels: Vec<String>,
40    /// Y axis data.
41    data: Vec<GraphLine>,
42}
43
44/// Graph component.
45#[allow(non_snake_case)]
46pub fn Graph(props: GraphProps) -> Element {
47    let platform = use_platform();
48    let (reference, size) = use_node_signal();
49    let GraphTheme { width, height } = use_applied_theme!(&props.theme, graph);
50
51    let canvas = use_canvas_with_deps(&props, move |props| {
52        platform.invalidate_drawing_area(size.peek().area);
53        platform.request_animation_frame();
54        move |ctx: &mut CanvasRunnerContext| {
55            ctx.canvas.translate((ctx.area.min_x(), ctx.area.min_y()));
56
57            let mut paragraph_style = ParagraphStyle::default();
58            paragraph_style.set_text_align(TextAlign::Center);
59
60            let mut text_style = TextStyle::new();
61            text_style.set_color(Color::BLACK);
62            text_style.set_font_size(16. * ctx.scale_factor);
63            paragraph_style.set_text_style(&text_style);
64
65            let x_labels = &props.labels;
66            let x_height: f32 = 50.0;
67
68            let start_x = ctx.area.min_x();
69            let start_y = ctx.area.height() - x_height;
70            let height = ctx.area.height() - x_height;
71
72            let space_x = ctx.area.width() / x_labels.len() as f32;
73
74            // Calculate the smallest and biggest items
75            let (smallest_y, biggest_y) = {
76                let mut smallest_y = 0;
77                let mut biggest_y = 0;
78                for line in props.data.iter() {
79                    let max = line.points.iter().max().unwrap();
80                    let min = line.points.iter().min().unwrap();
81
82                    if let Some(max) = *max {
83                        if max > biggest_y {
84                            biggest_y = max;
85                        }
86                    }
87                    if let Some(min) = *min {
88                        if min < smallest_y {
89                            smallest_y = min;
90                        }
91                    }
92                }
93
94                (smallest_y, biggest_y)
95            };
96
97            // Difference between the smalles and biggest Y Axis item
98            let y_axis_len = biggest_y - smallest_y;
99            // Space between items in the Y axis
100            let space_y = height / y_axis_len as f32;
101
102            // Draw the lines
103            for line in &props.data {
104                let mut paint = Paint::default();
105
106                paint.set_anti_alias(true);
107                paint.set_style(PaintStyle::Fill);
108                paint.set_color(Color::parse(&line.color).unwrap());
109                paint.set_stroke_width(3.0 * ctx.scale_factor);
110
111                let mut previous_x = None;
112                let mut previous_y = None;
113
114                for (i, y_point) in line.points.iter().enumerate() {
115                    let line_x = (space_x * i as f32) + start_x + (space_x / 2.0);
116                    // Save the position where the last point drawed
117                    let new_previous_x = previous_x.unwrap_or(line_x);
118
119                    if let Some(y_point) = y_point {
120                        let line_y = start_y - (space_y * ((y_point - smallest_y) as f32));
121                        let new_previous_y = previous_y.unwrap_or(line_y);
122
123                        // Draw the line and circle
124                        ctx.canvas
125                            .draw_circle((line_x, line_y), 5.0 * ctx.scale_factor, &paint);
126                        ctx.canvas.draw_line(
127                            (new_previous_x, new_previous_y),
128                            (line_x, line_y),
129                            &paint,
130                        );
131
132                        previous_y = Some(line_y);
133                        previous_x = Some(line_x);
134                    } else {
135                        previous_y = None;
136                        previous_x = None;
137                    }
138                }
139            }
140
141            // Space between labels
142            let space_x = ctx.area.width() / x_labels.len() as f32;
143
144            // Draw the labels
145            for (i, point) in x_labels.iter().enumerate() {
146                let x = (space_x * i as f32) + start_x;
147
148                let mut paragrap_builder =
149                    ParagraphBuilder::new(&paragraph_style, ctx.font_collection.clone());
150                paragrap_builder.add_text(point);
151                let mut text = paragrap_builder.build();
152
153                text.layout(space_x);
154                text.paint(ctx.canvas, (x, start_y + x_height - 30.0));
155            }
156
157            ctx.canvas.restore();
158        }
159    });
160
161    rsx!(
162        rect {
163            width: "{width}",
164            height: "{height}",
165            padding: "15 5",
166            background: "white",
167            rect {
168                canvas_reference: canvas.attribute(),
169                reference,
170                width: "100%",
171                height: "100%",
172            }
173        }
174    )
175}