mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-11-28 03:35:51 +03:00
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:
parent
db435cd56d
commit
942f2292fc
@ -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);
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user