use std::io::Write;
use xml::writer::{EventWriter, XmlEvent};
use crate::chart::colors::ColorMap;
use crate::chart::day::Hour;
use crate::chart::{utils, DayHours, Percentages};
use crate::emit_xml;
use crate::Result;
use crate::TaskEvent;
const STYLESHEET: &str = r#"
text {
font-size: 12;
text-anchor: middle;
}
"#;
const BAR_HEIGHT: f32 = 60.0;
const CHART_HEIGHT: f32 = BAR_HEIGHT + 20.0;
pub struct BarGraph {
bar_width: usize,
colors: ColorMap
}
impl BarGraph {
pub fn new(percents: &Percentages) -> Self {
Self { bar_width: 19, colors: ColorMap::new(percents) }
}
fn bar_width(&self) -> usize { self.bar_width }
fn hour_width(&self) -> usize { self.bar_width + 1 }
fn write_hour<W: Write>(
&self, w: &mut EventWriter<W>, begin: usize, hr: &Hour, i: usize
) -> Result<()> {
let id = format!("hr{:02}", i + begin);
let xform = format!("translate({}, {})", i * self.hour_width(), BAR_HEIGHT);
emit_xml!(w, g, id: &id, transform: &xform => {
let mut offset = 0.0;
for task in hr.iter() {
offset = self.write_task(w, task, offset)?;
}
emit_xml!(w, text, x: "10", y: "13"; &format!("{}", begin + i))
})
}
fn write_task<W: Write>(
&self, w: &mut EventWriter<W>, task: &TaskEvent, offset: f32
) -> Result<f32> {
#![allow(clippy::cast_precision_loss)]
let height = task.as_secs() as f32 / BAR_HEIGHT;
let offset = offset - height;
let ht_str = utils::format_coord(height);
let off_str = utils::format_coord(offset);
let bar = format!("{}", self.bar_width());
emit_xml!(w, rect, x: "0", y: &off_str, height: &ht_str, width: &bar,
fill: self.colors.get(&task.project()).unwrap_or("black") )?;
Ok(offset)
}
pub fn write<W: Write>(&self, w: &mut EventWriter<W>, day_hours: &DayHours) -> Result<()> {
let width = format!("{}", day_hours.num_hours() * self.hour_width());
let view = format!("0 0 {width} {CHART_HEIGHT}");
emit_xml!(w, svg, viewbox: &view, width: &width, height: &format!("{CHART_HEIGHT}") => {
emit_xml!(w, style; STYLESHEET)?;
let path = format!("M0,{} h{}", BAR_HEIGHT, day_hours.num_hours() * self.hour_width() - 1);
emit_xml!(w, path, d: &path, stroke: "black")?;
let begin = day_hours.start();
for (i, hr) in day_hours.iter().enumerate() {
self.write_hour(w, begin, hr, i)?;
}
Ok(())
})
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use spectral::prelude::*;
use xml::EmitterConfig;
use super::*;
use crate::chart::{DayHours, TagPercent};
use crate::date::DateTime;
fn percentages() -> Percentages {
[
TagPercent::new("david", 40.0).unwrap(),
TagPercent::new("connie", 30.0).unwrap(),
TagPercent::new("mark", 20.0).unwrap(),
TagPercent::new("kirsten", 10.0).unwrap()
]
.iter()
.cloned()
.collect()
}
fn make_task(proj: &str, time: (u32, u32, u32), secs: u64) -> TaskEvent {
TaskEvent::new(
DateTime::new((2022, 3, 21), time).unwrap(),
proj,
Duration::from_secs(secs)
)
}
fn day() -> DayHours {
let mut day_hrs = DayHours::default();
for ev in [
make_task("david", (9, 0, 0), 300),
make_task("kirsten", (9, 5, 0), 3600),
make_task("mark", (10, 5, 0), 3300),
make_task("connie", (11, 0, 0), 1800),
make_task("kirsten", (11, 30, 0), 1800),
make_task("mark", (13, 0, 0), 3600),
make_task("connie", (14, 0, 0), 1800)
]
.into_iter()
{
day_hrs.add(ev);
}
day_hrs
}
#[test]
fn test_new() {
let percents = percentages();
let bg = BarGraph::new(&percents);
assert_that!(bg.hour_width()).is_equal_to(&20);
assert_that!(bg.bar_width()).is_equal_to(&19);
}
#[test]
fn test_write_histogram() {
let percents = percentages();
let bg = BarGraph::new(&percents);
let day_hrs = day();
let mut output: Vec<u8> = Vec::new();
let mut w = EmitterConfig::new()
.perform_indent(true)
.write_document_declaration(false)
.create_writer(&mut output);
assert_that!(bg.write(&mut w, &day_hrs)).is_ok();
let actual = String::from_utf8(output).unwrap();
let expected = r##"<svg viewbox="0 0 120 80" width="120" height="80">
<style>
text {
font-size: 12;
text-anchor: middle;
}
</style>
<path d="M0,60 h119" stroke="black" />
<g id="hr09" transform="translate(0, 60)">
<rect x="0" y="-5" height="5" width="19" fill="#1f78b4" />
<rect x="0" y="-60" height="55" width="19" fill="#b2df8a" />
<text x="10" y="13">9</text>
</g>
<g id="hr10" transform="translate(20, 60)">
<rect x="0" y="-5" height="5" width="19" fill="#b2df8a" />
<rect x="0" y="-60" height="55" width="19" fill="#33a02c" />
<text x="10" y="13">10</text>
</g>
<g id="hr11" transform="translate(40, 60)">
<rect x="0" y="0" height="0" width="19" fill="#33a02c" />
<rect x="0" y="-30" height="30" width="19" fill="#a6cee3" />
<rect x="0" y="-60" height="30" width="19" fill="#b2df8a" />
<text x="10" y="13">11</text>
</g>
<g id="hr12" transform="translate(60, 60)">
<rect x="0" y="0" height="0" width="19" fill="#b2df8a" />
<text x="10" y="13">12</text>
</g>
<g id="hr13" transform="translate(80, 60)">
<rect x="0" y="-60" height="60" width="19" fill="#33a02c" />
<text x="10" y="13">13</text>
</g>
<g id="hr14" transform="translate(100, 60)">
<rect x="0" y="0" height="0" width="19" fill="#33a02c" />
<rect x="0" y="-30" height="30" width="19" fill="#a6cee3" />
<text x="10" y="13">14</text>
</g>
</svg>"##;
assert_that!(actual.as_str()).is_equal_to(expected);
}
}