From 65484955c94e3e6ba9512d856ccb1c6004c7e984 Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Thu, 28 Feb 2019 22:49:53 +0000 Subject: [PATCH 1/6] Add support for grids This support is basic so far, and only for continuous views for now. The user can choose how many grid points they wish to use for each axis (defaults to 3 each), and the colour of the grid lines (defaults to a grey colour). --- examples/with_grid.rs | 16 ++++++++ src/grid.rs | 21 +++++++++++ src/lib.rs | 1 + src/svg_render.rs | 85 +++++++++++++++++++++++++++++++++---------- src/view.rs | 28 ++++++++++++++ 5 files changed, 131 insertions(+), 20 deletions(-) create mode 100644 examples/with_grid.rs create mode 100644 src/grid.rs diff --git a/examples/with_grid.rs b/examples/with_grid.rs new file mode 100644 index 0000000..7b24a18 --- /dev/null +++ b/examples/with_grid.rs @@ -0,0 +1,16 @@ +extern crate plotlib; + +use plotlib::grid::Grid; +use plotlib::style::Line; +use plotlib::view::View; + +fn main() { + let l1 = plotlib::line::Line::new(&[(0., 1.), (2., 1.5), (3., 1.2), (4., 1.1)]) + .style(plotlib::line::Style::new().colour("burlywood")); + let grid = Grid::new(3, 8); + let mut v = plotlib::view::ContinuousView::new().add(&l1); + v.add_grid(grid); + plotlib::page::Page::single(&v) + .save("line.svg") + .expect("saving svg"); +} diff --git a/src/grid.rs b/src/grid.rs new file mode 100644 index 0000000..53b9493 --- /dev/null +++ b/src/grid.rs @@ -0,0 +1,21 @@ +pub struct Grid { + pub nx: u32, + pub ny: u32, + pub color: String, +} + +impl Default for Grid { + fn default() -> Self { + Grid::new(3, 3) + } +} + +impl Grid { + pub fn new(nx: u32, ny: u32) -> Grid { + Grid { + nx, + ny, + color: "darkgrey".to_owned(), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 7d021fd..6e0b2b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,6 +88,7 @@ pub mod barchart; pub mod boxplot; mod errors; pub mod function; +pub mod grid; pub mod histogram; pub mod line; pub mod scatter; diff --git a/src/svg_render.rs b/src/svg_render.rs index 94eaab7..1b8d672 100644 --- a/src/svg_render.rs +++ b/src/svg_render.rs @@ -4,6 +4,7 @@ use svg::node; use svg::Node; use crate::axis; +use crate::grid::Grid; use crate::histogram; use crate::style; use crate::utils; @@ -14,14 +15,34 @@ fn value_to_face_offset(value: f64, axis: &axis::ContinuousAxis, face_size: f64) (face_size * (value - axis.min())) / range } +fn vertical_line(xpos: f64, ymin: f64, ymax: f64, color: S) -> node::element::Line +where + S: AsRef, +{ + node::element::Line::new() + .set("x1", xpos) + .set("x2", xpos) + .set("y1", ymin) + .set("y2", ymax) + .set("stroke", color.as_ref()) + .set("stroke-width", 1) +} + +fn horizontal_line(ypos: f64, xmin: f64, xmax: f64, color: S) -> node::element::Line +where + S: AsRef, +{ + node::element::Line::new() + .set("x1", xmin) + .set("x2", xmax) + .set("y1", ypos) + .set("y2", ypos) + .set("stroke", color.as_ref()) + .set("stroke-width", 1) +} + pub fn draw_x_axis(a: &axis::ContinuousAxis, face_width: f64) -> node::element::Group { - let axis_line = node::element::Line::new() - .set("x1", 0) - .set("y1", 0) - .set("x2", face_width) - .set("y2", 0) - .set("stroke", "black") - .set("stroke-width", 1); + let axis_line = horizontal_line(0.0, 0.0, face_width, "black"); let mut ticks = node::element::Group::new(); let mut labels = node::element::Group::new(); @@ -61,13 +82,7 @@ pub fn draw_x_axis(a: &axis::ContinuousAxis, face_width: f64) -> node::element:: } pub fn draw_y_axis(a: &axis::ContinuousAxis, face_height: f64) -> node::element::Group { - let axis_line = node::element::Line::new() - .set("x1", 0) - .set("y1", 0) - .set("x2", 0) - .set("y2", -face_height) - .set("stroke", "black") - .set("stroke-0", 1); + let axis_line = vertical_line(0.0, 0.0, -face_height, "black"); let mut ticks = node::element::Group::new(); let mut labels = node::element::Group::new(); @@ -101,7 +116,8 @@ pub fn draw_y_axis(a: &axis::ContinuousAxis, face_height: f64) -> node::element: .set( "transform", format!("rotate(-90 {} {})", -30, -(face_height / 2.)), - ).add(node::Text::new(a.get_label())); + ) + .add(node::Text::new(a.get_label())); node::element::Group::new() .add(ticks) @@ -214,7 +230,8 @@ where .set( "stroke", style.get_colour().clone().unwrap_or_else(|| "".into()), - ).set("stroke-width", 2) + ) + .set("stroke-width", 2) .set("d", path), ); } @@ -253,7 +270,8 @@ where .get_fill() .clone() .unwrap_or_else(|| "burlywood".into()), - ).set("stroke", "black"); + ) + .set("stroke", "black"); group.append(rect); } @@ -298,7 +316,8 @@ where .set( "stroke", style.get_colour().clone().unwrap_or_else(|| "".into()), - ).set("stroke-width", style.get_width().clone().unwrap_or(2.)) + ) + .set("stroke-width", style.get_width().clone().unwrap_or(2.)) .set("d", path), ); @@ -344,7 +363,8 @@ where .get_fill() .clone() .unwrap_or_else(|| "burlywood".into()), - ).set("stroke", "black"), + ) + .set("stroke", "black"), ); let mid_line = -value_to_face_offset(median, y_axis, face_height); @@ -421,12 +441,37 @@ where .get_fill() .clone() .unwrap_or_else(|| "burlywood".into()), - ).set("stroke", "black"), + ) + .set("stroke", "black"), ); group } +pub(crate) fn draw_grid(grid: &Grid, face_width: f64, face_height: f64) -> node::element::Group { + let (xmin, xmax) = (0f64, face_width); + let (ymin, ymax) = (0f64, face_height); + + let x_step = (xmax - xmin) / f64::from(grid.nx); + let y_step = (ymax - ymin) / f64::from(grid.ny); + + let mut lines = node::element::Group::new(); + + for iy in 0..=grid.ny { + let y = f64::from(iy) * y_step + ymin; + let line = horizontal_line(-y, 0.0, face_width, grid.color.as_str()); + lines = lines.add(line); + } + + for ix in 0..=grid.nx { + let x = f64::from(ix) * x_step + xmin; + let line = vertical_line(x, 0.0, -face_height, grid.color.as_str()); + lines = lines.add(line); + } + + lines +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/view.rs b/src/view.rs index 14fdc93..a30d1ba 100644 --- a/src/view.rs +++ b/src/view.rs @@ -14,6 +14,7 @@ use svg::Node; use crate::axis; use crate::errors::Result; +use crate::grid::Grid; use crate::representation::{CategoricalRepresentation, ContinuousRepresentation}; use crate::svg_render; use crate::text_render; @@ -21,6 +22,8 @@ use crate::text_render; pub trait View { fn to_svg(&self, face_width: f64, face_height: f64) -> Result; fn to_text(&self, face_width: u32, face_height: u32) -> Result; + fn add_grid(&mut self, grid: Grid); + fn grid<'a>(&'a self) -> &'a Option; } /// Standard 1-dimensional view with a continuous x-axis @@ -31,6 +34,7 @@ pub struct ContinuousView<'a> { y_range: Option, x_label: Option, y_label: Option, + grid: Option, } impl<'a> ContinuousView<'a> { @@ -44,6 +48,7 @@ impl<'a> ContinuousView<'a> { y_range: None, x_label: None, y_label: None, + grid: None, } } @@ -164,6 +169,11 @@ impl<'a> View for ContinuousView<'a> { // Add in the axes view_group.append(svg_render::draw_x_axis(&x_axis, face_width)); view_group.append(svg_render::draw_y_axis(&y_axis, face_height)); + + if let Some(grid) = &self.grid { + view_group.append(svg_render::draw_grid(grid, face_width, face_height)); + } + Ok(view_group) } @@ -212,6 +222,14 @@ impl<'a> View for ContinuousView<'a> { Ok(view_string) } + + fn add_grid(&mut self, grid: Grid) { + self.grid = Some(grid) + } + + fn grid(&self) -> &Option { + &self.grid + } } /// A view with categorical entries along the x-axis and continuous values along the y-axis @@ -222,6 +240,7 @@ pub struct CategoricalView<'a> { y_range: Option, x_label: Option, y_label: Option, + grid: Option, } impl<'a> CategoricalView<'a> { @@ -235,6 +254,7 @@ impl<'a> CategoricalView<'a> { y_range: None, x_label: None, y_label: None, + grid: None, } } @@ -355,6 +375,14 @@ impl<'a> View for CategoricalView<'a> { fn to_text(&self, _face_width: u32, _face_height: u32) -> Result { Ok("".into()) } + + fn add_grid(&mut self, grid: Grid) { + self.grid = Some(grid); + } + + fn grid(&self) -> &Option { + &self.grid + } } /*pub struct AnyView<'a> { From 6cebf72c55ba5867d2e9008fbe36f9bd79ae9575 Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Fri, 1 Mar 2019 16:26:40 +0000 Subject: [PATCH 2/6] Update changelog to include grid change --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index e5080cd..dc6792c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,7 @@ - Introduce categorical representation, views and axes. - Add ability to set dimensions of plot (PR #8) - Added ability to display a histogram as densities +- Add ability to display grids ([PR #23](https://github.com/milliams/plotlib/pull/23)) ### Changed - Change `create_axes`, `save`, `to_svg` and `to_text` to return `Result` indicating an error. From fd746161af536d7809dc5e1adeeb956d358b12a3 Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Fri, 1 Mar 2019 16:27:49 +0000 Subject: [PATCH 3/6] Update changelog to keep consistent formatting Turns out it's not markdown after all... --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index dc6792c..8b221fd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,7 +8,7 @@ - Introduce categorical representation, views and axes. - Add ability to set dimensions of plot (PR #8) - Added ability to display a histogram as densities -- Add ability to display grids ([PR #23](https://github.com/milliams/plotlib/pull/23)) +- Add ability to display grids (PR #23) ### Changed - Change `create_axes`, `save`, `to_svg` and `to_text` to return `Result` indicating an error. From b00065ead41726c4dd985cb82dc760b80599ace6 Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Sat, 2 Mar 2019 09:06:26 +0000 Subject: [PATCH 4/6] Support different grid types depending on the plot Categorical views only show horizontal lines, whereas continuous views show lines in both directions. --- examples/with_grid.rs | 32 +++++++++++++++++++++++--- src/grid.rs | 5 ++++ src/page.rs | 15 ++++++++---- src/svg_render.rs | 53 ++++++++++++++++++++++++++++--------------- src/view.rs | 17 ++++++++++++-- 5 files changed, 95 insertions(+), 27 deletions(-) diff --git a/examples/with_grid.rs b/examples/with_grid.rs index 7b24a18..269923f 100644 --- a/examples/with_grid.rs +++ b/examples/with_grid.rs @@ -1,16 +1,42 @@ extern crate plotlib; use plotlib::grid::Grid; +use plotlib::style::BarChart; use plotlib::style::Line; use plotlib::view::View; fn main() { + render_line_chart("line_with_grid.svg"); + render_barchart("barchart_with_grid.svg"); +} + +fn render_line_chart(filename: S) +where + S: AsRef, +{ let l1 = plotlib::line::Line::new(&[(0., 1.), (2., 1.5), (3., 1.2), (4., 1.1)]) .style(plotlib::line::Style::new().colour("burlywood")); - let grid = Grid::new(3, 8); let mut v = plotlib::view::ContinuousView::new().add(&l1); - v.add_grid(grid); + v.add_grid(Grid::new(3, 8)); + plotlib::page::Page::single(&v) + .save(filename.as_ref()) + .expect("saving svg"); +} + +fn render_barchart(filename: S) +where + S: AsRef, +{ + let b1 = plotlib::barchart::BarChart::new(5.3).label("1"); + let b2 = plotlib::barchart::BarChart::new(2.6) + .label("2") + .style(plotlib::barchart::Style::new().fill("darkolivegreen")); + let mut v = plotlib::view::CategoricalView::new() + .add(&b1) + .add(&b2) + .x_label("Experiment"); + v.add_grid(Grid::new(3, 8)); plotlib::page::Page::single(&v) - .save("line.svg") + .save(filename.as_ref()) .expect("saving svg"); } diff --git a/src/grid.rs b/src/grid.rs index 53b9493..ccc175c 100644 --- a/src/grid.rs +++ b/src/grid.rs @@ -1,3 +1,8 @@ +pub(crate) enum GridType<'a> { + HorizontalOnly(&'a Grid), + Both(&'a Grid), +} + pub struct Grid { pub nx: u32, pub ny: u32, diff --git a/src/page.rs b/src/page.rs index d86d85b..dff6293 100644 --- a/src/page.rs +++ b/src/page.rs @@ -25,16 +25,23 @@ pub struct Page<'a> { impl<'a> Page<'a> { /** - Creates a plot containing a single view + Creates an empty page container for plots to be added to */ - pub fn single(view: &'a View) -> Self { + pub fn empty() -> Self { Page { - views: vec![view], - num_views: 1, + views: Vec::new(), + num_views: 0, dimensions: (600, 400), } } + /** + Creates a plot containing a single view + */ + pub fn single(view: &'a View) -> Self { + Page::empty().add_plot(view) + } + /// Set the dimensions of the plot. pub fn dimensions(mut self, x: u32, y: u32) -> Self { self.dimensions = (x, y); diff --git a/src/svg_render.rs b/src/svg_render.rs index 1b8d672..e6815a9 100644 --- a/src/svg_render.rs +++ b/src/svg_render.rs @@ -4,7 +4,7 @@ use svg::node; use svg::Node; use crate::axis; -use crate::grid::Grid; +use crate::grid::GridType; use crate::histogram; use crate::style; use crate::utils; @@ -448,28 +448,45 @@ where group } -pub(crate) fn draw_grid(grid: &Grid, face_width: f64, face_height: f64) -> node::element::Group { - let (xmin, xmax) = (0f64, face_width); - let (ymin, ymax) = (0f64, face_height); +pub(crate) fn draw_grid(grid: GridType, face_width: f64, face_height: f64) -> node::element::Group { + match grid { + GridType::HorizontalOnly(grid) => { + let (ymin, ymax) = (0f64, face_height); + let y_step = (ymax - ymin) / f64::from(grid.ny); + let mut lines = node::element::Group::new(); + + for iy in 0..=grid.ny { + let y = f64::from(iy) * y_step + ymin; + let line = horizontal_line(-y, 0.0, face_width, grid.color.as_str()); + lines = lines.add(line); + } - let x_step = (xmax - xmin) / f64::from(grid.nx); - let y_step = (ymax - ymin) / f64::from(grid.ny); + lines + } + GridType::Both(grid) => { + let (xmin, xmax) = (0f64, face_width); + let (ymin, ymax) = (0f64, face_height); - let mut lines = node::element::Group::new(); + let x_step = (xmax - xmin) / f64::from(grid.nx); + let y_step = (ymax - ymin) / f64::from(grid.ny); - for iy in 0..=grid.ny { - let y = f64::from(iy) * y_step + ymin; - let line = horizontal_line(-y, 0.0, face_width, grid.color.as_str()); - lines = lines.add(line); - } + let mut lines = node::element::Group::new(); - for ix in 0..=grid.nx { - let x = f64::from(ix) * x_step + xmin; - let line = vertical_line(x, 0.0, -face_height, grid.color.as_str()); - lines = lines.add(line); - } + for iy in 0..=grid.ny { + let y = f64::from(iy) * y_step + ymin; + let line = horizontal_line(-y, 0.0, face_width, grid.color.as_str()); + lines = lines.add(line); + } - lines + for ix in 0..=grid.nx { + let x = f64::from(ix) * x_step + xmin; + let line = vertical_line(x, 0.0, -face_height, grid.color.as_str()); + lines = lines.add(line); + } + + lines + } + } } #[cfg(test)] diff --git a/src/view.rs b/src/view.rs index d1a5dc7..dd9c988 100644 --- a/src/view.rs +++ b/src/view.rs @@ -14,7 +14,7 @@ use svg::Node; use crate::axis; use crate::errors::Result; -use crate::grid::Grid; +use crate::grid::{Grid, GridType}; use crate::representation::{CategoricalRepresentation, ContinuousRepresentation}; use crate::svg_render; use crate::text_render; @@ -171,7 +171,11 @@ impl<'a> View for ContinuousView<'a> { view_group.append(svg_render::draw_y_axis(&y_axis, face_height)); if let Some(grid) = &self.grid { - view_group.append(svg_render::draw_grid(grid, face_width, face_height)); + view_group.append(svg_render::draw_grid( + GridType::Both(grid), + face_width, + face_height, + )); } Ok(view_group) @@ -369,6 +373,15 @@ impl<'a> View for CategoricalView<'a> { // Add in the axes view_group.append(svg_render::draw_categorical_x_axis(&x_axis, face_width)); view_group.append(svg_render::draw_y_axis(&y_axis, face_height)); + + if let Some(grid) = &self.grid { + view_group.append(svg_render::draw_grid( + GridType::HorizontalOnly(grid), + face_width, + face_height, + )); + } + Ok(view_group) } From 5d9c8233af63494717c9ecd043c9c3467f6fb4a1 Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Sat, 2 Mar 2019 10:06:36 +0000 Subject: [PATCH 5/6] Move grid rendering earlier This puts the data in front of the grid, which is more akin to how matplotlib renders grids. Grids should be relatively low in the z axis. --- src/view.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/view.rs b/src/view.rs index dd9c988..99d8cfa 100644 --- a/src/view.rs +++ b/src/view.rs @@ -160,6 +160,14 @@ impl<'a> View for ContinuousView<'a> { let (x_axis, y_axis) = self.create_axes()?; + if let Some(grid) = &self.grid { + view_group.append(svg_render::draw_grid( + GridType::Both(grid), + face_width, + face_height, + )); + } + // Then, based on those ranges, draw each repr as an SVG for repr in &self.representations { let repr_group = repr.to_svg(&x_axis, &y_axis, face_width, face_height); @@ -170,14 +178,6 @@ impl<'a> View for ContinuousView<'a> { view_group.append(svg_render::draw_x_axis(&x_axis, face_width)); view_group.append(svg_render::draw_y_axis(&y_axis, face_height)); - if let Some(grid) = &self.grid { - view_group.append(svg_render::draw_grid( - GridType::Both(grid), - face_width, - face_height, - )); - } - Ok(view_group) } @@ -364,6 +364,14 @@ impl<'a> View for CategoricalView<'a> { let (x_axis, y_axis) = self.create_axes()?; + if let Some(grid) = &self.grid { + view_group.append(svg_render::draw_grid( + GridType::HorizontalOnly(grid), + face_width, + face_height, + )); + } + // Then, based on those ranges, draw each repr as an SVG for repr in &self.representations { let repr_group = repr.to_svg(&x_axis, &y_axis, face_width, face_height); @@ -374,14 +382,6 @@ impl<'a> View for CategoricalView<'a> { view_group.append(svg_render::draw_categorical_x_axis(&x_axis, face_width)); view_group.append(svg_render::draw_y_axis(&y_axis, face_height)); - if let Some(grid) = &self.grid { - view_group.append(svg_render::draw_grid( - GridType::HorizontalOnly(grid), - face_width, - face_height, - )); - } - Ok(view_group) } From a3460bcb6537d47c7d5eb0fa1098a5b89f3c8bfc Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Sat, 2 Mar 2019 18:19:15 +0000 Subject: [PATCH 6/6] Add documentation for grids --- src/grid.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/grid.rs b/src/grid.rs index ccc175c..bcf24db 100644 --- a/src/grid.rs +++ b/src/grid.rs @@ -1,11 +1,51 @@ +#![deny(missing_docs)] + +//! Configure a grid on a plot. +//! +//! Grids allow for easier estimating of data values. This module allows the configuration of grids +//! on plots. +//! +//! Grids are created by creating a `Grid` definition, and adding it to a plot: +//! +//! The grid lines for `plotlib` are rendered +//! _underneath_ the data so as to not detract from the data. +//! +//! # Examples +//! +//! ```rust +//! # use plotlib::view::ContinuousView; +//! use plotlib::grid::Grid; +//! # use plotlib::style::Line; +//! # use plotlib::view::View; +//! +//! # let l1 = plotlib::line::Line::new(&[(0., 1.), (2., 1.5), (3., 1.2), (4., 1.1)]) +//! # .style(plotlib::line::Style::new().colour("burlywood")); +//! // let l1 = Line::new() ... +//! let mut v = ContinuousView::new().add(&l1); +//! +//! // 3 vertical lines and 8 horizontal lines +//! v.add_grid(Grid::new(3, 8)); +//! +//! // Render plot +//! ``` + +// Internal type representing the logic of when do we render only horizontal lines, and when do we +// render a full grid pub(crate) enum GridType<'a> { HorizontalOnly(&'a Grid), Both(&'a Grid), } +/// Configuration for the grid on a plot +/// +/// Supports changing the number of grid lines for the x and y dimensions. +/// **Note:** for categorical plots, only horizontal lines will be shown. pub struct Grid { + /// Number of vertical grid lines (defaults to 3) pub nx: u32, + /// Number of horizontal grid lines (defaults to 3) pub ny: u32, + /// Color of the grid lines (defaults to "darkgrey") pub color: String, } @@ -16,6 +56,9 @@ impl Default for Grid { } impl Grid { + /// Create a new grid with `nx` vertical and `ny` horizontal grid lines + /// + /// The default colour is "darkgrey". pub fn new(nx: u32, ny: u32) -> Grid { Grid { nx,