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
100fn column_aesthetic(name: &str) -> Option<Aesthetic> {
106 match name {
107 "x" => Some(Aesthetic::X),
108 "y" => Some(Aesthetic::Y),
109 "fill" => Some(Aesthetic::Fill),
110 "color" | "colour" => Some(Aesthetic::Color),
111 "size" => Some(Aesthetic::Size),
112 "linewidth" => Some(Aesthetic::Linewidth),
113 "alpha" => Some(Aesthetic::Alpha),
114 _ => None,
115 }
116}
117
118fn render_tooltip_template(
125 template: &str,
126 data: &DataFrame,
127 scales: &ScaleSet,
128 row: usize,
129) -> String {
130 let mut out = String::new();
131 let mut chars = template.chars().peekable();
132 while let Some(c) = chars.next() {
133 match c {
134 '{' if chars.peek() == Some(&'{') => {
135 chars.next();
136 out.push('{');
137 }
138 '}' if chars.peek() == Some(&'}') => {
139 chars.next();
140 out.push('}');
141 }
142 '{' => {
143 let mut name = String::new();
144 let mut closed = false;
145 for nc in chars.by_ref() {
146 if nc == '}' {
147 closed = true;
148 break;
149 }
150 name.push(nc);
151 }
152 if closed {
153 out.push_str(&render_placeholder(&name, data, scales, row));
154 } else {
155 out.push('{');
158 out.push_str(&name);
159 }
160 }
161 c => out.push(c),
162 }
163 }
164 out
165}
166
167fn render_placeholder(name: &str, data: &DataFrame, scales: &ScaleSet, row: usize) -> String {
168 let Some(col) = data.column(name) else {
169 return String::new();
170 };
171 let Some(v) = col.get(row) else {
172 return String::new();
173 };
174 if v.is_na() {
175 return String::new();
176 }
177 if let Some(formatted) = column_aesthetic(name)
178 .and_then(|aes| scales.get(&aes))
179 .and_then(|s| s.format_value(v))
180 {
181 return formatted;
182 }
183 crate::geom::tip_value(v)
184}
185
186pub struct PlotBuilder;
188
189impl PlotBuilder {
190 pub fn build(plot: GGPlot) -> Result<BuiltPlot, GGError> {
191 let GGPlot {
192 data: plot_data,
193 mapping: plot_mapping,
194 layers,
195 scales: user_scales,
196 mut coord,
197 theme,
198 labels,
199 facet,
200 annotations,
201 guide_legend,
202 warnings: plot_warnings,
203 default_aspect_ratio,
204 locale,
205 svg_root_data: _,
206 root_attrs: _,
207 svg_warnings_attr: _,
208 svg_formatted_values: _,
209 } = plot;
210 let mut theme = theme;
211 if theme.aspect_ratio.is_none() {
212 theme.aspect_ratio = default_aspect_ratio;
213 }
214
215 plot_data.validate()?;
218 for layer in &layers {
219 if let Some(d) = &layer.data {
220 d.validate()?;
221 }
222 }
223
224 let mut scale_set = ScaleSet::new();
225
226 for s in user_scales {
228 scale_set.add(s);
229 }
230
231 let mut built_layers = Vec::new();
232 let mut warnings: Vec<String> = plot_warnings;
233
234 let facet_vars = Self::facet_vars(&facet);
237
238 for layer in layers {
239 let built = Self::build_layer(
240 layer,
241 &plot_data,
242 &plot_mapping,
243 &mut scale_set,
244 &theme,
245 &facet_vars,
246 &mut warnings,
247 )?;
248 built_layers.push(built);
249 }
250
251 for bl in &built_layers {
253 scale_set.train_layer(&bl.data);
254 for (col, aes) in &INTERCEPT_COLS {
255 if let (Some(values), None) = (bl.data.column(col), scale_set.get(aes)) {
257 let mut frame = DataFrame::new();
258 frame.add_column(aes.col_name().to_string(), values.to_vec());
259 scale_set.ensure_scale(aes, &frame);
260 }
261 if let (Some(values), Some(scale)) = (bl.data.column(col), scale_set.get_mut(aes)) {
262 train_intercepts(scale, values);
263 }
264 }
265 }
266
267 for scale in scale_set.iter() {
272 scale.validate(&mut warnings);
273 }
274
275 if built_layers.iter().all(|bl| bl.data.nrows() == 0) {
279 for aes in [Aesthetic::X, Aesthetic::Y] {
280 if scale_set.get(&aes).is_none() {
281 scale_set.add(Box::new(
282 crate::scale::continuous::ScaleContinuous::new().for_aesthetic(aes),
283 ));
284 }
285 }
286 }
287
288 if let Some((min, max)) = coord.zoom_x() {
290 scale_set.set_limits(&Aesthetic::X, min, max);
291 }
292 if let Some((min, max)) = coord.zoom_y() {
293 scale_set.set_limits(&Aesthetic::Y, min, max);
294 }
295
296 coord.train_scales(&mut scale_set);
298
299 let axis_span = |aes: &Aesthetic| {
303 scale_set.get(aes).and_then(|s| {
304 s.domain().map(|(min, max)| crate::coord::AxisSpan {
305 min,
306 max,
307 pmin: s.map(&crate::data::Value::Float(min)),
308 pmax: s.map(&crate::data::Value::Float(max)),
309 })
310 })
311 };
312 let x_span = axis_span(&Aesthetic::X);
313 let y_span = axis_span(&Aesthetic::Y);
314 coord.set_domains(x_span, y_span);
315
316 for spec in &plot_mapping.after_scale {
321 if let Some(src_scale) = scale_set.get(&spec.source) {
322 let modified = crate::scale::modified::ScaleColorModified::new(
323 src_scale.clone_box(),
324 spec.target.clone(),
325 spec.lightness,
326 );
327 let (src_col, tgt_col) = (spec.source.col_name(), spec.target.col_name());
328 for bl in &mut built_layers {
329 if !bl.data.has_column(tgt_col) {
330 if let Some(vals) = bl.data.column(src_col) {
331 let vals = vals.to_vec();
332 bl.data.add_column(tgt_col.to_string(), vals);
333 }
334 }
335 }
336 scale_set.add(Box::new(modified));
337 }
338 }
339
340 if let Some(locale) = &locale {
343 scale_set.set_default_locale(locale);
344 }
345
346 let (panels, panels_data) = Self::compute_facets(&facet, &built_layers, &plot_data);
348
349 let suppressed_aes = Self::compute_suppressed_aes(&built_layers);
351
352 let facet_scales_mode = match &facet {
354 Facet::Wrap { scales, .. } => scales.clone(),
355 Facet::Grid { scales, .. } => scales.clone(),
356 Facet::None => FacetScales::Fixed,
357 };
358 let panel_scales = Self::compute_panel_scales(&facet_scales_mode, &panels_data, &scale_set);
359
360 Ok(BuiltPlot {
361 layers: built_layers,
362 scales: scale_set,
363 coord,
364 theme,
365 labels,
366 facet,
367 panels,
368 panels_data,
369 annotations,
370 guide_legend,
371 suppressed_aes,
372 panel_scales,
373 warnings,
374 })
375 }
376
377 fn retain_rows(data: &mut DataFrame, keep: &[bool]) {
379 if keep.iter().all(|&k| k) {
380 return;
381 }
382 let mut result = DataFrame::new();
383 for col_name in data.column_names() {
384 if let Some(src) = data.column(col_name) {
385 let vals: Vec<_> = src
386 .iter()
387 .zip(keep)
388 .filter(|(_, &k)| k)
389 .map(|(v, _)| v.clone())
390 .collect();
391 result.add_column(col_name.to_string(), vals);
392 }
393 }
394 *data = result;
395 }
396
397 fn drop_non_finite(data: &mut DataFrame, allow_infinite: bool, keep_as_break: bool) -> usize {
406 let n = data.nrows();
407 let mut keep = vec![true; n];
408 for col in POSITION_COLS {
409 if let Some(values) = data.column(col) {
410 for (i, v) in values.iter().enumerate() {
411 if let crate::data::Value::Float(f) = v {
412 let bad = f.is_nan() || (f.is_infinite() && !allow_infinite);
413 if bad {
414 keep[i] = false;
415 }
416 }
417 }
418 }
419 }
420 let removed = keep.iter().filter(|&&k| !k).count();
421 if removed > 0 && !keep_as_break {
422 Self::retain_rows(data, &keep);
423 }
424 removed
425 }
426
427 fn facet_vars(facet: &Facet) -> Vec<String> {
429 match facet {
430 Facet::None => vec![],
431 Facet::Wrap { var, .. } => vec![var.clone()],
432 Facet::Grid {
433 row_var, col_var, ..
434 } => row_var.iter().chain(col_var.iter()).cloned().collect(),
435 }
436 }
437
438 fn compute_facets(
439 facet: &Facet,
440 built_layers: &[BuiltLayer],
441 _plot_data: &DataFrame,
442 ) -> (Vec<Panel>, Vec<Vec<DataFrame>>) {
443 match facet {
444 Facet::None => (vec![], vec![]),
445 Facet::Wrap {
446 var,
447 ncol,
448 labeller,
449 ..
450 } => {
451 let mut levels: Vec<String> = Vec::new();
453 for bl in built_layers {
454 if let Some(col) = bl.data.column(var) {
455 for v in col {
456 let key = v.to_group_key();
457 if !levels.contains(&key) {
458 levels.push(key);
459 }
460 }
461 }
462 }
463
464 let panels: Vec<Panel> = levels
466 .iter()
467 .enumerate()
468 .map(|(i, value)| {
469 let ncols =
470 ncol.unwrap_or_else(|| (levels.len() as f64).sqrt().ceil() as usize);
471 let formatted = labeller.format(var, value);
472 Panel {
473 row: i / ncols.max(1),
474 col: i % ncols.max(1),
475 label: formatted.clone(),
476 row_label: None,
477 col_label: Some(formatted),
478 rect: crate::render::Rect {
479 x: 0.0,
480 y: 0.0,
481 width: 0.0,
482 height: 0.0,
483 },
484 }
485 })
486 .collect();
487
488 let panels_data: Vec<Vec<DataFrame>> = levels
490 .iter()
491 .map(|level| {
492 built_layers
493 .iter()
494 .map(|bl| Self::filter_data_by_var(&bl.data, var, level))
495 .collect()
496 })
497 .collect();
498
499 (panels, panels_data)
500 }
501 Facet::Grid {
502 row_var,
503 col_var,
504 labeller,
505 ..
506 } => {
507 let mut row_levels: Vec<String> = Vec::new();
508 let mut col_levels: Vec<String> = Vec::new();
509
510 for bl in built_layers {
511 if let Some(rv) = row_var {
512 if let Some(col) = bl.data.column(rv) {
513 for v in col {
514 let key = v.to_group_key();
515 if !row_levels.contains(&key) {
516 row_levels.push(key);
517 }
518 }
519 }
520 }
521 if let Some(cv) = col_var {
522 if let Some(col) = bl.data.column(cv) {
523 for v in col {
524 let key = v.to_group_key();
525 if !col_levels.contains(&key) {
526 col_levels.push(key);
527 }
528 }
529 }
530 }
531 }
532
533 if row_levels.is_empty() {
534 row_levels.push("".to_string());
535 }
536 if col_levels.is_empty() {
537 col_levels.push("".to_string());
538 }
539
540 let mut panels = Vec::new();
541 let mut panels_data = Vec::new();
542
543 for (ri, rl) in row_levels.iter().enumerate() {
544 for (ci, cl) in col_levels.iter().enumerate() {
545 let row_fmt = if rl.is_empty() {
546 None
547 } else {
548 let rv = row_var.as_deref().unwrap_or("");
549 Some(labeller.format(rv, rl))
550 };
551 let col_fmt = if cl.is_empty() {
552 None
553 } else {
554 let cv = col_var.as_deref().unwrap_or("");
555 Some(labeller.format(cv, cl))
556 };
557 let label = match (&row_fmt, &col_fmt) {
558 (Some(r), Some(c)) => format!("{r} | {c}"),
559 (Some(r), None) => r.clone(),
560 (None, Some(c)) => c.clone(),
561 (None, None) => String::new(),
562 };
563 panels.push(Panel {
564 row: ri,
565 col: ci,
566 label,
567 row_label: row_fmt,
568 col_label: col_fmt,
569 rect: crate::render::Rect {
570 x: 0.0,
571 y: 0.0,
572 width: 0.0,
573 height: 0.0,
574 },
575 });
576
577 let layer_data: Vec<DataFrame> = built_layers
578 .iter()
579 .map(|bl| {
580 let mut data = bl.data.clone();
581 if let Some(rv) = row_var {
582 if !rl.is_empty() {
583 data = Self::filter_data_by_var(&data, rv, rl);
584 }
585 }
586 if let Some(cv) = col_var {
587 if !cl.is_empty() {
588 data = Self::filter_data_by_var(&data, cv, cl);
589 }
590 }
591 data
592 })
593 .collect();
594 panels_data.push(layer_data);
595 }
596 }
597
598 (panels, panels_data)
599 }
600 }
601 }
602
603 fn filter_data_by_var(data: &DataFrame, var: &str, level: &str) -> DataFrame {
604 if let Some(col) = data.column(var) {
605 let indices: Vec<usize> = col
606 .iter()
607 .enumerate()
608 .filter(|(_, v)| v.to_group_key() == level)
609 .map(|(i, _)| i)
610 .collect();
611
612 let mut result = DataFrame::new();
613 for col_name in data.column_names() {
614 if let Some(src) = data.column(col_name) {
615 let vals: Vec<_> = indices.iter().map(|&i| src[i].clone()).collect();
616 result.add_column(col_name.to_string(), vals);
617 }
618 }
619 result
620 } else {
621 data.clone()
622 }
623 }
624
625 fn build_layer(
626 layer: Layer,
627 plot_data: &DataFrame,
628 plot_mapping: &Aes,
629 scale_set: &mut ScaleSet,
630 theme: &Theme,
631 facet_vars: &[String],
632 warnings: &mut Vec<String>,
633 ) -> Result<BuiltLayer, GGError> {
634 let primary = theme.primary;
635 let geom_theme = &theme.geom;
636 let Layer {
637 data: layer_data,
638 mapping: layer_mapping,
639 mut geom,
640 stat,
641 position,
642 params: _,
643 show_legend,
644 explicit_style,
645 tooltip_template,
646 } = layer;
647
648 let source_data = layer_data.as_ref().unwrap_or(plot_data);
652
653 let merged_mapping = if geom.inherit_aes() {
657 plot_mapping.merge(&layer_mapping)
658 } else {
659 layer_mapping.clone()
660 };
661
662 if !explicit_style {
672 geom.apply_element_geom(geom_theme);
673 }
674
675 if let Some(color) = primary.filter(|_| !explicit_style) {
679 let has_color = merged_mapping.get_mapping(&Aesthetic::Color).is_some();
680 let has_fill = merged_mapping.get_mapping(&Aesthetic::Fill).is_some();
681 if !has_color && !has_fill {
682 geom.set_series_color(color);
683 }
684 }
685
686 let mut working_data = resolve_mappings(source_data, &merged_mapping, warnings);
688
689 let pre_stat_columns: Vec<String> = working_data
694 .column_names()
695 .iter()
696 .map(|s| s.to_string())
697 .collect();
698
699 for m in &merged_mapping.mappings {
701 scale_set.ensure_scale(&m.aesthetic, &working_data);
702 }
703
704 for scale in scale_set.iter() {
707 let aes = scale.aesthetic();
708 let cols: Vec<&str> = match aes {
709 Aesthetic::X => X_FAMILY.to_vec(),
710 Aesthetic::Y => Y_FAMILY.to_vec(),
711 _ => vec![aes.col_name()],
712 };
713 for col_name in cols {
714 let Some(col) = working_data.column(col_name) else {
715 continue;
716 };
717 let transformed: Vec<_> = col
721 .iter()
722 .map(|v| match (scale.transform(v), v.as_f64()) {
723 (crate::data::Value::Na, Some(_)) => crate::data::Value::Float(f64::NAN),
724 (t, _) => t,
725 })
726 .collect();
727 let any_changed = transformed.iter().zip(col.iter()).any(|(t, o)| {
728 match (t.as_f64(), o.as_f64()) {
729 (Some(a), Some(b)) => a.is_nan() || (a - b).abs() > f64::EPSILON,
730 _ => false,
731 }
732 });
733 if any_changed {
734 if let Some(col_mut) = working_data.column_mut(col_name) {
735 *col_mut = transformed;
736 }
737 }
738 }
739 }
740
741 Self::filter_oob_data(&mut working_data, scale_set);
743
744 let geom_label = format!("geom_{}", geom.name());
748 let removed = Self::drop_non_finite(
749 &mut working_data,
750 geom.allows_infinite(),
751 geom.keeps_na_as_break(),
752 );
753 if removed > 0 {
754 warnings.push(format!(
755 "{geom_label}: removed {removed} row{} containing non-finite values",
756 if removed == 1 { "" } else { "s" }
757 ));
758 }
759 let input_rows = working_data.nrows();
760
761 stat.validate(warnings);
765
766 let mut group_cols = if stat.panelwise() {
773 Vec::new()
774 } else {
775 Self::detect_group_columns(&working_data)
776 };
777 for fv in facet_vars {
778 if working_data.has_column(fv) && !group_cols.contains(fv) {
779 group_cols.push(fv.clone());
780 }
781 }
782
783 working_data = if !group_cols.is_empty() {
784 let groups =
785 working_data.group_by(&group_cols.iter().map(|s| s.as_str()).collect::<Vec<_>>());
786 let mut result = DataFrame::new();
787 for group in groups {
788 let mut computed = stat.compute_group(&group, scale_set);
789 let n = computed.nrows();
790 if n > 0 {
791 for fv in facet_vars {
792 if !computed.has_column(fv) {
793 if let Some(val) = group.column(fv).and_then(|c| c.first()).cloned() {
794 computed.add_column(fv.clone(), vec![val; n]);
795 }
796 }
797 }
798 }
799 result.vstack(&computed);
800 }
801 result
802 } else {
803 stat.compute_group(&working_data, scale_set)
804 };
805
806 apply_after_stat(&mut working_data, &merged_mapping, warnings);
808
809 let removed = Self::drop_non_finite(
812 &mut working_data,
813 geom.allows_infinite(),
814 geom.keeps_na_as_break(),
815 );
816 if removed > 0 {
817 warnings.push(format!(
818 "{geom_label}: removed {removed} row{} containing missing values (stat_{} output)",
819 if removed == 1 { "" } else { "s" },
820 stat.name()
821 ));
822 }
823
824 if working_data.nrows() == 0 {
829 if input_rows > 0 {
830 for aes in &stat.required_aes() {
831 let col_name = aes.col_name();
832 if !pre_stat_columns.iter().any(|c| c == col_name) {
833 return Err(GGError::ValidationError(format!(
834 "stat_{} requires aesthetic '{}' but it was not provided",
835 stat.name(),
836 col_name
837 )));
838 }
839 }
840 warnings.push(format!(
841 "{geom_label}: stat_{} produced no data from {input_rows} row{} \
842 (too few or degenerate values); layer skipped",
843 stat.name(),
844 if input_rows == 1 { "" } else { "s" }
845 ));
846 }
847 return Ok(BuiltLayer {
848 data: DataFrame::new(),
849 geom,
850 show_legend,
851 });
852 }
853
854 for aes in &geom.required_aes() {
860 let col_name = aes.col_name();
861 let supplied = pre_stat_columns.iter().any(|c| c == col_name);
862 if !supplied && !working_data.has_column(col_name) {
863 return Err(GGError::ValidationError(format!(
864 "geom_{} requires aesthetic '{}' but it was not provided",
865 geom.name(),
866 col_name
867 )));
868 }
869 }
870
871 let stat_aes = [
873 ("x", Aesthetic::X),
874 ("y", Aesthetic::Y),
875 ("xmin", Aesthetic::X),
876 ("xmax", Aesthetic::X),
877 ("ymin", Aesthetic::Y),
878 ("ymax", Aesthetic::Y),
879 ];
880 for (col, aes) in &stat_aes {
881 if working_data.has_column(col) {
882 scale_set.ensure_scale(aes, &working_data);
883 }
884 }
885
886 let y_is_user_mapped = merged_mapping.get_mapping(&Aesthetic::Y).is_some();
890 if (!y_is_user_mapped || geom.include_zero_baseline()) && working_data.has_column("y") {
891 if let Some(y_scale) = scale_set.get_mut(&Aesthetic::Y) {
892 y_scale.train(&[crate::data::Value::Float(0.0)]);
893 }
894 }
895
896 let params = PositionParams::default();
898 position.compute(&mut working_data, ¶ms);
899
900 geom.setup_data(&mut working_data);
903 for (col, aes) in &stat_aes {
904 if working_data.has_column(col) {
905 scale_set.ensure_scale(aes, &working_data);
906 }
907 }
908
909 scale_set.train_layer(&working_data);
911 for (col, aes) in &INTERCEPT_COLS {
912 if let (Some(values), Some(scale)) = (working_data.column(col), scale_set.get_mut(aes))
913 {
914 train_intercepts(scale, values);
915 }
916 }
917
918 for (col, aes) in &stat_aes {
922 if let Some(values) = working_data.column(col) {
923 if let Some(scale) = scale_set.get_mut(aes) {
924 scale.train(values);
925 }
926 }
927 }
928
929 if let Some(template) = &tooltip_template {
934 if !geom.consumes_tooltips() {
939 warnings.push(format!(
940 "{geom_label}: tooltip(\"{template}\") has no effect — this geom doesn't read tooltips"
941 ));
942 }
943 let rendered: Vec<crate::data::Value> = (0..working_data.nrows())
944 .map(|i| {
945 crate::data::Value::Str(render_tooltip_template(
946 template,
947 &working_data,
948 scale_set,
949 i,
950 ))
951 })
952 .collect();
953 working_data.add_column(crate::aes::TOOLTIP_INTERNAL_NAME.to_string(), rendered);
954 }
955
956 Ok(BuiltLayer {
957 data: working_data,
958 geom,
959 show_legend,
960 })
961 }
962
963 fn filter_oob_data(data: &mut DataFrame, scale_set: &ScaleSet) {
965 let x_limits = scale_set.get(&Aesthetic::X).and_then(|s| s.filter_limits());
966 let y_limits = scale_set.get(&Aesthetic::Y).and_then(|s| s.filter_limits());
967
968 if x_limits.is_none() && y_limits.is_none() {
969 return;
970 }
971
972 let nrows = data.nrows();
973 let mut keep = vec![true; nrows];
974
975 if let Some((min, max)) = x_limits {
976 if let Some(col) = data.column("x") {
977 for (i, v) in col.iter().enumerate() {
978 if let Some(f) = v.as_f64() {
979 if f < min || f > max {
980 keep[i] = false;
981 }
982 }
983 }
984 }
985 }
986
987 if let Some((min, max)) = y_limits {
988 if let Some(col) = data.column("y") {
989 for (i, v) in col.iter().enumerate() {
990 if let Some(f) = v.as_f64() {
991 if f < min || f > max {
992 keep[i] = false;
993 }
994 }
995 }
996 }
997 }
998
999 if keep.iter().all(|&k| k) {
1001 return;
1002 }
1003
1004 let indices: Vec<usize> = keep
1005 .iter()
1006 .enumerate()
1007 .filter(|(_, &k)| k)
1008 .map(|(i, _)| i)
1009 .collect();
1010
1011 let mut result = DataFrame::new();
1012 for col_name in data.column_names() {
1013 if let Some(src) = data.column(col_name) {
1014 let vals: Vec<_> = indices.iter().map(|&i| src[i].clone()).collect();
1015 result.add_column(col_name.to_string(), vals);
1016 }
1017 }
1018 *data = result;
1019 }
1020
1021 fn compute_panel_scales(
1024 facet_scales: &FacetScales,
1025 panels_data: &[Vec<DataFrame>],
1026 base_scales: &ScaleSet,
1027 ) -> Vec<ScaleSet> {
1028 if matches!(facet_scales, FacetScales::Fixed) || panels_data.is_empty() {
1029 return vec![];
1030 }
1031
1032 let free_x = matches!(facet_scales, FacetScales::FreeX | FacetScales::Free);
1033 let free_y = matches!(facet_scales, FacetScales::FreeY | FacetScales::Free);
1034
1035 panels_data
1036 .iter()
1037 .map(|panel_layers| {
1038 let mut panel_set = base_scales.clone();
1039
1040 if free_x {
1042 if let Some(s) = panel_set.get_mut(&Aesthetic::X) {
1043 s.reset_training();
1044 }
1045 }
1046 if free_y {
1047 if let Some(s) = panel_set.get_mut(&Aesthetic::Y) {
1048 s.reset_training();
1049 }
1050 }
1051
1052 for layer_data in panel_layers {
1055 panel_set.train_layer(layer_data);
1056 for (cols, aes) in [
1057 (["xmin", "xmax", "xintercept"], Aesthetic::X),
1058 (["ymin", "ymax", "yintercept"], Aesthetic::Y),
1059 ] {
1060 let freed = match aes {
1061 Aesthetic::X => free_x,
1062 _ => free_y,
1063 };
1064 if !freed {
1065 continue;
1066 }
1067 for c in cols {
1068 if let (Some(vals), Some(s)) =
1069 (layer_data.column(c), panel_set.get_mut(&aes))
1070 {
1071 if c.ends_with("intercept") {
1072 train_intercepts(s, vals);
1073 } else {
1074 s.train(vals);
1075 }
1076 }
1077 }
1078 }
1079 }
1080
1081 panel_set
1082 })
1083 .collect()
1084 }
1085
1086 fn compute_suppressed_aes(built_layers: &[BuiltLayer]) -> std::collections::HashSet<Aesthetic> {
1090 use std::collections::HashSet;
1091 let legend_aes = [
1092 Aesthetic::Color,
1093 Aesthetic::Fill,
1094 Aesthetic::Shape,
1095 Aesthetic::Linetype,
1096 Aesthetic::Size,
1097 Aesthetic::Alpha,
1098 Aesthetic::Linewidth,
1099 ];
1100 let mut suppressed = HashSet::new();
1101 for aes in &legend_aes {
1102 let col_name = aes.col_name();
1103 let mut any_has = false;
1104 let mut all_hidden = true;
1105 for bl in built_layers {
1106 if bl.data.has_column(col_name) {
1107 any_has = true;
1108 match bl.show_legend {
1109 Some(false) => {} _ => {
1111 all_hidden = false;
1112 break;
1113 }
1114 }
1115 }
1116 }
1117 if any_has && all_hidden {
1118 suppressed.insert(aes.clone());
1119 }
1120 }
1121 suppressed
1122 }
1123
1124 fn detect_group_columns(data: &DataFrame) -> Vec<String> {
1127 let candidates = ["group", "color", "fill", "x"];
1128 let mut group_cols = Vec::new();
1129 for &col in &candidates {
1130 if data.has_column(col) {
1131 if let Some(values) = data.column(col) {
1132 let is_discrete = values
1133 .iter()
1134 .any(|v| matches!(v, crate::data::Value::Str(_)));
1135 if is_discrete {
1136 group_cols.push(col.to_string());
1137 }
1138 }
1139 }
1140 }
1141 group_cols
1142 }
1143}