1use itertools::Itertools;
2use plotters::data::fitting_range;
3use plotters::prelude::*;
4use std::collections::BTreeMap;
5use std::collections::HashMap;
6use std::env;
7use std::fs;
8use std::io::{self, prelude::*, BufReader};
9
10fn read_data<BR: BufRead>(reader: BR) -> HashMap<(String, String), Vec<f64>> {
11 let mut ds = HashMap::new();
12 for l in reader.lines() {
13 let line = l.unwrap();
14 let tuple: Vec<&str> = line.split('\t').collect();
15 if tuple.len() == 3 {
16 let key = (String::from(tuple[0]), String::from(tuple[1]));
17 let entry = ds.entry(key).or_insert_with(Vec::new);
18 entry.push(tuple[2].parse::<f64>().unwrap());
19 }
20 }
21 ds
22}
23
24const OUT_FILE_NAME: &'static str = "plotters-doc-data/boxplot.svg";
25fn main() -> Result<(), Box<dyn std::error::Error>> {
26 let root = SVGBackend::new(OUT_FILE_NAME, (1024, 768)).into_drawing_area();
27 root.fill(&WHITE)?;
28
29 let root = root.margin(5, 5, 5, 5);
30
31 let (upper, lower) = root.split_vertically(512);
32
33 let args: Vec<String> = env::args().collect();
34
35 let ds = if args.len() < 2 {
36 read_data(io::Cursor::new(get_data()))
37 } else {
38 let file = fs::File::open(&args[1])?;
39 read_data(BufReader::new(file))
40 };
41 let dataset: Vec<(String, String, Quartiles)> = ds
42 .iter()
43 .map(|(k, v)| (k.0.clone(), k.1.clone(), Quartiles::new(&v)))
44 .collect();
45
46 let host_list: Vec<_> = dataset
47 .iter()
48 .unique_by(|x| x.0.clone())
49 .sorted_by(|a, b| b.2.median().partial_cmp(&a.2.median()).unwrap())
50 .map(|x| x.0.clone())
51 .collect();
52
53 let mut colors = (0..).map(Palette99::pick);
54 let mut offsets = (-12..).step_by(24);
55 let mut series = BTreeMap::new();
56 for x in dataset.iter() {
57 let entry = series
58 .entry(x.1.clone())
59 .or_insert_with(|| (Vec::new(), colors.next().unwrap(), offsets.next().unwrap()));
60 entry.0.push((x.0.clone(), &x.2));
61 }
62
63 let values: Vec<f32> = dataset
64 .iter()
65 .map(|x| x.2.values().to_vec())
66 .flatten()
67 .collect();
68 let values_range = fitting_range(values.iter());
69
70 let mut chart = ChartBuilder::on(&upper)
71 .x_label_area_size(40)
72 .y_label_area_size(80)
73 .caption("Ping Boxplot", ("sans-serif", 20))
74 .build_cartesian_2d(
75 values_range.start - 1.0..values_range.end + 1.0,
76 host_list[..].into_segmented(),
77 )?;
78
79 chart
80 .configure_mesh()
81 .x_desc("Ping, ms")
82 .y_desc("Host")
83 .y_labels(host_list.len())
84 .light_line_style(&WHITE)
85 .draw()?;
86
87 for (label, (values, style, offset)) in &series {
88 chart
89 .draw_series(values.iter().map(|x| {
90 Boxplot::new_horizontal(SegmentValue::CenterOf(&x.0), &x.1)
91 .width(20)
92 .whisker_width(0.5)
93 .style(style)
94 .offset(*offset)
95 }))?
96 .label(label)
97 .legend(move |(x, y)| Rectangle::new([(x, y - 6), (x + 12, y + 6)], style.filled()));
98 }
99 chart
100 .configure_series_labels()
101 .position(SeriesLabelPosition::UpperRight)
102 .background_style(WHITE.filled())
103 .border_style(&BLACK.mix(0.5))
104 .legend_area_size(22)
105 .draw()?;
106
107 let drawing_areas = lower.split_evenly((1, 2));
108 let (left, right) = (&drawing_areas[0], &drawing_areas[1]);
109
110 let quartiles_a = Quartiles::new(&[
111 6.0, 7.0, 15.9, 36.9, 39.0, 40.0, 41.0, 42.0, 43.0, 47.0, 49.0,
112 ]);
113 let quartiles_b = Quartiles::new(&[16.0, 17.0, 50.0, 60.0, 40.2, 41.3, 42.7, 43.3, 47.0]);
114
115 let ab_axis = ["a", "b"];
116
117 let values_range = fitting_range(
118 quartiles_a
119 .values()
120 .iter()
121 .chain(quartiles_b.values().iter()),
122 );
123 let mut chart = ChartBuilder::on(&left)
124 .x_label_area_size(40)
125 .y_label_area_size(40)
126 .caption("Vertical Boxplot", ("sans-serif", 20))
127 .build_cartesian_2d(
128 ab_axis[..].into_segmented(),
129 values_range.start - 10.0..values_range.end + 10.0,
130 )?;
131
132 chart.configure_mesh().light_line_style(&WHITE).draw()?;
133 chart.draw_series(vec![
134 Boxplot::new_vertical(SegmentValue::CenterOf(&"a"), &quartiles_a),
135 Boxplot::new_vertical(SegmentValue::CenterOf(&"b"), &quartiles_b),
136 ])?;
137
138 let mut chart = ChartBuilder::on(&right)
139 .x_label_area_size(40)
140 .y_label_area_size(40)
141 .caption("Horizontal Boxplot", ("sans-serif", 20))
142 .build_cartesian_2d(-30f32..90f32, 0..3)?;
143
144 chart.configure_mesh().light_line_style(&WHITE).draw()?;
145 chart.draw_series(vec![
146 Boxplot::new_horizontal(1, &quartiles_a),
147 Boxplot::new_horizontal(2, &Quartiles::new(&[30])),
148 ])?;
149
150 // To avoid the IO failure being ignored silently, we manually call the present function
151 root.present().expect("Unable to write result to file, please make sure 'plotters-doc-data' dir exists under current dir");
152 println!("Result has been saved to {}", OUT_FILE_NAME);
153 Ok(())
154}
155
156fn get_data() -> String {
157 String::from(
158 "
159 1.1.1.1 wireless 41.6
160 1.1.1.1 wireless 32.5
161 1.1.1.1 wireless 33.1
162 1.1.1.1 wireless 32.3
163 1.1.1.1 wireless 36.7
164 1.1.1.1 wireless 32.0
165 1.1.1.1 wireless 33.1
166 1.1.1.1 wireless 32.0
167 1.1.1.1 wireless 32.9
168 1.1.1.1 wireless 32.7
169 1.1.1.1 wireless 34.5
170 1.1.1.1 wireless 36.5
171 1.1.1.1 wireless 31.9
172 1.1.1.1 wireless 33.7
173 1.1.1.1 wireless 32.6
174 1.1.1.1 wireless 35.1
175 8.8.8.8 wireless 42.3
176 8.8.8.8 wireless 32.9
177 8.8.8.8 wireless 32.9
178 8.8.8.8 wireless 34.3
179 8.8.8.8 wireless 32.0
180 8.8.8.8 wireless 33.3
181 8.8.8.8 wireless 31.5
182 8.8.8.8 wireless 33.1
183 8.8.8.8 wireless 33.2
184 8.8.8.8 wireless 35.9
185 8.8.8.8 wireless 42.3
186 8.8.8.8 wireless 34.1
187 8.8.8.8 wireless 34.2
188 8.8.8.8 wireless 34.2
189 8.8.8.8 wireless 32.4
190 8.8.8.8 wireless 33.0
191 1.1.1.1 wired 31.8
192 1.1.1.1 wired 28.6
193 1.1.1.1 wired 29.4
194 1.1.1.1 wired 28.8
195 1.1.1.1 wired 28.2
196 1.1.1.1 wired 28.8
197 1.1.1.1 wired 28.4
198 1.1.1.1 wired 28.6
199 1.1.1.1 wired 28.3
200 1.1.1.1 wired 28.5
201 1.1.1.1 wired 28.5
202 1.1.1.1 wired 28.5
203 1.1.1.1 wired 28.4
204 1.1.1.1 wired 28.6
205 1.1.1.1 wired 28.4
206 1.1.1.1 wired 28.9
207 8.8.8.8 wired 33.3
208 8.8.8.8 wired 28.4
209 8.8.8.8 wired 28.7
210 8.8.8.8 wired 29.1
211 8.8.8.8 wired 29.6
212 8.8.8.8 wired 28.9
213 8.8.8.8 wired 28.6
214 8.8.8.8 wired 29.3
215 8.8.8.8 wired 28.6
216 8.8.8.8 wired 29.1
217 8.8.8.8 wired 28.7
218 8.8.8.8 wired 28.3
219 8.8.8.8 wired 28.3
220 8.8.8.8 wired 28.6
221 8.8.8.8 wired 29.4
222 8.8.8.8 wired 33.1
223",
224 )
225}
226#[test]
227fn entry_point() {
228 main().unwrap()
229}
230