fluent-plots 0.1.0

A declarative data visualization library with a fluent API, built on Polars and Charming.
Documentation
use polars::prelude::*;
use plotters::prelude::*;
use std::error::Error;

#[cfg(feature = "interactive")]
use charming::{
    component::{Axis, Title},
    element::AxisType,
    series::{Bar, Line, Scatter, Series},
    Chart as CharmingChart,
};

#[derive(Clone)]
pub enum ChartType {
    Bar,
    Line,
    Scatter,
}

#[derive(Clone)]
pub struct Chart {
    chart_type: ChartType,
    dataframe: DataFrame,
    x_column: String,
    y_column: String,
}

pub fn barchart(dataframe: DataFrame) -> Chart {
    Chart {
        chart_type: ChartType::Bar,
        dataframe,
        x_column: String::new(),
        y_column: String::new(),
    }
}

pub fn linechart(dataframe: DataFrame) -> Chart {
    Chart {
        chart_type: ChartType::Line,
        dataframe,
        x_column: String::new(),
        y_column: String::new(),
    }
}

pub fn scatterplot(dataframe: DataFrame) -> Chart {
    Chart {
        chart_type: ChartType::Scatter,
        dataframe,
        x_column: String::new(),
        y_column: String::new(),
    }
}

impl Chart {
    pub fn x(mut self, column_name: &str) -> Self {
        self.x_column = column_name.to_string();
        self
    }

    pub fn y(mut self, column_name: &str) -> Self {
        self.y_column = column_name.to_string();
        self
    }

    pub fn draw(&self, path: &str) -> std::result::Result<(), Box<dyn Error>> {
        let root = BitMapBackend::new(path, (640, 480)).into_drawing_area();
        root.fill(&WHITE)?;

        println!("Drawing static chart with x='{}' and y='{}'", self.x_column, self.y_column);

        root.present()?;
        Ok(())
    }
}

#[cfg(feature = "interactive")]
impl Chart {
    pub fn to_interactive_html(&self) -> std::result::Result<String, Box<dyn Error>> {
        let x_data: Vec<String> = self
            .dataframe
            .column(&self.x_column)?
            .str()?
            .into_iter()
            .map(|opt_val| opt_val.unwrap_or("").to_string())
            .collect();

        let y_data: Vec<f64> = self
            .dataframe
            .column(&self.y_column)?
            .cast(&DataType::Float64)?
            .f64()?
            .into_iter()
            .map(|opt_val| opt_val.unwrap_or(0.0))
            .collect();

        let series: Series = match self.chart_type {
            ChartType::Bar => Bar::new().data(y_data).into(),
            ChartType::Line => Line::new().data(y_data).into(),
            ChartType::Scatter => Scatter::new().data(y_data).into(),
        };

        let chart = CharmingChart::new()
            .title(Title::new().text("fluent-plots chart"))
            .x_axis(Axis::new().type_(AxisType::Category).data(x_data))
            .y_axis(Axis::new().type_(AxisType::Value))
            .series(series);

        let html = format!(
            r#"
            <!DOCTYPE html>
            <html>
            <head>
                <meta charset="utf-8" />
                <title>Fluent-Plots Chart</title>
                <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
            </head>
            <body>
                <div id="main" style="width: 800px;height:600px;"></div>
                <script type="text/javascript">
                    var myChart = echarts.init(document.getElementById('main'));
                    var option = {json_config};
                    myChart.setOption(option);
                </script>
            </body>
            </html>
            "#,
            json_config = chart.to_string()
        );

        Ok(html)
    }
}