widgetry/widgets/
compare_times.rs

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