T15 Charts: cartesian with axes, legend, crosshair, tooltip, zoom, annotations and export (T15a); pie, donut, radar, heatmaps, treemap, sunburst, waterfall and spreads (T15b); sankey, funnel, gantt, force graph, chord, parallel coordinates, bullet and progress (T15c)
This commit is contained in:
44 files changed
+8639
-35
No files matched your search
@@ -0,0 +1,295 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
App, Div, ElementId, Entity, IntoElement, ParentElement, PathBuilder, Pixels, Refineable,
|
||||
RenderOnce, SharedString, StyleRefinement, Styled, Window, canvas, div,
|
||||
};
|
||||
|
||||
use super::{
|
||||
axes::anchored,
|
||||
geometry::Rect,
|
||||
paint::{at, finish, measure, tint},
|
||||
parts::ChartTooltip,
|
||||
plot::Format,
|
||||
scale::{Linear, compact, nice},
|
||||
tiles::{Tiles, tracked},
|
||||
};
|
||||
use crate::theme::{ActiveTheme, TextSize};
|
||||
|
||||
/// Parallel axes: where each stands, its scale, and each record's line across them.
|
||||
pub(crate) struct Parallel {
|
||||
pub xs: Vec<f32>,
|
||||
pub scales: Vec<Linear>,
|
||||
pub lines: Vec<Vec<(f32, f32)>>,
|
||||
}
|
||||
|
||||
/// Records as lines across parallel axes, one axis per measure, each with its own rounded scale.
|
||||
pub(crate) fn parallel(records: &[Vec<f64>], axes: usize, frame: Rect) -> Parallel {
|
||||
let xs: Vec<f32> = (0..axes)
|
||||
.map(|ix| frame.x + frame.w * ix as f32 / (axes - 1).max(1) as f32)
|
||||
.collect();
|
||||
let scales: Vec<Linear> = (0..axes)
|
||||
.map(|axis| {
|
||||
let (low, high) = records
|
||||
.iter()
|
||||
.map(|record| record[axis])
|
||||
.fold((f64::MAX, f64::MIN), |(low, high), value| {
|
||||
(low.min(value), high.max(value))
|
||||
});
|
||||
let (domain, _) = if low <= high {
|
||||
nice(low, high, 4)
|
||||
} else {
|
||||
((0.0, 1.0), Vec::new())
|
||||
};
|
||||
Linear::new(domain, (frame.y + frame.h, frame.y))
|
||||
})
|
||||
.collect();
|
||||
let lines = records
|
||||
.iter()
|
||||
.map(|record| {
|
||||
record
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(axis, value)| (xs[axis], scales[axis].at(*value)))
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
Parallel { xs, scales, lines }
|
||||
}
|
||||
|
||||
/// How far a place lies from a line through points.
|
||||
fn distance((x, y): (f32, f32), line: &[(f32, f32)]) -> f32 {
|
||||
line.windows(2)
|
||||
.map(|pair| {
|
||||
let ((ax, ay), (bx, by)) = (pair[0], pair[1]);
|
||||
let (dx, dy) = (bx - ax, by - ay);
|
||||
let share = (((x - ax) * dx + (y - ay) * dy) / (dx * dx + dy * dy).max(f32::EPSILON))
|
||||
.clamp(0.0, 1.0);
|
||||
(x - ax - dx * share).hypot(y - ay - dy * share)
|
||||
})
|
||||
.fold(f32::MAX, f32::min)
|
||||
}
|
||||
|
||||
/// Records with many measures as lines across parallel axes, each axis on its own scale. Hover a line to read its record.
|
||||
#[derive(IntoElement)]
|
||||
pub struct ParallelCoordinates {
|
||||
base: Div,
|
||||
id: ElementId,
|
||||
axes: Vec<SharedString>,
|
||||
records: Vec<(SharedString, Vec<f64>)>,
|
||||
format: Format,
|
||||
}
|
||||
|
||||
impl ParallelCoordinates {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
axes: impl IntoIterator<Item = impl Into<SharedString>>,
|
||||
) -> Self {
|
||||
let axes: Vec<SharedString> = axes.into_iter().map(Into::into).collect();
|
||||
assert!(axes.len() >= 2, "parallel coordinates need two axes");
|
||||
Self {
|
||||
base: div(),
|
||||
id: id.into(),
|
||||
axes,
|
||||
records: Vec::new(),
|
||||
format: Rc::new(compact),
|
||||
}
|
||||
}
|
||||
|
||||
/// A record, a value for each axis.
|
||||
pub fn record(
|
||||
mut self,
|
||||
name: impl Into<SharedString>,
|
||||
values: impl IntoIterator<Item = f64>,
|
||||
) -> Self {
|
||||
let values: Vec<f64> = values.into_iter().collect();
|
||||
assert_eq!(
|
||||
values.len(),
|
||||
self.axes.len(),
|
||||
"a record needs a value per axis"
|
||||
);
|
||||
assert!(
|
||||
values.iter().all(|value| value.is_finite()),
|
||||
"a record needs finite values"
|
||||
);
|
||||
self.records.push((name.into(), values));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn format(mut self, format: impl Fn(f64) -> String + 'static) -> Self {
|
||||
self.format = Rc::new(format);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for ParallelCoordinates {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
self.base.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ParallelCoordinates {
|
||||
fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let tiles: Entity<Tiles> =
|
||||
window.use_keyed_state((self.id.clone(), "lines"), cx, |_, _| Tiles::default());
|
||||
let (bounds, hover) = (tiles.read(cx).bounds, tiles.read(cx).hover);
|
||||
let theme = cx.theme();
|
||||
let (colors, sizes, rem) = (theme.colors.clone(), theme.chart(), window.rem_size());
|
||||
let pixels = |length: gpui::Rems| f32::from(length.to_pixels(rem));
|
||||
let (side, top) = (pixels(sizes.gutter), pixels(sizes.foot));
|
||||
let (width, height) = (f32::from(bounds.size.width), f32::from(bounds.size.height));
|
||||
let frame = Rect {
|
||||
x: side,
|
||||
y: top,
|
||||
w: (width - side * 2.0).max(0.0),
|
||||
h: (height - top * 2.0).max(0.0),
|
||||
};
|
||||
let values: Vec<Vec<f64>> = self
|
||||
.records
|
||||
.iter()
|
||||
.map(|(_, values)| values.clone())
|
||||
.collect();
|
||||
let Parallel { xs, scales, lines } = parallel(&values, self.axes.len(), frame);
|
||||
let format = self.format.clone();
|
||||
let text = |content: String, color| {
|
||||
div()
|
||||
.flex_none()
|
||||
.whitespace_nowrap()
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.text_color(color)
|
||||
.child(content)
|
||||
};
|
||||
let axis_labels: Vec<Div> = xs
|
||||
.iter()
|
||||
.zip(&self.axes)
|
||||
.map(|(x, name)| {
|
||||
div()
|
||||
.absolute()
|
||||
.left(Pixels::from(*x))
|
||||
.top_0()
|
||||
.w_0()
|
||||
.h(Pixels::from(top))
|
||||
.flex()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child(text(name.to_string(), colors.fg_muted))
|
||||
})
|
||||
.collect();
|
||||
let ends: Vec<Div> = xs
|
||||
.iter()
|
||||
.zip(&scales)
|
||||
.flat_map(|(x, scale)| {
|
||||
[
|
||||
(scale.domain.1, frame.y),
|
||||
(scale.domain.0, frame.y + frame.h),
|
||||
]
|
||||
.map(|(value, y)| (*x, value, y))
|
||||
})
|
||||
.map(|(x, value, y)| {
|
||||
div()
|
||||
.absolute()
|
||||
.left(Pixels::from(x))
|
||||
.top(Pixels::from(y))
|
||||
.h_0()
|
||||
.flex()
|
||||
.items_center()
|
||||
.pl_1p5()
|
||||
.child(text(format(value), colors.fg_subtle))
|
||||
})
|
||||
.collect();
|
||||
let tooltip = hover.map(|ix| {
|
||||
let card = self.axes.iter().zip(&self.records[ix].1).fold(
|
||||
ChartTooltip::new(self.records[ix].0.clone()),
|
||||
|card, (axis, value)| card.row(None, axis.clone(), format(*value)),
|
||||
);
|
||||
let (x, y) = lines[ix][lines[ix].len() / 2];
|
||||
anchored((x, y), x > width * 0.6, card)
|
||||
});
|
||||
let (stroke, hairline, palette, painted) = (
|
||||
sizes.stroke.to_pixels(rem),
|
||||
sizes.hairline,
|
||||
colors.clone(),
|
||||
lines.clone(),
|
||||
);
|
||||
let reach = pixels(sizes.inset);
|
||||
let mut root = div().relative().w_full().h(sizes.height);
|
||||
root.style().refine(self.base.style());
|
||||
tracked(
|
||||
root,
|
||||
(self.id.clone(), "parallel").into(),
|
||||
&tiles,
|
||||
move |place| {
|
||||
lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, line)| (ix, distance(place, line)))
|
||||
.filter(|(_, far)| *far <= reach)
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||||
.map(|(ix, _)| ix)
|
||||
},
|
||||
)
|
||||
.child(
|
||||
canvas(
|
||||
|_, _, _| {},
|
||||
move |bounds, _, window, _| {
|
||||
let origin = bounds.origin;
|
||||
for x in &xs {
|
||||
let mut axis = PathBuilder::stroke(hairline);
|
||||
axis.move_to(at(origin, (*x, frame.y)));
|
||||
axis.line_to(at(origin, (*x, frame.y + frame.h)));
|
||||
finish(axis, palette.border_strong, window);
|
||||
}
|
||||
let order = (0..painted.len())
|
||||
.filter(|ix| Some(*ix) != hover)
|
||||
.chain(hover);
|
||||
for ix in order {
|
||||
let ink = tint(&palette, ix);
|
||||
let (color, width) = match hover {
|
||||
None => (ink.opacity(0.7), stroke / 1.5),
|
||||
Some(on) if on == ix => (ink, stroke),
|
||||
Some(_) => (ink.opacity(0.15), stroke / 1.5),
|
||||
};
|
||||
let mut path = PathBuilder::stroke(width);
|
||||
path.move_to(at(origin, painted[ix][0]));
|
||||
painted[ix][1..]
|
||||
.iter()
|
||||
.for_each(|point| path.line_to(at(origin, *point)));
|
||||
finish(path, color, window);
|
||||
}
|
||||
},
|
||||
)
|
||||
.absolute()
|
||||
.inset_0(),
|
||||
)
|
||||
.children(axis_labels)
|
||||
.children(ends)
|
||||
.children(tooltip)
|
||||
.child(measure(tiles, |tiles| &mut tiles.bounds))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parallel_axes_share_the_frame_and_lines_meet_their_values() {
|
||||
let frame = Rect {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
w: 100.0,
|
||||
h: 100.0,
|
||||
};
|
||||
let Parallel { xs, scales, lines } =
|
||||
parallel(&[vec![0.0, 10.0], vec![100.0, 0.0]], 2, frame);
|
||||
assert_eq!(xs, [0.0, 100.0]);
|
||||
assert_eq!(scales[0].domain, (0.0, 100.0));
|
||||
assert_eq!(
|
||||
lines[0],
|
||||
[(0.0, 100.0), (100.0, 0.0)],
|
||||
"the least sits low, the most high"
|
||||
);
|
||||
assert_eq!(distance((50.0, 50.0), &lines[0]), 0.0);
|
||||
assert!(distance((0.0, 0.0), &lines[0]) > 60.0);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user