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
5 changes: 5 additions & 0 deletions src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,11 @@ pub trait Column: std::fmt::Debug {
/// Get the number of non-null values in the column
fn len_some(&self) -> usize;

/// Check if the column is empty (i.e. has no samples)
fn is_empty(&self) -> bool {
self.len() == 0
}

/// Get an iterator over the samples in the column
fn sample_iter(&self) -> Box<dyn Iterator<Item = SampleRef<'_>> + '_> {
#[cfg(feature = "time")]
Expand Down
3 changes: 3 additions & 0 deletions src/drawing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ pub enum Error {
InconsistentData(String),
/// Font or text related error, e.g. missing glyphs or font not found
FontOrText(text::Error),
/// Not enough space to draw the figure, e.g. due to too many ticks or too long tick labels
NotEnoughSpace,
}

impl From<text::Error> for Error {
Expand Down Expand Up @@ -73,6 +75,7 @@ impl fmt::Display for Error {
}
Error::InconsistentData(reason) => write!(f, "Inconsistent data: {}", reason),
Error::FontOrText(err) => err.fmt(f),
Error::NotEnoughSpace => write!(f, "Not enough space to draw the figure"),
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/drawing/plot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,9 @@ where

// Now we can determine width of horizontal axes and set them all up
let subplot_rect_width = (rect.width() - vert_space_width) / des_plots.cols() as f32;
if subplot_rect_width <= 0.0 || subplot_rect_height <= 0.0 {
return Err(Error::NotEnoughSpace);
}
let x_axes =
self.setup_orientation_axes(Orientation::X, des_plots, &plot_data, subplot_rect_width)?;

Expand Down
156 changes: 118 additions & 38 deletions src/drawing/series.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ fn calc_xy_bounds<D>(
data_source: &D,
x_data: &des::series::DataCol,
y_data: &des::series::DataCol,
) -> Result<(axis::Bounds, axis::Bounds), Error>
) -> Result<Option<(axis::Bounds, axis::Bounds)>, Error>
where
D: data::Source + ?Sized,
{
Expand All @@ -95,10 +95,14 @@ where
));
}

if x_col.is_empty() {
return Ok(None);
}

let x_bounds = x_col.bounds().ok_or(Error::UnboundedAxis)?;
let y_bounds = y_col.bounds().ok_or(Error::UnboundedAxis)?;

Ok((x_bounds, y_bounds))
Ok(Some((x_bounds, y_bounds)))
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -192,9 +196,13 @@ impl Series {
continue;
}

let Some(sb) = s.bounds() else {
continue;
};

let b = match or {
Orientation::X => &s.bounds().0,
Orientation::Y => &s.bounds().1,
Orientation::X => &sb.0,
Orientation::Y => &sb.1,
};

