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