plotlib/
svg_render.rs

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(); // TODO this should raise an error
335    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(); // TODO this should raise an error
408    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}