lc_render/shape/
point.rs

1use crate::render::svg::*;
2use svg::Node;
3
4const DEFAULT_LABEL_VISIBLE: bool = true;
5const DEFAULT_LABEL_POSITION: PointLabelPosition = PointLabelPosition::Top;
6
7const DEFAULT_STROKE_WIDTH: &str = "2px";
8
9const DEFAULT_X_LABEL_HORIZONTAL: i32 = 8;
10const DEFAULT_X_LABEL_VERTICAL: i32 = 0;
11const DEFAULT_X_LABEL_BETWEEN: i32 = 4;
12
13const DEFAULT_Y_LABEL_HORIZONTAL: i32 = 0;
14const DEFAULT_Y_LABEL_VERTICAL: i32 = 12;
15const DEFAULT_Y_LABEL_BETWEEN: i32 = 8;
16
17const DEFAULT_FONT_SIZE: &str = "14px";
18
19const DEFAULT_POINT_VISIBLE: bool = true;
20
21/// PointType contains available types of points.
22#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
23pub enum PointType {
24    Circle,
25    Square,
26    X,
27}
28
29/// PointLabelPosition contains available types of point label positions.
30#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
31pub enum PointLabelPosition {
32    Top,
33    TopRight,
34    TopLeft,
35    Left,
36    Right,
37    Bottom,
38    BottomLeft,
39    BottomRight,
40}
41
42/// Point represents a point shape.
43#[derive(Clone)]
44pub struct Point {
45    x: f32,
46    y: f32,
47    label_visible: bool,
48    label_position: PointLabelPosition,
49    point_visible: bool,
50    point_type: PointType,
51    size: i32,
52    x_label: String,
53    y_label: String,
54    fill_color: String,
55    stroke_color: String,
56    label_text_anchor: String,
57    label_x_attr: i32,
58    label_y_attr: i32,
59}
60
61impl Point {
62    /// Create a new Point.
63    pub fn new(
64        x: f32,
65        y: f32,
66        point_type: PointType,
67        size: i32,
68        y_label: &str,
69        fill_color: &str,
70        stroke_color: &str,
71    ) -> Self {
72        Point {
73            x,
74            y,
75            point_visible: DEFAULT_POINT_VISIBLE,
76            point_type,
77            size,
78            x_label: String::new(),
79            y_label: y_label.to_string(),
80            fill_color: fill_color.to_string(),
81            stroke_color: stroke_color.to_string(),
82            label_visible: DEFAULT_LABEL_VISIBLE,
83            label_position: DEFAULT_LABEL_POSITION,
84            label_text_anchor: Self::label_text_anchor(DEFAULT_LABEL_POSITION),
85            label_x_attr: Self::label_x_attr(DEFAULT_LABEL_POSITION, size),
86            label_y_attr: Self::label_y_attr(DEFAULT_LABEL_POSITION, size),
87        }
88    }
89
90    /// Set point visibility.
91    pub fn set_point_visible(mut self, point_visible: bool) -> Self {
92        self.point_visible = point_visible;
93        self
94    }
95
96    /// Set custom x for label.
97    pub fn set_x_label(mut self, x_label: &str) -> Self {
98        self.x_label = x_label.to_string();
99        self
100    }
101
102    /// Set point visibility.
103    pub fn set_label_visible(mut self, label_visible: bool) -> Self {
104        self.label_visible = label_visible;
105        self
106    }
107
108    /// Set position for point label.
109    pub fn set_label_position(mut self, label_position: PointLabelPosition) -> Self {
110        self.label_position = label_position;
111        self.label_text_anchor = Self::label_text_anchor(label_position);
112        self.label_x_attr = Self::label_x_attr(label_position, self.size);
113        self.label_y_attr = Self::label_y_attr(label_position, self.size);
114        self
115    }
116
117    /// Get x value of a point.
118    pub fn x(&self) -> f32 {
119        self.x
120    }
121    /// Get y x value of a point.
122    pub fn y(&self) -> f32 {
123        self.y
124    }
125
126    fn label_text_anchor(label_position: PointLabelPosition) -> String {
127        match label_position {
128            PointLabelPosition::Top | PointLabelPosition::Bottom => TEXT_ANCHOR_MIDDLE.to_string(),
129            PointLabelPosition::TopRight
130            | PointLabelPosition::BottomRight
131            | PointLabelPosition::Right => TEXT_ANCHOR_START.to_string(),
132            PointLabelPosition::TopLeft
133            | PointLabelPosition::BottomLeft
134            | PointLabelPosition::Left => TEXT_ANCHOR_END.to_string(),
135        }
136    }
137
138    fn label_x_attr(label_position: PointLabelPosition, size: i32) -> i32 {
139        match label_position {
140            PointLabelPosition::Top | PointLabelPosition::Bottom => DEFAULT_X_LABEL_VERTICAL,
141            PointLabelPosition::TopRight | PointLabelPosition::BottomRight => {
142                size + DEFAULT_X_LABEL_BETWEEN
143            }
144            PointLabelPosition::Right => size + DEFAULT_X_LABEL_HORIZONTAL,
145            PointLabelPosition::TopLeft | PointLabelPosition::BottomLeft => {
146                -size - DEFAULT_X_LABEL_BETWEEN
147            }
148            PointLabelPosition::Left => -size - DEFAULT_X_LABEL_HORIZONTAL,
149        }
150    }
151
152    fn label_y_attr(label_position: PointLabelPosition, size: i32) -> i32 {
153        match label_position {
154            PointLabelPosition::Top => -size - DEFAULT_Y_LABEL_VERTICAL,
155            PointLabelPosition::TopRight | PointLabelPosition::TopLeft => {
156                -size - DEFAULT_Y_LABEL_BETWEEN
157            }
158            PointLabelPosition::Right | PointLabelPosition::Left => DEFAULT_Y_LABEL_HORIZONTAL,
159            PointLabelPosition::BottomRight | PointLabelPosition::BottomLeft => {
160                size + DEFAULT_Y_LABEL_BETWEEN
161            }
162            PointLabelPosition::Bottom => size + DEFAULT_Y_LABEL_VERTICAL,
163        }
164    }
165
166    /// Get point SVG representation.
167    pub fn to_svg(&self) -> svg::node::element::Group {
168        let mut res = svg::node::element::Group::new()
169            .set(TRANSFORM_ATTR, translate_x_y(self.x, self.y))
170            .set(CLASS_ATTR, CLASS_POINT);
171
172        // Draw point if needed.
173        if self.point_visible {
174            match self.point_type {
175                PointType::Circle => {
176                    res.append(
177                        svg::node::element::Circle::new()
178                            .set(CX_ATTR, START)
179                            .set(CY_ATTR, START)
180                            .set(R_ATTR, self.size)
181                            .set(FILL_ATTR, self.fill_color.as_ref())
182                            .set(STROKE_ATTR, self.stroke_color.as_ref()),
183                    );
184                }
185                PointType::Square => {
186                    res.append(
187                        svg::node::element::Rectangle::new()
188                            .set(X_ATTR, -(self.size as i32))
189                            .set(Y_ATTR, -(self.size as i32))
190                            .set(WIDTH_ATTR, 2 * self.size)
191                            .set(HEIGHT_ATTR, 2 * self.size)
192                            .set(FILL_ATTR, self.fill_color.as_ref())
193                            .set(STROKE_ATTR, self.stroke_color.as_ref()),
194                    );
195                }
196                PointType::X => {
197                    res.append(
198                        svg::node::element::Group::new()
199                            .add(
200                                svg::node::element::Line::new()
201                                    .set(X1_ATTR, -(self.size as i32))
202                                    .set(Y1_ATTR, -(self.size as i32))
203                                    .set(X2_ATTR, self.size)
204                                    .set(Y2_ATTR, self.size)
205                                    .set(STROKE_WIDTH_ATTR, DEFAULT_STROKE_WIDTH)
206                                    .set(STROKE_ATTR, self.stroke_color.as_ref()),
207                            )
208                            .add(
209                                svg::node::element::Line::new()
210                                    .set(X1_ATTR, self.size)
211                                    .set(Y1_ATTR, -(self.size as i32))
212                                    .set(X2_ATTR, -(self.size as i32))
213                                    .set(Y2_ATTR, self.size)
214                                    .set(STROKE_WIDTH_ATTR, DEFAULT_STROKE_WIDTH)
215                                    .set(STROKE_ATTR, self.stroke_color.as_ref()),
216                            ),
217                    );
218                }
219            }
220        };
221
222        // Draw label if needed.
223        if self.label_visible {
224            let mut label: svg::node::element::Text;
225
226            // X label will be empty in case of Area or Line chart.
227            if self.x_label.is_empty() {
228                label = svg::node::element::Text::new()
229                    .set(DY_ATTR, DEFAULT_DY)
230                    .set(FONT_FAMILY_ATTR, DEFAULT_FONT_FAMILY)
231                    .set(FILL_ATTR, DEFAULT_FONT_COLOR)
232                    .set(FONT_SIZE_ATTR, DEFAULT_FONT_SIZE)
233                    .add(svg::node::Text::new(self.y_label.to_string()));
234            } else {
235                label = svg::node::element::Text::new()
236                    .set(DY_ATTR, DEFAULT_DY)
237                    .set(FONT_FAMILY_ATTR, DEFAULT_FONT_FAMILY)
238                    .set(FILL_ATTR, DEFAULT_FONT_COLOR)
239                    .set(FONT_SIZE_ATTR, DEFAULT_FONT_SIZE)
240                    .add(svg::node::Text::new(pair_x_y(&self.x_label, &self.y_label)));
241            }
242
243            label.assign(X_ATTR, self.label_x_attr);
244            label.assign(Y_ATTR, self.label_y_attr);
245            label.assign(TEXT_ANCHOR_ATTR, self.label_text_anchor.clone());
246
247            res.append(label);
248        }
249
250        res
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn bar_basic() {
260        let expected_svg_group = r##"<g class="point" transform="translate(10,20)">
261<circle cx="0" cy="0" fill="#f289ff" r="21" stroke="#8a87f6"/>
262<text dy=".35em" fill="#080808" font-family="sans-serif" font-size="14px" text-anchor="end" x="-25" y="29">
263thirty
264</text>
265</g>"##;
266
267        let point_svg = Point::new(
268            10_f32,
269            20_f32,
270            PointType::Circle,
271            21,
272            "thirty",
273            "#f289ff",
274            "#8a87f6",
275        )
276        .set_label_position(PointLabelPosition::BottomLeft)
277        .to_svg();
278        assert_eq!(point_svg.to_string(), expected_svg_group);
279    }
280}