moving generic plotting stuff away from trip stats

This commit is contained in:
Dustin Carlino 2019-11-05 13:13:09 -08:00
parent 1a28768df3
commit 21724aa826
6 changed files with 255 additions and 260 deletions

View File

@ -3,6 +3,7 @@ mod associated;
mod colors;
mod info;
mod navigate;
mod plot;
mod route_explorer;
mod route_viewer;
mod shortcuts;
@ -16,6 +17,7 @@ pub use self::agent::AgentTools;
pub use self::colors::{
ColorLegend, ObjectColorer, ObjectColorerBuilder, RoadColorer, RoadColorerBuilder,
};
pub use self::plot::{Plot, Series};
pub use self::route_explorer::RouteExplorer;
pub use self::speed::SpeedControls;
pub use self::time::time_controls;

158
game/src/common/plot.rs Normal file
View File

@ -0,0 +1,158 @@
use crate::common::ColorLegend;
use ezgui::{
Color, Drawable, EventCtx, GeomBatch, GfxCtx, Line, MultiText, ScreenPt, ScreenRectangle, Text,
};
use geom::{Distance, Duration, PolyLine, Polygon, Pt2D};
pub struct Plot {
draw: Drawable,
legend: ColorLegend,
labels: MultiText,
rect: ScreenRectangle,
}
impl Plot {
pub fn new<T: Ord + PartialEq + Copy + Yvalue<T>>(
title: &str,
series: Vec<Series<T>>,
ctx: &EventCtx,
) -> Option<Plot> {
let mut batch = GeomBatch::new();
let mut labels = MultiText::new();
let x1 = 0.1 * ctx.canvas.window_width;
let x2 = 0.7 * ctx.canvas.window_width;
let y1 = 0.2 * 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),
),
);
// Assume min_x is Duration::ZERO and min_y is 0
let max_x = series
.iter()
.map(|s| s.pts.iter().map(|(t, _)| *t).max().unwrap())
.max()
.unwrap();
let max_y = series
.iter()
.map(|s| s.pts.iter().map(|(_, cnt)| *cnt).max().unwrap())
.max()
.unwrap();
if max_x == Duration::ZERO {
return None;
}
let num_x_labels = 5;
for i in 0..num_x_labels {
let percent_x = (i as f64) / ((num_x_labels - 1) as f64);
let t = percent_x * max_x;
labels.add(
Text::from(Line(t.to_string())),
ScreenPt::new(x1 + percent_x * (x2 - x1), y2),
);
}
let num_y_labels = 5;
for i in 0..num_y_labels {
let percent_y = (i as f64) / ((num_y_labels - 1) as f64);
labels.add(
Text::from(Line(max_y.from_percent(percent_y).prettyprint())),
ScreenPt::new(x1, y2 - percent_y * (y2 - y1)),
);
}
let legend = ColorLegend::new(
Text::prompt(title),
series.iter().map(|s| (s.label.as_str(), s.color)).collect(),
);
for s in series {
let mut pts = Vec::new();
if max_y == T::zero() {
pts.push(Pt2D::new(x1, y2));
pts.push(Pt2D::new(x2, y2));
} else {
for (t, y) in s.pts {
let percent_x = t / max_x;
let percent_y = y.to_percent(max_y);
pts.push(Pt2D::new(
x1 + (x2 - x1) * percent_x,
// Y inversion! :D
y2 - (y2 - y1) * percent_y,
));
}
}
batch.push(
s.color,
PolyLine::new(pts).make_polygons(Distance::meters(5.0)),
);
}
Some(Plot {
draw: ctx.prerender.upload(batch),
labels,
legend,
rect: ScreenRectangle { x1, y1, x2, y2 },
})
}
pub fn draw(&self, g: &mut GfxCtx) {
self.legend.draw(g);
g.fork_screenspace();
g.redraw(&self.draw);
g.unfork();
self.labels.draw(g);
g.canvas.mark_covered_area(self.rect.clone());
}
}
pub trait Yvalue<T> {
// percent is [0.0, 1.0]
fn from_percent(self, percent: f64) -> T;
fn to_percent(self, max: T) -> f64;
fn prettyprint(self) -> String;
fn zero() -> T;
}
impl Yvalue<usize> for usize {
fn from_percent(self, percent: f64) -> usize {
((self as f64) * percent) as usize
}
fn to_percent(self, max: usize) -> f64 {
(self as f64) / (max as f64)
}
fn prettyprint(self) -> String {
abstutil::prettyprint_usize(self)
}
fn zero() -> usize {
0
}
}
impl Yvalue<Duration> for Duration {
fn from_percent(self, percent: f64) -> Duration {
percent * self
}
fn to_percent(self, max: Duration) -> f64 {
self / max
}
fn prettyprint(self) -> String {
self.minimal_tostring()
}
fn zero() -> Duration {
Duration::ZERO
}
}
pub struct Series<T> {
pub label: String,
pub color: Color,
// X-axis is time. Assume this is sorted by X.
pub pts: Vec<(Duration, T)>,
}

