rust-ppm 0.1.1

Small RGB image and plotting library for generating PPM graphics
Documentation
/// Axis limits, ticks, labels, and title for a plot.
#[derive(Clone, Debug, PartialEq)]
pub struct Axes {
    /// Lower and upper x-axis limits.
    pub xlim: (f64, f64),
    /// Lower and upper y-axis limits.
    pub ylim: (f64, f64),
    /// Tick values shown on the x axis.
    pub xticks: Vec<f64>,
    /// Tick values shown on the y axis.
    pub yticks: Vec<f64>,
    /// Label drawn beneath the plot.
    pub xlabel: String,
    /// Label drawn beside the plot.
    pub ylabel: String,
    /// Plot title.
    pub title: String,
    auto_xlim: bool,
    auto_ylim: bool,
}

impl Axes {
    /// Creates a new axis configuration with automatic limits and default labels.
    pub fn new() -> Self {
        Self {
            xlim: (0.0, 1.0),
            ylim: (0.0, 1.0),
            xticks: evenly_spaced_ticks((0.0, 1.0), 4),
            yticks: evenly_spaced_ticks((0.0, 1.0), 4),
            xlabel: "x".to_owned(),
            ylabel: "y".to_owned(),
            title: "plot".to_owned(),
            auto_xlim: true,
            auto_ylim: true,
        }
    }

    /// Creates a new axes configuration from explicit x/y limits.
    pub fn from_limits(xlim: (f64, f64), ylim: (f64, f64)) -> Self {
        Self::new().with_xlim(xlim).with_ylim(ylim)
    }

    /// Sets the x-axis limits while keeping the builder pattern.
    pub fn x_limits(self, limits: (f64, f64)) -> Self {
        self.with_xlim(limits)
    }

    /// Sets the y-axis limits while keeping the builder pattern.
    pub fn y_limits(self, limits: (f64, f64)) -> Self {
        self.with_ylim(limits)
    }

    /// Sets the x and y axis labels while keeping the builder pattern.
    pub fn labels(self, x: impl Into<String>, y: impl Into<String>) -> Self {
        self.with_labels(x, y)
    }

    /// Sets the plot title while keeping the builder pattern.
    pub fn title(self, title: impl Into<String>) -> Self {
        self.with_title(title)
    }

    /// Replaces the x-axis tick positions with explicit values.
    pub fn x_ticks(mut self, ticks: impl IntoIterator<Item = f64>) -> Self {
        self.xticks = ticks.into_iter().collect();
        self
    }

    /// Replaces the y-axis tick positions with explicit values.
    pub fn y_ticks(mut self, ticks: impl IntoIterator<Item = f64>) -> Self {
        self.yticks = ticks.into_iter().collect();
        self
    }

    /// Sets the x-axis limits and disables automatic rescaling for that axis.
    pub fn with_xlim(mut self, xlim: (f64, f64)) -> Self {
        self.set_xlim(xlim);
        self
    }

    /// Sets the y-axis limits and disables automatic rescaling for that axis.
    pub fn with_ylim(mut self, ylim: (f64, f64)) -> Self {
        self.set_ylim(ylim);
        self
    }

    /// Sets the x-axis limits and disables automatic rescaling for that axis.
    pub fn set_xlim(&mut self, xlim: (f64, f64)) {
        self.xlim = xlim;
        self.xticks = evenly_spaced_ticks(xlim, 4);
        self.auto_xlim = false;
    }

    /// Sets the y-axis limits and disables automatic rescaling for that axis.
    pub fn set_ylim(&mut self, ylim: (f64, f64)) {
        self.ylim = ylim;
        self.yticks = evenly_spaced_ticks(ylim, 4);
        self.auto_ylim = false;
    }

    /// Re-enables automatic fitting for the x-axis.
    pub fn use_auto_xlim(&mut self) {
        self.auto_xlim = true;
    }

    /// Re-enables automatic fitting for the y-axis.
    pub fn use_auto_ylim(&mut self) {
        self.auto_ylim = true;
    }

    /// Sets the x and y axis labels.
    pub fn with_labels(mut self, xlabel: impl Into<String>, ylabel: impl Into<String>) -> Self {
        self.xlabel = xlabel.into();
        self.ylabel = ylabel.into();
        self
    }

    /// Sets the plot title.
    pub fn with_title(mut self, title: impl Into<String>) -> Self {
        self.title = title.into();
        self
    }

    pub(crate) fn update_auto_limits(
        &mut self,
        xlim: Option<(f64, f64)>,
        ylim: Option<(f64, f64)>,
    ) -> bool {
        let mut changed = false;

        if self.auto_xlim
            && let Some(xlim) = xlim
        {
            changed |= self.xlim != xlim;
            self.xlim = xlim;
            self.xticks = evenly_spaced_ticks(xlim, 4);
        }
        if self.auto_ylim
            && let Some(ylim) = ylim
        {
            changed |= self.ylim != ylim;
            self.ylim = ylim;
            self.yticks = evenly_spaced_ticks(ylim, 4);
        }

        changed
    }
}

impl Default for Axes {
    fn default() -> Self {
        Self::new()
    }
}

fn evenly_spaced_ticks(limits: (f64, f64), intervals: usize) -> Vec<f64> {
    if intervals == 0 {
        return vec![limits.0];
    }

    (0..=intervals)
        .map(|index| limits.0 + (limits.1 - limits.0) * index as f64 / intervals as f64)
        .collect()
}