started a histogram to show relative trip durations for fixing traffic

signals. lots of work needed.
This commit is contained in:
Dustin Carlino 2019-12-09 12:50:55 -08:00
parent 7ca08dfaff
commit 954ac97a9f
7 changed files with 196 additions and 4 deletions

View File

@ -16,7 +16,7 @@ pub use self::colors::{
ColorLegend, ObjectColorer, ObjectColorerBuilder, RoadColorer, RoadColorerBuilder,
};
pub use self::minimap::Minimap;
pub use self::plot::{Plot, Series};
pub use self::plot::{Histogram, Plot, Series};
pub use self::route_explorer::RouteExplorer;
pub use self::trip_explorer::TripExplorer;
pub use self::warp::Warping;

View File

@ -4,7 +4,9 @@ use ezgui::{
};
use geom::{Bounds, Circle, Distance, Duration, FindClosest, PolyLine, Polygon, Pt2D, Time};
// The X is always time
pub struct Plot<T> {
// TODO Could DrawBoth instead of MultiText here
draw: Drawable,
legend: ColorLegend,
labels: MultiText,
@ -200,3 +202,101 @@ pub struct Series<T> {
// X-axis is time. Assume this is sorted by X.
pub pts: Vec<(Time, T)>,
}
// The X axis is Durations, with positive meaning "faster" (considered good) and negative "slower"
pub struct Histogram {
draw: Drawable,
labels: MultiText,
rect: ScreenRectangle,
}
impl Histogram {
pub fn new(title: &str, unsorted_dts: Vec<Duration>, ctx: &EventCtx) -> Histogram {
let mut batch = GeomBatch::new();
let mut labels = MultiText::new();
let x1 = 0.5 * ctx.canvas.window_width;
let x2 = 0.9 * ctx.canvas.window_width;
let y1 = 0.4 * ctx.canvas.window_height;
let y2 = 0.8 * ctx.canvas.window_height;
batch.push(
Color::grey(0.8),
Polygon::rectangle_topleft(
Pt2D::new(x1, y1),
Distance::meters(x2 - x1),
Distance::meters(y2 - y1),
),
);
if unsorted_dts.len() < 10 {
// TODO Add some warning label or something
} else {
// TODO Generic "bucket into 10 groups, give (min, max, count)"
let min_x = *unsorted_dts.iter().min().unwrap();
let max_x = *unsorted_dts.iter().max().unwrap();
let num_buckets = 10;
let bucket_size = (max_x - min_x) / (num_buckets as f64);
// lower, upper, count
let mut bars: Vec<(Duration, Duration, usize)> = (0..num_buckets)
.map(|idx| {
let i = idx as f64;
(min_x + bucket_size * i, min_x + bucket_size * (i + 1.0), 0)
})
.collect();
for dt in unsorted_dts {
// TODO Could sort them and do this more efficiently.
if dt == max_x {
// Most bars represent [low, high) except the last
bars[num_buckets - 1].2 += 1;
} else {
let bin = ((dt - min_x) / bucket_size).floor() as usize;
bars[bin].2 += 1;
}
}
let min_y = 0;
let max_y = bars.iter().map(|(_, _, cnt)| *cnt).max().unwrap();
for (idx, (min, max, cnt)) in bars.into_iter().enumerate() {
// TODO Or maybe the average?
let color = if min < Duration::ZERO {
Color::RED
} else {
Color::GREEN
};
let percent_x_left = (idx as f64) / (num_buckets as f64);
let percent_x_right = ((idx + 1) as f64) / (num_buckets as f64);
if let Some(rect) = Polygon::rectangle_two_corners(
// Top-left
Pt2D::new(
x1 + (x2 - x1) * percent_x_left,
y2 - (y2 - y1) * ((cnt as f64) / ((max_y - min_y) as f64)),
),
// Bottom-right
Pt2D::new(x1 + (x2 - x1) * percent_x_right, y2),
) {
batch.push(color, rect);
}
labels.add(
Text::from(Line(min.to_string())),
ScreenPt::new(x1 + (x2 - x1) * percent_x_left, y2),
);
}
}
Histogram {
draw: batch.upload(ctx),
labels,
rect: ScreenRectangle { x1, y1, x2, y2 },
}
}
pub fn draw(&self, g: &mut GfxCtx) {
g.canvas.mark_covered_area(self.rect.clone());
g.fork_screenspace();
g.redraw(&self.draw);
g.unfork();
self.labels.draw(g);
}
}

View File

@ -19,6 +19,7 @@ impl FixTrafficSignals {
"Fix traffic signals",
vec![
(hotkey(Key::F), "find slowest traffic signals"),
(hotkey(Key::D), "show finished trip distribution"),
(hotkey(Key::H), "help"),
(hotkey(Key::S), "final score"),
],
@ -57,6 +58,20 @@ impl GameplayState for FixTrafficSignals {
) {
*overlays = Overlays::intersection_delay(ctx, ui);
}
if manage_overlays(
menu,
ctx,
"show finished trip distribution",
"hide finished trip distribution",
overlays,
match overlays {
Overlays::FinishedTripsHistogram(_, _) => true,
_ => false,
},
self.time != ui.primary.sim.time(),
) {
*overlays = Overlays::finished_trips_histogram(ctx, ui, prebaked);
}
if self.time != ui.primary.sim.time() {
self.time = ui.primary.sim.time();

View File

@ -125,7 +125,10 @@ impl State for SandboxMode {
if let Some(t) = self.common.event(ctx, ui) {
return t;
}
if let Some(t) = self.overlay.event(ctx, ui, &mut self.info_tools) {
if let Some(t) = self
.overlay
.event(ctx, ui, &mut self.info_tools, &self.gameplay.prebaked)
{
return t;
}
self.minimap.event(ui, ctx);

View File

@ -1,5 +1,5 @@
use crate::common::{
ObjectColorer, ObjectColorerBuilder, Plot, RoadColorer, RoadColorerBuilder, Series,
Histogram, ObjectColorer, ObjectColorerBuilder, Plot, RoadColorer, RoadColorerBuilder, Series,
};
use crate::game::{Transition, WizardState};
use crate::helpers::{rotating_color, ID};
@ -11,7 +11,7 @@ use abstutil::{prettyprint_usize, Counter};
use ezgui::{Choice, Color, EventCtx, GfxCtx, Key, Line, MenuUnderButton, Text};
use geom::{Duration, Statistic, Time};
use map_model::{IntersectionID, LaneID, PathConstraints, PathStep, RoadID};
use sim::{ParkingSpot, TripMode};
use sim::{Analytics, ParkingSpot, TripMode};
use std::collections::{BTreeMap, HashSet};
pub enum Overlays {
@ -20,6 +20,7 @@ pub enum Overlays {
IntersectionDelay(Time, ObjectColorer),
CumulativeThroughput(Time, ObjectColorer),
FinishedTrips(Time, Plot<usize>),
FinishedTripsHistogram(Time, Histogram),
Chokepoints(Time, ObjectColorer),
BikeNetwork(RoadColorer),
BikePathCosts(RoadColorer),
@ -53,6 +54,7 @@ impl Overlays {
ctx: &mut EventCtx,
ui: &UI,
menu: &mut MenuUnderButton,
baseline: &Analytics,
) -> Option<Transition> {
if menu.action("change analytics overlay") {
return Some(Transition::Push(WizardState::new(Box::new(
@ -66,6 +68,8 @@ impl Overlays {
Choice::new("intersection delay", ()).key(Key::I),
Choice::new("cumulative throughput", ()).key(Key::T),
Choice::new("finished trips", ()).key(Key::F),
// TODO baseline borrow doesn't live long enough
//Choice::new("finished trips histogram", ()).key(Key::H),
Choice::new("chokepoints", ()).key(Key::C),
Choice::new("bike network", ()).key(Key::B),
Choice::new("bike path costs", ()).key(Key::X),
@ -116,6 +120,9 @@ impl Overlays {
Overlays::FinishedTrips(t, _) if now != *t => {
*self = Overlays::finished_trips(ctx, ui);
}
Overlays::FinishedTripsHistogram(t, _) if now != *t => {
*self = Overlays::finished_trips_histogram(ctx, ui, baseline);
}
Overlays::Chokepoints(t, _) if now != *t => {
*self = Overlays::chokepoints(ctx, ui);
}
@ -153,6 +160,16 @@ impl Overlays {
plot.draw(g);
true
}
Overlays::FinishedTripsHistogram(_, ref hgram) => {
ui.draw(
g,
DrawOptions::new(),
&ui.primary.sim,
&ShowEverything::new(),
);
hgram.draw(g);
true
}
Overlays::BusDelaysOverTime(ref plot)
| Overlays::IntersectionDelayOverTime { ref plot, .. } => {
ui.draw(
@ -560,6 +577,21 @@ impl Overlays {
Overlays::FinishedTrips(ui.primary.sim.time(), plot)
}
pub fn finished_trips_histogram(ctx: &EventCtx, ui: &UI, baseline: &Analytics) -> Overlays {
let now = ui.primary.sim.time();
Overlays::FinishedTripsHistogram(
now,
Histogram::new(
"Finished trip time deltas",
ui.primary
.sim
.get_analytics()
.finished_trip_deltas(now, baseline),
ctx,
),
)
}
fn bike_path_costs(ctx: &EventCtx, ui: &UI) -> Overlays {
let mut cost_per_lane: BTreeMap<LaneID, usize> = BTreeMap::new();
for l in ui.primary.map.all_lanes() {

View File

@ -221,6 +221,17 @@ impl ops::Div<Duration> for Duration {
}
}
impl ops::Div<f64> for Duration {
type Output = Duration;
fn div(self, other: f64) -> Duration {
if other == 0.0 {
panic!("Can't divide {} / {}", self, other);
}
Duration::seconds(self.0 / other)
}
}
impl ops::Rem<Duration> for Duration {
type Output = Duration;

View File

@ -175,6 +175,37 @@ impl Analytics {
(all, num_aborted, per_mode)
}
// Returns unsorted list of deltas, one for each trip finished in both worlds. Positive dt
// means faster.
pub fn finished_trip_deltas(&self, now: Time, baseline: &Analytics) -> Vec<Duration> {
let a: BTreeMap<TripID, Duration> = self
.finished_trips
.iter()
.filter_map(|(t, id, mode, dt)| {
if *t <= now && mode.is_some() {
Some((*id, *dt))
} else {
None
}
})
.collect();
let b: BTreeMap<TripID, Duration> = baseline
.finished_trips
.iter()
.filter_map(|(t, id, mode, dt)| {
if *t <= now && mode.is_some() {
Some((*id, *dt))
} else {
None
}
})
.collect();
a.into_iter()
.filter_map(|(id, dt1)| b.get(&id).map(|dt2| *dt2 - dt1))
.collect()
}
pub fn bus_arrivals(&self, now: Time, r: BusRouteID) -> BTreeMap<BusStopID, DurationHistogram> {
let mut per_bus: BTreeMap<CarID, Vec<(Time, BusStopID)>> = BTreeMap::new();
for (t, car, route, stop) in &self.bus_arrivals {