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 locale,
119 } = plot;
120 let mut theme = theme;
121 if theme.aspect_ratio.is_none() {
122 theme.aspect_ratio = default_aspect_ratio;
123 }
124
125 plot_data.validate()?;
128 for layer in &layers {
129 if let Some(d) = &layer.data {
130 d.validate()?;
131 }
132 }
133
134 let mut scale_set = ScaleSet::new();
135
136 for s in user_scales {
138 scale_set.add(s);
139 }
140
141 let mut built_layers = Vec::new();
142 let mut warnings: Vec<String> = plot_warnings;
143
144 let facet_vars = Self::facet_vars(&facet);
147
148 for layer in layers {
149 let built = Self::build_layer(
150 layer,
151 &plot_data,
152 &plot_mapping,
153 &mut scale_set,
154 theme.primary,
155 &facet_vars,
156 &mut warnings,
157 )?;
158 built_layers.push(built);
159 }
160
161 for bl in &built_layers {
163 scale_set.train_layer(&bl.data);
164 for (col, aes) in &INTERCEPT_COLS {
165 if let (Some(values), None) = (bl.data.column(col), scale_set.get(aes)) {
167 let mut frame = DataFrame::new();
168 frame.add_column(aes.col_name().to_string(), values.to_vec());
169 scale_set.ensure_scale(aes, &frame);
170 }
171 if let (Some(values), Some(scale)) = (bl.data.column(col), scale_set.get_mut(aes)) {
172 train_intercepts(scale, values);
173 }
174 }
175 }
176
177 if built_layers.iter().all(|bl| bl.data.nrows() == 0) {
181 for aes in [Aesthetic::X, Aesthetic::Y] {
182 if scale_set.get(&aes).is_none() {
183 scale_set.add(Box::new(
184 crate::scale::continuous::ScaleContinuous::new().for_aesthetic(aes),
185 ));
186 }
187 }
188 }
189
190 if let Some((min, max)) = coord.zoom_x() {
192 scale_set.set_limits(&Aesthetic::X, min, max);
193 }
194 if let Some((min, max)) = coord.zoom_y() {
195 scale_set.set_limits(&Aesthetic::Y, min, max);
196 }
197
198 coord.train_scales(&mut scale_set);
200
201 let axis_span = |aes: &Aesthetic| {
205 scale_set.get(aes).and_then(|s| {
206 s.domain().map(|(min, max)| crate::coord::AxisSpan {
207 min,
208 max,
209 pmin: s.map(&crate::data::Value::Float(min)),
210 pmax: s.map(&crate::data::Value::Float(max)),
211 })
212 })
213 };
214 let x_span = axis_span(&Aesthetic::X);
215 let y_span = axis_span(&Aesthetic::Y);
216 coord.set_domains(x_span, y_span);
217
218 for spec in &plot_mapping.after_scale {
223 if let Some(src_scale) = scale_set.get(&spec.source) {
224 let modified = crate::scale::modified::ScaleColorModified::new(
225 src_scale.clone_box(),
226 spec.target.clone(),
227 spec.lightness,
228 );
229 let (src_col, tgt_col) = (spec.source.col_name(), spec.target.col_name());
230 for bl in &mut built_layers {
231 if !bl.data.has_column(tgt_col) {
232 if let Some(vals) = bl.data.column(src_col) {
233 let vals = vals.to_vec();
234 bl.data.add_column(tgt_col.to_string(), vals);
235 }
236 }
237 }
238 scale_set.add(Box::new(modified));
239 }
240 }
241
242 if let Some(locale) = &locale {
245 scale_set.set_default_locale(locale);
246 }
247
248 let (panels, panels_data) = Self::compute_facets(&facet, &built_layers, &plot_data);
250
251 let suppressed_aes = Self::compute_suppressed_aes(&built_layers);
253
254 let facet_scales_mode = match &facet {
256 Facet::Wrap { scales, .. } => scales.clone(),
257 Facet::Grid { scales, .. } => scales.clone(),
258 Facet::None => FacetScales::Fixed,
259 };
260 let panel_scales = Self::compute_panel_scales(&facet_scales_mode, &panels_data, &scale_set);
261
262 Ok(BuiltPlot {
263 layers: built_layers,
264 scales: scale_set,
265 coord,
266 theme,
267 labels,
268 facet,
269 panels,
270 panels_data,
271 annotations,
272 guide_legend,
273 suppressed_aes,
274 panel_scales,
275 warnings,
276 })
277 }
278
279 fn retain_rows(data: &mut DataFrame, keep: &[bool]) {
281 if keep.iter().all(|&k| k) {
282 return;
283 }
284 let mut result = DataFrame::new();
285 for col_name in data.column_names() {
286 if let Some(src) = data.column(col_name) {
287 let vals: Vec<_> = src
288 .iter()
289 .zip(keep)
290 .filter(|(_, &k)| k)
291 .map(|(v, _)| v.clone())
292 .collect();
293 result.add_column(col_name.to_string(), vals);
294 }
295 }
296 *data = result;
297 }
298
299 fn drop_non_finite(data: &mut DataFrame, allow_infinite: bool) -> usize {
303 let n = data.nrows();
304 let mut keep = vec![true; n];
305 for col in POSITION_COLS {
306 if let Some(values) = data.column(col) {
307 for (i, v) in values.iter().enumerate() {
308 if let crate::data::Value::Float(f) = v {
309 let bad = f.is_nan() || (f.is_infinite() && !allow_infinite);
310 if bad {
311 keep[i] = false;
312 }
313 }
314 }
315 }
316 }
317 let removed = keep.iter().filter(|&&k| !k).count();
318 if removed > 0 {
319 Self::retain_rows(data, &keep);
320 }
321 removed
322 }
323
324 fn facet_vars(facet: &Facet) -> Vec<String> {
326 match facet {
327 Facet::None => vec![],
328 Facet::Wrap { var, .. } => vec![var.clone()],
329 Facet::Grid {
330 row_var, col_var, ..
331 } => row_var.iter().chain(col_var.iter()).cloned().collect(),
332 }
333 }
334
335 fn compute_facets(
336 facet: &Facet,
337 built_layers: &[BuiltLayer],
338 _plot_data: &DataFrame,
339 ) -> (Vec<Panel>, Vec<Vec<DataFrame>>) {
340 match facet {
341 Facet::None => (vec![], vec![]),
342 Facet::Wrap {
343 var,
344 ncol,
345 labeller,
346 ..
347 } => {
348 let mut levels: Vec<String> = Vec::new();
350 for bl in built_layers {
351 if let Some(col) = bl.data.column(var) {
352 for v in col {
353 let key = v.to_group_key();
354 if !levels.contains(&key) {
355 levels.push(key);
356 }
357 }
358 }
359 }
360
361 let panels: Vec<Panel> = levels
363 .iter()
364 .enumerate()
365 .map(|(i, value)| {
366 let ncols =
367 ncol.unwrap_or_else(|| (levels.len() as f64).sqrt().ceil() as usize);
368 let formatted = labeller.format(var, value);
369 Panel {
370 row: i / ncols.max(1),
371 col: i % ncols.max(1),
372 label: formatted.clone(),
373 row_label: None,
374 col_label: Some(formatted),
375 rect: crate::render::Rect {
376 x: 0.0,
377 y: 0.0,
378 width: 0.0,
379 height: 0.0,
380 },
381 }
382 })
383 .collect();
384
385 let panels_data: Vec<Vec<DataFrame>> = levels
387 .iter()
388 .map(|level| {
389 built_layers
390 .iter()
391 .map(|bl| Self::filter_data_by_var(&bl.data, var, level))
392 .collect()
393 })
394 .collect();
395
396 (panels, panels_data)
397 }
398 Facet::Grid {
399 row_var,
400 col_var,
401 labeller,
402 ..
403 } => {
404 let mut row_levels: Vec<String> = Vec::new();
405 let mut col_levels: Vec<String> = Vec::new();
406
407 for bl in built_layers {
408 if let Some(rv) = row_var {
409 if let Some(col) = bl.data.column(rv) {
410 for v in col {
411 let key = v.to_group_key();
412 if !row_levels.contains(&key) {
413 row_levels.push(key);
414 }
415 }
416 }
417 }
418 if let Some(cv) = col_var {
419 if let Some(col) = bl.data.column(cv) {
420 for v in col {
421 let key = v.to_group_key();
422 if !col_levels.contains(&key) {
423 col_levels.push(key);
424 }
425 }
426 }
427 }
428 }
429
430 if row_levels.is_empty() {
431 row_levels.push("".to_string());
432 }
433 if col_levels.is_empty() {
434 col_levels.push("".to_string());
435 }
436
437 let mut panels = Vec::new();
438 let mut panels_data = Vec::new();
439
440 for (ri, rl) in row_levels.iter().enumerate() {
441 for (ci, cl) in col_levels.iter().enumerate() {
442 let row_fmt = if rl.is_empty() {
443 None
444 } else {
445 let rv = row_var.as_deref().unwrap_or("");
446 Some(labeller.format(rv, rl))
447 };
448 let col_fmt = if cl.is_empty() {
449 None
450 } else {
451 let cv = col_var.as_deref().unwrap_or("");
452 Some(labeller.format(cv, cl))
453 };
454 let label = match (&row_fmt, &col_fmt) {
455 (Some(r), Some(c)) => format!("{r} | {c}"),
456 (Some(r), None) => r.clone(),
457 (None, Some(c)) => c.clone(),
458 (None, None) => String::new(),
459 };
460 panels.push(Panel {
461 row: ri,
462 col: ci,
463 label,
464 row_label: row_fmt,
465 col_label: col_fmt,
466 rect: crate::render::Rect {
467 x: 0.0,
468 y: 0.0,
469 width: 0.0,
470 height: 0.0,
471 },
472 });
473
474 let layer_data: Vec<DataFrame> = built_layers
475 .iter()
476 .map(|bl| {
477 let mut data = bl.data.clone();
478 if let Some(rv) = row_var {
479 if !rl.is_empty() {
480 data = Self::filter_data_by_var(&data, rv, rl);
481 }
482 }
483 if let Some(cv) = col_var {
484 if !cl.is_empty() {
485 data = Self::filter_data_by_var(&data, cv, cl);
486 }
487 }
488 data
489 })
490 .collect();
491 panels_data.push(layer_data);
492 }
493 }
494
495 (panels, panels_data)
496 }
497 }
498 }
499
500 fn filter_data_by_var(data: &DataFrame, var: &str, level: &str) -> DataFrame {
501 if let Some(col) = data.column(var) {
502 let indices: Vec<usize> = col
503 .iter()
504 .enumerate()
505 .filter(|(_, v)| v.to_group_key() == level)
506 .map(|(i, _)| i)
507 .collect();
508
509 let mut result = DataFrame::new();
510 for col_name in data.column_names() {
511 if let Some(src) = data.column(col_name) {
512 let vals: Vec<_> = indices.iter().map(|&i| src[i].clone()).collect();
513 result.add_column(col_name.to_string(), vals);
514 }
515 }
516 result
517 } else {
518 data.clone()
519 }
520 }
521
522 fn build_layer(
523 layer: Layer,
524 plot_data: &DataFrame,
525 plot_mapping: &Aes,
526 scale_set: &mut ScaleSet,
527 primary: Option<(u8, u8, u8)>,
528 facet_vars: &[String],
529 warnings: &mut Vec<String>,
530 ) -> Result<BuiltLayer, GGError> {
531 let Layer {
532 data: layer_data,
533 mapping: layer_mapping,
534 mut geom,
535 stat,
536 position,
537 params: _,
538 show_legend,
539 explicit_style,
540 } = layer;
541
542 let source_data = layer_data.as_ref().unwrap_or(plot_data);
546
547 let merged_mapping = if geom.inherit_aes() {
551 plot_mapping.merge(&layer_mapping)
552 } else {
553 layer_mapping.clone()
554 };
555
556 if let Some(color) = primary.filter(|_| !explicit_style) {
560 let has_color = merged_mapping.get_mapping(&Aesthetic::Color).is_some();
561 let has_fill = merged_mapping.get_mapping(&Aesthetic::Fill).is_some();
562 if !has_color && !has_fill {
563 geom.set_series_color(color);
564 }
565 }
566
567 let mut working_data = resolve_mappings(source_data, &merged_mapping);
569
570 let pre_stat_columns: Vec<String> = working_data
575 .column_names()
576 .iter()
577 .map(|s| s.to_string())
578 .collect();
579
580 for m in &merged_mapping.mappings {
582 scale_set.ensure_scale(&m.aesthetic, &working_data);
583 }
584
585 for scale in scale_set.iter() {
588 let aes = scale.aesthetic();
589 let cols: Vec<&str> = match aes {
590 Aesthetic::X => X_FAMILY.to_vec(),
591 Aesthetic::Y => Y_FAMILY.to_vec(),
592 _ => vec![aes.col_name()],
593 };
594 for col_name in cols {
595 let Some(col) = working_data.column(col_name) else {
596 continue;
597 };
598 let transformed: Vec<_> = col
602 .iter()
603 .map(|v| match (scale.transform(v), v.as_f64()) {
604 (crate::data::Value::Na, Some(_)) => crate::data::Value::Float(f64::NAN),
605 (t, _) => t,
606 })
607 .collect();
608 let any_changed = transformed.iter().zip(col.iter()).any(|(t, o)| {
609 match (t.as_f64(), o.as_f64()) {
610 (Some(a), Some(b)) => a.is_nan() || (a - b).abs() > f64::EPSILON,
611 _ => false,
612 }
613 });
614 if any_changed {
615 if let Some(col_mut) = working_data.column_mut(col_name) {
616 *col_mut = transformed;
617 }
618 }
619 }
620 }
621
622 Self::filter_oob_data(&mut working_data, scale_set);
624
625 let geom_label = format!("geom_{}", geom.name());
629 let removed = Self::drop_non_finite(&mut working_data, geom.allows_infinite());
630 if removed > 0 {
631 warnings.push(format!(
632 "{geom_label}: removed {removed} row{} containing non-finite values",
633 if removed == 1 { "" } else { "s" }
634 ));
635 }
636 let input_rows = working_data.nrows();
637
638 let mut group_cols = if stat.panelwise() {
645 Vec::new()
646 } else {
647 Self::detect_group_columns(&working_data)
648 };
649 for fv in facet_vars {
650 if working_data.has_column(fv) && !group_cols.contains(fv) {
651 group_cols.push(fv.clone());
652 }
653 }
654
655 working_data = if !group_cols.is_empty() {
656 let groups =
657 working_data.group_by(&group_cols.iter().map(|s| s.as_str()).collect::<Vec<_>>());
658 let mut result = DataFrame::new();
659 for group in groups {
660 let mut computed = stat.compute_group(&group, scale_set);
661 let n = computed.nrows();
662 if n > 0 {
663 for fv in facet_vars {
664 if !computed.has_column(fv) {
665 if let Some(val) = group.column(fv).and_then(|c| c.first()).cloned() {
666 computed.add_column(fv.clone(), vec![val; n]);
667 }
668 }
669 }
670 }
671 result.vstack(&computed);
672 }
673 result
674 } else {
675 stat.compute_group(&working_data, scale_set)
676 };
677
678 apply_after_stat(&mut working_data, &merged_mapping);
680
681 let removed = Self::drop_non_finite(&mut working_data, geom.allows_infinite());
684 if removed > 0 {
685 warnings.push(format!(
686 "{geom_label}: removed {removed} row{} containing missing values (stat_{} output)",
687 if removed == 1 { "" } else { "s" },
688 stat.name()
689 ));
690 }
691
692 if working_data.nrows() == 0 {
697 if input_rows > 0 {
698 for aes in &stat.required_aes() {
699 let col_name = aes.col_name();
700 if !pre_stat_columns.iter().any(|c| c == col_name) {
701 return Err(GGError::ValidationError(format!(
702 "stat_{} requires aesthetic '{}' but it was not provided",
703 stat.name(),
704 col_name
705 )));
706 }
707 }
708 warnings.push(format!(
709 "{geom_label}: stat_{} produced no data from {input_rows} row{} \
710 (too few or degenerate values); layer skipped",
711 stat.name(),
712 if input_rows == 1 { "" } else { "s" }
713 ));
714 }
715 return Ok(BuiltLayer {
716 data: DataFrame::new(),
717 geom,
718 show_legend,
719 });
720 }
721
722 for aes in &geom.required_aes() {
728 let col_name = aes.col_name();
729 let supplied = pre_stat_columns.iter().any(|c| c == col_name);
730 if !supplied && !working_data.has_column(col_name) {
731 return Err(GGError::ValidationError(format!(
732 "geom_{} requires aesthetic '{}' but it was not provided",
733 geom.name(),
734 col_name
735 )));
736 }
737 }
738
739 let stat_aes = [
741 ("x", Aesthetic::X),
742 ("y", Aesthetic::Y),
743 ("xmin", Aesthetic::X),
744 ("xmax", Aesthetic::X),
745 ("ymin", Aesthetic::Y),
746 ("ymax", Aesthetic::Y),
747 ];
748 for (col, aes) in &stat_aes {
749 if working_data.has_column(col) {
750 scale_set.ensure_scale(aes, &working_data);
751 }
752 }
753
754 let y_is_user_mapped = merged_mapping.get_mapping(&Aesthetic::Y).is_some();
758 if (!y_is_user_mapped || geom.include_zero_baseline()) && working_data.has_column("y") {
759 if let Some(y_scale) = scale_set.get_mut(&Aesthetic::Y) {
760 y_scale.train(&[crate::data::Value::Float(0.0)]);
761 }
762 }
763
764 let params = PositionParams::default();
766 position.compute(&mut working_data, ¶ms);
767
768 geom.setup_data(&mut working_data);
771 for (col, aes) in &stat_aes {
772 if working_data.has_column(col) {
773 scale_set.ensure_scale(aes, &working_data);
774 }
775 }
776
777 scale_set.train_layer(&working_data);
779 for (col, aes) in &INTERCEPT_COLS {
780 if let (Some(values), Some(scale)) = (working_data.column(col), scale_set.get_mut(aes))
781 {
782 train_intercepts(scale, values);
783 }
784 }
785
786 for (col, aes) in &stat_aes {
790 if let Some(values) = working_data.column(col) {
791 if let Some(scale) = scale_set.get_mut(aes) {
792 scale.train(values);
793 }
794 }
795 }
796
797 Ok(BuiltLayer {
798 data: working_data,
799 geom,
800 show_legend,
801 })
802 }
803
804 fn filter_oob_data(data: &mut DataFrame, scale_set: &ScaleSet) {
806 let x_limits = scale_set.get(&Aesthetic::X).and_then(|s| s.filter_limits());
807 let y_limits = scale_set.get(&Aesthetic::Y).and_then(|s| s.filter_limits());
808
809 if x_limits.is_none() && y_limits.is_none() {
810 return;
811 }
812
813 let nrows = data.nrows();
814 let mut keep = vec![true; nrows];
815
816 if let Some((min, max)) = x_limits {
817 if let Some(col) = data.column("x") {
818 for (i, v) in col.iter().enumerate() {
819 if let Some(f) = v.as_f64() {
820 if f < min || f > max {
821 keep[i] = false;
822 }
823 }
824 }
825 }
826 }
827
828 if let Some((min, max)) = y_limits {
829 if let Some(col) = data.column("y") {
830 for (i, v) in col.iter().enumerate() {
831 if let Some(f) = v.as_f64() {
832 if f < min || f > max {
833 keep[i] = false;
834 }
835 }
836 }
837 }
838 }
839
840 if keep.iter().all(|&k| k) {
842 return;
843 }
844
845 let indices: Vec<usize> = keep
846 .iter()
847 .enumerate()
848 .filter(|(_, &k)| k)
849 .map(|(i, _)| i)
850 .collect();
851
852 let mut result = DataFrame::new();
853 for col_name in data.column_names() {
854 if let Some(src) = data.column(col_name) {
855 let vals: Vec<_> = indices.iter().map(|&i| src[i].clone()).collect();
856 result.add_column(col_name.to_string(), vals);
857 }
858 }
859 *data = result;
860 }
861
862 fn compute_panel_scales(
865 facet_scales: &FacetScales,
866 panels_data: &[Vec<DataFrame>],
867 base_scales: &ScaleSet,
868 ) -> Vec<ScaleSet> {
869 if matches!(facet_scales, FacetScales::Fixed) || panels_data.is_empty() {
870 return vec![];
871 }
872
873 let free_x = matches!(facet_scales, FacetScales::FreeX | FacetScales::Free);
874 let free_y = matches!(facet_scales, FacetScales::FreeY | FacetScales::Free);
875
876 panels_data
877 .iter()
878 .map(|panel_layers| {
879 let mut panel_set = base_scales.clone();
880
881 if free_x {
883 if let Some(s) = panel_set.get_mut(&Aesthetic::X) {
884 s.reset_training();
885 }
886 }
887 if free_y {
888 if let Some(s) = panel_set.get_mut(&Aesthetic::Y) {
889 s.reset_training();
890 }
891 }
892
893 for layer_data in panel_layers {
896 panel_set.train_layer(layer_data);
897 for (cols, aes) in [
898 (["xmin", "xmax", "xintercept"], Aesthetic::X),
899 (["ymin", "ymax", "yintercept"], Aesthetic::Y),
900 ] {
901 let freed = match aes {
902 Aesthetic::X => free_x,
903 _ => free_y,
904 };
905 if !freed {
906 continue;
907 }
908 for c in cols {
909 if let (Some(vals), Some(s)) =
910 (layer_data.column(c), panel_set.get_mut(&aes))
911 {
912 if c.ends_with("intercept") {
913 train_intercepts(s, vals);
914 } else {
915 s.train(vals);
916 }
917 }
918 }
919 }
920 }
921
922 panel_set
923 })
924 .collect()
925 }
926
927 fn compute_suppressed_aes(built_layers: &[BuiltLayer]) -> std::collections::HashSet<Aesthetic> {
931 use std::collections::HashSet;
932 let legend_aes = [
933 Aesthetic::Color,
934 Aesthetic::Fill,
935 Aesthetic::Shape,
936 Aesthetic::Linetype,
937 Aesthetic::Size,
938 Aesthetic::Alpha,
939 ];
940 let mut suppressed = HashSet::new();
941 for aes in &legend_aes {
942 let col_name = aes.col_name();
943 let mut any_has = false;
944 let mut all_hidden = true;
945 for bl in built_layers {
946 if bl.data.has_column(col_name) {
947 any_has = true;
948 match bl.show_legend {
949 Some(false) => {} _ => {
951 all_hidden = false;
952 break;
953 }
954 }
955 }
956 }
957 if any_has && all_hidden {
958 suppressed.insert(aes.clone());
959 }
960 }
961 suppressed
962 }
963
964 fn detect_group_columns(data: &DataFrame) -> Vec<String> {
967 let candidates = ["group", "color", "fill", "x"];
968 let mut group_cols = Vec::new();
969 for &col in &candidates {
970 if data.has_column(col) {
971 if let Some(values) = data.column(col) {
972 let is_discrete = values
973 .iter()
974 .any(|v| matches!(v, crate::data::Value::Str(_)));
975 if is_discrete {
976 group_cols.push(col.to_string());
977 }
978 }
979 }
980 }
981 group_cols
982 }
983}