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;
pub struct PieChart<'a> {
r: f32,
legend: Legend<'a>
}
impl<'a> PieChart<'a> {
pub fn new(r: f32, legend: Legend<'a>) -> Self { Self { r, legend } }
pub fn radius(&self) -> f32 { self.r }
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(())
})
}
#[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);
}
}