use geom::{Angle, Circle, Distance, Duration, PolyLine, Pt2D, Time, UnitFmt};
use crate::widgets::plots::{make_legend, Axis, PlotOptions, Series};
use crate::{
Color, Drawable, EventCtx, GeomBatch, GfxCtx, ScreenDims, ScreenPt, Text, TextExt, Widget,
WidgetImpl, WidgetOutput,
};
pub struct ScatterPlot {
draw: Drawable,
top_left: ScreenPt,
dims: ScreenDims,
}
impl ScatterPlot {
pub fn new_widget<Y: Axis<Y> + std::ops::AddAssign + std::ops::Div<f64, Output = Y>>(
ctx: &EventCtx,
mut series: Vec<Series<Time, Y>>,
opts: PlotOptions<Time, Y>,
unit_fmt: UnitFmt,
) -> Widget {
let legend = make_legend(ctx, &series, &opts);
series.retain(|s| !opts.disabled.contains(&s.label));
let max_x = opts.max_x.unwrap_or_else(|| {
series
.iter()
.map(|s| {
s.pts
.iter()
.map(|(t, _)| *t)
.max()
.unwrap_or(Time::START_OF_DAY)
})
.max()
.unwrap_or(Time::START_OF_DAY)
});
let max_y = opts.max_y.unwrap_or_else(|| {
series
.iter()
.map(|s| {
s.pts
.iter()
.map(|(_, value)| *value)
.max()
.unwrap_or_else(Y::zero)
})
.max()
.unwrap_or_else(Y::zero)
});
let width = 0.22 * ctx.canvas.window_width;
let height = 0.2 * ctx.canvas.window_height;
let mut batch = GeomBatch::new();
{
let order_of_mag = 10.0_f64.powf(max_y.to_f64().log10().ceil());
for i in 0..10 {
let y = max_y.from_f64(order_of_mag / 10.0 * (i as f64));
let pct = y.to_percent(max_y);
if pct > 1.0 {
break;
}
batch.push(
Color::hex("#7C7C7C"),
PolyLine::must_new(vec![
Pt2D::new(0.0, (1.0 - pct) * height),
Pt2D::new(width, (1.0 - pct) * height),
])
.make_polygons(Distance::meters(1.0)),
);
}
}
if max_x != Time::START_OF_DAY {
let order_of_mag = 10.0_f64.powf(max_x.inner_seconds().log10().ceil());
for i in 0..10 {
let x = Time::START_OF_DAY + Duration::seconds(order_of_mag / 10.0 * (i as f64));
let pct = x.to_percent(max_x);
if pct > 1.0 {
break;
}
batch.push(
Color::hex("#7C7C7C"),
PolyLine::must_new(vec![
Pt2D::new(pct * width, 0.0),
Pt2D::new(pct * width, height),
])
.make_polygons(Distance::meters(1.0)),
);
}
}
let circle = Circle::new(Pt2D::new(0.0, 0.0), Distance::meters(4.0)).to_polygon();
let mut sum = Y::zero();
let mut cnt = 0;
for s in series {
for (t, y) in s.pts {
cnt += 1;
sum += y;
let percent_x = t.to_percent(max_x);
let percent_y = y.to_percent(max_y);
batch.push(
s.color,
circle.translate(percent_x * width, (1.0 - percent_y) * height),
);
}
}
if sum != Y::zero() {
let avg = (sum / (cnt as f64)).to_percent(max_y);
batch.extend(
Color::hex("#F2F2F2"),
PolyLine::must_new(vec![
Pt2D::new(0.0, (1.0 - avg) * height),
Pt2D::new(width, (1.0 - avg) * height),
])
.exact_dashed_polygons(
Distance::meters(1.0),
Distance::meters(10.0),
Distance::meters(4.0),
),
);
let txt = Text::from("avg").render(ctx).autocrop();
let width = txt.get_dims().width;
batch.append(txt.centered_on(Pt2D::new(-width / 2.0, (1.0 - avg) * height)));
}
let plot = ScatterPlot {
draw: ctx.upload(batch),
top_left: ScreenPt::new(0.0, 0.0),
dims: ScreenDims::new(width, height),
};
let num_x_labels = 3;
let mut row = Vec::new();
for i in 0..num_x_labels {
let percent_x = (i as f64) / ((num_x_labels - 1) as f64);
let t = max_x.percent_of(percent_x);
let batch = Text::from(t.to_string())
.render(ctx)
.rotate(Angle::degrees(-15.0))
.autocrop();
row.push(batch.into_widget(ctx));
}
let x_axis = Widget::custom_row(row).padding(10).evenly_spaced();
let num_y_labels = 4;
let mut col = Vec::new();
for i in 0..num_y_labels {
let percent_y = (i as f64) / ((num_y_labels - 1) as f64);
col.push(
max_y
.from_percent(percent_y)
.prettyprint(&unit_fmt)
.text_widget(ctx),
);
}
col.reverse();
let y_axis = Widget::custom_col(col).padding(10).evenly_spaced();
Widget::custom_col(vec![
legend.margin_below(10),
Widget::custom_row(vec![y_axis, Widget::new(Box::new(plot))]),
x_axis,
])
.container()
}
}
impl WidgetImpl for ScatterPlot {
fn get_dims(&self) -> ScreenDims {
self.dims
}
fn set_pos(&mut self, top_left: ScreenPt) {
self.top_left = top_left;
}
fn event(&mut self, _: &mut EventCtx, _: &mut WidgetOutput) {}
fn draw(&self, g: &mut GfxCtx) {
g.redraw_at(self.top_left, &self.draw);
}
}