plotlars 0.12.0

Plotlars is a Rust library designed to facilitate the integration between the Polars data analysis library and visualization libraries.
use plotlars::polars::prelude::*;
use plotlars::{
    Arrangement, Axis, BarPlot, BoxPlot, CandlestickPlot, ColorBar, CsvReader, Direction, HeatMap,
    Histogram, Legend, Line, Mode, Orientation, Palette, Rgb, SankeyDiagram, Scatter3dPlot,
    ScatterGeo, ScatterMap, ScatterPlot, ScatterPolar, Shape, SubplotGrid, Text, TickDirection,
    TimeSeriesPlot, ValueExponent,
};

fn main() {
    regular_grid_example();
    irregular_grid_example();
    mixed_grid_example();
}

fn regular_grid_example() {
    let dataset1 = CsvReader::new("data/animal_statistics.csv")
        .finish()
        .unwrap();

    let plot1 = BarPlot::builder()
        .data(&dataset1)
        .labels("animal")
        .values("value")
        .orientation(Orientation::Vertical)
        .group("gender")
        .sort_groups_by(|a, b| a.len().cmp(&b.len()))
        .error("error")
        .colors(vec![Rgb(255, 127, 80), Rgb(64, 224, 208)])
        .plot_title(Text::from("Bar Plot").x(-0.05).y(1.35).size(14))
        .y_title(Text::from("value").x(-0.055).y(0.76))
        .x_title(Text::from("animal").x(0.97).y(-0.2))
        .legend(
            &Legend::new()
                .orientation(Orientation::Horizontal)
                .x(0.4)
                .y(1.2),
        )
        .build();

    let dataset2 = CsvReader::new("data/penguins.csv")
        .finish()
        .unwrap()
        .lazy()
        .select([
            col("species"),
            col("sex").alias("gender"),
            col("flipper_length_mm").cast(DataType::Int16),
            col("body_mass_g").cast(DataType::Int16),
        ])
        .collect()
        .unwrap();

    let axis = Axis::new()
        .show_line(true)
        .tick_direction(TickDirection::OutSide)
        .value_thousands(true);

    let plot2 = ScatterPlot::builder()
        .data(&dataset2)
        .x("body_mass_g")
        .y("flipper_length_mm")
        .group("species")
        .sort_groups_by(|a, b| {
            if a.len() == b.len() {
                a.cmp(b)
            } else {
                a.len().cmp(&b.len())
            }
        })
        .opacity(0.5)
        .size(12)
        .colors(vec![Rgb(178, 34, 34), Rgb(65, 105, 225), Rgb(255, 140, 0)])
        .shapes(vec![Shape::Circle, Shape::Square, Shape::Diamond])
        .plot_title(Text::from("Scatter Plot").x(-0.075).y(1.35).size(14))
        .x_title(Text::from("body mass (g)").y(-0.4))
        .y_title(Text::from("flipper length (mm)").x(-0.078).y(0.5))
        .legend_title("species")
        .x_axis(&axis.clone().value_range(2500.0, 6500.0))
        .y_axis(&axis.clone().value_range(170.0, 240.0))
        .legend(&Legend::new().x(0.98).y(0.95))
        .build();

    let dataset3 = CsvReader::new("data/debilt_2023_temps.csv")
        .has_header(true)
        .try_parse_dates(true)
        .finish()
        .unwrap()
        .lazy()
        .with_columns(vec![
            (col("tavg") / lit(10)).alias("avg"),
            (col("tmin") / lit(10)).alias("min"),
            (col("tmax") / lit(10)).alias("max"),
        ])
        .collect()
        .unwrap();

    let plot3 = TimeSeriesPlot::builder()
        .data(&dataset3)
        .x("date")
        .y("avg")
        .additional_series(vec!["min", "max"])
        .colors(vec![Rgb(128, 128, 128), Rgb(0, 122, 255), Rgb(255, 128, 0)])
        .lines(vec![Line::Solid, Line::Dot, Line::Dot])
        .plot_title(Text::from("Time Series Plot").x(-0.05).y(1.35).size(14))
        .y_title(Text::from("temperature (ºC)").x(-0.055).y(0.6))
        .legend(&Legend::new().x(0.9).y(1.25))
        .build();

    let plot4 = BoxPlot::builder()
        .data(&dataset2)
        .labels("species")
        .values("body_mass_g")
        .orientation(Orientation::Vertical)
        .group("gender")
        .box_points(true)
        .point_offset(-1.5)
        .jitter(0.01)
        .opacity(0.1)
        .colors(vec![Rgb(0, 191, 255), Rgb(57, 255, 20), Rgb(255, 105, 180)])
        .plot_title(Text::from("Box Plot").x(-0.075).y(1.35).size(14))
        .x_title(Text::from("species").y(-0.3))
        .y_title(Text::from("body mass (g)").x(-0.08).y(0.5))
        .legend_title(Text::from("gender").size(12))
        .y_axis(&Axis::new().value_thousands(true))
        .legend(&Legend::new().x(1.0))
        .build();

    SubplotGrid::regular()
        .plots(vec![&plot1, &plot2, &plot3, &plot4])
        .rows(2)
        .cols(2)
        .v_gap(0.4)
        .title(
            Text::from("Regular Subplot Grid")
                .size(16)
                .font("Arial Black")
                .y(0.95),
        )
        .build()
        .plot();
}

