diff --git a/src/des/series.rs b/src/des/series.rs index 7995fc6..5054a48 100644 --- a/src/des/series.rs +++ b/src/des/series.rs @@ -344,6 +344,7 @@ impl Line { /// | i64 | [`ColorMap::Literal`] | Interpreted as a color, where the integer is treated as a 32-bit RGBA value (e.g. 0xRRGGBBAA). See [`Rgba8::to_rgba_int`](crate::Rgba8::to_rgba_int). | /// Other combinations of data type and color map will result in an error when rendering the plot. /// If the plot's colorbar is set, the colorbar will be automatically configured based on the color data column and color map. +/// Color categories can be added to the legend if [`color_cats_to_legend`](Scatter::color_cats_to_legend) is true. #[derive(Debug, Clone, PartialEq)] pub struct Scatter { x_data: DataCol, @@ -355,6 +356,7 @@ pub struct Scatter { marker: style::series::Marker, size_data: Option, color_data: Option<(DataCol, ColorMap)>, + color_cats_to_legend: bool, } impl Scatter { @@ -370,6 +372,7 @@ impl Scatter { marker: style::series::Marker::default(), size_data: None, color_data: None, + color_cats_to_legend: false, } } @@ -413,6 +416,13 @@ impl Scatter { self } + /// Configure this series so that the each category in the color data contributes + /// to legend entries + pub fn with_color_cats_to_legend(mut self) -> Self { + self.color_cats_to_legend = true; + self + } + /// Get the x data column pub fn x_data(&self) -> &DataCol { &self.x_data @@ -453,6 +463,11 @@ impl Scatter { self.color_data.as_ref().map(|(data, cmap)| (data, cmap)) } + /// Check whether the color categories contribute to the legend + pub fn color_cats_to_legend(&self) -> bool { + self.color_cats_to_legend + } + /// Chaining helper to build a plot from this series /// This can only be used if your plot contains a single series. /// This is equivalent to `Plot::new(vec![self.into()])` @@ -462,8 +477,8 @@ impl Scatter { /// use plotive::des; /// use plotive::des::series::{self, data_src_ref}; /// - /// let fig: des::Figure = series::Line::new(data_src_ref("x_values"), data_src_ref("y_values")) - /// .with_name("Line Series") + /// let fig: des::Figure = series::Scatter::new(data_src_ref("x_values"), data_src_ref("y_values")) + /// .with_name("Scatter Series") /// .into_plot() /// .with_x_axis(des::Axis::new().with_ticks(Default::default())) /// .with_y_axis(des::Axis::new().with_ticks(Default::default()).with_grid(Default::default())) diff --git a/src/drawing.rs b/src/drawing.rs index 6e8b662..88ffd05 100644 --- a/src/drawing.rs +++ b/src/drawing.rs @@ -396,6 +396,10 @@ impl Categories { self.cats.iter().map(|c| c.0.as_str()) } + fn into_iter(self) -> impl Iterator { + self.cats.into_iter().map(|c| c.0) + } + fn get(&self, idx: usize) -> Option<&str> { self.cats.get(idx).map(|c| c.0.as_str()) } diff --git a/src/drawing/figure.rs b/src/drawing/figure.rs index aed6277..5e4aee7 100644 --- a/src/drawing/figure.rs +++ b/src/drawing/figure.rs @@ -136,12 +136,22 @@ where self.fontdb(), ); + let data_source = self.data_source(); for plot in fig.plots().iter().filter_map(|p| p) { let mut idx = 0; - plot::for_each_series(plot, |s| { - if let Some(entry) = s.legend_entry() { - builder.add_entry(idx, entry)?; - idx += 1; + plot::for_each_legend_entries(plot, data_source, |entries| { + match entries { + legend::Entries::Single(entry) => { + builder.add_entry(idx, entry)?; + idx += 1; + } + legend::Entries::Multi(entries) => { + for entry in entries { + builder.add_entry(idx, entry)?; + idx += 1; + } + } + legend::Entries::None => {} } Ok(()) })?; diff --git a/src/drawing/legend.rs b/src/drawing/legend.rs index 9c865d1..babd8bf 100644 --- a/src/drawing/legend.rs +++ b/src/drawing/legend.rs @@ -16,46 +16,26 @@ pub enum Shape { }, } -#[derive(Debug, Clone, Copy)] -pub enum ShapeRef<'a> { - Line(&'a style::series::Stroke, Option<&'a style::series::Marker>), - Marker(&'a style::series::Marker), - Rect( - Option<&'a style::series::Fill>, - Option<&'a style::series::Stroke>, - ), - AreaRect { - fill: Option<&'a style::series::Fill>, - y1_stroke: Option<&'a style::series::Stroke>, - y2_stroke: Option<&'a style::series::Stroke>, - }, +/// A legend entry, used to populate the legend +#[derive(Debug, Clone)] +pub struct Entry { + pub label: String, + pub txt_props: Option>, + pub shape: Shape, } -impl ShapeRef<'_> { - pub fn to_shape(&self) -> Shape { - match self { - &ShapeRef::Line(line, marker) => Shape::Line(line.clone(), marker.cloned()), - &ShapeRef::Marker(marker) => Shape::Marker(marker.clone()), - &ShapeRef::Rect(fill, line) => Shape::Rect(fill.cloned(), line.cloned()), - &ShapeRef::AreaRect { - fill, - y1_stroke, - y2_stroke, - } => Shape::AreaRect { - fill: fill.cloned(), - y1_stroke: y1_stroke.cloned(), - y2_stroke: y2_stroke.cloned(), - }, - } - } +#[derive(Debug, Clone, Default)] +pub enum Entries { + #[default] + None, + Single(Entry), + Multi(Vec), } -/// A legend entry, used to populate the legend -#[derive(Debug, Clone)] -pub struct Entry<'a> { - pub label: &'a str, - pub txt_props: Option<&'a text::TextProps>, - pub shape: ShapeRef<'a>, +impl From for Entries { + fn from(entry: Entry) -> Self { + Entries::Single(entry) + } } /// A legend entry, as built during setup phase @@ -127,8 +107,8 @@ impl<'a> LegendBuilder<'a> { } pub fn add_entry(&mut self, index: usize, entry: Entry) -> Result<(), drawing::Error> { - let shape = entry.shape.to_shape(); - let txt_props = entry.txt_props.unwrap_or(&self.txt_props); + let shape = entry.shape; + let txt_props = entry.txt_props.as_ref().unwrap_or_else(|| &self.txt_props); let font = super::resolve_line_font(txt_props, Default::default()); let font_size = txt_props.size.unwrap_or(defaults::LEGEND_LABEL_FONT_SIZE); let fill = txt_props diff --git a/src/drawing/plot.rs b/src/drawing/plot.rs index fcafa65..e0d8b1a 100644 --- a/src/drawing/plot.rs +++ b/src/drawing/plot.rs @@ -4,14 +4,14 @@ use std::f32; use std::rc::Rc; use std::sync::Arc; -use crate::des::{PlotIdx, annot, colorbar}; +use crate::des::{PlotIdx, annot}; use crate::drawing::annot::Annot; use crate::drawing::axis::{ AsBoundRef, Axis, AxisCacheKey, AxisCacheMap, AxisScale, Bounds, Orientation, Side, }; use crate::drawing::cmap::ColorMap; -use crate::drawing::colorbar::{ColorBar, ColorBarBuilder}; -use crate::drawing::legend::{Legend, LegendBuilder}; +use crate::drawing::colorbar::{self, ColorBar, ColorBarBuilder}; +use crate::drawing::legend::{self, Legend, LegendBuilder}; use crate::drawing::scale::CoordMapXy; use crate::drawing::series::{self, Series, SeriesExt}; use crate::drawing::{ColumnExt, Ctx, Error, get_column}; @@ -595,11 +595,21 @@ where self.fontdb(), ); + let data_source = self.data_source(); let mut idx = 0; - for_each_series(des_plot, |s| { - if let Some(entry) = s.legend_entry() { - builder.add_entry(idx, entry)?; - idx += 1; + for_each_legend_entries(des_plot, data_source, |entries| { + match entries { + legend::Entries::Single(entry) => { + builder.add_entry(idx, entry)?; + idx += 1; + } + legend::Entries::Multi(entries) => { + for entry in entries { + builder.add_entry(idx, entry)?; + idx += 1; + } + } + legend::Entries::None => {} } Ok(()) })?; @@ -615,30 +625,28 @@ where let mut builders: Vec = Vec::new(); - for_each_series(des_plot, |s| { - if let Some(entry) = s.colorbar_entry() { - let col = get_column(entry.data_col, self.data_source())?; - let bounds = col - .bounds() - .expect("Should get bounds for colormap data column"); - - let locator = des_colorbar - .map(|cb| cb.ticks_locator()) - .cloned() - .unwrap_or_default(); - - let hash = entry.cmap_build.hash(bounds.as_bound_ref()); - - if let Some(cbb) = builders.iter_mut().find(|b| b.hash() == hash) { - cbb.unite_bounds(bounds.as_bound_ref())?; - } else { - builders.push(ColorBarBuilder::new( - entry.cmap_build, - hash, - bounds, - locator, - )?); - } + for_each_colorbar_entry(des_plot, |entry| { + let col = get_column(entry.data_col, self.data_source())?; + let bounds = col + .bounds() + .expect("Should get bounds for colormap data column"); + + let locator = des_colorbar + .map(|cb| cb.ticks_locator()) + .cloned() + .unwrap_or_default(); + + let hash = entry.cmap_build.hash(bounds.as_bound_ref()); + + if let Some(cbb) = builders.iter_mut().find(|b| b.hash() == hash) { + cbb.unite_bounds(bounds.as_bound_ref())?; + } else { + builders.push(ColorBarBuilder::new( + entry.cmap_build, + hash, + bounds, + locator, + )?); } Ok(()) })?; @@ -962,20 +970,68 @@ where } } -pub fn for_each_series(plot: &des::Plot, mut f: F) -> Result<(), Error> +pub fn for_each_legend_entries( + plot: &des::Plot, + data_source: &D, + mut f: F, +) -> Result<(), Error> where - F: FnMut(&dyn SeriesExt) -> Result<(), Error>, + D: data::Source + ?Sized, + F: FnMut(legend::Entries) -> Result<(), Error>, { for s in plot.series() { match &s { - des::Series::Line(line) => f(line)?, - des::Series::Scatter(scatter) => f(scatter)?, - des::Series::Area(area) => f(area)?, - des::Series::Histogram(hist) => f(hist)?, - des::Series::Bars(bars) => f(bars)?, + des::Series::Line(line) => f(line.legend_entries(data_source)?)?, + des::Series::Scatter(scatter) => f(scatter.legend_entries(data_source)?)?, + des::Series::Area(area) => f(area.legend_entries(data_source)?)?, + des::Series::Histogram(hist) => f(hist.legend_entries(data_source)?)?, + des::Series::Bars(bars) => f(bars.legend_entries(data_source)?)?, des::Series::BarsGroup(bars_group) => { for bs in bars_group.series() { - f(bs)? + f(bs.legend_entries(data_source)?)? + } + } + } + } + Ok(()) +} + +pub fn for_each_colorbar_entry(plot: &des::Plot, mut f: F) -> Result<(), Error> +where + F: FnMut(colorbar::Entry<'_>) -> Result<(), Error>, +{ + for s in plot.series() { + match &s { + des::Series::Line(line) => { + if let Some(entry) = line.colorbar_entry() { + f(entry)?; + } + } + des::Series::Scatter(scatter) => { + if let Some(entry) = scatter.colorbar_entry() { + f(entry)?; + } + } + des::Series::Area(area) => { + if let Some(entry) = area.colorbar_entry() { + f(entry)?; + } + } + des::Series::Histogram(hist) => { + if let Some(entry) = hist.colorbar_entry() { + f(entry)?; + } + } + des::Series::Bars(bars) => { + if let Some(entry) = bars.colorbar_entry() { + f(entry)?; + } + } + des::Series::BarsGroup(bars_group) => { + for bs in bars_group.series() { + if let Some(entry) = bs.colorbar_entry() { + f(entry)?; + } } } } @@ -1038,18 +1094,18 @@ fn y_side_matches_out_legend_pos(side: des::axis::Side, legend_pos: des::plot::L } } -fn x_side_matches_colorbar_pos(side: des::axis::Side, pos: colorbar::Pos) -> bool { +fn x_side_matches_colorbar_pos(side: des::axis::Side, pos: des::colorbar::Pos) -> bool { match (side, pos) { - (des::axis::Side::Main, colorbar::Pos::Bottom) => true, - (des::axis::Side::Opposite, colorbar::Pos::Top) => true, + (des::axis::Side::Main, des::colorbar::Pos::Bottom) => true, + (des::axis::Side::Opposite, des::colorbar::Pos::Top) => true, _ => false, } } -fn y_side_matches_colorbar_pos(side: des::axis::Side, pos: colorbar::Pos) -> bool { +fn y_side_matches_colorbar_pos(side: des::axis::Side, pos: des::colorbar::Pos) -> bool { match (side, pos) { - (des::axis::Side::Main, colorbar::Pos::Left) => true, - (des::axis::Side::Opposite, colorbar::Pos::Right) => true, + (des::axis::Side::Main, des::colorbar::Pos::Left) => true, + (des::axis::Side::Opposite, des::colorbar::Pos::Right) => true, _ => false, } } diff --git a/src/drawing/series.rs b/src/drawing/series.rs index 062d1ff..c078937 100644 --- a/src/drawing/series.rs +++ b/src/drawing/series.rs @@ -15,29 +15,101 @@ use crate::{Style, data, des, geom, render, style}; /// trait implemented by series, or any other item that /// has to populate the legend pub trait SeriesExt { - fn legend_entry(&self) -> Option>; + fn legend_entries(&self, data_source: &D) -> Result + where + D: data::Source + ?Sized; + fn colorbar_entry(&self) -> Option> { None } } impl SeriesExt for des::series::Line { - fn legend_entry(&self) -> Option> { - self.name().map(|n| legend::Entry { - label: n.as_ref(), - txt_props: None, - shape: legend::ShapeRef::Line(self.stroke(), self.marker()), - }) + fn legend_entries(&self, _data_source: &D) -> Result + where + D: data::Source + ?Sized, + { + Ok(self + .name() + .map(|n| { + legend::Entry { + label: n.to_string(), + txt_props: None, + shape: legend::Shape::Line(self.stroke().clone(), self.marker().cloned()), + } + .into() + }) + .unwrap_or_default()) } } impl SeriesExt for des::series::Scatter { - fn legend_entry(&self) -> Option> { - self.name().map(|n| legend::Entry { - label: n.as_ref(), - txt_props: None, - shape: legend::ShapeRef::Marker(self.marker()), - }) + fn legend_entries(&self, data_source: &D) -> Result + where + D: data::Source + ?Sized, + { + if self.color_cats_to_legend() { + if let Some((color_col, cmap)) = self.color_data() { + match cmap { + des::cmap::ColorMap::Auto + | des::cmap::ColorMap::Cat(des::cmap::CatColorMap::Auto) => { + let col = get_column(color_col, data_source)?; + if let Some(col) = col.str() { + let cats: super::Categories = col.into(); + let entries = cats + .into_iter() + .enumerate() + .map(|(i, cat)| { + let color: style::series::Color = + style::series::IndexColor(i).into(); + legend::Entry { + label: cat, + txt_props: None, + shape: legend::Shape::Marker( + self.marker().clone().with_color(color), + ), + } + }) + .collect(); + return Ok(legend::Entries::Multi(entries)); + } + } + des::cmap::ColorMap::Cat(des::cmap::CatColorMap::Strings(cmap)) => { + let col = get_column(color_col, data_source)?; + if let Some(col) = col.str() { + let cats: super::Categories = col.into(); + let entries = cats + .into_iter() + .filter_map(|cat| { + let color = cmap.get(&cat)?; + Some(legend::Entry { + label: cat, + txt_props: None, + shape: legend::Shape::Marker( + self.marker().clone().with_color(*color), + ), + }) + }) + .collect(); + return Ok(legend::Entries::Multi(entries)); + } + } + _ => (), + } + } + } + + Ok(self + .name() + .map(|n| { + legend::Entry { + label: n.to_string(), + txt_props: None, + shape: legend::Shape::Marker(self.marker().clone()), + } + .into() + }) + .unwrap_or_default()) } fn colorbar_entry(&self) -> Option> { @@ -50,46 +122,82 @@ impl SeriesExt for des::series::Scatter { } impl SeriesExt for des::series::Area { - fn legend_entry(&self) -> Option> { - self.name().map(|n| legend::Entry { - label: n.as_ref(), - txt_props: None, - shape: legend::ShapeRef::AreaRect { - fill: Some(self.fill()), - y1_stroke: self.y1_stroke(), - y2_stroke: self.y2_stroke(), - }, - }) + fn legend_entries(&self, _data_source: &D) -> Result + where + D: data::Source + ?Sized, + { + Ok(self + .name() + .map(|n| { + legend::Entry { + label: n.to_string(), + txt_props: None, + shape: legend::Shape::AreaRect { + fill: Some(self.fill().clone()), + y1_stroke: self.y1_stroke().cloned(), + y2_stroke: self.y2_stroke().cloned(), + }, + } + .into() + }) + .unwrap_or_default()) } } impl SeriesExt for des::series::Histogram { - fn legend_entry(&self) -> Option> { - self.name().map(|n| legend::Entry { - label: n.as_ref(), - txt_props: None, - shape: legend::ShapeRef::Rect(Some(self.fill()), self.stroke()), - }) + fn legend_entries(&self, _data_source: &D) -> Result + where + D: data::Source + ?Sized, + { + Ok(self + .name() + .map(|n| { + legend::Entry { + label: n.to_string(), + txt_props: None, + shape: legend::Shape::Rect(Some(self.fill().clone()), self.stroke().cloned()), + } + .into() + }) + .unwrap_or_default()) } } impl SeriesExt for des::series::Bars { - fn legend_entry(&self) -> Option> { - self.name().map(|n| legend::Entry { - label: n.as_ref(), - txt_props: None, - shape: legend::ShapeRef::Rect(Some(self.fill()), self.stroke()), - }) + fn legend_entries(&self, _data_source: &D) -> Result + where + D: data::Source + ?Sized, + { + Ok(self + .name() + .map(|n| { + legend::Entry { + label: n.to_string(), + txt_props: None, + shape: legend::Shape::Rect(Some(self.fill().clone()), self.stroke().cloned()), + } + .into() + }) + .unwrap_or_default()) } } impl SeriesExt for des::series::BarSeries { - fn legend_entry(&self) -> Option> { - self.name().map(|n| legend::Entry { - label: n.as_ref(), - txt_props: None, - shape: legend::ShapeRef::Rect(Some(self.fill()), self.outline()), - }) + fn legend_entries(&self, _data_source: &D) -> Result + where + D: data::Source + ?Sized, + { + Ok(self + .name() + .map(|n| { + legend::Entry { + label: n.to_string(), + txt_props: None, + shape: legend::Shape::Rect(Some(self.fill().clone()), self.outline().cloned()), + } + .into() + }) + .unwrap_or_default()) } } diff --git a/src/sd/series.rs b/src/sd/series.rs index d62eb97..13e9248 100644 --- a/src/sd/series.rs +++ b/src/sd/series.rs @@ -454,6 +454,10 @@ impl serde::Serialize for series::Scatter { } } + if self.color_cats_to_legend() { + state.serialize_entry("colorCatsToLegend", &true)?; + } + state.end() } } @@ -478,6 +482,7 @@ where "name" => name: Option, "xAxis" => x_axis: Option, "yAxis" => y_axis: Option, + "colorCatsToLegend" => color_cats_to_legend: Option, } let mut scatter = series::Scatter::new(x_data, y_data); @@ -505,6 +510,9 @@ where if let Some(y_axis) = y_axis { scatter = scatter.with_y_axis(y_axis); } + if color_cats_to_legend == Some(true) { + scatter = scatter.with_color_cats_to_legend(); + } Ok(scatter) } diff --git a/tests/figs/legend/scatter-color-cats.json b/tests/figs/legend/scatter-color-cats.json new file mode 100644 index 0000000..21a50ea --- /dev/null +++ b/tests/figs/legend/scatter-color-cats.json @@ -0,0 +1,13 @@ +{ + "size": [400, 300], + "plot": { + "series": { + "type": "scatter", + "x": [1, 2, 3, 4, 5], + "y": [1, 4, 9, 16, 25], + "colors": ["cat1", "cat2", "cat1", "cat3", "cat2"], + "colorCatsToLegend": true + }, + "legend": "auto" + } +} diff --git a/tests/refs/series/area-double-legend.png b/tests/refs/legend/area-double.png similarity index 100% rename from tests/refs/series/area-double-legend.png rename to tests/refs/legend/area-double.png diff --git a/tests/refs/series/area-double-legend.svg b/tests/refs/legend/area-double.svg similarity index 100% rename from tests/refs/series/area-double-legend.svg rename to tests/refs/legend/area-double.svg diff --git a/tests/refs/legend-pos/bottom.png b/tests/refs/legend/pos-bottom.png similarity index 100% rename from tests/refs/legend-pos/bottom.png rename to tests/refs/legend/pos-bottom.png diff --git a/tests/refs/legend-pos/bottom.svg b/tests/refs/legend/pos-bottom.svg similarity index 100% rename from tests/refs/legend-pos/bottom.svg rename to tests/refs/legend/pos-bottom.svg diff --git a/tests/refs/legend-pos/in_bottom.png b/tests/refs/legend/pos-in_bottom.png similarity index 100% rename from tests/refs/legend-pos/in_bottom.png rename to tests/refs/legend/pos-in_bottom.png diff --git a/tests/refs/legend-pos/in_bottom.svg b/tests/refs/legend/pos-in_bottom.svg similarity index 100% rename from tests/refs/legend-pos/in_bottom.svg rename to tests/refs/legend/pos-in_bottom.svg diff --git a/tests/refs/legend-pos/in_bottom_left.png b/tests/refs/legend/pos-in_bottom_left.png similarity index 100% rename from tests/refs/legend-pos/in_bottom_left.png rename to tests/refs/legend/pos-in_bottom_left.png diff --git a/tests/refs/legend-pos/in_bottom_left.svg b/tests/refs/legend/pos-in_bottom_left.svg similarity index 100% rename from tests/refs/legend-pos/in_bottom_left.svg rename to tests/refs/legend/pos-in_bottom_left.svg diff --git a/tests/refs/legend-pos/in_bottom_right.png b/tests/refs/legend/pos-in_bottom_right.png similarity index 100% rename from tests/refs/legend-pos/in_bottom_right.png rename to tests/refs/legend/pos-in_bottom_right.png diff --git a/tests/refs/legend-pos/in_bottom_right.svg b/tests/refs/legend/pos-in_bottom_right.svg similarity index 100% rename from tests/refs/legend-pos/in_bottom_right.svg rename to tests/refs/legend/pos-in_bottom_right.svg diff --git a/tests/refs/legend-pos/in_left.png b/tests/refs/legend/pos-in_left.png similarity index 100% rename from tests/refs/legend-pos/in_left.png rename to tests/refs/legend/pos-in_left.png diff --git a/tests/refs/legend-pos/in_left.svg b/tests/refs/legend/pos-in_left.svg similarity index 100% rename from tests/refs/legend-pos/in_left.svg rename to tests/refs/legend/pos-in_left.svg diff --git a/tests/refs/legend-pos/in_right.png b/tests/refs/legend/pos-in_right.png similarity index 100% rename from tests/refs/legend-pos/in_right.png rename to tests/refs/legend/pos-in_right.png diff --git a/tests/refs/legend-pos/in_right.svg b/tests/refs/legend/pos-in_right.svg similarity index 100% rename from tests/refs/legend-pos/in_right.svg rename to tests/refs/legend/pos-in_right.svg diff --git a/tests/refs/legend-pos/in_top.png b/tests/refs/legend/pos-in_top.png similarity index 100% rename from tests/refs/legend-pos/in_top.png rename to tests/refs/legend/pos-in_top.png diff --git a/tests/refs/legend-pos/in_top.svg b/tests/refs/legend/pos-in_top.svg similarity index 100% rename from tests/refs/legend-pos/in_top.svg rename to tests/refs/legend/pos-in_top.svg diff --git a/tests/refs/legend-pos/in_top_left.png b/tests/refs/legend/pos-in_top_left.png similarity index 100% rename from tests/refs/legend-pos/in_top_left.png rename to tests/refs/legend/pos-in_top_left.png diff --git a/tests/refs/legend-pos/in_top_left.svg b/tests/refs/legend/pos-in_top_left.svg similarity index 100% rename from tests/refs/legend-pos/in_top_left.svg rename to tests/refs/legend/pos-in_top_left.svg diff --git a/tests/refs/legend-pos/in_top_right.png b/tests/refs/legend/pos-in_top_right.png similarity index 100% rename from tests/refs/legend-pos/in_top_right.png rename to tests/refs/legend/pos-in_top_right.png diff --git a/tests/refs/legend-pos/in_top_right.svg b/tests/refs/legend/pos-in_top_right.svg similarity index 100% rename from tests/refs/legend-pos/in_top_right.svg rename to tests/refs/legend/pos-in_top_right.svg diff --git a/tests/refs/legend-pos/left.png b/tests/refs/legend/pos-left.png similarity index 100% rename from tests/refs/legend-pos/left.png rename to tests/refs/legend/pos-left.png diff --git a/tests/refs/legend-pos/left.svg b/tests/refs/legend/pos-left.svg similarity index 100% rename from tests/refs/legend-pos/left.svg rename to tests/refs/legend/pos-left.svg diff --git a/tests/refs/legend-pos/right.png b/tests/refs/legend/pos-right.png similarity index 100% rename from tests/refs/legend-pos/right.png rename to tests/refs/legend/pos-right.png diff --git a/tests/refs/legend-pos/right.svg b/tests/refs/legend/pos-right.svg similarity index 100% rename from tests/refs/legend-pos/right.svg rename to tests/refs/legend/pos-right.svg diff --git a/tests/refs/legend-pos/top.png b/tests/refs/legend/pos-top.png similarity index 100% rename from tests/refs/legend-pos/top.png rename to tests/refs/legend/pos-top.png diff --git a/tests/refs/legend-pos/top.svg b/tests/refs/legend/pos-top.svg similarity index 100% rename from tests/refs/legend-pos/top.svg rename to tests/refs/legend/pos-top.svg diff --git a/tests/refs/legend/scatter-color-cats.png b/tests/refs/legend/scatter-color-cats.png new file mode 100644 index 0000000..2ca7aaf Binary files /dev/null and b/tests/refs/legend/scatter-color-cats.png differ diff --git a/tests/refs/legend/scatter-color-cats.svg b/tests/refs/legend/scatter-color-cats.svg new file mode 100644 index 0000000..d1e7013 --- /dev/null +++ b/tests/refs/legend/scatter-color-cats.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/src/tests/legend.rs b/tests/src/tests/legend.rs index 79404e6..8e8e0b0 100644 --- a/tests/src/tests/legend.rs +++ b/tests/src/tests/legend.rs @@ -1,4 +1,4 @@ -use plotive::des; +use plotive::{Style, des, style}; use super::{fig_small, line}; use crate::{TestHarness, assert_fig_eq_ref}; @@ -9,7 +9,7 @@ fn legend_pos_default() { let plot = des::Plot::new(vec![series]).with_legend(Default::default()); let fig = fig_small(plot); - assert_fig_eq_ref!(&fig, "legend-pos/bottom"); + assert_fig_eq_ref!(&fig, "legend/pos-bottom"); } #[test] @@ -18,7 +18,7 @@ fn legend_pos_top() { let plot = des::Plot::new(vec![series]).with_legend(des::plot::LegendPos::OutTop.into()); let fig = fig_small(plot); - assert_fig_eq_ref!(&fig, "legend-pos/top"); + assert_fig_eq_ref!(&fig, "legend/pos-top"); } #[test] @@ -27,7 +27,7 @@ fn legend_pos_right() { let plot = des::Plot::new(vec![series]).with_legend(des::plot::LegendPos::OutRight.into()); let fig = fig_small(plot); - assert_fig_eq_ref!(&fig, "legend-pos/right"); + assert_fig_eq_ref!(&fig, "legend/pos-right"); } #[test] @@ -36,7 +36,7 @@ fn legend_pos_bottom() { let plot = des::Plot::new(vec![series]).with_legend(des::plot::LegendPos::OutBottom.into()); let fig = fig_small(plot); - assert_fig_eq_ref!(&fig, "legend-pos/bottom"); + assert_fig_eq_ref!(&fig, "legend/pos-bottom"); } #[test] @@ -45,7 +45,7 @@ fn legend_pos_left() { let plot = des::Plot::new(vec![series]).with_legend(des::plot::LegendPos::OutLeft.into()); let fig = fig_small(plot); - assert_fig_eq_ref!(&fig, "legend-pos/left"); + assert_fig_eq_ref!(&fig, "legend/pos-left"); } #[test] @@ -54,7 +54,7 @@ fn legend_pos_in_top_left() { let plot = des::Plot::new(vec![series]).with_legend(des::plot::LegendPos::InTopLeft.into()); let fig = fig_small(plot); - assert_fig_eq_ref!(&fig, "legend-pos/in_top_left"); + assert_fig_eq_ref!(&fig, "legend/pos-in_top_left"); } #[test] @@ -63,7 +63,7 @@ fn legend_pos_in_top() { let plot = des::Plot::new(vec![series]).with_legend(des::plot::LegendPos::InTop.into()); let fig = fig_small(plot); - assert_fig_eq_ref!(&fig, "legend-pos/in_top"); + assert_fig_eq_ref!(&fig, "legend/pos-in_top"); } #[test] @@ -72,7 +72,7 @@ fn legend_pos_in_top_right() { let plot = des::Plot::new(vec![series]).with_legend(des::plot::LegendPos::InTopRight.into()); let fig = fig_small(plot); - assert_fig_eq_ref!(&fig, "legend-pos/in_top_right"); + assert_fig_eq_ref!(&fig, "legend/pos-in_top_right"); } #[test] @@ -81,7 +81,7 @@ fn legend_pos_in_right() { let plot = des::Plot::new(vec![series]).with_legend(des::plot::LegendPos::InRight.into()); let fig = fig_small(plot); - assert_fig_eq_ref!(&fig, "legend-pos/in_right"); + assert_fig_eq_ref!(&fig, "legend/pos-in_right"); } #[test] @@ -90,7 +90,7 @@ fn legend_pos_in_bottom_right() { let plot = des::Plot::new(vec![series]).with_legend(des::plot::LegendPos::InBottomRight.into()); let fig = fig_small(plot); - assert_fig_eq_ref!(&fig, "legend-pos/in_bottom_right"); + assert_fig_eq_ref!(&fig, "legend/pos-in_bottom_right"); } #[test] @@ -99,7 +99,7 @@ fn legend_pos_in_bottom() { let plot = des::Plot::new(vec![series]).with_legend(des::plot::LegendPos::InBottom.into()); let fig = fig_small(plot); - assert_fig_eq_ref!(&fig, "legend-pos/in_bottom"); + assert_fig_eq_ref!(&fig, "legend/pos-in_bottom"); } #[test] @@ -108,7 +108,7 @@ fn legend_pos_in_bottom_left() { let plot = des::Plot::new(vec![series]).with_legend(des::plot::LegendPos::InBottomLeft.into()); let fig = fig_small(plot); - assert_fig_eq_ref!(&fig, "legend-pos/in_bottom_left"); + assert_fig_eq_ref!(&fig, "legend/pos-in_bottom_left"); } #[test] @@ -117,5 +117,48 @@ fn legend_pos_in_left() { let plot = des::Plot::new(vec![series]).with_legend(des::plot::LegendPos::InLeft.into()); let fig = fig_small(plot); - assert_fig_eq_ref!(&fig, "legend-pos/in_left"); + assert_fig_eq_ref!(&fig, "legend/pos-in_left"); +} + +#[test] +fn legend_area_double() { + let x = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0]; + let y1 = vec![10.0, 15.0, 8.0, 6.0, 12.0, 10.0]; + let y2 = vec![4.0, 9.0, 2.0, 0.0, 6.0, 4.0]; + + let fill1 = style::series::Fill::solid(plotive::Rgba8::from_hex(b"#888").into()); + let fill2 = style::series::Fill::solid(plotive::Rgba8::from_hex(b"#444").into()); + let stroke = style::series::Stroke::solid(plotive::Rgba8::from_hex(b"#000").into()); + + let plot = des::Plot::new(vec![ + des::series::Area::new( + des::data_inline(x.clone()), + des::data_inline(y1.clone()), + des::data_inline(y2.clone()).into(), + ) + .with_name("area1") + .with_fill(fill1) + .with_y1_stroke(stroke.clone()) + .with_y2_stroke(stroke.clone()) + .into(), + des::series::Area::new( + des::data_inline(x.clone()), + des::data_inline(y2.clone()), + Default::default(), + ) + .with_name("area2") + .with_fill(fill2) + .with_y1_stroke(stroke.clone()) + .with_y2_stroke(stroke.clone()) + .into(), + ]); + let fig = fig_small(plot).with_legend(Default::default()); + + assert_fig_eq_ref!(&fig, "legend/area-double"); +} + +#[test] +fn legend_scatter_color_cats() { + let fig: des::Figure = crate::json_figure("legend/scatter-color-cats"); + assert_fig_eq_ref!(&fig, "legend/scatter-color-cats", &Style::light()); } diff --git a/tests/src/tests/series.rs b/tests/src/tests/series.rs index e2f9a49..6bdea83 100644 --- a/tests/src/tests/series.rs +++ b/tests/src/tests/series.rs @@ -166,40 +166,3 @@ fn series_area_double() { assert_fig_eq_ref!(&fig, "series/area-double"); } - -#[test] -fn series_area_double_legend() { - let x = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0]; - let y1 = vec![10.0, 15.0, 8.0, 6.0, 12.0, 10.0]; - let y2 = vec![4.0, 9.0, 2.0, 0.0, 6.0, 4.0]; - - let fill1 = style::series::Fill::solid(plotive::Rgba8::from_hex(b"#888").into()); - let fill2 = style::series::Fill::solid(plotive::Rgba8::from_hex(b"#444").into()); - let stroke = style::series::Stroke::solid(plotive::Rgba8::from_hex(b"#000").into()); - - let plot = des::Plot::new(vec![ - des::series::Area::new( - des::data_inline(x.clone()), - des::data_inline(y1.clone()), - des::data_inline(y2.clone()).into(), - ) - .with_name("area1") - .with_fill(fill1) - .with_y1_stroke(stroke.clone()) - .with_y2_stroke(stroke.clone()) - .into(), - des::series::Area::new( - des::data_inline(x.clone()), - des::data_inline(y2.clone()), - Default::default(), - ) - .with_name("area2") - .with_fill(fill2) - .with_y1_stroke(stroke.clone()) - .with_y2_stroke(stroke.clone()) - .into(), - ]); - let fig = fig_small(plot).with_legend(Default::default()); - - assert_fig_eq_ref!(&fig, "series/area-double-legend"); -}