From b598aa5c960d0e5c20904ecf752d6ee4297f2e9e Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Wed, 6 May 2026 15:52:39 -0700 Subject: [PATCH] better color support --- rust/pspp/src/spv/read/graph.rs | 142 +++++++++++++----- .../pspp/src/spv/testdata/graph2.expected.svg | 12 +- 2 files changed, 111 insertions(+), 43 deletions(-) diff --git a/rust/pspp/src/spv/read/graph.rs b/rust/pspp/src/spv/read/graph.rs index c06222b946..7d05f07393 100644 --- a/rust/pspp/src/spv/read/graph.rs +++ b/rust/pspp/src/spv/read/graph.rs @@ -403,10 +403,13 @@ impl Visualization { series: &BTreeMap<&str, Series>, bin_widths: &mut BTreeMap, axis_domains: &mut EnumMap>, + styles: &'a HashMap, ) -> Result>, GraphError> { let elements = graph .elements() - .map(|(raw, graphic)| Element::new(raw, graphic, series, bin_widths, axis_domains)) + .map(|(raw, graphic)| { + Element::new(raw, graphic, series, bin_widths, axis_domains, styles) + }) .collect::, _>>()?; let coordinates = graph .coordinates() @@ -494,7 +497,6 @@ impl Visualization { mut csv_data: Option, warn: &mut dyn FnMut(GraphWarning), ) -> Result { - let _styles = self.decode_styles(); let style_cycles = self.decode_style_cycles(); for delimited_source in self .children @@ -524,12 +526,7 @@ impl Visualization { .map_or(&EN, |lang| Locale::for_lang(lang)); let series = self.decode_series(locale, data, warn); let domains = self.decode_domains(&series, warn); - let _styles = self - .children - .iter() - .filter_map(|child| child.style()) - .filter_map(|style| style.id.as_ref().map(|id| (id.as_str(), style))) - .collect::>(); + let styles = self.decode_styles(); let frames = self.decode_frames(); /* @@ -608,6 +605,7 @@ impl Visualization { &domains, &series, &style_cycles, + &styles, )?); } @@ -621,6 +619,7 @@ impl Visualization { domains: &BTreeMap<&str, Domain>, series: &BTreeMap<&str, Series>, style_cycles: &HashMap<&str, &StyleCycle>, + styles: &HashMap, ) -> Result { let mut graph_svg = Group::new().with_id(&graph.id); let mut bin_widths = BTreeMap::new(); @@ -631,6 +630,7 @@ impl Visualization { &series, &mut bin_widths, &mut axis_domains, + styles, )?; let elements = self.pass( &graph, @@ -638,6 +638,7 @@ impl Visualization { &series, &mut bin_widths, &mut axis_domains, + styles, )?; let mut font_metrics = FontMetrics::new(); @@ -768,7 +769,11 @@ impl Visualization { for element in &elements { let mut categoricals = IndexMap::new(); let mut color_index = None; - if let Some(color) = element.raw.color.first() + if let Some(color) = element + .raw + .color + .iter() + .find(|color| color.affect.unwrap_or_default().includes_main()) && let Some(variable) = &color.variable { let series = series @@ -779,6 +784,22 @@ impl Visualization { Some(categoricals.insert_full(series.name.as_str(), series).0); } } + let mut color2_index = None; + if let Some(color2) = element + .raw + .color + .iter() + .find(|color| color.affect.unwrap_or_default().includes_secondary()) + && let Some(variable) = &color2.variable + { + let series = series + .get(variable.as_str()) + .ok_or_else(|| GraphError::MissingVariable(variable.clone()))?; + if series.categorical { + color2_index = + Some(categoricals.insert_full(series.name.as_str(), series).0); + } + } let mut styling = None; if let Some(style_by) = element.raw.style_by.first() && let Some(variable) = &style_by.variable @@ -843,6 +864,9 @@ impl Visualization { } for (categoricals, group) in &grouped_data { let mut color = Color::BLACK; + if let Some(style_color) = element.style.color { + color = style_color; + } if let Some(color_index) = color_index { let index = categoricals[color_index] .as_number() @@ -863,6 +887,30 @@ impl Visualization { } } + let mut color2 = Color::BLACK; + if let Some(style_color) = element.style.color2 { + color2 = style_color; + } + if let Some(color2_index) = color2_index { + let index = categoricals[color2_index] + .as_number() + .unwrap_or_default() + .unwrap_or_default(); + let index = ::from(index).unwrap_or_default(); + color2 = PALETTE[index % PALETTE.len()]; + } + if let Some((index, style_cycle)) = styling { + let index = categoricals[index] + .as_number() + .unwrap_or_default() + .unwrap_or_default(); + let index = ::from(index).unwrap_or_default(); + let style = style_cycle.get_style(index); + if let Some(style_color) = style.color2 { + color2 = style_color; + } + } + fn points( rect: &Rect, group: &[Vec<&SeriesValue>], @@ -1018,8 +1066,8 @@ impl Visualization { } element_svg.append( Polygon::new() - .set("fill", "black") - .set("stroke", "black") + .set("fill", color.display_css().to_string()) + .set("stroke", color2.display_css().to_string()) .set("points", data), ); } @@ -1298,7 +1346,7 @@ impl EmbeddedSource { } } -#[derive(Deserialize, Debug, Default)] +#[derive(Copy, Clone, Deserialize, Debug, Default, PartialEq, Eq)] #[serde(rename_all = "camelCase")] enum DelimitedFileSourceMissing { #[default] @@ -1751,7 +1799,7 @@ struct CategoryOrder { order: String, } -#[derive(Deserialize, Debug)] +#[derive(Copy, Clone, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] enum SortMethod { Data, @@ -2422,6 +2470,7 @@ struct ValueMapEntry { struct Element<'a> { raw: &'a RawElement, + style: Cow<'a, Style>, graphic: Graphic, variables: EnumMap>, ignore_minmax: EnumMap, @@ -2434,6 +2483,7 @@ impl<'a> Element<'a> { series: &BTreeMap<&str, Series>, bin_widths: &mut BTreeMap, axis_domains: &mut EnumMap>, + styles: &'a HashMap, ) -> Result { let mut variables = EnumMap::default(); for (coordinate, axis) in @@ -2674,6 +2724,13 @@ impl<'a> Element<'a> { } Ok(Self { raw, + style: if let Some(style) = raw.style.as_deref() + && let Some(style) = styles.get(style) + { + Cow::Borrowed(style) + } else { + Cow::Owned(Style::default()) + }, graphic, variables, ignore_minmax, @@ -2765,7 +2822,7 @@ impl GraphChild { } } -#[derive(Deserialize, Debug)] +#[derive(Copy, Clone, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] enum BreakRepresentation { Gap, @@ -2806,8 +2863,9 @@ enum Graphic { Polygon, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Default)] #[serde(rename_all = "camelCase")] +#[serde(default)] struct RawElement { #[serde(rename = "@id")] id: Option, @@ -2815,18 +2873,15 @@ struct RawElement { #[serde(rename = "@breakRepresentation")] break_representation: Option, - #[serde(default, rename = "@closed")] + #[serde(rename = "@closed")] closed: bool, - #[serde(default, rename = "@preserveStraightLines")] + #[serde(rename = "@preserveStraightLines")] preserve_straight_lines: bool, - #[serde(default, rename = "@centerIn3D")] + #[serde(rename = "@centerIn3D")] center_in_3d: bool, - #[serde(default)] - position: Vec, - #[serde(rename = "@medianStyle")] median_style: Option, @@ -2842,15 +2897,17 @@ struct RawElement { #[serde(rename = "@extremeStyle")] extreme_style: Option, - #[serde(default, rename = "$value")] + #[serde(rename = "@style")] + style: Option, + + #[serde(rename = "$value")] statistics: Vec, + position: Vec, x: Option, y: Option, z: Option, - #[serde(default)] color: Vec, - #[serde(default)] style_by: Vec, } @@ -2924,7 +2981,7 @@ struct ColorMapping { to: String, } -#[derive(Deserialize, Debug)] +#[derive(Copy, Clone, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] enum ColorScale { BipolarBlack, @@ -2938,14 +2995,25 @@ enum ColorScale { Interpolated, } -#[derive(Deserialize, Debug)] +#[derive(Copy, Clone, Deserialize, Debug, Default, PartialEq, Eq)] #[serde(rename_all = "camelCase")] enum ColorAffect { Both, + #[serde(alias = "fill")] + #[default] Main, + #[serde(alias = "stroke")] Secondary, - Stroke, - Fill, +} + +impl ColorAffect { + pub fn includes_main(&self) -> bool { + matches!(self, ColorAffect::Both | ColorAffect::Main) + } + + pub fn includes_secondary(&self) -> bool { + matches!(self, ColorAffect::Both | ColorAffect::Secondary) + } } #[derive(Deserialize, Debug)] @@ -3084,7 +3152,7 @@ impl Location { } } -#[derive(Deserialize, Debug)] +#[derive(Copy, Clone, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] enum LocationMethod { Fixed, @@ -3416,7 +3484,7 @@ struct Scale { method: Method, } -#[derive(Deserialize, Debug)] +#[derive(Copy, Clone, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] enum Method { Linear, @@ -3491,7 +3559,7 @@ struct BinStatistic { dimensions: String, } -#[derive(Deserialize, Debug, Default)] +#[derive(Copy, Clone, Deserialize, Debug, Default, PartialEq, Eq)] #[serde(rename_all = "camelCase")] enum BinShape { #[default] @@ -3501,7 +3569,7 @@ enum BinShape { Voronoi, } -#[derive(Deserialize, Debug, Default)] +#[derive(Copy, Clone, Deserialize, Debug, Default, PartialEq, Eq)] #[serde(rename_all = "camelCase")] enum BinLocation { /// Center of the bin, disregarding the data in the bin. @@ -3513,7 +3581,7 @@ enum BinLocation { Centroid, } -#[derive(Deserialize, Debug, Default)] +#[derive(Copy, Clone, Deserialize, Debug, Default, PartialEq, Eq)] #[serde(rename_all = "camelCase")] enum BinMethod { /// Bin across all the dimensions, without aggregating. All the points in a @@ -3565,7 +3633,7 @@ struct SummaryStatistic { augment: Augment, } -#[derive(Deserialize, Debug, Default)] +#[derive(Copy, Clone, Deserialize, Debug, Default, PartialEq, Eq)] #[serde(rename_all = "camelCase")] enum SummaryMethod { Mean, @@ -3604,7 +3672,7 @@ enum SummaryMethod { FractionTrue, } -#[derive(Deserialize, Debug, Default)] +#[derive(Copy, Clone, Deserialize, Debug, Default, PartialEq, Eq)] #[serde(rename_all = "camelCase")] enum SummaryBase { /// From data for a chart or panel. @@ -3618,7 +3686,7 @@ enum SummaryBase { Coordinate, } -#[derive(Deserialize, Debug, Default)] +#[derive(Copy, Clone, Deserialize, Debug, Default, PartialEq, Eq)] #[serde(rename_all = "camelCase")] enum Augment { /// Treat missing values as zero. @@ -3649,7 +3717,7 @@ struct DistributionStatistic { segments: Option, } -#[derive(Deserialize, Debug, Default)] +#[derive(Copy, Clone, Deserialize, Debug, Default, PartialEq, Eq)] #[serde(rename_all = "lowercase")] enum Distribution { Uniform, diff --git a/rust/pspp/src/spv/testdata/graph2.expected.svg b/rust/pspp/src/spv/testdata/graph2.expected.svg index 621915534c..24dad4c95c 100644 --- a/rust/pspp/src/spv/testdata/graph2.expected.svg +++ b/rust/pspp/src/spv/testdata/graph2.expected.svg @@ -47,10 +47,10 @@ V2 60.0 - + - + @@ -98,10 +98,10 @@ V1 50.0 - + - + @@ -141,10 +141,10 @@ V3 30.0 - + - + -- 2.47.3