1use crate::aes::{mapping::apply_after_stat, mapping::resolve_mappings, Aes, Aesthetic};
2use crate::annotate::Annotation;
3use crate::coord::Coord;
4use crate::data::DataFrame;
5use crate::facet::{Facet, FacetScales, Panel};
6use crate::geom::Geom;
7use crate::plot::{GGError, GGPlot, Labels, Layer};
8use crate::position::PositionParams;
9use crate::scale::ScaleSet;
10use crate::theme::Theme;
11
12pub struct BuiltLayer {
14 pub data: DataFrame,
15 pub geom: Box<dyn Geom>,
16 pub show_legend: Option<bool>,
17}
18
19pub struct BuiltPlot {
21 pub layers: Vec<BuiltLayer>,
22 pub scales: ScaleSet,
23 pub coord: Box<dyn Coord>,
24 pub theme: Theme,
25 pub labels: Labels,
26 pub facet: Facet,
27 pub panels: Vec<Panel>,
28 pub panels_data: Vec<Vec<DataFrame>>,
30 pub annotations: Vec<Annotation>,
31 pub guide_legend: crate::guide::config::GuideLegend,
32 pub suppressed_aes: std::collections::HashSet<Aesthetic>,
34 pub panel_scales: Vec<ScaleSet>,
36 pub warnings: Vec<String>,
40}
41
42impl BuiltPlot {
43 pub fn warnings(&self) -> &[String] {
47 &self.warnings
48 }
49}
50
51const POSITION_COLS: &[&str] = &[
54 "x",
55 "y",
56 "xmin",
57 "xmax",
58 "ymin",
59 "ymax",
60 "xend",
61 "yend",
62 "open",
63 "high",
64 "low",
65 "close",
66 "xintercept",
67 "yintercept",
68 "slope",
69 "intercept",
70];
71
72const X_FAMILY: &[&str] = &["x", "xmin", "xmax", "xend", "xintercept"];
74const Y_FAMILY: &[&str] = &[
75 "y",
76 "ymin",
77 "ymax",
78 "yend",
79 "open",
80 "high",
81 "low",
82 "close",
83 "yintercept",
84];
85
86const INTERCEPT_COLS: [(&str, Aesthetic); 2] =
89 [("xintercept", Aesthetic::X), ("yintercept", Aesthetic::Y)];
90
91fn train_intercepts(scale: &mut Box<dyn crate::scale::Scale>, values: &[crate::data::Value]) {
94 if scale.is_discrete() && values.iter().all(|v| v.as_f64().is_some() || v.is_na()) {
95 return;
96 }
97 scale.train(values);
98}
99
100pub struct PlotBuilder;
102
103impl PlotBuilder {
104 pub fn build(plot: GGPlot) -> Result<BuiltPlot, GGError> {
105 let GGPlot {
106 data: plot_data,
107 mapping: plot_mapping,
108 layers,
109 scales: user_scales,
110 mut coord,
111 theme,
112 labels,
113 facet,
114 annotations,
115 guide_legend,
116 warnings: plot_warnings,
117 default_aspect_ratio,
118 } = plot;
119 let mut theme = theme;
120 if theme.aspect_ratio.is_none() {
121 theme.aspect_ratio = default_aspect_ratio;
122 }
123
124 plot_data.validate()?;
127 for layer in &layers {
128 if let Some(d) = &layer.data {
129 d.validate()?;
130 }
131 }
132
133 let mut scale_set = ScaleSet::new();
134
135 for s in user_scales {
137 scale_set.add(s);
138 }
139
140 let mut built_layers = Vec::new();
141 let mut warnings: Vec<String> = plot_warnings;
142
143 let facet_vars = Self::facet_vars(&facet);
146
147 for layer in layers {
148 let built = Self::build_layer(
149 layer,
150 &plot_data,
151 &plot_mapping,
152 &mut scale_set,
153 theme.primary,
154 &facet_vars,
155 &mut warnings,
156 )?;
157 built_layers.push(built);
158 }
159
160 for bl in &built_layers {
162 scale_set.train_layer(&bl.data);
163 for (col, aes) in &INTERCEPT_COLS {
164 if let (Some(values), None) = (bl.data.column(col), scale_set.get(aes)) {
166 let mut frame = DataFrame::new();
167 frame.add_column(aes.col_name().to_string(), values.to_vec());
168 scale_set.ensure_scale(aes, &frame);
169 }
170 if let (Some(values), Some(scale)) = (bl.data.column(col), scale_set.get_mut(aes)) {
171 train_intercepts(scale, values);
172 }
173 }
174 }
175
176 if built_layers.iter().all(|bl| bl.data.nrows() == 0) {
180 for aes in [Aesthetic::X, Aesthetic::Y] {
181 if scale_set.get(&aes).is_none() {
182 scale_set.add(Box::new(
183 crate::scale::continuous::ScaleContinuous::new().for_aesthetic(aes),
184 ));
185 }
186 }
187 }
188
189 if let Some((min, max)) = coord.zoom_x() {
191 scale_set.set_limits(&Aesthetic::X, min, max);
192 }
193 if let Some((min, max)) = coord.zoom_y() {
194 scale_set.set_limits(&Aesthetic::Y, min, max);
195 }
196
197 coord.train_scales(&mut scale_set);
199
200 let axis_span = |aes: &Aesthetic| {
204 scale_set.get(aes).and_then(|s| {
205 s.domain().map(|(min, max)| crate::coord::AxisSpan {
206 min,
207 max,
208 pmin: s.map(&crate::data::Value::Float(min)),
209 pmax: s.map(&crate::data::Value::Float(max)),
210 })
211 })
212 };
213 let x_span = axis_span(&Aesthetic::X);
214 let y_span = axis_span(&Aesthetic::Y);
215 coord.set_domains(x_span, y_span);
216
217 for spec in &plot_mapping.after_scale {
222 if let Some(src_scale) = scale_set.get(&spec.source) {
223 let modified = crate::scale::modified::ScaleColorModified::new(
224 src_scale.clone_box(),
225 spec.target.clone(),
226 spec.lightness,
227 );
228 let (src_col, tgt_col) = (spec.source.col_name(), spec.target.col_name());
229 for bl in &mut built_layers {
230 if !bl.data.has_column(tgt_col) {
231 if let Some(vals) = bl.data.column(src_col) {
232 let vals = vals.to_vec();
233 bl.data.add_column(tgt_col.to_string(), vals);
234 }
235 }
236 }
237 scale_set.add(Box::new(modified));
238 }
239 }
240
241 let (panels, panels_data) = Self::compute_facets(&facet, &built_layers, &plot_data);
243
244 let suppressed_aes = Self::compute_suppressed_aes(&built_layers);
246
247 let facet_scales_mode = match &facet {
249 Facet::Wrap { scales, .. } => scales.clone(),
250 Facet::Grid { scales, .. } => scales.clone(),
251 Facet::None => FacetScales::Fixed,
252 };
253 let panel_scales = Self::compute_panel_scales(&facet_scales_mode, &panels_data, &scale_set);
254
255 Ok(BuiltPlot {
256 layers: built_layers,
257 scales: scale_set,
258 coord,
259 theme,
260 labels,
261 facet,
262 panels,
263 panels_data,
264 annotations,
265 guide_legend,
266 suppressed_aes,
267 panel_scales,
268 warnings,
269 })
270 }
271
272 fn retain_rows(data: &mut DataFrame, keep: &[bool]) {
274 if keep.iter().all(|&k| k) {
275 return;
276 }
277 let mut result = DataFrame::new();
278 for col_name in data.column_names() {
279 if let Some(src) = data.column(col_name) {
280 let vals: Vec<_> = src
281 .iter()
282 .zip(keep)
283 .filter(|(_, &k)| k)
284 .map(|(v, _)| v.clone())
285 .collect();
286 result.add_column(col_name.to_string(), vals);
287 }
288 }
289 *data = result;
290 }
291
292 fn drop_non_finite(data: &mut DataFrame, allow_infinite: bool) -> usize {
296 let n = data.nrows();
297 let mut keep = vec![true; n];
298 for col in POSITION_COLS {
299 if let Some(values) = data.column(col) {
300 for (i, v) in values.iter().enumerate() {
301 if let crate::data::Value::Float(f) = v {
302 let bad = f.is_nan() || (f.is_infinite() && !allow_infinite);
303 if bad {
304 keep[i] = false;
305 }
306 }
307 }
308 }
309 }
310 let removed = keep.iter().filter(|&&k| !k).count();
311 if removed > 0 {
312 Self::retain_rows(data, &keep);
313 }
314 removed
315 }
316
317 fn facet_vars(facet: &Facet) -> Vec<String> {
319 match facet {
320 Facet::None => vec![],
321 Facet::Wrap { var, .. } => vec![var.clone()],
322 Facet::Grid {
323 row_var, col_var, ..
324 } => row_var.iter().chain(col_var.iter()).cloned().collect(),
325 }
326 }
327
328 fn compute_facets(
329 facet: &Facet,
330 built_layers: &[BuiltLayer],
331 _plot_data: &DataFrame,
332 ) -> (Vec<Panel>, Vec<Vec<DataFrame>>) {
333 match facet {
334 Facet::None => (vec![], vec![]),
335 Facet::Wrap {
336 var,
337 ncol,
338 labeller,
339 ..
340 } => {
341 let mut levels: Vec<String> = Vec::new();
343 for bl in built_layers {
344 if let Some(col) = bl.data.column(var) {
345 for v in col {
346 let key = v.to_group_key();
347 if !levels.contains(&key) {
348 levels.push(key);
349 }
350 }
351 }
352 }
353
354 let panels: Vec<Panel> = levels
356 .iter()
357 .enumerate()
358 .map(|(i, value)| {
359 let ncols =
360 ncol.unwrap_or_else(|| (levels.len() as f64).sqrt().ceil() as usize);
361 let formatted = labeller.format(var, value);
362 Panel {
363 row: i / ncols.max(1),
364 col: i % ncols.max(1),
365 label: formatted.clone(),
366 row_label: None,
367 col_label: Some(formatted),
368 rect: crate::render::Rect {
369 x: 0.0,
370 y: 0.0,
371 width: 0.0,
372 height: 0.0,
373 },
374 }
375 })
376 .collect();
377
378 let panels_data: Vec<Vec<DataFrame>> = levels
380 .iter()
381 .map(|level| {
382 built_layers
383 .iter()
384 .map(|bl| Self::filter_data_by_var(&bl.data, var, level))
385 .collect()
386 })
387 .collect();
388
389 (panels, panels_data)
390 }
391 Facet::Grid {
392 row_var,
393 col_var,
394 labeller,
395 ..
396 } => {
397 let mut row_levels: Vec<String> = Vec::new();
398 let mut col_levels: Vec<String> = Vec::new();
399
400 for bl in built_layers {
401 if let Some(rv) = row_var {
402 if let Some(col) = bl.data.column(rv) {
403 for v in col {
404 let key = v.to_group_key();
405 if !row_levels.contains(&key) {
406 row_levels.push(key);
407 }
408 }
409 }
410 }
411 if let Some(cv) = col_var {
412 if let Some(col) = bl.data.column(cv) {
413 for v in col {
414 let key = v.to_group_key();
415 if !col_levels.contains(&key) {
416 col_levels.push(key);
417 }
418 }
419 }
420 }
421 }
422
423 if row_levels.is_empty() {
424 row_levels.push("".to_string());
425 }
426 if col_levels.is_empty() {
427 col_levels.push("".to_string());
428 }
429
430 let mut panels = Vec::new();
431 let mut panels_data = Vec::new();
432
433 for (ri, rl) in row_levels.iter().enumerate() {
434 for (ci, cl) in col_levels.iter().enumerate() {
435 let row_fmt = if rl.is_empty() {
436 None
437 } else {
438 let rv = row_var.as_deref().unwrap_or("");
439 Some(labeller.format(rv, rl))
440 };
441 let col_fmt = if cl.is_empty() {
442 None
443 } else {
444 let cv = col_var.as_deref().unwrap_or("");
445 Some(labeller.format(cv, cl))
446 };
447 let label = match (&row_fmt, &col_fmt) {
448 (Some(r), Some(c)) => format!("{r} | {c}"),
449 (Some(r), None) => r.clone(),
450 (None, Some(c)) => c.clone(),
451 (None, None) => String::new(),
452 };
453 panels.push(Panel {
454 row: ri,
455 col: ci,
456 label,
457 row_label: row_fmt,
458 col_label: col_fmt,
459 rect: crate::render::Rect {
460 x: 0.0,
461 y: 0.0,
462 width: 0.0,
463 height: 0.0,
464 },
465 });
466
467 let layer_data: Vec<DataFrame> = built_layers
468 .iter()
469 .map(|bl| {
470 let mut data = bl.data.clone();
471 if let Some(rv) = row_var {
472 if !rl.is_empty() {
473 data = Self::filter_data_by_var(&data, rv, rl);
474 }
475 }
476 if let Some(cv) = col_var {
477 if !cl.is_empty() {
478 data = Self::filter_data_by_var(&data, cv, cl);
479 }
480 }
481 data
482 })
483 .collect();
484 panels_data.push(layer_data);
485 }
486 }
487
488 (panels, panels_data)
489 }
490 }
491 }
492
493 fn filter_data_by_var(data: &DataFrame, var: &str, level: &str) -> DataFrame {
494 if let Some(col) = data.column(var) {
495 let indices: Vec<usize> = col
496 .iter()
497 .enumerate()
498 .filter(|(_, v)| v.to_group_key() == level)
499 .map(|(i, _)| i)
500 .collect();
501
502 let mut result = DataFrame::new();
503 for col_name in data.column_names() {
504 if let Some(src) = data.column(col_name) {
505 let vals: Vec<_> = indices.iter().map(|&i| src[i].clone()).collect();
506 result.add_column(col_name.to_string(), vals);
507 }
508 }
509 result
510 } else {
511 data.clone()
512 }
513 }
514
515 fn build_layer(
516 layer: Layer,
517 plot_data: &DataFrame,
518 plot_mapping: &Aes,
519 scale_set: &mut ScaleSet,
520 primary: Option<(u8, u8, u8)>,
521 facet_vars: &[String],
522 warnings: &mut Vec<String>,
523 ) -> Result<BuiltLayer, GGError> {
524 let Layer {
525 data: layer_data,
526 mapping: layer_mapping,
527 mut geom,
528 stat,
529 position,
530 params: _,
531 show_legend,
532 explicit_style,
533 } = layer;
534
535 let source_data = layer_data.as_ref().unwrap_or(plot_data);
539
540 let merged_mapping = if geom.inherit_aes() {
544 plot_mapping.merge(&layer_mapping)
545 } else {
546 layer_mapping.clone()
547 };
548
549 if let Some(color) = primary.filter(|_| !explicit_style) {
553 let has_color = merged_mapping.get_mapping(&Aesthetic::Color).is_some();
554 let has_fill = merged_mapping.get_mapping(&Aesthetic::Fill).is_some();
555 if !has_color && !has_fill {
556 geom.set_series_color(color);
557 }
558 }
559
560 let mut working_data = resolve_mappings(source_data, &merged_mapping);
562
563 let pre_stat_columns: Vec<String> = working_data
568 .column_names()
569 .iter()
570 .map(|s| s.to_string())
571 .collect();
572
573 for m in &merged_mapping.mappings {
575 scale_set.ensure_scale(&m.aesthetic, &working_data);
576 }
577
578 for scale in scale_set.iter() {
581 let aes = scale.aesthetic();
582 let cols: Vec<&str> = match aes {
583 Aesthetic::X => X_FAMILY.to_vec(),
584 Aesthetic::Y => Y_FAMILY.to_vec(),
585 _ => vec![aes.col_name()],
586 };
587 for col_name in cols {
588 let Some(col) = working_data.column(col_name) else {
589 continue;
590 };
591 let transformed: Vec<_> = col
595 .iter()
596 .map(|v| match (scale.transform(v), v.as_f64()) {
597 (crate::data::Value::Na, Some(_)) => crate::data::Value::Float(f64::NAN),
598 (t, _) => t,
599 })
600 .collect();
601 let any_changed = transformed.iter().zip(col.iter()).any(|(t, o)| {
602 match (t.as_f64(), o.as_f64()) {
603 (Some(a), Some(b)) => a.is_nan() || (a - b).abs() > f64::EPSILON,
604 _ => false,
605 }
606 });
607 if any_changed {
608 if let Some(col_mut) = working_data.column_mut(col_name) {
609 *col_mut = transformed;
610 }
611 }
612 }
613 }
614
615 Self::filter_oob_data(&mut working_data, scale_set);
617
618 let geom_label = format!("geom_{}", geom.name());
622 let removed = Self::drop_non_finite(&mut working_data, geom.allows_infinite());
623 if removed > 0 {
624 warnings.push(format!(
625 "{geom_label}: removed {removed} row{} containing non-finite values",
626 if removed == 1 { "" } else { "s" }
627 ));
628 }
629 let input_rows = working_data.nrows();
630
631 let mut group_cols = if stat.panelwise() {
638 Vec::new()
639 } else {
640 Self::detect_group_columns(&working_data)
641 };
642 for fv in facet_vars {
643 if working_data.has_column(fv) && !group_cols.contains(fv) {
644 group_cols.push(fv.clone());
645 }
646 }
647
648 working_data = if !group_cols.is_empty() {
649 let groups =
650 working_data.group_by(&group_cols.iter().map(|s| s.as_str()).collect::<Vec<_>>());
651 let mut result = DataFrame::new();
652 for group in groups {
653 let mut computed = stat.compute_group(&group, scale_set);
654 let n = computed.nrows();
655 if n > 0 {
656 for fv in facet_vars {
657 if !computed.has_column(fv) {
658 if let Some(val) = group.column(fv).and_then(|c| c.first()).cloned() {
659 computed.add_column(fv.clone(), vec![val; n]);
660 }
661 }
662 }
663 }
664 result.vstack(&computed);
665 }
666 result
667 } else {
668 stat.compute_group(&working_data, scale_set)
669 };
670
671 apply_after_stat(&mut working_data, &merged_mapping);
673
674 let removed = Self::drop_non_finite(&mut working_data, geom.allows_infinite());
677 if removed > 0 {
678 warnings.push(format!(
679 "{geom_label}: removed {removed} row{} containing missing values (stat_{} output)",
680 if removed == 1 { "" } else { "s" },
681 stat.name()
682 ));
683 }
684
685 if working_data.nrows() == 0 {
690 if input_rows > 0 {
691 for aes in &stat.required_aes() {
692 let col_name = aes.col_name();
693 if !pre_stat_columns.iter().any(|c| c == col_name) {
694 return Err(GGError::ValidationError(format!(
695 "stat_{} requires aesthetic '{}' but it was not provided",
696 stat.name(),
697 col_name
698 )));
699 }
700 }
701 warnings.push(format!(
702 "{geom_label}: stat_{} produced no data from {input_rows} row{} \
703 (too few or degenerate values); layer skipped",
704 stat.name(),
705 if input_rows == 1 { "" } else { "s" }
706 ));
707 }
708 return Ok(BuiltLayer {
709 data: DataFrame::new(),
710 geom,
711 show_legend,
712 });
713 }
714
715 for aes in &geom.required_aes() {
721 let col_name = aes.col_name();
722 let supplied = pre_stat_columns.iter().any(|c| c == col_name);
723 if !supplied && !working_data.has_column(col_name) {
724 return Err(GGError::ValidationError(format!(
725 "geom_{} requires aesthetic '{}' but it was not provided",
726 geom.name(),
727 col_name
728 )));
729 }
730 }
731
732 let stat_aes = [
734 ("x", Aesthetic::X),
735 ("y", Aesthetic::Y),
736 ("xmin", Aesthetic::X),
737 ("xmax", Aesthetic::X),
738 ("ymin", Aesthetic::Y),
739 ("ymax", Aesthetic::Y),
740 ];
741 for (col, aes) in &stat_aes {
742 if working_data.has_column(col) {
743 scale_set.ensure_scale(aes, &working_data);
744 }
745 }
746
747 let y_is_user_mapped = merged_mapping.get_mapping(&Aesthetic::Y).is_some();
751 if (!y_is_user_mapped || geom.include_zero_baseline()) && working_data.has_column("y") {
752 if let Some(y_scale) = scale_set.get_mut(&Aesthetic::Y) {
753 y_scale.train(&[crate::data::Value::Float(0.0)]);
754 }
755 }
756
757 let params = PositionParams::default();
759 position.compute(&mut working_data, ¶ms);
760
761 geom.setup_data(&mut working_data);
764 for (col, aes) in &stat_aes {
765 if working_data.has_column(col) {
766 scale_set.ensure_scale(aes, &working_data);
767 }
768 }
769
770 scale_set.train_layer(&working_data);
772 for (col, aes) in &INTERCEPT_COLS {
773 if let (Some(values), Some(scale)) = (working_data.column(col), scale_set.get_mut(aes))
774 {
775 train_intercepts(scale, values);
776 }
777 }
778
779 for (col, aes) in &stat_aes {
783 if let Some(values) = working_data.column(col) {
784 if let Some(scale) = scale_set.get_mut(aes) {
785 scale.train(values);
786 }
787 }
788 }
789
790 Ok(BuiltLayer {
791 data: working_data,
792 geom,
793 show_legend,
794 })
795 }
796
797 fn filter_oob_data(data: &mut DataFrame, scale_set: &ScaleSet) {
799 let x_limits = scale_set.get(&Aesthetic::X).and_then(|s| s.filter_limits());
800 let y_limits = scale_set.get(&Aesthetic::Y).and_then(|s| s.filter_limits());
801
802 if x_limits.is_none() && y_limits.is_none() {
803 return;
804 }
805
806 let nrows = data.nrows();
807 let mut keep = vec![true; nrows];
808
809 if let Some((min, max)) = x_limits {
810 if let Some(col) = data.column("x") {
811 for (i, v) in col.iter().enumerate() {
812 if let Some(f) = v.as_f64() {
813 if f < min || f > max {
814 keep[i] = false;
815 }
816 }
817 }
818 }
819 }
820
821 if let Some((min, max)) = y_limits {
822 if let Some(col) = data.column("y") {
823 for (i, v) in col.iter().enumerate() {
824 if let Some(f) = v.as_f64() {
825 if f < min || f > max {
826 keep[i] = false;
827 }
828 }
829 }
830 }
831 }
832
833 if keep.iter().all(|&k| k) {
835 return;
836 }
837
838 let indices: Vec<usize> = keep
839 .iter()
840 .enumerate()
841 .filter(|(_, &k)| k)
842 .map(|(i, _)| i)
843 .collect();
844
845 let mut result = DataFrame::new();
846 for col_name in data.column_names() {
847 if let Some(src) = data.column(col_name) {
848 let vals: Vec<_> = indices.iter().map(|&i| src[i].clone()).collect();
849 result.add_column(col_name.to_string(), vals);
850 }
851 }
852 *data = result;
853 }
854
855 fn compute_panel_scales(
858 facet_scales: &FacetScales,
859 panels_data: &[Vec<DataFrame>],
860 base_scales: &ScaleSet,
861 ) -> Vec<ScaleSet> {
862 if matches!(facet_scales, FacetScales::Fixed) || panels_data.is_empty() {
863 return vec![];
864 }
865
866 let free_x = matches!(facet_scales, FacetScales::FreeX | FacetScales::Free);
867 let free_y = matches!(facet_scales, FacetScales::FreeY | FacetScales::Free);
868
869 panels_data
870 .iter()
871 .map(|panel_layers| {
872 let mut panel_set = base_scales.clone();
873
874 if free_x {
876 if let Some(s) = panel_set.get_mut(&Aesthetic::X) {
877 s.reset_training();
878 }
879 }
880 if free_y {
881 if let Some(s) = panel_set.get_mut(&Aesthetic::Y) {
882 s.reset_training();
883 }
884 }
885
886 for layer_data in panel_layers {
889 panel_set.train_layer(layer_data);
890 for (cols, aes) in [
891 (["xmin", "xmax", "xintercept"], Aesthetic::X),
892 (["ymin", "ymax", "yintercept"], Aesthetic::Y),
893 ] {
894 let freed = match aes {
895 Aesthetic::X => free_x,
896 _ => free_y,
897 };
898 if !freed {
899 continue;
900 }
901 for c in cols {
902 if let (Some(vals), Some(s)) =
903 (layer_data.column(c), panel_set.get_mut(&aes))
904 {
905 if c.ends_with("intercept") {
906 train_intercepts(s, vals);
907 } else {
908 s.train(vals);
909 }
910 }
911 }
912 }
913 }
914
915 panel_set
916 })
917 .collect()
918 }
919
920 fn compute_suppressed_aes(built_layers: &[BuiltLayer]) -> std::collections::HashSet<Aesthetic> {
924 use std::collections::HashSet;
925 let legend_aes = [
926 Aesthetic::Color,
927 Aesthetic::Fill,
928 Aesthetic::Shape,
929 Aesthetic::Linetype,
930 Aesthetic::Size,
931 Aesthetic::Alpha,
932 ];
933 let mut suppressed = HashSet::new();
934 for aes in &legend_aes {
935 let col_name = aes.col_name();
936 let mut any_has = false;
937 let mut all_hidden = true;
938 for bl in built_layers {
939 if bl.data.has_column(col_name) {
940 any_has = true;
941 match bl.show_legend {
942 Some(false) => {} _ => {
944 all_hidden = false;
945 break;
946 }
947 }
948 }
949 }
950 if any_has && all_hidden {
951 suppressed.insert(aes.clone());
952 }
953 }
954 suppressed
955 }
956
957 fn detect_group_columns(data: &DataFrame) -> Vec<String> {
960 let candidates = ["group", "color", "fill", "x"];
961 let mut group_cols = Vec::new();
962 for &col in &candidates {
963 if data.has_column(col) {
964 if let Some(values) = data.column(col) {
965 let is_discrete = values
966 .iter()
967 .any(|v| matches!(v, crate::data::Value::Str(_)));
968 if is_discrete {
969 group_cols.push(col.to_string());
970 }
971 }
972 }
973 }
974 group_cols
975 }
976}