widgetry/widgets/
plots.rs

1use std::collections::HashSet;
2
3use abstutil::prettyprint_usize;
4use geom::{Circle, Distance, Duration, Percent, Pt2D, Tessellation, Time, UnitFmt};
5
6use crate::{Color, EventCtx, GeomBatch, ScreenDims, TextExt, Toggle, Widget};
7
8#[derive(Default)]
9pub struct PlotOptions<X: Axis<X>, Y: Axis<Y>> {
10    pub filterable: bool,
11    pub max_x: Option<X>,
12    pub max_y: Option<Y>,
13    pub disabled: HashSet<String>,
14    pub dims: Option<ScreenDims>,
15}
16
17impl<X: Axis<X>, Y: Axis<Y>> PlotOptions<X, Y> {
18    pub fn filterable() -> PlotOptions<X, Y> {
19        PlotOptions {
20            filterable: true,
21            ..Default::default()
22        }
23    }
24
25    pub fn fixed() -> PlotOptions<X, Y> {
26        PlotOptions {
27            filterable: false,
28            ..Default::default()
29        }
30    }
31}
32
33pub trait Axis<T>: 'static + Copy + std::cmp::Ord + Default {
34    // percent is [0.0, 1.0]
35    fn from_percent(&self, percent: f64) -> T;
36    fn to_percent(self, max: T) -> f64;
37    fn prettyprint(self, unit_fmt: &UnitFmt) -> String;
38    // For order of magnitude calculations
39    fn to_f64(self) -> f64;
40    fn from_f64(&self, x: f64) -> T;
41    fn zero() -> T;
42}
43
44impl Axis<usize> for usize {
45    fn from_percent(&self, percent: f64) -> usize {
46        ((*self as f64) * percent) as usize
47    }
48    fn to_percent(self, max: usize) -> f64 {
49        if max == 0 {
50            0.0
51        } else {
52            (self as f64) / (max as f64)
53        }
54    }
55    fn prettyprint(self, _: &UnitFmt) -> String {
56        prettyprint_usize(self)
57    }
58    fn to_f64(self) -> f64 {
59        self as f64
60    }
61    fn from_f64(&self, x: f64) -> usize {
62        x as usize
63    }
64    fn zero() -> usize {
65        0
66    }
67}
68
69impl Axis<Duration> for Duration {
70    fn from_percent(&self, percent: f64) -> Duration {
71        *self * percent
72    }
73    fn to_percent(self, max: Duration) -> f64 {
74        if max == Duration::ZERO {
75            0.0
76        } else {
77            self / max
78        }
79    }
80    fn prettyprint(self, _: &UnitFmt) -> String {
81        self.to_string(&UnitFmt {
82            metric: false,
83            round_durations: true,
84        })
85    }
86    fn to_f64(self) -> f64 {
87        self.inner_seconds() as f64
88    }
89    fn from_f64(&self, x: f64) -> Duration {
90        Duration::seconds(x as f64)
91    }
92    fn zero() -> Duration {
93        Duration::ZERO
94    }
95}
96
97impl Axis<Time> for Time {
98    fn from_percent(&self, percent: f64) -> Time {
99        self.percent_of(percent)
100    }
101    fn to_percent(self, max: Time) -> f64 {
102        if max == Time::START_OF_DAY {
103            0.0
104        } else {
105            self.to_percent(max)
106        }
107    }
108    fn prettyprint(self, _: &UnitFmt) -> String {
109        self.ampm_tostring()
110    }
111    fn to_f64(self) -> f64 {
112        self.inner_seconds() as f64
113    }
114    fn from_f64(&self, x: f64) -> Time {
115        Time::START_OF_DAY + Duration::seconds(x as f64)
116    }
117    fn zero() -> Time {
118        Time::START_OF_DAY
119    }
120}
121
122impl Axis<Distance> for Distance {
123    fn from_percent(&self, percent: f64) -> Distance {
124        *self * percent
125    }
126    fn to_percent(self, max: Distance) -> f64 {
127        if max == Distance::ZERO {
128            0.0
129        } else {
130            self / max
131        }
132    }
133    fn prettyprint(self, unit_fmt: &UnitFmt) -> String {
134        self.to_string(unit_fmt)
135    }
136    fn to_f64(self) -> f64 {
137        self.inner_meters() as f64
138    }
139    fn from_f64(&self, x: f64) -> Distance {
140        Distance::meters(x as f64)
141    }
142    fn zero() -> Distance {
143        Distance::ZERO
144    }
145}
146
147pub struct Series<X, Y> {
148    pub label: String,
149    pub color: Color,
150    // Assume this is sorted by X.
151    pub pts: Vec<(X, Y)>,
152}
153
154pub fn make_legend<X: Axis<X>, Y: Axis<Y>>(
155    ctx: &EventCtx,
156    series: &[Series<X, Y>],
157    opts: &PlotOptions<X, Y>,
158) -> Widget {
159    let mut row = Vec::new();
160    let mut seen = HashSet::new();
161    for s in series {
162        if seen.contains(&s.label) {
163            continue;
164        }
165        seen.insert(s.label.clone());
166        if opts.filterable {
167            row.push(Toggle::colored_checkbox(
168                ctx,
169                &s.label,
170                s.color,
171                !opts.disabled.contains(&s.label),
172            ));
173        } else {
174            let radius = 15.0;
175            row.push(Widget::row(vec![
176                GeomBatch::from(vec![(
177                    s.color,
178                    Circle::new(Pt2D::new(radius, radius), Distance::meters(radius)).to_polygon(),
179                )])
180                .into_widget(ctx),
181                s.label.clone().text_widget(ctx),
182            ]));
183        }
184    }
185    match opts.dims {
186        Some(ScreenDims { width, .. }) => Widget::custom_row(row).force_width(width),
187        _ => Widget::custom_row(row).flex_wrap(ctx, Percent::int(24)),
188    }
189}
190
191// TODO If this proves useful, lift to geom
192pub fn thick_lineseries(pts: Vec<Pt2D>, width: Distance) -> Tessellation {
193    use lyon::math::{point, Point};
194    use lyon::path::Path;
195    use lyon::tessellation::geometry_builder::{BuffersBuilder, Positions, VertexBuffers};
196    use lyon::tessellation::{StrokeOptions, StrokeTessellator};
197
198    let mut builder = Path::builder().with_svg();
199    for (idx, pt) in pts.into_iter().enumerate() {
200        let pt = point(pt.x() as f32, pt.y() as f32);
201        if idx == 0 {
202            builder.move_to(pt);
203        } else {
204            builder.line_to(pt);
205        }
206    }
207    let path = builder.build();
208
209    let mut geom: VertexBuffers<Point, u32> = VertexBuffers::new();
210    let mut buffer = BuffersBuilder::new(&mut geom, Positions);
211    StrokeTessellator::new()
212        .tessellate(
213            &path,
214            &StrokeOptions::tolerance(0.01).with_line_width(width.inner_meters() as f32),
215            &mut buffer,
216        )
217        .unwrap();
218    Tessellation::new(
219        geom.vertices
220            .into_iter()
221            .map(|v| Pt2D::new(f64::from(v.x), f64::from(v.y)))
222            .collect(),
223        geom.indices.into_iter().map(|idx| idx as usize).collect(),
224    )
225}