From 821a655698f5a4a8d85eb4770f20ad00af1f6847 Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Tue, 25 May 2021 23:14:45 -0400 Subject: [PATCH 01/11] Initial work on flexbox layout --- Cargo.toml | 2 + examples/layout.rs | 20 ++ src/chart/builder.rs | 2 +- src/chart/layout/mod.rs | 90 +++++ src/chart/layout/nodes.rs | 565 ++++++++++++++++++++++++++++++++ src/chart/mod.rs | 2 + src/coord/ranged2d/cartesian.rs | 2 +- src/lib.rs | 4 +- src/style/colors/mod.rs | 1 - src/style/palette.rs | 58 +++- 10 files changed, 741 insertions(+), 5 deletions(-) create mode 100644 examples/layout.rs create mode 100644 src/chart/layout/mod.rs create mode 100644 src/chart/layout/nodes.rs diff --git a/Cargo.toml b/Cargo.toml index 98cc54e6..718d71d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,8 @@ num-traits = "0.2.14" chrono = { version = "0.4.19", optional = true } plotters-backend = "^0.3" plotters-svg = {version = "^0.3.*", optional = true} +stretch = "0.3.2" +paste = "1.0" [dependencies.plotters-bitmap] version = "^0.3.*" diff --git a/examples/layout.rs b/examples/layout.rs new file mode 100644 index 00000000..aabab5c5 --- /dev/null +++ b/examples/layout.rs @@ -0,0 +1,20 @@ +use plotters::prelude::*; + +const OUT_FILE_NAME: &'static str = "plotters-doc-data/layout2.png"; + +fn main() -> Result<(), Box> { + const W: u32 = 600; + const H: u32 = 400; + let root = BitMapBackend::new(OUT_FILE_NAME, (W, H)).into_drawing_area(); + root.fill(&full_palette::WHITE)?; + + let mut chart = ChartLayout::new(&root); + chart.set_title_text("Chart Title").unwrap(); + chart.draw().unwrap(); + + Ok(()) +} +#[test] +fn entry_point() { + main().unwrap() +} diff --git a/src/chart/builder.rs b/src/chart/builder.rs index b74167ce..4a4187eb 100644 --- a/src/chart/builder.rs +++ b/src/chart/builder.rs @@ -311,7 +311,7 @@ impl<'a, 'b, DB: DrawingBackend> ChartBuilder<'a, 'b, DB> { /// context, where data series can be rendered on. /// - `x_spec`: The specification of X axis /// - `y_spec`: The specification of Y axis - /// - `z_sepc`: The specification of Z axis + /// - `z_spec`: The specification of Z axis /// - Returns: A chart context pub fn build_cartesian_3d( &mut self, diff --git a/src/chart/layout/mod.rs b/src/chart/layout/mod.rs new file mode 100644 index 00000000..7876572b --- /dev/null +++ b/src/chart/layout/mod.rs @@ -0,0 +1,90 @@ +use crate::coord::Shift; + +use crate::drawing::DrawingArea; +use crate::style::IntoFont; +use crate::style::{colors, TextStyle}; + +use plotters_backend::DrawingBackend; + +mod nodes; +use nodes::*; + +/// The helper object to create a chart context, which is used for the high-level figure drawing. +/// With the help of this object, we can convert a basic drawing area into a chart context, which +/// allows the high-level charting API being used on the drawing area. +pub struct ChartLayout<'a, 'b, DB: DrawingBackend> { + root_area: &'a DrawingArea, + title_text: Option, + title_style: TextStyle<'b>, + nodes: ChartLayoutNodes, +} + +impl<'a, 'b, DB: DrawingBackend> ChartLayout<'a, 'b, DB> { + /// Create a chart builder on the given drawing area + /// - `root`: The root drawing area + /// - Returns: The chart layout object + pub fn new(root: &'a DrawingArea) -> Self { + Self { + root_area: root, + title_text: None, + title_style: TextStyle::from(("serif", 40.0).into_font()), + nodes: ChartLayoutNodes::new().unwrap(), + } + } + + pub fn draw(&mut self) -> Result<&mut Self, Box> { + let (x_range, y_range) = self.root_area.get_pixel_range(); + let (x_top, y_top) = (x_range.start, y_range.start); + let (w, h) = (x_range.end - x_top, y_range.end - y_top); + self.nodes.layout(w as u32, h as u32)?; + + if let Some(title) = self.title_text.as_ref() { + let extents = self.nodes.get_chart_title_extents().unwrap(); + self.root_area.draw_text( + title, + &self.title_style, + (extents.0 + x_top, extents.1 + y_top), + )?; + } + + Ok(self) + } + + /// Set the chart's title text + pub fn set_title_text>( + &mut self, + title: S, + ) -> Result<&mut Self, Box> { + self.title_text = Some(title.as_ref().to_string()); + let (w, h) = self + .root_area + .estimate_text_size(&self.title_text.as_ref().unwrap(), &self.title_style)?; + self.nodes.set_chart_title_size(w as i32, h as i32)?; + Ok(self) + } +} + +/* +#[cfg(test)] +mod test { + use super::*; + use crate::prelude::*; + #[test] + fn test_label_area_size() { + let drawing_area = create_mocked_drawing_area(200, 200, |_| {}); + let mut chart = ChartLayout::new(&drawing_area); + } + + #[test] + fn test_margin_configure() { + let drawing_area = create_mocked_drawing_area(200, 200, |_| {}); + let mut chart = ChartLayout::new(&drawing_area); + } + + #[test] + fn test_caption() { + let drawing_area = create_mocked_drawing_area(200, 200, |_| {}); + let mut chart = ChartLayout::new(&drawing_area); + } +} +*/ \ No newline at end of file diff --git a/src/chart/layout/nodes.rs b/src/chart/layout/nodes.rs new file mode 100644 index 00000000..0582624f --- /dev/null +++ b/src/chart/layout/nodes.rs @@ -0,0 +1,565 @@ +use paste::paste; +use std::collections::HashMap; +use std::iter::once; + +use stretch::number::OrElse; +use stretch::{ + geometry, + geometry::Size, + node::{MeasureFunc, Node, Stretch}, + number::Number, + style::*, +}; + +macro_rules! impl_get_size { + ($name:ident) => { + paste! { + #[doc = "Get the size of the `" $name "` container."] + #[doc = " * **Returns**: An option containing a tuple `(width, height)`."] + pub fn [](&self) -> Option<(i32, i32)> { + self.extents_cache.as_ref().and_then(|extents_cache| { + extents_cache + .get(&self.$name) + .map(|&(x1, y1, x2, y2)| (x2 - x1, y2 - y1)) + }) + } + #[doc = "Get the extents of the `" $name "` container."] + #[doc = " * **Returns**: An option containing a tuple `(x1,y1,x2,y2)`."] + pub fn [](&self) -> Option<(i32, i32, i32, i32)> { + self.extents_cache.as_ref().and_then(|extents_cache| { + extents_cache + .get(&self.$name) + .map(|&extent| extent) + }) + } + } + }; + ($name:ident, $sub_part:ident) => { + paste! { + #[doc = "Get the size of the `" $name "." $sub_part "` container."] + #[doc = " * **Returns**: An option containing a tuple `(width, height)`."] + pub fn [](&self) -> Option<(i32, i32)> { + self.extents_cache.as_ref().and_then(|extents_cache| { + extents_cache + .get(&self.$name.$sub_part) + .map(|&(x1, y1, x2, y2)| (x2 - x1, y2 - y1)) + }) + } + #[doc = "Get the size of the `" $name "." $sub_part "` container."] + #[doc = " * **Returns**: An option containing a tuple `(x1,y1,x2,y2)`."] + pub fn [](&self) -> Option<(i32, i32, i32, i32)> { + self.extents_cache.as_ref().and_then(|extents_cache| { + extents_cache + .get(&self.$name.$sub_part) + .map(|&extent| extent) + }) + } + } + }; +} + +macro_rules! impl_set_size { + ($name:ident) => { + paste! { + #[doc = "Set the size of the `" $name "` container."] + pub fn []( + &mut self, + w: i32, + h: i32, + ) -> Result<(), Box> { + self.stretch_context.set_measure( + self.$name, + Some(new_measure_func_with_min_sizes(w as f32, h as f32)), + )?; + Ok(()) + } + } + }; + ($name:ident, $sub_part:ident) => { + paste! { + #[doc = "Set the size of the `" $name "." $sub_part "` container."] + #[doc = " * **Returns**: An option containing a tuple `(width, height)`."] + pub fn []( + &mut self, + w: i32, + h: i32, + ) -> Result<(), Box> { + self.stretch_context.set_measure( + self.$name.$sub_part, + Some(new_measure_func_with_min_sizes(w as f32, h as f32)), + )?; + Ok(()) + } + } + }; +} + +/// A structure containing two nodes, `inner` and `outer`. +/// `inner` is contained within `outer` and will be centered within +/// `outer`. `inner` will be centered horizontally for a `row_layout` +/// and vertically for a `col_layout`. +#[derive(Debug, Clone)] +pub(crate) struct CenteredLabelLayout { + outer: Node, + inner: Node, +} +impl CenteredLabelLayout { + /// Create an inner node that is `justify-content: center` with respect + /// to its outer node. + fn new(stretch_context: &mut Stretch) -> Result> { + let inner = stretch_context.new_leaf( + Default::default(), + Box::new(|constraint| { + Ok(stretch::geometry::Size { + width: constraint.width.or_else(0.0), + height: constraint.height.or_else(0.0), + }) + }), + )?; + let outer = stretch_context.new_node( + Style { + justify_content: JustifyContent::Center, + ..Default::default() + }, + vec![inner], + )?; + + Ok(Self { inner, outer }) + } + /// Create an inner node that is horizontally centered in its 100% width parent. + fn new_row_layout(stretch_context: &mut Stretch) -> Result> { + let layout = Self::new(stretch_context)?; + // If the layout is placed in a row, the outer should have 100% width. + let outer_style = *stretch_context.style(layout.outer)?; + stretch_context.set_style( + layout.outer, + Style { + flex_direction: FlexDirection::Row, + ..outer_style + }, + )?; + + Ok(layout) + } + /// Create an inner node that is vertically centered in its 100% height parent. + fn new_col_layout(stretch_context: &mut Stretch) -> Result> { + let layout = Self::new(stretch_context)?; + // If the layout is placed in a row, the outer should have 100% width. + let outer_style = *stretch_context.style(layout.outer)?; + stretch_context.set_style( + layout.outer, + Style { + flex_direction: FlexDirection::Column, + ..outer_style + }, + )?; + + Ok(layout) + } +} + +/// A struct to store the layout structure of a chart using the `stretch` +/// library. The `stretch` library uses a flexbox-compatible algorithm to lay +/// out nodes. The layout hierarchy is equivalent to the following HTML. +/// ```html +/// +/// +/// Title +/// +/// +/// +/// +/// left_label +/// +/// +/// +/// +/// +/// +/// top_label +/// +/// +/// +/// CHART +/// +/// +/// bottom_label +/// +/// +/// +/// +/// +/// +/// right_label +/// +/// +/// +/// +/// +/// ``` +pub(crate) struct ChartLayoutNodes { + /// A map from nodes to extents of the form `(x1,y1,x2,y2)` where + /// `(x1,y1)` is the upper left corner of the node and + /// `(x2,y2)` is the lower right corner of the node. + extents_cache: Option>, + /// The `stretch` context that is used to compute the layout. + stretch_context: Stretch, + /// The outer-most node which contains all others. + outer_container: Node, + /// The title of the whole chart + chart_title: CenteredLabelLayout, + top_area: Node, + /// x-axis label above chart + top_label: CenteredLabelLayout, + top_tick_label: Node, + left_area: Node, + /// y-axis label left of chart + left_label: CenteredLabelLayout, + left_tick_label: Node, + right_area: Node, + /// y-axis label right of chart + right_label: CenteredLabelLayout, + right_tick_label: Node, + bottom_area: Node, + /// x-axis label above chart + bottom_label: CenteredLabelLayout, + bottom_tick_label: Node, + center_container: Node, + chart_area: Node, + chart_container: Node, +} + +impl ChartLayoutNodes { + /// Create a new `ChartLayoutNodes`. All margins/padding/sizes are set to 0 + /// and should be overridden as needed. + pub fn new() -> Result> { + // Set up the layout engine + let mut stretch_context = Stretch::new(); + + // Create the chart title + let chart_title = CenteredLabelLayout::new_row_layout(&mut stretch_context)?; + + // Create the labels + let (top_area, top_label, top_tick_label) = + packed_title_label_area(&mut stretch_context, FlexDirection::Column)?; + let (bottom_area, bottom_label, bottom_tick_label) = + packed_title_label_area(&mut stretch_context, FlexDirection::ColumnReverse)?; + let (left_area, left_label, left_tick_label) = + packed_title_label_area(&mut stretch_context, FlexDirection::Row)?; + let (right_area, right_label, right_tick_label) = + packed_title_label_area(&mut stretch_context, FlexDirection::RowReverse)?; + + // Create the center chart area and column + let chart_area = stretch_context.new_leaf( + Style { + flex_grow: 1.0, + ..Default::default() + }, + new_measure_func_with_defaults(), + )?; + let center_container = stretch_context.new_node( + Style { + flex_grow: 1.0, + flex_direction: FlexDirection::Column, + ..Default::default() + }, + vec![top_area, chart_area, bottom_area], + )?; + let chart_container = stretch_context.new_node( + Style { + flex_grow: 1.0, + flex_direction: FlexDirection::Row, + ..Default::default() + }, + vec![left_area, center_container, right_area], + )?; + + // Pack everything together to make a full chart + let outer_container = stretch_context.new_node( + Style { + size: Size { + width: Dimension::Percent(1.0), + height: Dimension::Percent(1.0), + }, + flex_grow: 1.0, + flex_direction: FlexDirection::Column, + ..Default::default() + }, + vec![chart_title.outer, chart_container], + )?; + + Ok(Self { + extents_cache: None, + stretch_context, + outer_container, + chart_title, + top_area, + top_label, + top_tick_label, + left_area, + left_label, + left_tick_label, + right_area, + right_label, + right_tick_label, + bottom_area, + bottom_label, + bottom_tick_label, + center_container, + chart_area, + chart_container, + }) + } + /// Compute the layout of all items to fill a container of width + /// `w` and height `h`. + pub fn layout(&mut self, w: u32, h: u32) -> Result<(), Box> { + // Compute the initial layout + self.stretch_context.compute_layout( + self.outer_container, + Size { + width: Number::Defined(w as f32), + height: Number::Defined(h as f32), + }, + )?; + + // By default the flex containers on the left and right + // will be the full height of the `chart_container`. However, we'd + // actually like them to be the height of the `chart_area`. To achieve + // this, we apply margins of the appropriate size and then recompute + // the layout. + let top_area_layout = self.stretch_context.layout(self.top_area)?; + let bottom_area_layout = self.stretch_context.layout(self.bottom_area)?; + let margin = geometry::Rect { + top: Dimension::Points(top_area_layout.size.height), + bottom: Dimension::Points(bottom_area_layout.size.height), + start: Dimension::Undefined, + end: Dimension::Undefined, + }; + let old_style = *self.stretch_context.style(self.left_area)?; + self.stretch_context.set_style( + self.left_area, + Style { + margin, + ..old_style + }, + )?; + let old_style = *self.stretch_context.style(self.right_area)?; + self.stretch_context.set_style( + self.right_area, + Style { + margin, + ..old_style + }, + )?; + + // Recompute the layout with the new margins set. + // According to the `stretch` documentation, this is very efficient. + self.stretch_context.compute_layout( + self.outer_container, + Size { + width: Number::Defined(w as f32), + height: Number::Defined(h as f32), + }, + )?; + + self.extents_cache = Some(compute_child_extents( + &self.stretch_context, + self.outer_container, + )); + + Ok(()) + } + + pub fn get_chart_area_size(&self) -> Option<(i32, i32)> { + self.extents_cache.as_ref().and_then(|extents_cache| { + extents_cache + .get(&self.chart_area) + .map(|&(x1, y1, x2, y2)| (x2 - x1, y2 - y1)) + }) + } + pub fn set_chart_area_size( + &mut self, + w: i32, + h: i32, + ) -> Result<(), Box> { + self.stretch_context.set_measure( + self.chart_area, + Some(new_measure_func_with_min_sizes(w as f32, h as f32)), + )?; + Ok(()) + } + // Getters for relevant box sizes + impl_get_size!(outer_container); + impl_get_size!(top_tick_label); + impl_get_size!(bottom_tick_label); + impl_get_size!(left_tick_label); + impl_get_size!(right_tick_label); + impl_get_size!(chart_title, inner); + impl_get_size!(top_label, inner); + impl_get_size!(bottom_label, inner); + impl_get_size!(left_label, inner); + impl_get_size!(right_label, inner); + + // Setters for relevant box sizes + impl_set_size!(top_tick_label); + impl_set_size!(bottom_tick_label); + impl_set_size!(left_tick_label); + impl_set_size!(right_tick_label); + impl_set_size!(chart_title, inner); + impl_set_size!(top_label, inner); + impl_set_size!(bottom_label, inner); + impl_set_size!(left_label, inner); + impl_set_size!(right_label, inner); +} + +/// Pack a centered title and a label-area together in a row (`FlexDirection::Row`/`RowReverse`) +/// or column (`FlexDirection::Column`/`ColumnReverse`). +/// * `stretch_context` - The `Stretch` context +/// * `flex_direction` - How the title-area and label-area are to be layed out. +/// * **Returns**: A triple `(outer_area, title_area, label_area)`. The `outer_area` contains both the `title_area` and the `label_area`. +fn packed_title_label_area( + stretch_context: &mut Stretch, + flex_direction: FlexDirection, +) -> Result<(Node, CenteredLabelLayout, Node), Box> { + let title = match flex_direction { + FlexDirection::Row | FlexDirection::RowReverse => { + // If the title and the label are packed in a row, the title should be centered in a *column*. + CenteredLabelLayout::new_col_layout(stretch_context)? + } + FlexDirection::Column | FlexDirection::ColumnReverse => { + // If the title and the label are packed in a column, the title should be centered in a *row*. + CenteredLabelLayout::new_row_layout(stretch_context)? + } + }; + let label = stretch_context.new_leaf( + Default::default(), + Box::new(|constraint| { + Ok(stretch::geometry::Size { + width: constraint.width.or_else(0.0), + height: constraint.height.or_else(0.0), + }) + }), + )?; + let outer = stretch_context.new_node( + Style { + flex_direction, + ..Default::default() + }, + vec![title.outer, label], + )?; + + Ok((outer, title, label)) +} + +fn new_measure_func_with_min_sizes(w: f32, h: f32) -> MeasureFunc { + Box::new(move |constraint| { + Ok(stretch::geometry::Size { + width: constraint.width.or_else(w), + height: constraint.height.or_else(h), + }) + }) +} +fn new_measure_func_with_defaults() -> MeasureFunc { + Box::new(move |constraint| { + Ok(stretch::geometry::Size { + width: constraint.width.or_else(0.), + height: constraint.height.or_else(0.), + }) + }) +} + +/// When `stretch` computes the layout of a node, its +/// extents are computed relatively to the parent. We want absolute positions, +/// so we need to compute them manually. +/// * **Returns**: A `HashMap` from nodes to tuples `(x1,y1,x2,y2)` where `(x1,y1)` and `(x2,y2)` represent the upper left and lower right corners of the bounding rectangle. +fn compute_child_extents(stretch: &Stretch, node: Node) -> HashMap { + const DEFAULT_CAPACITY: usize = 16; + let mut ret = HashMap::with_capacity(DEFAULT_CAPACITY); + fn _compute_child_extents( + stretch: &Stretch, + node: Node, + offset: (i32, i32), + store: &mut HashMap, + ) { + let layout = stretch.layout(node).unwrap(); + let geometry::Point { x, y } = layout.location; + let geometry::Size { width, height } = layout.size; + let (x1, y1) = (x as i32 + offset.0, y as i32 + offset.1); + let (x2, y2) = ((width) as i32 + x1, (height) as i32 + y1); + store.insert(node, (x1, y1, x2, y2)); + + if stretch.child_count(node).unwrap() > 0 { + for child in stretch.children(node).unwrap() { + _compute_child_extents(stretch, child, (x1, y1), store); + } + } + } + _compute_child_extents(stretch, node, (0, 0), &mut ret); + ret +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + /// The default layout should make the chart area take the full area. + fn full_chart_area() { + let mut layout = ChartLayoutNodes::new().unwrap(); + layout.layout(70, 50).unwrap(); + let extents_cache = layout.extents_cache.unwrap(); + let &(x1, y1, x2, y2) = extents_cache.get(&layout.chart_area).unwrap(); + + assert_eq!(x1, 0); + assert_eq!(y1, 0); + assert_eq!(x2, 70); + assert_eq!(y2, 50); + } + #[test] + /// The default layout should make the chart area take the full area. + fn full_chart_area_with_getter() { + let mut layout = ChartLayoutNodes::new().unwrap(); + layout.layout(70, 50).unwrap(); + let (w, h) = layout.get_chart_area_size().unwrap(); + + assert_eq!(w, 70); + assert_eq!(h, 50); + } + #[test] + fn full_chart_area_with_getter_without_running_layout() { + let layout = ChartLayoutNodes::new().unwrap(); + assert_eq!(layout.get_chart_area_size(), None); + } + #[test] + /// The outer container should always be the full size. + fn full_outer_container_size_with_getter() { + let mut layout = ChartLayoutNodes::new().unwrap(); + layout.layout(70, 50).unwrap(); + let (w, h) = layout.get_outer_container_size().unwrap(); + + assert_eq!(w, 70); + assert_eq!(h, 50); + } + #[test] + fn zero_config_chart_title_size_with_getter() { + let mut layout = ChartLayoutNodes::new().unwrap(); + layout.layout(70, 50).unwrap(); + let (w, h) = layout.get_chart_title_size().unwrap(); + + assert_eq!(w, 0); + assert_eq!(h, 0); + } + #[test] + /// The outer container should always be the full size. + fn set_chart_title_size() { + let mut layout = ChartLayoutNodes::new().unwrap(); + layout.set_chart_title_size(20, 20).unwrap(); + layout.layout(70, 50).unwrap(); + let (w, h) = layout.get_chart_title_size().unwrap(); + + assert_eq!(w, 20); + assert_eq!(h, 20); + + let (x1, y1, x2, y2) = layout.get_chart_title_extents().unwrap(); + assert_eq!((x1, y1, x2, y2), (25, 0, 45, 20)); + } +} diff --git a/src/chart/mod.rs b/src/chart/mod.rs index 4a880296..7302a4a5 100644 --- a/src/chart/mod.rs +++ b/src/chart/mod.rs @@ -19,8 +19,10 @@ mod dual_coord; mod mesh; mod series; mod state; +mod layout; pub use builder::{ChartBuilder, LabelAreaPosition}; +pub use layout::ChartLayout; pub use context::ChartContext; pub use dual_coord::{DualCoordChartContext, DualCoordChartState}; pub use mesh::{MeshStyle, SecondaryMeshStyle}; diff --git a/src/coord/ranged2d/cartesian.rs b/src/coord/ranged2d/cartesian.rs index 897e7f52..34d2abfa 100644 --- a/src/coord/ranged2d/cartesian.rs +++ b/src/coord/ranged2d/cartesian.rs @@ -89,7 +89,7 @@ impl Cartesian2d { self.logic_y.range() } - /// Get the horizental backend coordinate range where X axis should be drawn + /// Get the horizontal backend coordinate range where X axis should be drawn pub fn get_x_axis_pixel_range(&self) -> Range { self.logic_x.axis_pixel_range(self.back_x) } diff --git a/src/lib.rs b/src/lib.rs index 24962836..07682d5d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -756,7 +756,9 @@ pub use palette; /// The module imports the most commonly used types and modules in Plotters pub mod prelude { // Chart related types - pub use crate::chart::{ChartBuilder, ChartContext, LabelAreaPosition, SeriesLabelPosition}; + pub use crate::chart::{ + ChartBuilder, ChartContext, ChartLayout, LabelAreaPosition, SeriesLabelPosition, + }; // Coordinates pub use crate::coord::{ diff --git a/src/style/colors/mod.rs b/src/style/colors/mod.rs index 11f28ed5..5549d012 100644 --- a/src/style/colors/mod.rs +++ b/src/style/colors/mod.rs @@ -21,7 +21,6 @@ macro_rules! doc { } } -#[macro_export] macro_rules! define_color { ($name:ident, $r:expr, $g:expr, $b:expr, $doc:expr) => { doc! { diff --git a/src/style/palette.rs b/src/style/palette.rs index 3b37b438..39389edc 100644 --- a/src/style/palette.rs +++ b/src/style/palette.rs @@ -1,4 +1,11 @@ -use super::color::PaletteColor; +use super::{color::PaletteColor, full_palette}; + +// Helper to quickly convert colors into tuples +macro_rules! color_to_tuple { + ($name:path) => { + ($name.0, $name.1, $name.2); + }; +} pub trait Palette { const COLORS: &'static [(u8, u8, u8)]; @@ -17,6 +24,55 @@ pub struct Palette9999; /// The palette of 100% accessibility pub struct Palette100; +/// A palette of light colors, suitable for backgrounds. +pub struct PaletteLight; +/// A palette of vivid colors. +pub struct PaletteVivid; + +impl Palette for PaletteLight { + const COLORS: &'static [(u8, u8, u8)] = &[ + color_to_tuple!(full_palette::RED_100), + color_to_tuple!(full_palette::GREEN_100), + color_to_tuple!(full_palette::BLUE_100), + color_to_tuple!(full_palette::ORANGE_100), + color_to_tuple!(full_palette::TEAL_100), + color_to_tuple!(full_palette::YELLOW_100), + color_to_tuple!(full_palette::CYAN_100), + color_to_tuple!(full_palette::DEEPORANGE_100), + color_to_tuple!(full_palette::BLUEGREY_100), + color_to_tuple!(full_palette::PURPLE_100), + color_to_tuple!(full_palette::LIME_100), + color_to_tuple!(full_palette::INDIGO_100), + color_to_tuple!(full_palette::DEEPPURPLE_100), + color_to_tuple!(full_palette::PINK_100), + color_to_tuple!(full_palette::LIGHTBLUE_100), + color_to_tuple!(full_palette::LIGHTGREEN_100), + color_to_tuple!(full_palette::AMBER_100), + ]; +} + +impl Palette for PaletteVivid { + const COLORS: &'static [(u8, u8, u8)] = &[ + color_to_tuple!(full_palette::RED_A400), + color_to_tuple!(full_palette::GREEN_A400), + color_to_tuple!(full_palette::BLUE_A400), + color_to_tuple!(full_palette::ORANGE_A400), + color_to_tuple!(full_palette::TEAL_A400), + color_to_tuple!(full_palette::YELLOW_A400), + color_to_tuple!(full_palette::CYAN_A400), + color_to_tuple!(full_palette::DEEPORANGE_A400), + color_to_tuple!(full_palette::BLUEGREY_A400), + color_to_tuple!(full_palette::PURPLE_A400), + color_to_tuple!(full_palette::LIME_A400), + color_to_tuple!(full_palette::INDIGO_A400), + color_to_tuple!(full_palette::DEEPPURPLE_A400), + color_to_tuple!(full_palette::PINK_A400), + color_to_tuple!(full_palette::LIGHTBLUE_A400), + color_to_tuple!(full_palette::LIGHTGREEN_A400), + color_to_tuple!(full_palette::AMBER_A400), + ]; +} + impl Palette for Palette99 { const COLORS: &'static [(u8, u8, u8)] = &[ (230, 25, 75), From 5c0f3a5c910b8732126a311071ed566a457c4316 Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Wed, 26 May 2021 12:46:53 -0400 Subject: [PATCH 02/11] Switch to concrete error type --- examples/layout.rs | 3 +-- src/chart/layout/mod.rs | 13 +++++++------ src/chart/layout/nodes.rs | 18 +++++++++--------- src/drawing/area.rs | 9 +++++++++ 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/examples/layout.rs b/examples/layout.rs index aabab5c5..b82aa05b 100644 --- a/examples/layout.rs +++ b/examples/layout.rs @@ -9,8 +9,7 @@ fn main() -> Result<(), Box> { root.fill(&full_palette::WHITE)?; let mut chart = ChartLayout::new(&root); - chart.set_title_text("Chart Title").unwrap(); - chart.draw().unwrap(); + chart.set_title_text("Chart Title")?.draw()?; Ok(()) } diff --git a/src/chart/layout/mod.rs b/src/chart/layout/mod.rs index 7876572b..9863be9f 100644 --- a/src/chart/layout/mod.rs +++ b/src/chart/layout/mod.rs @@ -1,8 +1,8 @@ use crate::coord::Shift; -use crate::drawing::DrawingArea; +use crate::drawing::{DrawingArea, DrawingAreaErrorKind}; use crate::style::IntoFont; -use crate::style::{colors, TextStyle}; +use crate::style::TextStyle; use plotters_backend::DrawingBackend; @@ -32,7 +32,7 @@ impl<'a, 'b, DB: DrawingBackend> ChartLayout<'a, 'b, DB> { } } - pub fn draw(&mut self) -> Result<&mut Self, Box> { + pub fn draw(&mut self) -> Result<&mut Self, DrawingAreaErrorKind> { let (x_range, y_range) = self.root_area.get_pixel_range(); let (x_top, y_top) = (x_range.start, y_range.start); let (w, h) = (x_range.end - x_top, y_range.end - y_top); @@ -54,11 +54,12 @@ impl<'a, 'b, DB: DrawingBackend> ChartLayout<'a, 'b, DB> { pub fn set_title_text>( &mut self, title: S, - ) -> Result<&mut Self, Box> { + ) -> Result<&mut Self, DrawingAreaErrorKind> { self.title_text = Some(title.as_ref().to_string()); let (w, h) = self .root_area - .estimate_text_size(&self.title_text.as_ref().unwrap(), &self.title_style)?; + .estimate_text_size(&self.title_text.as_ref().unwrap(), &self.title_style) + .unwrap(); self.nodes.set_chart_title_size(w as i32, h as i32)?; Ok(self) } @@ -87,4 +88,4 @@ mod test { let mut chart = ChartLayout::new(&drawing_area); } } -*/ \ No newline at end of file +*/ diff --git a/src/chart/layout/nodes.rs b/src/chart/layout/nodes.rs index 0582624f..4adecb52 100644 --- a/src/chart/layout/nodes.rs +++ b/src/chart/layout/nodes.rs @@ -66,7 +66,7 @@ macro_rules! impl_set_size { &mut self, w: i32, h: i32, - ) -> Result<(), Box> { + ) -> Result<(), stretch::Error> { self.stretch_context.set_measure( self.$name, Some(new_measure_func_with_min_sizes(w as f32, h as f32)), @@ -83,7 +83,7 @@ macro_rules! impl_set_size { &mut self, w: i32, h: i32, - ) -> Result<(), Box> { + ) -> Result<(), stretch::Error> { self.stretch_context.set_measure( self.$name.$sub_part, Some(new_measure_func_with_min_sizes(w as f32, h as f32)), @@ -106,7 +106,7 @@ pub(crate) struct CenteredLabelLayout { impl CenteredLabelLayout { /// Create an inner node that is `justify-content: center` with respect /// to its outer node. - fn new(stretch_context: &mut Stretch) -> Result> { + fn new(stretch_context: &mut Stretch) -> Result { let inner = stretch_context.new_leaf( Default::default(), Box::new(|constraint| { @@ -127,7 +127,7 @@ impl CenteredLabelLayout { Ok(Self { inner, outer }) } /// Create an inner node that is horizontally centered in its 100% width parent. - fn new_row_layout(stretch_context: &mut Stretch) -> Result> { + fn new_row_layout(stretch_context: &mut Stretch) -> Result { let layout = Self::new(stretch_context)?; // If the layout is placed in a row, the outer should have 100% width. let outer_style = *stretch_context.style(layout.outer)?; @@ -142,7 +142,7 @@ impl CenteredLabelLayout { Ok(layout) } /// Create an inner node that is vertically centered in its 100% height parent. - fn new_col_layout(stretch_context: &mut Stretch) -> Result> { + fn new_col_layout(stretch_context: &mut Stretch) -> Result { let layout = Self::new(stretch_context)?; // If the layout is placed in a row, the outer should have 100% width. let outer_style = *stretch_context.style(layout.outer)?; @@ -232,7 +232,7 @@ pub(crate) struct ChartLayoutNodes { impl ChartLayoutNodes { /// Create a new `ChartLayoutNodes`. All margins/padding/sizes are set to 0 /// and should be overridden as needed. - pub fn new() -> Result> { + pub fn new() -> Result { // Set up the layout engine let mut stretch_context = Stretch::new(); @@ -312,7 +312,7 @@ impl ChartLayoutNodes { } /// Compute the layout of all items to fill a container of width /// `w` and height `h`. - pub fn layout(&mut self, w: u32, h: u32) -> Result<(), Box> { + pub fn layout(&mut self, w: u32, h: u32) -> Result<(), stretch::Error> { // Compute the initial layout self.stretch_context.compute_layout( self.outer_container, @@ -381,7 +381,7 @@ impl ChartLayoutNodes { &mut self, w: i32, h: i32, - ) -> Result<(), Box> { + ) -> Result<(), stretch::Error> { self.stretch_context.set_measure( self.chart_area, Some(new_measure_func_with_min_sizes(w as f32, h as f32)), @@ -420,7 +420,7 @@ impl ChartLayoutNodes { fn packed_title_label_area( stretch_context: &mut Stretch, flex_direction: FlexDirection, -) -> Result<(Node, CenteredLabelLayout, Node), Box> { +) -> Result<(Node, CenteredLabelLayout, Node), stretch::Error> { let title = match flex_direction { FlexDirection::Row | FlexDirection::RowReverse => { // If the title and the label are packed in a row, the title should be centered in a *column*. diff --git a/src/drawing/area.rs b/src/drawing/area.rs index 1d075a07..fdc23ab7 100644 --- a/src/drawing/area.rs +++ b/src/drawing/area.rs @@ -141,6 +141,8 @@ pub enum DrawingAreaErrorKind { SharingError, /// The error caused by invalid layout LayoutError, + /// Layout error coming from the Stretch library + StretchError(stretch::Error), } impl std::fmt::Display for DrawingAreaErrorKind { @@ -151,12 +153,19 @@ impl std::fmt::Display for DrawingAreaErrorKind { write!(fmt, "Multiple backend operation in progress") } DrawingAreaErrorKind::LayoutError => write!(fmt, "Bad layout"), + DrawingAreaErrorKind::StretchError(e) => e.fmt(fmt), } } } impl Error for DrawingAreaErrorKind {} +impl From for DrawingAreaErrorKind { + fn from(err: stretch::Error) -> Self { + DrawingAreaErrorKind::StretchError(err) + } +} + #[allow(type_alias_bounds)] type DrawingAreaError = DrawingAreaErrorKind; From b403db3863af810e20036e0113ceb0f70381d0cc Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Thu, 27 May 2021 14:43:13 -0400 Subject: [PATCH 03/11] Added ability to set labels and margins --- examples/layout.rs | 17 +- src/chart/layout/mod.rs | 375 +++++++++++++++++++++++++++++++++---- src/chart/layout/nodes.rs | 378 +++++++++++++++++++++++++++++++------- src/chart/mod.rs | 2 +- src/drawing/area.rs | 28 ++- src/drawing/mod.rs | 2 +- src/style/text.rs | 1 + 7 files changed, 699 insertions(+), 104 deletions(-) diff --git a/examples/layout.rs b/examples/layout.rs index b82aa05b..33c4c9d6 100644 --- a/examples/layout.rs +++ b/examples/layout.rs @@ -9,7 +9,22 @@ fn main() -> Result<(), Box> { root.fill(&full_palette::WHITE)?; let mut chart = ChartLayout::new(&root); - chart.set_title_text("Chart Title")?.draw()?; + chart + .set_chart_title_text("Chart Title")? + .set_top_label_text("A label at the top")? + .set_chart_title_style(("serif", 60.).into_font().with_color(&RED))? + .set_left_label_text("Left label")? + .set_right_label_text("Right label")? + .set_bottom_label_text("Bottom label")? + .set_bottom_label_margin(10.)? + .set_top_label_margin(10.)? + .draw()?; + + let extent = chart.get_chart_area_extent()?; + root.draw(&Rectangle::new(extent.into_array_of_tuples(), &BLUE))?; + let extent = chart.get_chart_title_extent()?; + root.draw(&Rectangle::new(extent.into_array_of_tuples(), &BLUE))?; + //dbg!(); Ok(()) } diff --git a/src/chart/layout/mod.rs b/src/chart/layout/mod.rs index 9863be9f..1c9ad299 100644 --- a/src/chart/layout/mod.rs +++ b/src/chart/layout/mod.rs @@ -1,21 +1,152 @@ -use crate::coord::Shift; +use crate::{coord::Shift, style::IntoTextStyle}; +use paste::paste; -use crate::drawing::{DrawingArea, DrawingAreaErrorKind}; -use crate::style::IntoFont; -use crate::style::TextStyle; +use crate::drawing::{DrawingArea, DrawingAreaErrorKind, LayoutError}; +use crate::style::{FontTransform, IntoFont, TextStyle}; use plotters_backend::DrawingBackend; mod nodes; +pub use nodes::Margin; use nodes::*; +/// Create the `get__extent` and `get__size` functions +macro_rules! impl_get_extent { + ($name:ident) => { + paste! { + #[doc = "Get the extent (the bounding box) of the `" $name "` container."] + pub fn []( + &self, + ) -> Result, DrawingAreaErrorKind> { + let extent = self + .nodes + .[]() + .ok_or_else(|| LayoutError::ExtentsError)?; + + Ok(self.compute_absolute_pixel_coords(extent)) + } + #[doc = "Get the size of the `" $name "` container."] + #[doc = " * **Returns**: An option containing a tuple `(width, height)`."] + pub fn [](&self) -> Option<(i32, i32)> { + self.nodes.[]() + } + } + }; +} + +/// Create all the getters and setters associated with a label that is horizontally layed out +macro_rules! impl_label_horiz { + ($name:ident) => { + paste! { + #[doc = "Recomputes and sets the size of the `" $name "` container."] + #[doc = "To be called whenever the `" $name "` text/style changes."] + fn [](&mut self) -> Result<(), DrawingAreaErrorKind> { + let (w, h) = match &self.$name.text.as_ref() { + Some(text) => self + .root_area + .estimate_text_size(text, &self.$name.style)?, + None => (0, 0), + }; + self.nodes.[](w, h)?; + + Ok(()) + } + } + impl_label!($name); + } +} +/// Create all the getters and setters associated with a label that is horizontally layed out +macro_rules! impl_label_vert { + ($name:ident) => { + paste! { + #[doc = "Recomputes and sets the size of the `" $name "` container."] + #[doc = "To be called whenever the `" $name "` text/style changes."] + fn [](&mut self) -> Result<(), DrawingAreaErrorKind> { + let (w, h) = match &self.$name.text.as_ref() { + Some(text) => self + .root_area + .estimate_text_size(text, &self.$name.style)?, + None => (0, 0), + }; + // Because this is a label in a vertically layed out label, we swap the width and height + self.nodes.[](h, w)?; + + Ok(()) + } + } + impl_label!($name); + } +} + +/// Create all the getters and setters associated with a labe that don't depend on the label's layout direction +macro_rules! impl_label { + ($name:ident) => { + paste! { + #[doc = "Set the text content of `" $name "`. If `text` is the empty string,"] + #[doc = "the label will be cleared."] + pub fn []>( + &mut self, + text: S, + ) -> Result<&mut Self, DrawingAreaErrorKind> { + Self::set_text(&mut self.$name, text); + self.[]()?; + Ok(self) + } + #[doc = "Clears the text content of the `" $name "` label."] + pub fn []( + &mut self, + ) -> Result<&mut Self, DrawingAreaErrorKind> { + self.[]("")?; + Ok(self) + } + #[doc = "Set the style of the `" $name "` label's text."] + pub fn []>( + &mut self, + style: Style, + ) -> Result<&mut Self, DrawingAreaErrorKind> { + Self::set_style(self.root_area, &mut self.$name, style); + self.[]()?; + Ok(self) + } + #[doc = "Set the margin of the `" $name "` container. If `margin` is a single"] + #[doc = "number, that number is used for all margins. If `margin` is a tuple `(vert,horiz)`,"] + #[doc = "then the top and bottom margins will be `vert` and the left and right margins will"] + #[doc = "be `horiz`. To set each margin separately, use a [`Margin`] struct or a four-tuple."] + pub fn []>>( + &mut self, + margin: M, + ) -> Result<&mut Self, DrawingAreaErrorKind> { + self.nodes.[](margin)?; + Ok(self) + } + #[doc = "Gets the margin of the `" $name "` container."] + pub fn []( + self, + ) -> Result, DrawingAreaErrorKind> { + Ok(self.nodes.[]()?) + } + + + } + }; +} + +/// Hold the text and the style of a chart label +struct Label<'a> { + text: Option, + style: TextStyle<'a>, +} + /// The helper object to create a chart context, which is used for the high-level figure drawing. /// With the help of this object, we can convert a basic drawing area into a chart context, which /// allows the high-level charting API being used on the drawing area. pub struct ChartLayout<'a, 'b, DB: DrawingBackend> { root_area: &'a DrawingArea, - title_text: Option, - title_style: TextStyle<'b>, + chart_title: Label<'b>, + top_label: Label<'b>, + bottom_label: Label<'b>, + left_label: Label<'b>, + right_label: Label<'b>, nodes: ChartLayoutNodes, } @@ -26,66 +157,248 @@ impl<'a, 'b, DB: DrawingBackend> ChartLayout<'a, 'b, DB> { pub fn new(root: &'a DrawingArea) -> Self { Self { root_area: root, - title_text: None, - title_style: TextStyle::from(("serif", 40.0).into_font()), + chart_title: Label { + text: None, + style: TextStyle::from(("serif", 40.0).into_font()), + }, + top_label: Label { + text: None, + style: TextStyle::from(("serif", 25.0).into_font()), + }, + bottom_label: Label { + text: None, + style: TextStyle::from(("serif", 25.0).into_font()), + }, + left_label: Label { + text: None, + style: TextStyle::from(("serif", 25.0).into_font()), + }, + right_label: Label { + text: None, + style: TextStyle::from(("serif", 25.0).into_font()), + }, nodes: ChartLayoutNodes::new().unwrap(), } } + /// Translate the `extent` given in local pixel coordinates to be given + /// in absolute pixel coordinates. + fn compute_absolute_pixel_coords(&self, extent: Extent) -> Extent { + let (x_range, y_range) = self.root_area.get_pixel_range(); + extent.translate((x_range.start, y_range.start)) + } + pub fn draw(&mut self) -> Result<&mut Self, DrawingAreaErrorKind> { let (x_range, y_range) = self.root_area.get_pixel_range(); let (x_top, y_top) = (x_range.start, y_range.start); let (w, h) = (x_range.end - x_top, y_range.end - y_top); self.nodes.layout(w as u32, h as u32)?; - if let Some(title) = self.title_text.as_ref() { - let extents = self.nodes.get_chart_title_extents().unwrap(); + // Draw the horizontally oriented labels + if let Some(text) = self.chart_title.text.as_ref() { + let extent = self + .nodes + .get_chart_title_extent() + .ok_or_else(|| LayoutError::ExtentsError)?; self.root_area.draw_text( - title, - &self.title_style, - (extents.0 + x_top, extents.1 + y_top), + text, + &self.chart_title.style, + (extent.x0 + x_top, extent.y0 + y_top), + )?; + } + + if let Some(text) = self.top_label.text.as_ref() { + let extent = self + .nodes + .get_top_label_extent() + .ok_or_else(|| LayoutError::ExtentsError)?; + self.root_area.draw_text( + text, + &self.top_label.style, + (extent.x0 + x_top, extent.y0 + y_top), + )?; + } + if let Some(text) = self.bottom_label.text.as_ref() { + let extent = self + .nodes + .get_bottom_label_extent() + .ok_or_else(|| LayoutError::ExtentsError)?; + self.root_area.draw_text( + text, + &self.bottom_label.style, + (extent.x0 + x_top, extent.y0 + y_top), + )?; + } + // Draw the vertically oriented labels + if let Some(text) = self.left_label.text.as_ref() { + let extent = self + .nodes + .get_left_label_extent() + .ok_or_else(|| LayoutError::ExtentsError)?; + self.root_area.draw_text( + text, + &self.left_label.style.transform(FontTransform::Rotate270), + (extent.x0 + x_top, extent.y1 + y_top), + )?; + } + if let Some(text) = self.right_label.text.as_ref() { + let extent = self + .nodes + .get_right_label_extent() + .ok_or_else(|| LayoutError::ExtentsError)?; + self.root_area.draw_text( + text, + &self.right_label.style.transform(FontTransform::Rotate270), + (extent.x0 + x_top, extent.y1 + y_top), )?; } Ok(self) } - /// Set the chart's title text - pub fn set_title_text>( - &mut self, - title: S, - ) -> Result<&mut Self, DrawingAreaErrorKind> { - self.title_text = Some(title.as_ref().to_string()); - let (w, h) = self - .root_area - .estimate_text_size(&self.title_text.as_ref().unwrap(), &self.title_style) - .unwrap(); - self.nodes.set_chart_title_size(w as i32, h as i32)?; - Ok(self) + /// Set the text of a label. If the text is a blank screen, the label is cleared. + #[inline(always)] + fn set_text>(elm: &mut Label, text: S) { + let text = text.as_ref().to_string(); + elm.text = match text.is_empty() { + false => Some(text), + true => None, + }; } + /// Set the style of a label. + #[inline(always)] + fn set_style>( + root: &'a DrawingArea, + elm: &mut Label<'b>, + style: Style, + ) { + elm.style = style.into_text_style(root); + } + + impl_get_extent!(top_label); + impl_get_extent!(bottom_label); + impl_get_extent!(left_label); + impl_get_extent!(right_label); + impl_get_extent!(top_tick_label); + impl_get_extent!(bottom_tick_label); + impl_get_extent!(left_tick_label); + impl_get_extent!(right_tick_label); + impl_get_extent!(chart_area); + impl_get_extent!(chart_title); + + impl_label_horiz!(chart_title); + impl_label_horiz!(bottom_label); + impl_label_horiz!(top_label); + impl_label_vert!(left_label); + impl_label_vert!(right_label); } -/* #[cfg(test)] mod test { use super::*; use crate::prelude::*; + + fn extent_has_size( + extent: Extent, + ) -> bool { + (extent.x1 > extent.x0) && (extent.y1 > extent.y0) + } + #[test] - fn test_label_area_size() { + fn test_drawing_of_unset_and_set_chart_title() { let drawing_area = create_mocked_drawing_area(200, 200, |_| {}); let mut chart = ChartLayout::new(&drawing_area); + chart.draw().unwrap(); + chart.set_chart_title_text("title").unwrap(); + chart.draw().unwrap(); + + // Since we set actual text, the extent should have some area + let extent = chart.get_chart_title_extent().unwrap(); + assert!(extent_has_size(extent)); + + // Without any text, the extent shouldn't have any area. + chart.clear_chart_title_text().unwrap(); + chart.draw().unwrap(); + let extent = chart.get_chart_title_extent().unwrap(); + assert!(!extent_has_size(extent)); } #[test] - fn test_margin_configure() { + fn test_drawing_of_unset_and_set_labels() { let drawing_area = create_mocked_drawing_area(200, 200, |_| {}); let mut chart = ChartLayout::new(&drawing_area); + + // top_label + chart.draw().unwrap(); + chart.set_top_label_text("title").unwrap(); + chart.draw().unwrap(); + + let extent = chart.get_top_label_extent().unwrap(); + assert!(extent_has_size(extent)); + + chart.clear_top_label_text().unwrap(); + chart.draw().unwrap(); + let extent = chart.get_top_label_extent().unwrap(); + assert!(!extent_has_size(extent)); + + // bottom_label + chart.draw().unwrap(); + chart.set_bottom_label_text("title").unwrap(); + chart.draw().unwrap(); + + let extent = chart.get_bottom_label_extent().unwrap(); + assert!(extent_has_size(extent)); + + chart.clear_bottom_label_text().unwrap(); + chart.draw().unwrap(); + let extent = chart.get_bottom_label_extent().unwrap(); + assert!(!extent_has_size(extent)); + + // left_label + chart.draw().unwrap(); + chart.set_left_label_text("title").unwrap(); + chart.draw().unwrap(); + + let extent = chart.get_left_label_extent().unwrap(); + assert!(extent_has_size(extent)); + + chart.clear_left_label_text().unwrap(); + chart.draw().unwrap(); + let extent = chart.get_left_label_extent().unwrap(); + assert!(!extent_has_size(extent)); + + // right_label + chart.draw().unwrap(); + chart.set_right_label_text("title").unwrap(); + chart.draw().unwrap(); + + let extent = chart.get_right_label_extent().unwrap(); + assert!(extent_has_size(extent)); + + chart.clear_right_label_text().unwrap(); + chart.draw().unwrap(); + let extent = chart.get_right_label_extent().unwrap(); + assert!(!extent_has_size(extent)); } #[test] - fn test_caption() { - let drawing_area = create_mocked_drawing_area(200, 200, |_| {}); + fn test_layout_of_horizontal_and_vertical_labels() { + let drawing_area = create_mocked_drawing_area(800, 600, |_| {}); let mut chart = ChartLayout::new(&drawing_area); + + // top_label is horizontal + chart.set_top_label_text("some really long text").unwrap(); + chart.draw().unwrap(); + + let extent = chart.get_top_label_extent().unwrap(); + let size = extent.size(); + assert!(size.0 > size.1); + // left_label is vertically + chart.set_left_label_text("some really long text").unwrap(); + chart.draw().unwrap(); + + let extent = chart.get_left_label_extent().unwrap(); + let size = extent.size(); + assert!(size.1 > size.0); } } -*/ diff --git a/src/chart/layout/nodes.rs b/src/chart/layout/nodes.rs index 4adecb52..5ebfbc2a 100644 --- a/src/chart/layout/nodes.rs +++ b/src/chart/layout/nodes.rs @@ -1,6 +1,9 @@ use paste::paste; -use std::collections::HashMap; use std::iter::once; +use std::{ + collections::HashMap, + ops::{Add, Sub}, +}; use stretch::number::OrElse; use stretch::{ @@ -11,26 +14,115 @@ use stretch::{ style::*, }; +/// Trait to constrain a type to a standard numeric type +/// to prevent ambiguity in trait definitions. +/// Idea from: https://stackoverflow.com/questions/39159226/conflicting-implementations-of-trait-in-rust +pub trait Numeric {} +impl Numeric for f64 {} +impl Numeric for f32 {} +impl Numeric for i64 {} +impl Numeric for i32 {} +impl Numeric for i16 {} +impl Numeric for i8 {} +impl Numeric for isize {} +impl Numeric for u64 {} +impl Numeric for u32 {} +impl Numeric for u16 {} +impl Numeric for u8 {} +impl Numeric for usize {} + +/// An `Extent` stores the upper left and bottom right corner of a rectangular region +#[derive(Clone, Debug, PartialEq)] +pub struct Extent { + pub x0: T, + pub y0: T, + pub x1: T, + pub y1: T, +} +impl + Sub + Copy> Extent { + pub fn new(x0: T, y0: T, x1: T, y1: T) -> Self { + Self { x0, y0, x1, y1 } + } + /// Turn the extent into a tuple of the form `(x0,y0,x1,y1)`. + pub fn into_tuple(self) -> (T, T, T, T) { + (self.x0, self.y0, self.x1, self.y1) + } + /// Turn the extent into an array of tuples of the form `[(x0,y0),(x1,y1)]`. + pub fn into_array_of_tuples(self) -> [(T, T); 2] { + [(self.x0, self.y0), (self.x1, self.y1)] + } + /// Get `(width, height)` of the extent + pub fn size(&self) -> (T, T) { + (self.x1 - self.x0, self.y1 - self.y0) + } + /// Translate the extent by the point `(x,y)` + pub fn translate + Copy>(&self, (x, y): (S, S)) -> Self { + Extent { + x0: self.x0 + x.into(), + y0: self.y0 + y.into(), + x1: self.x1 + x.into(), + y1: self.y1 + y.into(), + } + } +} + +/// Margin of a box +#[derive(Debug, Clone, PartialEq)] +pub struct Margin> { + pub top: T, + pub right: T, + pub bottom: T, + pub left: T, +} +impl + Copy + Numeric> From<(T, T, T, T)> for Margin { + /// Convert from a tuple to a margins object. The tuple order + /// is the same as the CSS standard `(top, right, bottom, left)` + fn from(tuple: (T, T, T, T)) -> Self { + Margin { + top: tuple.0.into(), + right: tuple.1.into(), + bottom: tuple.2.into(), + left: tuple.3.into(), + } + } +} +impl + Copy + Numeric> From<(T, T)> for Margin { + /// Convert from a tuple to a margins object. The tuple order + /// is the same as the CSS standard `(vertical, horizontal)` + fn from(tuple: (T, T)) -> Self { + Margin { + top: tuple.0.into(), + right: tuple.1.into(), + bottom: tuple.0.into(), + left: tuple.1.into(), + } + } +} +impl + Copy + Numeric> From for Margin { + /// Convert a number to a margins object. The + /// number will be used for every margin + fn from(margin: T) -> Self { + Margin { + top: margin.into(), + right: margin.into(), + bottom: margin.into(), + left: margin.into(), + } + } +} + macro_rules! impl_get_size { ($name:ident) => { paste! { #[doc = "Get the size of the `" $name "` container."] #[doc = " * **Returns**: An option containing a tuple `(width, height)`."] pub fn [](&self) -> Option<(i32, i32)> { - self.extents_cache.as_ref().and_then(|extents_cache| { - extents_cache - .get(&self.$name) - .map(|&(x1, y1, x2, y2)| (x2 - x1, y2 - y1)) - }) + self.get_size(&self.$name) } #[doc = "Get the extents of the `" $name "` container."] #[doc = " * **Returns**: An option containing a tuple `(x1,y1,x2,y2)`."] - pub fn [](&self) -> Option<(i32, i32, i32, i32)> { - self.extents_cache.as_ref().and_then(|extents_cache| { - extents_cache - .get(&self.$name) - .map(|&extent| extent) - }) + pub fn [](&self) -> Option> { + self.get_extent(&self.$name) } } }; @@ -39,20 +131,31 @@ macro_rules! impl_get_size { #[doc = "Get the size of the `" $name "." $sub_part "` container."] #[doc = " * **Returns**: An option containing a tuple `(width, height)`."] pub fn [](&self) -> Option<(i32, i32)> { - self.extents_cache.as_ref().and_then(|extents_cache| { - extents_cache - .get(&self.$name.$sub_part) - .map(|&(x1, y1, x2, y2)| (x2 - x1, y2 - y1)) - }) + self.get_size(&self.$name.$sub_part) } #[doc = "Get the size of the `" $name "." $sub_part "` container."] #[doc = " * **Returns**: An option containing a tuple `(x1,y1,x2,y2)`."] - pub fn [](&self) -> Option<(i32, i32, i32, i32)> { - self.extents_cache.as_ref().and_then(|extents_cache| { - extents_cache - .get(&self.$name.$sub_part) - .map(|&extent| extent) - }) + pub fn [](&self) -> Option> { + self.get_extent(&self.$name.$sub_part) + } + } + }; +} + +macro_rules! impl_get_margin { + ($name:ident) => { + paste! { + #[doc = "Get the margin of the `" $name "` container."] + pub fn [](&self) -> Result, stretch::Error> { + self.get_margin(self.$name) + } + } + }; + ($name:ident, $sub_part:ident) => { + paste! { + #[doc = "Get the margin of the `" $name "." $sub_part "` container."] + pub fn [](&self) -> Result, stretch::Error> { + self.get_margin(self.$name.$sub_part) } } }; @@ -61,39 +164,54 @@ macro_rules! impl_get_size { macro_rules! impl_set_size { ($name:ident) => { paste! { - #[doc = "Set the size of the `" $name "` container."] + #[doc = "Set the minimum size of the `" $name "` container."] pub fn []( &mut self, - w: i32, - h: i32, + w: u32, + h: u32, ) -> Result<(), stretch::Error> { - self.stretch_context.set_measure( - self.$name, - Some(new_measure_func_with_min_sizes(w as f32, h as f32)), - )?; - Ok(()) + self.set_min_size(self.$name, w, h) } } }; ($name:ident, $sub_part:ident) => { paste! { - #[doc = "Set the size of the `" $name "." $sub_part "` container."] + #[doc = "Set the minimum size of the `" $name "." $sub_part "` container."] #[doc = " * **Returns**: An option containing a tuple `(width, height)`."] pub fn []( &mut self, - w: i32, - h: i32, + w: u32, + h: u32, ) -> Result<(), stretch::Error> { - self.stretch_context.set_measure( - self.$name.$sub_part, - Some(new_measure_func_with_min_sizes(w as f32, h as f32)), - )?; - Ok(()) + self.set_min_size(self.$name.$sub_part, w, h) + } + } + }; +} +macro_rules! impl_set_margin { + ($name:ident) => { + paste! { + #[doc = "Set the margin of the `" $name "` container."] + pub fn []>>( + &mut self, + margin: M, + ) -> Result<(), stretch::Error> { + self.set_margin(self.$name, margin) + } + } + }; + ($name:ident, $sub_part:ident) => { + paste! { + #[doc = "Set the margin of the `" $name "." $sub_part "` container."] + pub fn []>>( + &mut self, + margin: M, + ) -> Result<(), stretch::Error> { + self.set_margin(self.$name.$sub_part, margin) } } }; } - /// A structure containing two nodes, `inner` and `outer`. /// `inner` is contained within `outer` and will be centered within /// `outer`. `inner` will be centered horizontally for a `row_layout` @@ -201,7 +319,7 @@ pub(crate) struct ChartLayoutNodes { /// A map from nodes to extents of the form `(x1,y1,x2,y2)` where /// `(x1,y1)` is the upper left corner of the node and /// `(x2,y2)` is the lower right corner of the node. - extents_cache: Option>, + extents_cache: Option>>, /// The `stretch` context that is used to compute the layout. stretch_context: Stretch, /// The outer-most node which contains all others. @@ -369,27 +487,81 @@ impl ChartLayoutNodes { Ok(()) } + /// Gets the size of `node`. `layout()` must be called first, otherwise an invalid size is returned. + fn get_size(&self, node: &Node) -> Option<(i32, i32)> { + self.extents_cache + .as_ref() + .and_then(|extents_cache| extents_cache.get(node).map(|extent| extent.size())) + } + /// Sets the minimum size of `node`. The actual size of `node` may be larger after `layout()` is called. + fn set_min_size(&mut self, node: Node, w: u32, h: u32) -> Result<(), stretch::Error> { + self.stretch_context.set_measure( + node, + Some(new_measure_func_with_min_sizes(w as f32, h as f32)), + )?; + Ok(()) + } + /// Get the currently set margin for `node`. + fn get_margin(&self, node: Node) -> Result, stretch::Error> { + let style = self.stretch_context.style(node)?; + // A Stretch margin could be in `Points`, `Percent`, `Undefined` or `Auto`. We + // ignore everything but `Points`. + let top = match style.margin.top { + Dimension::Points(v) => v, + _ => 0.0, + }; + let left = match style.margin.start { + Dimension::Points(v) => v, + _ => 0.0, + }; + let bottom = match style.margin.bottom { + Dimension::Points(v) => v, + _ => 0.0, + }; + let right = match style.margin.end { + Dimension::Points(v) => v, + _ => 0.0, + }; - pub fn get_chart_area_size(&self) -> Option<(i32, i32)> { - self.extents_cache.as_ref().and_then(|extents_cache| { - extents_cache - .get(&self.chart_area) - .map(|&(x1, y1, x2, y2)| (x2 - x1, y2 - y1)) + Ok(Margin { + top, + right, + bottom, + left, }) } - pub fn set_chart_area_size( + /// Set the margin of `node`. + fn set_margin>>( &mut self, - w: i32, - h: i32, + node: Node, + margin: M, ) -> Result<(), stretch::Error> { - self.stretch_context.set_measure( - self.chart_area, - Some(new_measure_func_with_min_sizes(w as f32, h as f32)), + let &old_style = self.stretch_context.style(node)?; + let margin: Margin = margin.into(); + self.stretch_context.set_style( + node, + Style { + margin: geometry::Rect { + top: Dimension::Points(margin.top), + bottom: Dimension::Points(margin.bottom), + start: Dimension::Points(margin.left), + end: Dimension::Points(margin.right), + }, + ..old_style + }, )?; + Ok(()) } + /// Get the extent (the upper left and lower right coordinates of the bounding rectangle) of `node`. + fn get_extent(&self, node: &Node) -> Option> { + self.extents_cache + .as_ref() + .and_then(|extents_cache| extents_cache.get(node).map(|extent| extent.clone())) + } // Getters for relevant box sizes impl_get_size!(outer_container); + impl_get_size!(chart_area); impl_get_size!(top_tick_label); impl_get_size!(bottom_tick_label); impl_get_size!(left_tick_label); @@ -399,6 +571,11 @@ impl ChartLayoutNodes { impl_get_size!(bottom_label, inner); impl_get_size!(left_label, inner); impl_get_size!(right_label, inner); + impl_get_margin!(chart_title, inner); + impl_get_margin!(top_label, inner); + impl_get_margin!(bottom_label, inner); + impl_get_margin!(left_label, inner); + impl_get_margin!(right_label, inner); // Setters for relevant box sizes impl_set_size!(top_tick_label); @@ -410,6 +587,11 @@ impl ChartLayoutNodes { impl_set_size!(bottom_label, inner); impl_set_size!(left_label, inner); impl_set_size!(right_label, inner); + impl_set_margin!(chart_title, inner); + impl_set_margin!(top_label, inner); + impl_set_margin!(bottom_label, inner); + impl_set_margin!(left_label, inner); + impl_set_margin!(right_label, inner); } /// Pack a centered title and a label-area together in a row (`FlexDirection::Row`/`RowReverse`) @@ -469,24 +651,24 @@ fn new_measure_func_with_defaults() -> MeasureFunc { } /// When `stretch` computes the layout of a node, its -/// extents are computed relatively to the parent. We want absolute positions, +/// extent are computed relatively to the parent. We want absolute positions, /// so we need to compute them manually. /// * **Returns**: A `HashMap` from nodes to tuples `(x1,y1,x2,y2)` where `(x1,y1)` and `(x2,y2)` represent the upper left and lower right corners of the bounding rectangle. -fn compute_child_extents(stretch: &Stretch, node: Node) -> HashMap { +fn compute_child_extents(stretch: &Stretch, node: Node) -> HashMap> { const DEFAULT_CAPACITY: usize = 16; let mut ret = HashMap::with_capacity(DEFAULT_CAPACITY); fn _compute_child_extents( stretch: &Stretch, node: Node, offset: (i32, i32), - store: &mut HashMap, + store: &mut HashMap>, ) { let layout = stretch.layout(node).unwrap(); let geometry::Point { x, y } = layout.location; let geometry::Size { width, height } = layout.size; let (x1, y1) = (x as i32 + offset.0, y as i32 + offset.1); let (x2, y2) = ((width) as i32 + x1, (height) as i32 + y1); - store.insert(node, (x1, y1, x2, y2)); + store.insert(node, Extent::new(x1, y1, x2, y2)); if stretch.child_count(node).unwrap() > 0 { for child in stretch.children(node).unwrap() { @@ -507,12 +689,12 @@ mod test { let mut layout = ChartLayoutNodes::new().unwrap(); layout.layout(70, 50).unwrap(); let extents_cache = layout.extents_cache.unwrap(); - let &(x1, y1, x2, y2) = extents_cache.get(&layout.chart_area).unwrap(); + let extent = extents_cache.get(&layout.chart_area).unwrap(); - assert_eq!(x1, 0); - assert_eq!(y1, 0); - assert_eq!(x2, 70); - assert_eq!(y2, 50); + assert_eq!(extent.x0, 0); + assert_eq!(extent.y0, 0); + assert_eq!(extent.x1, 70); + assert_eq!(extent.y1, 50); } #[test] /// The default layout should make the chart area take the full area. @@ -559,7 +741,77 @@ mod test { assert_eq!(w, 20); assert_eq!(h, 20); - let (x1, y1, x2, y2) = layout.get_chart_title_extents().unwrap(); - assert_eq!((x1, y1, x2, y2), (25, 0, 45, 20)); + let extent = layout.get_chart_title_extent().unwrap(); + assert_eq!(extent, Extent::new(25, 0, 45, 20)); + } + #[test] + fn set_chart_title_margin() { + let mut layout = ChartLayoutNodes::new().unwrap(); + layout.set_chart_title_size(20, 20).unwrap(); + layout + .set_chart_title_margin(Margin { + top: 10.0, + right: 15.0, + bottom: 20.0, + left: 5.0, + }) + .unwrap(); + + let margin = layout.get_chart_title_margin().unwrap(); + + assert_eq!( + margin, + Margin { + top: 10.0, + right: 15.0, + bottom: 20.0, + left: 5.0, + } + ); + + layout.layout(100, 50).unwrap(); + let extent = layout.get_chart_title_extent().unwrap(); + assert_eq!(extent, Extent::new(35, 10, 55, 30)); + } + + #[test] + fn set_chart_title_margin_with_other_types() { + let mut layout = ChartLayoutNodes::new().unwrap(); + layout.set_chart_title_size(20, 20).unwrap(); + layout.set_chart_title_margin(10.).unwrap(); + let margin = layout.get_chart_title_margin().unwrap(); + assert_eq!( + margin, + Margin { + top: 10.0, + right: 10.0, + bottom: 10.0, + left: 10.0, + } + ); + + layout.set_chart_title_margin((10., 20.)).unwrap(); + let margin = layout.get_chart_title_margin().unwrap(); + assert_eq!( + margin, + Margin { + top: 10.0, + right: 20.0, + bottom: 10.0, + left: 20.0, + } + ); + + layout.set_chart_title_margin((10., 20., 30., 40.)).unwrap(); + let margin = layout.get_chart_title_margin().unwrap(); + assert_eq!( + margin, + Margin { + top: 10.0, + right: 20.0, + bottom: 30.0, + left: 40.0, + } + ); } } diff --git a/src/chart/mod.rs b/src/chart/mod.rs index 7302a4a5..32cb11ea 100644 --- a/src/chart/mod.rs +++ b/src/chart/mod.rs @@ -22,7 +22,7 @@ mod state; mod layout; pub use builder::{ChartBuilder, LabelAreaPosition}; -pub use layout::ChartLayout; +pub use layout::{ChartLayout, Margin}; pub use context::ChartContext; pub use dual_coord::{DualCoordChartContext, DualCoordChartState}; pub use mesh::{MeshStyle, SecondaryMeshStyle}; diff --git a/src/drawing/area.rs b/src/drawing/area.rs index fdc23ab7..de1b18cf 100644 --- a/src/drawing/area.rs +++ b/src/drawing/area.rs @@ -130,6 +130,14 @@ impl Clone for DrawingArea { @@ -139,10 +147,8 @@ pub enum DrawingAreaErrorKind { /// which indicates the drawing backend is current used by other /// drawing operation SharingError, - /// The error caused by invalid layout - LayoutError, - /// Layout error coming from the Stretch library - StretchError(stretch::Error), + /// Error encountered during layout + LayoutError(LayoutError), } impl std::fmt::Display for DrawingAreaErrorKind { @@ -152,8 +158,10 @@ impl std::fmt::Display for DrawingAreaErrorKind { DrawingAreaErrorKind::SharingError => { write!(fmt, "Multiple backend operation in progress") } - DrawingAreaErrorKind::LayoutError => write!(fmt, "Bad layout"), - DrawingAreaErrorKind::StretchError(e) => e.fmt(fmt), + DrawingAreaErrorKind::LayoutError(LayoutError::StretchError(e)) => e.fmt(fmt), + DrawingAreaErrorKind::LayoutError(LayoutError::ExtentsError) => { + write!(fmt, "Could not find the extends of node") + } } } } @@ -162,7 +170,13 @@ impl Error for DrawingAreaErrorKind {} impl From for DrawingAreaErrorKind { fn from(err: stretch::Error) -> Self { - DrawingAreaErrorKind::StretchError(err) + DrawingAreaErrorKind::LayoutError(LayoutError::StretchError(err)) + } +} + +impl From for DrawingAreaErrorKind { + fn from(err: LayoutError) -> Self { + DrawingAreaErrorKind::LayoutError(err) } } diff --git a/src/drawing/mod.rs b/src/drawing/mod.rs index 9e32d913..4d986383 100644 --- a/src/drawing/mod.rs +++ b/src/drawing/mod.rs @@ -13,6 +13,6 @@ about the [coordinate abstraction](../coord/index.html) and [element system](../ mod area; mod backend_impl; -pub use area::{DrawingArea, DrawingAreaErrorKind, IntoDrawingArea, Rect}; +pub use area::{DrawingArea, DrawingAreaErrorKind, IntoDrawingArea, LayoutError, Rect}; pub use backend_impl::*; diff --git a/src/style/text.rs b/src/style/text.rs index 374a5ce8..30005516 100644 --- a/src/style/text.rs +++ b/src/style/text.rs @@ -15,6 +15,7 @@ pub struct TextStyle<'a> { /// The anchor point position pub pos: text_anchor::Pos, } + pub trait IntoTextStyle<'a> { fn into_text_style(self, parent: &P) -> TextStyle<'a>; From 353c6a6141afee617bc1b5c39b29c715ef9efc99 Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Fri, 28 May 2021 16:38:38 -0400 Subject: [PATCH 04/11] Add accessor for chart_area drawing area --- examples/layout.rs | 46 +++++++++++++++++++++++++-------- src/chart/context.rs | 2 +- src/chart/layout/mod.rs | 12 +++++++++ src/chart/layout/nodes.rs | 1 - src/coord/ranged1d/mod.rs | 6 ++--- src/coord/ranged2d/cartesian.rs | 2 +- 6 files changed, 52 insertions(+), 17 deletions(-) diff --git a/examples/layout.rs b/examples/layout.rs index 33c4c9d6..fe8e4eb9 100644 --- a/examples/layout.rs +++ b/examples/layout.rs @@ -11,20 +11,44 @@ fn main() -> Result<(), Box> { let mut chart = ChartLayout::new(&root); chart .set_chart_title_text("Chart Title")? - .set_top_label_text("A label at the top")? .set_chart_title_style(("serif", 60.).into_font().with_color(&RED))? - .set_left_label_text("Left label")? + .set_left_label_text("Ratio of Sides")? .set_right_label_text("Right label")? - .set_bottom_label_text("Bottom label")? + .set_bottom_label_text("Radians")? .set_bottom_label_margin(10.)? - .set_top_label_margin(10.)? - .draw()?; - - let extent = chart.get_chart_area_extent()?; - root.draw(&Rectangle::new(extent.into_array_of_tuples(), &BLUE))?; - let extent = chart.get_chart_title_extent()?; - root.draw(&Rectangle::new(extent.into_array_of_tuples(), &BLUE))?; - //dbg!(); + .set_left_label_margin(10.)? + .set_right_label_margin(10.)? + .draw()?; + + // If we extract a drawing area corresponding to a chart area, we can + // use the usual chart API to draw. + let da_chart = chart.get_chart_drawing_area()?; + let x_axis = (-3.4f32..3.4).step(0.1); + let mut cc = ChartBuilder::on(&da_chart) + .margin(5) + .set_all_label_area_size(15) + .build_cartesian_2d(-3.4f32..3.4, -1.2f32..1.2f32)?; + + cc.configure_mesh() + .x_labels(20) + .y_labels(10) + .disable_mesh() + .x_label_formatter(&|v| format!("{:.1}", v)) + .y_label_formatter(&|v| format!("{:.1}", v)) + .draw()?; + + cc.draw_series(LineSeries::new(x_axis.values().map(|x| (x, x.sin())), &RED))? + .label("Sine") + .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &RED)); + + cc.draw_series(LineSeries::new( + x_axis.values().map(|x| (x, x.cos())), + &BLUE, + ))? + .label("Cosine") + .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &BLUE)); + + cc.configure_series_labels().border_style(&BLACK).draw()?; Ok(()) } diff --git a/src/chart/context.rs b/src/chart/context.rs index 7e0045e0..e202b330 100644 --- a/src/chart/context.rs +++ b/src/chart/context.rs @@ -311,7 +311,7 @@ impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesia .map(|(w, _)| w) .unwrap_or(0) as i32 } else { - // Don't ever do the layout estimationfor the drawing area that is either not + // Don't ever do the layout estimation for the drawing area that is either not // the right one or the tick mark is inward. 0 } diff --git a/src/chart/layout/mod.rs b/src/chart/layout/mod.rs index 1c9ad299..b7292009 100644 --- a/src/chart/layout/mod.rs +++ b/src/chart/layout/mod.rs @@ -256,6 +256,18 @@ impl<'a, 'b, DB: DrawingBackend> ChartLayout<'a, 'b, DB> { Ok(self) } + /// Return a drawing area which corresponds to the `chart_area` of the current layout. + /// [`layout`] should be called before this function. + pub fn get_chart_drawing_area( + &mut self, + ) -> Result, DrawingAreaErrorKind> { + let chart_area_extent = self.get_chart_area_extent()?; + Ok(DrawingArea::clone(self.root_area).shrink( + (chart_area_extent.x0, chart_area_extent.y0), + chart_area_extent.size(), + )) + } + /// Set the text of a label. If the text is a blank screen, the label is cleared. #[inline(always)] fn set_text>(elm: &mut Label, text: S) { diff --git a/src/chart/layout/nodes.rs b/src/chart/layout/nodes.rs index 5ebfbc2a..c65d0447 100644 --- a/src/chart/layout/nodes.rs +++ b/src/chart/layout/nodes.rs @@ -1,5 +1,4 @@ use paste::paste; -use std::iter::once; use std::{ collections::HashMap, ops::{Add, Sub}, diff --git a/src/coord/ranged1d/mod.rs b/src/coord/ranged1d/mod.rs index 06de6bfd..2ac893df 100644 --- a/src/coord/ranged1d/mod.rs +++ b/src/coord/ranged1d/mod.rs @@ -109,7 +109,7 @@ impl KeyPointWeight { /// The trait for a hint provided to the key point algorithm used by the coordinate specs. /// The most important constraint is the `max_num_points` which means the algorithm could emit no more than specific number of key points /// `weight` is used to determine if this is used as a bold grid line or light grid line -/// `bold_points` returns the max number of coresponding bold grid lines +/// `bold_points` returns the max number of corresponding bold grid lines pub trait KeyPointHint { /// Returns the max number of key points fn max_num_points(&self) -> usize; @@ -178,12 +178,12 @@ impl KeyPointHint for LightPoints { /// Which is used to describe any 1D axis. pub trait Ranged { /// This marker decides if Plotters default [ValueFormatter](trait.ValueFormatter.html) implementation should be used. - /// This assicated type can be one of follow two types: + /// This associated type can be one of follow two types: /// - [DefaultFormatting](struct.DefaultFormatting.html) will allow Plotters automatically impl /// the formatter based on `Debug` trait, if `Debug` trait is not impl for the `Self::Value`, /// [ValueFormatter](trait.ValueFormatter.html) will not impl unless you impl it manually. /// - /// - [NoDefaultFormatting](struct.NoDefaultFormatting.html) Disable the automatical `Debug` + /// - [NoDefaultFormatting](struct.NoDefaultFormatting.html) Disable the automatic `Debug` /// based value formatting. Thus you have to impl the /// [ValueFormatter](trait.ValueFormatter.html) manually. /// diff --git a/src/coord/ranged2d/cartesian.rs b/src/coord/ranged2d/cartesian.rs index 34d2abfa..c679dc9c 100644 --- a/src/coord/ranged2d/cartesian.rs +++ b/src/coord/ranged2d/cartesian.rs @@ -2,7 +2,7 @@ The 2-dimensional cartesian coordinate system. This module provides the 2D cartesian coordinate system, which is composed by two independent - ranged 1D coordinate sepcification. + ranged 1D coordinate specification. This types of coordinate system is used by the chart constructed with [ChartBuilder::build_cartesian_2d](../../chart/ChartBuilder.html#method.build_cartesian_2d). */ From 9938d7bfb98a5b37a5807285af9061954c7650be Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Fri, 28 May 2021 16:49:09 -0400 Subject: [PATCH 05/11] Fix incorrect calculation of drawing area pixels --- src/chart/layout/mod.rs | 34 +++++++++------------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/src/chart/layout/mod.rs b/src/chart/layout/mod.rs index b7292009..71985581 100644 --- a/src/chart/layout/mod.rs +++ b/src/chart/layout/mod.rs @@ -23,7 +23,7 @@ macro_rules! impl_get_extent { .[]() .ok_or_else(|| LayoutError::ExtentsError)?; - Ok(self.compute_absolute_pixel_coords(extent)) + Ok(extent) } #[doc = "Get the size of the `" $name "` container."] #[doc = " * **Returns**: An option containing a tuple `(width, height)`."] @@ -181,13 +181,6 @@ impl<'a, 'b, DB: DrawingBackend> ChartLayout<'a, 'b, DB> { } } - /// Translate the `extent` given in local pixel coordinates to be given - /// in absolute pixel coordinates. - fn compute_absolute_pixel_coords(&self, extent: Extent) -> Extent { - let (x_range, y_range) = self.root_area.get_pixel_range(); - extent.translate((x_range.start, y_range.start)) - } - pub fn draw(&mut self) -> Result<&mut Self, DrawingAreaErrorKind> { let (x_range, y_range) = self.root_area.get_pixel_range(); let (x_top, y_top) = (x_range.start, y_range.start); @@ -200,11 +193,8 @@ impl<'a, 'b, DB: DrawingBackend> ChartLayout<'a, 'b, DB> { .nodes .get_chart_title_extent() .ok_or_else(|| LayoutError::ExtentsError)?; - self.root_area.draw_text( - text, - &self.chart_title.style, - (extent.x0 + x_top, extent.y0 + y_top), - )?; + self.root_area + .draw_text(text, &self.chart_title.style, (extent.x0, extent.y0))?; } if let Some(text) = self.top_label.text.as_ref() { @@ -212,22 +202,16 @@ impl<'a, 'b, DB: DrawingBackend> ChartLayout<'a, 'b, DB> { .nodes .get_top_label_extent() .ok_or_else(|| LayoutError::ExtentsError)?; - self.root_area.draw_text( - text, - &self.top_label.style, - (extent.x0 + x_top, extent.y0 + y_top), - )?; + self.root_area + .draw_text(text, &self.top_label.style, (extent.x0, extent.y0))?; } if let Some(text) = self.bottom_label.text.as_ref() { let extent = self .nodes .get_bottom_label_extent() .ok_or_else(|| LayoutError::ExtentsError)?; - self.root_area.draw_text( - text, - &self.bottom_label.style, - (extent.x0 + x_top, extent.y0 + y_top), - )?; + self.root_area + .draw_text(text, &self.bottom_label.style, (extent.x0, extent.y0))?; } // Draw the vertically oriented labels if let Some(text) = self.left_label.text.as_ref() { @@ -238,7 +222,7 @@ impl<'a, 'b, DB: DrawingBackend> ChartLayout<'a, 'b, DB> { self.root_area.draw_text( text, &self.left_label.style.transform(FontTransform::Rotate270), - (extent.x0 + x_top, extent.y1 + y_top), + (extent.x0, extent.y1), )?; } if let Some(text) = self.right_label.text.as_ref() { @@ -249,7 +233,7 @@ impl<'a, 'b, DB: DrawingBackend> ChartLayout<'a, 'b, DB> { self.root_area.draw_text( text, &self.right_label.style.transform(FontTransform::Rotate270), - (extent.x0 + x_top, extent.y1 + y_top), + (extent.x0, extent.y1), )?; } From 79ec8f35517ff71cbc135397260fd190427d1203 Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Fri, 28 May 2021 17:34:38 -0400 Subject: [PATCH 06/11] Spelling fixes --- src/coord/ranged1d/combinators/logarithmic.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/coord/ranged1d/combinators/logarithmic.rs b/src/coord/ranged1d/combinators/logarithmic.rs index 84b64bf5..1b17c48a 100644 --- a/src/coord/ranged1d/combinators/logarithmic.rs +++ b/src/coord/ranged1d/combinators/logarithmic.rs @@ -75,7 +75,7 @@ impl IntoLogRange for Range { } } -/// The logarithmic coodinate decorator. +/// The logarithmic coordinate decorator. /// This decorator is used to make the axis rendered as logarithmically. #[derive(Clone)] pub struct LogRangeExt { @@ -100,7 +100,7 @@ impl LogRangeExt { self } - /// Set the base multipler + /// Set the base multiplier pub fn base(mut self, base: f64) -> Self { if self.base > 1.0 { self.base = base; @@ -254,7 +254,7 @@ impl Ranged for LogCoord { } } -/// The logarithmic coodinate decorator. +/// The logarithmic coordinate decorator. /// This decorator is used to make the axis rendered as logarithmically. #[deprecated(note = "LogRange is deprecated, use IntoLogRange trait method instead")] #[derive(Clone)] From 45cb820e6723628714e4c1fa38e4e3fe387ee49c Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Wed, 2 Jun 2021 13:32:30 -0400 Subject: [PATCH 07/11] Added auto-sizing axes --- examples/layout.rs | 31 +- src/chart/layout/mod.rs | 590 ++++++++++++++++++++++++++++++++++++ src/chart/layout/nodes.rs | 30 +- src/coord/mod.rs | 1 + src/element/basic_shapes.rs | 43 +++ 5 files changed, 671 insertions(+), 24 deletions(-) diff --git a/examples/layout.rs b/examples/layout.rs index fe8e4eb9..37597e61 100644 --- a/examples/layout.rs +++ b/examples/layout.rs @@ -8,46 +8,33 @@ fn main() -> Result<(), Box> { let root = BitMapBackend::new(OUT_FILE_NAME, (W, H)).into_drawing_area(); root.fill(&full_palette::WHITE)?; + let x_spec = -3.1..3.01f32; + let y_spec = -1.1..1.1f32; + let mut chart = ChartLayout::new(&root); chart .set_chart_title_text("Chart Title")? .set_chart_title_style(("serif", 60.).into_font().with_color(&RED))? .set_left_label_text("Ratio of Sides")? - .set_right_label_text("Right label")? .set_bottom_label_text("Radians")? .set_bottom_label_margin(10.)? - .set_left_label_margin(10.)? - .set_right_label_margin(10.)? + .set_left_label_margin((0., -5., 0., 10.))? + .build_cartesian_2d(x_spec.clone(), y_spec.clone())? .draw()?; // If we extract a drawing area corresponding to a chart area, we can // use the usual chart API to draw. let da_chart = chart.get_chart_drawing_area()?; - let x_axis = (-3.4f32..3.4).step(0.1); + let x_axis = x_spec.clone().step(0.1); let mut cc = ChartBuilder::on(&da_chart) - .margin(5) - .set_all_label_area_size(15) - .build_cartesian_2d(-3.4f32..3.4, -1.2f32..1.2f32)?; - - cc.configure_mesh() - .x_labels(20) - .y_labels(10) - .disable_mesh() - .x_label_formatter(&|v| format!("{:.1}", v)) - .y_label_formatter(&|v| format!("{:.1}", v)) - .draw()?; + //.margin(5) + //.set_all_label_area_size(15) + .build_cartesian_2d(x_spec.clone(), y_spec.clone())?; cc.draw_series(LineSeries::new(x_axis.values().map(|x| (x, x.sin())), &RED))? .label("Sine") .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &RED)); - cc.draw_series(LineSeries::new( - x_axis.values().map(|x| (x, x.cos())), - &BLUE, - ))? - .label("Cosine") - .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &BLUE)); - cc.configure_series_labels().border_style(&BLACK).draw()?; Ok(()) diff --git a/src/chart/layout/mod.rs b/src/chart/layout/mod.rs index 71985581..ccd8031b 100644 --- a/src/chart/layout/mod.rs +++ b/src/chart/layout/mod.rs @@ -1,5 +1,17 @@ +use std::ops::Range; + +use crate::coord::ticks::Tick; +use crate::coord::ticks::{ + suggest_tickmark_spacing_for_range, AxisTickEnumerator, SimpleLinearAxis, TickKind, +}; +use crate::element::LineSegment; +use crate::style::colors; +use crate::style::Color; use crate::{coord::Shift, style::IntoTextStyle}; use paste::paste; +use plotters_backend::text_anchor::HPos; +use plotters_backend::text_anchor::Pos; +use plotters_backend::text_anchor::VPos; use crate::drawing::{DrawingArea, DrawingAreaErrorKind, LayoutError}; use crate::style::{FontTransform, IntoFont, TextStyle}; @@ -10,6 +22,13 @@ mod nodes; pub use nodes::Margin; use nodes::*; +enum AxisSide { + Left, + Right, + Top, + Bottom, +} + /// Create the `get__extent` and `get__size` functions macro_rules! impl_get_extent { ($name:ident) => { @@ -137,6 +156,26 @@ struct Label<'a> { style: TextStyle<'a>, } +/// Stores the range of the tick labels for every +/// side of the chart area. +#[derive(Clone)] +struct AxisSpecs { + left: Option, + right: Option, + top: Option, + bottom: Option, +} +impl AxisSpecs { + pub fn new_blank() -> Self { + AxisSpecs { + left: None, + right: None, + top: None, + bottom: None, + } + } +} + /// The helper object to create a chart context, which is used for the high-level figure drawing. /// With the help of this object, we can convert a basic drawing area into a chart context, which /// allows the high-level charting API being used on the drawing area. @@ -148,6 +187,7 @@ pub struct ChartLayout<'a, 'b, DB: DrawingBackend> { left_label: Label<'b>, right_label: Label<'b>, nodes: ChartLayoutNodes, + axis_ranges: AxisSpecs>, } impl<'a, 'b, DB: DrawingBackend> ChartLayout<'a, 'b, DB> { @@ -178,6 +218,7 @@ impl<'a, 'b, DB: DrawingBackend> ChartLayout<'a, 'b, DB> { style: TextStyle::from(("serif", 25.0).into_font()), }, nodes: ChartLayoutNodes::new().unwrap(), + axis_ranges: AxisSpecs::new_blank(), } } @@ -187,6 +228,51 @@ impl<'a, 'b, DB: DrawingBackend> ChartLayout<'a, 'b, DB> { let (w, h) = (x_range.end - x_top, y_range.end - y_top); self.nodes.layout(w as u32, h as u32)?; + let label_style = TextStyle::from(("sans", 16.0).into_font()); + let label_formatter = |label: f32| format!("{:1.}", label); + self.layout_axes(w as u32, h as u32, &label_style, &label_formatter)?; + + self.draw_ticks_helper(|pixel_coords, tick, axis_side| { + draw_tick( + pixel_coords, + axis_side, + tick.kind, + label_formatter(tick.label), + &label_style, + &self.root_area, + )?; + + Ok(()) + })?; + // Draw the chart border for each set of labels we have + let chart_area_extent = self + .nodes + .get_chart_area_extent() + .ok_or_else(|| LayoutError::ExtentsError)?; + let (x0, y0, x1, y1) = ( + chart_area_extent.x0, + chart_area_extent.y0, + chart_area_extent.x1, + chart_area_extent.y1, + ); + let axis_shape_style = Color::stroke_width(&colors::BLACK, 1); + if self.axis_ranges.left.is_some() { + self.root_area + .draw(&LineSegment::new([(x0, y0), (x0, y1)], &axis_shape_style))?; + } + if self.axis_ranges.right.is_some() { + self.root_area + .draw(&LineSegment::new([(x1, y0), (x1, y1)], &axis_shape_style))?; + } + if self.axis_ranges.top.is_some() { + self.root_area + .draw(&LineSegment::new([(x0, y0), (x1, y0)], &axis_shape_style))?; + } + if self.axis_ranges.bottom.is_some() { + self.root_area + .draw(&LineSegment::new([(x0, y1), (x1, y1)], &axis_shape_style))?; + } + // Draw the horizontally oriented labels if let Some(text) = self.chart_title.text.as_ref() { let extent = self @@ -240,6 +326,302 @@ impl<'a, 'b, DB: DrawingBackend> ChartLayout<'a, 'b, DB> { Ok(self) } + /// Decide how much space each of the axes will take up and allocate that space. + /// This function assumes `self.nodes.layout()` has already been called once. + fn layout_axes( + &mut self, + canvas_w: u32, + canvas_h: u32, + label_style: &TextStyle, + label_formatter: F, + ) -> Result<(), DrawingAreaErrorKind> + where + F: Fn(f32) -> String, + { + // After the initial layout, we compute how much space each + // axis should take. We estimate the size of the left/right axes first, + // because their labels are more impactful to the overall layout. + let mut left_tick_labels_extent = self + .nodes + .get_left_tick_label_extent() + .ok_or_else(|| LayoutError::ExtentsError)?; + let mut right_tick_labels_extent = self + .nodes + .get_right_tick_label_extent() + .ok_or_else(|| LayoutError::ExtentsError)?; + self.draw_ticks_helper(|pixel_coords, tick, axis_side| { + match axis_side { + AxisSide::Left => { + // Expand the extent to contain the drawn tickmark + let tick_extent = compute_tick_extent( + axis_side, + tick.kind, + label_formatter(tick.label), + &label_style, + &self.root_area, + ) + .ok_or_else(|| LayoutError::ExtentsError)? + .translate(pixel_coords); + + left_tick_labels_extent.union_mut(&tick_extent); + } + AxisSide::Right => { + // Expand the extent to contain the drawn tickmark + let tick_extent = compute_tick_extent( + axis_side, + tick.kind, + label_formatter(tick.label), + &label_style, + &self.root_area, + ) + .ok_or_else(|| LayoutError::ExtentsError)? + .translate(pixel_coords); + + right_tick_labels_extent.union_mut(&tick_extent); + } + _ => {} + } + + Ok(()) + })?; + let (axis_w, _axis_h) = left_tick_labels_extent.size(); + self.nodes.set_left_tick_label_size(axis_w as u32, 0)?; + let (axis_w, _axis_h) = right_tick_labels_extent.size(); + self.nodes.set_right_tick_label_size(axis_w as u32, 0)?; + + // Now the the left/right tick label sizes have been computed, we can compute + // the top/bottom tick label sizes, taking into account the left/right + self.nodes.layout(canvas_w as u32, canvas_h as u32)?; + let mut top_tick_labels_extent = self + .nodes + .get_top_tick_label_extent() + .ok_or_else(|| LayoutError::ExtentsError)?; + let mut bottom_tick_labels_extent = self + .nodes + .get_bottom_tick_label_extent() + .ok_or_else(|| LayoutError::ExtentsError)?; + self.draw_ticks_helper(|pixel_coords, tick, axis_side| { + match axis_side { + AxisSide::Top => { + // Expand the extent to contain the drawn tickmark + let tick_extent = compute_tick_extent( + axis_side, + tick.kind, + label_formatter(tick.label), + &label_style, + &self.root_area, + ) + .ok_or_else(|| LayoutError::ExtentsError)? + .translate(pixel_coords); + + top_tick_labels_extent.union_mut(&tick_extent); + } + AxisSide::Bottom => { + // Expand the extent to contain the drawn tickmark + let tick_extent = compute_tick_extent( + axis_side, + tick.kind, + label_formatter(tick.label), + &label_style, + &self.root_area, + ) + .ok_or_else(|| LayoutError::ExtentsError)? + .translate(pixel_coords); + + bottom_tick_labels_extent.union_mut(&tick_extent); + } + _ => {} + } + + Ok(()) + })?; + let (_axis_w, axis_h) = top_tick_labels_extent.size(); + self.nodes.set_top_tick_label_size(0, axis_h as u32)?; + let (_axis_w, axis_h) = bottom_tick_labels_extent.size(); + self.nodes.set_bottom_tick_label_size(0, axis_h as u32)?; + + // Now that the spacing has been computed, re-layout the axes and actually draw them. + self.nodes.layout(canvas_w as u32, canvas_h as u32)?; + + // It may be the case that parts of the label text "spill over" into the margin + // to the left/right of the chart_area. We want to make sure that we're + // not spilling over off the drawing_area. + let left_spill = top_tick_labels_extent.x0.min(bottom_tick_labels_extent.x0); + if left_spill < 0 { + let (w, h) = self + .nodes + .get_left_tick_label_size() + .ok_or_else(|| LayoutError::ExtentsError)?; + + self.nodes + .set_left_tick_label_size((w - left_spill) as u32, h as u32)?; + } + let right_spill = + canvas_w as i32 - top_tick_labels_extent.x1.max(bottom_tick_labels_extent.x1); + if right_spill < 0 { + let (w, h) = self + .nodes + .get_right_tick_label_size() + .ok_or_else(|| LayoutError::ExtentsError)?; + + self.nodes + .set_right_tick_label_size((w - right_spill) as u32, h as u32)?; + } + + // Layouts are cached, so if we didn't change anything, this is a very cheap function call + self.nodes.layout(canvas_w as u32, canvas_h as u32)?; + + // It may be the case that parts of the label text "spill over" into the margin + // to the top/bottom of the chart_area. We want to make sure that we're + // not spilling over off the drawing_area. + let top_spill = left_tick_labels_extent.y0.min(right_tick_labels_extent.y0); + if top_spill < 0 { + let (w, h) = self + .nodes + .get_top_tick_label_size() + .ok_or_else(|| LayoutError::ExtentsError)?; + + // When the left/right tick label extents were computed, the bottom/top tick labels + // had zero size. We only want to increase their size if needed. Otherwise, we should + // leave them the size they are. + self.nodes + .set_top_tick_label_size(w as u32, h.max(-top_spill) as u32)?; + } + let bottom_spill = + canvas_h as i32 - left_tick_labels_extent.y1.max(right_tick_labels_extent.y1); + if bottom_spill < 0 { + let (w, h) = self + .nodes + .get_bottom_tick_label_size() + .ok_or_else(|| LayoutError::ExtentsError)?; + + // When the left/right tick label extents were computed, the bottom/top tick labels + // had zero size. We only want to increase their size if needed. Otherwise, we should + // leave them the size they are. + self.nodes + .set_bottom_tick_label_size(w as u32, h.max(-bottom_spill) as u32)?; + } + + // Layouts are cached, so if we didn't change anything, this is a very cheap function call + self.nodes.layout(canvas_w as u32, canvas_h as u32)?; + + Ok(()) + } + + /// Helper function for drawing ticks. This function will call + /// `draw_func(pixel_coords, tick, axis_side)` for every tick on every axis. + fn draw_ticks_helper( + &self, + mut draw_func: F, + ) -> Result<(), DrawingAreaErrorKind> + where + F: FnMut( + (i32, i32), + Tick, + AxisSide, + ) -> Result<(), DrawingAreaErrorKind>, + { + let suggestion = self.suggest_tickmark_spacing_for_axes()?; + if let Some(axis) = suggestion.left { + let extent = self + .nodes + .get_chart_area_extent() + .ok_or_else(|| LayoutError::ExtentsError)?; + let start = self.axis_ranges.left.as_ref().unwrap().start; + let end = self.axis_ranges.left.as_ref().unwrap().end; + for tick in axis.iter() { + // Find out where the tick is to be drawn. + let y_pos = scale_to_pixel(tick.pos, start, end, extent.y0, extent.y1); + draw_func((extent.x0, y_pos), tick, AxisSide::Left)?; + } + } + if let Some(axis) = suggestion.right { + let extent = self + .nodes + .get_chart_area_extent() + .ok_or_else(|| LayoutError::ExtentsError)?; + let start = self.axis_ranges.right.as_ref().unwrap().start; + let end = self.axis_ranges.right.as_ref().unwrap().end; + for tick in axis.iter() { + // Find out where the tick is to be drawn. + let y_pos = scale_to_pixel(tick.pos, start, end, extent.y0, extent.y1); + draw_func((extent.x1, y_pos), tick, AxisSide::Right)?; + } + } + if let Some(axis) = suggestion.top { + let extent = self + .nodes + .get_chart_area_extent() + .ok_or_else(|| LayoutError::ExtentsError)?; + let start = self.axis_ranges.top.as_ref().unwrap().start; + let end = self.axis_ranges.top.as_ref().unwrap().end; + for tick in axis.iter() { + // Find out where the tick is to be drawn. + let x_pos = scale_to_pixel(tick.pos, start, end, extent.x0, extent.x1); + draw_func((x_pos, extent.y0), tick, AxisSide::Top)?; + } + } + if let Some(axis) = suggestion.bottom { + let extent = self + .nodes + .get_chart_area_extent() + .ok_or_else(|| LayoutError::ExtentsError)?; + let start = self.axis_ranges.bottom.as_ref().unwrap().start; + let end = self.axis_ranges.bottom.as_ref().unwrap().end; + for tick in axis.iter() { + // Find out where the tick is to be drawn. + let x_pos = scale_to_pixel(tick.pos, start, end, extent.x0, extent.x1); + draw_func((x_pos, extent.y1), tick, AxisSide::Bottom)?; + } + } + + Ok(()) + } + + /// Use some heuristics to guess the best tick spacing given the area we have. + fn suggest_tickmark_spacing_for_axes( + &self, + ) -> Result>, DrawingAreaErrorKind> { + let da_extent = self.get_chart_area_extent()?; + let (w, h) = da_extent.size(); + let ret = AxisSpecs { + top: self + .axis_ranges + .top + .as_ref() + .map(|range| suggest_tickmark_spacing_for_range(range, w)), + bottom: self + .axis_ranges + .bottom + .as_ref() + .map(|range| suggest_tickmark_spacing_for_range(range, w)), + left: self + .axis_ranges + .left + .as_ref() + .map(|range| suggest_tickmark_spacing_for_range(range, h)), + right: self + .axis_ranges + .right + .as_ref() + .map(|range| suggest_tickmark_spacing_for_range(range, h)), + }; + Ok(ret) + } + + /// Apply a cartesian grid to the chart area. Major/minor ticks are automatically + /// determined on a call to `draw`. + pub fn build_cartesian_2d( + &mut self, + x_spec: Range, + y_spec: Range, + ) -> Result<&mut Self, DrawingAreaErrorKind> { + self.axis_ranges.left = Some(y_spec.clone()); + self.axis_ranges.bottom = Some(x_spec.clone()); + + Ok(self) + } + /// Return a drawing area which corresponds to the `chart_area` of the current layout. /// [`layout`] should be called before this function. pub fn get_chart_drawing_area( @@ -289,6 +671,194 @@ impl<'a, 'b, DB: DrawingBackend> ChartLayout<'a, 'b, DB> { impl_label_vert!(right_label); } +/// Scale `val` which is in an interval between `a` and `b` to be within an interval between `pixel_a` and `pixel_b` +fn scale_to_pixel(val: f32, a: f32, b: f32, pixel_a: i32, pixel_b: i32) -> i32 { + ((val - a) / (b - a) * (pixel_b - pixel_a) as f32) as i32 + pixel_a +} +const MAJOR_TICK_LEN: i32 = 5; +const MINOR_TICK_LEN: i32 = 3; +const TICK_LABEL_PADDING: i32 = 3; + +/// Compute the extents of the given tick kind/label. The extents are computed +/// as if the tick were drawn at (0,0). It should be translated for other uses. +fn compute_tick_extent>( + axis_side: AxisSide, + tick_kind: TickKind, + label: S, + label_style: &TextStyle, + drawing_area: &DrawingArea, +) -> Option> { + let mut extent = Extent::new_with_size(0, 0); + match tick_kind { + // For a major tickmark we extend the extent by the tick itself and the area the tick label takes up + TickKind::Major => { + if let Ok((w, h)) = drawing_area.estimate_text_size(label.as_ref(), label_style) { + match axis_side { + AxisSide::Left => { + extent.union_mut(&Extent::new_with_size(-MAJOR_TICK_LEN, 0)); + extent.union_mut( + &Extent::new_with_size(-(w as i32), h as i32 + 2 * TICK_LABEL_PADDING) + .translate(( + -MAJOR_TICK_LEN - 2 * TICK_LABEL_PADDING, + -((h as f32) / 2.0) as i32 - TICK_LABEL_PADDING, + )), + ); + } + AxisSide::Right => { + extent.union_mut(&Extent::new_with_size(MAJOR_TICK_LEN, 0)); + extent.union_mut( + &Extent::new_with_size(w as i32, h as i32 + 2 * TICK_LABEL_PADDING) + .translate(( + MAJOR_TICK_LEN + 2 * TICK_LABEL_PADDING, + -((h as f32) / 2.0) as i32 - TICK_LABEL_PADDING, + )), + ); + } + AxisSide::Top => { + extent.union_mut(&Extent::new_with_size(0, -MAJOR_TICK_LEN)); + extent.union_mut( + &Extent::new_with_size(w as i32 + 2 * TICK_LABEL_PADDING, -(h as i32)) + .translate(( + -((w as f32) / 2.0) as i32 - TICK_LABEL_PADDING, + -MAJOR_TICK_LEN - 2 * TICK_LABEL_PADDING, + )), + ); + } + AxisSide::Bottom => { + extent.union_mut(&Extent::new_with_size(0, MAJOR_TICK_LEN)); + extent.union_mut( + &Extent::new_with_size(w as i32 + 2 * TICK_LABEL_PADDING, h as i32) + .translate(( + -((w as f32) / 2.0) as i32 - TICK_LABEL_PADDING, + MAJOR_TICK_LEN + 2 * TICK_LABEL_PADDING, + )), + ); + } + } + Some(extent) + } else { + None + } + } + TickKind::Minor => { + match axis_side { + AxisSide::Left => { + extent.union_mut(&Extent::new_with_size(-MINOR_TICK_LEN, 0)); + } + AxisSide::Right => { + extent.union_mut(&Extent::new_with_size(MINOR_TICK_LEN, 0)); + } + AxisSide::Top => { + extent.union_mut(&Extent::new_with_size(0, -MINOR_TICK_LEN)); + } + AxisSide::Bottom => { + extent.union_mut(&Extent::new_with_size(0, MINOR_TICK_LEN)); + } + } + Some(extent) + } + } +} + +/// Draw the tick at the correct location. +fn draw_tick>( + pixel_coords: (i32, i32), + axis_side: AxisSide, + tick_kind: TickKind, + label_text: S, + label_style: &TextStyle, + drawing_area: &DrawingArea, +) -> Result<(), DrawingAreaErrorKind> { + let tick_len = match tick_kind { + TickKind::Major => MAJOR_TICK_LEN, + TickKind::Minor => MINOR_TICK_LEN, + }; + match axis_side { + AxisSide::Left => { + drawing_area.draw(&LineSegment::new( + [ + (pixel_coords.0 - tick_len, pixel_coords.1), + (pixel_coords.0, pixel_coords.1), + ], + &colors::BLACK.into(), + ))?; + // On a major tick, we draw a label + if tick_kind == TickKind::Major { + drawing_area.draw_text( + label_text.as_ref(), + &label_style.pos(Pos::new(HPos::Right, VPos::Center)), + ( + pixel_coords.0 - MAJOR_TICK_LEN - TICK_LABEL_PADDING, + pixel_coords.1, + ), + )?; + } + } + AxisSide::Right => { + drawing_area.draw(&LineSegment::new( + [ + (pixel_coords.0, pixel_coords.1), + (pixel_coords.0 + tick_len, pixel_coords.1), + ], + &colors::BLACK.into(), + ))?; + // On a major tick, we draw a label + if tick_kind == TickKind::Major { + drawing_area.draw_text( + label_text.as_ref(), + &label_style.pos(Pos::new(HPos::Left, VPos::Center)), + ( + pixel_coords.0 + MAJOR_TICK_LEN + TICK_LABEL_PADDING, + pixel_coords.1, + ), + )?; + } + } + AxisSide::Top => { + drawing_area.draw(&LineSegment::new( + [ + (pixel_coords.0, pixel_coords.1 - tick_len), + (pixel_coords.0, pixel_coords.1), + ], + &colors::BLACK.into(), + ))?; + // On a major tick, we draw a label + if tick_kind == TickKind::Major { + drawing_area.draw_text( + label_text.as_ref(), + &label_style.pos(Pos::new(HPos::Center, VPos::Bottom)), + ( + pixel_coords.0, + pixel_coords.1 - MAJOR_TICK_LEN - TICK_LABEL_PADDING, + ), + )?; + } + } + AxisSide::Bottom => { + drawing_area.draw(&LineSegment::new( + [ + (pixel_coords.0, pixel_coords.1 + tick_len), + (pixel_coords.0, pixel_coords.1), + ], + &colors::BLACK.into(), + ))?; + // On a major tick, we draw a label + if tick_kind == TickKind::Major { + drawing_area.draw_text( + label_text.as_ref(), + &label_style.pos(Pos::new(HPos::Center, VPos::Top)), + ( + pixel_coords.0, + pixel_coords.1 + MAJOR_TICK_LEN + TICK_LABEL_PADDING, + ), + )?; + } + } + } + + Ok(()) +} + #[cfg(test)] mod test { use super::*; @@ -397,4 +967,24 @@ mod test { let size = extent.size(); assert!(size.1 > size.0); } + + #[test] + fn test_adding_axes_should_take_up_room() { + let drawing_area = create_mocked_drawing_area(800, 600, |_| {}); + let mut chart = ChartLayout::new(&drawing_area); + chart + .build_cartesian_2d(0.0f32..100.0, 0.0f32..100.0f32) + .unwrap(); + chart.draw().unwrap(); + + let extent = chart.get_bottom_tick_label_extent().unwrap(); + assert!(extent_has_size(extent)); + + let extent = chart.get_left_tick_label_extent().unwrap(); + assert!(extent_has_size(extent)); + + let extent = chart.get_chart_area_extent().unwrap(); + assert!(extent.x0 > 0); + assert!(extent.y1 < 600); + } } diff --git a/src/chart/layout/nodes.rs b/src/chart/layout/nodes.rs index c65d0447..30927ba0 100644 --- a/src/chart/layout/nodes.rs +++ b/src/chart/layout/nodes.rs @@ -32,16 +32,26 @@ impl Numeric for usize {} /// An `Extent` stores the upper left and bottom right corner of a rectangular region #[derive(Clone, Debug, PartialEq)] -pub struct Extent { +pub struct Extent { pub x0: T, pub y0: T, pub x1: T, pub y1: T, } -impl + Sub + Copy> Extent { +impl + Sub + Copy + Ord> Extent { pub fn new(x0: T, y0: T, x1: T, y1: T) -> Self { Self { x0, y0, x1, y1 } } + /// Creates a new extent with upper-left corner at (0,0) and width/height given by `w`/`h`. + pub fn new_with_size(w: T, h: T) -> Self { + let zero = w - w; + Self { + x0: zero, + y0: zero, + x1: w, + y1: h, + } + } /// Turn the extent into a tuple of the form `(x0,y0,x1,y1)`. pub fn into_tuple(self) -> (T, T, T, T) { (self.x0, self.y0, self.x1, self.y1) @@ -63,6 +73,18 @@ impl + Sub + Copy> Extent { y1: self.y1 + y.into(), } } + /// Expand `self` as needed to contain both `self` and `other` + pub fn union_mut(&mut self, other: &Self) { + // Canonicalize the coordinates so the 0th is the upper left and 1st is the lower right. + let x0 = self.x0.min(self.x1).min(other.x0).min(other.x1); + let y0 = self.y0.min(self.y1).min(other.y0).min(other.y1); + let x1 = self.x0.max(self.x1).max(other.x0).max(other.x1); + let y1 = self.y0.max(self.y1).max(other.y0).max(other.y1); + self.x0 = x0; + self.y0 = y0; + self.x1 = x1; + self.y1 = y1; + } } /// Margin of a box @@ -314,6 +336,7 @@ impl CenteredLabelLayout { /// /// /// ``` +#[allow(dead_code)] pub(crate) struct ChartLayoutNodes { /// A map from nodes to extents of the form `(x1,y1,x2,y2)` where /// `(x1,y1)` is the upper left corner of the node and @@ -346,6 +369,7 @@ pub(crate) struct ChartLayoutNodes { chart_container: Node, } +#[allow(dead_code)] impl ChartLayoutNodes { /// Create a new `ChartLayoutNodes`. All margins/padding/sizes are set to 0 /// and should be overridden as needed. @@ -374,6 +398,7 @@ impl ChartLayoutNodes { }, new_measure_func_with_defaults(), )?; + // Center column with top label/chart/bottom label let center_container = stretch_context.new_node( Style { flex_grow: 1.0, @@ -382,6 +407,7 @@ impl ChartLayoutNodes { }, vec![top_area, chart_area, bottom_area], )?; + // Container with everything except the title let chart_container = stretch_context.new_node( Style { flex_grow: 1.0, diff --git a/src/coord/mod.rs b/src/coord/mod.rs index 5cc17080..a1a96838 100644 --- a/src/coord/mod.rs +++ b/src/coord/mod.rs @@ -27,6 +27,7 @@ Currently we support the following 2D coordinate system: use plotters_backend::BackendCoord; pub mod ranged1d; +pub mod ticks; /// The coordinate combinators /// diff --git a/src/element/basic_shapes.rs b/src/element/basic_shapes.rs index eae015c1..dba42ef2 100644 --- a/src/element/basic_shapes.rs +++ b/src/element/basic_shapes.rs @@ -222,6 +222,49 @@ fn test_rect_element() { } } +/// A line element +pub struct LineSegment { + points: [Coord; 2], + style: ShapeStyle, +} + +impl LineSegment { + /// Create a new path + /// - `points`: The left upper and right lower corner of the rectangle + /// - `style`: The shape style + /// - returns the created element + pub fn new(points: [Coord; 2], style: &ShapeStyle) -> Self { + Self { + points, + style: style.clone(), + } + } +} + +impl<'a, Coord> PointCollection<'a, Coord> for &'a LineSegment { + type Point = &'a Coord; + type IntoIter = &'a [Coord]; + fn point_iter(self) -> &'a [Coord] { + &self.points + } +} + +impl Drawable for LineSegment { + fn draw>( + &self, + mut points: I, + backend: &mut DB, + _: (u32, u32), + ) -> Result<(), DrawingErrorKind> { + match (points.next(), points.next()) { + (Some(a), Some(b)) => { + backend.draw_line(a, b, &self.style) + } + _ => Ok(()), + } + } +} + /// A circle element pub struct Circle { center: Coord, From 6d81f7c01cc071360da4c14ff60ed8965ceeca6b Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Wed, 2 Jun 2021 13:34:24 -0400 Subject: [PATCH 08/11] Added ticks module --- src/coord/ticks/mod.rs | 403 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 403 insertions(+) create mode 100644 src/coord/ticks/mod.rs diff --git a/src/coord/ticks/mod.rs b/src/coord/ticks/mod.rs new file mode 100644 index 00000000..f726b76a --- /dev/null +++ b/src/coord/ticks/mod.rs @@ -0,0 +1,403 @@ +use std::iter::once; +use std::ops::Range; + +/// The type of tick mark +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum TickKind { + Major, + Minor, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Tick { + pub kind: TickKind, + pub pos: T, + pub label: L, +} + +/// Trait for axes whose tick marks can be iterated over. +/// * `T` - Type of the `pos` (in the [`Tick`] returned by the iterator). +/// * `L` - Type of the `label` (in the [`Tick`] returned by the iterator). +pub trait AxisTickEnumerator { + fn iter(&self) -> Box> + '_>; + // fn iter_for_range(&self, range: Range) -> Box> + '_>; +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SimpleLinearAxis { + major_tick_spacing: T, + minor_ticks_per_major_tick: usize, + range: Range, +} + +impl AxisTickEnumerator for SimpleLinearAxis { + // fn iter_for_range(&self, range: Range) -> Box> + '_> { + // let start = range.start as f32; + // let len = (range.end - range.start) as f32; + // + // Box::new(self.iter().map(move |tick| Tick { + // kind: tick.kind, + // label: tick.label, + // pos: (start + len * tick.pos) as i32, + // })) + // } + fn iter(&self) -> Box> + '_> { + let (range_start, range_end) = ( + self.range.start.min(self.range.end), + self.range.end.max(self.range.start), + ); + + // Information that is needed for the main body + let start = (range_start / self.major_tick_spacing).ceil() as isize; + let end = (range_end / self.major_tick_spacing).floor() as isize; + let minor_tick_spacing = + self.major_tick_spacing / ((self.minor_ticks_per_major_tick + 1) as f32); + let major_tick_spacing = self.major_tick_spacing; + + // Information needed for the start/end + let major_tick_start = start as f32 * major_tick_spacing; + let major_tick_end = end as f32 * major_tick_spacing; + let minor_ticks_before_first_major = (1..) + .take_while(|i| major_tick_start - *i as f32 * minor_tick_spacing >= range_start) + .count(); + let minor_ticks_after_last_major = (1..) + .take_while(|i| major_tick_end + *i as f32 * minor_tick_spacing <= range_end) + .count(); + + let iter = (start..end).flat_map(move |k| { + let start = k as f32 * major_tick_spacing; + (0..=self.minor_ticks_per_major_tick).map(move |i| { + let pos = start + (i as f32) * minor_tick_spacing; + Tick { + pos, + kind: match i { + 0 => TickKind::Major, + _ => TickKind::Minor, + }, + label: pos, + } + }) + }); + + // Right now, iter will iterate through the main body of the ticks, + // but will not iterate through the minor ticks before the first major + // or the last major tick/minor ticks after the last major. Those need to + // be inserted manually. + let start_iter = (1..=minor_ticks_before_first_major).rev().map(move |i| { + let pos = major_tick_start - (i as f32) * minor_tick_spacing; + Tick { + pos, + kind: TickKind::Minor, + label: pos, + } + }); + let end_iter = once(Tick { + pos: major_tick_end, + kind: TickKind::Major, + label: major_tick_end, + }) + .chain((1..=minor_ticks_after_last_major).map(move |i| { + let pos = major_tick_end + (i as f32) * minor_tick_spacing; + Tick { + pos, + kind: TickKind::Minor, + label: pos, + } + })); + + Box::new(start_iter.chain(iter).chain(end_iter)) + } +} + +/// Use some heuristics to guess the best tick spacing for `range` given a length of `len` pixels. +pub(crate) fn suggest_tickmark_spacing_for_range( + range: &Range, + len: i32, +) -> SimpleLinearAxis { + let range_len = (range.end - range.start).abs(); + let scale = len as f32 / range_len; + + // Ideally we want to space our major ticks between 50 and 120 pixels. + // So start searching to see if we find such a condition. + let mut major_tick_spacing = 1.0; + for &tick_hint in &[1.0, 2.5, 2.0, 5.0] { + // Check if there is a power of 10 so that the tick_hint works as a major tick + // That amounts to solving the equation `50 <= tick_hint*scale*10^n <= 120` for `n`. + let upper = (120. / (tick_hint * scale)).log10(); + let lower = (50. / (tick_hint * scale)).log10(); + if upper.floor() >= lower.ceil() { + // In this condition, we have an integer solution (in theory we might + // have more than one which is the reason for the funny check). + let pow = upper.floor() as i32; + // We prefer tick steps of .25 and 25, but not 2.5, so exclude this case + // specifically. + if pow != 0 || tick_hint != 2.5 { + major_tick_spacing = tick_hint * 10_f32.powi(pow); + break; + } + } + } + + let mut minor_ticks_per_major_tick: usize = 0; + // We want minor ticks to be at least 15 px apart + for &tick_hint in &[9, 4, 3, 1] { + if major_tick_spacing * scale / ((tick_hint + 1) as f32) > 15. { + minor_ticks_per_major_tick = tick_hint; + break; + } + } + + SimpleLinearAxis { + major_tick_spacing, + minor_ticks_per_major_tick, + range: range.clone(), + } +} + +#[cfg(test)] +mod test { + use super::*; + + /* + #[test] + fn test_iter_for_range() { + let linear_axis = SimpleLinearAxis { + major_tick_spacing: 1.0_f32, + minor_ticks_per_major_tick: 0, + range: -1.0..4.0, + }; + let ticks = linear_axis + .iter_for_range(-1..4) + .map(|tick| tick.pos) + .collect::>(); + let ticks2 = linear_axis + .iter() + .map(|tick| tick.pos) + .collect::>(); + dbg!(ticks2); + + assert_eq!(ticks, vec![-1,0,1,2,3,4]); + }*/ + + #[test] + fn test_spacing_suggestions() { + let suggestion = suggest_tickmark_spacing_for_range(&(0.0..5.0), 500); + + assert_eq!( + suggestion, + SimpleLinearAxis { + major_tick_spacing: 1.0, + minor_ticks_per_major_tick: 4, + range: 0.0..5.0, + } + ); + } + + #[test] + fn test_tick_spacing() { + let linear_axis = SimpleLinearAxis { + major_tick_spacing: 1.0_f32, + minor_ticks_per_major_tick: 0, + range: -1.0..4.0, + }; + let ticks = linear_axis.iter().collect::>>(); + assert_eq!( + ticks, + vec![ + Tick { + kind: TickKind::Major, + pos: -1.0, + label: -1.0 + }, + Tick { + kind: TickKind::Major, + pos: 0.0, + label: 0.0 + }, + Tick { + kind: TickKind::Major, + pos: 1.0, + label: 1.0 + }, + Tick { + kind: TickKind::Major, + pos: 2.0, + label: 2.0 + }, + Tick { + kind: TickKind::Major, + pos: 3.0, + label: 3.0 + }, + Tick { + kind: TickKind::Major, + pos: 4.0, + label: 4.0 + }, + ] + ); + + let linear_axis = SimpleLinearAxis { + major_tick_spacing: 1.0_f32, + minor_ticks_per_major_tick: 0, + range: -1.5..2.9, + }; + let ticks = linear_axis.iter().collect::>>(); + assert_eq!( + ticks, + vec![ + Tick { + kind: TickKind::Major, + pos: -1.0, + label: -1.0 + }, + Tick { + kind: TickKind::Major, + pos: 0.0, + label: 0.0 + }, + Tick { + kind: TickKind::Major, + pos: 1.0, + label: 1.0 + }, + Tick { + kind: TickKind::Major, + pos: 2.0, + label: 2.0 + }, + ] + ); + + let linear_axis = SimpleLinearAxis { + major_tick_spacing: 1.0_f32, + minor_ticks_per_major_tick: 1, + range: -1.0..1.0, + }; + let ticks = linear_axis.iter().collect::>>(); + assert_eq!( + ticks, + vec![ + Tick { + kind: TickKind::Major, + pos: -1.0, + label: -1.0 + }, + Tick { + kind: TickKind::Minor, + pos: -0.5, + label: -0.5 + }, + Tick { + kind: TickKind::Major, + pos: 0.0, + label: 0.0 + }, + Tick { + kind: TickKind::Minor, + pos: 0.5, + label: 0.5 + }, + Tick { + kind: TickKind::Major, + pos: 1.0, + label: 1.0 + }, + ] + ); + } + + #[test] + fn test_minor_ticks_before_first_major() { + let linear_axis = SimpleLinearAxis { + major_tick_spacing: 1.0_f32, + minor_ticks_per_major_tick: 1, + range: -1.6..1.0, + }; + let ticks = linear_axis.iter().collect::>>(); + assert_eq!( + ticks, + vec![ + Tick { + kind: TickKind::Minor, + pos: -1.5, + label: -1.5 + }, + Tick { + kind: TickKind::Major, + pos: -1.0, + label: -1.0 + }, + Tick { + kind: TickKind::Minor, + pos: -0.5, + label: -0.5 + }, + Tick { + kind: TickKind::Major, + pos: 0.0, + label: 0.0 + }, + Tick { + kind: TickKind::Minor, + pos: 0.5, + label: 0.5 + }, + Tick { + kind: TickKind::Major, + pos: 1.0, + label: 1.0 + }, + ] + ); + } + #[test] + fn test_minor_ticks_after_last_major() { + let linear_axis = SimpleLinearAxis { + major_tick_spacing: 1.0_f32, + minor_ticks_per_major_tick: 1, + range: -1.6..1.6, + }; + let ticks = linear_axis.iter().collect::>>(); + assert_eq!( + ticks, + vec![ + Tick { + kind: TickKind::Minor, + pos: -1.5, + label: -1.5 + }, + Tick { + kind: TickKind::Major, + pos: -1.0, + label: -1.0 + }, + Tick { + kind: TickKind::Minor, + pos: -0.5, + label: -0.5 + }, + Tick { + kind: TickKind::Major, + pos: 0.0, + label: 0.0 + }, + Tick { + kind: TickKind::Minor, + pos: 0.5, + label: 0.5 + }, + Tick { + kind: TickKind::Major, + pos: 1.0, + label: 1.0 + }, + Tick { + kind: TickKind::Minor, + pos: 1.5, + label: 1.5 + }, + ] + ); + } +} From 6514ed9fdd04b1cdd91d4d139b55fadb01fba7cb Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Wed, 2 Jun 2021 13:51:40 -0400 Subject: [PATCH 09/11] Put ChartLayout under a features flag --- Cargo.toml | 16 +++++++++++++--- src/coord/mod.rs | 1 + src/lib.rs | 6 +++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f1225c60..aa9d26e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,6 @@ exclude = ["doc-template/*"] num-traits = "0.2.14" chrono = { version = "0.4.19", optional = true } plotters-svg = {version = "^0.3.*", optional = true} -stretch = "0.3.2" -paste = "1.0" [dependencies.plotters-backend] version = "^0.3" @@ -27,6 +25,14 @@ version = "^0.3" optional = true default_features = false +[dependencies.stretch] +version = "0.3.2" +optional = true + +[dependencies.paste] +version = "1.0" +optional = true + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] ttf-parser = { version = "0.12.0", optional = true } lazy_static = { version = "1.4.0", optional = true } @@ -63,7 +69,8 @@ default = [ "ttf", "image", "deprecated_items", "all_series", "all_elements", - "full_palette" + "full_palette", + "advanced_layout" ] all_series = ["area_series", "line_series", "point_series", "surface_series"] all_elements = ["errorbar", "candlestick", "boxplot", "histogram"] @@ -77,6 +84,9 @@ svg_backend = ["plotters-svg"] # Colors full_palette = [] +# Advanced Layout +advanced_layout = ["stretch", "paste"] + # Elements errorbar = [] candlestick = [] diff --git a/src/coord/mod.rs b/src/coord/mod.rs index a1a96838..a447e889 100644 --- a/src/coord/mod.rs +++ b/src/coord/mod.rs @@ -27,6 +27,7 @@ Currently we support the following 2D coordinate system: use plotters_backend::BackendCoord; pub mod ranged1d; +#[cfg(feature = "advanced_layout")] pub mod ticks; /// The coordinate combinators diff --git a/src/lib.rs b/src/lib.rs index 07682d5d..c3397802 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -756,9 +756,9 @@ pub use palette; /// The module imports the most commonly used types and modules in Plotters pub mod prelude { // Chart related types - pub use crate::chart::{ - ChartBuilder, ChartContext, ChartLayout, LabelAreaPosition, SeriesLabelPosition, - }; + #[cfg(feature = "advanced_layout")] + pub use crate::chart::ChartLayout; + pub use crate::chart::{ChartBuilder, ChartContext, LabelAreaPosition, SeriesLabelPosition}; // Coordinates pub use crate::coord::{ From 1b85ad0dca386091b904abc3f19466edfd6a282e Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Wed, 2 Jun 2021 13:53:19 -0400 Subject: [PATCH 10/11] Call `present` at the end of example to make sure an error is thrown for a missing file --- examples/layout.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/layout.rs b/examples/layout.rs index 37597e61..36fb5e42 100644 --- a/examples/layout.rs +++ b/examples/layout.rs @@ -36,6 +36,7 @@ fn main() -> Result<(), Box> { .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &RED)); cc.configure_series_labels().border_style(&BLACK).draw()?; + root.present()?; Ok(()) } From a9e70372c5fcfd91b4d66c1e9fab9879464b1207 Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Wed, 2 Jun 2021 14:00:23 -0400 Subject: [PATCH 11/11] Autosizing of labels --- src/chart/layout/mod.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/chart/layout/mod.rs b/src/chart/layout/mod.rs index ccd8031b..e431e1a1 100644 --- a/src/chart/layout/mod.rs +++ b/src/chart/layout/mod.rs @@ -192,30 +192,35 @@ pub struct ChartLayout<'a, 'b, DB: DrawingBackend> { impl<'a, 'b, DB: DrawingBackend> ChartLayout<'a, 'b, DB> { /// Create a chart builder on the given drawing area - /// - `root`: The root drawing area - /// - Returns: The chart layout object + /// - `root`: The root drawing area + /// - Returns: The chart layout object pub fn new(root: &'a DrawingArea) -> Self { + let (w, h) = root.dim_in_pixel(); + let min_dim = w.min(h) as f32; + let title_text_size = (min_dim / 10.).clamp(10., 100.); + let label_text_size = (min_dim / 16.).clamp(10., 100.); + Self { root_area: root, chart_title: Label { text: None, - style: TextStyle::from(("serif", 40.0).into_font()), + style: ("serif", title_text_size).into(), }, top_label: Label { text: None, - style: TextStyle::from(("serif", 25.0).into_font()), + style: ("serif", label_text_size).into(), }, bottom_label: Label { text: None, - style: TextStyle::from(("serif", 25.0).into_font()), + style: ("serif", label_text_size).into(), }, left_label: Label { text: None, - style: TextStyle::from(("serif", 25.0).into_font()), + style: ("serif", label_text_size).into(), }, right_label: Label { text: None, - style: TextStyle::from(("serif", 25.0).into_font()), + style: ("serif", label_text_size).into(), }, nodes: ChartLayoutNodes::new().unwrap(), axis_ranges: AxisSpecs::new_blank(),