try showing delay measurements as a raw scatter plot

This commit is contained in:
Dustin Carlino 2020-05-26 14:59:20 -07:00
parent 8d0981587a
commit 10bc4b9c32
4 changed files with 185 additions and 49 deletions

View File

@ -68,7 +68,7 @@ pub(crate) use crate::widgets::just_draw::JustDraw;
pub use crate::widgets::line_plot::{LinePlot, PlotOptions, Series};
pub(crate) use crate::widgets::menu::Menu;
pub use crate::widgets::persistent_split::PersistentSplit;
pub use crate::widgets::scatter_plot::ScatterPlot;
pub use crate::widgets::scatter_plot::{ScatterPlot, ScatterPlotV2};
pub use crate::widgets::slider::Slider;
pub use crate::widgets::spinner::Spinner;
pub(crate) use crate::widgets::text_box::TextBox;

View File

@ -106,7 +106,7 @@ impl<T: Yvalue<T>> LinePlot<T> {
// TODO Tuned to fit the info panel. Instead these should somehow stretch to fill their
// container.
let width = 0.25 * ctx.canvas.window_width;
let width = 0.23 * ctx.canvas.window_width;
let height = 0.2 * ctx.canvas.window_height;
let mut grid_batch = GeomBatch::new();
@ -201,7 +201,7 @@ impl<T: Yvalue<T>> LinePlot<T> {
draw_grid: ctx.upload(grid_batch),
closest,
max_x,
max_y: max_y,
max_y,
top_left: ScreenPt::new(0.0, 0.0),
dims: ScreenDims::new(width, height),

View File

@ -1,8 +1,9 @@
use crate::widgets::line_plot::Yvalue;
use crate::{
Color, Drawable, EventCtx, GeomBatch, GfxCtx, JustDraw, Line, ScreenDims, ScreenPt,
ScreenRectangle, Text, TextExt, Widget, WidgetImpl, WidgetOutput,
Color, Drawable, EventCtx, GeomBatch, GfxCtx, JustDraw, Line, PlotOptions, ScreenDims,
ScreenPt, ScreenRectangle, Series, Text, TextExt, Widget, WidgetImpl, WidgetOutput,
};
use geom::{Angle, Circle, Distance, Duration, Pt2D};
use geom::{Angle, Circle, Distance, Duration, PolyLine, Pt2D, Time};
// TODO This is tuned for the trip time comparison right now.
// - Generic types for x and y axis
@ -179,3 +180,150 @@ impl WidgetImpl for ScatterPlot {
}
}
}
// TODO Dedupe
// The X is always time
pub struct ScatterPlotV2 {
draw_data: Drawable,
draw_grid: Drawable,
top_left: ScreenPt,
dims: ScreenDims,
}
impl ScatterPlotV2 {
pub fn new<T: Yvalue<T>>(ctx: &EventCtx, data: Series<T>, opts: PlotOptions<T>) -> Widget {
// Assume min_x is Time::START_OF_DAY and min_y is T::zero()
let max_x = opts.max_x.unwrap_or_else(|| {
data.pts
.iter()
.map(|(t, _)| *t)
.max()
.unwrap_or(Time::START_OF_DAY)
});
let max_y = opts.max_y.unwrap_or_else(|| {
data.pts
.iter()
.map(|(_, value)| *value)
.max()
.unwrap_or(T::zero())
});
// TODO Tuned to fit the info panel. Instead these should somehow stretch to fill their
// container.
let width = 0.23 * ctx.canvas.window_width;
let height = 0.2 * ctx.canvas.window_height;
let mut grid_batch = GeomBatch::new();
// Grid lines for the Y scale. Draw up to 10 lines max to cover the order of magnitude of
// the range.
// TODO This caps correctly, but if the max is 105, then suddenly we just have 2 grid
// lines.
{
let order_of_mag = 10.0_f64.powf(max_y.to_f64().log10().ceil());
for i in 0..10 {
let y = max_y.from_f64(order_of_mag / 10.0 * (i as f64));
let pct = y.to_percent(max_y);
if pct > 1.0 {
break;
}
grid_batch.push(
Color::BLACK,
PolyLine::new(vec![
Pt2D::new(0.0, (1.0 - pct) * height),
Pt2D::new(width, (1.0 - pct) * height),
])
.make_polygons(Distance::meters(5.0)),
);
}
}
// X axis grid
if max_x != Time::START_OF_DAY {
let order_of_mag = 10.0_f64.powf(max_x.inner_seconds().log10().ceil());
for i in 0..10 {
let x = Time::START_OF_DAY + Duration::seconds(order_of_mag / 10.0 * (i as f64));
let pct = x.to_percent(max_x);
if pct > 1.0 {
break;
}
grid_batch.push(
Color::BLACK,
PolyLine::new(vec![
Pt2D::new(pct * width, 0.0),
Pt2D::new(pct * width, height),
])
.make_polygons(Distance::meters(5.0)),
);
}
}
let mut batch = GeomBatch::new();
let circle = Circle::new(Pt2D::new(0.0, 0.0), Distance::meters(4.0)).to_polygon();
for (t, y) in data.pts {
let percent_x = t.to_percent(max_x);
let percent_y = y.to_percent(max_y);
// Y inversion
batch.push(
data.color,
circle.translate(percent_x * width, (1.0 - percent_y) * height),
);
}
let plot = ScatterPlotV2 {
draw_data: ctx.upload(batch),
draw_grid: ctx.upload(grid_batch),
top_left: ScreenPt::new(0.0, 0.0),
dims: ScreenDims::new(width, height),
};
let num_x_labels = 3;
let mut row = Vec::new();
for i in 0..num_x_labels {
let percent_x = (i as f64) / ((num_x_labels - 1) as f64);
let t = max_x.percent_of(percent_x);
// TODO Need ticks now to actually see where this goes
let mut batch = GeomBatch::new();
for (color, poly) in Text::from(Line(t.to_string())).render_ctx(ctx).consume() {
batch.fancy_push(color, poly.rotate(Angle::new_degs(-15.0)));
}
// The text is already scaled; don't use Widget::draw_batch and scale it again.
row.push(JustDraw::wrap(ctx, batch.autocrop()));
}
let x_axis = Widget::row(row).padding(10);
let num_y_labels = 4;
let mut col = Vec::new();
for i in 0..num_y_labels {
let percent_y = (i as f64) / ((num_y_labels - 1) as f64);
col.push(max_y.from_percent(percent_y).prettyprint().draw_text(ctx));
}
col.reverse();
let y_axis = Widget::col(col).padding(10);
// Don't let the x-axis fill the parent container
Widget::row(vec![Widget::col(vec![
Line(data.label).draw(ctx),
Widget::row(vec![y_axis.evenly_spaced(), Widget::new(Box::new(plot))]),
x_axis.evenly_spaced(),
])])
}
}
impl WidgetImpl for ScatterPlotV2 {
fn get_dims(&self) -> ScreenDims {
self.dims
}
fn set_pos(&mut self, top_left: ScreenPt) {
self.top_left = top_left;
}
fn event(&mut self, _ctx: &mut EventCtx, _output: &mut WidgetOutput) {}
fn draw(&self, g: &mut GfxCtx) {
g.redraw_at(self.top_left, &self.draw_grid);
g.redraw_at(self.top_left, &self.draw_data);
}
}

