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,232 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use ely_gpui_component::{
|
||||
buttons::{Button, ButtonVariant},
|
||||
charts::{
|
||||
AreaChart, BarChart, ChartTooltip, LineChart, Points, RealtimeChart, ScatterChart, Series,
|
||||
},
|
||||
theme::ActiveTheme,
|
||||
typography::Caption,
|
||||
};
|
||||
use gpui::{App, ClipboardItem, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{keep, row, section, set},
|
||||
};
|
||||
|
||||
const MONTHS: [&str; 12] = [
|
||||
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
||||
];
|
||||
|
||||
/// A steady pseudo-random series, so captures repeat.
|
||||
pub(super) fn noise(seed: u64) -> impl FnMut() -> f64 {
|
||||
let mut state = seed;
|
||||
move || {
|
||||
state = state
|
||||
.wrapping_mul(6364136223846793005)
|
||||
.wrapping_add(1442695040888963407);
|
||||
(state >> 33) as f64 / (1u64 << 31) as f64
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lines(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let turn = keep("lines-turn", || 0usize, window, cx);
|
||||
let (now, next) = (*turn.read(cx), turn.clone());
|
||||
let copied = keep("lines-copied", || None::<&'static str>, window, cx);
|
||||
let (told, tell) = (*copied.read(cx), copied.clone());
|
||||
let mut rng = noise(21 + now as u64);
|
||||
let revenue: Vec<f64> = (0..12)
|
||||
.map(|month| 40.0 + month as f64 * 4.0 + rng() * 12.0)
|
||||
.collect();
|
||||
let costs: Vec<f64> = (0..12)
|
||||
.map(|month| 30.0 + month as f64 * 2.5 + rng() * 8.0)
|
||||
.collect();
|
||||
let chart = || {
|
||||
LineChart::new("revenue", MONTHS)
|
||||
.series(Series::new("Revenue", revenue.clone()))
|
||||
.series(Series::new("Costs", costs.clone()))
|
||||
.smooth()
|
||||
.rule(70.0, "Target")
|
||||
.note(8, "Launch")
|
||||
.zoom()
|
||||
.format(|value| {
|
||||
if value == 0.0 {
|
||||
"$0".into()
|
||||
} else {
|
||||
format!("${value:.0}k")
|
||||
}
|
||||
})
|
||||
};
|
||||
let (for_svg, for_csv) = (chart(), chart());
|
||||
section(
|
||||
"LineChart / ChartAxis / ChartGrid / ChartLegend / ChartTooltip / ChartCrosshair / ChartAnnotation / ChartZoom / ChartExport",
|
||||
"Hover for a crosshair and each series' value. Press a name in the legend to hide it. Drag across the chart to zoom into a span; a double press shows the whole year. The target and the launch are annotations. Copy the chart as SVG or its data as CSV.",
|
||||
cx,
|
||||
)
|
||||
.child(probe("revenue", div().w(px(720.)).child(chart())))
|
||||
.child(
|
||||
row()
|
||||
.child(Button::new("lines-next", "New data").variant(ButtonVariant::Ghost).on_click(move |_, _, cx| set(&next, now + 1, cx)))
|
||||
.child(Button::new("lines-svg", "Copy SVG").variant(ButtonVariant::Ghost).on_click({
|
||||
let tell = tell.clone();
|
||||
move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(for_svg.svg(720.0, 240.0, cx)));
|
||||
set(&tell, Some("SVG"), cx)
|
||||
}
|
||||
}))
|
||||
.child(Button::new("lines-csv", "Copy CSV").variant(ButtonVariant::Ghost).on_click(move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(for_csv.csv()));
|
||||
set(&tell, Some("CSV"), cx)
|
||||
})),
|
||||
)
|
||||
.children(told.map(|what| Caption::new(format!("Copied the chart as {what}."))))
|
||||
}
|
||||
|
||||
pub fn areas_and_bars(cx: &mut App) -> impl IntoElement + use<> {
|
||||
let quarters = ["Q1", "Q2", "Q3", "Q4"];
|
||||
let mut rng = noise(5);
|
||||
let mut series = |name: &'static str, base: f64| {
|
||||
Series::new(
|
||||
name,
|
||||
(0..4)
|
||||
.map(|q| base + q as f64 * 6.0 + rng() * 10.0)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
};
|
||||
let (web, ios, android) = (
|
||||
series("Web", 30.0),
|
||||
series("iOS", 22.0),
|
||||
series("Android", 16.0),
|
||||
);
|
||||
section(
|
||||
"AreaChart / StackedArea / BarChart",
|
||||
"Areas stack each platform on the ones before it. Bars stand grouped, stacked, or run sideways.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_6()
|
||||
.child(div().w(px(360.)).child(AreaChart::new("areas", quarters).series(web.clone()).series(ios.clone()).series(android.clone()).stacked().smooth()))
|
||||
.child(probe("bars", div().w(px(360.)).child(BarChart::new("bars", quarters).series(web.clone()).series(ios.clone()).series(android.clone())))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_6()
|
||||
.child(div().w(px(360.)).child(BarChart::new("stacked", quarters).series(web.clone()).series(ios.clone()).series(android.clone()).stacked()))
|
||||
.child(div().w(px(360.)).child(BarChart::new("sideways", ["Search", "Direct", "Social", "Email"]).series(Series::new("Visits", [412.0, 288.0, 164.0, 91.0])).horizontal())),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn scatter(cx: &mut App) -> impl IntoElement + use<> {
|
||||
let mut rng = noise(9);
|
||||
let cloud = |name: &'static str, cx0: f64, cy0: f64, rng: &mut dyn FnMut() -> f64| {
|
||||
Points::new(
|
||||
name,
|
||||
(0..28)
|
||||
.map(|_| (cx0 + (rng() - 0.5) * 40.0, cy0 + (rng() - 0.5) * 30.0))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
};
|
||||
let (a, b) = (
|
||||
cloud("Trial", 40.0, 60.0, &mut rng),
|
||||
cloud("Paid", 75.0, 35.0, &mut rng),
|
||||
);
|
||||
let cities = Points::new(
|
||||
"Cities",
|
||||
[
|
||||
(12.0, 70.0),
|
||||
(35.0, 52.0),
|
||||
(58.0, 81.0),
|
||||
(80.0, 44.0),
|
||||
(94.0, 66.0),
|
||||
],
|
||||
)
|
||||
.sizes([3.2, 8.9, 1.4, 12.5, 5.6]);
|
||||
section(
|
||||
"ScatterChart / BubbleChart",
|
||||
"Points placed by two values; sizes turn them into bubbles whose areas compare.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_6()
|
||||
.child(
|
||||
div().w(px(360.)).child(
|
||||
ScatterChart::new("scatter")
|
||||
.points(a)
|
||||
.points(b)
|
||||
.axes("Sessions", "Retention"),
|
||||
),
|
||||
)
|
||||
.child(probe(
|
||||
"bubbles",
|
||||
div().w(px(360.)).child(
|
||||
ScatterChart::new("bubbles")
|
||||
.points(cities)
|
||||
.axes("Price", "Rating"),
|
||||
),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn realtime(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let stream = keep(
|
||||
"stream",
|
||||
|| {
|
||||
let mut rng = noise(17);
|
||||
(
|
||||
(0..40).map(|_| 50.0 + rng() * 20.0).collect::<Vec<f64>>(),
|
||||
40u64,
|
||||
)
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let ticking = keep("stream-tick", || false, window, cx);
|
||||
if !*ticking.read(cx) && !cx.theme().reduced_motion {
|
||||
set(&ticking, true, cx);
|
||||
let stream = stream.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let mut rng = noise(29);
|
||||
loop {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(900))
|
||||
.await;
|
||||
let pushed = cx.update(|cx| {
|
||||
stream.update(cx, |(values, pushed), cx| {
|
||||
let last = values.last().copied().unwrap_or(60.0);
|
||||
values.push((last + (rng() - 0.5) * 10.0).clamp(20.0, 100.0));
|
||||
values.remove(0);
|
||||
*pushed += 1;
|
||||
cx.notify();
|
||||
})
|
||||
});
|
||||
if pushed.is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
let (values, pushed) = stream.read(cx).clone();
|
||||
let latest = values.last().copied().unwrap_or_default();
|
||||
section(
|
||||
"RealtimeChart",
|
||||
"A new reading every second; the line slides left as each arrives.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(px(720.))
|
||||
.child(RealtimeChart::new("stream", values, pushed)),
|
||||
)
|
||||
.child(div().w(px(200.)).child(ChartTooltip::new("Latest").row(
|
||||
Some(cx.theme().colors.chart[0]),
|
||||
"Requests per second",
|
||||
SharedString::from(format!("{latest:.1}")),
|
||||
)))
|
||||
}
|
||||
Reference in new issue
Block a user