Steps towards clicking cells in the problem matrix to explore trips with differences. #600

- Add optional clickable labels to DrawWithTooltips
- Wire up problem_matrix to remember the list of trips associated with
  each cell
- When clicking a cell, just open one arbitrary example trip
This commit is contained in:
Dustin Carlino 2021-07-17 13:55:01 -07:00
parent db435cd56d
commit 942f2292fc
8 changed files with 172 additions and 97 deletions

View File

@ -200,7 +200,7 @@ fn current_demand_body(ctx: &mut EventCtx, app: &App, id: IntersectionID) -> Wid
polygon.translate(-bounds.min_x, -bounds.min_y).scale(zoom), polygon.translate(-bounds.min_x, -bounds.min_y).scale(zoom),
); );
let mut tooltips: Vec<(Polygon, Text)> = Vec::new(); let mut tooltips: Vec<(Polygon, Text, Option<String>)> = Vec::new();
let mut outlines = Vec::new(); let mut outlines = Vec::new();
for (pl, demand) in demand_per_movement { for (pl, demand) in demand_per_movement {
let percent = (demand as f64) / (total_demand as f64); let percent = (demand as f64) / (total_demand as f64);
@ -212,7 +212,7 @@ fn current_demand_body(ctx: &mut EventCtx, app: &App, id: IntersectionID) -> Wid
outlines.push(p); outlines.push(p);
} }
batch.push(Color::hex("#A3A3A3"), arrow.clone()); batch.push(Color::hex("#A3A3A3"), arrow.clone());
tooltips.push((arrow, Text::from(prettyprint_usize(demand)))); tooltips.push((arrow, Text::from(prettyprint_usize(demand)), None));
} }
batch.extend(Color::WHITE, outlines); batch.extend(Color::WHITE, outlines);

View File

