rtimelog 0.51.0

System for tracking time in a text-log-based format.
Documentation
//! Represent the construction of an SVG [`PieChart`].
//!
//! # Examples
//!
//! ```rust, no_run
//! use std::fs::File;
//! use xml::{EventWriter, EmitterConfig};
//! use timelog::chart::{ColorIter, Legend, Percentages, PieChart, PieData};
//! # use std::fmt;
//!
//! # fn main() {
//! let mut piedata = PieData::default();
//! [("foo", 10), ("bar", 70), ("baz", 50), ("foobar", 35)].iter()
//!     .for_each(|(l, p)| piedata.add_secs(l, *p));
//! let percents: Percentages = piedata.percentages();
//! let radius = 100.0;
//! let mut pie = PieChart::new(
//!     radius,
//!     Legend::new(14.0, ColorIter::default()),
//! );
//! let mut file = File::create("output.svg").expect("Can't create file");
//! let mut w = EmitterConfig::new().create_writer(&mut file);
//! pie.write_pie(&mut w, &percents);
//! # }
//! ```

use std::io::Write;

use xml::writer::{EventWriter, XmlEvent};

use crate::chart::colors::ColorIter;
use crate::chart::legend::Legend;
use crate::chart::tag_percent::TagPercent;
use crate::chart::utils;
use crate::emit_xml;
use crate::Result;

/// Configuration of a pie chart.
pub struct PieChart<'a> {
    r:      f32,
    legend: Legend<'a>
}

impl<'a> PieChart<'a> {
    /// Create a new [`PieChart`] with the given radius and [`Legend`]
    pub fn new(r: f32, legend: Legend<'a>) -> Self { Self { r, legend } }

    /// Return the radius of the pie chart.
    pub fn radius(&self) -> f32 { self.r }

    /// Output a pie chart representing the supplied percentages.
    ///
    /// # Errors
    ///
    /// Could return any formatting error
    pub fn write_pie<W: Write>(
        &self, w: &mut EventWriter<W>, percents: &[TagPercent]
    ) -> Result<()> {
        emit_xml!(w, div, class: "piechart" => {
            emit_xml!(w, div, class: "pie" => {
                let size = format!("{}", 2.0 * (self.r + 1.0));
                let view = format!("{0:} {0:} {1:} {1:}", -(self.r + 1.0), size);
                emit_xml!(w, svg, viewbox: &view, width: &size, height: &size => {
                    emit_xml!(w, circle, r: &format!("{}", self.r), stroke: "black")?;
                    let colors = ColorIter::default();
                    let percents = colors.limit_percents(percents, "Other");
                    let mut alpha = -90.0;
                    for (p, clr) in percents.iter().zip(colors) {
                        let theta = p.percent_val() * 3.6f32;

                        emit_xml!(w, path, fill: clr, d: &self.pie_slice(alpha, theta))?;
                        alpha += theta;
                    }
                    Ok(())
                })
            })?;
            self.legend.write(w, percents.iter())?;
            Ok(())
        })
    }

    // Create a single pie slice between the angles of `alpha` and `theta`.
    #[rustfmt::skip]
    fn pie_slice(&self, alpha: f32, theta: f32) -> String {
        let alpha_rad = alpha.to_radians();
        let sx = utils::format_coord(self.r * alpha_rad.cos());
        let sy = utils::format_coord(self.r * alpha_rad.sin());

        if (theta - 360.0).abs() < 0.01 {
            let rend = (alpha + 180.0).to_radians();
            let ex = utils::format_coord(self.r * rend.cos());
            let ey = utils::format_coord(self.r * rend.sin());

            format!(
                "M0,0 L{sx},{sy} A{r},{r} 0 1,1 {ex},{ey} A{r},{r} 0 1,1 {sx},{sy} z",
                sx=sx, sy=sy,
                r=self.r,
                ex=ex, ey=ey
            )
        }
        else {
            let rend = (alpha + theta).to_radians();
            let ex = utils::format_coord(self.r * rend.cos());
            let ey = utils::format_coord(self.r * rend.sin());

            let large = i32::from(theta >= 180.0);
            format!(
                "M0,0 L{sx},{sy} A{r},{r} 0 {lg},1 {ex},{ey} z",
                sx=sx, sy=sy,
                r=self.r,
                lg=large,
                ex=ex, ey=ey
            )
        }
    }
}

#[cfg(test)]
mod tests {
    use spectral::prelude::*;
    use xml::EmitterConfig;

