mod bar;
mod line;
mod pie;
mod sparkline;
pub use bar::BarChart;
pub use line::LineChart;
pub use pie::PieChart;
pub use sparkline::Sparkline;
use gpui::{point, px, Bounds, Hsla, PathBuilder, Pixels, Point, Window};
use crate::style::ColorValue;
use crate::theme::{ColorName, Theme};
pub(crate) const SERIES_HUES: [ColorName; 12] = [
ColorName::Blue,
ColorName::Teal,
ColorName::Grape,
ColorName::Orange,
ColorName::Green,
ColorName::Red,
ColorName::Indigo,
ColorName::Yellow,
ColorName::Cyan,
ColorName::Pink,
ColorName::Lime,
ColorName::Violet,
];
pub(crate) fn chart_shade(t: &Theme) -> usize {
if t.scheme.is_dark() {
4
} else {
6
}
}
pub(crate) fn resolve_color(t: &Theme, color: ColorValue) -> Hsla {
match color {
ColorValue::Named(name) => t.color(name, chart_shade(t)).hsla(),
ColorValue::Custom(c) => c,
}
}
pub(crate) fn series_color(t: &Theme, overrides: &[ColorValue], index: usize) -> Hsla {
if overrides.is_empty() {
t.color(SERIES_HUES[index % SERIES_HUES.len()], chart_shade(t))
.hsla()
} else {
resolve_color(t, overrides[index % overrides.len()])
}
}
pub(crate) fn min_max(values: &[f32]) -> Option<(f32, f32)> {
let mut lo = f32::INFINITY;
let mut hi = f32::NEG_INFINITY;
for &v in values {
if !v.is_finite() {
continue;
}
lo = lo.min(v);
hi = hi.max(v);
}
(lo.is_finite() && hi.is_finite()).then_some((lo, hi))
}
pub(crate) fn normalize(values: &[f32]) -> Vec<f32> {
let Some((lo, hi)) = min_max(values) else {
return Vec::new();
};
let span = hi - lo;
values
.iter()
.map(|&v| {
if !v.is_finite() || span <= 0.0 {
0.5
} else {
((v - lo) / span).clamp(0.0, 1.0)
}
})
.collect()
}
pub(crate) fn bar_heights(values: &[f32]) -> Vec<f32> {
let max = values.iter().copied().fold(0.0_f32, f32::max);
values
.iter()
.map(|&v| {
if max > 0.0 && v.is_finite() {
(v.max(0.0) / max).min(1.0)
} else {
0.0
}
})
.collect()
}
pub(crate) fn bar_slot(i: usize, n: usize, w: f32, gap: f32) -> (f32, f32) {
let slot = w / n.max(1) as f32;
let bar = slot * (1.0 - gap.clamp(0.0, 0.9));
(i as f32 * slot + (slot - bar) / 2.0, bar)
}
pub(crate) fn slice_spans(values: &[f32]) -> Vec<(f32, f32)> {
let clean: Vec<f32> = values
.iter()
.map(|&v| if v.is_finite() && v > 0.0 { v } else { 0.0 })
.collect();
let total: f32 = clean.iter().sum();
if total <= 0.0 {
return clean.iter().map(|_| (0.0, 0.0)).collect();
}
let mut acc = 0.0;
clean
.iter()
.map(|&v| {
let start = acc / total;
acc += v;
(start, acc / total)
})
.collect()
}
pub(crate) fn arc_point(center: (f32, f32), radius: f32, fraction: f32) -> (f32, f32) {
let angle = fraction * std::f32::consts::TAU - std::f32::consts::FRAC_PI_2;
(
center.0 + radius * angle.cos(),
center.1 + radius * angle.sin(),
)
}
pub(crate) fn paint_polyline(
window: &mut Window,
bounds: Bounds<Pixels>,
values: &[f32],
stroke: f32,
line: Hsla,
area: Option<Hsla>,
) {
let ys = normalize(values);
if ys.len() < 2 {
return;
}
let w = f32::from(bounds.size.width);
let h = f32::from(bounds.size.height);
if w <= 0.0 || h <= 0.0 {
return;
}
let inset = stroke / 2.0;
let step = w / (ys.len() - 1) as f32;
let pts: Vec<Point<Pixels>> = ys
.iter()
.enumerate()
.map(|(i, t)| {
bounds.origin
+ point(
px(i as f32 * step),
px(inset + (h - 2.0 * inset) * (1.0 - t)),
)
})
.collect();
if let Some(color) = area {
let mut pb = PathBuilder::fill();
pb.move_to(bounds.origin + point(px(0.0), px(h)));
for p in &pts {
pb.line_to(*p);
}
pb.line_to(bounds.origin + point(px(w), px(h)));
pb.close();
if let Ok(path) = pb.build() {
window.paint_path(path, color);
}
}
let mut pb = PathBuilder::stroke(px(stroke));
pb.move_to(pts[0]);
for p in &pts[1..] {
pb.line_to(*p);
}
if let Ok(path) = pb.build() {
window.paint_path(path, line);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn min_max_finds_extremes_and_skips_non_finite() {
assert_eq!(min_max(&[3.0, -1.0, 7.0]), Some((-1.0, 7.0)));
assert_eq!(min_max(&[3.0, f32::NAN, 7.0]), Some((3.0, 7.0)));
assert_eq!(min_max(&[1.0, f32::INFINITY, 2.0]), Some((1.0, 2.0)));
assert_eq!(min_max(&[1.0, f32::NEG_INFINITY, 2.0]), Some((1.0, 2.0)));
assert_eq!(min_max(&[]), None);
assert_eq!(min_max(&[f32::NAN]), None);
assert_eq!(min_max(&[f32::INFINITY]), None);
}
#[test]
fn normalize_maps_min_to_zero_and_max_to_one() {
assert_eq!(normalize(&[2.0, 4.0, 6.0]), vec![0.0, 0.5, 1.0]);
}
#[test]
fn normalize_centers_flat_series() {
assert_eq!(normalize(&[5.0, 5.0, 5.0]), vec![0.5, 0.5, 0.5]);
assert!(normalize(&[f32::NAN, f32::NAN]).is_empty());
}
#[test]
fn normalize_centers_infinities_and_keeps_the_finite_trend() {
assert_eq!(normalize(&[1.0, 2.0, f32::INFINITY]), vec![0.0, 1.0, 0.5]);
}
#[test]
fn normalize_scales_tiny_spans() {
assert_eq!(normalize(&[0.0, 5.0e-8, 1.0e-7]), vec![0.0, 0.5, 1.0]);
}
#[test]
fn bar_heights_scale_to_tallest_and_clamp_negatives() {
assert_eq!(bar_heights(&[5.0, 10.0, -3.0]), vec![0.5, 1.0, 0.0]);
assert_eq!(bar_heights(&[-1.0, 0.0]), vec![0.0, 0.0]);
assert_eq!(bar_heights(&[]), Vec::<f32>::new());
}
#[test]
fn bar_slot_centers_bars_within_slots() {
assert_eq!(bar_slot(0, 2, 100.0, 0.2), (5.0, 40.0));
assert_eq!(bar_slot(1, 2, 100.0, 0.2), (55.0, 40.0));
assert_eq!(bar_slot(0, 4, 100.0, 0.0), (0.0, 25.0));
}
#[test]
fn slice_spans_accumulate_to_one() {
let spans = slice_spans(&[1.0, 1.0, 2.0]);
assert_eq!(spans, vec![(0.0, 0.25), (0.25, 0.5), (0.5, 1.0)]);
}
#[test]
fn slice_spans_zero_out_junk_values() {
let spans = slice_spans(&[-2.0, 4.0, f32::NAN, 4.0]);
assert_eq!(spans, vec![(0.0, 0.0), (0.0, 0.5), (0.5, 0.5), (0.5, 1.0)]);
assert_eq!(slice_spans(&[0.0, -1.0]), vec![(0.0, 0.0), (0.0, 0.0)]);
}
#[test]
fn arc_point_hits_the_cardinal_directions() {
let close = |(x, y): (f32, f32), (ex, ey): (f32, f32)| {
assert!((x - ex).abs() < 1e-4 && (y - ey).abs() < 1e-4, "({x},{y})");
};
let c = (10.0, 10.0);
close(arc_point(c, 5.0, 0.0), (10.0, 5.0)); close(arc_point(c, 5.0, 0.25), (15.0, 10.0)); close(arc_point(c, 5.0, 0.5), (10.0, 15.0)); close(arc_point(c, 5.0, 0.75), (5.0, 10.0)); }
}