mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-28 17:04:20 +03:00
Polish contingency matrix / risk screen
- better color contrast - don't color neutral/no-change buckets - show "-" rather than "0" for cells which have been filtered out - bespoke, non-linear, bucket durations for x-axis - less buckets for y-axis - better copy for tooltips - label axes - show empty chart rather than no chart - remove "delays" which aren't really risks - clarify that risk visualizations are just for bikes for now - remove filter UI and only include bikes
This commit is contained in:
parent
0ed933aa9c
commit
0d758c74f1
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1324,6 +1324,7 @@ dependencies = [
|
||||
"getrandom",
|
||||
"instant",
|
||||
"kml",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"lttb",
|
||||
"map_gui",
|
||||
|
@ -35,6 +35,15 @@ pub fn prettyprint_usize(x: usize) -> String {
|
||||
result
|
||||
}
|
||||
|
||||
pub fn abbreviated_format(x: usize) -> String {
|
||||
if x >= 1000 {
|
||||
let ks = x as f32 / 1000.0;
|
||||
format!("{:.1}k", ks)
|
||||
} else {
|
||||
x.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn basename<I: AsRef<str>>(path: I) -> String {
|
||||
std::path::Path::new(path.as_ref())
|
||||
.file_stem()
|
||||
|
@ -33,6 +33,7 @@ geom = { path = "../geom" }
|
||||
getrandom = { version = "0.2.2", optional = true }
|
||||
instant = "0.1.7"
|
||||
kml = { path = "../kml" }
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4.14"
|
||||
lttb = "0.2.0"
|
||||
maplit = "1.0.2"
|
||||
|
@ -409,7 +409,7 @@ fn make_main_panel(
|
||||
.unwrap()
|
||||
.0,
|
||||
]);
|
||||
stack.spacing(20.0);
|
||||
stack.set_spacing(20.0);
|
||||
|
||||
if can_reverse(lt) {
|
||||
stack.push(
|
||||
@ -455,7 +455,7 @@ fn make_main_panel(
|
||||
}
|
||||
|
||||
let current_lanes_ltr =
|
||||
Widget::evenly_spaced_row(current_lanes_ltr, 2).bg(Color::hex("#979797"));
|
||||
Widget::evenly_spaced_row(2, current_lanes_ltr).bg(Color::hex("#979797"));
|
||||
|
||||
let road_settings = Widget::row(vec![
|
||||
Text::from_all(vec![
|
||||
|
@ -441,7 +441,7 @@ fn describe_problems(
|
||||
]);
|
||||
|
||||
Widget::custom_row(vec![
|
||||
Line("Risk exposure")
|
||||
Line("Risk Exposure")
|
||||
.secondary()
|
||||
.into_widget(ctx)
|
||||
.container()
|
||||
|
@ -35,7 +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("Risk Exposure", DashTab::RiskSummaries),
|
||||
Choice::new("Parking Overhead", DashTab::ParkingOverhead),
|
||||
Choice::new("Active Traffic", DashTab::ActiveTraffic),
|
||||
Choice::new("Transit Routes", DashTab::TransitRoutes),
|
||||
@ -73,7 +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::RiskSummaries => risks::RiskSummaries::new(ctx, app, false),
|
||||
DashTab::ParkingOverhead => parking_overhead::ParkingOverhead::new(ctx, app),
|
||||
DashTab::ActiveTraffic => misc::ActiveTraffic::new(ctx, app),
|
||||
DashTab::TransitRoutes => misc::TransitRoutes::new(ctx, app),
|
||||
|
@ -1,17 +1,16 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt::Display;
|
||||
|
||||
use abstutil::prettyprint_usize;
|
||||
use geom::{Duration, Polygon, Pt2D, Time};
|
||||
use abstutil::{abbreviated_format, prettyprint_usize};
|
||||
use geom::{Angle, 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,
|
||||
Color, DrawWithTooltips, EventCtx, GeomBatch, GeomBatchStack, GfxCtx, Image, 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 {
|
||||
@ -19,49 +18,66 @@ pub struct RiskSummaries {
|
||||
}
|
||||
|
||||
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),
|
||||
));
|
||||
}
|
||||
pub fn new(ctx: &mut EventCtx, app: &App, include_no_changes: bool) -> Box<dyn State<App>> {
|
||||
let bike_filter = Filter {
|
||||
modes: maplit::btreeset! { TripMode::Bike },
|
||||
include_no_changes,
|
||||
};
|
||||
|
||||
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,
|
||||
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),
|
||||
Image::from_path("system/assets/meters/bike.svg")
|
||||
.dims(36.0)
|
||||
.into_widget(ctx)
|
||||
.centered_vert(),
|
||||
Line(format!(
|
||||
"Cyclist Risks - {} Finished Trips",
|
||||
bike_filter.finished_trip_count(app)
|
||||
))
|
||||
.big_heading_plain()
|
||||
.into_widget(ctx)
|
||||
.centered_vert(),
|
||||
])
|
||||
.evenly_spaced(),
|
||||
.margin_above(30),
|
||||
Widget::evenly_spaced_row(
|
||||
32,
|
||||
vec![
|
||||
Widget::col(vec![
|
||||
Line("Large intersection crossings")
|
||||
.small_heading()
|
||||
.into_widget(ctx)
|
||||
.centered_horiz(),
|
||||
safety_matrix(
|
||||
ctx,
|
||||
app,
|
||||
&bike_filter,
|
||||
ProblemType::LargeIntersectionCrossing,
|
||||
),
|
||||
])
|
||||
.section(ctx),
|
||||
Widget::col(vec![
|
||||
Line("Cars wanting to over-take cyclists")
|
||||
.small_heading()
|
||||
.into_widget(ctx)
|
||||
.centered_horiz(),
|
||||
safety_matrix(ctx, app, &bike_filter, ProblemType::OvertakeDesired),
|
||||
])
|
||||
.section(ctx),
|
||||
],
|
||||
)
|
||||
.margin_above(30),
|
||||
]))
|
||||
.exact_size_percent(90, 90)
|
||||
.build(ctx),
|
||||
@ -83,16 +99,8 @@ impl State<App> for RiskSummaries {
|
||||
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))
|
||||
let include_no_changes = self.panel.is_checked("include trips without any changes");
|
||||
Transition::Replace(RiskSummaries::new(ctx, app, include_no_changes))
|
||||
}
|
||||
_ => Transition::Keep,
|
||||
}
|
||||
@ -103,6 +111,10 @@ impl State<App> for RiskSummaries {
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref CLEAR_COLOR_SCALE: ColorScale = ColorScale(vec![Color::CLEAR, Color::CLEAR]);
|
||||
}
|
||||
|
||||
fn safety_matrix(
|
||||
ctx: &mut EventCtx,
|
||||
app: &App,
|
||||
@ -110,15 +122,18 @@ fn safety_matrix(
|
||||
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),
|
||||
);
|
||||
let duration_buckets = vec![
|
||||
Duration::ZERO,
|
||||
Duration::minutes(5),
|
||||
Duration::minutes(15),
|
||||
Duration::minutes(30),
|
||||
Duration::hours(1),
|
||||
Duration::hours(2),
|
||||
];
|
||||
|
||||
let num_buckets = 7;
|
||||
let mut matrix = Matrix::new(duration_buckets, bucketize_isizes(num_buckets, &points));
|
||||
for (x, y) in points {
|
||||
matrix.add_pt(x, y);
|
||||
}
|
||||
@ -126,31 +141,59 @@ fn safety_matrix(
|
||||
ctx,
|
||||
app,
|
||||
MatrixOptions {
|
||||
total_width: 500.0,
|
||||
total_height: 500.0,
|
||||
total_width: 600.0,
|
||||
total_height: 600.0,
|
||||
color_scale_for_bucket: Box::new(|app, _, n| {
|
||||
if n <= 0 {
|
||||
if n == 0 {
|
||||
&CLEAR_COLOR_SCALE
|
||||
} else 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
|
||||
))
|
||||
let trip_string = if count == 1 {
|
||||
"1 trip".to_string()
|
||||
} else {
|
||||
Line(format!(
|
||||
"with between {} and {} more problems encountered",
|
||||
problems1, problems2
|
||||
))
|
||||
format!("{} trips", prettyprint_usize(count))
|
||||
};
|
||||
let duration_string = match (t1, t2) {
|
||||
(None, Some(end)) => format!("shorter than {}", end),
|
||||
(Some(start), None) => format!("longer than {}", start),
|
||||
(Some(start), Some(end)) => format!("between {} and {}", start, end),
|
||||
(None, None) => {
|
||||
unreachable!("at least one end of the duration range must be specified")
|
||||
}
|
||||
};
|
||||
let mut txt = Text::from(format!("{} {}", trip_string, duration_string));
|
||||
txt.add_line(if problems1 == 0 {
|
||||
"had no change in the number of problems encountered.".to_string()
|
||||
} else if problems1 < 0 {
|
||||
if problems1.abs() == problems2.abs() + 1 {
|
||||
if problems1.abs() == 1 {
|
||||
"encountered 1 fewer problem.".to_string()
|
||||
} else {
|
||||
format!("encountered {} fewer problems.", problems1.abs())
|
||||
}
|
||||
} else {
|
||||
format!(
|
||||
"encountered {}-{} fewer problems.",
|
||||
problems2.abs() + 1,
|
||||
problems1.abs()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if problems1 == problems2 - 1 {
|
||||
if problems1 == 1 {
|
||||
"encountered 1 more problems.".to_string()
|
||||
} else {
|
||||
format!("encountered {} more problems.", problems1,)
|
||||
}
|
||||
} else {
|
||||
format!("encountered {}-{} more problems.", problems1, problems2 - 1)
|
||||
}
|
||||
});
|
||||
txt.add_line(Line(format!("Count: {} trips", prettyprint_usize(count))));
|
||||
txt
|
||||
}),
|
||||
},
|
||||
@ -188,13 +231,6 @@ pub struct Filter {
|
||||
}
|
||||
|
||||
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
|
||||
@ -220,6 +256,19 @@ impl Filter {
|
||||
}
|
||||
points
|
||||
}
|
||||
|
||||
fn finished_trip_count(&self, app: &App) -> usize {
|
||||
let before = app.prebaked();
|
||||
let after = app.primary.sim.get_analytics();
|
||||
|
||||
let mut count = 0;
|
||||
for (_, _, _, mode) in after.both_finished_trips(app.primary.sim.time(), before) {
|
||||
if self.modes.contains(&mode) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
/// Aka a 2D histogram. Counts the number of matching points in each cell.
|
||||
@ -275,33 +324,84 @@ impl<X: Copy + PartialOrd + Display, Y: Copy + PartialOrd + Display> Matrix<X, Y
|
||||
|
||||
for x in 0..self.buckets_x.len() - 1 {
|
||||
for y in 0..self.buckets_y.len() - 1 {
|
||||
let is_first_xbucket = x == 0;
|
||||
let is_last_xbucket = x == self.buckets_x.len() - 2;
|
||||
let is_middle_ybucket = y + 1 == self.buckets_y.len() / 2;
|
||||
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 =
|
||||
let color = if count == 0 {
|
||||
widgetry::Color::CLEAR
|
||||
} else {
|
||||
let density_pct = (count as f64) / max_count;
|
||||
(opts.color_scale_for_bucket)(app, self.buckets_x[x], self.buckets_y[y])
|
||||
.eval(density_pct);
|
||||
.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)),
|
||||
Text::from(if count == 0 && is_middle_ybucket {
|
||||
"-".to_string()
|
||||
} else {
|
||||
abbreviated_format(count)
|
||||
})
|
||||
.change_fg(if count == 0 || is_middle_ybucket {
|
||||
ctx.style().text_fg_color
|
||||
} else {
|
||||
Color::WHITE
|
||||
})
|
||||
.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,
|
||||
),
|
||||
));
|
||||
|
||||
if count != 0 || !is_middle_ybucket {
|
||||
tooltips.push((
|
||||
rect,
|
||||
(opts.tooltip_for_bucket)(
|
||||
(
|
||||
if is_first_xbucket {
|
||||
None
|
||||
} else {
|
||||
Some(self.buckets_x[x])
|
||||
},
|
||||
if is_last_xbucket {
|
||||
None
|
||||
} else {
|
||||
Some(self.buckets_x[x + 1])
|
||||
},
|
||||
),
|
||||
(self.buckets_y[y], self.buckets_y[y + 1]),
|
||||
count,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DrawWithTooltips::new(ctx, batch, tooltips, Box::new(|_| GeomBatch::new()))
|
||||
// Axis Labels
|
||||
let mut y_axis_label = Text::from("More Problems <--------> Fewer Problems")
|
||||
.change_fg(ctx.style().text_fg_color.dull(0.8))
|
||||
.render(ctx)
|
||||
.rotate(Angle::degrees(-90.0));
|
||||
y_axis_label.autocrop_dims = true;
|
||||
y_axis_label = y_axis_label.autocrop();
|
||||
|
||||
let x_axis_label = Text::from("Short Trips <--------> Long Trips")
|
||||
.change_fg(ctx.style().text_fg_color.dull(0.8))
|
||||
.render(ctx);
|
||||
|
||||
let vmargin = 32.0;
|
||||
for (polygon, _) in tooltips.iter_mut() {
|
||||
let mut translated =
|
||||
polygon.translate(vmargin + y_axis_label.get_bounds().width(), 0.0);
|
||||
std::mem::swap(&mut translated, polygon);
|
||||
}
|
||||
let mut row = GeomBatchStack::horizontal(vec![y_axis_label, batch]);
|
||||
row.set_spacing(vmargin);
|
||||
let mut chart = GeomBatchStack::vertical(vec![row.batch(), x_axis_label]);
|
||||
chart.set_spacing(16);
|
||||
|
||||
DrawWithTooltips::new(ctx, chart.batch(), tooltips, Box::new(|_| GeomBatch::new()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -309,23 +409,88 @@ 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>,
|
||||
tooltip_for_bucket: Box<dyn Fn((Option<X>, Option<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(max_buckets: usize, pts: &Vec<(Duration, isize)>) -> Vec<isize> {
|
||||
debug_assert!(
|
||||
max_buckets % 2 == 1,
|
||||
"num_buckets must be odd to have a symmetrical number of buckets around axis"
|
||||
);
|
||||
debug_assert!(max_buckets >= 3, "num_buckets must be at least 3");
|
||||
|
||||
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);
|
||||
let positive_buckets = (max_buckets - 1) / 2;
|
||||
// uniformly sized integer buckets
|
||||
let max = match pts.iter().max_by_key(|(_, cnt)| cnt.abs()) {
|
||||
Some(t) if (t.1.abs() as usize) >= positive_buckets => t.1.abs(),
|
||||
_ => {
|
||||
// Enforce a bucket width of at least 1.
|
||||
let negative_buckets = positive_buckets as isize * -1;
|
||||
return (negative_buckets..=(positive_buckets as isize + 1)).collect();
|
||||
}
|
||||
};
|
||||
|
||||
let bucket_size = (max as f64 / positive_buckets as f64).ceil() as isize;
|
||||
|
||||
// we start with a 0-based bucket, and build the other buckets out from that.
|
||||
let mut buckets = vec![0];
|
||||
|
||||
for i in 0..=positive_buckets {
|
||||
// the first positive bucket starts at `1`, to ensure that the 0 bucket stands alone
|
||||
buckets.push(1 + (i as isize) * bucket_size);
|
||||
}
|
||||
for i in 1..=positive_buckets {
|
||||
buckets.push(-(i as isize) * bucket_size);
|
||||
}
|
||||
buckets.sort();
|
||||
debug!("buckets: {:?}", buckets);
|
||||
|
||||
buckets
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_bucketize_isizes() {
|
||||
let buckets = bucketize_isizes(
|
||||
7,
|
||||
&vec![
|
||||
(Duration::minutes(3), -3),
|
||||
(Duration::minutes(3), -3),
|
||||
(Duration::minutes(3), -1),
|
||||
(Duration::minutes(3), 2),
|
||||
(Duration::minutes(3), 5),
|
||||
],
|
||||
);
|
||||
// there should be an even number of buckets on either side of zero so as to center
|
||||
// our x-axis.
|
||||
//
|
||||
// there should always be a 0-1 bucket, ensuring that only '0' falls into the zero-bucket.
|
||||
//
|
||||
// all other buckets edges should be evenly spaced from the zero bucket
|
||||
assert_eq!(buckets, vec![-6, -4, -2, 0, 1, 3, 5, 7])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bucketize_empty_isizes() {
|
||||
let buckets = bucketize_isizes(7, &vec![]);
|
||||
assert_eq!(buckets, vec![-2, -1, 0, 1, 2])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bucketize_small_isizes() {
|
||||
let buckets = bucketize_isizes(
|
||||
7,
|
||||
&vec![
|
||||
(Duration::minutes(3), -1),
|
||||
(Duration::minutes(3), -1),
|
||||
(Duration::minutes(3), 0),
|
||||
(Duration::minutes(3), -1),
|
||||
(Duration::minutes(3), 0),
|
||||
],
|
||||
);
|
||||
assert_eq!(buckets, vec![-3, -2, -1, 0, 1, 2, 3, 4])
|
||||
}
|
||||
}
|
||||
|
@ -148,7 +148,7 @@ fn make_agent_toggles(ctx: &mut EventCtx, app: &App, is_enabled: [bool; 4]) -> V
|
||||
checkbox.build_batch(ctx).expect("invalid svg").0,
|
||||
icon_batch.clone(),
|
||||
]);
|
||||
row.spacing(8.0);
|
||||
row.set_spacing(8.0);
|
||||
|
||||
let row_batch = row.batch();
|
||||
let bounds = row_batch.get_bounds();
|
||||
@ -168,7 +168,7 @@ fn make_agent_toggles(ctx: &mut EventCtx, app: &App, is_enabled: [bool; 4]) -> V
|
||||
checkbox.build_batch(ctx).expect("invalid svg").0,
|
||||
icon_batch,
|
||||
]);
|
||||
row.spacing(8.0);
|
||||
row.set_spacing(8.0);
|
||||
|
||||
let row_batch = row.batch();
|
||||
let bounds = row_batch.get_bounds();
|
||||
|
@ -6,6 +6,13 @@ pub enum Axis {
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Alignment {
|
||||
Center,
|
||||
Top,
|
||||
// TODO: Bottom, Left, Right
|
||||
}
|
||||
|
||||
/// Similar to [`Widget::row`]/[`Widget::column`], but for [`GeomBatch`]s instead of [`Widget`]s,
|
||||
/// and follows a builder pattern
|
||||
///
|
||||
@ -15,6 +22,7 @@ pub enum Axis {
|
||||
pub struct GeomBatchStack {
|
||||
batches: Vec<GeomBatch>,
|
||||
axis: Axis,
|
||||
alignment: Alignment,
|
||||
spacing: f64,
|
||||
}
|
||||
|
||||
@ -22,9 +30,8 @@ impl Default for GeomBatchStack {
|
||||
fn default() -> Self {
|
||||
GeomBatchStack {
|
||||
batches: vec![],
|
||||
// TODO:
|
||||
// alignment: Alignment::Center,
|
||||
axis: Axis::Horizontal,
|
||||
alignment: Alignment::Center,
|
||||
spacing: 0.0,
|
||||
}
|
||||
}
|
||||
@ -47,10 +54,6 @@ impl GeomBatchStack {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_axis(&mut self, new_value: Axis) {
|
||||
self.axis = new_value;
|
||||
}
|
||||
|
||||
pub fn push(&mut self, geom_batch: GeomBatch) {
|
||||
self.batches.push(geom_batch);
|
||||
}
|
||||
@ -59,8 +62,16 @@ impl GeomBatchStack {
|
||||
self.batches.append(geom_batches);
|
||||
}
|
||||
|
||||
pub fn spacing(&mut self, spacing: f64) -> &mut Self {
|
||||
self.spacing = spacing;
|
||||
pub fn set_axis(&mut self, new_value: Axis) {
|
||||
self.axis = new_value;
|
||||
}
|
||||
|
||||
pub fn set_alignment(&mut self, new_value: Alignment) {
|
||||
self.alignment = new_value;
|
||||
}
|
||||
|
||||
pub fn set_spacing(&mut self, spacing: impl Into<f64>) -> &mut Self {
|
||||
self.spacing = spacing.into();
|
||||
self
|
||||
}
|
||||
|
||||
@ -83,9 +94,17 @@ impl GeomBatchStack {
|
||||
let mut stack_offset = 0.0;
|
||||
for mut batch in self.batches {
|
||||
let bounds = batch.get_bounds();
|
||||
let alignment_inset = match self.axis {
|
||||
Axis::Vertical => (max_bound_for_axis.width() - bounds.width()) / 2.0,
|
||||
Axis::Horizontal => (max_bound_for_axis.height() - bounds.height()) / 2.0,
|
||||
let alignment_inset = match (self.alignment, self.axis) {
|
||||
(Alignment::Center, Axis::Vertical) => {
|
||||
(max_bound_for_axis.width() - bounds.width()) / 2.0
|
||||
}
|
||||
(Alignment::Center, Axis::Horizontal) => {
|
||||
(max_bound_for_axis.height() - bounds.height()) / 2.0
|
||||
}
|
||||
(Alignment::Top, Axis::Vertical) => {
|
||||
unreachable!("cannot top-align a vertical stack")
|
||||
}
|
||||
(Alignment::Top, Axis::Horizontal) => 0.0,
|
||||
};
|
||||
|
||||
let (dx, dy) = match self.axis {
|
||||
|
@ -37,7 +37,9 @@ pub use crate::color::{Color, Fill, LinearGradient, Texture};
|
||||
pub use crate::drawing::{GfxCtx, Prerender};
|
||||
pub use crate::event::{hotkeys, lctrl, Event, Key, MultiKey};
|
||||
pub use crate::event_ctx::{EventCtx, UpdateType};
|
||||
pub use crate::geom::geom_batch_stack::{Axis, GeomBatchStack};
|
||||
pub use crate::geom::geom_batch_stack::{
|
||||
Alignment as StackAlignment, Axis as StackAxis, GeomBatchStack,
|
||||
};
|
||||
pub use crate::geom::{GeomBatch, RewriteColor};
|
||||
pub use crate::input::UserInput;
|
||||
pub use crate::runner::{run, Settings};
|
||||
|
@ -656,7 +656,7 @@ impl<'b, 'a: 'b, 'c> ButtonBuilder<'a, 'c> {
|
||||
if let Some(stack_axis) = self.stack_axis {
|
||||
stack.set_axis(stack_axis);
|
||||
}
|
||||
stack.spacing(self.stack_spacing);
|
||||
stack.set_spacing(self.stack_spacing);
|
||||
|
||||
let mut button_widget = stack
|
||||
.batch()
|
||||
|
@ -414,11 +414,11 @@ impl Widget {
|
||||
|
||||
/// Creates a row with the specified widgets. Every member gets a default horizontal margin.
|
||||
pub fn row(widgets: Vec<Widget>) -> Widget {
|
||||
Widget::evenly_spaced_row(widgets, 10)
|
||||
Widget::evenly_spaced_row(10, widgets)
|
||||
}
|
||||
|
||||
/// Creates a row with the specified widgets, with a `spacing` sized margin between members
|
||||
pub fn evenly_spaced_row(widgets: Vec<Widget>, spacing: usize) -> Widget {
|
||||
pub fn evenly_spaced_row(spacing: usize, widgets: Vec<Widget>) -> Widget {
|
||||
let mut new = Vec::new();
|
||||
let len = widgets.len();
|
||||
// TODO Time for that is_last iterator?
|
||||
@ -437,8 +437,8 @@ impl Widget {
|
||||
Widget::new(Box::new(Container::new(false, widgets)))
|
||||
}
|
||||
|
||||
/// Creates a column with the specified widgets. Every member gets a default vertical margin.
|
||||
pub fn col(widgets: Vec<Widget>) -> Widget {
|
||||
/// Creates a column with the specified widgets, with a `spacing` sized margin between members
|
||||
pub fn evenly_spaced_col(spacing: usize, widgets: Vec<Widget>) -> Widget {
|
||||
let mut new = Vec::new();
|
||||
let len = widgets.len();
|
||||
// TODO Time for that is_last iterator?
|
||||
@ -446,12 +446,17 @@ impl Widget {
|
||||
if idx == len - 1 {
|
||||
new.push(w);
|
||||
} else {
|
||||
new.push(w.margin_below(10));
|
||||
new.push(w.margin_below(spacing));
|
||||
}
|
||||
}
|
||||
Widget::new(Box::new(Container::new(false, new)))
|
||||
}
|
||||
|
||||
/// Creates a column with the specified widgets. Every member gets a default vertical margin.
|
||||
pub fn col(widgets: Vec<Widget>) -> Widget {
|
||||
Self::evenly_spaced_col(10, widgets)
|
||||
}
|
||||
|
||||
pub fn nothing() -> Widget {
|
||||
Widget::new(Box::new(Nothing {}))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user