widgetry/widgets/
scatter_plot.rs
1use geom::{Angle, Circle, Distance, Duration, PolyLine, Pt2D, Time, UnitFmt};
2
3use crate::widgets::plots::{make_legend, Axis, PlotOptions, Series};
4use crate::{
5 Color, Drawable, EventCtx, GeomBatch, GfxCtx, ScreenDims, ScreenPt, Text, TextExt, Widget,
6 WidgetImpl, WidgetOutput,
7};
8
9pub struct ScatterPlot {
11 draw: Drawable,
12
13 top_left: ScreenPt,
14 dims: ScreenDims,
15}
16
17impl ScatterPlot {
18 pub fn new_widget<Y: Axis<Y> + std::ops::AddAssign + std::ops::Div<f64, Output = Y>>(
19 ctx: &EventCtx,
20 mut series: Vec<Series<Time, Y>>,
21 opts: PlotOptions<Time, Y>,
22 unit_fmt: UnitFmt,
23 ) -> Widget {
24 let legend = make_legend(ctx, &series, &opts);
25 series.retain(|s| !opts.disabled.contains(&s.label));
26
27 let max_x = opts.max_x.unwrap_or_else(|| {
30 series
31 .iter()
32 .map(|s| {
33 s.pts
34 .iter()
35 .map(|(t, _)| *t)
36 .max()
37 .unwrap_or(Time::START_OF_DAY)
38 })
39 .max()
40 .unwrap_or(Time::START_OF_DAY)
41 });
42 let max_y = opts.max_y.unwrap_or_else(|| {
43 series
44 .iter()
45 .map(|s| {
46 s.pts
47 .iter()
48 .map(|(_, value)| *value)
49 .max()
50 .unwrap_or_else(Y::zero)
51 })
52 .max()
53 .unwrap_or_else(Y::zero)
54 });
55
56 let width = 0.22 * ctx.canvas.window_width;
59 let height = 0.2 * ctx.canvas.window_height;
60
61 let mut batch = GeomBatch::new();
62 {
67 let order_of_mag = 10.0_f64.powf(max_y.to_f64().log10().ceil());
68 for i in 0..10 {
69 let y = max_y.from_f64(order_of_mag / 10.0 * (i as f64));
70 let pct = y.to_percent(max_y);
71 if pct > 1.0 {
72 break;
73 }
74 batch.push(
75 Color::hex("#7C7C7C"),
76 PolyLine::must_new(vec![
77 Pt2D::new(0.0, (1.0 - pct) * height),
78 Pt2D::new(width, (1.0 - pct) * height),
79 ])
80 .make_polygons(Distance::meters(1.0)),
81 );
82 }
83 }
84 if max_x != Time::START_OF_DAY {
86 let order_of_mag = 10.0_f64.powf(max_x.inner_seconds().log10().ceil());
87 for i in 0..10 {
88 let x = Time::START_OF_DAY + Duration::seconds(order_of_mag / 10.0 * (i as f64));
89 let pct = x.to_percent(max_x);
90 if pct > 1.0 {
91 break;
92 }
93 batch.push(
94 Color::hex("#7C7C7C"),
95 PolyLine::must_new(vec![
96 Pt2D::new(pct * width, 0.0),
97 Pt2D::new(pct * width, height),
98 ])
99 .make_polygons(Distance::meters(1.0)),
100 );
101 }
102 }
103
104 let circle = Circle::new(Pt2D::new(0.0, 0.0), Distance::meters(4.0)).to_polygon();
105 let mut sum = Y::zero();
106 let mut cnt = 0;
107 for s in series {
108 for (t, y) in s.pts {
109 cnt += 1;
110 sum += y;
111 let percent_x = t.to_percent(max_x);
112 let percent_y = y.to_percent(max_y);
113 batch.push(
115 s.color,
116 circle.translate(percent_x * width, (1.0 - percent_y) * height),
117 );
118 }
119 }
120
121 if sum != Y::zero() {
122 let avg = (sum / (cnt as f64)).to_percent(max_y);
123 batch.extend(
124 Color::hex("#F2F2F2"),
125 PolyLine::must_new(vec![
126 Pt2D::new(0.0, (1.0 - avg) * height),
127 Pt2D::new(width, (1.0 - avg) * height),
128 ])
129 .exact_dashed_polygons(
130 Distance::meters(1.0),
131 Distance::meters(10.0),
132 Distance::meters(4.0),
133 ),
134 );
135
136 let txt = Text::from("avg").render(ctx).autocrop();
137 let width = txt.get_dims().width;
138 batch.append(txt.centered_on(Pt2D::new(-width / 2.0, (1.0 - avg) * height)));
139 }
140
141 let plot = ScatterPlot {
142 draw: ctx.upload(batch),
143
144 top_left: ScreenPt::new(0.0, 0.0),
145 dims: ScreenDims::new(width, height),
146 };
147
148 let num_x_labels = 3;
149 let mut row = Vec::new();
150 for i in 0..num_x_labels {
151 let percent_x = (i as f64) / ((num_x_labels - 1) as f64);
152 let t = max_x.percent_of(percent_x);
153 let batch = Text::from(t.to_string())
155 .render(ctx)
156 .rotate(Angle::degrees(-15.0))
157 .autocrop();
158 row.push(batch.into_widget(ctx));
159 }
160 let x_axis = Widget::custom_row(row).padding(10).evenly_spaced();
161
162 let num_y_labels = 4;
163 let mut col = Vec::new();
164 for i in 0..num_y_labels {
165 let percent_y = (i as f64) / ((num_y_labels - 1) as f64);
166 col.push(
167 max_y
168 .from_percent(percent_y)
169 .prettyprint(&unit_fmt)
170 .text_widget(ctx),
171 );
172 }
173 col.reverse();
174 let y_axis = Widget::custom_col(col).padding(10).evenly_spaced();
175
176 Widget::custom_col(vec![
178 legend.margin_below(10),
179 Widget::custom_row(vec![y_axis, Widget::new(Box::new(plot))]),
180 x_axis,
181 ])
182 .container()
183 }
184}
185
186impl WidgetImpl for ScatterPlot {
187 fn get_dims(&self) -> ScreenDims {
188 self.dims
189 }
190
191 fn set_pos(&mut self, top_left: ScreenPt) {
192 self.top_left = top_left;
193 }
194
195 fn event(&mut self, _: &mut EventCtx, _: &mut WidgetOutput) {}
196
197 fn draw(&self, g: &mut GfxCtx) {
198 g.redraw_at(self.top_left, &self.draw);
199 }
200}