opsis 0.1.0

Config-driven framework for blazingly fast visualizations.
Documentation
//! # opsis
//!
//! Vega-style, TOML-driven plotting framework for Rust. One [`ChartSpec`]
//! drives either an interactive desktop window (`egui`) or a terminal UI
//! (`ratatui`).
//!
//! ```no_run
//! use opsis::ChartSpec;
//!
//! let spec = ChartSpec::from_toml_path("chart.toml")?;
//! # #[cfg(feature = "egui-backend")]
//! opsis::show(spec)?;
//! # Ok::<(), opsis::OpsisError>(())
//! ```
//!
//! See `examples/configs/` for sample TOML and `examples/` for runnable
//! Rust demos.

pub mod config;
pub mod data;
pub mod error;
pub mod render;

pub use config::{
    Aggregate, ChannelType, ChartMeta, ChartSpec, ChartType, DataFormat, DataSource, Encoding,
    Mark, Style,
};
pub use data::{Dataset, Record, Value};
pub use error::{OpsisError, Result};

/// Open an egui window for the spec. Blocks until the window is closed.
#[cfg(feature = "egui-backend")]
pub fn show(spec: ChartSpec) -> Result<()> {
    render::egui_backend::show_window(spec)
}

/// Open an egui window from a TOML file path.
#[cfg(feature = "egui-backend")]
pub fn show_path(path: impl AsRef<std::path::Path>) -> Result<()> {
    show(ChartSpec::from_toml_path(path)?)
}

/// Open an egui window from a TOML string.
#[cfg(feature = "egui-backend")]
pub fn show_str(toml: &str) -> Result<()> {
    show(ChartSpec::from_toml_str(toml)?)
}

/// Render the chart in the terminal until the user presses q/Esc/Ctrl-C.
#[cfg(feature = "ratatui-backend")]
pub fn show_terminal(spec: ChartSpec) -> Result<()> {
    render::ratatui_backend::run_terminal(spec)
}

/// Same, from a TOML file.
#[cfg(feature = "ratatui-backend")]
pub fn show_terminal_path(path: impl AsRef<std::path::Path>) -> Result<()> {
    show_terminal(ChartSpec::from_toml_path(path)?)
}

/// Same, from a TOML string.
#[cfg(feature = "ratatui-backend")]
pub fn show_terminal_str(toml: &str) -> Result<()> {
    show_terminal(ChartSpec::from_toml_str(toml)?)
}

#[cfg(test)]
mod tests {
    use super::*;

    const SAMPLE: &str = r#"
[chart]
type = "bar"
title = "Demo"

[data]
values = [
    { category = "A", value = 10 },
    { category = "B", value = 7 },
    { category = "C", value = 14 },
]

[encoding.x]
field = "category"
type = "categorical"

[encoding.y]
field = "value"
type = "quantitative"
"#;

    #[test]
    fn parses_minimal_spec() {
        let spec = ChartSpec::from_toml_str(SAMPLE).expect("parse");
        assert_eq!(spec.chart.r#type, ChartType::Bar);
        spec.validate().unwrap();
        let data = spec.load_data().unwrap();
        assert_eq!(data.len(), 3);
    }

    #[test]
    fn aggregate_categorical_sums() {
        let spec = ChartSpec::from_toml_str(SAMPLE).unwrap();
        let data = spec.load_data().unwrap();
        let bars = render::bar_data(&spec, &data).unwrap();
        assert_eq!(bars.len(), 3);
        assert!((bars.iter().map(|b| b.value).sum::<f64>() - 31.0).abs() < 1e-9);
    }

    #[test]
    fn histogram_default_binning() {
        let xs: Vec<f64> = (0..100).map(|i| i as f64).collect();
        let bins = render::histogram(&xs, None);
        assert!(bins.len() >= 2);
        let total: f64 = bins.iter().map(|b| b.value).sum();
        assert_eq!(total as usize, 100);
    }

    #[test]
    fn unknown_field_errors_helpfully() {
        let data = Dataset::from_json_str(r#"[{"a":1},{"a":2}]"#).unwrap();
        match data.column_f64("missing") {
            Err(OpsisError::UnknownField { field, available }) => {
                assert_eq!(field, "missing");
                assert_eq!(available, vec!["a".to_string()]);
            }
            other => panic!("expected UnknownField, got {other:?}"),
        }
    }
}