use gpui::prelude::*;
use gpui::{canvas, div, point, px, App, Hsla, IntoElement, PathBuilder, SharedString, Window};
use crate::style::ColorValue;
use crate::theme::{theme, Size};
use super::{arc_point, series_color, slice_spans};
#[derive(IntoElement)]
pub struct PieChart {
values: Vec<f32>,
labels: Vec<SharedString>,
colors: Vec<ColorValue>,
size: f32,
donut: Option<f32>,
}
impl PieChart {
pub fn new(values: impl IntoIterator<Item = f32>) -> Self {
PieChart {
values: values.into_iter().collect(),
labels: Vec::new(),
colors: Vec::new(),
size: 160.0,
donut: None,
}
}
pub fn entries(entries: impl IntoIterator<Item = (impl Into<SharedString>, f32)>) -> Self {
let (labels, values): (Vec<SharedString>, Vec<f32>) = entries
.into_iter()
.map(|(label, value)| (label.into(), value))
.unzip();
PieChart {
labels,
..PieChart::new(values)
}
}
pub fn color(mut self, color: impl Into<ColorValue>) -> Self {
self.colors = vec![color.into()];
self
}
pub fn colors(mut self, colors: impl IntoIterator<Item = impl Into<ColorValue>>) -> Self {
self.colors = colors.into_iter().map(Into::into).collect();
self
}
pub fn size(mut self, size: f32) -> Self {
self.size = size;
self
}
pub fn donut(mut self, inner_fraction: f32) -> Self {
self.donut = Some(inner_fraction.clamp(0.05, 0.95));
self
}
}
impl RenderOnce for PieChart {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let t = theme(cx);
let n = self.values.len();
let slice_colors: Vec<Hsla> = (0..n).map(|i| series_color(t, &self.colors, i)).collect();
let legend_colors = slice_colors.clone();
let dimmed = t.dimmed().hsla();
let font_xs = t.font_size(Size::Xs);
let spans = slice_spans(&self.values);
let donut = self.donut;
let diameter = self.size;
let plot = canvas(
|_, _, _| (),
move |bounds, _, window, _cx| {
let w = f32::from(bounds.size.width);
let h = f32::from(bounds.size.height);
let radius = w.min(h) / 2.0;
if radius <= 0.0 {
return;
}
let center = (
f32::from(bounds.origin.x) + w / 2.0,
f32::from(bounds.origin.y) + h / 2.0,
);
for (i, &(start, end)) in spans.iter().enumerate() {
if end - start <= f32::EPSILON {
continue;
}
let color = slice_colors[i];
let inner = donut.map(|f| radius * f);
if end - start >= 0.999 {
let mid = start + 0.5;
paint_slice(window, center, radius, inner, start, mid, color);
paint_slice(window, center, radius, inner, mid, start + 1.0, color);
} else {
paint_slice(window, center, radius, inner, start, end, color);
}
}
},
)
.w(px(diameter))
.h(px(diameter));
let mut root = div()
.flex()
.flex_col()
.items_center()
.gap(px(8.0))
.child(plot);
if !self.labels.is_empty() && !legend_colors.is_empty() {
let items = self.labels.iter().enumerate().map(|(i, label)| {
div()
.flex()
.flex_row()
.items_center()
.gap(px(6.0))
.child(
div()
.w(px(8.0))
.h(px(8.0))
.rounded(px(4.0))
.bg(legend_colors[i % legend_colors.len()]),
)
.child(
div()
.text_size(px(font_xs))
.text_color(dimmed)
.child(label.clone()),
)
});
root = root.child(
div()
.flex()
.flex_row()
.flex_wrap()
.justify_center()
.gap(px(12.0))
.children(items),
);
}
root
}
}
fn paint_slice(
window: &mut Window,
center: (f32, f32),
radius: f32,
inner: Option<f32>,
start: f32,
end: f32,
color: Hsla,
) {
let large = end - start > 0.5;
let to_pt = |(x, y): (f32, f32)| point(px(x), px(y));
let outer_start = to_pt(arc_point(center, radius, start));
let outer_end = to_pt(arc_point(center, radius, end));
let radii = point(px(radius), px(radius));
let mut pb = PathBuilder::fill();
match inner {
Some(hole) if hole > 0.0 => {
let inner_radii = point(px(hole), px(hole));
pb.move_to(outer_start);
pb.arc_to(radii, px(0.0), large, true, outer_end);
pb.line_to(to_pt(arc_point(center, hole, end)));
pb.arc_to(
inner_radii,
px(0.0),
large,
false,
to_pt(arc_point(center, hole, start)),
);
pb.close();
}
_ => {
pb.move_to(to_pt(center));
pb.line_to(outer_start);
pb.arc_to(radii, px(0.0), large, true, outer_end);
pb.close();
}
}
if let Ok(path) = pb.build() {
window.paint_path(path, color);
}
}