lc_render/view/
line.rs

1use crate::color::{COLOR_HEX_BLUE_1, COLOR_HEX_BLUE_2};
2use crate::render::svg::*;
3use crate::shape::point::Point;
4use crate::{BandScale, Color, Error, LinearScale, PointLabelPosition, PointType, Scale, View};
5use svg::Node;
6
7const DEFAULT_LABEL_VISIBLE: bool = true;
8const DEFAULT_LABEL_POSITION: PointLabelPosition = PointLabelPosition::Top;
9
10const DEFAULT_POINT_TYPE: PointType = PointType::Circle;
11const DEFAULT_POINT_VISIBLE: bool = true;
12
13const DEFAULT_LINE_STROKE_WIDTH: i32 = 2;
14
15/// LineView represents a single line.
16#[derive(Clone)]
17pub struct LineView {
18    x_scale: BandScale,
19    y_scale: LinearScale,
20    stroke_color: String,
21    point_fill_color: String,
22    point_stroke_color: String,
23    points: Vec<Point>,
24    point_type: PointType,
25    point_visible: bool,
26    point_label_visible: bool,
27    point_label_position: PointLabelPosition,
28}
29
30impl LineView {
31    /// Create a new LineView.
32    pub fn new(x_scale: BandScale, y_scale: LinearScale) -> Self {
33        Self {
34            x_scale,
35            y_scale,
36            stroke_color: COLOR_HEX_BLUE_1.to_string(),
37            point_fill_color: COLOR_HEX_BLUE_2.to_string(),
38            point_stroke_color: COLOR_HEX_BLUE_1.to_string(),
39            points: Vec::new(),
40            point_type: DEFAULT_POINT_TYPE,
41            point_visible: DEFAULT_POINT_VISIBLE,
42            point_label_visible: DEFAULT_LABEL_VISIBLE,
43            point_label_position: DEFAULT_LABEL_POSITION,
44        }
45    }
46
47    /// Set line stroke color.
48    pub fn set_stroke_color(mut self, stroke_color: Color) -> Self {
49        self.stroke_color = stroke_color.to_string();
50        self
51    }
52
53    /// Set fill color for the point.
54    pub fn set_point_fill_color(mut self, point_fill_color: Color) -> Self {
55        self.point_fill_color = point_fill_color.to_string();
56        self
57    }
58
59    /// Set stroke color for the point.
60    pub fn set_point_stroke_color(mut self, point_stroke_color: Color) -> Self {
61        self.point_stroke_color = point_stroke_color.to_string();
62        self
63    }
64
65    /// Set type of the point.
66    pub fn set_point_type(mut self, point_type: PointType) -> Self {
67        self.point_type = point_type;
68        self
69    }
70
71    /// Set point visibility.
72    pub fn set_point_visible(mut self, point_visible: bool) -> Self {
73        self.point_visible = point_visible;
74        self
75    }
76
77    /// Set point label visibility.
78    pub fn set_point_label_visible(mut self, point_label_visible: bool) -> Self {
79        self.point_label_visible = point_label_visible;
80        self
81    }
82
83    /// Set label position for the point.
84    pub fn set_point_label_position(mut self, point_label_position: PointLabelPosition) -> Self {
85        self.point_label_position = point_label_position;
86        self
87    }
88
89    /// Set data for line points.
90    pub fn set_data(mut self, data: &[f32]) -> Result<Self, Error> {
91        if data.is_empty() {
92            return Err(Error::DataIsEmpty);
93        }
94        if data.len() != self.x_scale.ticks().len() {
95            return Err(Error::CategoriesCountDoesntEqual);
96        }
97
98        // Compute offsets in case there is a non-zero bandwidth.
99        let x_bandwidth_offset = {
100            if self.x_scale.is_range_reversed() {
101                -self.x_scale.tick_offset()
102            } else {
103                self.x_scale.tick_offset()
104            }
105        };
106        let y_bandwidth_offset = {
107            if self.y_scale.is_range_reversed() {
108                -self.y_scale.tick_offset()
109            } else {
110                self.y_scale.tick_offset()
111            }
112        };
113
114        let categories = self.x_scale.ticks();
115        let mut points = Vec::new();
116        for (idx, value) in data.iter().enumerate() {
117            let category = &categories[idx];
118            let scaled_x = &self.x_scale.scale(category);
119            let scaled_y = self.y_scale.scale(&value);
120
121            let point = Point::new(
122                scaled_x + x_bandwidth_offset,
123                scaled_y + y_bandwidth_offset,
124                self.point_type,
125                DEFAULT_POINT_SIZE,
126                &value.to_string(),
127                &self.point_fill_color.to_string(),
128                &self.point_stroke_color.to_string(),
129            )
130            .set_point_visible(self.point_visible)
131            .set_label_visible(self.point_label_visible)
132            .set_label_position(self.point_label_position);
133            points.push(point);
134        }
135        self.points = points;
136
137        Ok(self)
138    }
139}
140
141impl View for LineView {
142    /// Get line SVG representation.
143    fn to_svg(&self) -> svg::node::element::Group {
144        let mut res = svg::node::element::Group::new();
145        let mut data = svg::node::element::path::Data::new();
146
147        for (point_idx, point) in self.points.iter().enumerate() {
148            if point_idx == 0 {
149                data = data.move_to((point.x(), point.y()));
150            } else {
151                data = data.line_to((point.x(), point.y()));
152            }
153
154            res.append(point.to_svg());
155        }
156        let line = svg::node::element::Path::new()
157            .set(CLASS_ATTR, CLASS_LINE)
158            .set(FILL_ATTR, FILL_NONE)
159            .set(STROKE_ATTR, self.stroke_color.clone())
160            .set(STROKE_WIDTH_ATTR, DEFAULT_LINE_STROKE_WIDTH)
161            .set(D_ATTR, data);
162
163        res.append(line);
164
165        res
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use crate::Color;
173
174    #[test]
175    fn line_basic() {
176        let expected_svg_group = r##"<g>
177<g class="point" transform="translate(13.414631,99.01)">
178<g>
179<line stroke="#ffffff" stroke-width="2px" x1="-5" x2="5" y1="-5" y2="5"/>
180<line stroke="#ffffff" stroke-width="2px" x1="5" x2="-5" y1="-5" y2="5"/>
181</g>
182<text dy=".35em" fill="#080808" font-family="sans-serif" font-size="14px" text-anchor="middle" x="0" y="17">
18389.1
184</text>
185</g>
186<g class="point" transform="translate(37.804874,99.865555)">
187<g>
188<line stroke="#ffffff" stroke-width="2px" x1="-5" x2="5" y1="-5" y2="5"/>
189<line stroke="#ffffff" stroke-width="2px" x1="5" x2="-5" y1="-5" y2="5"/>
190</g>
191<text dy=".35em" fill="#080808" font-family="sans-serif" font-size="14px" text-anchor="middle" x="0" y="17">
19212.1
193</text>
194</g>
195<g class="point" transform="translate(62.195118,99.5)">
196<g>
197<line stroke="#ffffff" stroke-width="2px" x1="-5" x2="5" y1="-5" y2="5"/>
198<line stroke="#ffffff" stroke-width="2px" x1="5" x2="-5" y1="-5" y2="5"/>
199</g>
200<text dy=".35em" fill="#080808" font-family="sans-serif" font-size="14px" text-anchor="middle" x="0" y="17">
20145
202</text>
203</g>
204<g class="point" transform="translate(86.585365,99.76667)">
205<g>
206<line stroke="#ffffff" stroke-width="2px" x1="-5" x2="5" y1="-5" y2="5"/>
207<line stroke="#ffffff" stroke-width="2px" x1="5" x2="-5" y1="-5" y2="5"/>
208</g>
209<text dy=".35em" fill="#080808" font-family="sans-serif" font-size="14px" text-anchor="middle" x="0" y="17">
21021
211</text>
212</g>
213<path class="line" d="M13.414631,99.01 L37.804874,99.865555 L62.195118,99.5 L86.585365,99.76667" fill="none" stroke="#ff006c" stroke-width="2"/>
214</g>"##;
215
216        let x_scale = BandScale::new(
217            vec![
218                "a".to_string(),
219                "n1".to_string(),
220                "n2".to_string(),
221                "y".to_string(),
222            ],
223            0,
224            100,
225        );
226        let y_scale = LinearScale::new(0_f32, 9000_f32, 100, 0);
227        let data = vec![89.1_f32, 12.1_f32, 45_f32, 21_f32];
228        let line = LineView::new(x_scale, y_scale)
229            .set_point_label_position(PointLabelPosition::Bottom)
230            .set_stroke_color(Color::new_from_hex("#ff006c"))
231            .set_point_fill_color(Color::new_from_hex("#ffe5f5"))
232            .set_point_type(PointType::X)
233            .set_point_stroke_color(Color::new_from_hex("#ffffff"))
234            .set_data(&data)
235            .expect("unable to set data");
236        let line_svg = line.to_svg();
237        assert_eq!(line_svg.to_string(), expected_svg_group);
238    }
239}