1use gpui::prelude::*;
12use gpui::{
13 canvas, div, fill, point, px, relative, size, App, Bounds, Hsla, IntoElement, SharedString,
14 Window,
15};
16
17use crate::overlay::tooltip;
18use crate::style::ColorValue;
19use crate::theme::theme;
20
21use super::axis::nice_ticks;
22use super::frame::{legend_row, y_axis_column};
23use super::{min_max, series_color, tick_label};
24use crate::devtools::Probed;
25
26const MARKER: f32 = 6.0;
28
29#[derive(IntoElement)]
32pub struct ScatterChart {
33 series: Vec<(Option<SharedString>, Vec<(f32, f32)>)>,
34 colors: Vec<ColorValue>,
35 hover: bool,
36 width: Option<f32>,
37 height: f32,
38}
39
40impl ScatterChart {
41 pub fn new(points: impl IntoIterator<Item = (f32, f32)>) -> Self {
42 ScatterChart {
43 series: vec![(None, points.into_iter().collect())],
44 colors: Vec::new(),
45 hover: false,
46 width: None,
47 height: 180.0,
48 }
49 }
50
51 pub fn series(
53 label: impl Into<SharedString>,
54 points: impl IntoIterator<Item = (f32, f32)>,
55 ) -> Self {
56 let mut chart = ScatterChart::new(points);
57 chart.series[0].0 = Some(label.into());
58 chart
59 }
60
61 pub fn add_series(
63 mut self,
64 label: impl Into<SharedString>,
65 points: impl IntoIterator<Item = (f32, f32)>,
66 ) -> Self {
67 self.series
68 .push((Some(label.into()), points.into_iter().collect()));
69 self
70 }
71
72 pub fn colors(mut self, colors: impl IntoIterator<Item = impl Into<ColorValue>>) -> Self {
74 self.colors = colors.into_iter().map(Into::into).collect();
75 self
76 }
77
78 pub fn hover(mut self) -> Self {
80 self.hover = true;
81 self
82 }
83
84 pub fn width(mut self, width: f32) -> Self {
86 self.width = Some(width);
87 self
88 }
89
90 pub fn height(mut self, height: f32) -> Self {
92 self.height = height;
93 self
94 }
95}
96
97impl RenderOnce for ScatterChart {
98 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
99 let t = theme(cx);
100 let colors: Vec<Hsla> = (0..self.series.len())
101 .map(|i| series_color(t, &self.colors, i))
102 .collect();
103 let grid = t.border().alpha(0.5);
104 let dimmed = t.dimmed().hsla();
105 let font_xs = t.font_size(crate::theme::Size::Xs);
106
107 let xs: Vec<f32> = self
108 .series
109 .iter()
110 .flat_map(|(_, pts)| pts.iter().map(|p| p.0))
111 .collect();
112 let ys: Vec<f32> = self
113 .series
114 .iter()
115 .flat_map(|(_, pts)| pts.iter().map(|p| p.1))
116 .collect();
117 let (x_lo, x_hi) = min_max(&xs).unwrap_or((0.0, 1.0));
118 let (y_lo, y_hi) = min_max(&ys).unwrap_or((0.0, 1.0));
119 let x_ticks = nice_ticks(x_lo, x_hi, 5);
120 let y_ticks = nice_ticks(y_lo, y_hi, 4);
121 let (x_lo, x_hi) = (*x_ticks.first().unwrap(), *x_ticks.last().unwrap());
122 let (y_lo, y_hi) = (*y_ticks.first().unwrap(), *y_ticks.last().unwrap());
123
124 let fx = move |x: f32| ((x - x_lo) / (x_hi - x_lo)).clamp(0.0, 1.0);
126 let fy = move |y: f32| ((y - y_lo) / (y_hi - y_lo)).clamp(0.0, 1.0);
127
128 let series = self.series.clone();
129 let paint_colors = colors.clone();
130 let x_grid = x_ticks.len().max(2);
131 let y_grid = y_ticks.len().max(2);
132 let plot = canvas(
133 |_, _, _| (),
134 move |bounds, _, window, _cx| {
135 let w = f32::from(bounds.size.width);
136 let h = f32::from(bounds.size.height);
137 if w <= 0.0 || h <= 0.0 {
138 return;
139 }
140 for i in 0..y_grid {
141 let y = (h - 1.0) * (i as f32 / (y_grid - 1) as f32);
142 window.paint_quad(fill(
143 Bounds::new(bounds.origin + point(px(0.0), px(y)), size(px(w), px(1.0))),
144 grid,
145 ));
146 }
147 for i in 0..x_grid {
148 let x = (w - 1.0) * (i as f32 / (x_grid - 1) as f32);
149 window.paint_quad(fill(
150 Bounds::new(bounds.origin + point(px(x), px(0.0)), size(px(1.0), px(h))),
151 grid,
152 ));
153 }
154 for (s, (_, pts)) in series.iter().enumerate() {
155 for &(x, y) in pts {
156 if !x.is_finite() || !y.is_finite() {
157 continue;
158 }
159 let cx0 = w * fx(x) - MARKER / 2.0;
160 let cy0 = h * (1.0 - fy(y)) - MARKER / 2.0;
161 window.paint_quad(
162 fill(
163 Bounds::new(
164 bounds.origin + point(px(cx0), px(cy0)),
165 size(px(MARKER), px(MARKER)),
166 ),
167 paint_colors[s],
168 )
169 .corner_radii(px(MARKER / 2.0)),
170 );
171 }
172 }
173 },
174 )
175 .w_full()
176 .h(px(self.height));
177
178 let mut plot_wrap = div().relative().flex_1().child(plot);
180 if self.hover {
181 let mut n = 0usize;
182 for (label, pts) in &self.series {
183 for &(x, y) in pts {
184 if !x.is_finite() || !y.is_finite() {
185 continue;
186 }
187 let text = match label {
188 Some(name) => format!("{name}: ({}, {})", tick_label(x), tick_label(y)),
189 None => format!("({}, {})", tick_label(x), tick_label(y)),
190 };
191 plot_wrap = plot_wrap.child(
192 div()
193 .id(("guise-scatter-pt", n))
194 .absolute()
195 .left(relative(fx(x)))
196 .top(relative(1.0 - fy(y)))
197 .ml(px(-8.0))
198 .mt(px(-8.0))
199 .w(px(16.0))
200 .h(px(16.0))
201 .tooltip(tooltip(text)),
202 );
203 n += 1;
204 }
205 }
206 }
207
208 let x_labels = div()
209 .flex()
210 .justify_between()
211 .w_full()
212 .children(x_ticks.iter().map(|tick| {
213 div()
214 .text_size(px(font_xs))
215 .text_color(dimmed)
216 .child(SharedString::from(tick_label(*tick)))
217 }));
218
219 let legend: Vec<(SharedString, Hsla)> = self
220 .series
221 .iter()
222 .enumerate()
223 .filter_map(|(i, (label, _))| label.clone().map(|l| (l, colors[i])))
224 .collect();
225
226 let body = div()
227 .flex()
228 .flex_row()
229 .w_full()
230 .child(y_axis_column(t, &y_ticks, self.height))
231 .child(
232 div()
233 .flex_1()
234 .flex()
235 .flex_col()
236 .gap(px(4.0))
237 .child(plot_wrap)
238 .child(x_labels),
239 );
240
241 let mut root = div().flex().flex_col();
242 root = match self.width {
243 Some(w) => root.w(px(w)),
244 None => root.w_full(),
245 };
246 root = root.child(body);
247 if !legend.is_empty() {
248 root = root.child(legend_row(t, &legend));
249 }
250 root.probe("ScatterChart")
251 }
252}