View File

@ -2,12 +2,11 @@ use crate::app::App;
use crate::info::{header_btns, make_tabs, throughput, DataOptions, Details, Tab};
use abstutil::prettyprint_usize;
use ezgui::{
Color, EventCtx, GeomBatch, Line, LinePlot, PlotOptions, RewriteColor, Series, Text, Widget,
Color, EventCtx, GeomBatch, Line, PlotOptions, RewriteColor, ScatterPlotV2, Series, Text,
Widget,
};
use geom::{Angle, ArrowCap, Distance, PolyLine};
use geom::{Duration, Statistic, Time};
use map_model::{IntersectionID, IntersectionType};
use sim::Analytics;
use std::collections::BTreeSet;
pub fn info(ctx: &EventCtx, app: &App, details: &mut Details, id: IntersectionID) -> Vec<Widget> {
@ -166,49 +165,35 @@ pub fn current_demand(
rows
}
// TODO a fan chart might be nicer
fn delay_plot(ctx: &EventCtx, app: &App, i: IntersectionID, opts: &DataOptions) -> Widget {
let get_data = |a: &Analytics, t: Time| {
let mut series: Vec<(Statistic, Vec<(Time, Duration)>)> = Statistic::all()
.into_iter()
.map(|stat| (stat, Vec::new()))
.collect();
for (t, distrib) in a.intersection_delays_bucketized(t, i, Duration::hours(1)) {
for (stat, pts) in series.iter_mut() {
if distrib.count() == 0 {
pts.push((t, Duration::ZERO));
} else {
pts.push((t, distrib.select(*stat)));
}
}
let series = if opts.show_before {
Series {
label: "Delay through intersection (before changes)".to_string(),
color: Color::BLUE.alpha(0.9),
pts: app
.prebaked()
.intersection_delays
.get(&i)
.cloned()
.unwrap_or_else(Vec::new),
}
} else {
Series {
label: "Delay through intersection (after changes)".to_string(),
color: Color::RED.alpha(0.9),
pts: app
.primary
.sim
.get_analytics()
.intersection_delays
.get(&i)
.cloned()
.unwrap_or_else(Vec::new),
}
series
};
let mut all_series = Vec::new();
for (idx, (stat, pts)) in get_data(app.primary.sim.get_analytics(), app.primary.sim.time())
.into_iter()
.enumerate()
{
all_series.push(Series {
label: stat.to_string(),
color: app.cs.rotating_color_plot(idx),
pts,
});
}
if opts.show_before {
for (idx, (stat, pts)) in get_data(app.prebaked(), app.primary.sim.get_end_of_day())
.into_iter()
.enumerate()
{
all_series.push(Series {
label: format!("{} (before changes)", stat),
color: app.cs.rotating_color_plot(idx).alpha(0.3),
pts,
});
}
}
LinePlot::new(ctx, "delay", all_series, PlotOptions::new())
ScatterPlotV2::new(ctx, series, PlotOptions::new())
}
fn header(
@ -242,7 +227,10 @@ fn header(
),
];
if i.is_traffic_signal() {
tabs.push(("Delay", Tab::IntersectionDelay(id, DataOptions::new(app))));
tabs.push((
"Delay",
Tab::IntersectionDelay(id, DataOptions { show_before: false }),
));
tabs.push(("Current demand", Tab::IntersectionDemand(id)));
}
tabs