    use super::*;
    use crate::chart::ColorIter;
    use crate::chart::Legend;

    #[test]
    fn test_new() {
        let legend = Legend::new(14.0, ColorIter::default());
        let pie = PieChart::new(100.0, legend);

        assert_that!(pie.radius()).is_equal_to(&100.0);
    }

    #[test]
    fn test_pie_one_slice() {
        let legend = Legend::new(14.0, ColorIter::default());
        let pie = PieChart::new(100.0, legend);

        let percents = [TagPercent::new("foo", 100.0).unwrap()];

        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!(pie.write_pie(&mut w, &percents)).is_ok();
        let actual = String::from_utf8(output).unwrap();
        let expected = r##"<div class="piechart">
  <div class="pie">
    <svg viewbox="-101 -101 202 202" width="202" height="202">
      <circle r="100" stroke="black" />
      <path fill="#1f78b4" d="M0,0 L-0,-100 A100,100 0 1,1 -0,100 A100,100 0 1,1 -0,-100 z" />
    </svg>
  </div>
  <table class="legend" style="font-size: 14px">
    <tr>
      <td>
        <svg height="14" width="14">
          <rect height="14" width="14" fill="#1f78b4" />
        </svg>
        <span>100% - foo</span>
      </td>
    </tr>
  </table>
</div>"##;
        assert_that!(actual.as_str()).is_equal_to(expected);
    }

    #[test]
    fn test_pie_multiple_slices() {
        let legend = Legend::new(14.0, ColorIter::default());
        let pie = PieChart::new(100.0, legend);

        #[rustfmt::skip]
        let percents = [
            TagPercent::new("david",   40.0).unwrap(),
            TagPercent::new("connie",  30.0).unwrap(),
            TagPercent::new("mark",    20.0).unwrap(),
            TagPercent::new("kirsten", 10.0).unwrap()
        ];

        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!(pie.write_pie(&mut w, &percents)).is_ok();
        let actual = String::from_utf8(output).unwrap();
        let expected = r##"<div class="piechart">
  <div class="pie">
    <svg viewbox="-101 -101 202 202" width="202" height="202">
      <circle r="100" stroke="black" />
      <path fill="#1f78b4" d="M0,0 L-0,-100 A100,100 0 0,1 58.779,80.902 z" />
      <path fill="#a6cee3" d="M0,0 L58.779,80.902 A100,100 0 0,1 -95.106,30.902 z" />
      <path fill="#33a02c" d="M0,0 L-95.106,30.902 A100,100 0 0,1 -58.779,-80.902 z" />
      <path fill="#b2df8a" d="M0,0 L-58.779,-80.902 A100,100 0 0,1 0,-100 z" />
    </svg>
  </div>
  <table class="legend" style="font-size: 14px">
    <tr>
      <td>
        <svg height="14" width="14">
          <rect height="14" width="14" fill="#1f78b4" />
        </svg>
        <span>40% - david</span>
      </td>
    </tr>
    <tr>
      <td>
        <svg height="14" width="14">
          <rect height="14" width="14" fill="#a6cee3" />
        </svg>
        <span>30% - connie</span>
      </td>
    </tr>
    <tr>
      <td>
        <svg height="14" width="14">
          <rect height="14" width="14" fill="#33a02c" />
        </svg>
        <span>20% - mark</span>
      </td>
    </tr>
    <tr>
      <td>
        <svg height="14" width="14">
          <rect height="14" width="14" fill="#b2df8a" />
        </svg>
        <span>10% - kirsten</span>
      </td>
    </tr>
  </table>
</div>"##;
        assert_that!(actual.as_str()).is_equal_to(expected);
    }
}