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}")),
|
||||
)))
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
use ely_gpui_component::charts::{
|
||||
Bullet, BulletChart, ChordDiagram, FunnelChart, GanttChart, NetworkGraph, ParallelCoordinates,
|
||||
ProgressChart, SankeyChart, Task,
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, Styled, div, px};
|
||||
use jiff::civil::date;
|
||||
|
||||
use super::cartesian::noise;
|
||||
use crate::{probe::probe, ui::section};
|
||||
|
||||
pub fn flows(cx: &mut App) -> impl IntoElement + use<> {
|
||||
let budget = SankeyChart::new(
|
||||
"budget",
|
||||
[
|
||||
"Salary",
|
||||
"Freelance",
|
||||
"Income",
|
||||
"Home",
|
||||
"Food",
|
||||
"Savings",
|
||||
"Travel",
|
||||
],
|
||||
)
|
||||
.link(0, 2, 5200.0)
|
||||
.link(1, 2, 1400.0)
|
||||
.link(2, 3, 2100.0)
|
||||
.link(2, 4, 900.0)
|
||||
.link(2, 5, 2600.0)
|
||||
.link(2, 6, 1000.0)
|
||||
.format(|value| format!("${value:.0}"));
|
||||
let signups = [
|
||||
("Visited", 48200.0),
|
||||
("Signed up", 9640.0),
|
||||
("Verified", 7420.0),
|
||||
("Invited a teammate", 3110.0),
|
||||
("Paid", 1260.0),
|
||||
]
|
||||
.iter()
|
||||
.fold(FunnelChart::new("signups"), |funnel, (name, value)| {
|
||||
funnel.stage(*name, *value)
|
||||
});
|
||||
section(
|
||||
"SankeyChart / FunnelChart",
|
||||
"A month's money from where it came to where it went, and how many of a month's visitors reached each step. Hover a node, a ribbon or a stage.",
|
||||
cx,
|
||||
)
|
||||
.child(probe("sankey", div().w(px(720.)).child(budget)))
|
||||
.child(probe("funnel", div().w(px(720.)).child(signups)))
|
||||
}
|
||||
|
||||
pub fn schedule(cx: &mut App) -> impl IntoElement + use<> {
|
||||
let plan = GanttChart::new("launch")
|
||||
.task(Task::new("Research", date(2026, 9, 1), date(2026, 9, 11)).done(1.0))
|
||||
.task(
|
||||
Task::new("Design", date(2026, 9, 9), date(2026, 9, 25))
|
||||
.done(0.8)
|
||||
.after(0),
|
||||
)
|
||||
.task(
|
||||
Task::new("Build", date(2026, 9, 21), date(2026, 10, 16))
|
||||
.done(0.35)
|
||||
.after(1),
|
||||
)
|
||||
.task(
|
||||
Task::new("Write the docs", date(2026, 10, 5), date(2026, 10, 20))
|
||||
.done(0.1)
|
||||
.after(1),
|
||||
)
|
||||
.task(
|
||||
Task::new("Beta", date(2026, 10, 19), date(2026, 10, 30))
|
||||
.after(2)
|
||||
.after(3),
|
||||
)
|
||||
.task(Task::new("Launch", date(2026, 11, 2), date(2026, 11, 3)).after(4))
|
||||
.today(date(2026, 9, 25));
|
||||
section(
|
||||
"GanttChart",
|
||||
"A launch plan: each bar filled as far as it is done, arrows from the work it waits for, and a line at today. Hover a row.",
|
||||
cx,
|
||||
)
|
||||
.child(probe("gantt", div().w(px(720.)).child(plan)))
|
||||
}
|
||||
|
||||
pub fn relations(cx: &mut App) -> impl IntoElement + use<> {
|
||||
let people = [
|
||||
"Ada", "Grace", "Alan", "Edsger", "Barbara", "Donald", "Ken", "Dennis", "Bjarne", "Guido",
|
||||
"Linus", "Margaret",
|
||||
];
|
||||
let graph = people
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(NetworkGraph::new("people"), |graph, (ix, name)| {
|
||||
graph.node(*name, ix % 3)
|
||||
});
|
||||
let graph = [
|
||||
(0, 1),
|
||||
(0, 2),
|
||||
(1, 2),
|
||||
(2, 3),
|
||||
(3, 4),
|
||||
(4, 1),
|
||||
(5, 3),
|
||||
(6, 7),
|
||||
(7, 8),
|
||||
(6, 8),
|
||||
(8, 9),
|
||||
(9, 10),
|
||||
(10, 6),
|
||||
(11, 0),
|
||||
(11, 5),
|
||||
(5, 6),
|
||||
]
|
||||
.into_iter()
|
||||
.fold(graph, |graph, (a, b)| graph.edge(a, b));
|
||||
let regions = ["Europe", "Americas", "Asia", "Africa", "Oceania"];
|
||||
let trade = vec![
|
||||
vec![0.0, 42.0, 31.0, 12.0, 5.0],
|
||||
vec![38.0, 0.0, 44.0, 6.0, 7.0],
|
||||
vec![35.0, 50.0, 0.0, 14.0, 18.0],
|
||||
vec![15.0, 5.0, 11.0, 0.0, 2.0],
|
||||
vec![4.0, 6.0, 20.0, 1.0, 0.0],
|
||||
];
|
||||
let mut rng = noise(43);
|
||||
let axes = ["Price", "Weight", "Range", "Charge", "Seats"];
|
||||
let cars = ["Aria", "Bolt", "Crest", "Dune", "Echo", "Flux"]
|
||||
.iter()
|
||||
.fold(ParallelCoordinates::new("cars", axes), |chart, name| {
|
||||
chart.record(
|
||||
*name,
|
||||
[
|
||||
28.0 + rng() * 40.0,
|
||||
1.4 + rng() * 1.2,
|
||||
250.0 + rng() * 350.0,
|
||||
20.0 + rng() * 40.0,
|
||||
(4.0 + rng() * 3.0).floor(),
|
||||
],
|
||||
)
|
||||
});
|
||||
section(
|
||||
"NetworkGraph / ForceGraph / ChordDiagram / ParallelCoordinates",
|
||||
"People and who they work with, settling as forces pull and push: drag a node, hover one to light its ties. Trade between regions around a circle, and cars on five measures at once.",
|
||||
cx,
|
||||
)
|
||||
.child(probe("network", div().w(px(720.)).child(graph)))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_6()
|
||||
.child(probe("chord", div().w(px(360.)).child(ChordDiagram::new("trade", regions, trade))))
|
||||
.child(probe("parallel", div().w(px(420.)).child(cars))),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn targets(cx: &mut App) -> impl IntoElement + use<> {
|
||||
let bullets = BulletChart::new("quarter")
|
||||
.bullet(Bullet::new("Revenue", 268.0, 250.0).bands([150.0, 225.0, 300.0]))
|
||||
.bullet(Bullet::new("Profit", 22.0, 27.0).bands([20.0, 25.0, 30.0]))
|
||||
.bullet(Bullet::new("New customers", 1680.0, 2000.0).bands([1000.0, 1500.0, 2500.0]));
|
||||
let rings = ProgressChart::new("goals")
|
||||
.goal("Move", 480.0, 600.0)
|
||||
.goal("Exercise", 27.0, 30.0)
|
||||
.goal("Stand", 9.0, 12.0);
|
||||
section(
|
||||
"Bullet Chart / ProgressChart",
|
||||
"A quarter's measures against their targets over bands from poor to good, and a day's goals as rings that fill toward them.",
|
||||
cx,
|
||||
)
|
||||
.child(probe("bullets", div().w(px(720.)).child(bullets)))
|
||||
.child(probe("rings", rings))
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
mod cartesian;
|
||||
mod flows;
|
||||
mod spread;
|
||||
mod wholes;
|
||||
|
||||
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
|
||||
|
||||
use super::Page;
|
||||
use crate::script::Step;
|
||||
|
||||
pub const PAGE: Page = Page {
|
||||
number: 15,
|
||||
slug: "charts",
|
||||
title: "Charts",
|
||||
summary: "Lines, bars, parts of a whole, spreads, flows and schedules on quiet axes, with legends, tooltips, zoom and export.",
|
||||
render,
|
||||
script: SCRIPT,
|
||||
};
|
||||
|
||||
const SCRIPT: &[Step] = &[
|
||||
Step::Rest,
|
||||
Step::HoverAt("revenue", 400.0, 140.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("crosshair"),
|
||||
Step::DownAt("revenue", 150.0, 140.0),
|
||||
Step::DragTo("revenue", 520.0, 140.0),
|
||||
Step::UpAt("revenue", 520.0, 140.0),
|
||||
Step::Wait(300),
|
||||
Step::HoverAt("revenue", 360.0, 120.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("zoomed"),
|
||||
Step::DownAt("revenue", 100.0, 8.0),
|
||||
Step::UpAt("revenue", 100.0, 8.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("hidden"),
|
||||
Step::HoverAt("bars", 160.0, 150.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("band"),
|
||||
Step::HoverAt("pie", 236.0, 95.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("slice"),
|
||||
Step::HoverAt("donut", 109.0, 223.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("donut"),
|
||||
Step::HoverAt("bubbles", 291.0, 232.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("bubble"),
|
||||
Step::HoverAt("radar", 309.0, 112.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("spoke"),
|
||||
Step::HoverAt("treemap", 60.0, 60.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("tile"),
|
||||
Step::HoverAt("sankey", 490.0, 177.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("ribbon"),
|
||||
Step::HoverAt("gantt", 400.0, 98.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("task"),
|
||||
Step::HoverAt("chord", 246.0, 58.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("chord"),
|
||||
Step::Rest,
|
||||
];
|
||||
|
||||
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
div()
|
||||
.child(cartesian::lines(window, cx))
|
||||
.child(cartesian::areas_and_bars(cx))
|
||||
.child(wholes::pies(cx))
|
||||
.child(cartesian::scatter(cx))
|
||||
.child(wholes::radar(cx))
|
||||
.child(wholes::heatmaps(cx))
|
||||
.child(wholes::parts(cx))
|
||||
.child(flows::flows(cx))
|
||||
.child(spread::waterfall(cx))
|
||||
.child(spread::distributions(cx))
|
||||
.child(flows::schedule(cx))
|
||||
.child(flows::relations(cx))
|
||||
.child(flows::targets(cx))
|
||||
.child(cartesian::realtime(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use ely_gpui_component::charts::{BoxPlot, Histogram, ViolinPlot, WaterfallChart};
|
||||
use gpui::{App, IntoElement, ParentElement, Styled, div, px};
|
||||
|
||||
use super::cartesian::noise;
|
||||
use crate::ui::section;
|
||||
|
||||
pub fn distributions(cx: &mut App) -> impl IntoElement + use<> {
|
||||
let mut rng = noise(13);
|
||||
let mut normal =
|
||||
|mean: f64, spread: f64| -> f64 { mean + spread * ((rng() + rng() + rng() + rng()) - 2.0) };
|
||||
let times: Vec<f64> = (0..240).map(|_| normal(320.0, 120.0).max(40.0)).collect();
|
||||
let groups: Vec<(&str, Vec<f64>)> = [("Europe", 220.0), ("Americas", 310.0), ("Asia", 270.0)]
|
||||
.into_iter()
|
||||
.map(|(name, mean)| (name, (0..60).map(|_| normal(mean, 90.0)).collect()))
|
||||
.collect();
|
||||
let boxes = groups
|
||||
.iter()
|
||||
.fold(BoxPlot::new("boxes"), |plot, (name, values)| {
|
||||
plot.group(*name, values.clone())
|
||||
});
|
||||
let violins = groups
|
||||
.iter()
|
||||
.fold(ViolinPlot::new("violins"), |plot, (name, values)| {
|
||||
plot.group(*name, values.clone())
|
||||
});
|
||||
section(
|
||||
"Histogram / BoxPlot / ViolinPlot",
|
||||
"Response times by count in equal bins, and by region as boxes and as violins that widen where values gather.",
|
||||
cx,
|
||||
)
|
||||
.child(div().w(px(720.)).child(Histogram::new("times", times).bins(12)))
|
||||
.child(div().flex().gap_6().child(div().w(px(360.)).child(boxes)).child(div().w(px(360.)).child(violins)))
|
||||
}
|
||||
|
||||
pub fn waterfall(cx: &mut App) -> impl IntoElement + use<> {
|
||||
section(
|
||||
"WaterfallChart",
|
||||
"How a year's revenue became its profit: each step floats from where the last left off, and the totals stand on zero.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div().w(px(720.)).child(
|
||||
WaterfallChart::new("profit")
|
||||
.step("Revenue", 420.0)
|
||||
.step("Refunds", -36.0)
|
||||
.total("Net sales")
|
||||
.step("Cost of sales", -148.0)
|
||||
.step("Operating", -121.0)
|
||||
.step("Other income", 18.0)
|
||||
.total("Profit"),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
use ely_gpui_component::charts::{
|
||||
CalendarHeatmap, HeatmapChart, PieChart, RadarChart, Series, Slice, Sunburst, Treemap,
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, Styled, div, px};
|
||||
use jiff::civil::date;
|
||||
|
||||
use super::cartesian::noise;
|
||||
use crate::{probe::probe, ui::section};
|
||||
|
||||
pub fn pies(cx: &mut App) -> impl IntoElement + use<> {
|
||||
let traffic = [
|
||||
("Search", 412.0),
|
||||
("Direct", 288.0),
|
||||
("Social", 164.0),
|
||||
("Email", 91.0),
|
||||
];
|
||||
let pie = traffic
|
||||
.iter()
|
||||
.fold(PieChart::new("sources"), |pie, (name, value)| {
|
||||
pie.slice(*name, *value)
|
||||
});
|
||||
let donut = [("Design", 38.0), ("Engineering", 54.0), ("Research", 12.0)]
|
||||
.iter()
|
||||
.fold(PieChart::new("time"), |pie, (name, hours)| {
|
||||
pie.slice(*name, *hours)
|
||||
})
|
||||
.donut("104 h", "This week");
|
||||
section(
|
||||
"PieChart / DonutChart",
|
||||
"Parts of a whole, clockwise from twelve. Hover a slice: it lifts and reads its share; the donut reads it in the middle.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_6()
|
||||
.child(probe("pie", div().w(px(360.)).child(pie)))
|
||||
.child(probe("donut", div().w(px(360.)).child(donut))),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn radar(cx: &mut App) -> impl IntoElement + use<> {
|
||||
let axes = ["Speed", "Quality", "Price", "Support", "Docs", "Reach"];
|
||||
section(
|
||||
"RadarChart",
|
||||
"Two products on six measures. Hover a spoke to read both; press a name to hide it.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"radar",
|
||||
div().w(px(480.)).child(
|
||||
RadarChart::new("compare", axes)
|
||||
.series(Series::new("Ely", [86.0, 92.0, 70.0, 81.0, 88.0, 64.0]))
|
||||
.series(Series::new("Others", [72.0, 68.0, 84.0, 60.0, 55.0, 79.0])),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn parts(cx: &mut App) -> impl IntoElement + use<> {
|
||||
let spend = [
|
||||
("Payroll", 612.0),
|
||||
("Cloud", 184.0),
|
||||
("Office", 96.0),
|
||||
("Travel", 58.0),
|
||||
("Software", 72.0),
|
||||
("Events", 31.0),
|
||||
("Legal", 22.0),
|
||||
];
|
||||
let treemap = spend
|
||||
.iter()
|
||||
.fold(Treemap::new("spend"), |map, (name, value)| {
|
||||
map.tile(*name, *value)
|
||||
})
|
||||
.format(|value| format!("${value:.0}k"));
|
||||
let teams = [
|
||||
Slice::new("Product", 0.0).children([
|
||||
Slice::new("Design", 14.0),
|
||||
Slice::new("Research", 6.0),
|
||||
Slice::new("Writing", 4.0),
|
||||
]),
|
||||
Slice::new("Engineering", 0.0).children([
|
||||
Slice::new("Platform", 0.0)
|
||||
.children([Slice::new("Runtime", 9.0), Slice::new("Build", 5.0)]),
|
||||
Slice::new("Apps", 18.0),
|
||||
]),
|
||||
Slice::new("Go to market", 0.0)
|
||||
.children([Slice::new("Sales", 11.0), Slice::new("Support", 8.0)]),
|
||||
];
|
||||
section(
|
||||
"Treemap / Sunburst",
|
||||
"A year's spend as tiles whose areas compare, and a company as rings: teams inside groups inside the whole. Hover to read a part.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_6()
|
||||
.child(probe("treemap", div().w(px(420.)).child(treemap)))
|
||||
.child(probe("sunburst", div().w(px(300.)).child(Sunburst::new("teams", teams)))),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn heatmaps(cx: &mut App) -> impl IntoElement + use<> {
|
||||
let hours = ["0", "3", "6", "9", "12", "15", "18", "21"];
|
||||
let mut rng = noise(31);
|
||||
let busy = |day: usize, hour: usize, rng: &mut dyn FnMut() -> f64| {
|
||||
let work = if day < 5 && (3..7).contains(&hour) {
|
||||
60.0
|
||||
} else {
|
||||
12.0
|
||||
};
|
||||
work + rng() * 30.0
|
||||
};
|
||||
let week = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(HeatmapChart::new("load", hours), |chart, (day, name)| {
|
||||
chart.row(
|
||||
*name,
|
||||
(0..8)
|
||||
.map(|hour| busy(day, hour, &mut rng))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
});
|
||||
let mut rng = noise(37);
|
||||
let commits: Vec<f64> = (0..365)
|
||||
.map(|day| {
|
||||
if day % 7 >= 5 {
|
||||
(rng() * 3.0).floor()
|
||||
} else {
|
||||
(rng() * 12.0).floor()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
section(
|
||||
"HeatmapChart / CalendarHeatmap",
|
||||
"Load by weekday and hour, and a year of commits a day, Monday first. Hover a cell to read it.",
|
||||
cx,
|
||||
)
|
||||
.child(probe("load", div().w(px(560.)).child(week)))
|
||||
.child(probe("year", div().w(px(720.)).child(CalendarHeatmap::new("commits", date(2026, 1, 1), commits))))
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
mod buttons;
|
||||
mod charts;
|
||||
mod data;
|
||||
mod feedback;
|
||||
mod forms;
|
||||
@@ -43,6 +44,7 @@ pub const ALL: &[Page] = &[
|
||||
data::PAGE,
|
||||
lists::PAGE,
|
||||
tables::PAGE,
|
||||
charts::PAGE,
|
||||
theme::PAGE,
|
||||
];
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ use crate::{
|
||||
pub enum Step {
|
||||
Rest,
|
||||
Hover(&'static str),
|
||||
/// Moves the pointer to an offset from the probe's top-left corner.
|
||||
HoverAt(&'static str, f32, f32),
|
||||
Down(&'static str),
|
||||
Up(&'static str),
|
||||
Click(&'static str),
|
||||
@@ -87,6 +89,10 @@ pub async fn play(
|
||||
let at = target(window, key, cx).await?;
|
||||
send(window, Mouse::Move, at, cx)?;
|
||||
}
|
||||
Step::HoverAt(key, x, y) => {
|
||||
last = target_bounds(window, key, cx).await?.origin + point(px(x), px(y));
|
||||
send(window, Mouse::Move, last, cx)?;
|
||||
}
|
||||
Step::Down(key) => {
|
||||
let at = target(window, key, cx).await?;
|
||||
send(window, Mouse::Move, at, cx)?;
|
||||
|
||||
Reference in new issue
Block a user