Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions src/des/series.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -355,6 +356,7 @@ pub struct Scatter {
marker: style::series::Marker,
size_data: Option<DataCol>,
color_data: Option<(DataCol, ColorMap)>,
color_cats_to_legend: bool,
}

impl Scatter {
Expand All @@ -370,6 +372,7 @@ impl Scatter {
marker: style::series::Marker::default(),
size_data: None,
color_data: None,
color_cats_to_legend: false,
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()])`
Expand All @@ -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()))
Expand Down
4 changes: 4 additions & 0 deletions src/drawing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,10 @@ impl Categories {
self.cats.iter().map(|c| c.0.as_str())
}

fn into_iter(self) -> impl Iterator<Item = String> {
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())
}
Expand Down
18 changes: 14 additions & 4 deletions src/drawing/figure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
})?;
Expand Down
56 changes: 18 additions & 38 deletions src/drawing/legend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<text::TextProps<theme::Color>>,
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<Entry>),
}

/// 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<theme::Color>>,
pub shape: ShapeRef<'a>,
impl From<Entry> for Entries {
fn from(entry: Entry) -> Self {
Entries::Single(entry)
}
}

/// A legend entry, as built during setup phase
Expand Down Expand Up @@ -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
Expand Down
146 changes: 101 additions & 45 deletions src/drawing/plot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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(())
})?;
Expand All @@ -615,30 +625,28 @@ where

let mut builders: Vec<ColorBarBuilder> = 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(())
})?;
Expand Down Expand Up @@ -962,20 +970,68 @@ where
}
}

pub fn for_each_series<F>(plot: &des::Plot, mut f: F) -> Result<(), Error>
pub fn for_each_legend_entries<D, F>(
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<F>(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)?;
}
}
}
}
Expand Down Expand Up @@ -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,
}
}
Expand Down
Loading
Loading