use std::fmt::Debug;
use plotters::coord::ranged1d::AsRangedCoord;
use plotters::coord::Shift;
use plotters::prelude::*;
use plotters::style::FontError;
const INDEX_TOP: usize = 0;
const INDEX_BOTTOM: usize = 1;
const INDEX_LEFT: usize = 2;
const INDEX_RIGHT: usize = 3;
type DrawingResult<T, DB> = Result<T, DrawingAreaErrorKind<<DB as DrawingBackend>::ErrorType>>;
type ChartContext2d<'a, DB, X, Y> = ChartContext<
'a,
DB,
Cartesian2d<<X as AsRangedCoord>::CoordDescType, <Y as AsRangedCoord>::CoordDescType>,
>;
#[derive(Clone)]
pub struct ChartLayout<'a> {
title_height: u32,
title_content: Option<(String, TextStyle<'a>, u32)>,
margin: [u32; 4],
label_area_size: [u32; 4],
}
impl<'a> Debug for ChartLayout<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChartLayout")
.field("title_height", &self.title_height)
.field(
"title_content",
&self.title_content.as_ref().map(|(t, _, _)| t),
)
.field("margin", &self.margin)
.field("label_area_size", &self.label_area_size)
.finish()
}
}
fn estimate_text_size(text: &str, font: &FontDesc) -> Result<(u32, u32), FontError> {
let text_layout = font.layout_box(text)?;
Ok((
((text_layout.1).0 - (text_layout.0).0) as u32,
((text_layout.1).1 - (text_layout.0).1) as u32,
))
}
impl<'a> ChartLayout<'a> {
pub fn new() -> Self {
Self {
label_area_size: [0; 4],
title_height: 0,
title_content: None,
margin: [0; 4],
}
}
pub fn set_all_label_area_size(
&mut self,
top: u32,
bottom: u32,
left: u32,
right: u32,
) -> &mut Self {
self.label_area_size = [top, bottom, left, right];
self
}
pub fn x_label_area_size(&mut self, size: u32) -> &mut Self {
self.label_area_size[INDEX_BOTTOM] = size;
self
}
pub fn y_label_area_size(&mut self, size: u32) -> &mut Self {
self.label_area_size[INDEX_LEFT] = size;
self
}
pub fn top_x_label_area_size(&mut self, size: u32) -> &mut Self {
self.label_area_size[INDEX_TOP] = size;
self
}
pub fn right_y_label_area_size(&mut self, size: u32) -> &mut Self {
self.label_area_size[INDEX_RIGHT] = size;
self
}
pub fn set_all_margin(&mut self, top: u32, bottom: u32, left: u32, right: u32) -> &mut Self {
self.margin = [top, bottom, left, right];
self
}
pub fn margin(&mut self, size: u32) -> &mut Self {
self.margin = [size, size, size, size];
self
}
pub fn margin_top(&mut self, size: u32) -> &mut Self {
self.margin[INDEX_TOP] = size;
self
}
pub fn margin_bottom(&mut self, size: u32) -> &mut Self {
self.margin[INDEX_BOTTOM] = size;
self
}
pub fn margin_left(&mut self, size: u32) -> &mut Self {
self.margin[INDEX_LEFT] = size;
self
}
pub fn margin_right(&mut self, size: u32) -> &mut Self {
self.margin[INDEX_RIGHT] = size;
self
}
pub fn no_caption(&mut self) -> &mut Self {
self.title_height = 0;
self.title_content = None;
self
}
pub fn caption(
&mut self,
text: impl Into<String>,
font: impl Into<FontDesc<'a>>,
) -> Result<&mut Self, FontError> {
let text: String = text.into();
let font: FontDesc = font.into();
let (_, text_h) = estimate_text_size(&text, &font)?;
let style: TextStyle = font.into();
let y_padding = (text_h / 2).min(5);
self.title_height = y_padding * 2 + text_h;
self.title_content = Some((text, style, y_padding));
Ok(self)
}
pub fn replace_caption(&mut self, text: impl Into<String>) -> &mut Self {
let text: String = text.into();
if let Some((_, style, y_padding)) = self.title_content.take() {
self.title_content = Some((text, style, y_padding));
}
self
}
fn additional_sizes(&self) -> (u32, u32) {
let [m_top, m_bottom, m_left, m_right] = self.margin;
let [l_top, l_bottom, l_left, l_right] = self.label_area_size;
let width = m_left + m_right + l_left + l_right;
let height = self.title_height + m_top + m_bottom + l_top + l_bottom;
(width, height)
}
pub fn desired_image_size(&self, plot_size: (u32, u32)) -> (u32, u32) {
let additional = self.additional_sizes();
(plot_size.0 + additional.0, plot_size.1 + additional.1)
}
pub fn desired_image_height_from_width(&self, image_width: u32, aspect_ratio: f64) -> u32 {
let additional = self.additional_sizes();
if image_width < additional.0 {
additional.1
} else {
((image_width - additional.0) as f64 * aspect_ratio) as u32 + additional.1
}
}
pub fn bind<'b, DB>(
&self,
root_area: &'b DrawingArea<DB, Shift>,
) -> DrawingResult<ChartLayoutBuilder<'b, DB>, DB>
where
'a: 'b,
DB: DrawingBackend,
{
use plotters::style::text_anchor::{HPos, Pos, VPos};
let title_area_height = self.title_height;
let main_area = if title_area_height > 0 {
let (title_area, main_area) = root_area.split_vertically(title_area_height);
if let Some((text, style, y_padding)) = &self.title_content {
let dim = title_area.dim_in_pixel();
let x_padding = dim.0 / 2;
let style = &style.pos(Pos::new(HPos::Center, VPos::Top));
title_area.draw_text(text, style, (x_padding as i32, *y_padding as i32))?;
main_area
} else {
main_area
}
} else {
root_area.clone()
};
Ok(ChartLayoutBuilder {
layout: self.clone(),
main_area,
})
}
}
impl<'a> Default for ChartLayout<'a> {
fn default() -> Self {
Self::new()
}
}
pub struct ChartLayoutBuilder<'a, DB: DrawingBackend> {
layout: ChartLayout<'a>,
main_area: DrawingArea<DB, Shift>,
}
impl<'a, DB: DrawingBackend> ChartLayoutBuilder<'a, DB> {
pub fn estimate_plot_area_size(&self) -> (u32, u32) {
let [m_top, m_bottom, m_left, m_right] = self.layout.margin;
let [l_top, l_bottom, l_left, l_right] = self.layout.label_area_size;
let (image_width, image_height) = self.main_area.dim_in_pixel();
let plot_width = image_width - (m_left + m_right + l_left + l_right);
let plot_height = image_height - (m_top + m_bottom + l_top + l_bottom);
(plot_width, plot_height)
}
pub fn build_cartesian_2d<X: AsRangedCoord, Y: AsRangedCoord>(
&self,
x_spec: X,
y_spec: Y,
) -> DrawingResult<ChartContext2d<DB, X, Y>, DB> {
let [m_top, m_bottom, m_left, m_right] = self.layout.margin;
let [l_top, l_bottom, l_left, l_right] = self.layout.label_area_size;
let mut builder = ChartBuilder::on(&self.main_area);
builder
.margin_top(m_top)
.margin_bottom(m_bottom)
.margin_left(m_left)
.margin_right(m_right)
.set_label_area_size(LabelAreaPosition::Top, l_top)
.set_label_area_size(LabelAreaPosition::Bottom, l_bottom)
.set_label_area_size(LabelAreaPosition::Left, l_left)
.set_label_area_size(LabelAreaPosition::Right, l_right);
builder.build_cartesian_2d(x_spec, y_spec)
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use std::ops::Range;
use plotters::backend::RGBPixel;
use plotters::prelude::*;
use super::ChartLayout;
#[test]
fn size_estimation() -> Result<(), Box<dyn Error>> {
let x_spec = 0.0..2.0;
let y_spec = -1.0..2.0;
let plot_size = (200, 350);
let mut layout = ChartLayout::new();
for i in 0..0x200 {
layout.set_all_margin(
if i & 0x1 == 0 { 5 } else { 0 },
if i & 0x2 == 0 { 10 } else { 0 },
if i & 0x4 == 0 { 12 } else { 0 },
if i & 0x8 == 0 { 15 } else { 0 },
);
layout.set_all_label_area_size(
if i & 0x10 == 0 { 20 } else { 0 },
if i & 0x20 == 0 { 25 } else { 0 },
if i & 0x40 == 0 { 30 } else { 0 },
if i & 0x80 == 0 { 32 } else { 0 },
);
if i & 0x100 == 0 {
layout.caption("Test Title", ("sans-serif", 20))?;
} else {
layout.no_caption();
}
bmp2d_size_estimation(&layout, plot_size, x_spec.clone(), y_spec.clone())?;
}
Ok(())
}
fn bmp2d_size_estimation(
layout: &ChartLayout,
plot_size: (u32, u32),
x_spec: Range<f64>,
y_spec: Range<f64>,
) -> Result<(), Box<dyn Error>> {
let image_size = layout.desired_image_size(plot_size);
let mut buf = vec![0u8; (3 * image_size.0 * image_size.1) as usize];
let backend: BitMapBackend<RGBPixel> =
BitMapBackend::with_buffer_and_format(&mut buf, image_size)?;
let root_area = backend.into_drawing_area();
let builder = layout.bind(&root_area)?;
let estimated_plot_size = builder.estimate_plot_area_size();
assert_eq!(
plot_size, estimated_plot_size,
"wrong estimation; layout = {layout:?}, image_size = {image_size:?}"
);
let chart = builder.build_cartesian_2d(x_spec, y_spec)?;
let actual_size = chart.plotting_area().dim_in_pixel();
assert_eq!(
plot_size, actual_size,
"wrong actual size, layout = {layout:?}, image_size = {image_size:?}"
);
Ok(())
}
}