1 | use itertools::Itertools; |
2 | use plotters::data::fitting_range; |
3 | use plotters::prelude::*; |
4 | use std::collections::BTreeMap; |
5 | use std::collections::HashMap; |
6 | use std::env; |
7 | use std::fs; |
8 | use std::io::{self, prelude::*, BufReader}; |
9 | |
10 | fn 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 | |
24 | const OUT_FILE_NAME: &'static str = "plotters-doc-data/boxplot.svg" ; |
25 | fn 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 | |
156 | fn 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] |
227 | fn entry_point() { |
228 | main().unwrap() |
229 | } |
230 | |