1use std::collections::VecDeque;
2
3use geom::{
4 Angle, Distance, Duration, HgramValue, Histogram, PolyLine, Pt2D, Statistic, Tessellation,
5 Time, UnitFmt,
6};
7
8use crate::widgets::plots::{make_legend, thick_lineseries, Axis, PlotOptions};
9use crate::{
10 Color, Drawable, EventCtx, GeomBatch, GfxCtx, ScreenDims, ScreenPt, Series, Text, TextExt,
11 Widget, WidgetImpl, WidgetOutput,
12};
13
14pub struct FanChart {
16 draw: Drawable,
17
18 top_left: ScreenPt,
19 dims: ScreenDims,
20}
21
22impl FanChart {
23 pub fn new_widget<Y: Axis<Y> + HgramValue<Y>>(
24 ctx: &EventCtx,
25 mut series: Vec<Series<Time, Y>>,
26 opts: PlotOptions<Time, Y>,
27 unit_fmt: UnitFmt,
28 ) -> Widget {
29 let legend = make_legend(ctx, &series, &opts);
30 series.retain(|s| !opts.disabled.contains(&s.label));
31
32 let max_x = opts.max_x.unwrap_or_else(|| {
35 series
36 .iter()
37 .map(|s| {
38 s.pts
39 .iter()
40 .map(|(t, _)| *t)
41 .max()
42 .unwrap_or(Time::START_OF_DAY)
43 })
44 .max()
45 .unwrap_or(Time::START_OF_DAY)
46 });
47 let max_y = opts.max_y.unwrap_or_else(|| {
48 series
49 .iter()
50 .map(|s| {
51 s.pts
52 .iter()
53 .map(|(_, value)| *value)
54 .max()
55 .unwrap_or_else(Y::zero)
56 })
57 .max()
58 .unwrap_or_else(Y::zero)
59 });
60
61 let width = 0.22 * ctx.canvas.window_width;
64 let height = 0.2 * ctx.canvas.window_height;
65
66 let mut batch = GeomBatch::new();
67 {
72 let order_of_mag = 10.0_f64.powf(max_y.to_f64().log10().ceil());
73 for i in 0..10 {
74 let y = max_y.from_f64(order_of_mag / 10.0 * (i as f64));
75 let pct = y.to_percent(max_y);
76 if pct > 1.0 {
77 break;
78 }
79 batch.push(
80 Color::hex("#7C7C7C"),
81 PolyLine::must_new(vec![
82 Pt2D::new(0.0, (1.0 - pct) * height),
83 Pt2D::new(width, (1.0 - pct) * height),
84 ])
85 .make_polygons(Distance::meters(1.0)),
86 );
87 }
88 }
89 if max_x != Time::START_OF_DAY {
91 let order_of_mag = 10.0_f64.powf(max_x.inner_seconds().log10().ceil());
92 for i in 0..10 {
93 let x = Time::START_OF_DAY + Duration::seconds(order_of_mag / 10.0 * (i as f64));
94 let pct = x.to_percent(max_x);
95 if pct > 1.0 {
96 break;
97 }
98 batch.push(
99 Color::hex("#7C7C7C"),
100 PolyLine::must_new(vec![
101 Pt2D::new(pct * width, 0.0),
102 Pt2D::new(pct * width, height),
103 ])
104 .make_polygons(Distance::meters(1.0)),
105 );
106 }
107 }
108
109 let transform = |input: Vec<(Time, Y)>| {
110 let mut pts = Vec::new();
112 for (t, y) in input {
113 let percent_x = t.to_percent(max_x);
114 let percent_y = y.to_percent(max_y);
115 pts.push(Pt2D::new(
116 percent_x * width,
117 (1.0 - percent_y) * height,
119 ));
120 }
121 pts.dedup();
122 pts
123 };
124
125 for s in series {
126 if s.pts.len() < 2 {
127 continue;
128 }
129 let (p50, p90, mut p99) = slidey_window(s.pts, Duration::hours(1));
130
131 let mut band = transform(p50);
133 p99.reverse();
134 band.extend(transform(p99));
135 band.push(band[0]);
136 batch.push(s.color.alpha(0.5), Tessellation::from_ring(band));
137
138 batch.push(
139 s.color,
140 thick_lineseries(transform(p90), Distance::meters(5.0)),
141 );
142 }
143
144 let plot = FanChart {
145 draw: ctx.upload(batch),
146
147 top_left: ScreenPt::new(0.0, 0.0),
148 dims: ScreenDims::new(width, height),
149 };
150
151 let num_x_labels = 3;
152 let mut row = Vec::new();
153 for i in 0..num_x_labels {
154 let percent_x = (i as f64) / ((num_x_labels - 1) as f64);
155 let t = max_x.percent_of(percent_x);
156 let batch = Text::from(t.to_string())
158 .render(ctx)
159 .rotate(Angle::degrees(-15.0))
160 .autocrop();
161 row.push(batch.into_widget(ctx));
162 }
163 let x_axis = Widget::custom_row(row).padding(10).evenly_spaced();
164
165 let num_y_labels = 4;
166 let mut col = Vec::new();
167 for i in 0..num_y_labels {
168 let percent_y = (i as f64) / ((num_y_labels - 1) as f64);
169 col.push(
170 max_y
171 .from_percent(percent_y)
172 .prettyprint(&unit_fmt)
173 .text_widget(ctx),
174 );
175 }
176 col.reverse();
177 let y_axis = Widget::custom_col(col).padding(10).evenly_spaced();
178
179 Widget::custom_col(vec![
181 legend.margin_below(10),
182 Widget::custom_row(vec![y_axis, Widget::new(Box::new(plot))]),
183 x_axis,
184 ])
185 .container()
186 }
187}
188
189impl WidgetImpl for FanChart {
190 fn get_dims(&self) -> ScreenDims {
191 self.dims
192 }
193
194 fn set_pos(&mut self, top_left: ScreenPt) {
195 self.top_left = top_left;
196 }
197
198 fn event(&mut self, _: &mut EventCtx, _: &mut WidgetOutput) {}
199
200 fn draw(&self, g: &mut GfxCtx) {
201 g.redraw_at(self.top_left, &self.draw);
202 }
203}
204
205fn slidey_window<Y: HgramValue<Y>>(
207 input: Vec<(Time, Y)>,
208 window_size: Duration,
209) -> (Vec<(Time, Y)>, Vec<(Time, Y)>, Vec<(Time, Y)>) {
210 let mut p50: Vec<(Time, Y)> = Vec::new();
211 let mut p90: Vec<(Time, Y)> = Vec::new();
212 let mut p99: Vec<(Time, Y)> = Vec::new();
213 let mut window: VecDeque<(Time, Y)> = VecDeque::new();
214 let mut hgram = Histogram::new();
215
216 let mut last_sample = Time::START_OF_DAY;
217
218 for (t, value) in input {
219 window.push_back((t, value));
220 hgram.add(value);
221
222 if t - last_sample > Duration::minutes(1) {
223 p50.push((t, hgram.select(Statistic::P50).unwrap()));
224 p90.push((t, hgram.select(Statistic::P90).unwrap()));
225 p99.push((t, hgram.select(Statistic::P99).unwrap()));
226 last_sample = t;
227 }
228
229 while !window.is_empty() && t - window.front().unwrap().0 > window_size {
230 let (_, old_value) = window.pop_front().unwrap();
231 hgram.remove(old_value);
232 }
233 }
234
235 (p50, p90, p99)
236}