use bonbon::prelude::*;
use chrono::{Duration, Utc};
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::{ExtendedColorType, ImageEncoder};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let entries = synthetic_week();
let stats = TirStats::compute(&entries, &TirThresholds::default()).unwrap();
println!(
"in range {:.0}% · mean {:.0} mg/dl · CV {:.1}% · GMI {:.1}%",
stats.percentages[TirBand::InRange as usize],
stats.mean_mgdl,
stats.cv_percent,
stats.gmi_percent,
);
let dark = TimeInRangeBuilder::new()
.with_entries(entries.clone())
.with_targets(70.0, 180.0)
.with_extreme_targets(54.0, 250.0)
.with_units(UnitDisplay::MgDl)
.with_theme(Theme::dark())
.with_scale(2.0)
.build()?;
write_png("tir_dark.png", &dark)?;
println!("Saved -> tir_dark.png");
let light = TimeInRangeBuilder::new()
.with_entries(entries)
.with_units(UnitDisplay::MmolL)
.with_theme(Theme::light())
.with_scale(2.0)
.build()?;
write_png("tir_light.png", &light)?;
println!("Saved -> tir_light.png");
Ok(())
}
fn synthetic_week() -> Vec<GraphEntry> {
let now = Utc::now();
let count = 7 * 288;
(0..count)
.map(|i| {
let mins = i as f32 * 5.0;
let day_phase = (mins / 1440.0) * std::f32::consts::TAU;
let meal_phase = (mins / 280.0) * std::f32::consts::TAU;
let drift = (mins / 3900.0).sin() * 52.0;
let sgv = 128.0 + day_phase.sin() * 46.0 + meal_phase.sin() * 36.0 + drift;
GraphEntry {
sgv: sgv.clamp(40.0, 350.0),
date: now - Duration::minutes(((count - 1 - i) * 5) as i64),
}
})
.collect()
}
fn write_png(path: &str, img: &image::RgbaImage) -> Result<(), Box<dyn std::error::Error>> {
use std::fs::File;
use std::io::BufWriter;
let file = File::create(path)?;
let writer = BufWriter::new(file);
PngEncoder::new_with_quality(writer, CompressionType::Level(6), FilterType::NoFilter)
.write_image(
img.as_raw(),
img.width(),
img.height(),
ExtendedColorType::Rgba8,
)?;
Ok(())
}