| 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 | |