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