charton 0.5.0

A high-performance, layered charting system for Rust, featuring a flexible data core and multi-backend rendering.
Documentation
pub mod color;
pub mod shape;
pub mod size;
pub mod text;
pub mod x;
pub mod y;
pub mod y2;

use self::{color::Color, shape::Shape, size::Size, text::Text, x::X, y::Y, y2::Y2};
use crate::scale::{Expansion, Scale};

/// Represents the various visual aesthetics that can be mapped to data.
///
/// By using an enum, we can write generic logic in the rendering engine
/// to process all channels in a loop rather than writing custom code for
/// each axis or legend.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum Channel {
    X,
    Y,
    Color,
    Shape,
    Size,
}

/// Unified application interface for encoding specifications.
///
/// This trait allows different encoding types (like X, Color, or Size) to be
/// added to the global `Encoding` container.
pub trait IntoEncoding {
    /// Consumes the specification and applies it to the provided `Encoding` container.
    fn apply(self, enc: &mut Encoding);
}

/// Global encoding container.
///
/// The `Encoding` struct serves as a central repository for all visual encoding
/// specifications in a chart. It holds the "Intent" (user configuration) for
/// how data fields map to visual properties.
///
/// By using the `Channel` enum, this container can be accessed dynamically
/// by the rendering engine during the "Resolution" phase.
#[derive(Default, Clone)]
pub struct Encoding {
    pub(crate) x: Option<X>,
    pub(crate) y: Option<Y>,
    pub(crate) y2: Option<Y2>,
    pub(crate) color: Option<Color>,
    pub(crate) shape: Option<Shape>,
    pub(crate) size: Option<Size>,
    pub(crate) text: Option<Text>,
}

impl Encoding {
    pub(crate) fn new() -> Self {
        Default::default()
    }

    /// Checks if any visual channels have been mapped to data fields.
    ///
    /// This is used by the Chart state machine to determine if validation
    /// and data transformation should be triggered during a mark transition.
    pub fn is_empty(&self) -> bool {
        self.x.is_none()
            && self.y.is_none()
            && self.y2.is_none()
            && self.color.is_none()
            && self.shape.is_none()
            && self.size.is_none()
            && self.text.is_none()
    }

    /// Returns the data field name associated with a specific visual channel.
    ///
    /// This is used by the `LayeredChart` to discover which columns in the
    /// dataframe need to be processed for scale training.
    pub fn get_field_by_channel(&self, channel: Channel) -> Option<&str> {
        match channel {
            Channel::X => self.x.as_ref().map(|v| v.field.as_str()),
            Channel::Y => self.y.as_ref().map(|v| v.field.as_str()),
            Channel::Color => self.color.as_ref().map(|v| v.field.as_str()),
            Channel::Shape => self.shape.as_ref().map(|v| v.field.as_str()),
            Channel::Size => self.size.as_ref().map(|v| v.field.as_str()),
        }
    }

    /// Retrieves the user-defined scale type (e.g., Linear, Log, Time) for a channel.
    pub fn get_scale_by_channel(&self, channel: Channel) -> Option<Scale> {
        match channel {
            Channel::X => self.x.as_ref().and_then(|v| v.scale_type),
            Channel::Y => self.y.as_ref().and_then(|v| v.scale_type),
            Channel::Color => self.color.as_ref().and_then(|v| v.scale_type),
            Channel::Shape => self.shape.as_ref().and_then(|v| v.scale_type),
            Channel::Size => self.size.as_ref().and_then(|v| v.scale_type),
        }
    }

    /// Retrieves the expansion (padding) preferences for a channel.
    pub fn get_expand_by_channel(&self, channel: Channel) -> Option<Expansion> {
        match channel {
            Channel::X => self.x.as_ref().and_then(|v| v.expansion),
            Channel::Y => self.y.as_ref().and_then(|v| v.expansion),
            Channel::Color => self.color.as_ref().and_then(|v| v.expansion),
            Channel::Shape => self.shape.as_ref().and_then(|v| v.expansion),
            Channel::Size => self.size.as_ref().and_then(|v| v.expansion),
        }
    }

    /// Checks if the channel is explicitly configured to include zero in its axis range.
    pub fn get_zero_by_channel(&self, channel: Channel) -> bool {
        match channel {
            Channel::X => self.x.as_ref().and_then(|v| v.zero),
            Channel::Y => self.y.as_ref().and_then(|v| v.zero),
            _ => None,
        }
        .unwrap_or(false)
    }
}

/* ---------- IntoEncoding Implementations ---------- */

impl IntoEncoding for X {
    fn apply(self, enc: &mut Encoding) {
        enc.x = Some(self);
    }
}

impl IntoEncoding for Y {
    fn apply(self, enc: &mut Encoding) {
        enc.y = Some(self);
    }
}

impl IntoEncoding for Y2 {
    fn apply(self, enc: &mut Encoding) {
        enc.y2 = Some(self);
    }
}

impl IntoEncoding for Color {
    fn apply(self, enc: &mut Encoding) {
        enc.color = Some(self);
    }
}

impl IntoEncoding for Shape {
    fn apply(self, enc: &mut Encoding) {
        enc.shape = Some(self);
    }
}

impl IntoEncoding for Size {
    fn apply(self, enc: &mut Encoding) {
        enc.size = Some(self);
    }
}

impl IntoEncoding for Text {
    fn apply(self, enc: &mut Encoding) {
        enc.text = Some(self);
    }
}

/// Macro to implement IntoEncoding for tuples (e.g., .encode((X::new("a"), Y::new("b"))))
macro_rules! impl_tuple_encoding {
    ($($idx:tt $T:ident),+) => {
        impl<$($T: IntoEncoding),+> IntoEncoding for ($($T,)+) {
            #[inline]
            fn apply(self, enc: &mut Encoding) {
                $( self.$idx.apply(enc); )+
            }
        }
    };
}

impl_tuple_encoding!(0 T0);
impl_tuple_encoding!(0 T0, 1 T1);
impl_tuple_encoding!(0 T0, 1 T1, 2 T2);
impl_tuple_encoding!(0 T0, 1 T1, 2 T2, 3 T3);
impl_tuple_encoding!(0 T0, 1 T1, 2 T2, 3 T3, 4 T4);
impl_tuple_encoding!(0 T0, 1 T1, 2 T2, 3 T3, 4 T4, 5 T5);
impl_tuple_encoding!(0 T0, 1 T1, 2 T2, 3 T3, 4 T4, 5 T5, 6 T6);
impl_tuple_encoding!(0 T0, 1 T1, 2 T2, 3 T3, 4 T4, 5 T5, 6 T6, 7 T7);
impl_tuple_encoding!(0 T0, 1 T1, 2 T2, 3 T3, 4 T4, 5 T5, 6 T6, 7 T7, 8 T8);