1 use crate::{ 2 Color, Drawable, EventCtx, GeomBatch, GfxCtx, JustDraw, Line, ScreenDims, ScreenPt, 3 ScreenRectangle, Text, TextExt, Widget, WidgetImpl, WidgetOutput, 4 }; 5 use geom::{Angle, Circle, Distance, Duration, Pt2D}; 6 7 // TODO This is tuned for the trip time comparison right now. 8 // - Generic types for x and y axis 9 // - number of labels 10 // - rounding behavior 11 // - forcing the x and y axis to be on the same scale, be drawn as a square 12 // - coloring the better/worse 13 14 pub struct CompareTimes { 15 draw: Drawable, 16 17 max: Duration, 18 19 top_left: ScreenPt, 20 dims: ScreenDims, 21 } 22 23 impl CompareTimes { new<I: Into<String>>( ctx: &mut EventCtx, x_name: I, y_name: I, points: Vec<(Duration, Duration)>, ) -> Widget24 pub fn new<I: Into<String>>( 25 ctx: &mut EventCtx, 26 x_name: I, 27 y_name: I, 28 points: Vec<(Duration, Duration)>, 29 ) -> Widget { 30 if points.is_empty() { 31 return Widget::nothing(); 32 } 33 34 let actual_max = *points.iter().map(|(b, a)| a.max(b)).max().unwrap(); 35 // Excluding 0 36 let num_labels = 5; 37 let (max, labels) = actual_max.make_intervals_for_max(num_labels); 38 39 // We want a nice square so the scales match up. 40 let width = 500.0; 41 let height = width; 42 43 let mut batch = GeomBatch::new(); 44 batch.autocrop_dims = false; 45 46 // Grid lines 47 let thickness = Distance::meters(2.0); 48 for i in 1..num_labels { 49 let x = (i as f64) / (num_labels as f64) * width; 50 let y = (i as f64) / (num_labels as f64) * height; 51 // Horizontal 52 batch.push( 53 Color::grey(0.5), 54 geom::Line::new(Pt2D::new(0.0, y), Pt2D::new(width, y)) 55 .unwrap() 56 .make_polygons(thickness), 57 ); 58 // Vertical 59 batch.push( 60 Color::grey(0.5), 61 geom::Line::new(Pt2D::new(x, 0.0), Pt2D::new(x, height)) 62 .unwrap() 63 .make_polygons(thickness), 64 ); 65 } 66 // Draw the diagonal, since we're comparing things on the same scale 67 batch.push( 68 Color::grey(0.5), 69 geom::Line::new(Pt2D::new(0.0, height), Pt2D::new(width, 0.0)) 70 .unwrap() 71 .make_polygons(thickness), 72 ); 73 74 let circle = Circle::new(Pt2D::new(0.0, 0.0), Distance::meters(4.0)).to_polygon(); 75 for (b, a) in points { 76 let pt = Pt2D::new((b / max) * width, (1.0 - (a / max)) * height); 77 // TODO Could color circles by mode 78 let color = if a == b { 79 Color::YELLOW.alpha(0.5) 80 } else if a < b { 81 Color::GREEN.alpha(0.9) 82 } else { 83 Color::RED.alpha(0.9) 84 }; 85 batch.push(color, circle.translate(pt.x(), pt.y())); 86 } 87 let plot = Widget::new(Box::new(CompareTimes { 88 dims: batch.get_dims(), 89 draw: ctx.upload(batch), 90 max, 91 top_left: ScreenPt::new(0.0, 0.0), 92 })); 93 94 let y_axis = Widget::custom_col( 95 labels 96 .iter() 97 .rev() 98 .map(|x| Line(x.to_string()).small().draw(ctx)) 99 .collect(), 100 ) 101 .evenly_spaced(); 102 let y_label = { 103 let label = Text::from(Line(format!("{} (minutes)", y_name.into()))) 104 .render_ctx(ctx) 105 .rotate(Angle::new_degs(90.0)) 106 .autocrop(); 107 // The text is already scaled; don't use Widget::draw_batch and scale it again. 108 JustDraw::wrap(ctx, label).centered_vert().margin_right(5) 109 }; 110 111 let x_axis = Widget::custom_row( 112 labels 113 .iter() 114 .map(|x| Line(x.to_string()).small().draw(ctx)) 115 .collect(), 116 ) 117 .evenly_spaced(); 118 let x_label = format!("{} (minutes)", x_name.into()) 119 .draw_text(ctx) 120 .centered_horiz(); 121 122 // It's a bit of work to make both the x and y axis line up with the plot. :) 123 let plot_width = plot.get_width_for_forcing(); 124 Widget::custom_col(vec![ 125 Widget::custom_row(vec![y_label, y_axis, plot]), 126 Widget::custom_col(vec![x_axis, x_label]) 127 .force_width(plot_width) 128 .align_right(), 129 ]) 130 .container() 131 } 132 } 133 134 impl WidgetImpl for CompareTimes { get_dims(&self) -> ScreenDims135 fn get_dims(&self) -> ScreenDims { 136 self.dims 137 } 138 set_pos(&mut self, top_left: ScreenPt)139 fn set_pos(&mut self, top_left: ScreenPt) { 140 self.top_left = top_left; 141 } 142 event(&mut self, _: &mut EventCtx, _: &mut WidgetOutput)143 fn event(&mut self, _: &mut EventCtx, _: &mut WidgetOutput) {} 144 draw(&self, g: &mut GfxCtx)145 fn draw(&self, g: &mut GfxCtx) { 146 g.redraw_at(self.top_left, &self.draw); 147 148 if let Some(cursor) = g.canvas.get_cursor_in_screen_space() { 149 let rect = ScreenRectangle::top_left(self.top_left, self.dims); 150 if let Some((pct_x, pct_y)) = rect.pt_to_percent(cursor) { 151 let thickness = Distance::meters(2.0); 152 let mut batch = GeomBatch::new(); 153 // Horizontal 154 if let Some(l) = geom::Line::new(Pt2D::new(rect.x1, cursor.y), cursor.to_pt()) { 155 batch.push(Color::WHITE, l.make_polygons(thickness)); 156 } 157 // Vertical 158 if let Some(l) = geom::Line::new(Pt2D::new(cursor.x, rect.y2), cursor.to_pt()) { 159 batch.push(Color::WHITE, l.make_polygons(thickness)); 160 } 161 162 g.fork_screenspace(); 163 let draw = g.upload(batch); 164 g.redraw(&draw); 165 // TODO Quite specialized to the one use right now 166 let before = pct_x * self.max; 167 let after = (1.0 - pct_y) * self.max; 168 if after <= before { 169 g.draw_mouse_tooltip(Text::from_all(vec![ 170 Line(format!("{} faster", before - after)).fg(Color::GREEN), 171 Line(format!(" than {}", before)), 172 ])); 173 } else { 174 g.draw_mouse_tooltip(Text::from_all(vec![ 175 Line(format!("{} slower", after - before)).fg(Color::RED), 176 Line(format!(" than {}", before)), 177 ])); 178 } 179 g.unfork(); 180 } 181 } 182 } 183 } 184