fn irregular_grid_example() {
    let dataset1 = CsvReader::new("data/penguins.csv")
        .finish()
        .unwrap()
        .lazy()
        .select([
            col("species"),
            col("sex").alias("gender"),
            col("flipper_length_mm").cast(DataType::Int16),
            col("body_mass_g").cast(DataType::Int16),
        ])
        .collect()
        .unwrap();

    let axis = Axis::new()
        .show_line(true)
        .show_grid(true)
        .value_thousands(true)
        .tick_direction(TickDirection::OutSide);

    let plot1 = Histogram::builder()
        .data(&dataset1)
        .x("body_mass_g")
        .group("species")
        .opacity(0.5)
        .colors(vec![Rgb(255, 165, 0), Rgb(147, 112, 219), Rgb(46, 139, 87)])
        .plot_title(Text::from("Histogram").x(0.0).y(1.35).size(14))
        .x_title(Text::from("body mass (g)").x(0.94).y(-0.35))
        .y_title(Text::from("count").x(-0.062).y(0.83))
        .x_axis(&axis)
        .y_axis(&axis)
        .legend_title(Text::from("species"))
        .legend(&Legend::new().x(0.87).y(1.2))
        .build();

    let dataset2 = CsvReader::new("data/stock_prices.csv").finish().unwrap();

    let increasing = Direction::new()
        .line_color(Rgb(0, 200, 100))
        .line_width(0.5);

    let decreasing = Direction::new()
        .line_color(Rgb(200, 50, 50))
        .line_width(0.5);

    let plot2 = CandlestickPlot::builder()
        .data(&dataset2)
        .dates("date")
        .open("open")
        .high("high")
        .low("low")
        .close("close")
        .increasing(&increasing)
        .decreasing(&decreasing)
        .whisker_width(0.1)
        .plot_title(Text::from("Candlestick").x(0.0).y(1.35).size(14))
        .y_title(Text::from("price ($)").x(-0.06).y(0.76))
        .y_axis(&Axis::new().show_axis(true).show_grid(true))
        .build();

    let dataset3 = CsvReader::new("data/heatmap.csv").finish().unwrap();

    let plot3 = HeatMap::builder()
        .data(&dataset3)
        .x("x")
        .y("y")
        .z("z")
        .color_bar(
            &ColorBar::new()
                .value_exponent(ValueExponent::None)
                .separate_thousands(true)
                .tick_length(5)
                .tick_step(5000.0),
        )
        .plot_title(Text::from("Heat Map").x(0.0).y(1.35).size(14))
        .color_scale(Palette::Viridis)
        .build();

    SubplotGrid::irregular()
        .plots(vec![
            (&plot1, 0, 0, 1, 1),
            (&plot2, 0, 1, 1, 1),
            (&plot3, 1, 0, 1, 2),
        ])
        .rows(2)
        .cols(2)
        .v_gap(0.35)
        .h_gap(0.05)
        .title(
            Text::from("Irregular Subplot Grid")
                .size(16)
                .font("Arial Black")
                .y(0.95),
        )
        .build()
        .plot();
}