View File

@ -1,7 +1,8 @@
use crate::common::{Plot, Series};
use crate::game::{msg, Transition, WizardState};
use crate::render::AgentColorScheme;
use crate::sandbox::overlays::Overlays;
use crate::sandbox::{bus_explorer, spawner, trip_stats, SandboxMode};
use crate::sandbox::{bus_explorer, spawner, SandboxMode};
use crate::ui::UI;
use abstutil::{prettyprint_usize, Timer};
use ezgui::{hotkey, Choice, Color, EventCtx, GfxCtx, Key, Line, ModalMenu, Text, Wizard};
@ -275,7 +276,7 @@ impl GameplayState {
},
*time != ui.primary.sim.time(),
) {
if let Some(s) = trip_stats::ShowTripStats::bus_delays(route, ui, ctx) {
if let Some(s) = bus_delays(route, ui, ctx) {
*overlays = Overlays::BusDelaysOverTime(s);
} else {
println!("No route delay info yet");
@ -424,6 +425,29 @@ fn bus_route_panel(id: BusRouteID, ui: &UI, stat: Statistic, prebaked: &Analytic
txt
}
fn bus_delays(route: BusRouteID, ui: &UI, ctx: &mut EventCtx) -> Option<Plot> {
let delays_per_stop = ui
.primary
.sim
.get_analytics()
.bus_arrivals_over_time(ui.primary.sim.time(), route);
if delays_per_stop.is_empty() {
return None;
}
let mut series = Vec::new();
for (stop, delays) in delays_per_stop {
series.push(Series {
// TODO idx
label: stop.to_string(),
color: Color::RED,
pts: delays,
});
break;
}
Plot::new(&format!("delays for {}", route), series, ctx)
}
fn gridlock_panel(ui: &UI, _prebaked: &Analytics) -> Text {
let now = GridlockDelays::from(&ui.primary.sim);
// TODO Derive this from something in Analytics

View File

@ -3,7 +3,6 @@ mod gameplay;
mod overlays;
mod score;
mod spawner;
mod trip_stats;
use crate::common::{time_controls, AgentTools, CommonState, SpeedControls};
use crate::debug::DebugMode;

View File

@ -1,5 +1,6 @@
use super::trip_stats::ShowTripStats;
use crate::common::{ObjectColorer, ObjectColorerBuilder, RoadColorer, RoadColorerBuilder};
use crate::common::{
ObjectColorer, ObjectColorerBuilder, Plot, RoadColorer, RoadColorerBuilder, Series,
};
use crate::game::{Transition, WizardState};
use crate::helpers::ID;
use crate::render::DrawOptions;
@ -10,20 +11,20 @@ use abstutil::{prettyprint_usize, Counter};
use ezgui::{Choice, Color, EventCtx, GfxCtx, Line, MenuUnderButton, Text};
use geom::Duration;
use map_model::PathStep;
use sim::ParkingSpot;
use std::collections::HashSet;
use sim::{ParkingSpot, TripMode};
use std::collections::{BTreeMap, HashSet};
pub enum Overlays {
Inactive,
ParkingAvailability(Duration, RoadColorer),
IntersectionDelay(Duration, ObjectColorer),
Throughput(Duration, ObjectColorer),
FinishedTrips(Duration, ShowTripStats),
FinishedTrips(Duration, Plot),
Chokepoints(Duration, ObjectColorer),
BikeNetwork(RoadColorer),
// Only set by certain gameplay modes
BusRoute(ShowBusRoute),
BusDelaysOverTime(ShowTripStats),
BusDelaysOverTime(Plot),
}
impl Overlays {
@ -121,7 +122,7 @@ impl Overlays {
}
"cumulative throughput" => Overlays::Throughput(time, calculate_thruput(ctx, ui)),
"finished trips" => {
if let Some(s) = ShowTripStats::new(ui, ctx) {
if let Some(s) = trip_stats(ui, ctx) {
Overlays::FinishedTrips(time, s)
} else {
println!("No data on finished trips yet");
@ -341,3 +342,64 @@ fn calculate_bike_network(ctx: &mut EventCtx, ui: &UI) -> RoadColorer {
}
colorer.build(ctx, &ui.primary.map)
}
fn trip_stats(ui: &UI, ctx: &mut EventCtx) -> Option<Plot> {
if ui.primary.sim.get_analytics().finished_trips.is_empty() {
return None;
}
let lines: Vec<(&str, Color, Option<TripMode>)> = vec![
(
"walking",
ui.cs.get("unzoomed pedestrian"),
Some(TripMode::Walk),
),
("biking", ui.cs.get("unzoomed bike"), Some(TripMode::Bike)),
(
"transit",
ui.cs.get("unzoomed bus"),
Some(TripMode::Transit),
),
("driving", ui.cs.get("unzoomed car"), Some(TripMode::Drive)),
("aborted", Color::PURPLE.alpha(0.5), None),
];
// What times do we use for interpolation?
let num_x_pts = 100;
let mut times = Vec::new();
for i in 0..num_x_pts {
let percent_x = (i as f64) / ((num_x_pts - 1) as f64);
let t = ui.primary.sim.time() * percent_x;
times.push(t);
}
// Gather the data
let mut counts = Counter::new();
let mut pts_per_mode: BTreeMap<Option<TripMode>, Vec<(Duration, usize)>> =
lines.iter().map(|(_, _, m)| (*m, Vec::new())).collect();
for (t, m, _) in &ui.primary.sim.get_analytics().finished_trips {
counts.inc(*m);
if *t > times[0] {
times.remove(0);
for (_, _, mode) in &lines {
pts_per_mode
.get_mut(mode)
.unwrap()
.push((*t, counts.get(*mode)));
}
}
}
Plot::new(
"finished trips",
lines
.into_iter()
.map(|(name, color, m)| Series {
label: name.to_string(),
color,
pts: pts_per_mode.remove(&m).unwrap(),
})
.collect(),
ctx,
)
}

View File

@ -1,250 +0,0 @@
use crate::common::ColorLegend;
use crate::ui::UI;
use abstutil::Counter;
use ezgui::{
Color, Drawable, EventCtx, GeomBatch, GfxCtx, Line, MultiText, ScreenPt, ScreenRectangle, Text,
};
use geom::{Distance, Duration, PolyLine, Polygon, Pt2D};
use map_model::BusRouteID;
use sim::TripMode;
use std::collections::BTreeMap;
// TODO Show active trips too
pub struct ShowTripStats {
draw: Drawable,
legend: ColorLegend,
labels: MultiText,
rect: ScreenRectangle,
}
impl ShowTripStats {
pub fn new(ui: &UI, ctx: &mut EventCtx) -> Option<ShowTripStats> {
if ui.primary.sim.get_analytics().finished_trips.is_empty() {
return None;
}
let lines: Vec<(&str, Color, Option<TripMode>)> = vec![
(
"walking",
ui.cs.get("unzoomed pedestrian"),
Some(TripMode::Walk),
),
("biking", ui.cs.get("unzoomed bike"), Some(TripMode::Bike)),
(
"transit",
ui.cs.get("unzoomed bus"),
Some(TripMode::Transit),
),
("driving", ui.cs.get("unzoomed car"), Some(TripMode::Drive)),
("aborted", Color::PURPLE.alpha(0.5), None),
];
// What times do we use for interpolation?
let num_x_pts = 100;
let mut times = Vec::new();
for i in 0..num_x_pts {
let percent_x = (i as f64) / ((num_x_pts - 1) as f64);
let t = ui.primary.sim.time() * percent_x;
times.push(t);
}
// Gather the data
let mut counts = Counter::new();
let mut pts_per_mode: BTreeMap<Option<TripMode>, Vec<(Duration, usize)>> =
lines.iter().map(|(_, _, m)| (*m, Vec::new())).collect();
for (t, m, _) in &ui.primary.sim.get_analytics().finished_trips {
counts.inc(*m);
if *t > times[0] {
times.remove(0);
for (_, _, mode) in &lines {
pts_per_mode
.get_mut(mode)
.unwrap()
.push((*t, counts.get(*mode)));
}
}
}
plot(
"finished trips",
lines
.into_iter()
.map(|(name, color, m)| Series {
label: name.to_string(),
color,
pts: pts_per_mode.remove(&m).unwrap(),
})
.collect(),
ctx,
)
}
// TODO lumped in here temporarily
pub fn bus_delays(route: BusRouteID, ui: &UI, ctx: &mut EventCtx) -> Option<ShowTripStats> {
let delays_per_stop = ui
.primary
.sim
.get_analytics()
.bus_arrivals_over_time(ui.primary.sim.time(), route);
if delays_per_stop.is_empty() {
return None;
}
let mut series = Vec::new();
for (stop, delays) in delays_per_stop {
series.push(Series {
// TODO idx
label: stop.to_string(),
color: Color::RED,
pts: delays,
});
break;
}
plot(&format!("delays for {}", route), series, ctx)
}
pub fn draw(&self, g: &mut GfxCtx) {
self.legend.draw(g);
g.fork_screenspace();
g.redraw(&self.draw);
g.unfork();
self.labels.draw(g);
g.canvas.mark_covered_area(self.rect.clone());
}
}
trait Yvalue<T> {
// percent is [0.0, 1.0]
fn from_percent(self, percent: f64) -> T;
fn to_percent(self, max: T) -> f64;
fn prettyprint(self) -> String;
fn zero() -> T;
}
impl Yvalue<usize> for usize {
fn from_percent(self, percent: f64) -> usize {
((self as f64) * percent) as usize
}
fn to_percent(self, max: usize) -> f64 {
(self as f64) / (max as f64)
}
fn prettyprint(self) -> String {
abstutil::prettyprint_usize(self)
}
fn zero() -> usize {
0
}
}
impl Yvalue<Duration> for Duration {
fn from_percent(self, percent: f64) -> Duration {
percent * self
}
fn to_percent(self, max: Duration) -> f64 {
self / max
}
fn prettyprint(self) -> String {
self.minimal_tostring()
}
fn zero() -> Duration {
Duration::ZERO
}
}
struct Series<T> {
label: String,
color: Color,
// X-axis is time. Assume this is sorted by X.
pts: Vec<(Duration, T)>,
}
fn plot<T: Ord + PartialEq + Copy + Yvalue<T>>(
title: &str,
series: Vec<Series<T>>,
ctx: &EventCtx,
) -> Option<ShowTripStats> {
let mut batch = GeomBatch::new();
let mut labels = MultiText::new();
let x1 = 0.1 * ctx.canvas.window_width;
let x2 = 0.7 * ctx.canvas.window_width;
let y1 = 0.2 * 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),
),
);
// Assume min_x is Duration::ZERO and min_y is 0
let max_x = series
.iter()
.map(|s| s.pts.iter().map(|(t, _)| *t).max().unwrap())
.max()
.unwrap();
let max_y = series
.iter()
.map(|s| s.pts.iter().map(|(_, cnt)| *cnt).max().unwrap())
.max()
.unwrap();
if max_x == Duration::ZERO {
return None;
}
let num_x_labels = 5;
for i in 0..num_x_labels {
let percent_x = (i as f64) / ((num_x_labels - 1) as f64);
let t = percent_x * max_x;
labels.add(
Text::from(Line(t.to_string())),
ScreenPt::new(x1 + percent_x * (x2 - x1), y2),
);
}
let num_y_labels = 5;
for i in 0..num_y_labels {
let percent_y = (i as f64) / ((num_y_labels - 1) as f64);
labels.add(
Text::from(Line(max_y.from_percent(percent_y).prettyprint())),
ScreenPt::new(x1, y2 - percent_y * (y2 - y1)),
);
}
let legend = ColorLegend::new(
Text::prompt(title),
series.iter().map(|s| (s.label.as_str(), s.color)).collect(),
);
for s in series {
let mut pts = Vec::new();
if max_y == T::zero() {
pts.push(Pt2D::new(x1, y2));
pts.push(Pt2D::new(x2, y2));
} else {
for (t, y) in s.pts {
let percent_x = t / max_x;
let percent_y = y.to_percent(max_y);
pts.push(Pt2D::new(
x1 + (x2 - x1) * percent_x,
// Y inversion! :D
y2 - (y2 - y1) * percent_y,
));
}
}
batch.push(
s.color,
PolyLine::new(pts).make_polygons(Distance::meters(5.0)),
);
}
Some(ShowTripStats {
draw: ctx.prerender.upload(batch),
labels,
legend,
rect: ScreenRectangle { x1, y1, x2, y2 },
})
}