use super::Canvas;
use super::canvas;
use super::color::*;
use super::common::*;
use super::component::*;
use super::params::*;
use super::theme::{DEFAULT_Y_AXIS_WIDTH, Theme, get_default_theme_name, get_theme};
use super::util::*;
use crate::charts::measure_text_width_family;
use charts_rs_derive::Chart;
use core::f32;
use std::sync::Arc;
#[charts_rs_derive::chart_common_fields]
#[derive(Clone, Debug, Default, Chart)]
pub struct PieChart {
pub radius: f32,
pub inner_radius: f32,
pub rose_type: Option<bool>,
pub border_radius: Option<f32>,
pub start_angle: f32,
pub x_axis_data: Vec<String>,
pub x_axis_height: f32,
pub x_axis_stroke_color: Color,
pub x_axis_font_size: f32,
pub x_axis_font_color: Color,
pub x_axis_font_weight: Option<String>,
pub x_axis_name_gap: f32,
pub x_axis_name_rotate: f32,
pub x_axis_margin: Option<Box>,
pub x_boundary_gap: Option<bool>,
pub y_axis_configs: Vec<YAxisConfig>,
pub grid_stroke_color: Color,
pub grid_stroke_width: f32,
pub series_stroke_width: f32,
pub series_label_font_color: Color,
pub series_label_font_size: f32,
pub series_label_font_weight: Option<String>,
pub series_label_formatter: String,
pub series_label_position: Option<String>,
pub series_colors: Vec<Color>,
pub series_symbol: Option<Symbol>,
pub series_smooth: bool,
pub series_fill: bool,
pub animation: Option<AnimationConfig>,
pub tooltip_show: bool,
}
impl PieChart {
fn fill_default(&mut self) {
self.radius = 150.0;
self.inner_radius = 40.0;
self.legend_show = Some(false);
self.rose_type = Some(true);
}
pub fn from_json(data: &str) -> canvas::Result<PieChart> {
let mut p = PieChart {
..Default::default()
};
p.fill_default();
let value = p.fill_option(data)?;
if let Some(radius) = get_f32_from_value(&value, "radius") {
p.radius = radius;
}
if let Some(inner_radius) = get_f32_from_value(&value, "inner_radius") {
p.inner_radius = inner_radius;
}
if let Some(rose_type) = get_bool_from_value(&value, "rose_type") {
p.rose_type = Some(rose_type);
}
if let Some(border_radius) = get_f32_from_value(&value, "border_radius") {
p.border_radius = Some(border_radius);
}
if let Some(anim) = value.get("animation")
&& !anim.is_null()
{
let mut config = AnimationConfig::default();
if let Some(d) = get_usize_from_value(anim, "duration") {
config.duration = d as u32;
}
if let Some(e) = get_string_from_value(anim, "easing") {
config.easing = e;
}
if let Some(d) = get_usize_from_value(anim, "delay") {
config.delay = d as u32;
}
p.animation = Some(config);
}
if let Some(v) = get_bool_from_value(&value, "tooltip_show") {
p.tooltip_show = v;
}
Ok(p)
}
pub fn new_with_theme(series_list: Vec<Series>, theme: &str) -> PieChart {
let mut p = PieChart {
series_list,
..Default::default()
};
p.fill_default();
p.fill_theme(get_theme(theme));
p
}
pub fn new(series_list: Vec<Series>) -> PieChart {
PieChart::new_with_theme(series_list, &get_default_theme_name())
}
pub fn svg(&self) -> canvas::Result<String> {
let mut c = Canvas::new_width_xy(self.width, self.height, self.x, self.y);
let axis_top = self.render_header(&mut c);
if axis_top > 0.0 {
c = c.child(Box {
top: axis_top,
..Default::default()
});
}
let values: Vec<f32> = self
.series_list
.iter()
.map(|item| item.data_values().iter().sum())
.collect();
let mut max = 0.0;
let mut sum = 0.0;
for item in values.iter() {
sum += *item;
if *item > max {
max = *item;
}
}
let mut delta = 360.0 / values.len() as f32;
let mut half_delta = delta / 2.0;
let mut start_angle = self.start_angle;
let mut radius_double = c.height();
if c.width() < radius_double {
radius_double = c.width();
}
radius_double *= 0.8;
let mut r = radius_double / 2.0;
if r > self.radius {
r = self.radius;
}
let cx = (c.width() - radius_double) / 2.0 + r;
let cy = (c.height() - radius_double) / 2.0 + r;
let label_offset = 20.0;
let mut series_label_formatter = self.series_label_formatter.clone();
if series_label_formatter.is_empty() {
series_label_formatter = "{a}: {d}".to_string();
}
let rose_type = self.rose_type.unwrap_or_default();
let mut prev_quadrant = u8::MAX;
let mut prev_end_y = f32::MAX;
for (index, series) in self.series_list.iter().enumerate() {
let value = values[index];
let mut cr = value / max * (r - self.inner_radius) + self.inner_radius;
let color = get_color(&self.series_colors, series.index.unwrap_or(index));
if !rose_type {
cr = r;
delta = value / sum * 360.0;
half_delta = delta / 2.0;
}
if cr - self.inner_radius < 1.0 {
cr = self.inner_radius + 1.0;
}
let (anim_class, anim_style, fade_class) = if let Some(ref a) = self.animation {
(
Some("pie-anim".to_string()),
Some(format!("animation-delay:{}ms", index as u32 * a.delay)),
Some("pie-fade".to_string()),
)
} else {
(None, None, None)
};
let mut pie = Pie {
fill: color.into(),
cx,
cy,
r: cr,
ir: self.inner_radius,
start_angle,
delta,
class: anim_class,
style: anim_style,
..Default::default()
};
if let Some(border_radius) = self.border_radius {
pie.border_radius = border_radius;
}
let tooltip_text = if self.tooltip_show {
Some(
LabelOption {
series_name: series.name.clone(),
value,
percentage: value / sum,
formatter: "{a}: {c} ({d})".to_string(),
..Default::default()
}
.format(),
)
} else {
None
};
if let Some(ref t) = tooltip_text {
pie.title = Some(t.clone());
let trigger = match pie.class.take() {
Some(c) => format!("{c} ct-trigger"),
None => "ct-trigger".to_string(),
};
pie.class = Some(trigger);
}
c.pie(pie);
let is_inside = self.series_label_position == Some("inside".to_string());
let angle = start_angle + half_delta;
if let Some(text) = tooltip_text {
let p = get_pie_point(cx, cy, (cr + self.inner_radius) / 2.0, angle);
c.text(Text {
text,
class: Some("ct-tip".to_string()),
font_family: Some(self.font_family.clone()),
font_color: Some(self.series_label_font_color),
font_size: Some(self.series_label_font_size),
x: Some(p.x),
y: Some(p.y),
text_anchor: Some("middle".to_string()),
dominant_baseline: Some("central".to_string()),
..Default::default()
});
}
let label_option = LabelOption {
series_name: series.name.clone(),
value,
percentage: value / sum,
formatter: series_label_formatter.clone(),
..Default::default()
};
let label_text = label_option.format();
let label_margin = if is_inside {
let label_point = get_pie_point(cx, cy, 2. * cr / 3., angle);
let mut label_margin = Box {
left: label_point.x,
top: label_point.y,
..Default::default()
};
if let Ok(b) = measure_text_width_family(
&self.font_family,
self.series_label_font_size,
&label_text,
) {
label_margin.left -= b.width() / 2.;
}
label_margin
} else {
let mut points = vec![];
points.push(get_pie_point(cx, cy, cr, angle));
let mut end = get_pie_point(cx, cy, r + label_offset, angle);
let quadrant = get_quadrant(cx, cy, &end);
if quadrant != prev_quadrant {
prev_end_y = f32::MAX;
prev_quadrant = quadrant;
}
if (end.y - prev_end_y).abs() < self.series_label_font_size {
if quadrant == 1 || quadrant == 4 {
end.y = prev_end_y + self.series_label_font_size;
} else {
end.y = prev_end_y - self.series_label_font_size;
}
}
prev_end_y = end.y;
points.push(end);
let is_left = angle > 180.0;
if is_left {
end.x -= label_offset;
} else {
end.x += label_offset;
}
let mut label_margin = Box {
left: end.x,
top: end.y + 5.0,
..Default::default()
};
if is_left {
if let Ok(b) = measure_text_width_family(
&self.font_family,
self.series_label_font_size,
&label_text,
) {
label_margin.left -= b.width();
}
} else {
label_margin.left += 3.0;
}
points.push(end);
c.smooth_line(SmoothLine {
color: Some(color),
points,
symbol: None,
class: fade_class.clone(),
..Default::default()
});
label_margin
};
c.child(label_margin).text(Text {
text: label_text,
font_family: Some(self.font_family.clone()),
font_size: Some(self.series_label_font_size),
font_color: Some(self.series_label_font_color),
class: fade_class,
..Default::default()
});
start_angle += delta;
}
let mut css = String::new();
if let Some(ref anim) = self.animation {
css.push_str(&format!(
"@keyframes pie-grow{{from{{transform:scale(0)}}to{{transform:scale(1)}}}} \
@keyframes pie-fade{{from{{opacity:0}}to{{opacity:1}}}} \
.pie-anim{{transform-origin:{}px {}px;animation:pie-grow {}ms {} both}} \
.pie-fade{{animation:pie-fade {}ms {} both}}",
format_float(cx + c.margin.left),
format_float(cy + c.margin.top),
anim.duration,
anim.easing,
anim.duration,
anim.easing
));
}
if self.tooltip_show {
if !css.is_empty() {
css.push(' ');
}
css.push_str(TOOLTIP_STYLE);
}
if css.is_empty() {
c.svg()
} else {
c.svg_with_style(&css)
}
}
}
#[cfg(test)]
mod tests {
use super::PieChart;
use pretty_assertions::assert_eq;
#[test]
fn pie_basic() {
let mut pie_chart = PieChart::new(vec![
("rose 1", vec![40.0]).into(),
("rose 2", vec![38.0]).into(),
("rose 3", vec![32.0]).into(),
("rose 4", vec![30.0]).into(),
("rose 5", vec![28.0]).into(),
("rose 6", vec![26.0]).into(),
("rose 7", vec![22.0]).into(),
("rose 8", vec![18.0]).into(),
]);
pie_chart.title_text = "Nightingale Chart".to_string();
pie_chart.sub_title_text = "Fake Data".to_string();
assert_eq!(
include_str!("../../asset/pie_chart/basic.svg"),
pie_chart.svg().unwrap()
);
}
#[test]
fn small_pie_basic() {
let mut pie_chart = PieChart::new(vec![
("rose 1", vec![400.0]).into(),
("rose 2", vec![38.0]).into(),
("rose 3", vec![32.0]).into(),
("rose 4", vec![30.0]).into(),
("rose 5", vec![28.0]).into(),
("rose 6", vec![26.0]).into(),
("rose 7", vec![22.0]).into(),
("rose 8", vec![18.0]).into(),
]);
pie_chart.width = 400.0;
pie_chart.height = 300.0;
pie_chart.title_text = "Nightingale Chart".to_string();
pie_chart.sub_title_text = "Fake Data".to_string();
assert_eq!(
include_str!("../../asset/pie_chart/small_basic.svg"),
pie_chart.svg().unwrap()
);
}
#[test]
fn not_rose_pie() {
let mut pie_chart = PieChart::new(vec![
("rose 1", vec![400.0]).into(),
("rose 2", vec![38.0]).into(),
("rose 3", vec![32.0]).into(),
("rose 4", vec![30.0]).into(),
("rose 5", vec![28.0]).into(),
("rose 6", vec![26.0]).into(),
("rose 7", vec![22.0]).into(),
("rose 8", vec![18.0]).into(),
]);
pie_chart.rose_type = Some(false);
pie_chart.title_text = "Pie Chart".to_string();
pie_chart.sub_title_text = "Fake Data".to_string();
assert_eq!(
include_str!("../../asset/pie_chart/not_rose.svg").trim(),
pie_chart.svg().unwrap()
);
}
#[test]
fn not_rose_radius_pie() {
let mut pie_chart = PieChart::new(vec![
("rose 1", vec![40.0]).into(),
("rose 2", vec![38.0]).into(),
("rose 3", vec![32.0]).into(),
("rose 4", vec![30.0]).into(),
]);
pie_chart.start_angle = 10.0;
pie_chart.series_label_position = Some("inside".to_string());
pie_chart.rose_type = Some(false);
pie_chart.inner_radius = 0.0;
pie_chart.border_radius = Some(0.0);
pie_chart.title_text = "Pie Chart".to_string();
pie_chart.sub_title_text = "Fake Data".to_string();
assert_eq!(
include_str!("../../asset/pie_chart/not_rose_radius.svg").trim(),
pie_chart.svg().unwrap()
);
}
#[test]
fn pie_animation_json() {
let chart = PieChart::from_json(
r###"{
"series_list": [
{"name": "a", "data": [40]},
{"name": "b", "data": [60]}
],
"rose_type": false,
"animation": {"duration": 800, "easing": "ease-out", "delay": 50}
}"###,
)
.unwrap();
let svg = chart.svg().unwrap();
assert!(svg.contains("pie-grow"), "missing @keyframes pie-grow");
assert!(svg.contains(".pie-anim"), "missing .pie-anim class rule");
assert!(svg.contains("transform-origin"), "missing transform-origin");
assert!(svg.contains("800ms ease-out"), "missing duration/easing");
assert!(
svg.contains(r#"class="pie-anim""#),
"missing class attr on slice"
);
assert!(
svg.contains("animation-delay:0ms"),
"missing delay for slice 0"
);
assert!(
svg.contains("animation-delay:50ms"),
"missing delay for slice 1"
);
assert!(
svg.contains(r#"class="pie-fade""#),
"missing fade class on labels"
);
}
#[test]
fn pie_rose_small_piece() {
let mut pie_chart = PieChart::new(vec![
("rose 1", vec![40000.0]).into(),
("rose 2", vec![38.0]).into(),
("rose 3", vec![32.0]).into(),
("rose 4", vec![30.0]).into(),
("rose 5", vec![28.0]).into(),
("rose 6", vec![26.0]).into(),
("rose 7", vec![22.0]).into(),
("rose 8", vec![18.0]).into(),
]);
pie_chart.title_text = "Nightingale Chart".to_string();
pie_chart.sub_title_text = "Fake Data".to_string();
assert_eq!(
include_str!("../../asset/pie_chart/rose_small_piece.svg"),
pie_chart.svg().unwrap()
);
}
#[test]
fn pie_chart_tooltip() {
let chart = PieChart::from_json(
r#"{"tooltip_show": true, "series_list": [{"name": "a", "data": [10]}, {"name": "b", "data": [30]}]}"#,
)
.unwrap();
let svg = chart.svg().unwrap();
assert!(
svg.contains("<title>a: 10 (25%)</title>"),
"missing pie title"
);
assert!(svg.contains(r#"class="ct-tip""#), "missing hover label");
assert!(
svg.contains(".ct-trigger:hover+.ct-tip"),
"missing hover css"
);
let off = PieChart::from_json(
r#"{"series_list": [{"name": "a", "data": [10]}, {"name": "b", "data": [30]}]}"#,
)
.unwrap();
let off_svg = off.svg().unwrap();
assert!(!off_svg.contains("<title>"));
assert!(!off_svg.contains("ct-tip"));
}
}