use crate::core::units::{REFERENCE_DPI, RenderScale, in_to_px, pt_to_px, px_to_in};
use crate::render::{FontFamily, FontWeight};
#[derive(Debug, Clone, PartialEq)]
pub struct FigureConfig {
pub width: f32,
pub height: f32,
pub dpi: f32,
}
impl FigureConfig {
pub fn new(width: f32, height: f32, dpi: f32) -> Self {
Self { width, height, dpi }
}
pub fn canvas_size(&self) -> (u32, u32) {
(
in_to_px(self.width, self.dpi) as u32,
in_to_px(self.height, self.dpi) as u32,
)
}
pub fn aspect_ratio(&self) -> f32 {
self.width / self.height
}
pub fn render_scale(&self) -> RenderScale {
RenderScale::new(self.dpi).with_figure(self.width, self.height)
}
}
impl Default for FigureConfig {
fn default() -> Self {
Self {
width: 6.4, height: 4.8, dpi: 100.0, }
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TypographyConfig {
pub base_size: f32,
pub title_scale: f32,
pub label_scale: f32,
pub tick_scale: f32,
pub legend_scale: f32,
pub family: FontFamily,
pub title_weight: FontWeight,
}
impl TypographyConfig {
pub fn new(base_size: f32) -> Self {
Self {
base_size,
..Default::default()
}
}
pub fn title_size(&self) -> f32 {
self.base_size * self.title_scale
}
pub fn label_size(&self) -> f32 {
self.base_size * self.label_scale
}
pub fn tick_size(&self) -> f32 {
self.base_size * self.tick_scale
}
pub fn legend_size(&self) -> f32 {
self.base_size * self.legend_scale
}
pub fn title_size_px(&self, dpi: f32) -> f32 {
pt_to_px(self.title_size(), dpi)
}
pub fn label_size_px(&self, dpi: f32) -> f32 {
pt_to_px(self.label_size(), dpi)
}
pub fn tick_size_px(&self, dpi: f32) -> f32 {
pt_to_px(self.tick_size(), dpi)
}
pub fn legend_size_px(&self, dpi: f32) -> f32 {
pt_to_px(self.legend_size(), dpi)
}
pub fn scale(mut self, factor: f32) -> Self {
self.base_size *= factor;
self
}
pub fn base_size(mut self, size: f32) -> Self {
self.base_size = size;
self
}
pub fn title_scale(mut self, scale: f32) -> Self {
self.title_scale = scale;
self
}
pub fn family(mut self, family: FontFamily) -> Self {
self.family = family;
self
}
pub fn title_weight(mut self, weight: FontWeight) -> Self {
self.title_weight = weight;
self
}
}
impl Default for TypographyConfig {
fn default() -> Self {
Self {
base_size: 10.0, title_scale: 1.4, label_scale: 1.0, tick_scale: 0.9, legend_scale: 0.9, family: FontFamily::SansSerif,
title_weight: FontWeight::Normal,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LineConfig {
pub data_width: f32,
pub axis_width: f32,
pub grid_width: f32,
pub tick_width: f32,
pub tick_length: f32,
}
impl LineConfig {
pub fn data_width_px(&self, dpi: f32) -> f32 {
pt_to_px(self.data_width, dpi)
}
pub fn axis_width_px(&self, dpi: f32) -> f32 {
pt_to_px(self.axis_width, dpi)
}
pub fn grid_width_px(&self, dpi: f32) -> f32 {
pt_to_px(self.grid_width, dpi)
}
pub fn tick_width_px(&self, dpi: f32) -> f32 {
pt_to_px(self.tick_width, dpi)
}
pub fn tick_length_px(&self, dpi: f32) -> f32 {
pt_to_px(self.tick_length, dpi)
}
pub fn data_width(mut self, width: f32) -> Self {
self.data_width = width;
self
}
pub fn axis_width(mut self, width: f32) -> Self {
self.axis_width = width;
self
}
pub fn grid_width(mut self, width: f32) -> Self {
self.grid_width = width;
self
}
}
impl Default for LineConfig {
fn default() -> Self {
Self {
data_width: 1.5, axis_width: 0.8,
grid_width: 0.5,
tick_width: 0.6,
tick_length: 4.0,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SpacingConfig {
pub title_pad: f32,
pub label_pad: f32,
pub tick_pad: f32,
pub legend_pad: f32,
}
impl SpacingConfig {
pub fn title_pad_px(&self, dpi: f32) -> f32 {
pt_to_px(self.title_pad, dpi)
}
pub fn label_pad_px(&self, dpi: f32) -> f32 {
pt_to_px(self.label_pad, dpi)
}
pub fn tick_pad_px(&self, dpi: f32) -> f32 {
pt_to_px(self.tick_pad, dpi)
}
pub fn legend_pad_px(&self, dpi: f32) -> f32 {
pt_to_px(self.legend_pad, dpi)
}
}
impl Default for SpacingConfig {
fn default() -> Self {
Self {
title_pad: 6.0, label_pad: 4.0, tick_pad: 3.5, legend_pad: 8.0,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum MarginConfig {
Proportional {
left: f32,
right: f32,
top: f32,
bottom: f32,
},
Auto {
min: f32,
max: f32,
},
Fixed {
left: f32,
right: f32,
top: f32,
bottom: f32,
},
ContentDriven {
edge_buffer: f32,
center_plot: bool,
},
}
impl MarginConfig {
pub fn proportional() -> Self {
MarginConfig::Proportional {
left: 0.125,
right: 0.1,
top: 0.12,
bottom: 0.11,
}
}
pub fn matplotlib() -> Self {
Self::proportional()
}
pub fn proportional_custom(left: f32, right: f32, top: f32, bottom: f32) -> Self {
MarginConfig::Proportional {
left,
right,
top,
bottom,
}
}
pub fn auto() -> Self {
MarginConfig::Auto { min: 0.3, max: 1.0 }
}
pub fn auto_with_bounds(min: f32, max: f32) -> Self {
MarginConfig::Auto { min, max }
}
pub fn fixed_uniform(margin: f32) -> Self {
MarginConfig::Fixed {
left: margin,
right: margin,
top: margin,
bottom: margin,
}
}
pub fn fixed(left: f32, right: f32, top: f32, bottom: f32) -> Self {
MarginConfig::Fixed {
left,
right,
top,
bottom,
}
}
pub fn content_driven() -> Self {
MarginConfig::ContentDriven {
edge_buffer: 5.0,
center_plot: true,
}
}
pub fn content_driven_custom(edge_buffer: f32, center_plot: bool) -> Self {
MarginConfig::ContentDriven {
edge_buffer,
center_plot,
}
}
}
impl Default for MarginConfig {
fn default() -> Self {
MarginConfig::ContentDriven {
edge_buffer: 5.0, center_plot: true, }
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ComputedMargins {
pub left: f32,
pub right: f32,
pub top: f32,
pub bottom: f32,
}
impl ComputedMargins {
pub fn left_px(&self, dpi: f32) -> f32 {
in_to_px(self.left, dpi)
}
pub fn right_px(&self, dpi: f32) -> f32 {
in_to_px(self.right, dpi)
}
pub fn top_px(&self, dpi: f32) -> f32 {
in_to_px(self.top, dpi)
}
pub fn bottom_px(&self, dpi: f32) -> f32 {
in_to_px(self.bottom, dpi)
}
}
impl Default for ComputedMargins {
fn default() -> Self {
Self {
left: 0.8,
right: 0.3,
top: 0.5,
bottom: 0.6,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SpineConfig {
pub left: bool,
pub right: bool,
pub top: bool,
pub bottom: bool,
pub offset: f32,
}
impl SpineConfig {
pub fn new() -> Self {
Self::default()
}
pub fn despine() -> Self {
Self {
left: true,
right: false,
top: false,
bottom: true,
offset: 0.0,
}
}
pub fn minimal() -> Self {
Self::despine()
}
pub fn none() -> Self {
Self {
left: false,
right: false,
top: false,
bottom: false,
offset: 0.0,
}
}
pub fn all() -> Self {
Self::default()
}
pub fn with_offset(mut self, offset: f32) -> Self {
self.offset = offset;
self
}
pub fn hide_left(mut self) -> Self {
self.left = false;
self
}
pub fn hide_right(mut self) -> Self {
self.right = false;
self
}
pub fn hide_top(mut self) -> Self {
self.top = false;
self
}
pub fn hide_bottom(mut self) -> Self {
self.bottom = false;
self
}
pub fn show_left(mut self) -> Self {
self.left = true;
self
}
pub fn show_right(mut self) -> Self {
self.right = true;
self
}
pub fn show_top(mut self) -> Self {
self.top = true;
self
}
pub fn show_bottom(mut self) -> Self {
self.bottom = true;
self
}
pub fn any_visible(&self) -> bool {
self.left || self.right || self.top || self.bottom
}
pub fn offset_px(&self, dpi: f32) -> f32 {
pt_to_px(self.offset, dpi)
}
}
impl Default for SpineConfig {
fn default() -> Self {
Self {
left: true,
right: true,
top: true,
bottom: true,
offset: 0.0,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct PlotConfig {
pub figure: FigureConfig,
pub typography: TypographyConfig,
pub lines: LineConfig,
pub spacing: SpacingConfig,
pub margins: MarginConfig,
pub spines: SpineConfig,
}
impl PlotConfig {
pub fn builder() -> PlotConfigBuilder {
PlotConfigBuilder::default()
}
pub fn canvas_size(&self) -> (u32, u32) {
self.figure.canvas_size()
}
pub fn dpi(&self) -> f32 {
self.figure.dpi
}
pub fn compute_margins(
&self,
has_title: bool,
has_xlabel: bool,
has_ylabel: bool,
) -> ComputedMargins {
match &self.margins {
MarginConfig::Proportional {
left,
right,
top,
bottom,
} => {
ComputedMargins {
left: self.figure.width * left,
right: self.figure.width * right,
top: self.figure.height * top,
bottom: self.figure.height * bottom,
}
}
MarginConfig::Auto { min, max } => {
let top = if has_title {
(crate::core::pt_to_in(self.typography.title_size())
+ crate::core::pt_to_in(self.spacing.title_pad)
+ 0.15)
.clamp(*min, *max)
} else {
*min
};
let bottom = if has_xlabel {
(crate::core::pt_to_in(self.typography.label_size())
+ crate::core::pt_to_in(self.typography.tick_size())
+ crate::core::pt_to_in(self.spacing.label_pad)
+ crate::core::pt_to_in(self.spacing.tick_pad)
+ 0.1)
.clamp(*min, *max)
} else {
(crate::core::pt_to_in(self.typography.tick_size())
+ crate::core::pt_to_in(self.spacing.tick_pad)
+ 0.1)
.clamp(*min, *max)
};
let left = if has_ylabel {
(crate::core::pt_to_in(self.typography.label_size())
+ crate::core::pt_to_in(self.typography.tick_size()) * 4.0
+ crate::core::pt_to_in(self.spacing.label_pad)
+ 0.2)
.clamp(*min, *max)
} else {
(crate::core::pt_to_in(self.typography.tick_size()) * 4.0 + 0.15)
.clamp(*min, *max)
};
let right = (*min).max(0.2);
ComputedMargins {
left,
right,
top,
bottom,
}
}
MarginConfig::Fixed {
left,
right,
top,
bottom,
} => ComputedMargins {
left: *left,
right: *right,
top: *top,
bottom: *bottom,
},
MarginConfig::ContentDriven { .. } => {
ComputedMargins {
left: 0.8,
right: 0.3,
top: 0.5,
bottom: 0.6,
}
}
}
}
pub fn from_pixels(width_px: u32, height_px: u32) -> Self {
Self {
figure: FigureConfig {
width: px_to_in(width_px as f32, REFERENCE_DPI),
height: px_to_in(height_px as f32, REFERENCE_DPI),
dpi: REFERENCE_DPI,
},
..Default::default()
}
}
}
#[derive(Debug, Clone, Default)]
pub struct PlotConfigBuilder {
config: PlotConfig,
}
impl PlotConfigBuilder {
pub fn figure(mut self, width: f32, height: f32) -> Self {
self.config.figure.width = width;
self.config.figure.height = height;
self
}
pub fn figure_px(mut self, width_px: u32, height_px: u32) -> Self {
self.config.figure.width = px_to_in(width_px as f32, REFERENCE_DPI);
self.config.figure.height = px_to_in(height_px as f32, REFERENCE_DPI);
self
}
pub fn dpi(mut self, dpi: f32) -> Self {
self.config.figure.dpi = dpi;
self
}
pub fn typography<F>(mut self, f: F) -> Self
where
F: FnOnce(TypographyConfig) -> TypographyConfig,
{
self.config.typography = f(self.config.typography);
self
}
pub fn font_size(mut self, size: f32) -> Self {
self.config.typography.base_size = size;
self
}
pub fn lines<F>(mut self, f: F) -> Self
where
F: FnOnce(LineConfig) -> LineConfig,
{
self.config.lines = f(self.config.lines);
self
}
pub fn line_width(mut self, width: f32) -> Self {
self.config.lines.data_width = width;
self
}
pub fn spacing<F>(mut self, f: F) -> Self
where
F: FnOnce(SpacingConfig) -> SpacingConfig,
{
self.config.spacing = f(self.config.spacing);
self
}
pub fn margins(mut self, margins: MarginConfig) -> Self {
self.config.margins = margins;
self
}
pub fn spines(mut self, spines: SpineConfig) -> Self {
self.config.spines = spines;
self
}
pub fn despine(mut self) -> Self {
self.config.spines = SpineConfig::despine();
self
}
pub fn build(self) -> PlotConfig {
self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_figure_config_default() {
let config = FigureConfig::default();
assert!((config.width - 6.4).abs() < 0.001);
assert!((config.height - 4.8).abs() < 0.001);
assert!((config.dpi - 100.0).abs() < 0.001);
}
#[test]
fn test_figure_config_canvas_size() {
let config = FigureConfig::default();
let (w, h) = config.canvas_size();
assert_eq!(w, 640);
assert_eq!(h, 480);
let config = FigureConfig::new(6.4, 4.8, 300.0);
let (w, h) = config.canvas_size();
assert_eq!(w, 1920);
assert_eq!(h, 1440);
}
#[test]
fn test_typography_config_default() {
let config = TypographyConfig::default();
assert!((config.base_size - 10.0).abs() < 0.001);
assert!((config.title_scale - 1.4).abs() < 0.001);
}
#[test]
fn test_typography_computed_sizes() {
let config = TypographyConfig::default();
assert!((config.title_size() - 14.0).abs() < 0.001);
assert!((config.label_size() - 10.0).abs() < 0.001);
assert!((config.tick_size() - 9.0).abs() < 0.001);
}
#[test]
fn test_typography_scale() {
let config = TypographyConfig::default().scale(1.5);
assert!((config.base_size - 15.0).abs() < 0.001);
assert!((config.title_size() - 21.0).abs() < 0.001); }
#[test]
fn test_line_config_default() {
let config = LineConfig::default();
assert!((config.data_width - 1.5).abs() < 0.001);
assert!((config.axis_width - 0.8).abs() < 0.001);
}
#[test]
fn test_margin_config_auto() {
let config = MarginConfig::auto();
match config {
MarginConfig::Auto { min, max } => {
assert!((min - 0.3).abs() < 0.001);
assert!((max - 1.0).abs() < 0.001);
}
_ => panic!("Expected Auto"),
}
}
#[test]
fn test_plot_config_builder() {
let config = PlotConfig::builder()
.figure(8.0, 6.0)
.dpi(150.0)
.font_size(12.0)
.line_width(2.0)
.build();
assert!((config.figure.width - 8.0).abs() < 0.001);
assert!((config.figure.height - 6.0).abs() < 0.001);
assert!((config.figure.dpi - 150.0).abs() < 0.001);
assert!((config.typography.base_size - 12.0).abs() < 0.001);
assert!((config.lines.data_width - 2.0).abs() < 0.001);
}
#[test]
fn test_plot_config_canvas_size() {
let config = PlotConfig::default();
let (w, h) = config.canvas_size();
assert_eq!(w, 640);
assert_eq!(h, 480);
}
#[test]
fn test_computed_margins() {
let config = PlotConfig::default();
let margins = config.compute_margins(true, true, true);
assert!(margins.left > 0.0);
assert!(margins.right > 0.0);
assert!(margins.top > 0.0);
assert!(margins.bottom > 0.0);
assert!(margins.left > margins.right);
}
#[test]
fn test_dpi_independence() {
let config = PlotConfig::default();
let font_px_100 = config.typography.title_size_px(100.0);
let canvas_100 = config.figure.canvas_size();
let ratio_100 = font_px_100 / canvas_100.0 as f32;
let font_px_300 = config.typography.title_size_px(300.0);
let canvas_300 = (
in_to_px(config.figure.width, 300.0) as u32,
in_to_px(config.figure.height, 300.0) as u32,
);
let ratio_300 = font_px_300 / canvas_300.0 as f32;
assert!((ratio_100 - ratio_300).abs() < 0.0001);
}
#[test]
fn test_spine_config_default() {
let spines = SpineConfig::default();
assert!(spines.left);
assert!(spines.right);
assert!(spines.top);
assert!(spines.bottom);
assert!((spines.offset - 0.0).abs() < 0.001);
}
#[test]
fn test_spine_config_despine() {
let spines = SpineConfig::despine();
assert!(spines.left);
assert!(!spines.right);
assert!(!spines.top);
assert!(spines.bottom);
}
#[test]
fn test_spine_config_none() {
let spines = SpineConfig::none();
assert!(!spines.left);
assert!(!spines.right);
assert!(!spines.top);
assert!(!spines.bottom);
assert!(!spines.any_visible());
}
#[test]
fn test_spine_config_builder_chain() {
let spines = SpineConfig::default()
.hide_top()
.hide_right()
.with_offset(5.0);
assert!(spines.left);
assert!(!spines.right);
assert!(!spines.top);
assert!(spines.bottom);
assert!((spines.offset - 5.0).abs() < 0.001);
}
#[test]
fn test_spine_config_offset_px() {
let spines = SpineConfig::default().with_offset(10.0);
assert!((spines.offset_px(72.0) - 10.0).abs() < 0.001);
assert!((spines.offset_px(144.0) - 20.0).abs() < 0.001);
}
#[test]
fn test_plot_config_builder_despine() {
let config = PlotConfig::builder().despine().build();
assert!(config.spines.left);
assert!(!config.spines.right);
assert!(!config.spines.top);
assert!(config.spines.bottom);
}
}