mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-29 01:13:53 +03:00
Create a contingency matrix showing how number of problems change per
trip duration. #600
This commit is contained in:
parent
af309bcd56
commit
0ed933aa9c
@ -11,6 +11,7 @@ mod commuter;
|
||||
mod generic_trip_table;
|
||||
mod misc;
|
||||
mod parking_overhead;
|
||||
mod risks;
|
||||
mod selector;
|
||||
mod summaries;
|
||||
mod traffic_signals;
|
||||
@ -21,6 +22,7 @@ mod trip_table;
|
||||
pub enum DashTab {
|
||||
TripTable,
|
||||
TripSummaries,
|
||||
RiskSummaries,
|
||||
ParkingOverhead,
|
||||
ActiveTraffic,
|
||||
TransitRoutes,
|
||||
@ -33,6 +35,7 @@ impl DashTab {
|
||||
let mut choices = vec![
|
||||
Choice::new("Trip Table", DashTab::TripTable),
|
||||
Choice::new("Trip Summaries", DashTab::TripSummaries),
|
||||
Choice::new("Risk exposure", DashTab::RiskSummaries),
|
||||
Choice::new("Parking Overhead", DashTab::ParkingOverhead),
|
||||
Choice::new("Active Traffic", DashTab::ActiveTraffic),
|
||||
Choice::new("Transit Routes", DashTab::TransitRoutes),
|
||||
@ -41,6 +44,7 @@ impl DashTab {
|
||||
];
|
||||
if app.has_prebaked().is_none() {
|
||||
choices.remove(1);
|
||||
choices.remove(1);
|
||||
}
|
||||
Widget::row(vec![
|
||||
Image::from_path("system/assets/meters/trip_histogram.svg").into_widget(ctx),
|
||||
@ -69,6 +73,7 @@ impl DashTab {
|
||||
DashTab::TripSummaries => {
|
||||
summaries::TripSummaries::new(ctx, app, summaries::Filter::new())
|
||||
}
|
||||
DashTab::RiskSummaries => risks::RiskSummaries::new(ctx, app, risks::Filter::new()),
|
||||
DashTab::ParkingOverhead => parking_overhead::ParkingOverhead::new(ctx, app),
|
||||
DashTab::ActiveTraffic => misc::ActiveTraffic::new(ctx, app),
|
||||
DashTab::TransitRoutes => misc::TransitRoutes::new(ctx, app),
|
||||
|
331
game/src/sandbox/dashboards/risks.rs
Normal file
331
game/src/sandbox/dashboards/risks.rs
Normal file
@ -0,0 +1,331 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt::Display;
|
||||
|
||||
use abstutil::prettyprint_usize;
|
||||
use geom::{Duration, Polygon, Pt2D, Time};
|
||||
use map_gui::tools::ColorScale;
|
||||
use sim::{Problem, TripMode};
|
||||
use widgetry::{
|
||||
DrawWithTooltips, EventCtx, GeomBatch, GfxCtx, Line, Outcome, Panel, State, Text, TextExt,
|
||||
Toggle, Widget,
|
||||
};
|
||||
|
||||
use crate::app::{App, Transition};
|
||||
use crate::common::color_for_mode;
|
||||
use crate::sandbox::dashboards::DashTab;
|
||||
|
||||
pub struct RiskSummaries {
|
||||
panel: Panel,
|
||||
}
|
||||
|
||||
impl RiskSummaries {
|
||||
pub fn new(ctx: &mut EventCtx, app: &App, filter: Filter) -> Box<dyn State<App>> {
|
||||
let mut filters = Vec::new();
|
||||
for mode in TripMode::all() {
|
||||
filters.push(Toggle::colored_checkbox(
|
||||
ctx,
|
||||
mode.ongoing_verb(),
|
||||
color_for_mode(app, mode),
|
||||
filter.modes.contains(&mode),
|
||||
));
|
||||
}
|
||||
|
||||
Box::new(RiskSummaries {
|
||||
panel: Panel::new(Widget::col(vec![
|
||||
DashTab::RiskSummaries.picker(ctx, app),
|
||||
Widget::col(vec![
|
||||
"Filters".text_widget(ctx),
|
||||
Widget::row(filters),
|
||||
Toggle::checkbox(
|
||||
ctx,
|
||||
"include trips without any changes",
|
||||
None,
|
||||
filter.include_no_changes,
|
||||
),
|
||||
])
|
||||
.section(ctx),
|
||||
Widget::row(vec![
|
||||
Widget::col(vec![
|
||||
"Delays at an intersection".text_widget(ctx),
|
||||
safety_matrix(ctx, app, &filter, ProblemType::IntersectionDelay),
|
||||
])
|
||||
.section(ctx),
|
||||
Widget::col(vec![
|
||||
"Large intersection crossings".text_widget(ctx),
|
||||
safety_matrix(ctx, app, &filter, ProblemType::LargeIntersectionCrossing),
|
||||
])
|
||||
.section(ctx),
|
||||
Widget::col(vec![
|
||||
"Cars wanting to over-take cyclists".text_widget(ctx),
|
||||
safety_matrix(ctx, app, &filter, ProblemType::OvertakeDesired),
|
||||
])
|
||||
.section(ctx),
|
||||
])
|
||||
.evenly_spaced(),
|
||||
]))
|
||||
.exact_size_percent(90, 90)
|
||||
.build(ctx),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl State<App> for RiskSummaries {
|
||||
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
|
||||
match self.panel.event(ctx) {
|
||||
Outcome::Clicked(x) => match x.as_ref() {
|
||||
"close" => {
|
||||
return Transition::Pop;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Outcome::Changed(_) => {
|
||||
if let Some(t) = DashTab::RiskSummaries.transition(ctx, app, &self.panel) {
|
||||
return t;
|
||||
}
|
||||
|
||||
let mut filter = Filter {
|
||||
modes: BTreeSet::new(),
|
||||
include_no_changes: self.panel.is_checked("include trips without any changes"),
|
||||
};
|
||||
for m in TripMode::all() {
|
||||
if self.panel.is_checked(m.ongoing_verb()) {
|
||||
filter.modes.insert(m);
|
||||
}
|
||||
}
|
||||
Transition::Replace(RiskSummaries::new(ctx, app, filter))
|
||||
}
|
||||
_ => Transition::Keep,
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&self, g: &mut GfxCtx, _app: &App) {
|
||||
self.panel.draw(g);
|
||||
}
|
||||
}
|
||||
|
||||
fn safety_matrix(
|
||||
ctx: &mut EventCtx,
|
||||
app: &App,
|
||||
filter: &Filter,
|
||||
problem_type: ProblemType,
|
||||
) -> Widget {
|
||||
let points = filter.get_trips(app, problem_type);
|
||||
if points.is_empty() {
|
||||
return Widget::nothing();
|
||||
}
|
||||
|
||||
let num_buckets = 10;
|
||||
let mut matrix = Matrix::new(
|
||||
bucketize_duration(num_buckets, &points),
|
||||
bucketize_isizes(num_buckets, &points),
|
||||
);
|
||||
for (x, y) in points {
|
||||
matrix.add_pt(x, y);
|
||||
}
|
||||
matrix.draw(
|
||||
ctx,
|
||||
app,
|
||||
MatrixOptions {
|
||||
total_width: 500.0,
|
||||
total_height: 500.0,
|
||||
color_scale_for_bucket: Box::new(|app, _, n| {
|
||||
if n <= 0 {
|
||||
&app.cs.good_to_bad_green
|
||||
} else {
|
||||
&app.cs.good_to_bad_red
|
||||
}
|
||||
}),
|
||||
tooltip_for_bucket: Box::new(|(t1, t2), (problems1, problems2), count| {
|
||||
let mut txt = Text::from(Line(format!("Trips between {} and {}", t1, t2)));
|
||||
txt.add_line(if problems1 == 0 || problems2 == 0 {
|
||||
Line("with no changes in number of problems encountered")
|
||||
} else if problems1 < 0 {
|
||||
Line(format!(
|
||||
"with between {} and {} less problems encountered",
|
||||
-problems2, -problems1
|
||||
))
|
||||
} else {
|
||||
Line(format!(
|
||||
"with between {} and {} more problems encountered",
|
||||
problems1, problems2
|
||||
))
|
||||
});
|
||||
txt.add_line(Line(format!("Count: {} trips", prettyprint_usize(count))));
|
||||
txt
|
||||
}),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum ProblemType {
|
||||
IntersectionDelay,
|
||||
LargeIntersectionCrossing,
|
||||
OvertakeDesired,
|
||||
}
|
||||
|
||||
impl ProblemType {
|
||||
fn count(self, problems: &Vec<(Time, Problem)>) -> usize {
|
||||
let mut cnt = 0;
|
||||
for (_, problem) in problems {
|
||||
if match problem {
|
||||
Problem::IntersectionDelay(_, _) => self == ProblemType::IntersectionDelay,
|
||||
Problem::LargeIntersectionCrossing(_) => {
|
||||
self == ProblemType::LargeIntersectionCrossing
|
||||
}
|
||||
Problem::OvertakeDesired(_) => self == ProblemType::OvertakeDesired,
|
||||
} {
|
||||
cnt += 1;
|
||||
}
|
||||
}
|
||||
cnt
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Filter {
|
||||
modes: BTreeSet<TripMode>,
|
||||
include_no_changes: bool,
|
||||
}
|
||||
|
||||
impl Filter {
|
||||
pub fn new() -> Filter {
|
||||
Filter {
|
||||
modes: TripMode::all().into_iter().collect(),
|
||||
include_no_changes: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Returns:
|
||||
// 1) trip duration after changes
|
||||
// 2) difference in number of matching problems, where positive means MORE problems after
|
||||
// changes
|
||||
fn get_trips(&self, app: &App, problem_type: ProblemType) -> Vec<(Duration, isize)> {
|
||||
let before = app.prebaked();
|
||||
let after = app.primary.sim.get_analytics();
|
||||
let empty = Vec::new();
|
||||
|
||||
let mut points = Vec::new();
|
||||
for (id, _, time_after, mode) in after.both_finished_trips(app.primary.sim.time(), before) {
|
||||
if self.modes.contains(&mode) {
|
||||
let count_before = problem_type
|
||||
.count(before.problems_per_trip.get(&id).unwrap_or(&empty))
|
||||
as isize;
|
||||
let count_after =
|
||||
problem_type.count(after.problems_per_trip.get(&id).unwrap_or(&empty)) as isize;
|
||||
if !self.include_no_changes && count_after == count_before {
|
||||
continue;
|
||||
}
|
||||
points.push((time_after, count_after - count_before));
|
||||
}
|
||||
}
|
||||
points
|
||||
}
|
||||
}
|
||||
|
||||
/// Aka a 2D histogram. Counts the number of matching points in each cell.
|
||||
struct Matrix<X, Y> {
|
||||
counts: Vec<usize>,
|
||||
buckets_x: Vec<X>,
|
||||
buckets_y: Vec<Y>,
|
||||
}
|
||||
|
||||
impl<X: Copy + PartialOrd + Display, Y: Copy + PartialOrd + Display> Matrix<X, Y> {
|
||||
fn new(buckets_x: Vec<X>, buckets_y: Vec<Y>) -> Matrix<X, Y> {
|
||||
Matrix {
|
||||
counts: std::iter::repeat(0)
|
||||
.take(buckets_x.len() * buckets_y.len())
|
||||
.collect(),
|
||||
buckets_x,
|
||||
buckets_y,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_pt(&mut self, x: X, y: Y) {
|
||||
// Find its bucket
|
||||
// TODO Unit test this
|
||||
let x_idx = self
|
||||
.buckets_x
|
||||
.iter()
|
||||
.position(|min| *min > x)
|
||||
.unwrap_or(self.buckets_x.len())
|
||||
- 1;
|
||||
let y_idx = self
|
||||
.buckets_y
|
||||
.iter()
|
||||
.position(|min| *min > y)
|
||||
.unwrap_or(self.buckets_y.len())
|
||||
- 1;
|
||||
let idx = self.idx(x_idx, y_idx);
|
||||
self.counts[idx] += 1;
|
||||
}
|
||||
|
||||
fn idx(&self, x: usize, y: usize) -> usize {
|
||||
// Row-major
|
||||
y * self.buckets_x.len() + x
|
||||
}
|
||||
|
||||
fn draw(self, ctx: &mut EventCtx, app: &App, opts: MatrixOptions<X, Y>) -> Widget {
|
||||
let mut batch = GeomBatch::new();
|
||||
let mut tooltips = Vec::new();
|
||||
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 = Polygon::rectangle(cell_width, cell_height);
|
||||
|
||||
let max_count = *self.counts.iter().max().unwrap() as f64;
|
||||
|
||||
for x in 0..self.buckets_x.len() - 1 {
|
||||
for y in 0..self.buckets_y.len() - 1 {
|
||||
let count = self.counts[self.idx(x, y)];
|
||||
// TODO Different colors for better/worse? Or are we just showing density?
|
||||
let density_pct = (count as f64) / max_count;
|
||||
let color =
|
||||
(opts.color_scale_for_bucket)(app, self.buckets_x[x], self.buckets_y[y])
|
||||
.eval(density_pct);
|
||||
let x1 = cell_width * (x as f64);
|
||||
let y1 = cell_height * (y as f64);
|
||||
let rect = cell.clone().translate(x1, y1);
|
||||
batch.push(color, rect.clone());
|
||||
batch.append(
|
||||
Text::from(Line(prettyprint_usize(count)))
|
||||
.render(ctx)
|
||||
.centered_on(Pt2D::new(x1 + cell_width / 2.0, y1 + cell_height / 2.0)),
|
||||
);
|
||||
tooltips.push((
|
||||
rect,
|
||||
(opts.tooltip_for_bucket)(
|
||||
(self.buckets_x[x], self.buckets_x[x + 1]),
|
||||
(self.buckets_y[y], self.buckets_y[y + 1]),
|
||||
count,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
DrawWithTooltips::new(ctx, batch, tooltips, Box::new(|_| GeomBatch::new()))
|
||||
}
|
||||
}
|
||||
|
||||
struct MatrixOptions<X, Y> {
|
||||
total_width: f64,
|
||||
total_height: f64,
|
||||
color_scale_for_bucket: Box<dyn Fn(&App, X, Y) -> &ColorScale>,
|
||||
tooltip_for_bucket: Box<dyn Fn((X, X), (Y, Y), usize) -> Text>,
|
||||
}
|
||||
|
||||
fn bucketize_duration(num_buckets: usize, pts: &Vec<(Duration, isize)>) -> Vec<Duration> {
|
||||
let max = pts.iter().max_by_key(|(dt, _)| *dt).unwrap().0;
|
||||
let (_, mins) = max.make_intervals_for_max(num_buckets);
|
||||
mins.into_iter().map(|x| Duration::minutes(x)).collect()
|
||||
}
|
||||
|
||||
fn bucketize_isizes(num_buckets: usize, pts: &Vec<(Duration, isize)>) -> Vec<isize> {
|
||||
let min = pts.iter().min_by_key(|(_, cnt)| *cnt).unwrap().1;
|
||||
let max = pts.iter().max_by_key(|(_, cnt)| *cnt).unwrap().1;
|
||||
// TODO Rounding is wrong. We need to make sure to cover the min/max range...
|
||||
let step_size = ((max - min).abs() as f64) / (num_buckets as f64);
|
||||
let mut buckets = Vec::new();
|
||||
for i in 0..num_buckets {
|
||||
buckets.push(min + ((i as f64) * step_size) as isize);
|
||||
}
|
||||
buckets
|
||||
}
|
@ -117,10 +117,6 @@ impl State<App> for TripSummaries {
|
||||
}
|
||||
|
||||
fn summary_boxes(ctx: &mut EventCtx, app: &App, filter: &Filter) -> Widget {
|
||||
if app.has_prebaked().is_none() {
|
||||
return Widget::nothing();
|
||||
}
|
||||
|
||||
let mut num_same = 0;
|
||||
let mut num_faster = 0;
|
||||
let mut num_slower = 0;
|
||||
@ -218,10 +214,6 @@ fn summary_boxes(ctx: &mut EventCtx, app: &App, filter: &Filter) -> Widget {
|
||||
}
|
||||
|
||||
fn scatter_plot(ctx: &mut EventCtx, app: &App, filter: &Filter) -> Widget {
|
||||
if app.has_prebaked().is_none() {
|
||||
return Widget::nothing();
|
||||
}
|
||||
|
||||
let points = filter.get_trips(app);
|
||||
if points.is_empty() {
|
||||
return Widget::nothing();
|
||||
@ -249,10 +241,6 @@ fn scatter_plot(ctx: &mut EventCtx, app: &App, filter: &Filter) -> Widget {
|
||||
}
|
||||
|
||||
fn contingency_table(ctx: &mut EventCtx, app: &App, filter: &Filter) -> Widget {
|
||||
if app.has_prebaked().is_none() {
|
||||
return Widget::nothing();
|
||||
}
|
||||
|
||||
let total_width = 500.0;
|
||||
let total_height = 300.0;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user