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#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
23pub enum PointType {
24 Circle,
25 Square,
26 X,
27}
28
29#[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#[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 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 pub fn set_point_visible(mut self, point_visible: bool) -> Self {
92 self.point_visible = point_visible;
93 self
94 }
95
96 pub fn set_x_label(mut self, x_label: &str) -> Self {
98 self.x_label = x_label.to_string();
99 self
100 }
101
102 pub fn set_label_visible(mut self, label_visible: bool) -> Self {
104 self.label_visible = label_visible;
105 self
106 }
107
108 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 pub fn x(&self) -> f32 {
119 self.x
120 }
121 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 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 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 if self.label_visible {
224 let mut label: svg::node::element::Text;
225
226 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}