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 fn from_percent(&self, percent: f64) -> T;
36 fn to_percent(self, max: T) -> f64;
37 fn prettyprint(self, unit_fmt: &UnitFmt) -> String;
38 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 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
191pub 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}