2022-10-07 13:27:43 +00:00
|
|
|
//! All code for drawing the [`plotters`] charts.
|
|
|
|
|
|
|
|
use {
|
|
|
|
async_std::{fs::create_dir_all, path::PathBuf},
|
|
|
|
color_eyre::Result,
|
|
|
|
plotters::prelude::*,
|
|
|
|
};
|
|
|
|
|
|
|
|
use crate::group_data::GroupDataModel;
|
|
|
|
|
2022-10-08 20:20:21 +00:00
|
|
|
const BACKGROUND_1: RGBColor = RGBColor(31, 23, 49);
|
|
|
|
const BACKGROUND_2: RGBColor = RGBColor(42, 32, 65);
|
2022-10-08 22:02:05 +00:00
|
|
|
const BACKGROUND_3: RGBColor = RGBColor(10, 8, 16);
|
2022-10-08 20:20:21 +00:00
|
|
|
const FOREGROUND: RGBColor = RGBColor(242, 239, 255);
|
|
|
|
const ACCENT_1: RGBColor = RGBColor(210, 184, 58);
|
2022-10-07 13:27:43 +00:00
|
|
|
|
|
|
|
/// The chart for the user count.
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct UserCountChart {
|
|
|
|
/// The groups to use for user counts.
|
|
|
|
pub groups: Vec<GroupDataModel>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl UserCountChart {
|
|
|
|
/// Render the chart and write it to file.
|
2023-06-07 17:09:44 +00:00
|
|
|
pub async fn render(
|
|
|
|
&self,
|
|
|
|
parent: &PathBuf,
|
|
|
|
group_name: &str,
|
2023-06-07 18:19:45 +00:00
|
|
|
render_point_circles: bool,
|
2023-06-14 10:44:41 +00:00
|
|
|
truncate: bool,
|
2023-06-24 09:19:59 +00:00
|
|
|
output_dir: &str,
|
2023-06-07 17:09:44 +00:00
|
|
|
) -> Result<PathBuf> {
|
2023-06-24 09:19:59 +00:00
|
|
|
let parent = parent.join(format!("{}/user-count", output_dir));
|
2022-10-07 13:27:43 +00:00
|
|
|
create_dir_all(&parent).await?;
|
|
|
|
|
|
|
|
let (mut datapoints, mut min_count, mut max_count) = (vec![], i64::MAX, 0);
|
|
|
|
|
|
|
|
for (index, group) in self.groups.iter().enumerate() {
|
|
|
|
datapoints.push(((index + 1) as isize, group.subscribers));
|
|
|
|
|
|
|
|
if group.subscribers > max_count {
|
|
|
|
max_count = group.subscribers;
|
|
|
|
}
|
|
|
|
|
|
|
|
if group.subscribers < min_count {
|
|
|
|
min_count = group.subscribers;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let datapoints_len = datapoints.len() as isize;
|
2023-06-14 10:44:41 +00:00
|
|
|
let min_count = if truncate { min_count - 10 } else { 0 };
|
2022-10-07 13:27:43 +00:00
|
|
|
let max_count = max_count + 10;
|
|
|
|
|
2023-06-07 17:09:44 +00:00
|
|
|
let path = parent.join(format!("{group_name}.svg"));
|
|
|
|
let output_path = path.clone();
|
2022-10-07 13:27:43 +00:00
|
|
|
let chart_root = SVGBackend::new(&path, (1280, 720)).into_drawing_area();
|
2022-10-08 20:20:21 +00:00
|
|
|
chart_root.fill(&BACKGROUND_2)?;
|
2022-10-07 13:27:43 +00:00
|
|
|
|
|
|
|
let text_style =
|
|
|
|
|font_size: i32| ("sans-serif", font_size).into_font().color(&FOREGROUND);
|
|
|
|
|
|
|
|
let chart_root = chart_root
|
|
|
|
.margin(20, 20, 20, 20)
|
2023-06-08 11:20:43 +00:00
|
|
|
.titled("User Count", text_style(30))?;
|
2022-10-07 13:27:43 +00:00
|
|
|
|
2022-10-08 20:20:21 +00:00
|
|
|
chart_root.fill(&BACKGROUND_2)?;
|
2022-10-07 13:27:43 +00:00
|
|
|
|
|
|
|
let mut chart = ChartBuilder::on(&chart_root)
|
|
|
|
.caption(
|
|
|
|
format!("Using the {group_name} subscriber count."),
|
|
|
|
text_style(20),
|
|
|
|
)
|
2022-10-08 20:40:05 +00:00
|
|
|
.x_label_area_size(50)
|
|
|
|
.y_label_area_size(50)
|
2022-10-07 13:27:43 +00:00
|
|
|
.margin(10)
|
|
|
|
.build_cartesian_2d(0..(datapoints_len + 1), min_count..max_count)?;
|
|
|
|
|
|
|
|
chart
|
|
|
|
.configure_mesh()
|
|
|
|
.x_labels(datapoints.len() + 2)
|
2023-06-07 18:21:13 +00:00
|
|
|
.x_label_formatter(&|x| {
|
2023-07-10 15:49:15 +00:00
|
|
|
if (x - 1) % (datapoints_len / 20).max(1) != 0 {
|
2023-06-07 18:21:13 +00:00
|
|
|
String::new()
|
|
|
|
} else {
|
|
|
|
format!("{:0}", datapoints_len - x)
|
|
|
|
}
|
|
|
|
})
|
2022-10-07 13:27:43 +00:00
|
|
|
.x_desc("N days ago")
|
|
|
|
.y_labels(5)
|
|
|
|
.y_label_formatter(&|y| format!("{y:0}"))
|
|
|
|
.label_style(text_style(20))
|
2022-10-08 20:20:21 +00:00
|
|
|
.axis_style(&BACKGROUND_1)
|
|
|
|
.light_line_style(&BACKGROUND_1)
|
2022-10-08 22:02:05 +00:00
|
|
|
.bold_line_style(&BACKGROUND_3)
|
2022-10-07 13:27:43 +00:00
|
|
|
.draw()?;
|
|
|
|
|
|
|
|
chart
|
|
|
|
.draw_series(LineSeries::new(
|
|
|
|
datapoints.clone(),
|
|
|
|
ACCENT_1.stroke_width(2),
|
|
|
|
))?
|
|
|
|
.label("User Count")
|
|
|
|
.legend(|(x, y)| {
|
|
|
|
PathElement::new(vec![(x, y), (x + 20, y)], ACCENT_1.stroke_width(4))
|
|
|
|
});
|
|
|
|
|
|
|
|
chart.draw_series(PointSeries::of_element(
|
|
|
|
datapoints,
|
|
|
|
5,
|
|
|
|
&ACCENT_1,
|
|
|
|
&|(x, y), size, style| {
|
|
|
|
EmptyElement::at((x, y))
|
2023-06-07 18:19:45 +00:00
|
|
|
+ Circle::new(
|
|
|
|
(0, 0),
|
|
|
|
size,
|
|
|
|
if render_point_circles {
|
|
|
|
style.filled()
|
|
|
|
} else {
|
|
|
|
TRANSPARENT.filled()
|
|
|
|
},
|
|
|
|
)
|
2022-10-07 13:27:43 +00:00
|
|
|
+ Text::new(
|
|
|
|
{
|
2023-07-10 15:49:15 +00:00
|
|
|
if (x - 1) % (datapoints_len / 10).max(1) != 0 {
|
2022-10-07 13:27:43 +00:00
|
|
|
String::new()
|
|
|
|
} else {
|
|
|
|
format!("{:0}", y)
|
|
|
|
}
|
|
|
|
},
|
2023-06-07 16:35:32 +00:00
|
|
|
(-20, -25),
|
2022-10-07 13:27:43 +00:00
|
|
|
text_style(20),
|
|
|
|
)
|
|
|
|
},
|
|
|
|
))?;
|
|
|
|
|
2023-06-07 17:09:44 +00:00
|
|
|
Ok(output_path)
|
2022-10-07 13:27:43 +00:00
|
|
|
}
|
|
|
|
}
|