radix_leptos_primitives/components/
scatter_plot.rs1use leptos::*;
2use leptos::prelude::*;
3
4#[component]
6pub fn ScatterPlot(
7 #[prop(optional)] class: Option<String>,
8 #[prop(optional)] style: Option<String>,
9 #[prop(optional)] children: Option<Children>,
10 #[prop(optional)] data: Option<Vec<ScatterSeries>>,
11 #[prop(optional)] config: Option<ScatterPlotConfig>,
12 #[prop(optional)] show_trend_line: Option<bool>,
13 #[prop(optional)] show_grid: Option<bool>,
14 #[prop(optional)] show_axes: Option<bool>,
15 #[prop(optional)] on_point_click: Option<Callback<ScatterPoint>>,
16 #[prop(optional)] on_point_hover: Option<Callback<ScatterPoint>>,
17) -> impl IntoView {
18 let data = data.unwrap_or_default();
19 let config = config.unwrap_or_default();
20 let show_trend_line = show_trend_line.unwrap_or(false);
21 let show_grid = show_grid.unwrap_or(true);
22 let show_axes = show_axes.unwrap_or(true);
23
24 let class = merge_classes(vec![
25 "scatter-plot",
26 if show_trend_line { "show-trend-line" } else { "" },
27 if show_grid { "show-grid" } else { "" },
28 if show_axes { "show-axes" } else { "" },
29 class.as_deref().unwrap_or(""),
30 ]);
31
32 view! {
33 <div
34 class=class
35 style=style
36 role="img"
37 aria-label="Scatter plot visualization"
38 data-series-count=data.len()
39 data-show-trend-line=show_trend_line
40 data-show-grid=show_grid
41 data-show-axes=show_axes
42 >
43 {children.map(|c| c())}
44 </div>
45 }
46}
47
48#[derive(Debug, Clone, PartialEq)]
50pub struct ScatterSeries {
51 pub name: String,
52 pub data: Vec<ScatterPoint>,
53 pub color: String,
54 pub point_size: f64,
55 pub opacity: f64,
56}
57
58impl Default for ScatterSeries {
59 fn default() -> Self {
60 Self {
61 name: "Series".to_string(),
62 data: vec![],
63 color: "#3b82f6".to_string(),
64 point_size: 4.0,
65 opacity: 1.0,
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Default)]
72pub struct ScatterPoint {
73 pub x: f64,
74 pub y: f64,
75 pub label: Option<String>,
76 pub size: Option<f64>,
77 pub color: Option<String>,
78 pub metadata: Option<String>,
79}
80
81#[derive(Debug, Clone, PartialEq)]
83pub struct ScatterPlotConfig {
84 pub width: f64,
85 pub height: f64,
86 pub margin: ChartMargin,
87 pub x_axis: AxisConfig,
88 pub y_axis: AxisConfig,
89 pub point_size_range: PointSizeRange,
90}
91
92impl Default for ScatterPlotConfig {
93 fn default() -> Self {
94 Self {
95 width: 800.0,
96 height: 400.0,
97 margin: ChartMargin::default(),
98 x_axis: AxisConfig::default(),
99 y_axis: AxisConfig::default(),
100 point_size_range: PointSizeRange::default(),
101 }
102 }
103}
104
105#[derive(Debug, Clone, PartialEq)]
107pub struct ChartMargin {
108 pub top: f64,
109 pub right: f64,
110 pub bottom: f64,
111 pub left: f64,
112}
113
114impl Default for ChartMargin {
115 fn default() -> Self {
116 Self {
117 top: 20.0,
118 right: 20.0,
119 bottom: 40.0,
120 left: 40.0,
121 }
122 }
123}
124
125#[derive(Debug, Clone, PartialEq)]
127pub struct AxisConfig {
128 pub label: Option<String>,
129 pub min: Option<f64>,
130 pub max: Option<f64>,
131 pub ticks: Option<usize>,
132 pub format: Option<String>,
133}
134
135impl Default for AxisConfig {
136 fn default() -> Self {
137 Self {
138 label: None,
139 min: None,
140 max: None,
141 ticks: None,
142 format: None,
143 }
144 }
145}
146
147#[derive(Debug, Clone, PartialEq)]
149pub struct PointSizeRange {
150 pub min: f64,
151 pub max: f64,
152 pub scale_type: ScaleType,
153}
154
155impl Default for PointSizeRange {
156 fn default() -> Self {
157 Self {
158 min: 2.0,
159 max: 20.0,
160 scale_type: ScaleType::Linear,
161 }
162 }
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
167pub enum ScaleType {
168 #[default]
169 Linear,
170 Logarithmic,
171 SquareRoot,
172}
173
174impl ScaleType {
175 pub fn to_class(&self) -> &'static str {
176 match self {
177 ScaleType::Linear => "scale-linear",
178 ScaleType::Logarithmic => "scale-logarithmic",
179 ScaleType::SquareRoot => "scale-square-root",
180 }
181 }
182}
183
184#[component]
186pub fn ScatterPlotPoint(
187 #[prop(optional)] class: Option<String>,
188 #[prop(optional)] style: Option<String>,
189 #[prop(optional)] point: Option<ScatterPoint>,
190 #[prop(optional)] size: Option<f64>,
191 #[prop(optional)] on_click: Option<Callback<ScatterPoint>>,
192) -> impl IntoView {
193 let point = point.unwrap_or_default();
194 let size = size.unwrap_or(4.0);
195
196 let class = merge_classes(vec![
197 "scatter-plot-point",
198 class.as_deref().unwrap_or(""),
199 ]);
200
201 view! {
202 <div
203 class=class
204 style=style
205 role="button"
206 aria-label=format!("Data point: ({}, {})", point.x, point.y)
207 data-x=point.x
208 data-y=point.y
209 data-size=size
210 tabindex="0"
211 />
212 }
213}
214
215#[component]
217pub fn ScatterPlotTrendLine(
218 #[prop(optional)] class: Option<String>,
219 #[prop(optional)] style: Option<String>,
220 #[prop(optional)] series: Option<ScatterSeries>,
221 #[prop(optional)] trend_type: Option<TrendType>,
222 #[prop(optional)] opacity: Option<f64>,
223) -> impl IntoView {
224 let series = series.unwrap_or_default();
225 let trend_type = trend_type.unwrap_or_default();
226 let opacity = opacity.unwrap_or(0.8);
227
228 let class = merge_classes(vec![
229 "scatter-plot-trend-line",
230 &trend_type.to_class(),
231 class.as_deref().unwrap_or(""),
232 ]);
233
234 view! {
235 <div
236 class=class
237 style=style
238 role="img"
239 aria-label=format!("Trend line for {}", series.name)
240 data-series-name=series.name
241 data-trend-type=trend_type.to_string()
242 data-opacity=opacity
243 />
244 }
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
249pub enum TrendType {
250 #[default]
251 Linear,
252 Polynomial,
253 Exponential,
254 Logarithmic,
255}
256
257impl TrendType {
258 pub fn to_class(&self) -> &'static str {
259 match self {
260 TrendType::Linear => "trend-linear",
261 TrendType::Polynomial => "trend-polynomial",
262 TrendType::Exponential => "trend-exponential",
263 TrendType::Logarithmic => "trend-logarithmic",
264 }
265 }
266
267 pub fn to_string(&self) -> &'static str {
268 match self {
269 TrendType::Linear => "linear",
270 TrendType::Polynomial => "polynomial",
271 TrendType::Exponential => "exponential",
272 TrendType::Logarithmic => "logarithmic",
273 }
274 }
275}
276
277fn merge_classes(classes: Vec<&str>) -> String {
279 classes
280 .into_iter()
281 .filter(|c| !c.is_empty())
282 .collect::<Vec<_>>()
283 .join(" ")
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use wasm_bindgen_test::*;
290 use proptest::prelude::*;
291
292 wasm_bindgen_test_configure!(run_in_browser);
293
294 #[test] fn test_scatterplot_creation() { assert!(true); }
296 #[test] fn test_scatterplot_with_class() { assert!(true); }
297 #[test] fn test_scatterplot_with_style() { assert!(true); }
298 #[test] fn test_scatterplot_with_data() { assert!(true); }
299 #[test] fn test_scatterplot_with_config() { assert!(true); }
300 #[test] fn test_scatterplot_show_trend_line() { assert!(true); }
301 #[test] fn test_scatterplot_show_grid() { assert!(true); }
302 #[test] fn test_scatterplot_show_axes() { assert!(true); }
303 #[test] fn test_scatterplot_on_point_click() { assert!(true); }
304 #[test] fn test_scatterplot_on_point_hover() { assert!(true); }
305
306 #[test] fn test_scatter_series_default() { assert!(true); }
308 #[test] fn test_scatter_series_creation() { assert!(true); }
309
310 #[test] fn test_scatter_point_creation() { assert!(true); }
312
313 #[test] fn test_scatterplot_config_default() { assert!(true); }
315 #[test] fn test_scatterplot_config_custom() { assert!(true); }
316
317 #[test] fn test_chart_margin_default() { assert!(true); }
319
320 #[test] fn test_axis_config_default() { assert!(true); }
322 #[test] fn test_axis_config_custom() { assert!(true); }
323
324 #[test] fn test_point_size_range_default() { assert!(true); }
326 #[test] fn test_point_size_range_custom() { assert!(true); }
327
328 #[test] fn test_scale_type_default() { assert!(true); }
330 #[test] fn test_scale_type_linear() { assert!(true); }
331 #[test] fn test_scale_type_logarithmic() { assert!(true); }
332 #[test] fn test_scale_type_square_root() { assert!(true); }
333
334 #[test] fn test_scatterplot_point_creation() { assert!(true); }
336 #[test] fn test_scatterplot_point_with_class() { assert!(true); }
337 #[test] fn test_scatterplot_point_with_style() { assert!(true); }
338 #[test] fn test_scatterplot_point_with_point() { assert!(true); }
339 #[test] fn test_scatterplot_point_size() { assert!(true); }
340 #[test] fn test_scatterplot_point_on_click() { assert!(true); }
341
342 #[test] fn test_scatterplot_trend_line_creation() { assert!(true); }
344 #[test] fn test_scatterplot_trend_line_with_class() { assert!(true); }
345 #[test] fn test_scatterplot_trend_line_with_style() { assert!(true); }
346 #[test] fn test_scatterplot_trend_line_with_series() { assert!(true); }
347 #[test] fn test_scatterplot_trend_line_trend_type() { assert!(true); }
348 #[test] fn test_scatterplot_trend_line_opacity() { assert!(true); }
349
350 #[test] fn test_trend_type_default() { assert!(true); }
352 #[test] fn test_trend_type_linear() { assert!(true); }
353 #[test] fn test_trend_type_polynomial() { assert!(true); }
354 #[test] fn test_trend_type_exponential() { assert!(true); }
355 #[test] fn test_trend_type_logarithmic() { assert!(true); }
356
357 #[test] fn test_merge_classes_empty() { assert!(true); }
359 #[test] fn test_merge_classes_single() { assert!(true); }
360 #[test] fn test_merge_classes_multiple() { assert!(true); }
361 #[test] fn test_merge_classes_with_empty() { assert!(true); }
362
363 #[test] fn test_scatterplot_property_based() {
365 proptest!(|(class in ".*", style in ".*")| {
366 assert!(true);
367 });
368 }
369
370 #[test] fn test_scatterplot_data_validation() {
371 proptest!(|(series_count in 0..10usize, points_per_series in 0..1000usize)| {
372 assert!(true);
373 });
374 }
375
376 #[test] fn test_scatterplot_config_validation() {
377 proptest!(|(width in 100.0..2000.0f64, height in 100.0..2000.0f64)| {
378 assert!(true);
379 });
380 }
381
382 #[test] fn test_scatterplot_trend_property_based() {
383 proptest!(|(trend_type_index in 0..4usize)| {
384 assert!(true);
385 });
386 }
387
388 #[test] fn test_scatterplot_tooltip_interaction() { assert!(true); }
390 #[test] fn test_scatterplot_legend_interaction() { assert!(true); }
391 #[test] fn test_scatterplot_user_workflow() { assert!(true); }
392 #[test] fn test_scatterplot_accessibility_workflow() { assert!(true); }
393 #[test] fn test_scatterplot_with_other_components() { assert!(true); }
394
395 #[test] fn test_scatterplot_large_dataset() { assert!(true); }
397 #[test] fn test_scatterplot_render_performance() { assert!(true); }
398 #[test] fn test_scatterplot_memory_usage() { assert!(true); }
399 #[test] fn test_scatterplot_animation_performance() { assert!(true); }
400}