lc_render/shape/
bar.rs

1use crate::color::{COLOR_HEX_BLUE_1, COLOR_HEX_BLUE_2};
2use crate::math::linear::range;
3use crate::render::svg::*;
4use crate::Orientation;
5use svg::Node;
6
7const DEFAULT_FONT_SIZE: &str = "14px";
8const DEFAULT_LABEL_HORIZONTAL_OFFSET: f32 = 12_f32;
9const DEFAULT_LABEL_VERTICAL_OFFSET: f32 = 16_f32;
10const DEFAULT_LABEL_VISIBLE: bool = true;
11const DEFAULT_LABEL_POSITION: BarLabelPosition = BarLabelPosition::Center;
12
13/// BarLabelPosition represents available label positions of a bar shape.
14#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
15pub enum BarLabelPosition {
16    StartOutside,
17    StartInside,
18    Center,
19    EndInside,
20    EndOutside,
21}
22
23/// Bar represents a shape that can be used with vertical or horizontal views.
24#[derive(Clone)]
25pub struct Bar {
26    start: f32,
27    end: f32,
28    size: f32,
29    width: f32,
30    offset: f32,
31    orientation: Orientation,
32    fill_color: String,
33    stroke_color: String,
34    stroke_width: i32,
35    label_visible: bool,
36    label_position: BarLabelPosition,
37    label_text_anchor: String,
38    label_x_attr: f32,
39}
40
41impl Bar {
42    /// Create a new Bar.
43    pub fn new(
44        start: f32,
45        end: f32,
46        size: f32,
47        width: f32,
48        offset: f32,
49        orientation: Orientation,
50    ) -> Self {
51        Bar {
52            start,
53            end,
54            size,
55            width,
56            offset,
57            orientation,
58            fill_color: COLOR_HEX_BLUE_2.to_string(),
59            stroke_color: COLOR_HEX_BLUE_1.to_string(),
60            stroke_width: DEFAULT_STROKE_WIDTH,
61            label_visible: DEFAULT_LABEL_VISIBLE,
62            label_position: DEFAULT_LABEL_POSITION,
63            label_text_anchor: Self::label_text_anchor(DEFAULT_LABEL_POSITION, orientation),
64            label_x_attr: Self::label_x_attr(start, end, DEFAULT_LABEL_POSITION, orientation),
65        }
66    }
67
68    /// Set bar fill color.
69    pub fn set_fill_color(mut self, fill_color: &str) -> Self {
70        self.fill_color = fill_color.to_string();
71        self
72    }
73
74    /// Set bar stroke color.
75    pub fn set_stroke_color(mut self, stroke_color: &str) -> Self {
76        self.stroke_color = stroke_color.to_string();
77        self
78    }
79
80    /// Set bar label visibility.
81    pub fn set_label_visible(mut self, label_visible: bool) -> Self {
82        self.label_visible = label_visible;
83        self
84    }
85
86    /// Set label position for bar.
87    pub fn set_label_position(mut self, label_position: BarLabelPosition) -> Self {
88        self.label_position = label_position;
89        self.label_text_anchor = Self::label_text_anchor(label_position, self.orientation);
90        self.label_x_attr =
91            Self::label_x_attr(self.start, self.end, label_position, self.orientation);
92        self
93    }
94
95    fn label_text_anchor(label_position: BarLabelPosition, orientation: Orientation) -> String {
96        match label_position {
97            BarLabelPosition::StartOutside => {
98                if orientation == Orientation::Horizontal {
99                    return TEXT_ANCHOR_END.to_string();
100                }
101                TEXT_ANCHOR_MIDDLE.to_string()
102            }
103            BarLabelPosition::StartInside => {
104                if orientation == Orientation::Horizontal {
105                    return TEXT_ANCHOR_START.to_string();
106                }
107                TEXT_ANCHOR_MIDDLE.to_string()
108            }
109            BarLabelPosition::Center => TEXT_ANCHOR_MIDDLE.to_string(),
110            BarLabelPosition::EndInside => {
111                if orientation == Orientation::Horizontal {
112                    return TEXT_ANCHOR_END.to_string();
113                }
114                TEXT_ANCHOR_MIDDLE.to_string()
115            }
116            BarLabelPosition::EndOutside => {
117                if orientation == Orientation::Horizontal {
118                    return TEXT_ANCHOR_START.to_string();
119                }
120                TEXT_ANCHOR_MIDDLE.to_string()
121            }
122        }
123    }
124
125    fn label_x_attr(
126        start: f32,
127        end: f32,
128        label_position: BarLabelPosition,
129        orientation: Orientation,
130    ) -> f32 {
131        match label_position {
132            BarLabelPosition::StartOutside => {
133                if orientation == Orientation::Horizontal {
134                    return start - DEFAULT_LABEL_HORIZONTAL_OFFSET;
135                }
136                end + DEFAULT_LABEL_VERTICAL_OFFSET
137            }
138            BarLabelPosition::StartInside => {
139                if orientation == Orientation::Horizontal {
140                    return start + DEFAULT_LABEL_HORIZONTAL_OFFSET;
141                }
142                end - DEFAULT_LABEL_VERTICAL_OFFSET
143            }
144            BarLabelPosition::Center => start + (range(start, end) / 2.0),
145            BarLabelPosition::EndInside => {
146                if orientation == Orientation::Horizontal {
147                    return end - DEFAULT_LABEL_VERTICAL_OFFSET;
148                }
149                start + DEFAULT_LABEL_HORIZONTAL_OFFSET
150            }
151            BarLabelPosition::EndOutside => {
152                if orientation == Orientation::Horizontal {
153                    return end + DEFAULT_LABEL_VERTICAL_OFFSET;
154                }
155                start - DEFAULT_LABEL_HORIZONTAL_OFFSET
156            }
157        }
158    }
159
160    /// Get bar SVG representation.
161    pub fn to_svg(&self) -> svg::node::element::Group {
162        let x_attr = match self.orientation {
163            Orientation::Horizontal => (X_ATTR),
164            Orientation::Vertical => (Y_ATTR),
165        };
166        let y_attr = match self.orientation {
167            Orientation::Horizontal => (Y_ATTR),
168            Orientation::Vertical => (X_ATTR),
169        };
170        let w_attr = match self.orientation {
171            Orientation::Horizontal => (WIDTH_ATTR),
172            Orientation::Vertical => (HEIGHT_ATTR),
173        };
174        let h_attr = match self.orientation {
175            Orientation::Horizontal => (HEIGHT_ATTR),
176            Orientation::Vertical => (WIDTH_ATTR),
177        };
178        let rect = svg::node::element::Rectangle::new()
179            .set(x_attr, self.start)
180            .set(y_attr, START)
181            .set(w_attr, range(self.start, self.end))
182            .set(h_attr, self.width)
183            .set(SHAPE_RENDERING_ATTR, SHAPE_RENDERING_CRISP_EDGES)
184            .set(FILL_ATTR, self.fill_color.to_string())
185            .set(STROKE_WIDTH_ATTR, self.stroke_width)
186            .set(STROKE_ATTR, self.stroke_color.to_string());
187
188        let offset_x = match self.orientation {
189            Orientation::Horizontal => 0.0,
190            Orientation::Vertical => self.offset,
191        };
192        let offset_y = match self.orientation {
193            Orientation::Horizontal => self.offset,
194            Orientation::Vertical => 0.0,
195        };
196        let mut group = svg::node::element::Group::new()
197            .set(TRANSFORM_ATTR, translate_x_y(offset_x, offset_y))
198            .set(CLASS_ATTR, CLASS_BAR);
199        group.append(rect);
200
201        if !self.label_visible {
202            return group;
203        }
204
205        let label = svg::node::element::Text::new()
206            .set(x_attr, self.label_x_attr)
207            .set(y_attr, self.width / 2.0) // we want label to be centered vertically
208            .set(TEXT_ANCHOR_ATTR, self.label_text_anchor.to_owned())
209            .set(DY_ATTR, DEFAULT_DY)
210            .set(FONT_FAMILY_ATTR, DEFAULT_FONT_FAMILY)
211            .set(FILL_ATTR, DEFAULT_FONT_COLOR)
212            .set(FONT_SIZE_ATTR, DEFAULT_FONT_SIZE)
213            .add(svg::node::Text::new(self.size.to_string()));
214        group.append(label);
215
216        group
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use crate::color::{COLOR_HEX_GREEN_2, COLOR_HEX_GREEN_4};
224
225    #[test]
226    fn bar_basic() {
227        let expected_svg_group = r##"<g class="bar" transform="translate(5,0)">
228<rect fill="#117401" height="10" shape-rendering="crispEdges" stroke="#00400e" stroke-width="1" width="40" x="0" y="10"/>
229<text dy=".35em" fill="#080808" font-family="sans-serif" font-size="14px" text-anchor="middle" x="20" y="36">
23030
231</text>
232</g>"##;
233
234        let bar_svg = Bar::new(10_f32, 20_f32, 30_f32, 40_f32, 5_f32, Orientation::Vertical)
235            .set_fill_color(COLOR_HEX_GREEN_4)
236            .set_stroke_color(COLOR_HEX_GREEN_2)
237            .set_label_visible(true)
238            .set_label_position(BarLabelPosition::StartOutside)
239            .to_svg();
240
241        assert_eq!(bar_svg.to_string(), expected_svg_group);
242    }
243}