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