fn mixed_grid_example() {
    // 2D cartesian scatter (baseline)
    let penguins = CsvReader::new("data/penguins.csv")
        .finish()
        .unwrap()
        .lazy()
        .select([
            col("species"),
            col("bill_length_mm"),
            col("flipper_length_mm"),
            col("body_mass_g"),
        ])
        .collect()
        .unwrap();

    let scatter_2d = ScatterPlot::builder()
        .data(&penguins)
        .x("bill_length_mm")
        .y("flipper_length_mm")
        .group("species")
        .opacity(0.65)
        .size(10)
        .plot_title(Text::from("Penguins 2D").y(1.3))
        .build();

    // 3D scene subplot
    let scatter_3d = Scatter3dPlot::builder()
        .data(&penguins)
        .x("bill_length_mm")
        .y("flipper_length_mm")
        .z("body_mass_g")
        .group("species")
        .opacity(0.35)
        .size(6)
        .plot_title(Text::from("Penguins 3D").y(1.45))
        .build();

    // Polar subplot
    let polar_df = CsvReader::new("data/product_comparison_polar.csv")
        .finish()
        .unwrap();

    let polar = ScatterPolar::builder()
        .data(&polar_df)
        .theta("angle")
        .r("score")
        .group("product")
        .mode(Mode::LinesMarkers)
        .size(10)
        .plot_title(Text::from("Product Comparison (Polar)").y(1.5).x(0.72))
        .legend(&Legend::new().x(0.8))
        .build();

    // Domain-based subplot (Sankey)
    let sankey_df = CsvReader::new("data/energy_transition.csv")
        .finish()
        .unwrap();

    let sankey = SankeyDiagram::builder()
        .data(&sankey_df)
        .sources("source")
        .targets("target")
        .values("value")
        .orientation(Orientation::Horizontal)
        .arrangement(Arrangement::Freeform)
        .plot_title(Text::from("Energy Flow").y(1.2))
        .build();

    // Mapbox subplot
    let map_df = CsvReader::new("data/cities.csv").finish().unwrap();

    let scatter_map = ScatterMap::builder()
        .data(&map_df)
        .latitude("latitude")
        .longitude("longitude")
        .group("city")
        .zoom(4)
        .center([50.0, 5.0])
        .opacity(0.8)
        .plot_title(Text::from("Cities (Mapbox)").y(1.2))
        .build();

    // Geo subplot
    let geo_df = CsvReader::new("data/world_cities.csv").finish().unwrap();

    let scatter_geo = ScatterGeo::builder()
        .data(&geo_df)
        .lat("lat")
        .lon("lon")
        .group("continent")
        .mode(Mode::Markers)
        .size(10)
        .color(Rgb(255, 140, 0))
        .shape(Shape::Circle)
        .plot_title(Text::from("Global Cities (Geo)").x(0.65).y(1.2))
        .legend(&Legend::new().x(0.8))
        .build();

    SubplotGrid::regular()
        .plots(vec![
            &scatter_2d,
            &scatter_3d,
            &polar,
            &sankey,
            &scatter_map,
            &scatter_geo,
        ])
        .rows(2)
        .cols(3)
        .h_gap(0.12)
        .v_gap(0.22)
        .title(
            Text::from("Mixed Subplot Grid")
                .size(16)
                .font("Arial Black")
                .y(0.95),
        )
        .build()
        .plot();
}