if let Some(a) = &mut a {
Expand All @@ -206,15 +214,21 @@ impl Series {
Ok(a)
}

fn bounds(&self) -> (axis::BoundsRef<'_>, axis::BoundsRef<'_>) {
fn bounds(&self) -> Option<(axis::BoundsRef<'_>, axis::BoundsRef<'_>)> {
match &self.plot {
SeriesPlot::Line(line) => (line.ab.0.as_bound_ref(), line.ab.1.as_bound_ref()),
SeriesPlot::Scatter(scatter) => {
(scatter.ab.0.as_bound_ref(), scatter.ab.1.as_bound_ref())
}
SeriesPlot::Histogram(hist) => (hist.ab.0.into(), hist.ab.1.into()),
SeriesPlot::Line(line) => line
.ab
.as_ref()
.map(|(x, y)| (x.as_bound_ref(), y.as_bound_ref())),
SeriesPlot::Scatter(scatter) => scatter
.ab
.as_ref()
.map(|(x, y)| (x.as_bound_ref(), y.as_bound_ref())),
SeriesPlot::Histogram(hist) => Some((hist.ab.0.into(), hist.ab.1.into())),
SeriesPlot::Bars(bars) => bars.bounds(),
SeriesPlot::BarsGroup(bg) => (bg.bounds.0.as_bound_ref(), bg.bounds.1.as_bound_ref()),
SeriesPlot::BarsGroup(bg) => {
Some((bg.bounds.0.as_bound_ref(), bg.bounds.1.as_bound_ref()))
}
}
}

Expand Down Expand Up @@ -283,7 +297,7 @@ impl Series {
struct Line {
index: usize,
cols: (des::DataCol, des::DataCol),
ab: (axis::Bounds, axis::Bounds),
ab: Option<(axis::Bounds, axis::Bounds)>,
axes: (des::axis::Ref, des::axis::Ref),
path: Option<geom::Path>,
stroke: style::series::Stroke,
Expand Down Expand Up @@ -486,11 +500,11 @@ impl Line {
D: data::Source + ?Sized,
{
let cols = (des.x_data().clone(), des.y_data().clone());
let (x_bounds, y_bounds) = calc_xy_bounds(data_source, &cols.0, &cols.1)?;
let xy_bounds = calc_xy_bounds(data_source, &cols.0, &cols.1)?;
Ok(Line {
index,
cols,
ab: (x_bounds, y_bounds),
ab: xy_bounds,
axes: (des.x_axis().clone(), des.y_axis().clone()),
path: None,
stroke: des.stroke().clone(),
Expand All @@ -508,6 +522,18 @@ impl Line {

debug_assert!(x_col.len() == y_col.len());

if self.ab.is_none() && x_col.is_empty() {
self.path = None;
return;
}

if self.ab.is_none() && !x_col.is_empty() {
let xy_bounds = calc_xy_bounds(data_source, &self.cols.0, &self.cols.1)
.expect("Should be able to calculate bounds for non-empty data")
.expect("Should be able to calculate bounds for non-empty data");
self.ab = Some(xy_bounds);
}

let path = match self.interpolation {
des::series::Interpolation::Linear => {
let mut liner = LinearLiner::new(x_col.len());
Expand Down Expand Up @@ -588,6 +614,10 @@ impl Line {
where
S: render::Surface,
{
if self.path.is_none() {
return;
}

let rc = (style, self.index);

let path = render::Path {
Expand All @@ -604,7 +634,7 @@ impl Line {
struct Scatter {
index: usize,
cols: (des::DataCol, des::DataCol),
ab: (axis::Bounds, axis::Bounds),
ab: Option<(axis::Bounds, axis::Bounds)>,
axes: (des::axis::Ref, des::axis::Ref),
path: geom::Path,
points: Vec<geom::Point>,
Expand All @@ -617,12 +647,12 @@ impl Scatter {
D: data::Source + ?Sized,
{
let cols = (des.x_data().clone(), des.y_data().clone());
let (x_bounds, y_bounds) = calc_xy_bounds(data_source, &cols.0, &cols.1)?;
let xy_bounds = calc_xy_bounds(data_source, &cols.0, &cols.1)?;
let path = marker::marker_path(des.marker());
Ok(Scatter {
index,
cols,
ab: (x_bounds, y_bounds),
ab: xy_bounds,
axes: (des.x_axis().clone(), des.y_axis().clone()),
path,
points: Vec::new(),
Expand All @@ -638,6 +668,18 @@ impl Scatter {
let y_col = get_column(&self.cols.1, data_source).unwrap();
debug_assert!(x_col.len() == y_col.len());

if self.ab.is_none() && x_col.is_empty() {
self.points.clear();
return;
}

if self.ab.is_none() && !x_col.is_empty() {
let xy_bounds = calc_xy_bounds(data_source, &self.cols.0, &self.cols.1)
.expect("Should be able to calculate bounds for non-empty data")
.expect("Should be able to calculate bounds for non-empty data");
self.ab = Some(xy_bounds);
}

let mut points = Vec::with_capacity(x_col.len());

for (x, y) in x_col.sample_iter().zip(y_col.sample_iter()) {
Expand All @@ -656,6 +698,10 @@ impl Scatter {
where
S: render::Surface,
{
if self.points.is_empty() {
return;
}

let rc = (style, self.index);

for p in &self.points {
Expand Down Expand Up @@ -788,11 +834,31 @@ enum BarsBounds {
Horizontal(axis::NumBounds, Categories),
}

impl BarsBounds {
fn calc(x_bounds: axis::Bounds, y_bounds: axis::Bounds) -> Result<Self, Error> {
match (x_bounds, y_bounds) {
(axis::Bounds::Num(mut x_bounds), axis::Bounds::Cat(y_bounds)) => {
x_bounds.add_sample(0.0);
Ok(BarsBounds::Horizontal(x_bounds, y_bounds))
}
(axis::Bounds::Cat(x_bounds), axis::Bounds::Num(mut y_bounds)) => {
y_bounds.add_sample(0.0);
Ok(BarsBounds::Vertical(x_bounds, y_bounds))
}
_ => {
return Err(Error::InconsistentData(
"One of X and Y data must be numeric and the other categorical".to_string(),
));
}
}
}
}

#[derive(Debug, Clone)]
struct Bars {
index: usize,
cols: (des::DataCol, des::DataCol),
bounds: BarsBounds,
bounds: Option<BarsBounds>,
axes: (des::axis::Ref, des::axis::Ref),
position: des::series::BarsPosition,
path: Option<geom::Path>,
Expand All @@ -806,22 +872,12 @@ impl Bars {
D: data::Source + ?Sized,
{
let cols = (des.x_data().clone(), des.y_data().clone());
let (x_bounds, y_bounds) = calc_xy_bounds(data_source, &cols.0, &cols.1)?;
let xy_bounds = calc_xy_bounds(data_source, &cols.0, &cols.1)?;

let bounds = match (x_bounds, y_bounds) {
(axis::Bounds::Num(mut x_bounds), axis::Bounds::Cat(y_bounds)) => {
x_bounds.add_sample(0.0);
BarsBounds::Horizontal(x_bounds, y_bounds)
}
(axis::Bounds::Cat(x_bounds), axis::Bounds::Num(mut y_bounds)) => {
y_bounds.add_sample(0.0);
BarsBounds::Vertical(x_bounds, y_bounds)
}
_ => {
return Err(Error::InconsistentData(
"One of X and Y data must be numeric and the other categorical".to_string(),
));
}
let bounds = if let Some((x_bounds, y_bounds)) = xy_bounds {
Some(BarsBounds::calc(x_bounds, y_bounds)?)
} else {
None
};

Ok(Bars {
Expand All @@ -836,10 +892,14 @@ impl Bars {
})
}

fn bounds(&self) -> (axis::BoundsRef<'_>, axis::BoundsRef<'_>) {
match &self.bounds {
&BarsBounds::Vertical(ref x_bounds, y_bounds) => (x_bounds.into(), y_bounds.into()),
&BarsBounds::Horizontal(x_bounds, ref y_bounds) => (x_bounds.into(), y_bounds.into()),
fn bounds(&self) -> Option<(axis::BoundsRef<'_>, axis::BoundsRef<'_>)> {
match self.bounds.as_ref()? {
&BarsBounds::Vertical(ref x_bounds, y_bounds) => {
Some((x_bounds.into(), y_bounds.into()))
}
&BarsBounds::Horizontal(x_bounds, ref y_bounds) => {
Some((x_bounds.into(), y_bounds.into()))
}
}
}

Expand All @@ -852,9 +912,25 @@ impl Bars {
let y_col = get_column(&self.cols.1, data_source).unwrap();
debug_assert!(x_col.len() == y_col.len());

if self.bounds.is_none() && x_col.is_empty() && y_col.is_empty() {
// no valid data, so no bars to draw
self.path = None;
return;
}

if self.bounds.is_none() {
let xy_bounds = calc_xy_bounds(data_source, &self.cols.0, &self.cols.1)
.expect("Should be able to calculate bounds for non-empty data")
.expect("Should be able to calculate bounds for non-empty data");
self.bounds = Some(
BarsBounds::calc(xy_bounds.0, xy_bounds.1)
.expect("Should be able to calculate bars bounds"),
);
}

let mut pb = geom::PathBuilder::new();

match &self.bounds {
match self.bounds.as_ref().unwrap() {
BarsBounds::Vertical(..) => {
let cat_bin_width = cm.x.cat_bin_size();
let y_start = rect.bottom() - cm.y.map_coord_num(0.0);
Expand Down Expand Up @@ -903,6 +979,10 @@ impl Bars {
where
S: render::Surface,
{
if self.path.is_none() {
return;
}

let rc = (style, self.index);

let path = render::Path {
Expand Down
Binary file added tests/refs/series/line-nodata.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions tests/refs/series/line-nodata.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/refs/series/scatter-nodata.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions tests/refs/series/scatter-nodata.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions tests/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ mod axes;
mod interp;
mod legend;
mod nulls;
mod series;
mod subplots;

#[test]
Expand Down
10 changes: 10 additions & 0 deletions tests/src/tests/axes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ fn axes_default() {
assert_fig_eq_ref!(&fig, "axes/default");
}

// #[test]
// fn axes_ticks_empty() {
// let plot = des::Plot::new(vec![])
// .with_x_axis(des::Axis::new().with_ticks(Default::default()))
// .with_y_axis(des::Axis::new().with_ticks(Default::default()));
// let fig = fig_small(plot);

// assert_fig_eq_ref!(&fig, "axes/ticks-empty");
// }

#[test]
fn axes_x_title() {
let series = line().into();
Expand Down
Loading
Loading