1use std;
2
3use svg::node;
4use svg::Node;
5
6use crate::axis;
7use crate::grid::GridType;
8use crate::repr;
9use crate::style;
10use crate::utils;
11use crate::utils::PairWise;
12
13fn value_to_face_offset(value: f64, axis: &axis::ContinuousAxis, face_size: f64) -> f64 {
14 let range = axis.max() - axis.min();
15 (face_size * (value - axis.min())) / range
16}
17
18fn vertical_line<S>(xpos: f64, ymin: f64, ymax: f64, color: S) -> node::element::Line
19where
20 S: AsRef<str>,
21{
22 node::element::Line::new()
23 .set("x1", xpos)
24 .set("x2", xpos)
25 .set("y1", ymin)
26 .set("y2", ymax)
27 .set("stroke", color.as_ref())
28 .set("stroke-width", 1)
29}
30
31fn horizontal_line<S>(ypos: f64, xmin: f64, xmax: f64, color: S) -> node::element::Line
32where
33 S: AsRef<str>,
34{
35 node::element::Line::new()
36 .set("x1", xmin)
37 .set("x2", xmax)
38 .set("y1", ypos)
39 .set("y2", ypos)
40 .set("stroke", color.as_ref())
41 .set("stroke-width", 1)
42}
43
44pub fn draw_x_axis(a: &axis::ContinuousAxis, face_width: f64) -> node::element::Group {
45 let axis_line = horizontal_line(0.0, 0.0, face_width, "black");
46
47 let mut ticks = node::element::Group::new();
48 let mut labels = node::element::Group::new();
49
50 for &tick in a.ticks().iter() {
51 let tick_pos = value_to_face_offset(tick, a, face_width);
52 let tick_mark = node::element::Line::new()
53 .set("x1", tick_pos)
54 .set("y1", 0)
55 .set("x2", tick_pos)
56 .set("y2", 10)
57 .set("stroke", "black")
58 .set("stroke-width", 1);
59 ticks.append(tick_mark);
60
61 let tick_label = node::element::Text::new()
62 .set("x", tick_pos)
63 .set("y", 20)
64 .set("text-anchor", "middle")
65 .set("font-size", 12)
66 .add(node::Text::new(tick.to_string()));
67 labels.append(tick_label);
68 }
69
70 let label = node::element::Text::new()
71 .set("x", face_width / 2.)
72 .set("y", 30)
73 .set("text-anchor", "middle")
74 .set("font-size", 12)
75 .add(node::Text::new(a.get_label()));
76
77 node::element::Group::new()
78 .add(ticks)
79 .add(axis_line)
80 .add(labels)
81 .add(label)
82}
83
84pub fn draw_y_axis(a: &axis::ContinuousAxis, face_height: f64) -> node::element::Group {
85 let axis_line = vertical_line(0.0, 0.0, -face_height, "black");
86
87 let mut ticks = node::element::Group::new();
88 let mut labels = node::element::Group::new();
89
90 let y_tick_font_size = 12;
91
92 for &tick in a.ticks().iter() {
93 let tick_pos = value_to_face_offset(tick, a, face_height);
94 let tick_mark = node::element::Line::new()
95 .set("x1", 0)
96 .set("y1", -tick_pos)
97 .set("x2", -10)
98 .set("y2", -tick_pos)
99 .set("stroke", "black")
100 .set("stroke-width", 1);
101 ticks.append(tick_mark);
102
103 let tick_label = node::element::Text::new()
104 .set("x", -15)
105 .set("y", -tick_pos)
106 .set("text-anchor", "end")
107 .set("dominant-baseline", "middle")
108 .set("font-size", y_tick_font_size)
109 .add(node::Text::new(tick.to_string()));
110 labels.append(tick_label);
111 }
112
113 let max_tick_length = a
114 .ticks()
115 .iter()
116 .map(|&t| t.to_string().len())
117 .max()
118 .expect("Could not calculate max tick length");
119
120 let x_offset = -(y_tick_font_size * max_tick_length as i32);
121 let y_label_offset = -(face_height / 2.);
122 let y_label_font_size = 12;
123 let label = node::element::Text::new()
124 .set("x", x_offset)
125 .set("y", y_label_offset - f64::from(y_label_font_size))
126 .set("text-anchor", "middle")
127 .set("font-size", y_label_font_size)
128 .set(
129 "transform",
130 format!("rotate(-90 {} {})", x_offset, y_label_offset),
131 )
132 .add(node::Text::new(a.get_label()));
133
134 node::element::Group::new()
135 .add(ticks)
136 .add(axis_line)
137 .add(labels)
138 .add(label)
139}
140
141pub fn draw_categorical_x_axis(a: &axis::CategoricalAxis, face_width: f64) -> node::element::Group {
142 let axis_line = node::element::Line::new()
143 .set("x1", 0)
144 .set("y1", 0)
145 .set("x2", face_width)
146 .set("y2", 0)
147 .set("stroke", "black")
148 .set("stroke-width", 1);
149
150 let mut ticks = node::element::Group::new();
151 let mut labels = node::element::Group::new();
152
153 let space_per_tick = face_width / a.ticks().len() as f64;
154
155 for (i, tick) in a.ticks().iter().enumerate() {
156 let tick_pos = (i as f64 * space_per_tick) + (0.5 * space_per_tick);
157 let tick_mark = node::element::Line::new()
158 .set("x1", tick_pos)
159 .set("y1", 0)
160 .set("x2", tick_pos)
161 .set("y2", 10)
162 .set("stroke", "black")
163 .set("stroke-width", 1);
164 ticks.append(tick_mark);
165
166 let tick_label = node::element::Text::new()
167 .set("x", tick_pos)
168 .set("y", 20)
169 .set("text-anchor", "middle")
170 .set("font-size", 12)
171 .add(node::Text::new(tick.to_owned()));
172 labels.append(tick_label);
173 }
174
175 let label = node::element::Text::new()
176 .set("x", face_width / 2.)
177 .set("y", 30)
178 .set("text-anchor", "middle")
179 .set("font-size", 12)
180 .add(node::Text::new(a.get_label()));
181
182 node::element::Group::new()
183 .add(ticks)
184 .add(axis_line)
185 .add(labels)
186 .add(label)
187}
188
189pub fn draw_face_points(
190 s: &[(f64, f64)],
191 x_axis: &axis::ContinuousAxis,
192 y_axis: &axis::ContinuousAxis,
193 face_width: f64,
194 face_height: f64,
195 style: &style::PointStyle,
196) -> node::element::Group {
197 let mut group = node::element::Group::new();
198
199 for &(x, y) in s {
200 let x_pos = value_to_face_offset(x, x_axis, face_width);
201 let y_pos = -value_to_face_offset(y, y_axis, face_height);
202 let radius = f64::from(style.get_size());
203 match style.get_marker() {
204 style::PointMarker::Circle => {
205 group.append(
206 node::element::Circle::new()
207 .set("cx", x_pos)
208 .set("cy", y_pos)
209 .set("r", radius)
210 .set("fill", style.get_colour()),
211 );
212 }
213 style::PointMarker::Square => {
214 group.append(
215 node::element::Rectangle::new()
216 .set("x", x_pos - radius)
217 .set("y", y_pos - radius)
218 .set("width", 2. * radius)
219 .set("height", 2. * radius)
220 .set("fill", style.get_colour()),
221 );
222 }
223 style::PointMarker::Cross => {
224 let path = node::element::path::Data::new()
225 .move_to((x_pos - radius, y_pos - radius))
226 .line_by((radius * 2., radius * 2.))
227 .move_by((-radius * 2., 0))
228 .line_by((radius * 2., -radius * 2.))
229 .close();
230 group.append(
231 node::element::Path::new()
232 .set("fill", "none")
233 .set("stroke", style.get_colour())
234 .set("stroke-width", 2)
235 .set("d", path),
236 );
237 }
238 };
239 }
240
241 group
242}
243
244pub fn draw_face_bars(
245 h: &repr::Histogram,
246 x_axis: &axis::ContinuousAxis,
247 y_axis: &axis::ContinuousAxis,
248 face_width: f64,
249 face_height: f64,
250 style: &style::BoxStyle,
251) -> node::element::Group {
252 let mut group = node::element::Group::new();
253
254 for ((&l, &u), &count) in h.bin_bounds.pairwise().zip(h.get_values()) {
255 let l_pos = value_to_face_offset(l, x_axis, face_width);
256 let u_pos = value_to_face_offset(u, x_axis, face_width);
257 let width = u_pos - l_pos;
258 let count_scaled = value_to_face_offset(count, y_axis, face_height);
259 let rect = node::element::Rectangle::new()
260 .set("x", l_pos)
261 .set("y", -count_scaled)
262 .set("width", width)
263 .set("height", count_scaled)
264 .set("fill", style.get_fill())
265 .set("stroke", "black");
266 group.append(rect);
267 }
268
269 group
270}
271
272pub fn draw_face_line(
273 s: &[(f64, f64)],
274 x_axis: &axis::ContinuousAxis,
275 y_axis: &axis::ContinuousAxis,
276 face_width: f64,
277 face_height: f64,
278 style: &style::LineStyle,
279) -> node::element::Group {
280 let mut group = node::element::Group::new();
281
282 let mut d: Vec<node::element::path::Command> = vec![];
283 let &(first_x, first_y) = s.first().unwrap();
284 let first_x_pos = value_to_face_offset(first_x, x_axis, face_width);
285 let first_y_pos = -value_to_face_offset(first_y, y_axis, face_height);
286 d.push(node::element::path::Command::Move(
287 node::element::path::Position::Absolute,
288 (first_x_pos, first_y_pos).into(),
289 ));
290 for &(x, y) in s {
291 let x_pos = value_to_face_offset(x, x_axis, face_width);
292 let y_pos = -value_to_face_offset(y, y_axis, face_height);
293 d.push(node::element::path::Command::Line(
294 node::element::path::Position::Absolute,
295 (x_pos, y_pos).into(),
296 ));
297 }
298
299 let path = node::element::path::Data::from(d);
300
301 group.append(
302 node::element::Path::new()
303 .set("fill", "none")
304 .set("stroke", style.get_colour())
305 .set("stroke-width", style.get_width())
306 .set(
307 "stroke-linejoin",
308 match style.get_linejoin() {
309 style::LineJoin::Miter => "miter",
310 style::LineJoin::Round => "round",
311 },
312 )
313 .set("d", path),
314 );
315
316 group
317}
318
319pub fn draw_face_boxplot<L>(
320 d: &[f64],
321 label: &L,
322 x_axis: &axis::CategoricalAxis,
323 y_axis: &axis::ContinuousAxis,
324 face_width: f64,
325 face_height: f64,
326 style: &style::BoxStyle,
327) -> node::element::Group
328where
329 L: Into<String>,
330 String: std::cmp::PartialEq<L>,
331{
332 let mut group = node::element::Group::new();
333
334 let tick_index = x_axis.ticks().iter().position(|t| t == label).unwrap(); let space_per_tick = face_width / x_axis.ticks().len() as f64;
336 let tick_pos = (tick_index as f64 * space_per_tick) + (0.5 * space_per_tick);
337
338 let box_width = space_per_tick / 2.;
339
340 let (q1, median, q3) = utils::quartiles(d);
341
342 let box_start = -value_to_face_offset(q3, y_axis, face_height);
343 let box_end = -value_to_face_offset(q1, y_axis, face_height);
344
345 group.append(
346 node::element::Rectangle::new()
347 .set("x", tick_pos - (box_width / 2.))
348 .set("y", box_start)
349 .set("width", box_width)
350 .set("height", box_end - box_start)
351 .set("fill", style.get_fill())
352 .set("stroke", "black"),
353 );
354
355 let mid_line = -value_to_face_offset(median, y_axis, face_height);
356
357 group.append(
358 node::element::Line::new()
359 .set("x1", tick_pos - (box_width / 2.))
360 .set("y1", mid_line)
361 .set("x2", tick_pos + (box_width / 2.))
362 .set("y2", mid_line)
363 .set("stroke", "black"),
364 );
365
366 let (min, max) = utils::range(d);
367
368 let whisker_bottom = -value_to_face_offset(min, y_axis, face_height);
369 let whisker_top = -value_to_face_offset(max, y_axis, face_height);
370
371 group.append(
372 node::element::Line::new()
373 .set("x1", tick_pos)
374 .set("y1", whisker_bottom)
375 .set("x2", tick_pos)
376 .set("y2", box_end)
377 .set("stroke", "black"),
378 );
379
380 group.append(
381 node::element::Line::new()
382 .set("x1", tick_pos)
383 .set("y1", whisker_top)
384 .set("x2", tick_pos)
385 .set("y2", box_start)
386 .set("stroke", "black"),
387 );
388
389 group
390}
391
392pub fn draw_face_barchart<L>(
393 d: f64,
394 label: &L,
395 x_axis: &axis::CategoricalAxis,
396 y_axis: &axis::ContinuousAxis,
397 face_width: f64,
398 face_height: f64,
399 style: &style::BoxStyle,
400) -> node::element::Group
401where
402 L: Into<String>,
403 String: std::cmp::PartialEq<L>,
404{
405 let mut group = node::element::Group::new();
406
407 let tick_index = x_axis.ticks().iter().position(|t| t == label).unwrap(); let space_per_tick = face_width / x_axis.ticks().len() as f64;
409 let tick_pos = (tick_index as f64 * space_per_tick) + (0.5 * space_per_tick);
410
411 let box_width = space_per_tick / 2.;
412
413 let box_start = -value_to_face_offset(d, y_axis, face_height);
414 let box_end = -value_to_face_offset(0.0, y_axis, face_height);
415
416 group.append(
417 node::element::Rectangle::new()
418 .set("x", tick_pos - (box_width / 2.))
419 .set("y", box_start)
420 .set("width", box_width)
421 .set("height", box_end - box_start)
422 .set("fill", style.get_fill())
423 .set("stroke", "black"),
424 );
425
426 group
427}
428
429pub(crate) fn draw_grid(grid: GridType, face_width: f64, face_height: f64) -> node::element::Group {
430 match grid {
431 GridType::HorizontalOnly(grid) => {
432 let (ymin, ymax) = (0f64, face_height);
433 let y_step = (ymax - ymin) / f64::from(grid.ny);
434 let mut lines = node::element::Group::new();
435
436 for iy in 0..=grid.ny {
437 let y = f64::from(iy) * y_step + ymin;
438 let line = horizontal_line(-y, 0.0, face_width, grid.color.as_str());
439 lines = lines.add(line);
440 }
441
442 lines
443 }
444 GridType::Both(grid) => {
445 let (xmin, xmax) = (0f64, face_width);
446 let (ymin, ymax) = (0f64, face_height);
447
448 let x_step = (xmax - xmin) / f64::from(grid.nx);
449 let y_step = (ymax - ymin) / f64::from(grid.ny);
450
451 let mut lines = node::element::Group::new();
452
453 for iy in 0..=grid.ny {
454 let y = f64::from(iy) * y_step + ymin;
455 let line = horizontal_line(-y, 0.0, face_width, grid.color.as_str());
456 lines = lines.add(line);
457 }
458
459 for ix in 0..=grid.nx {
460 let x = f64::from(ix) * x_step + xmin;
461 let line = vertical_line(x, 0.0, -face_height, grid.color.as_str());
462 lines = lines.add(line);
463 }
464
465 lines
466 }
467 }
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 #[test]
475 fn test_value_to_face_offset() {
476 let axis = axis::ContinuousAxis::new(-2., 5., 6);
477 assert_eq!(value_to_face_offset(-2.0, &axis, 14.0), 0.0);
478 assert_eq!(value_to_face_offset(5.0, &axis, 14.0), 14.0);
479 assert_eq!(value_to_face_offset(0.0, &axis, 14.0), 4.0);
480 assert_eq!(value_to_face_offset(-4.0, &axis, 14.0), -4.0);
481 assert_eq!(value_to_face_offset(7.0, &axis, 14.0), 18.0);
482 }
483}