diff --git a/Cargo.toml b/Cargo.toml index 9296eef0..aa9d26e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,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 } @@ -61,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"] @@ -75,6 +84,9 @@ svg_backend = ["plotters-svg"] # Colors full_palette = [] +# Advanced Layout +advanced_layout = ["stretch", "paste"] + # Elements errorbar = [] candlestick = [] diff --git a/examples/layout.rs b/examples/layout.rs new file mode 100644 index 00000000..36fb5e42 --- /dev/null +++ b/examples/layout.rs @@ -0,0 +1,46 @@ +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 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_bottom_label_text("Radians")? + .set_bottom_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 = x_spec.clone().step(0.1); + let mut cc = ChartBuilder::on(&da_chart) + //.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.configure_series_labels().border_style(&BLACK).draw()?; + root.present()?; + + Ok(()) +} +#[test] +fn entry_point() { + main().unwrap() +} diff --git a/src/chart/builder.rs b/src/chart/builder.rs index 6b3f770d..6b7dbf96 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 #[allow(clippy::type_complexity)] pub fn build_cartesian_3d( diff --git a/src/chart/layout/mod.rs b/src/chart/layout/mod.rs new file mode 100644 index 00000000..e431e1a1 --- /dev/null +++ b/src/chart/layout/mod.rs @@ -0,0 +1,995 @@ +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}; + +use plotters_backend::DrawingBackend; + +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) => { + 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(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>, +} + +/// 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. +pub struct ChartLayout<'a, 'b, DB: DrawingBackend> { + root_area: &'a DrawingArea, + chart_title: Label<'b>, + top_label: Label<'b>, + bottom_label: Label<'b>, + left_label: Label<'b>, + right_label: Label<'b>, + nodes: ChartLayoutNodes, + axis_ranges: AxisSpecs>, +} + +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 { + 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: ("serif", title_text_size).into(), + }, + top_label: Label { + text: None, + style: ("serif", label_text_size).into(), + }, + bottom_label: Label { + text: None, + style: ("serif", label_text_size).into(), + }, + left_label: Label { + text: None, + style: ("serif", label_text_size).into(), + }, + right_label: Label { + text: None, + style: ("serif", label_text_size).into(), + }, + nodes: ChartLayoutNodes::new().unwrap(), + axis_ranges: AxisSpecs::new_blank(), + } + } + + 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)?; + + 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 + .nodes + .get_chart_title_extent() + .ok_or_else(|| LayoutError::ExtentsError)?; + self.root_area + .draw_text(text, &self.chart_title.style, (extent.x0, extent.y0))?; + } + + 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, 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, extent.y0))?; + } + // 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, extent.y1), + )?; + } + 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, extent.y1), + )?; + } + + 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( + &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) { + 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); +} + +/// 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::*; + use crate::prelude::*; + + fn extent_has_size( + extent: Extent, + ) -> bool { + (extent.x1 > extent.x0) && (extent.y1 > extent.y0) + } + + #[test] + 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_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_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); + } + + #[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 new file mode 100644 index 00000000..30927ba0 --- /dev/null +++ b/src/chart/layout/nodes.rs @@ -0,0 +1,842 @@ +use paste::paste; +use std::{ + collections::HashMap, + ops::{Add, Sub}, +}; + +use stretch::number::OrElse; +use stretch::{ + geometry, + geometry::Size, + node::{MeasureFunc, Node, Stretch}, + number::Number, + 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 + 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) + } + /// 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(), + } + } + /// 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 +#[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.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> { + self.get_extent(&self.$name) + } + } + }; + ($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.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> { + 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) + } + } + }; +} + +macro_rules! impl_set_size { + ($name:ident) => { + paste! { + #[doc = "Set the minimum size of the `" $name "` container."] + pub fn []( + &mut self, + w: u32, + h: u32, + ) -> Result<(), stretch::Error> { + self.set_min_size(self.$name, w, h) + } + } + }; + ($name:ident, $sub_part:ident) => { + paste! { + #[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: u32, + h: u32, + ) -> Result<(), stretch::Error> { + 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` +/// 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 +/// +/// +/// +/// +/// +/// ``` +#[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 + /// `(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, +} + +#[allow(dead_code)] +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(), + )?; + // Center column with top label/chart/bottom label + 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], + )?; + // Container with everything except the title + 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<(), stretch::Error> { + // 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(()) + } + /// 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, + }; + + Ok(Margin { + top, + right, + bottom, + left, + }) + } + /// Set the margin of `node`. + fn set_margin>>( + &mut self, + node: Node, + margin: M, + ) -> Result<(), stretch::Error> { + 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); + 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); + 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); + 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); + 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`) +/// 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), 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*. + 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 +/// 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> { + 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, Extent::new(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 extent = extents_cache.get(&layout.chart_area).unwrap(); + + 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. + 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 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 4a880296..32cb11ea 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, Margin}; pub use context::ChartContext; pub use dual_coord::{DualCoordChartContext, DualCoordChartState}; pub use mesh::{MeshStyle, SecondaryMeshStyle}; diff --git a/src/coord/mod.rs b/src/coord/mod.rs index 5cc17080..a447e889 100644 --- a/src/coord/mod.rs +++ b/src/coord/mod.rs @@ -27,6 +27,8 @@ 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/coord/ranged1d/combinators/logarithmic.rs b/src/coord/ranged1d/combinators/logarithmic.rs index 9d600f8c..37214a79 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; @@ -253,7 +253,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)] 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 897e7f52..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). */ @@ -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/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 + }, + ] + ); + } +} diff --git a/src/drawing/area.rs b/src/drawing/area.rs index ec505b1d..80efb5b4 100644 --- a/src/drawing/area.rs +++ b/src/drawing/area.rs @@ -130,6 +130,14 @@ impl Clone for DrawingArea { @@ -139,8 +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, + /// Error encountered during layout + LayoutError(LayoutError), } impl std::fmt::Display for DrawingAreaErrorKind { @@ -150,13 +158,28 @@ impl std::fmt::Display for DrawingAreaErrorKind { DrawingAreaErrorKind::SharingError => { write!(fmt, "Multiple backend operation in progress") } - DrawingAreaErrorKind::LayoutError => write!(fmt, "Bad layout"), + DrawingAreaErrorKind::LayoutError(LayoutError::StretchError(e)) => e.fmt(fmt), + DrawingAreaErrorKind::LayoutError(LayoutError::ExtentsError) => { + write!(fmt, "Could not find the extends of node") + } } } } impl Error for DrawingAreaErrorKind {} +impl From for DrawingAreaErrorKind { + fn from(err: stretch::Error) -> Self { + DrawingAreaErrorKind::LayoutError(LayoutError::StretchError(err)) + } +} + +impl From for DrawingAreaErrorKind { + fn from(err: LayoutError) -> Self { + DrawingAreaErrorKind::LayoutError(err) + } +} + #[allow(type_alias_bounds)] type DrawingAreaError = DrawingAreaErrorKind; 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/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, diff --git a/src/lib.rs b/src/lib.rs index 7e1cdbd4..9ea50185 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -756,6 +756,8 @@ pub use palette; /// The module imports the most commonly used types and modules in Plotters pub mod prelude { // Chart related types + #[cfg(feature = "advanced_layout")] + pub use crate::chart::ChartLayout; pub use crate::chart::{ChartBuilder, ChartContext, LabelAreaPosition, SeriesLabelPosition}; // Coordinates 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), diff --git a/src/style/text.rs b/src/style/text.rs index 3a83806e..6c8ddfdf 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>;