1use gpui::{Bounds, Hsla, PathBuilder, canvas, fill, point, px};
10
11use crate::prelude::*;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum ChartKind {
16 #[default]
17 Bar,
18 Line,
19 Area,
20 Pie,
21}
22
23pub fn chart_colors() -> [Hsla; 5] {
25 [
26 palette::primary(500),
27 palette::info(500),
28 palette::success(500),
29 palette::warning(500),
30 palette::danger(500),
31 ]
32}
33
34#[derive(IntoElement, RegisterComponent)]
36pub struct Chart {
37 kind: ChartKind,
38 labels: Vec<SharedString>,
39 values: Vec<f32>,
40 colors: Vec<Hsla>,
41 height: Pixels,
42}
43
44impl Chart {
45 pub fn new(kind: ChartKind, labels: Vec<SharedString>, values: Vec<f32>) -> Self {
46 let colors: Vec<Hsla> = values
47 .iter()
48 .enumerate()
49 .map(|(i, _)| chart_colors()[i % chart_colors().len()])
50 .collect();
51 Self {
52 kind,
53 labels,
54 values,
55 colors,
56 height: px(200.),
57 }
58 }
59
60 pub fn bar(labels: Vec<SharedString>, values: Vec<f32>) -> Self {
61 Self::new(ChartKind::Bar, labels, values)
62 }
63
64 pub fn line(labels: Vec<SharedString>, values: Vec<f32>) -> Self {
65 Self::new(ChartKind::Line, labels, values)
66 }
67
68 pub fn area(labels: Vec<SharedString>, values: Vec<f32>) -> Self {
69 Self::new(ChartKind::Area, labels, values)
70 }
71
72 pub fn pie(labels: Vec<SharedString>, values: Vec<f32>) -> Self {
73 Self::new(ChartKind::Pie, labels, values)
74 }
75
76 pub fn height(mut self, height: Pixels) -> Self {
77 self.height = height;
78 self
79 }
80
81 pub fn colors(mut self, colors: Vec<Hsla>) -> Self {
82 self.colors = colors;
83 self
84 }
85}
86
87fn draw_bar_chart(bounds: Bounds<Pixels>, values: &[f32], colors: &[Hsla], window: &mut Window) {
88 if values.is_empty() {
89 return;
90 }
91 let max = values.iter().copied().fold(0.0f32, f32::max).max(1.0);
92 let padding = px(16.);
93 let chart_width = bounds.size.width - padding * 2.;
94 let chart_height = bounds.size.height - padding * 2.;
95 let bar_gap = px(8.);
96 let bar_width = (chart_width - bar_gap * (values.len().saturating_sub(1) as f32))
97 / values.len().max(1) as f32;
98
99 for (i, value) in values.iter().enumerate() {
100 let bar_h = chart_height * (*value / max);
101 let x = bounds.origin.x + padding + (bar_width + bar_gap) * i as f32;
102 let y = bounds.origin.y + padding + chart_height - bar_h;
103 let color = colors.get(i).copied().unwrap_or(palette::primary(500));
104 window.paint_quad(fill(
105 Bounds::from_corners(point(x, y), point(x + bar_width, y + bar_h)),
106 color,
107 ));
108 }
109}
110
111fn draw_line_or_area(
112 bounds: Bounds<Pixels>,
113 values: &[f32],
114 color: Hsla,
115 fill_area: bool,
116 window: &mut Window,
117) {
118 if values.len() < 2 {
119 return;
120 }
121 let max = values.iter().copied().fold(0.0f32, f32::max).max(1.0);
122 let padding = px(16.);
123 let chart_width = bounds.size.width - padding * 2.;
124 let chart_height = bounds.size.height - padding * 2.;
125 let step = chart_width / (values.len() - 1) as f32;
126
127 let points: Vec<_> = values
128 .iter()
129 .enumerate()
130 .map(|(i, v)| {
131 let x = bounds.origin.x + padding + step * i as f32;
132 let y = bounds.origin.y + padding + chart_height * (1.0 - v / max);
133 point(x, y)
134 })
135 .collect();
136
137 if fill_area {
138 let mut builder = PathBuilder::fill();
139 let baseline_y = bounds.origin.y + padding + chart_height;
140 builder.move_to(point(points[0].x, baseline_y));
141 for p in &points {
142 builder.line_to(*p);
143 }
144 builder.line_to(point(points.last().unwrap().x, baseline_y));
145 builder.close();
146 if let Ok(path) = builder.build() {
147 window.paint_path(path, color.opacity(0.25));
148 }
149 }
150
151 let mut stroke = PathBuilder::stroke(px(2.));
152 stroke.move_to(points[0]);
153 for p in points.iter().skip(1) {
154 stroke.line_to(*p);
155 }
156 if let Ok(path) = stroke.build() {
157 window.paint_path(path, color);
158 }
159}
160
161fn draw_pie_chart(bounds: Bounds<Pixels>, values: &[f32], colors: &[Hsla], window: &mut Window) {
162 let total: f32 = values.iter().sum();
163 if total <= 0.0 {
164 return;
165 }
166 let center = point(
167 bounds.origin.x + bounds.size.width / 2.,
168 bounds.origin.y + bounds.size.height / 2.,
169 );
170 let radius = (bounds.size.width.min(bounds.size.height) / 2.) - px(16.);
171 let mut start_angle = -std::f32::consts::FRAC_PI_2;
172
173 for (i, value) in values.iter().enumerate() {
174 let sweep = std::f32::consts::TAU * (value / total);
175 let end_angle = start_angle + sweep;
176 let color = colors.get(i).copied().unwrap_or(palette::primary(500));
177
178 let mut builder = PathBuilder::fill();
179 builder.move_to(center);
180 let start = point(
181 center.x + radius * start_angle.cos(),
182 center.y + radius * start_angle.sin(),
183 );
184 builder.line_to(start);
185 builder.arc_to(
186 point(radius, radius),
187 px(0.),
188 sweep.abs() > std::f32::consts::PI,
189 sweep > 0.0,
190 point(
191 center.x + radius * end_angle.cos(),
192 center.y + radius * end_angle.sin(),
193 ),
194 );
195 builder.close();
196 if let Ok(path) = builder.build() {
197 window.paint_path(path, color);
198 }
199 start_angle = end_angle;
200 }
201}
202
203impl RenderOnce for Chart {
204 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
205 let kind = self.kind;
206 let values = self.values;
207 let colors = self.colors.clone();
208 let legend_colors = self.colors;
209 let labels = self.labels;
210 let height = self.height;
211 let border = semantic::border(cx);
212 let canvas_colors = colors.clone();
213
214 v_flex()
215 .w_full()
216 .gap_2()
217 .debug_selector(move || format!("CHART-{kind:?}"))
223 .child(
224 canvas(
225 |_, _, _| {},
226 move |bounds, _, window, _cx| match kind {
227 ChartKind::Bar => draw_bar_chart(bounds, &values, &canvas_colors, window),
228 ChartKind::Line => draw_line_or_area(
229 bounds,
230 &values,
231 canvas_colors
232 .first()
233 .copied()
234 .unwrap_or(palette::primary(500)),
235 false,
236 window,
237 ),
238 ChartKind::Area => draw_line_or_area(
239 bounds,
240 &values,
241 canvas_colors
242 .first()
243 .copied()
244 .unwrap_or(palette::primary(500)),
245 true,
246 window,
247 ),
248 ChartKind::Pie => draw_pie_chart(bounds, &values, &canvas_colors, window),
249 },
250 )
251 .w_full()
252 .h(height)
253 .rounded_md()
254 .border_1()
255 .border_color(border),
256 )
257 .when(!labels.is_empty(), |this| {
258 this.child(h_flex().gap_3().flex_wrap().children(
259 labels.into_iter().enumerate().map(|(i, label)| {
260 let color = legend_colors
261 .get(i)
262 .copied()
263 .unwrap_or(palette::neutral(400));
264 h_flex()
265 .gap_1()
266 .items_center()
267 .child(div().size_2().rounded_full().bg(color))
268 .child(
269 Label::new(label)
270 .size(LabelSize::XSmall)
271 .color(Color::Muted),
272 )
273 }),
274 ))
275 })
276 }
277}
278
279impl Component for Chart {
280 fn scope() -> ComponentScope {
281 ComponentScope::DataDisplay
282 }
283
284 fn description() -> Option<&'static str> {
285 Some("Bar/Line/Area/Pie charts via GPUI canvas(); Radar/composed/scatter deferred.")
286 }
287
288 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
289 let labels = vec!["Jan".into(), "Feb".into(), "Mar".into(), "Apr".into()];
290 let values = vec![12.0, 18.0, 9.0, 22.0];
291 Some(
292 v_flex()
293 .gap_6()
294 .child(Chart::bar(labels.clone(), values.clone()))
295 .child(Chart::line(labels.clone(), values.clone()))
296 .child(Chart::area(labels.clone(), values.clone()))
297 .child(Chart::pie(labels, values))
298 .into_any_element(),
299 )
300 }
301}