@ -612,7 +612,7 @@ fn make_timeline(
// icons above them. // icons above them.
let mut batch = GeomBatch::new(); let mut batch = GeomBatch::new();
// And associate a tooltip with each rectangle segment // And associate a tooltip with each rectangle segment
let mut tooltips: Vec<(Polygon, Text)> = Vec::new(); let mut tooltips: Vec<(Polygon, Text, Option<String>)> = Vec::new();
// How far along are we from previous segments? // How far along are we from previous segments?
let mut x1 = 0.0; let mut x1 = 0.0;
let rectangle_height = 15.0; let rectangle_height = 15.0;
@ -660,6 +660,7 @@ fn make_timeline(
tooltips.push(( tooltips.push((
rectangle.clone(), rectangle.clone(),
Text::from_multiline(tooltip.into_iter().map(Line).collect()), Text::from_multiline(tooltip.into_iter().map(Line).collect()),
None,
)); ));
batch.push( batch.push(

View File

@ -1,19 +1,21 @@
use std::collections::BTreeSet; use std::collections::{BTreeSet, HashMap};
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
use anyhow::Result; use anyhow::Result;
use map_gui::tools::PopupMsg; use map_gui::tools::PopupMsg;
use sim::TripMode; use sim::{TripID, TripMode};
use widgetry::{EventCtx, GfxCtx, Image, Line, Outcome, Panel, State, TextExt, Toggle, Widget}; use widgetry::{EventCtx, GfxCtx, Image, Line, Outcome, Panel, State, TextExt, Toggle, Widget};
use super::trip_problems::{problem_matrix, ProblemType, TripProblemFilter}; use super::trip_problems::{problem_matrix, ProblemType, TripProblemFilter};
use crate::app::{App, Transition}; use crate::app::{App, Transition};
use crate::sandbox::dashboards::generic_trip_table::open_trip_transition;
use crate::sandbox::dashboards::DashTab; use crate::sandbox::dashboards::DashTab;
pub struct RiskSummaries { pub struct RiskSummaries {
panel: Panel, panel: Panel,
trip_lookup: HashMap<String, Vec<TripID>>,
} }
impl RiskSummaries { impl RiskSummaries {
@ -26,12 +28,33 @@ impl RiskSummaries {
modes: maplit::btreeset! { TripMode::Bike }, modes: maplit::btreeset! { TripMode::Bike },
include_no_changes, include_no_changes,
}; };
let ped_filter = Filter { let ped_filter = Filter {
modes: maplit::btreeset! { TripMode::Walk }, modes: maplit::btreeset! { TripMode::Walk },
include_no_changes, include_no_changes,
}; };
let (arterial_problems, mut trip_lookup) = problem_matrix(
ctx,
app,
"arterials",
ped_filter.trip_problems(app, ProblemType::ArterialIntersectionCrossing),
);
let (complex_intersection_problems, lookup2) = problem_matrix(
ctx,
app,
"complex intersections",
bike_filter.trip_problems(app, ProblemType::ComplexIntersectionCrossing),
);
let (overtaking_problems, lookup3) = problem_matrix(
ctx,
app,
"overtakings",
bike_filter.trip_problems(app, ProblemType::OvertakeDesired),
);
// The keys won't overlap, due to the unique title of each matrix.
trip_lookup.extend(lookup2);
trip_lookup.extend(lookup3);
Box::new(RiskSummaries { Box::new(RiskSummaries {
panel: Panel::new_builder(Widget::col(vec![ panel: Panel::new_builder(Widget::col(vec![
DashTab::RiskSummaries.picker(ctx, app), DashTab::RiskSummaries.picker(ctx, app),
@ -66,12 +89,7 @@ impl RiskSummaries {
.small_heading() .small_heading()
.into_widget(ctx) .into_widget(ctx)
.centered_horiz(), .centered_horiz(),
problem_matrix( arterial_problems,
ctx,
app,
&ped_filter
.trip_problems(app, ProblemType::ArterialIntersectionCrossing),
),
]) ])
.section(ctx)], .section(ctx)],
) )
@ -98,12 +116,7 @@ impl RiskSummaries {
.small_heading() .small_heading()
.into_widget(ctx) .into_widget(ctx)
.centered_horiz(), .centered_horiz(),
problem_matrix( complex_intersection_problems,
ctx,
app,
&bike_filter
.trip_problems(app, ProblemType::ComplexIntersectionCrossing),
),
]) ])
.section(ctx), .section(ctx),
Widget::col(vec![ Widget::col(vec![
@ -111,11 +124,7 @@ impl RiskSummaries {
.small_heading() .small_heading()
.into_widget(ctx) .into_widget(ctx)
.centered_horiz(), .centered_horiz(),
problem_matrix( overtaking_problems,
ctx,
app,
&bike_filter.trip_problems(app, ProblemType::OvertakeDesired),
),
]) ])
.section(ctx), .section(ctx),
], ],
@ -125,6 +134,7 @@ impl RiskSummaries {
])) ]))
.exact_size_percent(90, 90) .exact_size_percent(90, 90)
.build(ctx), .build(ctx),
trip_lookup,
}) })
} }
} }
@ -146,7 +156,10 @@ impl State<App> for RiskSummaries {
} }
}); });
} }
_ => unreachable!(), x => {
// TODO Handle browsing multiple trips
return open_trip_transition(app, self.trip_lookup[x][0].0);
}
}, },
Outcome::Changed(_) => { Outcome::Changed(_) => {
if let Some(t) = DashTab::RiskSummaries.transition(ctx, app, &self.panel) { if let Some(t) = DashTab::RiskSummaries.transition(ctx, app, &self.panel) {

View File

@ -1,4 +1,4 @@
use std::collections::BTreeSet; use std::collections::{BTreeSet, HashMap};
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
@ -7,7 +7,7 @@ use anyhow::Result;
use abstutil::prettyprint_usize; use abstutil::prettyprint_usize;
use geom::{Distance, Duration, Polygon, Pt2D}; use geom::{Distance, Duration, Polygon, Pt2D};
use map_gui::tools::PopupMsg; use map_gui::tools::PopupMsg;
use sim::TripMode; use sim::{TripID, TripMode};
use widgetry::{ use widgetry::{
Choice, Color, CompareTimes, DrawWithTooltips, EventCtx, GeomBatch, GfxCtx, Line, Outcome, Choice, Color, CompareTimes, DrawWithTooltips, EventCtx, GeomBatch, GfxCtx, Line, Outcome,
Panel, State, Text, TextExt, Toggle, Widget, Panel, State, Text, TextExt, Toggle, Widget,
@ -16,20 +16,20 @@ use widgetry::{
use super::trip_problems::{problem_matrix, ProblemType, TripProblemFilter}; use super::trip_problems::{problem_matrix, ProblemType, TripProblemFilter};
use crate::app::{App, Transition}; use crate::app::{App, Transition};
use crate::common::color_for_mode; use crate::common::color_for_mode;
use crate::sandbox::dashboards::generic_trip_table::open_trip_transition;
use crate::sandbox::dashboards::DashTab; use crate::sandbox::dashboards::DashTab;
pub struct TravelTimes { pub struct TravelTimes {
panel: Panel, panel: Panel,
trip_lookup: HashMap<String, Vec<TripID>>,
} }
impl TravelTimes { impl TravelTimes {
pub fn new_state(ctx: &mut EventCtx, app: &App, filter: Filter) -> Box<dyn State<App>> { pub fn new_state(ctx: &mut EventCtx, app: &App, filter: Filter) -> Box<dyn State<App>> {
Box::new(TravelTimes { Box::new(TravelTimes::new(ctx, app, filter))
panel: TravelTimes::make_panel(ctx, app, filter),
})
} }
fn make_panel(ctx: &mut EventCtx, app: &App, filter: Filter) -> Panel { fn new(ctx: &mut EventCtx, app: &App, filter: Filter) -> TravelTimes {
let mut filters = vec!["Filters".text_widget(ctx)]; let mut filters = vec!["Filters".text_widget(ctx)];
for mode in TripMode::all() { for mode in TripMode::all() {
filters.push(Toggle::colored_checkbox( filters.push(Toggle::colored_checkbox(
@ -48,7 +48,14 @@ impl TravelTimes {
.align_bottom(), .align_bottom(),
); );
Panel::new_builder(Widget::col(vec![ let (problems, trip_lookup) = problem_matrix(
ctx,
app,
"delays",
filter.trip_problems(app, ProblemType::IntersectionDelay),
);
let panel = Panel::new_builder(Widget::col(vec![
DashTab::TravelTimes.picker(ctx, app), DashTab::TravelTimes.picker(ctx, app),
Widget::row(vec![ Widget::row(vec![
Widget::col(filters).section(ctx), Widget::col(filters).section(ctx),
@ -92,19 +99,16 @@ impl TravelTimes {
filter.include_no_changes(), filter.include_no_changes(),
), ),
]), ]),
problem_matrix( problems.margin_left(32),
ctx,
app,
&filter.trip_problems(app, ProblemType::IntersectionDelay),
)
.margin_left(32),
]) ])
.section(ctx), .section(ctx),
]), ]),
]), ]),
])) ]))
.exact_size_percent(90, 90) .exact_size_percent(90, 90)
.build(ctx) .build(ctx);
TravelTimes { panel, trip_lookup }
} }
} }
@ -125,7 +129,10 @@ impl State<App> for TravelTimes {
}); });
} }
"close" => Transition::Pop, "close" => Transition::Pop,
_ => unreachable!(), x => {
// TODO Handle browsing multiple trips
return open_trip_transition(app, self.trip_lookup[x][0].0);
}
}, },
Outcome::Changed(_) => { Outcome::Changed(_) => {
if let Some(t) = DashTab::TravelTimes.transition(ctx, app, &self.panel) { if let Some(t) = DashTab::TravelTimes.transition(ctx, app, &self.panel) {
@ -142,9 +149,9 @@ impl State<App> for TravelTimes {
filter.modes.insert(m); filter.modes.insert(m);
} }
} }
let mut new_panel = TravelTimes::make_panel(ctx, app, filter); let mut new = TravelTimes::new(ctx, app, filter);
new_panel.restore(ctx, &self.panel); new.panel.restore(ctx, &self.panel);
self.panel = new_panel; *self = new;
Transition::Keep Transition::Keep
} }
_ => Transition::Keep, _ => Transition::Keep,
@ -462,6 +469,7 @@ fn contingency_table(ctx: &mut EventCtx, app: &App, filter: &Filter) -> Widget {
)) ))
.fg(Color::hex("#72CE36")), .fg(Color::hex("#72CE36")),
]), ]),
None,
)); ));
} }
if num_loss > 0 { if num_loss > 0 {
@ -496,6 +504,7 @@ fn contingency_table(ctx: &mut EventCtx, app: &App, filter: &Filter) -> Widget {
Line(format!("Lost {} in total", total_loss.to_rounded_string(1))) Line(format!("Lost {} in total", total_loss.to_rounded_string(1)))
.fg(Color::hex("#EB3223")), .fg(Color::hex("#EB3223")),
]), ]),
None,
)); ));
} }
x1 += bar_width; x1 += bar_width;

View File

@ -1,10 +1,11 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt::Display; use std::fmt::Display;
use abstutil::{abbreviated_format, prettyprint_usize}; use abstutil::{abbreviated_format, prettyprint_usize};
use geom::{Angle, Distance, Duration, Line, Polygon, Pt2D, Time}; use geom::{Angle, Distance, Duration, Line, Polygon, Pt2D, Time};
use map_gui::tools::ColorScale; use map_gui::tools::ColorScale;
use sim::{Problem, TripMode}; use sim::{Problem, TripID, TripMode};
use widgetry::{Color, DrawWithTooltips, GeomBatch, GeomBatchStack, StackAlignment, Text, Widget}; use widgetry::{Color, DrawWithTooltips, GeomBatch, GeomBatchStack, StackAlignment, Text, Widget};
use crate::{App, EventCtx}; use crate::{App, EventCtx};
@ -54,10 +55,15 @@ pub trait TripProblemFilter {
fn include_no_changes(&self) -> bool; fn include_no_changes(&self) -> bool;
// Returns: // Returns:
// 1) trip duration after changes // 1) trip ID
// 2) difference in number of matching problems, where positive means MORE problems after // 2) trip duration after changes
// 3) difference in number of matching problems, where positive means MORE problems after
// changes // changes
fn trip_problems(&self, app: &App, problem_type: ProblemType) -> Vec<(Duration, isize)> { fn trip_problems(
&self,
app: &App,
problem_type: ProblemType,
) -> Vec<(TripID, Duration, isize)> {
let before = app.prebaked(); let before = app.prebaked();
let after = app.primary.sim.get_analytics(); let after = app.primary.sim.get_analytics();
let empty = Vec::new(); let empty = Vec::new();
@ -73,7 +79,7 @@ pub trait TripProblemFilter {
if !self.include_no_changes() && count_after == count_before { if !self.include_no_changes() && count_after == count_before {
continue; continue;
} }
points.push((time_after, count_after - count_before)); points.push((id, time_after, count_after - count_before));
} }
} }
points points
@ -97,9 +103,14 @@ lazy_static::lazy_static! {
static ref CLEAR_COLOR_SCALE: ColorScale = ColorScale(vec![Color::CLEAR, Color::CLEAR]); static ref CLEAR_COLOR_SCALE: ColorScale = ColorScale(vec![Color::CLEAR, Color::CLEAR]);
} }
pub fn problem_matrix(ctx: &mut EventCtx, app: &App, trips: &[(Duration, isize)]) -> Widget { /// The `title` is just used to generate unique labels. Returns a widget and a mapping from
let points = trips; /// `Outcome::Clicked` labels to the list of trips matching the bucket.
pub fn problem_matrix(
ctx: &mut EventCtx,
app: &App,
title: &str,
trips: Vec<(TripID, Duration, isize)>,
) -> (Widget, HashMap<String, Vec<TripID>>) {
let duration_buckets = vec![ let duration_buckets = vec![
Duration::ZERO, Duration::ZERO,
Duration::minutes(5), Duration::minutes(5),
@ -110,14 +121,15 @@ pub fn problem_matrix(ctx: &mut EventCtx, app: &App, trips: &[(Duration, isize)]
]; ];
let num_buckets = 7; let num_buckets = 7;
let mut matrix = Matrix::new(duration_buckets, bucketize_isizes(num_buckets, points)); let mut matrix = Matrix::new(duration_buckets, bucketize_isizes(num_buckets, &trips));
for (x, y) in points { for (id, x, y) in trips {
matrix.add_pt(*x, *y); matrix.add_pt(id, x, y);
} }
matrix.draw( matrix.draw(
ctx, ctx,
app, app,
MatrixOptions { MatrixOptions {
title: title.to_string(),
total_width: 600.0, total_width: 600.0,
total_height: 600.0, total_height: 600.0,
color_scale_for_bucket: Box::new(|app, _, n| match n.cmp(&0) { color_scale_for_bucket: Box::new(|app, _, n| match n.cmp(&0) {
@ -186,17 +198,17 @@ pub fn problem_matrix(ctx: &mut EventCtx, app: &App, trips: &[(Duration, isize)]
) )
} }
/// Aka a 2D histogram. Counts the number of matching points in each cell. /// Aka a 2D histogram. Tracks matching IDs in each cell.
struct Matrix<X, Y> { struct Matrix<ID, X, Y> {
counts: Vec<usize>, entries: Vec<Vec<ID>>,
buckets_x: Vec<X>, buckets_x: Vec<X>,
buckets_y: Vec<Y>, buckets_y: Vec<Y>,
} }
impl<X: Copy + PartialOrd + Display, Y: Copy + PartialOrd + Display> Matrix<X, Y> { impl<ID, X: Copy + PartialOrd + Display, Y: Copy + PartialOrd + Display> Matrix<ID, X, Y> {
fn new(buckets_x: Vec<X>, buckets_y: Vec<Y>) -> Matrix<X, Y> { fn new(buckets_x: Vec<X>, buckets_y: Vec<Y>) -> Matrix<ID, X, Y> {
Matrix { Matrix {
counts: std::iter::repeat(0) entries: std::iter::repeat_with(Vec::new)
.take(buckets_x.len() * buckets_y.len()) .take(buckets_x.len() * buckets_y.len())
.collect(), .collect(),
buckets_x, buckets_x,
@ -204,7 +216,7 @@ impl<X: Copy + PartialOrd + Display, Y: Copy + PartialOrd + Display> Matrix<X, Y
} }
} }
fn add_pt(&mut self, x: X, y: Y) { fn add_pt(&mut self, id: ID, x: X, y: Y) {
// Find its bucket // Find its bucket
// TODO Unit test this // TODO Unit test this
let x_idx = self let x_idx = self
@ -220,7 +232,7 @@ impl<X: Copy + PartialOrd + Display, Y: Copy + PartialOrd + Display> Matrix<X, Y
.unwrap_or(self.buckets_y.len()) .unwrap_or(self.buckets_y.len())
- 1; - 1;
let idx = self.idx(x_idx, y_idx); let idx = self.idx(x_idx, y_idx);
self.counts[idx] += 1; self.entries[idx].push(id);
} }
fn idx(&self, x: usize, y: usize) -> usize { fn idx(&self, x: usize, y: usize) -> usize {
@ -228,21 +240,32 @@ impl<X: Copy + PartialOrd + Display, Y: Copy + PartialOrd + Display> Matrix<X, Y
y * self.buckets_x.len() + x y * self.buckets_x.len() + x
} }
fn draw(self, ctx: &mut EventCtx, app: &App, opts: MatrixOptions<X, Y>) -> Widget { fn draw(
mut self,
ctx: &mut EventCtx,
app: &App,
opts: MatrixOptions<X, Y>,
) -> (Widget, HashMap<String, Vec<ID>>) {
let mut grid_batch = GeomBatch::new(); let mut grid_batch = GeomBatch::new();
let mut tooltips = Vec::new(); let mut tooltips = Vec::new();
let cell_width = opts.total_width / (self.buckets_x.len() as f64); let cell_width = opts.total_width / (self.buckets_x.len() as f64);
let cell_height = opts.total_height / (self.buckets_y.len() as f64); let cell_height = opts.total_height / (self.buckets_y.len() as f64);
let cell = Polygon::rectangle(cell_width, cell_height); let cell = Polygon::rectangle(cell_width, cell_height);
let max_count = *self.counts.iter().max().unwrap() as f64; let max_count = self.entries.iter().map(|list| list.len()).max().unwrap() as f64;
let mut mapping = HashMap::new();
for x in 0..self.buckets_x.len() - 1 { for x in 0..self.buckets_x.len() - 1 {
for y in 0..self.buckets_y.len() - 1 { for y in 0..self.buckets_y.len() - 1 {
let is_first_xbucket = x == 0; let is_first_xbucket = x == 0;
let is_last_xbucket = x == self.buckets_x.len() - 2; let is_last_xbucket = x == self.buckets_x.len() - 2;
let is_middle_ybucket = y + 1 == self.buckets_y.len() / 2; let is_middle_ybucket = y + 1 == self.buckets_y.len() / 2;
let count = self.counts[self.idx(x, y)]; let idx = self.idx(x, y);
let count = self.entries[idx].len();
let bucket_label = format!("{}/{}", opts.title, idx);
if count != 0 {
mapping.insert(bucket_label.clone(), std::mem::take(&mut self.entries[idx]));
}
let color = if count == 0 { let color = if count == 0 {
widgetry::Color::CLEAR widgetry::Color::CLEAR
} else { } else {
@ -288,6 +311,7 @@ impl<X: Copy + PartialOrd + Display, Y: Copy + PartialOrd + Display> Matrix<X, Y
(self.buckets_y[y], self.buckets_y[y + 1]), (self.buckets_y[y], self.buckets_y[y + 1]),
count, count,
), ),
Some(bucket_label),
)); ));
} }
} }
@ -388,7 +412,7 @@ impl<X: Copy + PartialOrd + Display, Y: Copy + PartialOrd + Display> Matrix<X, Y
x_axis_scale x_axis_scale
}; };
for (polygon, _) in tooltips.iter_mut() { for (polygon, _, _) in &mut tooltips {
let mut translated = polygon.translate(y_axis_batch.get_bounds().width(), 0.0); let mut translated = polygon.translate(y_axis_batch.get_bounds().width(), 0.0);
std::mem::swap(&mut translated, polygon); std::mem::swap(&mut translated, polygon);
} }
@ -398,11 +422,21 @@ impl<X: Copy + PartialOrd + Display, Y: Copy + PartialOrd + Display> Matrix<X, Y
let mut chart = GeomBatchStack::horizontal(vec![y_axis_batch, col.batch()]); let mut chart = GeomBatchStack::horizontal(vec![y_axis_batch, col.batch()]);
chart.set_alignment(StackAlignment::Top); chart.set_alignment(StackAlignment::Top);
DrawWithTooltips::new_widget(ctx, chart.batch(), tooltips, Box::new(|_| GeomBatch::new())) (
DrawWithTooltips::new_widget(
ctx,
chart.batch(),
tooltips,
Box::new(|_| GeomBatch::new()),
),
mapping,
)
} }
} }
struct MatrixOptions<X, Y> { struct MatrixOptions<X, Y> {
// To disambiguate labels
title: String,
total_width: f64, total_width: f64,
total_height: f64, total_height: f64,
// (lower_bound, upper_bound) -> Cell Label // (lower_bound, upper_bound) -> Cell Label
@ -411,7 +445,7 @@ struct MatrixOptions<X, Y> {
tooltip_for_bucket: Box<dyn Fn((Option<X>, Option<X>), (Y, Y), usize) -> Text>, tooltip_for_bucket: Box<dyn Fn((Option<X>, Option<X>), (Y, Y), usize) -> Text>,
} }
fn bucketize_isizes(max_buckets: usize, pts: &[(Duration, isize)]) -> Vec<isize> { fn bucketize_isizes(max_buckets: usize, pts: &[(TripID, Duration, isize)]) -> Vec<isize> {
debug_assert!( debug_assert!(
max_buckets % 2 == 1, max_buckets % 2 == 1,
"num_buckets must be odd to have a symmetrical number of buckets around axis" "num_buckets must be odd to have a symmetrical number of buckets around axis"
@ -420,8 +454,8 @@ fn bucketize_isizes(max_buckets: usize, pts: &[(Duration, isize)]) -> Vec<isize>
let positive_buckets = (max_buckets - 1) / 2; let positive_buckets = (max_buckets - 1) / 2;
// uniformly sized integer buckets // uniformly sized integer buckets
let max = match pts.iter().max_by_key(|(_, cnt)| cnt.abs()) { let max = match pts.iter().max_by_key(|(_, _, cnt)| cnt.abs()) {
Some(t) if (t.1.abs() as usize) >= positive_buckets => t.1.abs(), Some(t) if (t.2.abs() as usize) >= positive_buckets => t.2.abs(),
_ => { _ => {
// Enforce a bucket width of at least 1. // Enforce a bucket width of at least 1.
let negative_buckets = -(positive_buckets as isize); let negative_buckets = -(positive_buckets as isize);

View File

@ -4,8 +4,9 @@ use map_gui::tools::PopupMsg;
use map_gui::ID; use map_gui::ID;
use sim::AlertLocation; use sim::AlertLocation;
use widgetry::{ use widgetry::{
Choice, Color, ControlState, EdgeInsets, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Choice, Color, ControlState, DrawWithTooltips, EdgeInsets, EventCtx, GeomBatch, GfxCtx,
Line, Outcome, Panel, PersistentSplit, ScreenDims, Text, TextExt, VerticalAlignment, Widget, HorizontalAlignment, Key, Line, Outcome, Panel, PersistentSplit, ScreenDims, Text, TextExt,
VerticalAlignment, Widget,
}; };
use crate::app::{App, Transition}; use crate::app::{App, Transition};
@ -253,7 +254,6 @@ impl TimePanel {
progress_bar.push(cursor_fg, triangle_poly); progress_bar.push(cursor_fg, triangle_poly);
} }
use widgetry::DrawWithTooltips;
let mut tooltip_text = Text::from("Finished Trips"); let mut tooltip_text = Text::from("Finished Trips");
tooltip_text.add_line(format!( tooltip_text.add_line(format!(
"{} ({}% of total)", "{} ({}% of total)",
@ -286,7 +286,7 @@ impl TimePanel {
let bounds = progress_bar.get_bounds(); let bounds = progress_bar.get_bounds();
let bounding_box = Polygon::rectangle(bounds.width(), bounds.height()); let bounding_box = Polygon::rectangle(bounds.width(), bounds.height());
let tooltip = vec![(bounding_box, tooltip_text)]; let tooltip = vec![(bounding_box, tooltip_text, None)];
DrawWithTooltips::new_widget(ctx, progress_bar, tooltip, Box::new(|_| GeomBatch::new())) DrawWithTooltips::new_widget(ctx, progress_bar, tooltip, Box::new(|_| GeomBatch::new()))
} }

View File

@ -293,7 +293,7 @@ impl<'a, 'c> Image<'a, 'c> {
DrawWithTooltips::new_widget( DrawWithTooltips::new_widget(
ctx, ctx,
batch, batch,
vec![(bounds.get_rectangle(), tooltip)], vec![(bounds.get_rectangle(), tooltip, None)],
Box::new(|_| GeomBatch::new()), Box::new(|_| GeomBatch::new()),
) )
} else { } else {

View File

@ -1,8 +1,8 @@
use geom::Polygon; use geom::Polygon;
use crate::{ use crate::{
Drawable, EventCtx, GeomBatch, GfxCtx, ScreenDims, ScreenPt, ScreenRectangle, Text, Widget, Drawable, EventCtx, GeomBatch, GfxCtx, Outcome, ScreenDims, ScreenPt, ScreenRectangle, Text,
WidgetImpl, WidgetOutput, Widget, WidgetImpl, WidgetOutput,
}; };
// Just draw something, no interaction. // Just draw something, no interaction.
@ -41,8 +41,9 @@ impl WidgetImpl for JustDraw {
pub struct DrawWithTooltips { pub struct DrawWithTooltips {
draw: Drawable, draw: Drawable,
tooltips: Vec<(Polygon, Text)>, tooltips: Vec<(Polygon, Text, Option<String>)>,
hover: Box<dyn Fn(&Polygon) -> GeomBatch>, hover: Box<dyn Fn(&Polygon) -> GeomBatch>,
hovering_on_idx: Option<usize>,
top_left: ScreenPt, top_left: ScreenPt,
dims: ScreenDims, dims: ScreenDims,
@ -50,20 +51,23 @@ pub struct DrawWithTooltips {
impl DrawWithTooltips { impl DrawWithTooltips {
/// `batch`: the `GeomBatch` to draw /// `batch`: the `GeomBatch` to draw
/// `tooltips`: (hitbox, text) tuples where each `text` is shown when the user hovers over /// `tooltips`: (hitbox, text, clickable label) tuples where each `text` is shown when the user hovers over
/// the respective `hitbox` /// the respective `hitbox`. If a label is present and the user clicks the
/// `hitbox`, then it acts like a button click. It's assumed the hitboxes are
/// non-overlapping.
/// `hover`: returns a GeomBatch to render upon hovering. Return an `GeomBox::new()` if /// `hover`: returns a GeomBatch to render upon hovering. Return an `GeomBox::new()` if
/// you want hovering to be a no-op /// you want hovering to be a no-op
pub fn new_widget( pub fn new_widget(
ctx: &EventCtx, ctx: &EventCtx,
batch: GeomBatch, batch: GeomBatch,
tooltips: Vec<(Polygon, Text)>, tooltips: Vec<(Polygon, Text, Option<String>)>,
hover: Box<dyn Fn(&Polygon) -> GeomBatch>, hover: Box<dyn Fn(&Polygon) -> GeomBatch>,
) -> Widget { ) -> Widget {
Widget::new(Box::new(DrawWithTooltips { Widget::new(Box::new(DrawWithTooltips {
dims: batch.get_dims(), dims: batch.get_dims(),
top_left: ScreenPt::new(0.0, 0.0), top_left: ScreenPt::new(0.0, 0.0),
hover, hover,
hovering_on_idx: None,
draw: ctx.upload(batch), draw: ctx.upload(batch),
tooltips, tooltips,
})) }))
@ -79,26 +83,40 @@ impl WidgetImpl for DrawWithTooltips {
self.top_left = top_left; self.top_left = top_left;
} }
fn event(&mut self, _: &mut EventCtx, _: &mut WidgetOutput) {} fn event(&mut self, ctx: &mut EventCtx, output: &mut WidgetOutput) {
if ctx.redo_mouseover() {
self.hovering_on_idx = None;
if let Some(cursor) = ctx.canvas.get_cursor_in_screen_space() {
if !ScreenRectangle::top_left(self.top_left, self.dims).contains(cursor) {
return;
}
let translated =
ScreenPt::new(cursor.x - self.top_left.x, cursor.y - self.top_left.y).to_pt();
for (idx, (hitbox, _, _)) in self.tooltips.iter().enumerate() {
if hitbox.contains_pt(translated) {
self.hovering_on_idx = Some(idx);
break;
}
}
}
}
if let Some(idx) = self.hovering_on_idx {
if ctx.normal_left_click() {
if let Some(ref label) = self.tooltips[idx].2 {
output.outcome = Outcome::Clicked(label.clone());
}
}
}
}
fn draw(&self, g: &mut GfxCtx) { fn draw(&self, g: &mut GfxCtx) {
g.redraw_at(self.top_left, &self.draw); g.redraw_at(self.top_left, &self.draw);
if let Some(idx) = self.hovering_on_idx {
if let Some(cursor) = g.canvas.get_cursor_in_screen_space() { let (hitbox, txt, _) = &self.tooltips[idx];
if !ScreenRectangle::top_left(self.top_left, self.dims).contains(cursor) { let extra = g.upload((self.hover)(hitbox));
return; g.redraw_at(self.top_left, &extra);
} g.draw_mouse_tooltip(txt.clone());
let translated =
ScreenPt::new(cursor.x - self.top_left.x, cursor.y - self.top_left.y).to_pt();
// TODO Assume regions are non-overlapping
for (region, txt) in &self.tooltips {
if region.contains_pt(translated) {
let extra = g.upload((self.hover)(region));
g.redraw_at(self.top_left, &extra);
g.draw_mouse_tooltip(txt.clone());
return;
}
}
} }
} }
} }