├── data ├── image.png ├── sankey_flow.csv ├── cities.csv ├── flight_path.csv ├── contour_surface.csv ├── animal_statistics.csv ├── employee_data.csv ├── product_comparison_polar.csv ├── world_cities.csv ├── industry_region.csv ├── us_cities_regions.csv ├── regional_sales.csv ├── us_city_density.csv ├── revenue_and_cost.csv ├── stock_prices.csv ├── energy_transition.csv ├── financial_timeseries.csv ├── wind_patterns.csv ├── heatmap.csv └── debilt_2023_temps.csv ├── src ├── common │ ├── mod.rs │ ├── line.rs │ ├── polar.rs │ ├── mark.rs │ └── layout.rs ├── plots │ ├── mod.rs │ ├── template.rs │ ├── image.rs │ ├── array2dplot.rs │ ├── ohlc.rs │ ├── table.rs │ ├── density_mapbox.rs │ ├── candlestick.rs │ └── scattermap.rs ├── components │ ├── coloring.rs │ ├── mod.rs │ ├── orientation.rs │ ├── exponent.rs │ ├── tick.rs │ ├── line.rs │ ├── color.rs │ ├── intensity.rs │ ├── fill.rs │ ├── arrangement.rs │ ├── mode.rs │ ├── palette.rs │ ├── cell.rs │ ├── header.rs │ ├── direction.rs │ ├── lighting.rs │ ├── legend.rs │ ├── dimensions.rs │ ├── text.rs │ └── facet.rs └── lib.rs ├── examples ├── image.rs ├── array2dplot.rs ├── piechart.rs ├── density_mapbox.rs ├── ohlc.rs ├── contourplot.rs ├── scattermap.rs ├── heatmap.rs ├── candlestick.rs ├── scatter3dplot.rs ├── barplot.rs ├── table.rs ├── sankeydiagram.rs ├── histogram.rs ├── boxplot.rs ├── scatterplot.rs ├── surfaceplot.rs ├── lineplot.rs ├── timeseriesplot.rs ├── export_image.rs ├── scattergeo.rs ├── scatterpolar.rs ├── mesh3d.rs └── dimensions.rs ├── .pre-commit-config.yaml ├── .github └── workflows │ └── rust.yml ├── LICENSE.txt ├── Cargo.toml ├── .gitignore └── CHANGELOG.md /data/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alceal/plotlars/HEAD/data/image.png -------------------------------------------------------------------------------- /data/sankey_flow.csv: -------------------------------------------------------------------------------- 1 | source,target,value 2 | A1,B1,8 3 | A2,B2,4 4 | A1,B2,2 5 | B1,C1,8 6 | B2,C1,4 7 | B2,C2,2 8 | -------------------------------------------------------------------------------- /data/cities.csv: -------------------------------------------------------------------------------- 1 | city,latitude,longitude 2 | London,51.507351,-0.127758 3 | Paris,48.856613,2.352222 4 | Berlin,52.520008,13.404954 5 | -------------------------------------------------------------------------------- /data/flight_path.csv: -------------------------------------------------------------------------------- 1 | city,lat,lon 2 | New York,40.7128,-74.0060 3 | Chicago,41.8781,-87.6298 4 | Denver,39.7392,-104.9903 5 | Los Angeles,34.0522,-118.2437 6 | -------------------------------------------------------------------------------- /data/contour_surface.csv: -------------------------------------------------------------------------------- 1 | x,y,z 2 | 0.0,0.0,0.0 3 | 0.0,7.5,5.0 4 | 0.0,15.0,10.0 5 | 2.5,0.0,5.0 6 | 2.5,7.5,2.5 7 | 2.5,15.0,5.0 8 | 5.0,0.0,10.0 9 | 5.0,7.5,0.0 10 | 5.0,15.0,0.0 11 | -------------------------------------------------------------------------------- /data/animal_statistics.csv: -------------------------------------------------------------------------------- 1 | animal,gender,value,error 2 | giraffe,female,20.0,1.0 3 | giraffe,male,25.0,0.5 4 | orangutan,female,14.0,1.5 5 | orangutan,male,18.0,1.0 6 | monkey,female,23.0,0.5 7 | monkey,male,31.0,1.5 8 | -------------------------------------------------------------------------------- /data/employee_data.csv: -------------------------------------------------------------------------------- 1 | name,department,salary,years 2 | Alice Johnson,Engineering,95000,5 3 | Bob Smith,Marketing,78000,3 4 | Charlie Davis,Engineering,102000,7 5 | Diana Martinez,Sales,85000,4 6 | Eva Wilson,Marketing,82000,2 7 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod layout; 2 | pub(crate) mod line; 3 | pub(crate) mod mark; 4 | pub(crate) mod plot; 5 | pub(crate) mod polar; 6 | 7 | pub(crate) use layout::Layout; 8 | pub(crate) use line::Line; 9 | pub(crate) use mark::Marker; 10 | pub(crate) use plot::PlotHelper; 11 | pub(crate) use polar::Polar; 12 | -------------------------------------------------------------------------------- /examples/image.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{Axis, Image, Plot}; 2 | 3 | fn main() { 4 | let axis = Axis::new().show_axis(false); 5 | 6 | Image::builder() 7 | .path("data/image.png") 8 | .x_axis(&axis) 9 | .y_axis(&axis) 10 | .plot_title("Image Plot") 11 | .build() 12 | .plot(); 13 | } 14 | -------------------------------------------------------------------------------- /data/product_comparison_polar.csv: -------------------------------------------------------------------------------- 1 | angle,score,product 2 | 0,7.0,Product A 3 | 60,8.5,Product A 4 | 120,6.0,Product A 5 | 180,5.5,Product A 6 | 240,9.0,Product A 7 | 300,8.0,Product A 8 | 360,7.0,Product A 9 | 0,6.0,Product B 10 | 60,7.0,Product B 11 | 120,8.0,Product B 12 | 180,9.0,Product B 13 | 240,6.5,Product B 14 | 300,7.5,Product B 15 | 360,6.0,Product B 16 | -------------------------------------------------------------------------------- /data/world_cities.csv: -------------------------------------------------------------------------------- 1 | city,lat,lon,continent,population_millions 2 | London,51.5074,-0.1278,Europe,9.0 3 | Paris,48.8566,2.3522,Europe,2.2 4 | Tokyo,35.6762,139.6503,Asia,13.9 5 | Sydney,-33.8688,151.2093,Oceania,5.3 6 | Cairo,30.0444,31.2357,Africa,9.5 7 | Mumbai,19.0760,72.8777,Asia,12.4 8 | Beijing,39.9042,116.4074,Asia,21.5 9 | Rio de Janeiro,-22.9068,-43.1729,South America,6.7 10 | Toronto,43.6532,-79.3832,North America,2.9 11 | -------------------------------------------------------------------------------- /data/industry_region.csv: -------------------------------------------------------------------------------- 1 | category,region 2 | Tech,North 3 | Tech,North 4 | Tech,North 5 | Finance,North 6 | Finance,North 7 | Healthcare,North 8 | Retail,North 9 | Tech,South 10 | Finance,South 11 | Finance,South 12 | Finance,South 13 | Healthcare,South 14 | Healthcare,South 15 | Retail,South 16 | Tech,West 17 | Finance,West 18 | Healthcare,West 19 | Healthcare,West 20 | Healthcare,West 21 | Retail,West 22 | Retail,West 23 | Retail,West 24 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | 4 | # General 5 | 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.6.0 8 | hooks: 9 | - id: trailing-whitespace 10 | - id: check-toml 11 | - id: check-yaml 12 | - id: check-json 13 | 14 | 15 | # Specific 16 | 17 | - repo: https://github.com/doublify/pre-commit-rust 18 | rev: v1.0 19 | hooks: 20 | - id: cargo-check 21 | - id: clippy 22 | -------------------------------------------------------------------------------- /examples/array2dplot.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{Array2dPlot, Plot, Text}; 2 | 3 | fn main() { 4 | let data = vec![ 5 | vec![[255, 0, 0], [0, 255, 0], [0, 0, 255]], 6 | vec![[0, 0, 255], [255, 0, 0], [0, 255, 0]], 7 | vec![[0, 255, 0], [0, 0, 255], [255, 0, 0]], 8 | ]; 9 | 10 | Array2dPlot::builder() 11 | .data(&data) 12 | .plot_title(Text::from("Array 2D Plot").font("Arial").size(18)) 13 | .build() 14 | .plot(); 15 | } 16 | -------------------------------------------------------------------------------- /data/us_cities_regions.csv: -------------------------------------------------------------------------------- 1 | city,lat,lon,population,region 2 | New York,40.7128,-74.0060,8336817,Northeast 3 | Los Angeles,34.0522,-118.2437,3979576,West 4 | Chicago,41.8781,-87.6298,2693976,Midwest 5 | Houston,29.7604,-95.3698,2320268,South 6 | Phoenix,33.4484,-112.0740,1680992,West 7 | Philadelphia,39.9526,-75.1652,1584064,Northeast 8 | San Antonio,29.4241,-98.4936,1547253,South 9 | San Diego,32.7157,-117.1611,1423851,West 10 | Dallas,32.7767,-96.7970,1343573,South 11 | San Jose,37.3382,-121.8863,1021795,West 12 | -------------------------------------------------------------------------------- /data/regional_sales.csv: -------------------------------------------------------------------------------- 1 | region,product,sales 2 | North,A,180.0 3 | North,B,250.0 4 | North,C,210.0 5 | South,A,55.0 6 | South,B,85.0 7 | South,C,65.0 8 | East,A,140.0 9 | East,B,175.0 10 | East,C,160.0 11 | West,A,35.0 12 | West,B,60.0 13 | West,C,48.0 14 | Southwest,A,95.0 15 | Southwest,B,115.0 16 | Southwest,C,105.0 17 | Northeast,A,230.0 18 | Northeast,B,280.0 19 | Northeast,C,255.0 20 | Southeast,A,70.0 21 | Southeast,B,95.0 22 | Southeast,C,80.0 23 | Northwest,A,45.0 24 | Northwest,B,195.0 25 | Northwest,C,120.0 26 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose 21 | # - name: Run tests 22 | # run: cargo test --verbose 23 | - name: Format 24 | run: cargo fmt 25 | - name: Clippy 26 | run: cargo clippy 27 | - name: Machete 28 | uses: bnjbvr/cargo-machete@main 29 | -------------------------------------------------------------------------------- /examples/piechart.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{PieChart, Plot, Text}; 2 | use polars::prelude::*; 3 | 4 | fn main() { 5 | let dataset = LazyCsvReader::new(PlPath::new("data/penguins.csv")) 6 | .finish() 7 | .unwrap() 8 | .select([col("species")]) 9 | .collect() 10 | .unwrap(); 11 | 12 | PieChart::builder() 13 | .data(&dataset) 14 | .labels("species") 15 | .hole(0.4) 16 | .pull(0.01) 17 | .rotation(20.0) 18 | .plot_title(Text::from("Pie Chart").font("Arial").size(18).x(0.485)) 19 | .build() 20 | .plot(); 21 | } 22 | -------------------------------------------------------------------------------- /examples/density_mapbox.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{DensityMapbox, Plot, Text}; 2 | use polars::prelude::*; 3 | 4 | fn main() { 5 | let data = LazyCsvReader::new(PlPath::new("data/us_city_density.csv")) 6 | .finish() 7 | .unwrap() 8 | .collect() 9 | .unwrap(); 10 | 11 | DensityMapbox::builder() 12 | .data(&data) 13 | .lat("city_lat") 14 | .lon("city_lon") 15 | .z("population_density") 16 | .center([39.8283, -98.5795]) 17 | .zoom(3) 18 | .plot_title(Text::from("Density Mapbox").font("Arial").size(20)) 19 | .build() 20 | .plot(); 21 | } 22 | -------------------------------------------------------------------------------- /examples/ohlc.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{Axis, OhlcPlot, Plot}; 2 | use polars::prelude::*; 3 | 4 | fn main() { 5 | let stock_data = LazyCsvReader::new(PlPath::new("data/stock_prices.csv")) 6 | .finish() 7 | .unwrap() 8 | .collect() 9 | .unwrap(); 10 | 11 | OhlcPlot::builder() 12 | .data(&stock_data) 13 | .dates("date") 14 | .open("open") 15 | .high("high") 16 | .low("low") 17 | .close("close") 18 | .plot_title("OHLC Plot") 19 | .y_title("price ($)") 20 | .y_axis(&Axis::new().show_axis(true)) 21 | .build() 22 | .plot(); 23 | } 24 | -------------------------------------------------------------------------------- /data/us_city_density.csv: -------------------------------------------------------------------------------- 1 | city_lat,city_lon,population_density,city_name 2 | 40.7128,-74.0060,27000.0,New York 3 | 34.0522,-118.2437,8092.0,Los Angeles 4 | 41.8781,-87.6298,11841.0,Chicago 5 | 29.7604,-95.3698,3540.0,Houston 6 | 33.4484,-112.0740,3165.0,Phoenix 7 | 37.7749,-122.4194,18581.0,San Francisco 8 | 47.6062,-122.3321,8386.0,Seattle 9 | 42.3601,-71.0589,13321.0,Boston 10 | 32.7767,-79.9309,4707.0,Charleston 11 | 39.9526,-75.1652,11379.0,Philadelphia 12 | 38.9072,-77.0369,9856.0,Washington DC 13 | 35.2271,-80.8431,2457.0,Charlotte 14 | 30.2672,-97.7431,1386.0,Austin 15 | 36.1699,-115.1398,4525.0,Las Vegas 16 | 39.7392,-104.9903,4193.0,Denver 17 | -------------------------------------------------------------------------------- /data/revenue_and_cost.csv: -------------------------------------------------------------------------------- 1 | Date,Revenue,Cost 2 | 2023-01-31,10320.183877729267,8251.593217832866 3 | 2023-02-28,23361.49949229147,15876.260386054937 4 | 2023-03-31,33853.53981955318,17737.523093159074 5 | 2023-04-30,47114.92739147375,34797.129368772985 6 | 2023-05-31,61960.96581901887,48052.000872334065 7 | 2023-06-30,73843.24767793017,49471.418315826566 8 | 2023-07-31,85638.49495007776,48950.09739941198 9 | 2023-08-31,93548.36563617105,64578.173115080426 10 | 2023-09-30,104221.93731957133,77044.56755616776 11 | 2023-10-31,117776.12613238876,65033.11155742126 12 | 2023-11-30,128275.85434203708,69543.76388145327 13 | 2023-12-31,140358.10583941432,103142.19972170686 14 | -------------------------------------------------------------------------------- /examples/contourplot.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{Coloring, ContourPlot, Palette, Plot, Text}; 2 | use polars::prelude::*; 3 | 4 | fn main() { 5 | let dataset = LazyCsvReader::new(PlPath::new("data/contour_surface.csv")) 6 | .finish() 7 | .unwrap() 8 | .collect() 9 | .unwrap(); 10 | 11 | ContourPlot::builder() 12 | .data(&dataset) 13 | .x("x") 14 | .y("y") 15 | .z("z") 16 | .color_scale(Palette::Viridis) 17 | .reverse_scale(true) 18 | .coloring(Coloring::Fill) 19 | .show_lines(false) 20 | .plot_title(Text::from("Contour Plot").font("Arial").size(18)) 21 | .build() 22 | .plot(); 23 | } 24 | -------------------------------------------------------------------------------- /examples/scattermap.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{Plot, ScatterMap, Text}; 2 | use polars::prelude::*; 3 | 4 | fn main() { 5 | let dataset = LazyCsvReader::new(PlPath::new("data/cities.csv")) 6 | .finish() 7 | .unwrap() 8 | .collect() 9 | .unwrap(); 10 | 11 | ScatterMap::builder() 12 | .data(&dataset) 13 | .latitude("latitude") 14 | .longitude("longitude") 15 | .center([48.856613, 2.352222]) 16 | .zoom(4) 17 | .group("city") 18 | .opacity(0.5) 19 | .size(12) 20 | .plot_title(Text::from("Scatter Map").font("Arial").size(18)) 21 | .legend_title("cities") 22 | .build() 23 | .plot(); 24 | } 25 | -------------------------------------------------------------------------------- /src/plots/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod array2dplot; 2 | pub(crate) mod barplot; 3 | pub(crate) mod boxplot; 4 | pub(crate) mod candlestick; 5 | pub(crate) mod contourplot; 6 | pub(crate) mod density_mapbox; 7 | pub(crate) mod heatmap; 8 | pub(crate) mod histogram; 9 | pub(crate) mod image; 10 | pub(crate) mod lineplot; 11 | pub(crate) mod mesh3d; 12 | pub(crate) mod ohlc; 13 | pub(crate) mod piechart; 14 | pub(crate) mod sankeydiagram; 15 | pub(crate) mod scatter3dplot; 16 | pub(crate) mod scattergeo; 17 | pub(crate) mod scattermap; 18 | pub(crate) mod scatterplot; 19 | pub(crate) mod scatterpolar; 20 | pub(crate) mod subplot_grid; 21 | pub(crate) mod surfaceplot; 22 | pub(crate) mod table; 23 | pub(crate) mod timeseriesplot; 24 | -------------------------------------------------------------------------------- /data/stock_prices.csv: -------------------------------------------------------------------------------- 1 | date,open,high,low,close 2 | 2024-01-01,100.0,103.0,99.0,102.5 3 | 2024-01-02,102.5,104.0,101.5,101.0 4 | 2024-01-03,101.0,103.5,100.0,103.5 5 | 2024-01-04,103.5,106.0,102.5,105.0 6 | 2024-01-05,105.0,107.5,104.0,104.5 7 | 2024-01-08,104.5,107.0,103.5,106.0 8 | 2024-01-09,106.0,108.5,105.0,105.5 9 | 2024-01-10,105.5,108.0,104.5,107.0 10 | 2024-01-11,107.0,109.5,106.0,108.5 11 | 2024-01-12,108.5,111.0,107.5,108.0 12 | 2024-01-15,108.0,110.5,107.0,110.0 13 | 2024-01-16,110.0,112.5,109.0,109.5 14 | 2024-01-17,109.5,112.0,108.5,111.0 15 | 2024-01-18,111.0,113.5,110.0,112.5 16 | 2024-01-19,112.5,115.0,111.5,112.0 17 | 2024-01-22,112.0,114.5,111.0,113.5 18 | 2024-01-23,113.5,116.0,112.5,113.0 19 | 2024-01-24,113.0,115.5,112.0,114.5 20 | 2024-01-25,114.5,117.0,113.5,115.0 21 | 2024-01-26,115.0,117.5,114.0,116.5 22 | -------------------------------------------------------------------------------- /data/energy_transition.csv: -------------------------------------------------------------------------------- 1 | year,source,target,value 2 | 2020,Coal,Fossil Energy,45 3 | 2020,Natural Gas,Fossil Energy,55 4 | 2020,Oil,Fossil Energy,30 5 | 2020,Solar,Renewable Energy,25 6 | 2020,Wind,Renewable Energy,30 7 | 2020,Hydro,Renewable Energy,35 8 | 2021,Coal,Fossil Energy,40 9 | 2021,Natural Gas,Fossil Energy,50 10 | 2021,Oil,Fossil Energy,25 11 | 2021,Solar,Renewable Energy,30 12 | 2021,Wind,Renewable Energy,35 13 | 2021,Hydro,Renewable Energy,40 14 | 2022,Coal,Fossil Energy,35 15 | 2022,Natural Gas,Fossil Energy,45 16 | 2022,Oil,Fossil Energy,20 17 | 2022,Solar,Renewable Energy,35 18 | 2022,Wind,Renewable Energy,40 19 | 2022,Hydro,Renewable Energy,45 20 | 2023,Coal,Fossil Energy,30 21 | 2023,Natural Gas,Fossil Energy,40 22 | 2023,Oil,Fossil Energy,15 23 | 2023,Solar,Renewable Energy,40 24 | 2023,Wind,Renewable Energy,45 25 | 2023,Hydro,Renewable Energy,50 26 | -------------------------------------------------------------------------------- /examples/heatmap.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{ColorBar, HeatMap, Palette, Plot, Text, ValueExponent}; 2 | use polars::prelude::*; 3 | 4 | fn main() { 5 | let dataset = LazyCsvReader::new(PlPath::new("data/heatmap.csv")) 6 | .finish() 7 | .unwrap() 8 | .collect() 9 | .unwrap(); 10 | 11 | HeatMap::builder() 12 | .data(&dataset) 13 | .x("x") 14 | .y("y") 15 | .z("z") 16 | .color_bar( 17 | &ColorBar::new() 18 | .length(0.7) 19 | .value_exponent(ValueExponent::None) 20 | .separate_thousands(true) 21 | .tick_length(5) 22 | .tick_step(2500.0), 23 | ) 24 | .plot_title(Text::from("Heat Map").font("Arial").size(18)) 25 | .color_scale(Palette::Viridis) 26 | .build() 27 | .plot(); 28 | } 29 | -------------------------------------------------------------------------------- /examples/candlestick.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{Axis, CandlestickPlot, Direction, Plot, Rgb}; 2 | use polars::prelude::*; 3 | 4 | fn main() { 5 | let stock_data = LazyCsvReader::new(PlPath::new("data/stock_prices.csv")) 6 | .finish() 7 | .unwrap() 8 | .collect() 9 | .unwrap(); 10 | 11 | let increasing = Direction::new() 12 | .line_color(Rgb(0, 200, 100)) 13 | .line_width(0.5); 14 | 15 | let decreasing = Direction::new() 16 | .line_color(Rgb(200, 50, 50)) 17 | .line_width(0.5); 18 | 19 | CandlestickPlot::builder() 20 | .data(&stock_data) 21 | .dates("date") 22 | .open("open") 23 | .high("high") 24 | .low("low") 25 | .close("close") 26 | .increasing(&increasing) 27 | .decreasing(&decreasing) 28 | .whisker_width(0.1) 29 | .plot_title("Candlestick") 30 | .y_title("price ($)") 31 | .y_axis(&Axis::new().show_axis(true).show_grid(true)) 32 | .build() 33 | .plot(); 34 | } 35 | -------------------------------------------------------------------------------- /examples/scatter3dplot.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{Legend, Plot, Rgb, Scatter3dPlot, Shape}; 2 | use polars::prelude::*; 3 | 4 | fn main() { 5 | let dataset = LazyCsvReader::new(PlPath::new("data/penguins.csv")) 6 | .finish() 7 | .unwrap() 8 | .select([ 9 | col("species"), 10 | col("sex").alias("gender"), 11 | col("bill_length_mm").cast(DataType::Float32), 12 | col("flipper_length_mm").cast(DataType::Int16), 13 | col("body_mass_g").cast(DataType::Int16), 14 | ]) 15 | .collect() 16 | .unwrap(); 17 | 18 | Scatter3dPlot::builder() 19 | .data(&dataset) 20 | .x("body_mass_g") 21 | .y("flipper_length_mm") 22 | .z("bill_length_mm") 23 | .group("species") 24 | .opacity(0.25) 25 | .size(8) 26 | .colors(vec![Rgb(178, 34, 34), Rgb(65, 105, 225), Rgb(255, 140, 0)]) 27 | .shapes(vec![Shape::Circle, Shape::Square, Shape::Diamond]) 28 | .plot_title("Scatter 3D Plot") 29 | .legend(&Legend::new().x(0.6)) 30 | .build() 31 | .plot(); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Alberto Cebada Aleu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/barplot.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{BarPlot, Legend, Orientation, Plot, Rgb, Text}; 2 | use polars::prelude::*; 3 | 4 | fn main() { 5 | let dataset = LazyCsvReader::new(PlPath::new("data/animal_statistics.csv")) 6 | .finish() 7 | .unwrap() 8 | .collect() 9 | .unwrap(); 10 | 11 | BarPlot::builder() 12 | .data(&dataset) 13 | .labels("animal") 14 | .values("value") 15 | .orientation(Orientation::Vertical) 16 | .group("gender") 17 | .sort_groups_by(|a, b| a.len().cmp(&b.len())) 18 | .error("error") 19 | .colors(vec![Rgb(255, 127, 80), Rgb(64, 224, 208)]) 20 | .plot_title(Text::from("Bar Plot").font("Arial").size(18)) 21 | .x_title(Text::from("animal").font("Arial").size(15)) 22 | .y_title(Text::from("value").font("Arial").size(15)) 23 | .legend_title(Text::from("gender").font("Arial").size(15)) 24 | .legend( 25 | &Legend::new() 26 | .orientation(Orientation::Horizontal) 27 | .y(1.0) 28 | .x(0.43), 29 | ) 30 | .build() 31 | .plot(); 32 | } 33 | -------------------------------------------------------------------------------- /examples/table.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{Cell, Header, Plot, Rgb, Table, Text}; 2 | use polars::prelude::*; 3 | 4 | fn main() { 5 | let dataset = LazyCsvReader::new(PlPath::new("data/employee_data.csv")) 6 | .finish() 7 | .unwrap() 8 | .collect() 9 | .unwrap(); 10 | 11 | let header = Header::new() 12 | .values(vec![ 13 | "Employee Name", 14 | "Department", 15 | "Annual Salary ($)", 16 | "Years of Service", 17 | ]) 18 | .align("center") 19 | .font("Arial Bold") 20 | .fill(Rgb(70, 130, 180)); 21 | 22 | let cell = Cell::new() 23 | .align("center") 24 | .height(25.0) 25 | .font("Arial") 26 | .fill(Rgb(240, 248, 255)); 27 | 28 | Table::builder() 29 | .data(&dataset) 30 | .columns(vec!["name", "department", "salary", "years"]) 31 | .header(&header) 32 | .cell(&cell) 33 | .plot_title( 34 | Text::from("Employee Data") 35 | .font("Arial") 36 | .size(20) 37 | .color(Rgb(25, 25, 112)), 38 | ) 39 | .build() 40 | .plot(); 41 | } 42 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plotlars" 3 | version = "0.11.4" 4 | authors = ["Alberto Cebada Aleu "] 5 | edition = "2021" 6 | rust-version = "1.86.0" 7 | description = "Plotlars is a Rust library designed to facilitate the integration between the Polars data analysis library and visualization libraries." 8 | documentation = "https://docs.rs/plotlars/latest/plotlars/" 9 | readme = "README.md" 10 | homepage = "https://github.com/alceal/plotlars" 11 | repository = "https://github.com/alceal/plotlars" 12 | license = "MIT" 13 | keywords = ["chart", "plot", "plotly", "polars", "visualization"] 14 | categories = ["visualization"] 15 | 16 | [lib] 17 | doctest = false 18 | 19 | [dependencies] 20 | bon = "3.8.1" 21 | image = "0.25.9" 22 | indexmap = "2.12.1" 23 | ordered-float = "5.1.0" 24 | plotly = "0.13.5" 25 | polars = { version = "0.52.0", features = ["lazy", "dtype-categorical", "timezones"] } 26 | serde = "1.0.228" 27 | serde_json = "1.0.145" 28 | 29 | [features] 30 | export-chrome = ["plotly/static_export_chromedriver"] 31 | export-firefox = ["plotly/static_export_geckodriver"] 32 | export-download = ["plotly/static_export_wd_download"] 33 | export-default = ["plotly/static_export_default"] 34 | -------------------------------------------------------------------------------- /examples/sankeydiagram.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{Arrangement, Orientation, Plot, Rgb, SankeyDiagram, Text}; 2 | use polars::prelude::*; 3 | 4 | fn main() { 5 | let dataset = LazyCsvReader::new(PlPath::new("data/sankey_flow.csv")) 6 | .finish() 7 | .unwrap() 8 | .collect() 9 | .unwrap(); 10 | 11 | SankeyDiagram::builder() 12 | .data(&dataset) 13 | .sources("source") 14 | .targets("target") 15 | .values("value") 16 | .orientation(Orientation::Horizontal) 17 | .arrangement(Arrangement::Freeform) 18 | .node_colors(vec![ 19 | Rgb(222, 235, 247), 20 | Rgb(198, 219, 239), 21 | Rgb(158, 202, 225), 22 | Rgb(107, 174, 214), 23 | Rgb(66, 146, 198), 24 | Rgb(33, 113, 181), 25 | ]) 26 | .link_colors(vec![ 27 | Rgb(222, 235, 247), 28 | Rgb(198, 219, 239), 29 | Rgb(158, 202, 225), 30 | Rgb(107, 174, 214), 31 | Rgb(66, 146, 198), 32 | Rgb(33, 113, 181), 33 | ]) 34 | .pad(20) 35 | .thickness(30) 36 | .plot_title(Text::from("Sankey Diagram").font("Arial").size(18)) 37 | .build() 38 | .plot(); 39 | } 40 | -------------------------------------------------------------------------------- /src/common/line.rs: -------------------------------------------------------------------------------- 1 | use plotly::common::Line as LinePlotly; 2 | 3 | use crate::components::Line as LineStyle; 4 | 5 | pub(crate) trait Line { 6 | fn create_line( 7 | index: usize, 8 | width: Option, 9 | style: Option, 10 | styles: Option>, 11 | ) -> LinePlotly { 12 | let mut line = LinePlotly::new(); 13 | line = Self::set_width(line, width); 14 | line = Self::set_style(line, style, styles, index); 15 | line 16 | } 17 | 18 | fn set_width(mut line: LinePlotly, width: Option) -> LinePlotly { 19 | if let Some(width) = width { 20 | line = line.width(width); 21 | } 22 | 23 | line 24 | } 25 | 26 | fn set_style( 27 | mut line: LinePlotly, 28 | style: Option, 29 | styles: Option>, 30 | index: usize, 31 | ) -> LinePlotly { 32 | if let Some(style) = style { 33 | line = line.dash(style.to_plotly()); 34 | return line; 35 | } 36 | 37 | if let Some(styles) = styles { 38 | if let Some(style) = styles.get(index) { 39 | line = line.dash(style.to_plotly()); 40 | } 41 | } 42 | 43 | line 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/histogram.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{Axis, Histogram, Legend, Plot, Rgb, Text, TickDirection}; 2 | use polars::prelude::*; 3 | 4 | fn main() { 5 | let dataset = LazyCsvReader::new(PlPath::new("data/penguins.csv")) 6 | .finish() 7 | .unwrap() 8 | .select([ 9 | col("species"), 10 | col("sex").alias("gender"), 11 | col("flipper_length_mm").cast(DataType::Int16), 12 | col("body_mass_g").cast(DataType::Int16), 13 | ]) 14 | .collect() 15 | .unwrap(); 16 | 17 | let axis = Axis::new() 18 | .show_line(true) 19 | .show_grid(true) 20 | .value_thousands(true) 21 | .tick_direction(TickDirection::OutSide); 22 | 23 | Histogram::builder() 24 | .data(&dataset) 25 | .x("body_mass_g") 26 | .group("species") 27 | .opacity(0.5) 28 | .colors(vec![Rgb(255, 165, 0), Rgb(147, 112, 219), Rgb(46, 139, 87)]) 29 | .plot_title(Text::from("Histogram").font("Arial").size(18)) 30 | .x_title(Text::from("body mass (g)").font("Arial").size(15)) 31 | .y_title(Text::from("count").font("Arial").size(15)) 32 | .legend_title(Text::from("species").font("Arial").size(15)) 33 | .x_axis(&axis) 34 | .y_axis(&axis) 35 | .legend(&Legend::new().x(0.9)) 36 | .build() 37 | .plot(); 38 | } 39 | -------------------------------------------------------------------------------- /examples/boxplot.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{Axis, BoxPlot, Legend, Orientation, Plot, Rgb, Text}; 2 | use polars::prelude::*; 3 | 4 | fn main() { 5 | let dataset = LazyCsvReader::new(PlPath::new("data/penguins.csv")) 6 | .finish() 7 | .unwrap() 8 | .select([ 9 | col("species"), 10 | col("sex").alias("gender"), 11 | col("flipper_length_mm").cast(DataType::Int16), 12 | col("body_mass_g").cast(DataType::Int16), 13 | ]) 14 | .collect() 15 | .unwrap(); 16 | 17 | BoxPlot::builder() 18 | .data(&dataset) 19 | .labels("species") 20 | .values("body_mass_g") 21 | .orientation(Orientation::Vertical) 22 | .group("gender") 23 | .box_points(true) 24 | .point_offset(-1.5) 25 | .jitter(0.01) 26 | .opacity(0.1) 27 | .colors(vec![Rgb(0, 191, 255), Rgb(57, 255, 20), Rgb(255, 105, 180)]) 28 | .plot_title(Text::from("Box Plot").font("Arial").size(18)) 29 | .x_title(Text::from("species").font("Arial").size(15)) 30 | .y_title(Text::from("body mass (g)").font("Arial").size(15).x(-0.04)) 31 | .legend_title(Text::from("gender").font("Arial").size(15)) 32 | .y_axis(&Axis::new().value_thousands(true)) 33 | .legend(&Legend::new().border_width(1).x(0.9)) 34 | .build() 35 | .plot(); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/coloring.rs: -------------------------------------------------------------------------------- 1 | use plotly::contour::Coloring as ColoringPlotly; 2 | 3 | /// Enumeration representing the coloring strategy applied to contour levels. 4 | /// 5 | /// # Example 6 | /// 7 | /// ```rust 8 | /// use polars::prelude::*; 9 | /// use plotlars::{Coloring, ContourPlot, Palette, Plot}; 10 | /// 11 | /// let dataset = df!( 12 | /// "x" => &[0.0, 0.0, 0.0, 2.5, 2.5, 2.5, 5.0, 5.0, 5.0], 13 | /// "y" => &[0.0, 7.5, 15.0, 0.0, 7.5, 15.0, 0.0, 7.5, 15.0], 14 | /// "z" => &[0.0, 5.0, 10.0, 5.0, 2.5, 5.0, 10.0, 0.0, 0.0], 15 | /// ) 16 | /// .unwrap(); 17 | /// 18 | /// ContourPlot::builder() 19 | /// .data(&dataset) 20 | /// .x("x") 21 | /// .y("y") 22 | /// .z("z") 23 | /// .coloring(Coloring::Lines) 24 | /// .color_scale(Palette::Viridis) 25 | /// .build() 26 | /// .plot(); 27 | /// ``` 28 | /// 29 | /// ![Example](https://imgur.com/hFD2A82.png) 30 | #[derive(Copy, Clone)] 31 | pub enum Coloring { 32 | Fill, 33 | HeatMap, 34 | Lines, 35 | None, 36 | } 37 | 38 | impl Coloring { 39 | pub(crate) fn to_plotly(self) -> ColoringPlotly { 40 | match self { 41 | Coloring::Fill => ColoringPlotly::Fill, 42 | Coloring::HeatMap => ColoringPlotly::HeatMap, 43 | Coloring::Lines => ColoringPlotly::Lines, 44 | Coloring::None => ColoringPlotly::None, 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod arrangement; 2 | pub(crate) mod axis; 3 | pub(crate) mod cell; 4 | pub(crate) mod color; 5 | pub(crate) mod colorbar; 6 | pub(crate) mod coloring; 7 | pub(crate) mod dimensions; 8 | pub(crate) mod direction; 9 | pub(crate) mod exponent; 10 | pub(crate) mod facet; 11 | pub(crate) mod fill; 12 | pub(crate) mod header; 13 | pub(crate) mod intensity; 14 | pub(crate) mod legend; 15 | pub(crate) mod lighting; 16 | pub(crate) mod line; 17 | pub(crate) mod mode; 18 | pub(crate) mod orientation; 19 | pub(crate) mod palette; 20 | pub(crate) mod shape; 21 | pub(crate) mod text; 22 | pub(crate) mod tick; 23 | 24 | pub(crate) use arrangement::Arrangement; 25 | pub(crate) use axis::Axis; 26 | pub(crate) use cell::Cell; 27 | pub(crate) use color::{Rgb, DEFAULT_PLOTLY_COLORS}; 28 | pub(crate) use colorbar::ColorBar; 29 | pub(crate) use coloring::Coloring; 30 | pub(crate) use dimensions::Dimensions; 31 | pub(crate) use direction::Direction; 32 | pub(crate) use exponent::ValueExponent; 33 | pub(crate) use facet::{FacetConfig, FacetScales}; 34 | pub(crate) use fill::Fill; 35 | pub(crate) use header::Header; 36 | pub(crate) use intensity::IntensityMode; 37 | pub(crate) use legend::Legend; 38 | pub(crate) use lighting::Lighting; 39 | pub(crate) use line::Line; 40 | pub(crate) use mode::Mode; 41 | pub(crate) use orientation::Orientation; 42 | pub(crate) use palette::Palette; 43 | pub(crate) use shape::Shape; 44 | pub(crate) use text::Text; 45 | pub(crate) use tick::TickDirection; 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## General 2 | 3 | # Visual Studio Code 4 | .vscode/ 5 | 6 | # Local History for Visual Studio Code 7 | .history/ 8 | 9 | # Built Visual Studio Code Extensions 10 | *.vsix 11 | 12 | # Dev container 13 | .devcontainer/ 14 | 15 | # Environment variables 16 | .env 17 | 18 | # git-cliff 19 | cliff.toml 20 | 21 | # Coverage 22 | lcov.info 23 | 24 | # markdownlint 25 | .markdownlint.json 26 | 27 | # justfile 28 | justfile 29 | 30 | # MacOS 31 | # General 32 | .DS_Store 33 | .AppleDouble 34 | .LSOverride 35 | 36 | # Icon must end with two 37 | Icon 38 | 39 | # Thumbnails 40 | ._* 41 | 42 | # Files that might appear in the root of a volume 43 | .DocumentRevisions-V100 44 | .fseventsd 45 | .Spotlight-V100 46 | .TemporaryItems 47 | .Trashes 48 | .VolumeIcon.icns 49 | .com.apple.timemachine.donotpresent 50 | 51 | # Directories potentially created on remote AFP share 52 | .AppleDB 53 | .AppleDesktop 54 | Network Trash Folder 55 | Temporary Items 56 | .apdisk 57 | 58 | 59 | ## Specific 60 | 61 | # Generated by Cargo 62 | # will have compiled files and executables 63 | debug/ 64 | target/ 65 | 66 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 67 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 68 | # Cargo.lock 69 | 70 | 71 | # These are backup files generated by rustfmt 72 | **/*.rs.bk 73 | 74 | # MSVC Windows builds of rustc generate these, which store debugging information 75 | *.pdb 76 | 77 | # Notebook 78 | notebook 79 | 80 | **/main*.rs 81 | project_docs/ -------------------------------------------------------------------------------- /src/components/orientation.rs: -------------------------------------------------------------------------------- 1 | use plotly::common::Orientation as OrientationPlotly; 2 | 3 | /// An enumeration representing the orientation of the legend. 4 | /// 5 | /// # Example 6 | /// 7 | /// ```rust 8 | /// use polars::prelude::*; 9 | /// use plotlars::{BarPlot, Legend, Orientation, Plot, Rgb}; 10 | /// 11 | /// let dataset = df![ 12 | /// "animal" => &["giraffe", "giraffe", "orangutan", "orangutan", "monkey", "monkey"], 13 | /// "gender" => &vec!["female", "male", "female", "male", "female", "male"], 14 | /// "value" => &vec![20.0f32, 25.0, 14.0, 18.0, 23.0, 31.0], 15 | /// "error" => &vec![1.0, 0.5, 1.5, 1.0, 0.5, 1.5], 16 | /// ] 17 | /// .unwrap(); 18 | /// 19 | /// let legend = Legend::new() 20 | /// .orientation(Orientation::Horizontal) 21 | /// .y(1.1) 22 | /// .x(0.3); 23 | /// 24 | /// BarPlot::builder() 25 | /// .data(&dataset) 26 | /// .labels("animal") 27 | /// .values("value") 28 | /// .orientation(Orientation::Horizontal) 29 | /// .group("gender") 30 | /// .error("error") 31 | /// .colors(vec![Rgb(255, 127, 80), Rgb(64, 224, 208)]) 32 | /// .legend(&legend) 33 | /// .build() 34 | /// .plot(); 35 | /// ``` 36 | /// 37 | /// ![Example](https://imgur.com/6kspyX7.png) 38 | #[derive(Clone)] 39 | pub enum Orientation { 40 | Horizontal, 41 | Vertical, 42 | } 43 | 44 | impl Orientation { 45 | pub(crate) fn to_plotly(&self) -> OrientationPlotly { 46 | match self { 47 | Self::Horizontal => OrientationPlotly::Horizontal, 48 | Self::Vertical => OrientationPlotly::Vertical, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/exponent.rs: -------------------------------------------------------------------------------- 1 | use plotly::common::ExponentFormat; 2 | 3 | /// An enumeration representing the format for value exponents on the axis. 4 | /// 5 | /// # Example 6 | /// 7 | /// ```rust 8 | /// use polars::prelude::*; 9 | /// use plotlars::{Axis, Plot, TimeSeriesPlot, ValueExponent}; 10 | /// 11 | /// let dataset = LazyCsvReader::new(PlPath::new("data/revenue_and_cost.csv")) 12 | /// .finish() 13 | /// .unwrap() 14 | /// .select([ 15 | /// col("Date").cast(DataType::String), 16 | /// col("Revenue").cast(DataType::Int32), 17 | /// col("Cost").cast(DataType::Int32), 18 | /// ]) 19 | /// .collect() 20 | /// .unwrap(); 21 | /// 22 | /// TimeSeriesPlot::builder() 23 | /// .data(&dataset) 24 | /// .x("Date") 25 | /// .y("Revenue") 26 | /// .y_axis( 27 | /// &Axis::new() 28 | /// .value_exponent(ValueExponent::SmallE) 29 | /// ) 30 | /// .build() 31 | /// .plot(); 32 | /// ``` 33 | /// 34 | /// ![Example](https://imgur.com/I6gYYkb.png) 35 | #[derive(Clone)] 36 | pub enum ValueExponent { 37 | None, 38 | SmallE, 39 | CapitalE, 40 | Power, 41 | SI, 42 | B, 43 | } 44 | 45 | impl ValueExponent { 46 | pub(crate) fn to_plotly(&self) -> ExponentFormat { 47 | match self { 48 | ValueExponent::None => ExponentFormat::None, 49 | ValueExponent::SmallE => ExponentFormat::SmallE, 50 | ValueExponent::CapitalE => ExponentFormat::CapitalE, 51 | ValueExponent::Power => ExponentFormat::Power, 52 | ValueExponent::SI => ExponentFormat::SI, 53 | ValueExponent::B => ExponentFormat::B, 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/tick.rs: -------------------------------------------------------------------------------- 1 | use plotly::{common::Ticks, layout::TicksDirection}; 2 | 3 | /// Enumeration representing the direction of axis ticks. 4 | /// 5 | /// # Example 6 | /// 7 | /// ```rust 8 | /// use polars::prelude::*; 9 | /// use plotlars::{Axis, Plot, ScatterPlot, TickDirection}; 10 | /// 11 | /// let x = vec![1]; 12 | /// let y = vec![1]; 13 | /// 14 | /// let dataset = DataFrame::new(vec![ 15 | /// Column::new("x".into(), x), 16 | /// Column::new("y".into(), y), 17 | /// ]).unwrap(); 18 | /// 19 | /// ScatterPlot::builder() 20 | /// .data(&dataset) 21 | /// .x("x") 22 | /// .y("y") 23 | /// .x_axis( 24 | /// &Axis::new() 25 | /// .tick_direction(TickDirection::OutSide) 26 | /// ) 27 | /// .y_axis( 28 | /// &Axis::new() 29 | /// .tick_direction(TickDirection::InSide) 30 | /// ) 31 | /// .build() 32 | /// .plot(); 33 | /// ``` 34 | /// 35 | /// ![Example](https://imgur.com/9DSwJnx.png) 36 | #[derive(Clone)] 37 | pub enum TickDirection { 38 | OutSide, 39 | InSide, 40 | None, 41 | } 42 | 43 | impl TickDirection { 44 | pub(crate) fn to_plotly_tickdirection(&self) -> TicksDirection { 45 | match self { 46 | TickDirection::OutSide => TicksDirection::Outside, 47 | TickDirection::InSide => TicksDirection::Inside, 48 | TickDirection::None => TicksDirection::Outside, 49 | } 50 | } 51 | 52 | pub(crate) fn to_plotly_ticks(&self) -> Ticks { 53 | match self { 54 | TickDirection::OutSide => Ticks::Outside, 55 | TickDirection::InSide => Ticks::Inside, 56 | TickDirection::None => Ticks::None, 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/scatterplot.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{Axis, Legend, Plot, Rgb, ScatterPlot, Shape, Text, TickDirection}; 2 | use polars::prelude::*; 3 | 4 | fn main() { 5 | let dataset = LazyCsvReader::new(PlPath::new("data/penguins.csv")) 6 | .finish() 7 | .unwrap() 8 | .select([ 9 | col("species"), 10 | col("sex").alias("gender"), 11 | col("flipper_length_mm").cast(DataType::Int16), 12 | col("body_mass_g").cast(DataType::Int16), 13 | ]) 14 | .collect() 15 | .unwrap(); 16 | 17 | let axis = Axis::new() 18 | .show_line(true) 19 | .tick_direction(TickDirection::OutSide) 20 | .value_thousands(true); 21 | 22 | ScatterPlot::builder() 23 | .data(&dataset) 24 | .x("body_mass_g") 25 | .y("flipper_length_mm") 26 | .group("species") 27 | .sort_groups_by(|a, b| { 28 | if a.len() == b.len() { 29 | a.cmp(b) 30 | } else { 31 | a.len().cmp(&b.len()) 32 | } 33 | }) 34 | .opacity(0.5) 35 | .size(12) 36 | .colors(vec![Rgb(178, 34, 34), Rgb(65, 105, 225), Rgb(255, 140, 0)]) 37 | .shapes(vec![Shape::Circle, Shape::Square, Shape::Diamond]) 38 | .plot_title(Text::from("Scatter Plot").font("Arial").size(20).x(0.065)) 39 | .x_title("body mass (g)") 40 | .y_title("flipper length (mm)") 41 | .legend_title("species") 42 | .x_axis(&axis.clone().value_range(vec![2500.0, 6500.0])) 43 | .y_axis(&axis.clone().value_range(vec![170.0, 240.0])) 44 | .legend(&Legend::new().x(0.85).y(0.15)) 45 | .build() 46 | .plot(); 47 | } 48 | -------------------------------------------------------------------------------- /examples/surfaceplot.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{ColorBar, Palette, Plot, SurfacePlot, Text}; 2 | use polars::prelude::*; 3 | use std::iter; 4 | 5 | fn main() { 6 | let n: usize = 100; 7 | let x_base: Vec = (0..n) 8 | .map(|i| { 9 | let step = (10.0 - (-10.0)) / (n - 1) as f64; 10 | -10.0 + step * i as f64 11 | }) 12 | .collect(); 13 | let y_base: Vec = (0..n) 14 | .map(|i| { 15 | let step = (10.0 - (-10.0)) / (n - 1) as f64; 16 | -10.0 + step * i as f64 17 | }) 18 | .collect(); 19 | 20 | let x = x_base 21 | .iter() 22 | .flat_map(|&xi| iter::repeat_n(xi, n)) 23 | .collect::>(); 24 | 25 | let y = y_base 26 | .iter() 27 | .cycle() 28 | .take(n * n) 29 | .cloned() 30 | .collect::>(); 31 | 32 | let z = x_base 33 | .iter() 34 | .flat_map(|i| { 35 | y_base 36 | .iter() 37 | .map(|j| 1.0 / (j * j + 5.0) * j.sin() + 1.0 / (i * i + 5.0) * i.cos()) 38 | .collect::>() 39 | }) 40 | .collect::>(); 41 | 42 | let dataset = df![ 43 | "x" => &x, 44 | "y" => &y, 45 | "z" => &z, 46 | ] 47 | .unwrap(); 48 | 49 | SurfacePlot::builder() 50 | .data(&dataset) 51 | .x("x") 52 | .y("y") 53 | .z("z") 54 | .plot_title(Text::from("Surface Plot").font("Arial").size(18)) 55 | .color_bar(&ColorBar::new().border_width(1)) 56 | .color_scale(Palette::Cividis) 57 | .reverse_scale(true) 58 | .opacity(0.5) 59 | .build() 60 | .plot(); 61 | } 62 | -------------------------------------------------------------------------------- /src/components/line.rs: -------------------------------------------------------------------------------- 1 | use plotly::common::DashType; 2 | 3 | /// An enumeration representing different styles of lines that can be used in plots. 4 | /// 5 | /// # Example 6 | /// 7 | /// ```rust 8 | /// use polars::prelude::*; 9 | /// use plotlars::{Legend, Line, Plot, Rgb, TimeSeriesPlot}; 10 | /// 11 | /// let dataset = LazyCsvReader::new(PlPath::new("data/revenue_and_cost.csv")) 12 | /// .finish() 13 | /// .unwrap() 14 | /// .select([ 15 | /// col("Date").cast(DataType::String), 16 | /// col("Revenue").cast(DataType::Int32), 17 | /// col("Cost").cast(DataType::Int32), 18 | /// ]) 19 | /// .collect() 20 | /// .unwrap(); 21 | /// 22 | /// TimeSeriesPlot::builder() 23 | /// .data(&dataset) 24 | /// .x("Date") 25 | /// .y("Revenue") 26 | /// .additional_series(vec!["Cost"]) 27 | /// .size(8) 28 | /// .colors(vec![Rgb(255, 0, 0), Rgb(0, 255, 0)]) 29 | /// .lines(vec![Line::Dash, Line::Solid]) 30 | /// .legend( 31 | /// &Legend::new() 32 | /// .x(0.05) 33 | /// .y(0.9) 34 | /// ) 35 | /// .build() 36 | /// .plot(); 37 | /// ``` 38 | /// 39 | /// ![Example](https://imgur.com/y6ZyypZ.png) 40 | #[derive(Clone, Copy)] 41 | pub enum Line { 42 | Solid, 43 | Dot, 44 | Dash, 45 | LongDash, 46 | DashDot, 47 | LongDashDot, 48 | } 49 | 50 | impl Line { 51 | #[allow(clippy::wrong_self_convention)] 52 | pub(crate) fn to_plotly(&self) -> DashType { 53 | match self { 54 | Line::Solid => DashType::Solid, 55 | Line::Dot => DashType::Dot, 56 | Line::Dash => DashType::Dash, 57 | Line::LongDash => DashType::LongDash, 58 | Line::DashDot => DashType::DashDot, 59 | Line::LongDashDot => DashType::LongDashDot, 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/lineplot.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{Axis, Line, LinePlot, Plot, Rgb, Text, TickDirection}; 2 | use polars::prelude::*; 3 | 4 | fn main() { 5 | let x_values: Vec = (0..1000) 6 | .map(|i| { 7 | let step = (2.0 * std::f64::consts::PI - 0.0) / 999.0; 8 | 0.0 + step * i as f64 9 | }) 10 | .collect(); 11 | let sine_values = x_values 12 | .iter() 13 | .map(|arg0: &f64| f64::sin(*arg0)) 14 | .collect::>(); 15 | let cosine_values = x_values 16 | .iter() 17 | .map(|arg0: &f64| f64::cos(*arg0)) 18 | .collect::>(); 19 | 20 | let dataset = df![ 21 | "x" => &x_values, 22 | "sine" => &sine_values, 23 | "cosine" => &cosine_values, 24 | ] 25 | .unwrap(); 26 | 27 | LinePlot::builder() 28 | .data(&dataset) 29 | .x("x") 30 | .y("sine") 31 | .additional_lines(vec!["cosine"]) 32 | .colors(vec![Rgb(255, 0, 0), Rgb(0, 255, 0)]) 33 | .lines(vec![Line::Solid, Line::Dot]) 34 | .width(3.0) 35 | .with_shape(false) 36 | .plot_title(Text::from("Line Plot").font("Arial").size(18)) 37 | .x_axis( 38 | &Axis::new() 39 | .tick_direction(TickDirection::OutSide) 40 | .axis_position(0.5) 41 | .tick_values(vec![ 42 | 0.5 * std::f64::consts::PI, 43 | std::f64::consts::PI, 44 | 1.5 * std::f64::consts::PI, 45 | 2.0 * std::f64::consts::PI, 46 | ]) 47 | .tick_labels(vec!["π/2", "π", "3π/2", "2π"]), 48 | ) 49 | .y_axis( 50 | &Axis::new() 51 | .tick_direction(TickDirection::OutSide) 52 | .tick_values(vec![-1.0, 0.0, 1.0]) 53 | .tick_labels(vec!["-1", "0", "1"]), 54 | ) 55 | .build() 56 | .plot(); 57 | } 58 | -------------------------------------------------------------------------------- /src/components/color.rs: -------------------------------------------------------------------------------- 1 | use plotly::color::{Color, Rgb as RgbPlotly}; 2 | 3 | use serde::Serialize; 4 | 5 | /// A structure representing an RGB color with red, green, and blue components. 6 | /// 7 | /// # Example 8 | /// 9 | /// ```rust 10 | /// use polars::prelude::*; 11 | /// use plotlars::{Axis, BarPlot, Legend, Orientation, Plot, Rgb}; 12 | /// 13 | /// let dataset = df![ 14 | /// "label" => &["", "", ""], 15 | /// "color" => &["red", "green", "blue"], 16 | /// "value" => &[1, 1, 1], 17 | /// ] 18 | /// .unwrap(); 19 | /// 20 | /// let axis = Axis::new() 21 | /// .show_axis(false); 22 | /// 23 | /// let legend = Legend::new() 24 | /// .orientation(Orientation::Horizontal) 25 | /// .x(0.3); 26 | /// 27 | /// BarPlot::builder() 28 | /// .data(&dataset) 29 | /// .labels("label") 30 | /// .values("value") 31 | /// .group("color") 32 | /// .colors(vec![ 33 | /// Rgb(255, 0, 0), 34 | /// Rgb(0, 255, 0), 35 | /// Rgb(0, 0, 255), 36 | /// ]) 37 | /// .x_axis(&axis) 38 | /// .y_axis(&axis) 39 | /// .legend(&legend) 40 | /// .build() 41 | /// .plot(); 42 | /// ``` 43 | /// ![example](https://imgur.com/HPmtj9I.png) 44 | #[derive(Debug, Default, Clone, Copy, Serialize)] 45 | pub struct Rgb( 46 | /// Red component 47 | pub u8, 48 | /// Green component 49 | pub u8, 50 | /// Blue component 51 | pub u8, 52 | ); 53 | 54 | impl Rgb { 55 | #[allow(clippy::wrong_self_convention)] 56 | pub(crate) fn to_plotly(&self) -> RgbPlotly { 57 | RgbPlotly::new(self.0, self.1, self.2) 58 | } 59 | } 60 | 61 | impl Color for Rgb {} 62 | 63 | pub(crate) const DEFAULT_PLOTLY_COLORS: [Rgb; 10] = [ 64 | Rgb(99, 110, 250), 65 | Rgb(239, 85, 59), 66 | Rgb(0, 204, 150), 67 | Rgb(171, 99, 250), 68 | Rgb(255, 161, 90), 69 | Rgb(25, 211, 243), 70 | Rgb(255, 102, 146), 71 | Rgb(182, 232, 128), 72 | Rgb(255, 151, 255), 73 | Rgb(254, 203, 82), 74 | ]; 75 | -------------------------------------------------------------------------------- /src/components/intensity.rs: -------------------------------------------------------------------------------- 1 | use plotly::traces::mesh3d::IntensityMode as PlotlyIntensityMode; 2 | 3 | /// An enumeration representing the source of intensity values for mesh coloring. 4 | /// 5 | /// The `IntensityMode` enum specifies whether intensity values should be taken 6 | /// from vertices or cells (faces) of a 3D mesh. 7 | /// 8 | /// # Example 9 | /// 10 | /// ```rust 11 | /// use plotlars::{ColorBar, IntensityMode, Mesh3D, Palette, Plot}; 12 | /// use polars::prelude::*; 13 | /// 14 | /// // Create sample mesh data with intensity values 15 | /// let x = vec![0.0, 1.0, 2.0, 0.0]; 16 | /// let y = vec![0.0, 0.0, 1.0, 2.0]; 17 | /// let z = vec![0.0, 2.0, 0.0, 1.0]; 18 | /// let intensity = vec![0.0, 0.5, 0.8, 1.0]; 19 | /// 20 | /// let dataset = DataFrame::new(vec![ 21 | /// Column::new("x".into(), x), 22 | /// Column::new("y".into(), y), 23 | /// Column::new("z".into(), z), 24 | /// Column::new("intensity".into(), intensity), 25 | /// ]) 26 | /// .unwrap(); 27 | /// 28 | /// Mesh3D::builder() 29 | /// .data(&dataset) 30 | /// .x("x") 31 | /// .y("y") 32 | /// .z("z") 33 | /// .intensity("intensity") 34 | /// .intensity_mode(IntensityMode::Vertex) 35 | /// .color_scale(Palette::Viridis) 36 | /// .color_bar( 37 | /// &ColorBar::new() 38 | /// .x(0.65) // Position color bar extremely close to the plot 39 | /// ) 40 | /// .build() 41 | /// .plot(); 42 | /// ``` 43 | /// 44 | /// ![Example](https://imgur.com/J4qIyU2.png) 45 | #[derive(Clone, Copy)] 46 | pub enum IntensityMode { 47 | /// Use intensity values from mesh vertices 48 | Vertex, 49 | /// Use intensity values from mesh cells (faces) 50 | Cell, 51 | } 52 | 53 | impl IntensityMode { 54 | #[allow(clippy::wrong_self_convention)] 55 | pub(crate) fn to_plotly(&self) -> PlotlyIntensityMode { 56 | match self { 57 | IntensityMode::Vertex => PlotlyIntensityMode::Vertex, 58 | IntensityMode::Cell => PlotlyIntensityMode::Cell, 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/common/polar.rs: -------------------------------------------------------------------------------- 1 | use polars::{ 2 | frame::DataFrame, 3 | prelude::{col, lit, DataType, IntoLazy}, 4 | }; 5 | 6 | pub(crate) trait Polar { 7 | fn get_unique_groups( 8 | data: &DataFrame, 9 | group_col: &str, 10 | sort_groups_by: Option std::cmp::Ordering>, 11 | ) -> Vec { 12 | let unique_groups = data 13 | .column(group_col) 14 | .unwrap() 15 | .unique() 16 | .unwrap() 17 | .cast(&DataType::String) 18 | .unwrap(); 19 | 20 | let mut groups: Vec = unique_groups 21 | .str() 22 | .unwrap() 23 | .iter() 24 | .map(|x| x.unwrap().to_string()) 25 | .collect(); 26 | 27 | // Sort the groups to ensure consistent ordering 28 | if let Some(sort_fn) = sort_groups_by { 29 | groups.sort_by(|a, b| sort_fn(a, b)); 30 | } else { 31 | //default sort (lexical) 32 | groups.sort(); 33 | } 34 | 35 | groups 36 | } 37 | 38 | fn filter_data_by_group(data: &DataFrame, group_col: &str, group_name: &str) -> DataFrame { 39 | data.clone() 40 | .lazy() 41 | .filter(col(group_col).cast(DataType::String).eq(lit(group_name))) 42 | .collect() 43 | .unwrap() 44 | } 45 | 46 | fn get_numeric_column(data: &DataFrame, column_name: &str) -> Vec> { 47 | data.column(column_name) 48 | .unwrap() 49 | .clone() 50 | .cast(&DataType::Float32) 51 | .unwrap() 52 | .f32() 53 | .unwrap() 54 | .to_vec() 55 | } 56 | 57 | fn get_string_column(data: &DataFrame, column_name: &str) -> Vec> { 58 | data.column(column_name) 59 | .unwrap() 60 | .clone() 61 | .cast(&DataType::String) 62 | .unwrap() 63 | .str() 64 | .unwrap() 65 | .iter() 66 | .map(|x| x.map(|s| s.to_string())) 67 | .collect::>>() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /data/financial_timeseries.csv: -------------------------------------------------------------------------------- 1 | date,region,revenue,costs 2 | 2023-01,North,100000.0,60000.0 3 | 2023-02,North,108000.0,61800.0 4 | 2023-03,North,116640.00000000001,63654.0 5 | 2023-04,North,125971.20000000001,65563.62 6 | 2023-05,North,136048.89600000004,67530.5286 7 | 2023-06,North,146932.80768000006,69556.444458 8 | 2023-07,North,158687.43229440006,71643.13779174001 9 | 2023-08,North,171382.4268779521,73792.43192549222 10 | 2023-09,North,185093.02102818826,76006.20488325699 11 | 2023-10,North,199900.46271044333,78286.3910297547 12 | 2023-11,North,215892.49972727883,80634.98276064733 13 | 2023-12,North,233163.8997054611,83054.03224346676 14 | 2023-01,South,150000.0,90000.0 15 | 2023-02,South,157500.0,92700.0 16 | 2023-03,South,165375.0,95481.0 17 | 2023-04,South,173643.75000000003,98345.43000000001 18 | 2023-05,South,182325.93750000003,101295.79290000001 19 | 2023-06,South,191442.23437500006,104334.666687 20 | 2023-07,South,201014.34609375006,107464.70668761001 21 | 2023-08,South,211065.06339843757,110688.64788823832 22 | 2023-09,South,221618.31656835944,114009.30732488548 23 | 2023-10,South,232699.23239677743,117429.58654463204 24 | 2023-11,South,244334.1940166163,120952.474140971 25 | 2023-12,South,256550.90371744713,124581.04836520013 26 | 2023-01,West,120000.0,70000.0 27 | 2023-02,West,132000.0,72100.0 28 | 2023-03,West,145200.00000000003,74263.0 29 | 2023-04,West,159720.00000000006,76490.89 30 | 2023-05,West,175692.00000000006,78785.61670000001 31 | 2023-06,West,193261.20000000007,81149.185201 32 | 2023-07,West,212587.3200000001,83583.66075703001 33 | 2023-08,West,233846.05200000014,86091.17057974091 34 | 2023-09,West,257230.6572000002,88673.90569713314 35 | 2023-10,West,282953.7229200002,91334.12286804714 36 | 2023-11,West,311249.0952120003,94074.14655408855 37 | 2023-12,West,342374.0047332003,96896.37095071122 38 | 2023-01,East,130000.0,75000.0 39 | 2023-02,East,137800.0,77250.0 40 | 2023-03,East,146068.00000000003,79567.5 41 | 2023-04,East,154832.08000000002,81954.525 42 | 2023-05,East,164122.00480000005,84413.16075000001 43 | 2023-06,East,173969.32508800004,86945.5555725 44 | 2023-07,East,184407.48459328004,89553.92223967501 45 | 2023-08,East,195471.93366887688,92240.53990686526 46 | 2023-09,East,207200.2496890095,95007.75610407123 47 | 2023-10,East,219632.26467035006,97857.98878719336 48 | 2023-11,East,232810.20055057108,100793.72845080917 49 | 2023-12,East,246778.81258360538,103817.54030433344 50 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![allow(clippy::needless_doctest_main)] 3 | 4 | mod common; 5 | mod components; 6 | mod plots; 7 | 8 | pub use crate::common::plot::Plot; 9 | pub use crate::common::plot::PlotHelper; 10 | pub use crate::components::arrangement::Arrangement; 11 | pub use crate::components::axis::{Axis, AxisSide, AxisType}; 12 | pub use crate::components::cell::Cell; 13 | pub use crate::components::color::Rgb; 14 | pub use crate::components::colorbar::ColorBar; 15 | pub use crate::components::coloring::Coloring; 16 | pub use crate::components::dimensions::Dimensions; 17 | pub use crate::components::direction::Direction; 18 | pub use crate::components::exponent::ValueExponent; 19 | pub use crate::components::facet::{FacetConfig, FacetScales}; 20 | pub use crate::components::fill::Fill; 21 | pub use crate::components::header::Header; 22 | pub use crate::components::intensity::IntensityMode; 23 | pub use crate::components::legend::Legend; 24 | pub use crate::components::lighting::Lighting; 25 | pub use crate::components::line::Line; 26 | pub use crate::components::mode::Mode; 27 | pub use crate::components::orientation::Orientation; 28 | pub use crate::components::palette::Palette; 29 | pub use crate::components::shape::Shape; 30 | pub use crate::components::text::Text; 31 | pub use crate::components::tick::TickDirection; 32 | pub use crate::plots::array2dplot::Array2dPlot; 33 | pub use crate::plots::barplot::BarPlot; 34 | pub use crate::plots::boxplot::BoxPlot; 35 | pub use crate::plots::candlestick::CandlestickPlot; 36 | pub use crate::plots::contourplot::ContourPlot; 37 | pub use crate::plots::density_mapbox::DensityMapbox; 38 | pub use crate::plots::heatmap::HeatMap; 39 | pub use crate::plots::histogram::Histogram; 40 | pub use crate::plots::image::Image; 41 | pub use crate::plots::lineplot::LinePlot; 42 | pub use crate::plots::mesh3d::Mesh3D; 43 | pub use crate::plots::ohlc::OhlcPlot; 44 | pub use crate::plots::piechart::PieChart; 45 | pub use crate::plots::sankeydiagram::SankeyDiagram; 46 | pub use crate::plots::scatter3dplot::Scatter3dPlot; 47 | pub use crate::plots::scattergeo::ScatterGeo; 48 | pub use crate::plots::scattermap::ScatterMap; 49 | pub use crate::plots::scatterplot::ScatterPlot; 50 | pub use crate::plots::scatterpolar::ScatterPolar; 51 | pub use crate::plots::subplot_grid::SubplotGrid; 52 | pub use crate::plots::surfaceplot::SurfacePlot; 53 | pub use crate::plots::table::Table; 54 | pub use crate::plots::timeseriesplot::TimeSeriesPlot; 55 | -------------------------------------------------------------------------------- /src/common/mark.rs: -------------------------------------------------------------------------------- 1 | use plotly::common::Marker as MarkerPlotly; 2 | 3 | use crate::components::{Rgb, Shape}; 4 | 5 | pub(crate) trait Marker { 6 | fn create_marker( 7 | index: usize, 8 | opacity: Option, 9 | size: Option, 10 | color: Option, 11 | colors: Option>, 12 | shape: Option, 13 | shapes: Option>, 14 | ) -> MarkerPlotly { 15 | let mut marker = MarkerPlotly::new(); 16 | marker = Self::set_opacity(marker, opacity); 17 | marker = Self::set_size(marker, size); 18 | marker = Self::set_color(marker, color, colors, index); 19 | marker = Self::set_shape(marker, shape, shapes, index); 20 | marker 21 | } 22 | 23 | fn set_opacity(mut marker: MarkerPlotly, opacity: Option) -> MarkerPlotly { 24 | if let Some(opacity) = opacity { 25 | marker = marker.opacity(opacity); 26 | } 27 | 28 | marker 29 | } 30 | 31 | fn set_size(mut marker: MarkerPlotly, size: Option) -> MarkerPlotly { 32 | if let Some(size) = size { 33 | marker = marker.size(size); 34 | } 35 | 36 | marker 37 | } 38 | 39 | fn set_color( 40 | mut marker: MarkerPlotly, 41 | color: Option, 42 | colors: Option>, 43 | index: usize, 44 | ) -> MarkerPlotly { 45 | if let Some(rgb) = color { 46 | let color = plotly::color::Rgb::new(rgb.0, rgb.1, rgb.2); 47 | 48 | marker = marker.color(color); 49 | return marker; 50 | } 51 | 52 | if let Some(colors) = colors { 53 | if let Some(rgb) = colors.get(index) { 54 | let group_color = plotly::color::Rgb::new(rgb.0, rgb.1, rgb.2); 55 | marker = marker.color(group_color); 56 | } 57 | } 58 | marker 59 | } 60 | 61 | fn set_shape( 62 | mut marker: MarkerPlotly, 63 | shape: Option, 64 | shapes: Option>, 65 | index: usize, 66 | ) -> MarkerPlotly { 67 | if let Some(shape) = shape { 68 | marker = marker.symbol(shape.to_plotly()); 69 | return marker; 70 | } 71 | 72 | if let Some(shapes) = shapes { 73 | if let Some(shape) = shapes.get(index) { 74 | marker = marker.symbol(shape.to_plotly()); 75 | } 76 | } 77 | 78 | marker 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/components/fill.rs: -------------------------------------------------------------------------------- 1 | use plotly::common::Fill as PlotlyFill; 2 | 3 | /// An enumeration representing different fill modes for area traces in plots. 4 | /// 5 | /// The `Fill` enum specifies how the area under or between traces should be filled 6 | /// in plots like scatter plots, line plots, and polar scatter plots. 7 | /// 8 | /// # Example 9 | /// 10 | /// ```rust 11 | /// use plotlars::{Fill, Mode, Plot, Rgb, ScatterPolar, Text}; 12 | /// use polars::prelude::*; 13 | /// 14 | /// let angles: Vec = (0..=360).step_by(10).map(|x| x as f64).collect(); 15 | /// let radii: Vec = angles.iter() 16 | /// .map(|&angle| 5.0 + 3.0 * (angle * std::f64::consts::PI / 180.0).sin()) 17 | /// .collect(); 18 | /// 19 | /// let dataset = DataFrame::new(vec![ 20 | /// Column::new("angle".into(), angles), 21 | /// Column::new("radius".into(), radii), 22 | /// ]) 23 | /// .unwrap(); 24 | /// 25 | /// ScatterPolar::builder() 26 | /// .data(&dataset) 27 | /// .theta("angle") 28 | /// .r("radius") 29 | /// .mode(Mode::Lines) 30 | /// .fill(Fill::ToSelf) // Fill the area enclosed by the trace 31 | /// .color(Rgb(135, 206, 250)) 32 | /// .opacity(0.6) 33 | /// .plot_title(Text::from("Filled Polar Area Chart")) 34 | /// .build() 35 | /// .plot(); 36 | /// ``` 37 | /// 38 | /// ![Example](https://imgur.com/0QAmKuS.png) 39 | #[derive(Clone, Copy)] 40 | pub enum Fill { 41 | /// Fill area from the trace to y=0 (horizontal axis) 42 | ToZeroY, 43 | /// Fill area from the trace to x=0 (vertical axis) 44 | ToZeroX, 45 | /// Fill area between this trace and the next trace along the y-direction 46 | ToNextY, 47 | /// Fill area between this trace and the next trace along the x-direction 48 | ToNextX, 49 | /// Fill the area enclosed by the trace (connecting the last point to the first) 50 | ToSelf, 51 | /// Fill area between this trace and the next trace 52 | ToNext, 53 | /// Do not fill any area 54 | None, 55 | } 56 | 57 | impl Fill { 58 | #[allow(clippy::wrong_self_convention)] 59 | pub(crate) fn to_plotly(&self) -> PlotlyFill { 60 | match self { 61 | Fill::ToZeroY => PlotlyFill::ToZeroY, 62 | Fill::ToZeroX => PlotlyFill::ToZeroX, 63 | Fill::ToNextY => PlotlyFill::ToNextY, 64 | Fill::ToNextX => PlotlyFill::ToNextX, 65 | Fill::ToSelf => PlotlyFill::ToSelf, 66 | Fill::ToNext => PlotlyFill::ToNext, 67 | Fill::None => PlotlyFill::None, 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/timeseriesplot.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{Axis, Legend, Line, Plot, Rgb, Shape, Text, TimeSeriesPlot}; 2 | use polars::prelude::*; 3 | 4 | fn main() { 5 | let revenue_dataset = LazyCsvReader::new(PlPath::new("data/revenue_and_cost.csv")) 6 | .finish() 7 | .unwrap() 8 | .select([ 9 | col("Date").cast(DataType::String), 10 | col("Revenue").cast(DataType::Int32), 11 | col("Cost").cast(DataType::Int32), 12 | ]) 13 | .collect() 14 | .unwrap(); 15 | 16 | TimeSeriesPlot::builder() 17 | .data(&revenue_dataset) 18 | .x("Date") 19 | .y("Revenue") 20 | .additional_series(vec!["Cost"]) 21 | .size(8) 22 | .colors(vec![Rgb(0, 0, 255), Rgb(255, 0, 0)]) 23 | .lines(vec![Line::Dash, Line::Solid]) 24 | .with_shape(true) 25 | .shapes(vec![Shape::Circle, Shape::Square]) 26 | .plot_title(Text::from("Time Series Plot").font("Arial").size(18)) 27 | .legend(&Legend::new().x(0.05).y(0.9)) 28 | .x_title("x") 29 | .y_title(Text::from("y").color(Rgb(0, 0, 255))) 30 | .y2_title(Text::from("y2").color(Rgb(255, 0, 0))) 31 | .y_axis( 32 | &Axis::new() 33 | .value_color(Rgb(0, 0, 255)) 34 | .show_grid(false) 35 | .zero_line_color(Rgb(0, 0, 0)), 36 | ) 37 | .y2_axis( 38 | &Axis::new() 39 | .axis_side(plotlars::AxisSide::Right) 40 | .value_color(Rgb(255, 0, 0)) 41 | .show_grid(false), 42 | ) 43 | .build() 44 | .plot(); 45 | 46 | let temperature_dataset = LazyCsvReader::new(PlPath::new("data/debilt_2023_temps.csv")) 47 | .with_has_header(true) 48 | .with_try_parse_dates(true) 49 | .finish() 50 | .unwrap() 51 | .with_columns(vec![ 52 | (col("tavg") / lit(10)).alias("tavg"), 53 | (col("tmin") / lit(10)).alias("tmin"), 54 | (col("tmax") / lit(10)).alias("tmax"), 55 | ]) 56 | .collect() 57 | .unwrap(); 58 | 59 | TimeSeriesPlot::builder() 60 | .data(&temperature_dataset) 61 | .x("date") 62 | .y("tavg") 63 | .additional_series(vec!["tmin", "tmax"]) 64 | .colors(vec![Rgb(128, 128, 128), Rgb(0, 122, 255), Rgb(255, 128, 0)]) 65 | .lines(vec![Line::Solid, Line::Dot, Line::Dot]) 66 | .plot_title("Temperature at De Bilt (2023)") 67 | .legend_title("Legend") 68 | .build() 69 | .plot(); 70 | } 71 | -------------------------------------------------------------------------------- /src/components/arrangement.rs: -------------------------------------------------------------------------------- 1 | use plotly::sankey::Arrangement as ArrangementPlotly; 2 | 3 | /// An enumeration representing node arrangement strategies for Sankey diagrams. 4 | /// 5 | /// The `Arrangement` enum controls how nodes are positioned relative to each other: 6 | /// 7 | /// * `Snap` — If value is `snap` (the default), the node arrangement is assisted by 8 | /// automatic snapping of elements to preserve space between nodes specified via `nodepad`. 9 | /// * `Perpendicular` — Nodes can only move along a line perpendicular to the primary flow. 10 | /// * `Freeform` — Nodes can freely move anywhere on the plane without automatic constraints. 11 | /// * `Fixed` — Nodes remain stationary at their specified positions and are not adjusted by the layout algorithm. 12 | /// 13 | /// # Example 14 | /// 15 | /// ```rust 16 | /// use plotlars::{Arrangement, SankeyDiagram, Orientation, Plot, Rgb, Text}; 17 | /// use polars::prelude::*; 18 | /// 19 | /// let dataset = df![ 20 | /// "source" => &["A1", "A2", "A1", "B1", "B2", "B2"], 21 | /// "target" => &["B1", "B2", "B2", "C1", "C1", "C2"], 22 | /// "value" => &[8, 4, 2, 8, 4, 2], 23 | /// ].unwrap(); 24 | /// 25 | /// SankeyDiagram::builder() 26 | /// .data(&dataset) 27 | /// .sources("source") 28 | /// .targets("target") 29 | /// .values("value") 30 | /// .orientation(Orientation::Horizontal) 31 | /// .arrangement(Arrangement::Freeform) 32 | /// .node_colors(vec![ 33 | /// Rgb(222, 235, 247), 34 | /// Rgb(198, 219, 239), 35 | /// Rgb(158, 202, 225), 36 | /// Rgb(107, 174, 214), 37 | /// Rgb( 66, 146, 198), 38 | /// Rgb( 33, 113, 181), 39 | /// ]) 40 | /// .link_colors(vec![ 41 | /// Rgb(222, 235, 247), 42 | /// Rgb(198, 219, 239), 43 | /// Rgb(158, 202, 225), 44 | /// Rgb(107, 174, 214), 45 | /// Rgb( 66, 146, 198), 46 | /// Rgb( 33, 113, 181), 47 | /// ]) 48 | /// .build() 49 | /// .plot(); 50 | /// ``` 51 | /// 52 | /// ![Example](https://imgur.com/oCvuAZB.png) 53 | #[derive(Clone)] 54 | pub enum Arrangement { 55 | Snap, 56 | Perpendicular, 57 | Freeform, 58 | Fixed, 59 | } 60 | 61 | impl Arrangement { 62 | pub(crate) fn to_plotly(&self) -> ArrangementPlotly { 63 | match self { 64 | Arrangement::Snap => ArrangementPlotly::Snap, 65 | Arrangement::Perpendicular => ArrangementPlotly::Perpendicular, 66 | Arrangement::Freeform => ArrangementPlotly::Freeform, 67 | Arrangement::Fixed => ArrangementPlotly::Fixed, 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/components/mode.rs: -------------------------------------------------------------------------------- 1 | use plotly::common::Mode as PlotlyMode; 2 | 3 | /// An enumeration representing different drawing modes for scatter-type plots. 4 | /// 5 | /// The `Mode` enum specifies how data points should be displayed in plots like 6 | /// scatter plots, line plots, and polar scatter plots. 7 | /// 8 | /// # Example 9 | /// 10 | /// ```rust 11 | /// use plotlars::{Line, Mode, Plot, Rgb, ScatterPolar, Shape, Text}; 12 | /// use polars::prelude::*; 13 | /// 14 | /// // Create sample data - radar chart style 15 | /// let categories = vec![0., 72., 144., 216., 288., 360.]; 16 | /// let performance = vec![8.0, 6.5, 7.0, 9.0, 5.5, 8.0]; 17 | /// 18 | /// let dataset = DataFrame::new(vec![ 19 | /// Column::new("category".into(), categories), 20 | /// Column::new("performance".into(), performance), 21 | /// ]) 22 | /// .unwrap(); 23 | /// 24 | /// ScatterPolar::builder() 25 | /// .data(&dataset) 26 | /// .theta("category") 27 | /// .r("performance") 28 | /// .mode(Mode::LinesMarkers) 29 | /// .color(Rgb(255, 0, 0)) 30 | /// .shape(Shape::Diamond) 31 | /// .line(Line::Solid) 32 | /// .width(3.0) 33 | /// .size(12) 34 | /// .opacity(0.8) 35 | /// .plot_title( 36 | /// Text::from("Performance Radar Chart") 37 | /// .font("Arial") 38 | /// .size(22) 39 | /// .x(0.5) 40 | /// ) 41 | /// .build() 42 | /// .plot(); 43 | /// ``` 44 | /// 45 | /// ![Example](https://imgur.com/PKDr2RJ.png) 46 | #[derive(Clone, Copy)] 47 | pub enum Mode { 48 | /// Draw only lines connecting the data points 49 | Lines, 50 | /// Draw only markers at each data point 51 | Markers, 52 | /// Draw only text labels at each data point 53 | Text, 54 | /// Draw both lines and markers 55 | LinesMarkers, 56 | /// Draw both lines and text labels 57 | LinesText, 58 | /// Draw both markers and text labels 59 | MarkersText, 60 | /// Draw lines, markers, and text labels 61 | LinesMarkersText, 62 | /// Do not draw any visual elements (useful for invisible traces) 63 | None, 64 | } 65 | 66 | impl Mode { 67 | #[allow(clippy::wrong_self_convention)] 68 | pub(crate) fn to_plotly(&self) -> PlotlyMode { 69 | match self { 70 | Mode::Lines => PlotlyMode::Lines, 71 | Mode::Markers => PlotlyMode::Markers, 72 | Mode::Text => PlotlyMode::Text, 73 | Mode::LinesMarkers => PlotlyMode::LinesMarkers, 74 | Mode::LinesText => PlotlyMode::LinesText, 75 | Mode::MarkersText => PlotlyMode::MarkersText, 76 | Mode::LinesMarkersText => PlotlyMode::LinesMarkersText, 77 | Mode::None => PlotlyMode::None, 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /examples/export_image.rs: -------------------------------------------------------------------------------- 1 | // This example requires one of the export features to be enabled: 2 | // cargo run --example export_image --features export-default 3 | // cargo run --example export_image --features export-chrome 4 | // cargo run --example export_image --features export-firefox 5 | 6 | #[cfg(any( 7 | feature = "export-chrome", 8 | feature = "export-firefox", 9 | feature = "export-default" 10 | ))] 11 | use plotlars::{Axis, BoxPlot, Legend, Orientation, Plot, Rgb, Text}; 12 | 13 | #[cfg(any( 14 | feature = "export-chrome", 15 | feature = "export-firefox", 16 | feature = "export-default" 17 | ))] 18 | use polars::prelude::*; 19 | 20 | #[cfg(any( 21 | feature = "export-chrome", 22 | feature = "export-firefox", 23 | feature = "export-default" 24 | ))] 25 | fn main() -> Result<(), Box> { 26 | // Load penguins dataset 27 | let dataset = LazyCsvReader::new(PlPath::new("data/penguins.csv")) 28 | .finish()? 29 | .select([ 30 | col("species"), 31 | col("sex").alias("gender"), 32 | col("flipper_length_mm").cast(DataType::Int16), 33 | col("body_mass_g").cast(DataType::Int16), 34 | ]) 35 | .collect()?; 36 | 37 | // Create a box plot 38 | let plot = BoxPlot::builder() 39 | .data(&dataset) 40 | .labels("species") 41 | .values("body_mass_g") 42 | .orientation(Orientation::Vertical) 43 | .group("gender") 44 | .box_points(true) 45 | .point_offset(-1.5) 46 | .jitter(0.01) 47 | .opacity(0.1) 48 | .colors(vec![Rgb(0, 191, 255), Rgb(57, 255, 20), Rgb(255, 105, 180)]) 49 | .plot_title( 50 | Text::from("Box Plot - Export Example") 51 | .font("Arial") 52 | .size(18), 53 | ) 54 | .x_title(Text::from("species").font("Arial").size(15)) 55 | .y_title(Text::from("body mass (g)").font("Arial").size(15).x(-0.04)) 56 | .legend_title(Text::from("gender").font("Arial").size(15)) 57 | .y_axis(&Axis::new().value_thousands(true)) 58 | .legend(&Legend::new().border_width(1).x(0.9)) 59 | .build(); 60 | 61 | // Export in different formats 62 | println!("Exporting PNG..."); 63 | plot.write_image("output_boxplot.png", 1200, 800, 2.0)?; 64 | 65 | println!("Exporting JPEG..."); 66 | plot.write_image("output_boxplot.jpg", 1200, 800, 1.0)?; 67 | 68 | println!("Exporting SVG..."); 69 | plot.write_image("output_boxplot.svg", 1200, 800, 1.0)?; 70 | 71 | println!("All images exported successfully!"); 72 | 73 | Ok(()) 74 | } 75 | 76 | #[cfg(not(any( 77 | feature = "export-chrome", 78 | feature = "export-firefox", 79 | feature = "export-default" 80 | )))] 81 | fn main() { 82 | eprintln!("This example requires one of the export features to be enabled."); 83 | eprintln!("Run with: cargo run --example export_image --features export-default"); 84 | } 85 | -------------------------------------------------------------------------------- /src/components/palette.rs: -------------------------------------------------------------------------------- 1 | use plotly::common::{ColorScale, ColorScalePalette}; 2 | 3 | /// 4 | /// 5 | /// # Example 6 | /// 7 | /// ```rust 8 | /// use polars::prelude::*; 9 | /// use plotlars::{ColorBar, HeatMap, Palette, Plot, Text, ValueExponent}; 10 | /// 11 | /// let dataset = LazyCsvReader::new(PlPath::new("data/heatmap.csv")) 12 | /// .finish() 13 | /// .unwrap() 14 | /// .collect() 15 | /// .unwrap(); 16 | /// 17 | /// HeatMap::builder() 18 | /// .data(&dataset) 19 | /// .x("x") 20 | /// .y("y") 21 | /// .z("z") 22 | /// .color_bar( 23 | /// &ColorBar::new() 24 | /// .length(290) 25 | /// .value_exponent(ValueExponent::None) 26 | /// .separate_thousands(true) 27 | /// .tick_length(5) 28 | /// .tick_step(2500.0) 29 | /// ) 30 | /// .color_scale(Palette::Portland) 31 | /// .build() 32 | /// .plot(); 33 | /// ``` 34 | /// 35 | /// ![Example](https://imgur.com/E9LHPAy.png) 36 | #[derive(Clone, Copy)] 37 | pub enum Palette { 38 | Greys, 39 | YlGnBu, 40 | Greens, 41 | YlOrRd, 42 | Bluered, 43 | RdBu, 44 | Reds, 45 | Blues, 46 | Picnic, 47 | Rainbow, 48 | Portland, 49 | Jet, 50 | Hot, 51 | Blackbody, 52 | Earth, 53 | Electric, 54 | Viridis, 55 | Cividis, 56 | } 57 | 58 | impl Palette { 59 | #[allow(clippy::wrong_self_convention)] 60 | pub(crate) fn to_plotly(&self) -> ColorScale { 61 | match self { 62 | Palette::Greys => ColorScale::Palette(ColorScalePalette::Greys), 63 | Palette::YlGnBu => ColorScale::Palette(ColorScalePalette::YlGnBu), 64 | Palette::Greens => ColorScale::Palette(ColorScalePalette::Greens), 65 | Palette::YlOrRd => ColorScale::Palette(ColorScalePalette::YlOrRd), 66 | Palette::Bluered => ColorScale::Palette(ColorScalePalette::Bluered), 67 | Palette::RdBu => ColorScale::Palette(ColorScalePalette::RdBu), 68 | Palette::Reds => ColorScale::Palette(ColorScalePalette::Reds), 69 | Palette::Blues => ColorScale::Palette(ColorScalePalette::Blues), 70 | Palette::Picnic => ColorScale::Palette(ColorScalePalette::Picnic), 71 | Palette::Rainbow => ColorScale::Palette(ColorScalePalette::Rainbow), 72 | Palette::Portland => ColorScale::Palette(ColorScalePalette::Portland), 73 | Palette::Jet => ColorScale::Palette(ColorScalePalette::Jet), 74 | Palette::Hot => ColorScale::Palette(ColorScalePalette::Hot), 75 | Palette::Blackbody => ColorScale::Palette(ColorScalePalette::Blackbody), 76 | Palette::Earth => ColorScale::Palette(ColorScalePalette::Earth), 77 | Palette::Electric => ColorScale::Palette(ColorScalePalette::Electric), 78 | Palette::Viridis => ColorScale::Palette(ColorScalePalette::Viridis), 79 | Palette::Cividis => ColorScale::Palette(ColorScalePalette::Cividis), 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /data/wind_patterns.csv: -------------------------------------------------------------------------------- 1 | angle,speed,season,time 2 | 0,12.0,Spring,Morning 3 | 30,16.33012701892219,Spring,Morning 4 | 60,16.330127018922195,Spring,Morning 5 | 90,12.0,Spring,Morning 6 | 120,7.669872981077808,Spring,Morning 7 | 150,7.669872981077807,Spring,Morning 8 | 180,11.999999999999998,Spring,Morning 9 | 210,16.330127018922195,Spring,Morning 10 | 240,16.330127018922195,Spring,Morning 11 | 270,12.000000000000002,Spring,Morning 12 | 300,7.669872981077806,Spring,Morning 13 | 330,7.669872981077805,Spring,Morning 14 | 360,11.999999999999998,Spring,Morning 15 | 0,18.0,Spring,Evening 16 | 30,22.33012701892219,Spring,Evening 17 | 60,22.330127018922195,Spring,Evening 18 | 90,18.0,Spring,Evening 19 | 120,13.669872981077809,Spring,Evening 20 | 150,13.669872981077807,Spring,Evening 21 | 180,18.0,Spring,Evening 22 | 210,22.330127018922195,Spring,Evening 23 | 240,22.330127018922195,Spring,Evening 24 | 270,18.000000000000004,Spring,Evening 25 | 300,13.669872981077805,Spring,Evening 26 | 330,13.669872981077805,Spring,Evening 27 | 360,17.999999999999996,Spring,Evening 28 | 0,8.0,Summer,Morning 29 | 30,12.330127018922193,Summer,Morning 30 | 60,12.330127018922195,Summer,Morning 31 | 90,8.0,Summer,Morning 32 | 120,3.669872981077808,Summer,Morning 33 | 150,3.6698729810778072,Summer,Morning 34 | 180,7.999999999999999,Summer,Morning 35 | 210,12.330127018922195,Summer,Morning 36 | 240,12.330127018922195,Summer,Morning 37 | 270,8.000000000000002,Summer,Morning 38 | 300,3.6698729810778064,Summer,Morning 39 | 330,3.6698729810778046,Summer,Morning 40 | 360,7.999999999999997,Summer,Morning 41 | 0,12.0,Summer,Evening 42 | 30,16.33012701892219,Summer,Evening 43 | 60,16.330127018922195,Summer,Evening 44 | 90,12.0,Summer,Evening 45 | 120,7.669872981077808,Summer,Evening 46 | 150,7.669872981077807,Summer,Evening 47 | 180,11.999999999999998,Summer,Evening 48 | 210,16.330127018922195,Summer,Evening 49 | 240,16.330127018922195,Summer,Evening 50 | 270,12.000000000000002,Summer,Evening 51 | 300,7.669872981077806,Summer,Evening 52 | 330,7.669872981077805,Summer,Evening 53 | 360,11.999999999999998,Summer,Evening 54 | 0,16.0,Fall,Morning 55 | 30,20.33012701892219,Fall,Morning 56 | 60,20.330127018922195,Fall,Morning 57 | 90,16.0,Fall,Morning 58 | 120,11.669872981077809,Fall,Morning 59 | 150,11.669872981077807,Fall,Morning 60 | 180,15.999999999999998,Fall,Morning 61 | 210,20.330127018922195,Fall,Morning 62 | 240,20.330127018922195,Fall,Morning 63 | 270,16.000000000000004,Fall,Morning 64 | 300,11.669872981077805,Fall,Morning 65 | 330,11.669872981077805,Fall,Morning 66 | 360,15.999999999999998,Fall,Morning 67 | 0,24.0,Fall,Evening 68 | 30,28.33012701892219,Fall,Evening 69 | 60,28.330127018922195,Fall,Evening 70 | 90,24.0,Fall,Evening 71 | 120,19.66987298107781,Fall,Evening 72 | 150,19.66987298107781,Fall,Evening 73 | 180,24.0,Fall,Evening 74 | 210,28.330127018922195,Fall,Evening 75 | 240,28.330127018922195,Fall,Evening 76 | 270,24.000000000000004,Fall,Evening 77 | 300,19.669872981077805,Fall,Evening 78 | 330,19.669872981077805,Fall,Evening 79 | 360,23.999999999999996,Fall,Evening 80 | -------------------------------------------------------------------------------- /src/plots/template.rs: -------------------------------------------------------------------------------- 1 | use bon::bon; 2 | 3 | use plotly::{ 4 | // Plot struct here, 5 | Layout as LayoutPlotly, 6 | Trace, 7 | }; 8 | 9 | use polars::frame::DataFrame; 10 | use serde::Serialize; 11 | 12 | use crate::{ 13 | common::{Layout, PlotHelper, Polar}, 14 | components::{Axis, Text}, 15 | }; 16 | 17 | #[derive(Clone, Serialize)] 18 | pub struct TemplatePlot { 19 | traces: Vec>, 20 | layout: LayoutPlotly, 21 | } 22 | 23 | #[bon] 24 | impl TemplatePlot { 25 | #[builder(on(String, into), on(Text, into))] 26 | pub fn new( 27 | data: &DataFrame, 28 | x: &str, 29 | y: &str, 30 | plot_title: Option, 31 | x_title: Option, 32 | y_title: Option, 33 | x_axis: Option<&Axis>, 34 | y_axis: Option<&Axis>, 35 | ) -> Self { 36 | let legend = None; 37 | let legend_title = None; 38 | let z_title = None; 39 | let z_axis = None; 40 | 41 | let layout = Self::create_layout( 42 | plot_title, 43 | x_title, 44 | y_title, 45 | z_title, 46 | legend_title, 47 | x_axis, 48 | y_axis, 49 | z_axis, 50 | legend, 51 | None, 52 | ); 53 | 54 | let traces = Self::create_traces(data, x, y); 55 | 56 | Self { traces, layout } 57 | } 58 | 59 | #[allow(clippy::too_many_arguments)] 60 | fn create_traces( 61 | data: &DataFrame, 62 | x: &str, 63 | y: &str, 64 | ) -> Vec> { 65 | let mut traces: Vec> = Vec::new(); 66 | 67 | let trace = Self::create_trace(data, x, y); 68 | 69 | traces.push(trace); 70 | traces 71 | } 72 | 73 | #[allow(clippy::too_many_arguments)] 74 | fn create_trace( 75 | data: &DataFrame, 76 | x: &str, 77 | y: &str, 78 | ) -> Box { 79 | let x = Self::get_string_column(data, x); 80 | let y = Self::get_numeric_column(data, y); 81 | 82 | let mut trace = Plot here::new(x, y); 83 | 84 | trace = Self::set_something(trace, something); 85 | 86 | trace 87 | } 88 | 89 | fn set_something( 90 | mut trace: Box>, 91 | something: Option<&something>, 92 | ) -> Box> 93 | where 94 | X: Serialize + Clone, 95 | Y: Serialize + Clone, 96 | Z: Serialize + Clone, 97 | { 98 | if let Some(something) = something { 99 | trace = trace.something(something.to_plotly()) 100 | } 101 | 102 | trace 103 | } 104 | } 105 | 106 | impl Layout for TemplatePlot {} 107 | impl Polar for TemplatePlot {} 108 | 109 | impl PlotHelper for TemplatePlot { 110 | fn get_layout(&self) -> &LayoutPlotly { 111 | &self.layout 112 | } 113 | 114 | fn get_traces(&self) -> &Vec> { 115 | &self.traces 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /examples/scattergeo.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{Mode, Plot, Rgb, ScatterGeo, Shape, Text}; 2 | use polars::prelude::*; 3 | 4 | fn main() { 5 | let cities = LazyCsvReader::new(PlPath::new("data/us_cities_regions.csv")) 6 | .finish() 7 | .unwrap() 8 | .select([col("city"), col("lat"), col("lon")]) 9 | .limit(5) 10 | .collect() 11 | .unwrap(); 12 | 13 | ScatterGeo::builder() 14 | .data(&cities) 15 | .lat("lat") 16 | .lon("lon") 17 | .text("city") 18 | .plot_title(Text::from("US Major Cities").font("Arial").size(20)) 19 | .build() 20 | .plot(); 21 | 22 | let cities_with_regions = LazyCsvReader::new(PlPath::new("data/us_cities_regions.csv")) 23 | .finish() 24 | .unwrap() 25 | .collect() 26 | .unwrap(); 27 | 28 | ScatterGeo::builder() 29 | .data(&cities_with_regions) 30 | .lat("lat") 31 | .lon("lon") 32 | .mode(Mode::Markers) 33 | .text("city") 34 | .group("region") 35 | .size(20) 36 | .colors(vec![ 37 | Rgb(255, 0, 0), 38 | Rgb(0, 255, 0), 39 | Rgb(0, 0, 255), 40 | Rgb(255, 165, 0), 41 | ]) 42 | .shapes(vec![ 43 | Shape::Circle, 44 | Shape::Square, 45 | Shape::Diamond, 46 | Shape::Cross, 47 | ]) 48 | .plot_title( 49 | Text::from("US Cities by Region") 50 | .font("Arial") 51 | .size(24) 52 | .x(0.5), 53 | ) 54 | .legend_title(Text::from("Region").size(14)) 55 | .build() 56 | .plot(); 57 | 58 | // Example 3: ScatterGeo with lines connecting cities (flight paths) 59 | let flight_path = LazyCsvReader::new(PlPath::new("data/flight_path.csv")) 60 | .finish() 61 | .unwrap() 62 | .collect() 63 | .unwrap(); 64 | 65 | ScatterGeo::builder() 66 | .data(&flight_path) 67 | .lat("lat") 68 | .lon("lon") 69 | .mode(Mode::LinesMarkers) 70 | .text("city") 71 | .size(15) 72 | .color(Rgb(0, 123, 255)) 73 | .line_width(2.0) 74 | .line_color(Rgb(255, 123, 0)) 75 | .opacity(0.8) 76 | .plot_title(Text::from("Flight Path: NY to LA").font("Arial").size(20)) 77 | .build() 78 | .plot(); 79 | 80 | // Example 4: World cities with custom styling 81 | let world_cities = LazyCsvReader::new(PlPath::new("data/world_cities.csv")) 82 | .finish() 83 | .unwrap() 84 | .collect() 85 | .unwrap(); 86 | 87 | ScatterGeo::builder() 88 | .data(&world_cities) 89 | .lat("lat") 90 | .lon("lon") 91 | .mode(Mode::Markers) 92 | .text("city") 93 | .group("continent") 94 | .size(25) 95 | .opacity(0.7) 96 | .colors(vec![ 97 | Rgb(255, 0, 0), 98 | Rgb(0, 255, 0), 99 | Rgb(0, 0, 255), 100 | Rgb(255, 255, 0), 101 | Rgb(255, 0, 255), 102 | Rgb(0, 255, 255), 103 | ]) 104 | .plot_title( 105 | Text::from("Major World Cities by Continent") 106 | .font("Arial") 107 | .size(24), 108 | ) 109 | .legend_title(Text::from("Continent").size(16)) 110 | .build() 111 | .plot(); 112 | } 113 | -------------------------------------------------------------------------------- /data/heatmap.csv: -------------------------------------------------------------------------------- 1 | x,y,z 2 | x1,y1,5795 3 | x1,y2,-9140 4 | x1,y3,-4610 5 | x1,y4,1964 6 | x1,y5,1284 7 | x1,y6,-3735 8 | x1,y7,6850 9 | x1,y8,-5574 10 | x1,y9,4423 11 | x1,y10,1363 12 | x2,y1,6023 13 | x2,y2,-1678 14 | x2,y3,-8315 15 | x2,y4,-9231 16 | x2,y5,-7567 17 | x2,y6,-4689 18 | x2,y7,-4949 19 | x2,y8,-3580 20 | x2,y9,7568 21 | x2,y10,9769 22 | x3,y1,-3604 23 | x3,y2,-1334 24 | x3,y3,8942 25 | x3,y4,8431 26 | x3,y5,-7253 27 | x3,y6,-9811 28 | x3,y7,9118 29 | x3,y8,-6995 30 | x3,y9,-8101 31 | x3,y10,-8733 32 | x4,y1,7912 33 | x4,y2,1394 34 | x4,y3,-6444 35 | x4,y4,-6110 36 | x4,y5,-1162 37 | x4,y6,4502 38 | x4,y7,627 39 | x4,y8,-1208 40 | x4,y9,555 41 | x4,y10,253 42 | x5,y1,-1567 43 | x5,y2,233 44 | x5,y3,1016 45 | x5,y4,-7388 46 | x5,y5,5787 47 | x5,y6,7159 48 | x5,y7,2206 49 | x5,y8,-1774 50 | x5,y9,4541 51 | x5,y10,-6848 52 | x6,y1,-8415 53 | x6,y2,-6057 54 | x6,y3,9457 55 | x6,y4,-8979 56 | x6,y5,1653 57 | x6,y6,805 58 | x6,y7,3417 59 | x6,y8,-2011 60 | x6,y9,-308 61 | x6,y10,2990 62 | x7,y1,-3127 63 | x7,y2,-4325 64 | x7,y3,-9839 65 | x7,y4,-5703 66 | x7,y5,-9005 67 | x7,y6,1534 68 | x7,y7,-2371 69 | x7,y8,-8984 70 | x7,y9,-1471 71 | x7,y10,7262 72 | x8,y1,-732 73 | x8,y2,2185 74 | x8,y3,-3669 75 | x8,y4,-1429 76 | x8,y5,-2792 77 | x8,y6,-4724 78 | x8,y7,8446 79 | x8,y8,6448 80 | x8,y9,6216 81 | x8,y10,-1994 82 | x9,y1,-7432 83 | x9,y2,-7973 84 | x9,y3,-7305 85 | x9,y4,5422 86 | x9,y5,-4742 87 | x9,y6,-3264 88 | x9,y7,-9609 89 | x9,y8,3986 90 | x9,y9,2666 91 | x9,y10,-4108 92 | x10,y1,-6439 93 | x10,y2,-3816 94 | x10,y3,9483 95 | x10,y4,-1608 96 | x10,y5,3067 97 | x10,y6,5265 98 | x10,y7,9488 99 | x10,y8,-7546 100 | x10,y9,1837 101 | x10,y10,4039 102 | x11,y1,9115 103 | x11,y2,965 104 | x11,y3,-238 105 | x11,y4,-4944 106 | x11,y5,4948 107 | x11,y6,-1890 108 | x11,y7,3773 109 | x11,y8,7412 110 | x11,y9,-9498 111 | x11,y10,-3090 112 | x12,y1,2685 113 | x12,y2,-9794 114 | x12,y3,7868 115 | x12,y4,5934 116 | x12,y5,7247 117 | x12,y6,9174 118 | x12,y7,-1245 119 | x12,y8,2383 120 | x12,y9,8141 121 | x12,y10,4820 122 | x13,y1,-2426 123 | x13,y2,-3626 124 | x13,y3,-8322 125 | x13,y4,9626 126 | x13,y5,-8941 127 | x13,y6,6198 128 | x13,y7,-86 129 | x13,y8,9541 130 | x13,y9,817 131 | x13,y10,921 132 | x14,y1,-211 133 | x14,y2,6312 134 | x14,y3,1252 135 | x14,y4,-7307 136 | x14,y5,3931 137 | x14,y6,-6373 138 | x14,y7,6157 139 | x14,y8,173 140 | x14,y9,8047 141 | x14,y10,230 142 | x15,y1,5707 143 | x15,y2,1494 144 | x15,y3,-8694 145 | x15,y4,-3224 146 | x15,y5,-526 147 | x15,y6,-2474 148 | x15,y7,-4470 149 | x15,y8,-6252 150 | x15,y9,3545 151 | x15,y10,-9337 152 | x16,y1,-8002 153 | x16,y2,-2006 154 | x16,y3,7879 155 | x16,y4,-6696 156 | x16,y5,8237 157 | x16,y6,3808 158 | x16,y7,-3415 159 | x16,y8,7675 160 | x16,y9,9965 161 | x16,y10,1649 162 | x17,y1,-8364 163 | x17,y2,7082 164 | x17,y3,-5263 165 | x17,y4,4555 166 | x17,y5,3877 167 | x17,y6,-9146 168 | x17,y7,-4145 169 | x17,y8,-2608 170 | x17,y9,3949 171 | x17,y10,8091 172 | x18,y1,-4209 173 | x18,y2,-5069 174 | x18,y3,9894 175 | x18,y4,-9798 176 | x18,y5,1447 177 | x18,y6,2688 178 | x18,y7,-5611 179 | x18,y8,-7673 180 | x18,y9,-1996 181 | x18,y10,9315 182 | x19,y1,-2223 183 | x19,y2,-9803 184 | x19,y3,-8070 185 | x19,y4,1774 186 | x19,y5,5087 187 | x19,y6,-661 188 | x19,y7,1589 189 | x19,y8,8895 190 | x19,y9,5708 191 | x19,y10,7043 192 | x20,y1,-7189 193 | x20,y2,4243 194 | x20,y3,-3454 195 | x20,y4,-8014 196 | x20,y5,-1662 197 | x20,y6,1411 198 | x20,y7,-7089 199 | x20,y8,-8266 200 | x20,y9,8227 201 | x20,y10,-1320 202 | -------------------------------------------------------------------------------- /src/components/cell.rs: -------------------------------------------------------------------------------- 1 | use plotly::common::Font; 2 | use plotly::traces::table::Cells as CellsPlotly; 3 | 4 | use crate::components::Rgb; 5 | 6 | /// A structure representing cell formatting for tables. 7 | /// 8 | /// The `Cell` struct allows customization of table cells including height, 9 | /// alignment, font, and fill color. 10 | /// 11 | /// # Example 12 | /// 13 | /// ```rust 14 | /// use plotlars::{Table, Cell, Plot, Text, Rgb}; 15 | /// use polars::prelude::*; 16 | /// 17 | /// let dataset = df![ 18 | /// "product" => &["Laptop", "Mouse", "Keyboard", "Monitor"], 19 | /// "price" => &[999.99, 29.99, 79.99, 299.99], 20 | /// "stock" => &[15, 250, 87, 42] 21 | /// ] 22 | /// .unwrap(); 23 | /// 24 | /// let cell = Cell::new() 25 | /// .height(30.0) 26 | /// .align("left") 27 | /// .font("Arial") 28 | /// .fill(Rgb(240, 240, 240)); 29 | /// 30 | /// Table::builder() 31 | /// .data(&dataset) 32 | /// .columns(vec!["product", "price", "stock"]) 33 | /// .cell(&cell) 34 | /// .plot_title(Text::from("Product Inventory")) 35 | /// .build() 36 | /// .plot(); 37 | /// ``` 38 | /// 39 | /// ![Example](https://imgur.com/FYYcWRH.png) 40 | #[derive(Clone, Default)] 41 | pub struct Cell { 42 | pub(crate) height: Option, 43 | pub(crate) align: Option, 44 | pub(crate) font: Option, 45 | pub(crate) fill: Option, 46 | } 47 | 48 | impl Cell { 49 | /// Creates a new `Cell` instance with default values. 50 | pub fn new() -> Self { 51 | Self::default() 52 | } 53 | 54 | /// Sets the height of the cells. 55 | /// 56 | /// # Argument 57 | /// 58 | /// * `height` - A `f64` value specifying the cell height. 59 | pub fn height(mut self, height: f64) -> Self { 60 | self.height = Some(height); 61 | self 62 | } 63 | 64 | /// Sets the alignment of the cell text. 65 | /// 66 | /// # Argument 67 | /// 68 | /// * `align` - A string specifying the alignment (left, center, right). 69 | pub fn align(mut self, align: impl Into) -> Self { 70 | self.align = Some(align.into()); 71 | self 72 | } 73 | 74 | /// Sets the font family of the cell text. 75 | /// 76 | /// # Argument 77 | /// 78 | /// * `font` - A string slice specifying the font family name. 79 | pub fn font(mut self, font: &str) -> Self { 80 | self.font = Some(font.to_string()); 81 | self 82 | } 83 | 84 | /// Sets the fill color of the cells. 85 | /// 86 | /// # Argument 87 | /// 88 | /// * `fill` - An `Rgb` value specifying the background color. 89 | pub fn fill(mut self, fill: Rgb) -> Self { 90 | self.fill = Some(fill); 91 | self 92 | } 93 | 94 | pub(crate) fn to_plotly(&self, values: Vec>) -> CellsPlotly 95 | where 96 | T: serde::Serialize + Clone + Default + 'static, 97 | { 98 | let mut cells = CellsPlotly::new(values); 99 | 100 | if let Some(height) = self.height { 101 | cells = cells.height(height); 102 | } 103 | 104 | if let Some(align) = &self.align { 105 | cells = cells.align(align.as_str()); 106 | } 107 | 108 | if let Some(font) = &self.font { 109 | cells = cells.font(Font::new().family(font.as_str())); 110 | } 111 | 112 | if let Some(fill) = &self.fill { 113 | cells = cells.fill(plotly::traces::table::Fill::new().color(fill.to_plotly())); 114 | } 115 | 116 | cells 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/plots/image.rs: -------------------------------------------------------------------------------- 1 | use bon::bon; 2 | 3 | use plotly::{ 4 | color::Rgb as RgbPlotly, image::ColorModel, Image as ImagePlotly, Layout as LayoutPlotly, Trace, 5 | }; 6 | 7 | use serde::Serialize; 8 | 9 | use crate::{ 10 | common::{Layout, PlotHelper}, 11 | components::{Axis, Rgb, Text}, 12 | }; 13 | 14 | /// A structure representing an image plot. 15 | /// 16 | /// The `Image` struct allows for the integration of image data into plots, enabling visualization of raster data 17 | /// or standalone images within a plotting context. It supports customizable titles, axis labels, legend configuration, 18 | /// and layout adjustments for better presentation. 19 | /// 20 | /// # Arguments 21 | /// 22 | /// * `path` - A string slice specifying the file path of the image to be displayed. 23 | /// * `plot_title` - An optional `Text` struct specifying the title of the plot. 24 | /// * `x_title` - An optional `Text` struct specifying the title of the x-axis. 25 | /// * `y_title` - An optional `Text` struct specifying the title of the y-axis. 26 | /// * `x_axis` - An optional reference to an `Axis` struct for customizing the x-axis. 27 | /// * `y_axis` - An optional reference to an `Axis` struct for customizing the y-axis. 28 | /// 29 | /// # Example 30 | /// 31 | /// ```rust 32 | /// use plotlars::{Axis, Image, Plot}; 33 | /// 34 | /// let axis = Axis::new() 35 | /// .show_axis(false); 36 | /// 37 | /// Image::builder() 38 | /// .path("data/image.png") 39 | /// .x_axis(&axis) 40 | /// .y_axis(&axis) 41 | /// .plot_title("Image Plot") 42 | /// .build() 43 | /// .plot(); 44 | /// ``` 45 | /// 46 | /// ![Example](https://imgur.com/PAtdaHj.png) 47 | #[derive(Clone, Serialize)] 48 | pub struct Image { 49 | traces: Vec>, 50 | layout: LayoutPlotly, 51 | } 52 | 53 | #[bon] 54 | impl Image { 55 | #[builder(on(String, into), on(Text, into))] 56 | pub fn new( 57 | path: &str, 58 | plot_title: Option, 59 | x_title: Option, 60 | y_title: Option, 61 | x_axis: Option<&Axis>, 62 | y_axis: Option<&Axis>, 63 | ) -> Self { 64 | let z_title = None; 65 | let legend_title = None; 66 | let z_axis = None; 67 | let legend = None; 68 | let y2_title = None; 69 | let y2_axis = None; 70 | 71 | let layout = Self::create_layout( 72 | plot_title, 73 | x_title, 74 | y_title, 75 | y2_title, 76 | z_title, 77 | legend_title, 78 | x_axis, 79 | y_axis, 80 | y2_axis, 81 | z_axis, 82 | legend, 83 | None, 84 | ); 85 | 86 | let mut traces = vec![]; 87 | 88 | let trace = Self::create_trace(path); 89 | 90 | traces.push(trace); 91 | 92 | Self { traces, layout } 93 | } 94 | 95 | fn create_trace(path: &str) -> Box { 96 | let im: image::ImageBuffer, Vec> = 97 | image::open(path).unwrap().into_rgb8(); 98 | 99 | let (width, height) = im.dimensions(); 100 | let mut pixels = vec![vec![RgbPlotly::new(0, 0, 0); width as usize]; height as usize]; 101 | 102 | for (x, y, pixel) in im.enumerate_pixels() { 103 | let rgb = Rgb(pixel[0], pixel[1], pixel[2]); 104 | pixels[y as usize][x as usize] = rgb.to_plotly(); 105 | } 106 | 107 | ImagePlotly::new(pixels).color_model(ColorModel::RGB) 108 | } 109 | } 110 | 111 | impl Layout for Image {} 112 | 113 | impl PlotHelper for Image { 114 | fn get_layout(&self) -> &LayoutPlotly { 115 | &self.layout 116 | } 117 | 118 | fn get_traces(&self) -> &Vec> { 119 | &self.traces 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/plots/array2dplot.rs: -------------------------------------------------------------------------------- 1 | use bon::bon; 2 | 3 | use plotly::{image::ColorModel, Image as ImagePlotly, Layout as LayoutPlotly, Trace}; 4 | 5 | use serde::Serialize; 6 | 7 | use crate::{ 8 | common::{Layout, PlotHelper}, 9 | components::{Axis, Rgb, Text}, 10 | }; 11 | 12 | /// A structure representing a 2D array plot. 13 | /// 14 | /// The `Array2dPlot` struct allows for visualizing 2D arrays of RGB color values as images or heatmaps. 15 | /// Each element in the 2D array corresponds to a pixel, with its color defined by an `[u8; 3]` RGB triplet. 16 | /// This struct supports customizable titles, axis labels, and axis configurations for better presentation. 17 | /// 18 | /// # Arguments 19 | /// 20 | /// * `data` - A 2D vector of RGB triplets (`&[Vec<[u8; 3]>]`) representing pixel colors for the plot. 21 | /// * `plot_title` - An optional `Text` struct specifying the title of the plot. 22 | /// * `x_title` - An optional `Text` struct specifying the title of the x-axis. 23 | /// * `y_title` - An optional `Text` struct specifying the title of the y-axis. 24 | /// * `x_axis` - An optional reference to an `Axis` struct for customizing the x-axis. 25 | /// * `y_axis` - An optional reference to an `Axis` struct for customizing the y-axis. 26 | /// 27 | /// # Example 28 | /// 29 | /// ## Basic 2D Array Plot 30 | /// 31 | /// ```rust 32 | /// use plotlars::{Array2dPlot, Plot, Text}; 33 | /// 34 | /// let data = vec![ 35 | /// vec![[255, 0, 0], [0, 255, 0], [0, 0, 255]], 36 | /// vec![[0, 0, 255], [255, 0, 0], [0, 255, 0]], 37 | /// vec![[0, 255, 0], [0, 0, 255], [255, 0, 0]], 38 | /// ]; 39 | /// 40 | /// Array2dPlot::builder() 41 | /// .data(&data) 42 | /// .plot_title( 43 | /// Text::from("Array2D Plot") 44 | /// .font("Arial") 45 | /// .size(18) 46 | /// ) 47 | /// .build() 48 | /// .plot(); 49 | /// ``` 50 | /// 51 | /// ![Example](https://imgur.com/LMrqAaT.png) 52 | #[derive(Clone, Serialize)] 53 | pub struct Array2dPlot { 54 | traces: Vec>, 55 | layout: LayoutPlotly, 56 | } 57 | 58 | #[bon] 59 | impl Array2dPlot { 60 | #[builder(on(String, into), on(Text, into))] 61 | pub fn new( 62 | data: &[Vec<[u8; 3]>], 63 | plot_title: Option, 64 | x_title: Option, 65 | y_title: Option, 66 | x_axis: Option<&Axis>, 67 | y_axis: Option<&Axis>, 68 | ) -> Self { 69 | let z_title = None; 70 | let legend_title = None; 71 | let z_axis = None; 72 | let legend = None; 73 | let y2_title = None; 74 | let y2_axis = None; 75 | 76 | let layout = Self::create_layout( 77 | plot_title, 78 | x_title, 79 | y_title, 80 | y2_title, 81 | z_title, 82 | legend_title, 83 | x_axis, 84 | y_axis, 85 | y2_axis, 86 | z_axis, 87 | legend, 88 | None, 89 | ); 90 | 91 | let mut traces = vec![]; 92 | 93 | let trace = Self::create_trace(data); 94 | 95 | traces.push(trace); 96 | 97 | Self { traces, layout } 98 | } 99 | 100 | fn create_trace(data: &[Vec<[u8; 3]>]) -> Box { 101 | let pixels = data 102 | .iter() 103 | .map(|row| { 104 | row.iter() 105 | .map(|&rgb| Rgb(rgb[0], rgb[1], rgb[2]).to_plotly()) 106 | .collect::>() 107 | }) 108 | .collect::>(); 109 | 110 | ImagePlotly::new(pixels).color_model(ColorModel::RGB) 111 | } 112 | } 113 | 114 | impl Layout for Array2dPlot {} 115 | 116 | impl PlotHelper for Array2dPlot { 117 | fn get_layout(&self) -> &LayoutPlotly { 118 | &self.layout 119 | } 120 | 121 | fn get_traces(&self) -> &Vec> { 122 | &self.traces 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/components/header.rs: -------------------------------------------------------------------------------- 1 | use plotly::common::Font; 2 | use plotly::traces::table::Header as HeaderPlotly; 3 | 4 | use crate::components::Rgb; 5 | 6 | /// A structure representing header formatting for tables. 7 | /// 8 | /// The `Header` struct allows customization of table headers including custom values, 9 | /// height, alignment, font, and fill color. 10 | /// 11 | /// # Example 12 | /// 13 | /// ```rust 14 | /// use plotlars::{Table, Header, Plot, Text, Rgb}; 15 | /// use polars::prelude::*; 16 | /// 17 | /// let dataset = df![ 18 | /// "name" => &["Alice", "Bob", "Charlie"], 19 | /// "age" => &[25, 30, 35], 20 | /// "city" => &["New York", "London", "Tokyo"] 21 | /// ] 22 | /// .unwrap(); 23 | /// 24 | /// let header = Header::new() 25 | /// .values(vec!["Full Name", "Years", "Location"]) 26 | /// .height(40.0) 27 | /// .align("center") 28 | /// .font("Arial") 29 | /// .fill(Rgb(200, 200, 200)); 30 | /// 31 | /// Table::builder() 32 | /// .data(&dataset) 33 | /// .columns(vec!["name", "age", "city"]) 34 | /// .header(&header) 35 | /// .plot_title(Text::from("Employee Information")) 36 | /// .build() 37 | /// .plot(); 38 | /// ``` 39 | /// 40 | /// ![Example](https://imgur.com/J2XhcUt.png) 41 | #[derive(Clone, Default)] 42 | pub struct Header { 43 | pub(crate) values: Option>, 44 | pub(crate) height: Option, 45 | pub(crate) align: Option, 46 | pub(crate) font: Option, 47 | pub(crate) fill: Option, 48 | } 49 | 50 | impl Header { 51 | /// Creates a new `Header` instance with default values. 52 | pub fn new() -> Self { 53 | Self::default() 54 | } 55 | 56 | /// Sets custom header values. 57 | /// 58 | /// # Argument 59 | /// 60 | /// * `values` - A vector of string slices representing custom header names. 61 | pub fn values(mut self, values: Vec<&str>) -> Self { 62 | self.values = Some(values.into_iter().map(|s| s.to_string()).collect()); 63 | self 64 | } 65 | 66 | /// Sets the height of the header. 67 | /// 68 | /// # Argument 69 | /// 70 | /// * `height` - A `f64` value specifying the header height. 71 | pub fn height(mut self, height: f64) -> Self { 72 | self.height = Some(height); 73 | self 74 | } 75 | 76 | /// Sets the alignment of the header text. 77 | /// 78 | /// # Argument 79 | /// 80 | /// * `align` - A string specifying the alignment (left, center, right). 81 | pub fn align(mut self, align: impl Into) -> Self { 82 | self.align = Some(align.into()); 83 | self 84 | } 85 | 86 | /// Sets the font family of the header text. 87 | /// 88 | /// # Argument 89 | /// 90 | /// * `font` - A string slice specifying the font family name. 91 | pub fn font(mut self, font: &str) -> Self { 92 | self.font = Some(font.to_string()); 93 | self 94 | } 95 | 96 | /// Sets the fill color of the header. 97 | /// 98 | /// # Argument 99 | /// 100 | /// * `fill` - An `Rgb` value specifying the background color. 101 | pub fn fill(mut self, fill: Rgb) -> Self { 102 | self.fill = Some(fill); 103 | self 104 | } 105 | 106 | pub(crate) fn to_plotly(&self, default_values: Vec) -> HeaderPlotly 107 | where 108 | T: serde::Serialize + Clone + Default + 'static, 109 | { 110 | let mut header = HeaderPlotly::new(default_values); 111 | 112 | if let Some(height) = self.height { 113 | header = header.height(height); 114 | } 115 | 116 | if let Some(align) = &self.align { 117 | header = header.align(align.as_str()); 118 | } 119 | 120 | if let Some(font) = &self.font { 121 | header = header.font(Font::new().family(font.as_str())); 122 | } 123 | 124 | if let Some(fill) = &self.fill { 125 | header = header.fill(plotly::traces::table::Fill::new().color(fill.to_plotly())); 126 | } 127 | 128 | header 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /examples/scatterpolar.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{Fill, Legend, Line, Mode, Plot, Rgb, ScatterPolar, Shape, Text}; 2 | use polars::prelude::*; 3 | 4 | fn main() { 5 | // Example 1: Basic scatter polar plot with markers only 6 | basic_scatter_polar(); 7 | 8 | // Example 2: Lines and markers with custom styling 9 | styled_scatter_polar(); 10 | 11 | // Example 3: Grouped data with multiple traces 12 | grouped_scatter_polar(); 13 | 14 | // Example 4: Filled area polar plot 15 | filled_scatter_polar(); 16 | } 17 | 18 | fn basic_scatter_polar() { 19 | // Create sample data - wind direction and speed 20 | let directions = vec![0., 45., 90., 135., 180., 225., 270., 315., 360.]; 21 | let speeds = vec![5.0, 7.5, 10.0, 8.5, 6.0, 4.5, 3.0, 2.5, 5.0]; 22 | 23 | let dataset = DataFrame::new(vec![ 24 | Column::new("direction".into(), directions), 25 | Column::new("speed".into(), speeds), 26 | ]) 27 | .unwrap(); 28 | 29 | ScatterPolar::builder() 30 | .data(&dataset) 31 | .theta("direction") 32 | .r("speed") 33 | .mode(Mode::Markers) 34 | .color(Rgb(65, 105, 225)) 35 | .shape(Shape::Circle) 36 | .size(10) 37 | .plot_title(Text::from("Wind Speed by Direction").font("Arial").size(20)) 38 | .build() 39 | .plot(); 40 | } 41 | 42 | fn styled_scatter_polar() { 43 | // Create sample data - radar chart style 44 | let categories = vec![0., 72., 144., 216., 288., 360.]; 45 | let performance = vec![8.0, 6.5, 7.0, 9.0, 5.5, 8.0]; 46 | 47 | let dataset = DataFrame::new(vec![ 48 | Column::new("category".into(), categories), 49 | Column::new("performance".into(), performance), 50 | ]) 51 | .unwrap(); 52 | 53 | ScatterPolar::builder() 54 | .data(&dataset) 55 | .theta("category") 56 | .r("performance") 57 | .mode(Mode::LinesMarkers) 58 | .color(Rgb(255, 0, 0)) 59 | .shape(Shape::Diamond) 60 | .line(Line::Solid) 61 | .width(3.0) 62 | .size(12) 63 | .opacity(0.8) 64 | .plot_title( 65 | Text::from("Performance Radar Chart") 66 | .font("Arial") 67 | .size(22) 68 | .x(0.5), 69 | ) 70 | .build() 71 | .plot(); 72 | } 73 | 74 | fn grouped_scatter_polar() { 75 | let dataset = LazyCsvReader::new(PlPath::new("data/product_comparison_polar.csv")) 76 | .finish() 77 | .unwrap() 78 | .collect() 79 | .unwrap(); 80 | 81 | ScatterPolar::builder() 82 | .data(&dataset) 83 | .theta("angle") 84 | .r("score") 85 | .group("product") 86 | .mode(Mode::LinesMarkers) 87 | .colors(vec![Rgb(255, 99, 71), Rgb(60, 179, 113)]) 88 | .shapes(vec![Shape::Circle, Shape::Square]) 89 | .lines(vec![Line::Solid, Line::Dash]) 90 | .width(2.5) 91 | .size(8) 92 | .plot_title(Text::from("Product Comparison").font("Arial").size(24)) 93 | .legend_title(Text::from("Products").font("Arial").size(14)) 94 | .legend(&Legend::new().x(0.85).y(0.95)) 95 | .build() 96 | .plot(); 97 | } 98 | 99 | fn filled_scatter_polar() { 100 | // Create sample data - filled area chart 101 | let angles: Vec = (0..=360).step_by(10).map(|x| x as f64).collect(); 102 | let radii: Vec = angles 103 | .iter() 104 | .map(|&angle| 5.0 + 3.0 * (angle * std::f64::consts::PI / 180.0).sin()) 105 | .collect(); 106 | 107 | let dataset = DataFrame::new(vec![ 108 | Column::new("angle".into(), angles), 109 | Column::new("radius".into(), radii), 110 | ]) 111 | .unwrap(); 112 | 113 | ScatterPolar::builder() 114 | .data(&dataset) 115 | .theta("angle") 116 | .r("radius") 117 | .mode(Mode::Lines) 118 | .fill(Fill::ToSelf) 119 | .color(Rgb(135, 206, 250)) 120 | .line(Line::Solid) 121 | .width(2.0) 122 | .opacity(0.6) 123 | .plot_title(Text::from("Filled Polar Area Chart").font("Arial").size(20)) 124 | .build() 125 | .plot(); 126 | } 127 | -------------------------------------------------------------------------------- /src/components/direction.rs: -------------------------------------------------------------------------------- 1 | use plotly::common::{Direction as DirectionPlotly, Line as LinePlotly}; 2 | 3 | use crate::components::Rgb; 4 | 5 | /// A structure representing the styling for candlestick directions (increasing/decreasing). 6 | /// 7 | /// The `Direction` struct allows customization of how candlestick lines appear when the closing price 8 | /// is higher (increasing) or lower (decreasing) than the opening price. This includes setting 9 | /// the line color and width for the candlesticks. 10 | /// 11 | /// Note: Fill color is not currently supported by the underlying plotly library. 12 | /// 13 | /// # Example 14 | /// 15 | /// ```rust 16 | /// use plotlars::{CandlestickPlot, Direction, Plot, Rgb}; 17 | /// use polars::prelude::*; 18 | /// 19 | /// let dates = vec!["2024-01-01", "2024-01-02", "2024-01-03"]; 20 | /// let open_prices = vec![100.0, 102.5, 101.0]; 21 | /// let high_prices = vec![103.0, 104.0, 103.5]; 22 | /// let low_prices = vec![99.0, 101.5, 100.0]; 23 | /// let close_prices = vec![102.5, 101.0, 103.5]; 24 | /// 25 | /// let stock_data = df! { 26 | /// "date" => dates, 27 | /// "open" => open_prices, 28 | /// "high" => high_prices, 29 | /// "low" => low_prices, 30 | /// "close" => close_prices, 31 | /// } 32 | /// .unwrap(); 33 | /// 34 | /// let increasing = Direction::new() 35 | /// .line_color(Rgb(0, 150, 255)) 36 | /// .line_width(2.0); 37 | /// 38 | /// let decreasing = Direction::new() 39 | /// .line_color(Rgb(200, 0, 100)) 40 | /// .line_width(2.0); 41 | /// 42 | /// CandlestickPlot::builder() 43 | /// .data(&stock_data) 44 | /// .dates("date") 45 | /// .open("open") 46 | /// .high("high") 47 | /// .low("low") 48 | /// .close("close") 49 | /// .increasing(&increasing) 50 | /// .decreasing(&decreasing) 51 | /// .build() 52 | /// .plot(); 53 | /// ``` 54 | /// 55 | /// ![Example](https://imgur.com/SygxOCm.png) 56 | #[derive(Clone, Default)] 57 | pub struct Direction { 58 | pub(crate) line_color: Option, 59 | pub(crate) line_width: Option, 60 | } 61 | 62 | impl Direction { 63 | /// Creates a new `Direction` instance with default settings. 64 | /// 65 | /// # Returns 66 | /// 67 | /// A new `Direction` instance with no customizations applied. 68 | pub fn new() -> Self { 69 | Self::default() 70 | } 71 | 72 | /// Sets the line color for the candlestick outline and wicks. 73 | /// 74 | /// # Arguments 75 | /// 76 | /// * `color` - An `Rgb` color for the candlestick lines. 77 | /// 78 | /// # Returns 79 | /// 80 | /// The modified `Direction` instance for method chaining. 81 | pub fn line_color(mut self, color: Rgb) -> Self { 82 | self.line_color = Some(color); 83 | self 84 | } 85 | 86 | /// Sets the line width for the candlestick outline and wicks. 87 | /// 88 | /// # Arguments 89 | /// 90 | /// * `width` - The width of the candlestick lines in pixels. 91 | /// 92 | /// # Returns 93 | /// 94 | /// The modified `Direction` instance for method chaining. 95 | pub fn line_width(mut self, width: f64) -> Self { 96 | self.line_width = Some(width); 97 | self 98 | } 99 | 100 | /// Converts the `Direction` to plotly's Direction::Increasing type. 101 | /// 102 | /// # Returns 103 | /// 104 | /// A `DirectionPlotly::Increasing` instance with the configured settings. 105 | pub(crate) fn to_plotly_increasing(&self) -> DirectionPlotly { 106 | let mut line = LinePlotly::new(); 107 | 108 | if let Some(line_color) = &self.line_color { 109 | line = line.color(line_color.to_plotly()); 110 | } 111 | 112 | if let Some(width) = self.line_width { 113 | line = line.width(width); 114 | } 115 | 116 | DirectionPlotly::Increasing { line } 117 | } 118 | 119 | /// Converts the `Direction` to plotly's Direction::Decreasing type. 120 | /// 121 | /// # Returns 122 | /// 123 | /// A `DirectionPlotly::Decreasing` instance with the configured settings. 124 | pub(crate) fn to_plotly_decreasing(&self) -> DirectionPlotly { 125 | let mut line = LinePlotly::new(); 126 | 127 | if let Some(line_color) = &self.line_color { 128 | line = line.color(line_color.to_plotly()); 129 | } 130 | 131 | if let Some(width) = self.line_width { 132 | line = line.width(width); 133 | } 134 | 135 | DirectionPlotly::Decreasing { line } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /examples/mesh3d.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{ColorBar, IntensityMode, Lighting, Mesh3D, Palette, Plot, Rgb, Text}; 2 | use polars::prelude::*; 3 | 4 | fn main() { 5 | example_basic_mesh(); 6 | example_with_indices(); 7 | example_with_intensity(); 8 | example_with_lighting(); 9 | } 10 | 11 | fn example_basic_mesh() { 12 | let x = vec![0.0, 1.0, 2.0, 0.0, 1.0, 2.0]; 13 | let y = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]; 14 | let z = vec![0.0, 0.5, 0.0, 0.0, 0.8, 0.0]; 15 | 16 | let dataset = DataFrame::new(vec![ 17 | Column::new("x".into(), x), 18 | Column::new("y".into(), y), 19 | Column::new("z".into(), z), 20 | ]) 21 | .unwrap(); 22 | 23 | Mesh3D::builder() 24 | .data(&dataset) 25 | .x("x") 26 | .y("y") 27 | .z("z") 28 | .color(Rgb(100, 150, 200)) 29 | .opacity(0.8) 30 | .plot_title("Basic Mesh3D") 31 | .build() 32 | .plot(); 33 | } 34 | 35 | fn example_with_indices() { 36 | let x = vec![0.0, 1.0, 0.5, 0.5]; 37 | let y = vec![0.0, 0.0, 0.866, 0.289]; 38 | let z = vec![0.0, 0.0, 0.0, 0.816]; 39 | let i = vec![0, 0, 0, 1]; 40 | let j = vec![1, 2, 3, 2]; 41 | let k = vec![2, 3, 1, 3]; 42 | 43 | let dataset = DataFrame::new(vec![ 44 | Column::new("x".into(), x), 45 | Column::new("y".into(), y), 46 | Column::new("z".into(), z), 47 | Column::new("i".into(), i), 48 | Column::new("j".into(), j), 49 | Column::new("k".into(), k), 50 | ]) 51 | .unwrap(); 52 | 53 | Mesh3D::builder() 54 | .data(&dataset) 55 | .x("x") 56 | .y("y") 57 | .z("z") 58 | .i("i") 59 | .j("j") 60 | .k("k") 61 | .color(Rgb(255, 100, 100)) 62 | .opacity(0.9) 63 | .flat_shading(true) 64 | .plot_title("Tetrahedron with Explicit Indices") 65 | .build() 66 | .plot(); 67 | } 68 | 69 | fn example_with_intensity() { 70 | let mut x = Vec::new(); 71 | let mut y = Vec::new(); 72 | let mut z = Vec::new(); 73 | let mut intensity = Vec::new(); 74 | 75 | for i in 0..10 { 76 | for j in 0..10 { 77 | let xi = i as f64 * 0.1; 78 | let yj = j as f64 * 0.1; 79 | x.push(xi); 80 | y.push(yj); 81 | z.push( 82 | (xi * 2.0 * std::f64::consts::PI).sin() 83 | * (yj * 2.0 * std::f64::consts::PI).cos() 84 | * 0.3, 85 | ); 86 | intensity.push(xi * yj); 87 | } 88 | } 89 | 90 | let dataset = DataFrame::new(vec![ 91 | Column::new("x".into(), x), 92 | Column::new("y".into(), y), 93 | Column::new("z".into(), z), 94 | Column::new("intensity".into(), intensity), 95 | ]) 96 | .unwrap(); 97 | 98 | Mesh3D::builder() 99 | .data(&dataset) 100 | .x("x") 101 | .y("y") 102 | .z("z") 103 | .intensity("intensity") 104 | .intensity_mode(IntensityMode::Vertex) 105 | .color_scale(Palette::Viridis) 106 | .reverse_scale(false) 107 | .show_scale(true) 108 | .color_bar(&ColorBar::new().x(0.85).title("Intensity")) 109 | .opacity(0.95) 110 | .plot_title( 111 | Text::from("Mesh3D with Intensity Coloring") 112 | .font("Arial") 113 | .size(20), 114 | ) 115 | .build() 116 | .plot(); 117 | } 118 | 119 | fn example_with_lighting() { 120 | // Create a simple wavy surface mesh without explicit indices 121 | // The mesh will be auto-triangulated 122 | let mut x = Vec::new(); 123 | let mut y = Vec::new(); 124 | let mut z = Vec::new(); 125 | 126 | let n = 20; 127 | for i in 0..n { 128 | for j in 0..n { 129 | let xi = (i as f64 / (n - 1) as f64) * 2.0 - 1.0; 130 | let yj = (j as f64 / (n - 1) as f64) * 2.0 - 1.0; 131 | x.push(xi); 132 | y.push(yj); 133 | // Create a wavy surface 134 | z.push(0.3 * ((xi * 3.0).sin() + (yj * 3.0).cos())); 135 | } 136 | } 137 | 138 | let dataset = DataFrame::new(vec![ 139 | Column::new("x".into(), x), 140 | Column::new("y".into(), y), 141 | Column::new("z".into(), z), 142 | ]) 143 | .unwrap(); 144 | 145 | Mesh3D::builder() 146 | .data(&dataset) 147 | .x("x") 148 | .y("y") 149 | .z("z") 150 | .color(Rgb(200, 200, 255)) 151 | .lighting( 152 | &Lighting::new() 153 | .ambient(0.5) 154 | .diffuse(0.8) 155 | .specular(0.5) 156 | .roughness(0.2) 157 | .fresnel(0.2), 158 | ) 159 | .light_position((1, 1, 2)) 160 | .opacity(1.0) 161 | .flat_shading(false) 162 | .contour(true) 163 | .plot_title(Text::from("Mesh 3D").font("Arial").size(22)) 164 | .build() 165 | .plot(); 166 | } 167 | -------------------------------------------------------------------------------- /examples/dimensions.rs: -------------------------------------------------------------------------------- 1 | use plotlars::{ 2 | Axis, BarPlot, BoxPlot, Dimensions, Legend, Line, Orientation, Plot, Rgb, ScatterPlot, Shape, 3 | SubplotGrid, Text, TickDirection, TimeSeriesPlot, 4 | }; 5 | use polars::prelude::*; 6 | 7 | fn main() { 8 | let penguins_dataset = LazyCsvReader::new(PlPath::new("data/penguins.csv")) 9 | .finish() 10 | .unwrap() 11 | .select([ 12 | col("species"), 13 | col("sex").alias("gender"), 14 | col("flipper_length_mm").cast(DataType::Int16), 15 | col("body_mass_g").cast(DataType::Int16), 16 | ]) 17 | .collect() 18 | .unwrap(); 19 | 20 | let temperature_dataset = LazyCsvReader::new(PlPath::new("data/debilt_2023_temps.csv")) 21 | .with_has_header(true) 22 | .with_try_parse_dates(true) 23 | .finish() 24 | .unwrap() 25 | .with_columns(vec![ 26 | (col("tavg") / lit(10)).alias("tavg"), 27 | (col("tmin") / lit(10)).alias("tmin"), 28 | (col("tmax") / lit(10)).alias("tmax"), 29 | ]) 30 | .collect() 31 | .unwrap(); 32 | 33 | let animals_dataset = LazyCsvReader::new(PlPath::new("data/animal_statistics.csv")) 34 | .finish() 35 | .unwrap() 36 | .collect() 37 | .unwrap(); 38 | 39 | let axis = Axis::new() 40 | .show_line(true) 41 | .tick_direction(TickDirection::OutSide) 42 | .value_thousands(true); 43 | 44 | let plot1 = TimeSeriesPlot::builder() 45 | .data(&temperature_dataset) 46 | .x("date") 47 | .y("tavg") 48 | .additional_series(vec!["tmin", "tmax"]) 49 | .colors(vec![Rgb(128, 128, 128), Rgb(0, 122, 255), Rgb(255, 128, 0)]) 50 | .lines(vec![Line::Solid, Line::Dot, Line::Dot]) 51 | .plot_title( 52 | Text::from("De Bilt Temperature 2023") 53 | .font("Arial Bold") 54 | .size(16), 55 | ) 56 | .y_title(Text::from("temperature (°C)").size(13).x(-0.08)) 57 | // .legend_title(Text::from("Measure").size(12)) 58 | .legend(&Legend::new().x(0.1).y(0.9)) 59 | .build(); 60 | 61 | let plot2 = ScatterPlot::builder() 62 | .data(&penguins_dataset) 63 | .x("body_mass_g") 64 | .y("flipper_length_mm") 65 | .group("species") 66 | .sort_groups_by(|a, b| { 67 | if a.len() == b.len() { 68 | a.cmp(b) 69 | } else { 70 | a.len().cmp(&b.len()) 71 | } 72 | }) 73 | .opacity(0.6) 74 | .size(10) 75 | .colors(vec![Rgb(178, 34, 34), Rgb(65, 105, 225), Rgb(255, 140, 0)]) 76 | .shapes(vec![Shape::Circle, Shape::Square, Shape::Diamond]) 77 | .plot_title(Text::from("Penguin Morphology").font("Arial Bold").size(16)) 78 | .x_title(Text::from("body mass (g)").size(13)) 79 | .y_title(Text::from("flipper length (mm)").size(13).x(-0.11)) 80 | .legend_title(Text::from("Species").size(12)) 81 | .x_axis(&axis.clone().value_range(vec![2500.0, 6500.0])) 82 | .y_axis(&axis.clone().value_range(vec![170.0, 240.0])) 83 | .legend(&Legend::new().x(0.85).y(0.4)) 84 | .build(); 85 | 86 | let plot3 = BarPlot::builder() 87 | .data(&animals_dataset) 88 | .labels("animal") 89 | .values("value") 90 | .orientation(Orientation::Vertical) 91 | .group("gender") 92 | .sort_groups_by(|a, b| a.len().cmp(&b.len())) 93 | .error("error") 94 | .colors(vec![Rgb(255, 127, 80), Rgb(64, 224, 208)]) 95 | .plot_title(Text::from("Animal Statistics").font("Arial Bold").size(16)) 96 | .x_title(Text::from("animal").size(13)) 97 | .y_title(Text::from("value").size(13)) 98 | .legend_title(Text::from("Gender").size(12)) 99 | .legend( 100 | &Legend::new() 101 | .orientation(Orientation::Horizontal) 102 | .x(0.35) 103 | .y(0.9), 104 | ) 105 | .build(); 106 | 107 | let plot4 = BoxPlot::builder() 108 | .data(&penguins_dataset) 109 | .labels("species") 110 | .values("body_mass_g") 111 | .orientation(Orientation::Vertical) 112 | .group("gender") 113 | .box_points(true) 114 | .point_offset(-1.5) 115 | .jitter(0.01) 116 | .opacity(0.15) 117 | .colors(vec![Rgb(0, 191, 255), Rgb(57, 255, 20), Rgb(255, 105, 180)]) 118 | .plot_title( 119 | Text::from("Body Mass Distribution") 120 | .font("Arial Bold") 121 | .size(16), 122 | ) 123 | .x_title(Text::from("species").size(13)) 124 | .y_title(Text::from("body mass (g)").size(13).x(-0.12)) 125 | .legend_title(Text::from("Gender").size(12)) 126 | .y_axis(&Axis::new().value_thousands(true)) 127 | .legend(&Legend::new().x(0.85).y(0.9)) 128 | .build(); 129 | 130 | let dimensions = Dimensions::new().width(1400).height(850).auto_size(false); 131 | 132 | SubplotGrid::regular() 133 | .plots(vec![&plot1, &plot2, &plot3, &plot4]) 134 | .rows(2) 135 | .cols(2) 136 | .v_gap(0.3) 137 | .h_gap(0.2) 138 | .dimensions(&dimensions) 139 | .title( 140 | Text::from("Scientific Data Visualization Dashboard") 141 | .size(26) 142 | .font("Arial Bold"), 143 | ) 144 | .build() 145 | .plot(); 146 | } 147 | -------------------------------------------------------------------------------- /src/components/lighting.rs: -------------------------------------------------------------------------------- 1 | use plotly::surface::Lighting as LightingPlotly; 2 | 3 | /// A structure describing the lighting model. 4 | /// 5 | /// # Example 6 | /// 7 | /// ```rust 8 | /// use ndarray::Array; 9 | /// use plotlars::{ColorBar, Lighting, Palette, Plot, SurfacePlot, Text}; 10 | /// use polars::prelude::*; 11 | /// use std::iter; 12 | /// 13 | /// let n: usize = 100; 14 | /// let x_base: Vec = Array::linspace(-10.0, 10.0, n).into_raw_vec(); 15 | /// let y_base: Vec = Array::linspace(-10.0, 10.0, n).into_raw_vec(); 16 | /// 17 | /// let x = x_base 18 | /// .iter() 19 | /// .flat_map(|&xi| iter::repeat(xi).take(n)) 20 | /// .collect::>(); 21 | /// 22 | /// let y = y_base 23 | /// .iter() 24 | /// .cycle() 25 | /// .take(n * n) 26 | /// .cloned() 27 | /// .collect::>(); 28 | /// 29 | /// let z = x_base 30 | /// .iter() 31 | /// .map(|i| { 32 | /// y_base 33 | /// .iter() 34 | /// .map(|j| 1.0 / (j * j + 5.0) * j.sin() + 1.0 / (i * i + 5.0) * i.cos()) 35 | /// .collect::>() 36 | /// }) 37 | /// .flatten() 38 | /// .collect::>(); 39 | /// 40 | /// let dataset = df![ 41 | /// "x" => &x, 42 | /// "y" => &y, 43 | /// "z" => &z, 44 | /// ] 45 | /// .unwrap(); 46 | /// 47 | /// SurfacePlot::builder() 48 | /// .data(&dataset) 49 | /// .x("x") 50 | /// .y("y") 51 | /// .z("z") 52 | /// .plot_title( 53 | /// Text::from("Surface Plot") 54 | /// .font("Arial") 55 | /// .size(18), 56 | /// ) 57 | /// .color_bar( 58 | /// &ColorBar::new() 59 | /// .border_width(1), 60 | /// ) 61 | /// .color_scale(Palette::Cividis) 62 | /// .reverse_scale(true) 63 | /// .lighting( 64 | /// &Lighting::new() 65 | /// .position(1, 0, 0) 66 | /// .ambient(1.0) 67 | /// .diffuse(1.0) 68 | /// .fresnel(1.0) 69 | /// .roughness(1.0) 70 | /// .specular(1.0), 71 | /// ) 72 | /// .opacity(0.5) 73 | /// .build() 74 | /// .plot(); 75 | /// ``` 76 | /// 77 | /// ![example](https://imgur.com/LEjedUE.png) 78 | #[derive(Default, Clone)] 79 | pub struct Lighting { 80 | pub(crate) position: Option<[i32; 3]>, 81 | pub(crate) ambient: Option, 82 | pub(crate) diffuse: Option, 83 | pub(crate) fresnel: Option, 84 | pub(crate) roughness: Option, 85 | pub(crate) specular: Option, 86 | } 87 | 88 | impl Lighting { 89 | /// Creates a new `Lighting` instance with default values. 90 | pub fn new() -> Self { 91 | Self::default() 92 | } 93 | 94 | /// Sets the position of the virtual light source. 95 | /// 96 | /// # Arguments 97 | /// 98 | /// * `x` – An `i32` value representing the *x*‑coordinate of the light. 99 | /// * `y` – An `i32` value representing the *y*‑coordinate of the light. 100 | /// * `z` – An `i32` value representing the *z*‑coordinate of the light (positive z points toward the viewer). 101 | pub fn position(mut self, x: i32, y: i32, z: i32) -> Self { 102 | self.position = Some([x, y, z]); 103 | self 104 | } 105 | 106 | /// Sets the ambient light component. 107 | /// 108 | /// # Arguments 109 | /// 110 | /// * `value` – A `f64` value in the range 0.0 – 1.0 specifying the uniform tint strength. 111 | pub fn ambient(mut self, value: f64) -> Self { 112 | self.ambient = Some(value); 113 | self 114 | } 115 | 116 | /// Sets the diffuse light component. 117 | /// 118 | /// # Arguments 119 | /// 120 | /// * `value` – A `f64` value in the range 0.0 – 1.0 specifying the Lambertian reflection strength. 121 | pub fn diffuse(mut self, value: f64) -> Self { 122 | self.diffuse = Some(value); 123 | self 124 | } 125 | 126 | /// Sets the Fresnel (edge brightness) component. 127 | /// 128 | /// # Arguments 129 | /// 130 | /// * `value` – A `f64` value in the range 0.0 – 1.0 specifying the rim‑light intensity. 131 | pub fn fresnel(mut self, value: f64) -> Self { 132 | self.fresnel = Some(value); 133 | self 134 | } 135 | 136 | /// Sets the roughness of the material. 137 | /// 138 | /// # Arguments 139 | /// 140 | /// * `value` – A `f64` value in the range 0.0 – 1.0 that controls highlight width (0.0 = glossy, 1.0 = matte). 141 | pub fn roughness(mut self, value: f64) -> Self { 142 | self.roughness = Some(value); 143 | self 144 | } 145 | 146 | /// Sets the specular highlight intensity. 147 | /// 148 | /// # Arguments 149 | /// 150 | /// * `value` – A `f64` value in the range 0.0 – 1.0 specifying the mirror‑like highlight strength. 151 | pub fn specular(mut self, value: f64) -> Self { 152 | self.specular = Some(value); 153 | self 154 | } 155 | 156 | pub(crate) fn set_lighting(lighting: Option<&Self>) -> LightingPlotly { 157 | let mut lighting_plotly = LightingPlotly::new(); 158 | 159 | if let Some(light) = lighting { 160 | if let Some(ambient) = light.ambient { 161 | lighting_plotly = lighting_plotly.ambient(ambient); 162 | } 163 | 164 | if let Some(diffuse) = light.diffuse { 165 | lighting_plotly = lighting_plotly.diffuse(diffuse); 166 | } 167 | 168 | if let Some(fresnel) = light.fresnel { 169 | lighting_plotly = lighting_plotly.fresnel(fresnel); 170 | } 171 | 172 | if let Some(roughness) = light.roughness { 173 | lighting_plotly = lighting_plotly.roughness(roughness); 174 | } 175 | 176 | if let Some(specular) = light.specular { 177 | lighting_plotly = lighting_plotly.specular(specular); 178 | } 179 | } 180 | 181 | lighting_plotly 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/components/legend.rs: -------------------------------------------------------------------------------- 1 | use plotly::{common::Font, layout::Legend as LegendPlotly}; 2 | 3 | use crate::{Orientation, Rgb, Text}; 4 | 5 | /// A structure representing a customizable plot legend. 6 | /// 7 | /// # Example 8 | /// 9 | /// ```rust 10 | /// use polars::prelude::*; 11 | /// use plotlars::{Histogram, Legend, Orientation, Plot, Rgb}; 12 | /// 13 | /// let dataset = LazyCsvReader::new(PlPath::new("data/penguins.csv")) 14 | /// .finish() 15 | /// .unwrap() 16 | /// .select([ 17 | /// col("species"), 18 | /// col("sex").alias("gender"), 19 | /// col("flipper_length_mm").cast(DataType::Int16), 20 | /// col("body_mass_g").cast(DataType::Int16), 21 | /// ]) 22 | /// .collect() 23 | /// .unwrap(); 24 | /// 25 | /// let legend = Legend::new() 26 | /// .orientation(Orientation::Horizontal) 27 | /// .border_width(1) 28 | /// .x(0.78) 29 | /// .y(0.825); 30 | /// 31 | /// Histogram::builder() 32 | /// .data(&dataset) 33 | /// .x("body_mass_g") 34 | /// .group("species") 35 | /// .colors(vec![ 36 | /// Rgb(255, 0, 0), 37 | /// Rgb(0, 255, 0), 38 | /// Rgb(0, 0, 255), 39 | /// ]) 40 | /// .opacity(0.5) 41 | /// .x_title("Body Mass (g)") 42 | /// .y_title("Frequency") 43 | /// .legend_title("Species") 44 | /// .legend(&legend) 45 | /// .build() 46 | /// .plot(); 47 | /// ``` 48 | /// 49 | /// ![example](https://imgur.com/GpUsgli.png) 50 | #[derive(Clone)] 51 | pub struct Legend { 52 | pub(crate) background_color: Option, 53 | pub(crate) border_color: Option, 54 | pub(crate) border_width: Option, 55 | pub(crate) font: Option, 56 | pub(crate) orientation: Option, 57 | pub(crate) x: Option, 58 | pub(crate) y: Option, 59 | } 60 | 61 | impl Default for Legend { 62 | fn default() -> Self { 63 | Self { 64 | background_color: Some(Rgb(255, 255, 255)), 65 | border_color: None, 66 | border_width: None, 67 | font: None, 68 | orientation: None, 69 | x: None, 70 | y: None, 71 | } 72 | } 73 | } 74 | 75 | impl Legend { 76 | /// Creates a new `Legend` instance with default values. 77 | pub fn new() -> Self { 78 | Self::default() 79 | } 80 | 81 | /// Sets the background color of the legend. 82 | /// 83 | /// # Argument 84 | /// 85 | /// * `color` - An `Rgb` struct representing the background color. 86 | pub fn background_color(mut self, color: Rgb) -> Self { 87 | self.background_color = Some(color); 88 | self 89 | } 90 | 91 | /// Sets the border color of the legend. 92 | /// 93 | /// # Argument 94 | /// 95 | /// * `color` - An `Rgb` struct representing the border color. 96 | pub fn border_color(mut self, color: Rgb) -> Self { 97 | self.border_color = Some(color); 98 | self 99 | } 100 | 101 | /// Sets the border width of the legend. 102 | /// 103 | /// # Argument 104 | /// 105 | /// * `width` - A `usize` value representing the width of the border. 106 | pub fn border_width(mut self, width: usize) -> Self { 107 | self.border_width = Some(width); 108 | self 109 | } 110 | 111 | /// Sets the font of the legend labels. 112 | /// 113 | /// # Argument 114 | /// 115 | /// * `font` - A value that can be converted into a `String`, representing the font name for the labels. 116 | pub fn font(mut self, font: impl Into) -> Self { 117 | self.font = Some(font.into()); 118 | self 119 | } 120 | 121 | /// Sets the orientation of the legend. 122 | /// 123 | /// # Argument 124 | /// 125 | /// * `orientation` - An `Orientation` enum value representing the layout direction of the legend. 126 | pub fn orientation(mut self, orientation: Orientation) -> Self { 127 | self.orientation = Some(orientation); 128 | self 129 | } 130 | 131 | /// Sets the horizontal position of the legend. 132 | /// 133 | /// # Argument 134 | /// 135 | /// * `x` - A `f64` value representing the horizontal position of the legend. 136 | pub fn x(mut self, x: f64) -> Self { 137 | self.x = Some(x); 138 | self 139 | } 140 | 141 | /// Sets the vertical position of the legend. 142 | /// 143 | /// # Argument 144 | /// 145 | /// * `y` - A `f64` value representing the vertical position of the legend. 146 | pub fn y(mut self, y: f64) -> Self { 147 | self.y = Some(y); 148 | self 149 | } 150 | 151 | pub(crate) fn set_legend(title: Option, format: Option<&Legend>) -> LegendPlotly { 152 | let mut legend = LegendPlotly::new(); 153 | 154 | if let Some(title) = title { 155 | legend = legend.title(title.to_plotly()); 156 | } 157 | 158 | if let Some(format) = format { 159 | legend = Self::set_format(legend, format); 160 | } 161 | 162 | legend 163 | } 164 | 165 | fn set_format(mut legend: LegendPlotly, format: &Legend) -> LegendPlotly { 166 | if let Some(color) = format.background_color { 167 | legend = legend.background_color(color.to_plotly()); 168 | } 169 | 170 | if let Some(color) = format.border_color { 171 | legend = legend.border_color(color.to_plotly()); 172 | } 173 | 174 | if let Some(width) = format.border_width { 175 | legend = legend.border_width(width); 176 | } 177 | 178 | if let Some(font) = &format.font { 179 | legend = legend.font(Font::new().family(font.as_str())); 180 | } 181 | 182 | if let Some(orientation) = &format.orientation { 183 | legend = legend.orientation(orientation.to_plotly()); 184 | } 185 | 186 | if let Some(x) = format.x { 187 | legend = legend.x(x); 188 | } 189 | 190 | if let Some(y) = format.y { 191 | legend = legend.y(y); 192 | } 193 | 194 | legend 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/plots/ohlc.rs: -------------------------------------------------------------------------------- 1 | use bon::bon; 2 | 3 | use plotly::{Layout as LayoutPlotly, Ohlc as OhlcPlotly, Trace}; 4 | 5 | use polars::frame::DataFrame; 6 | use serde::Serialize; 7 | 8 | use crate::{ 9 | common::{Layout, PlotHelper, Polar}, 10 | components::{Axis, Text}, 11 | }; 12 | 13 | /// A structure representing an OHLC (Open-High-Low-Close) financial chart. 14 | /// 15 | /// The `OhlcPlot` struct facilitates the creation and customization of OHLC charts commonly used 16 | /// for visualizing financial data such as stock prices. It supports multiple OHLC series, custom 17 | /// styling for increasing/decreasing values, hover information, and comprehensive layout customization 18 | /// including range selectors and sliders for interactive time navigation. 19 | /// 20 | /// # Arguments 21 | /// 22 | /// * `data` - A reference to the `DataFrame` containing the data to be plotted. 23 | /// * `dates` - A string slice specifying the column name for dates/timestamps. 24 | /// * `open` - A string slice specifying the column name for opening values. 25 | /// * `high` - A string slice specifying the column name for high values. 26 | /// * `low` - A string slice specifying the column name for low values. 27 | /// * `close` - A string slice specifying the column name for closing values. 28 | /// * `tick_width` - An optional `f64` specifying the width of the open/close ticks (0-1 range). 29 | /// * `plot_title` - An optional `Text` struct specifying the title of the plot. 30 | /// * `x_title` - An optional `Text` struct specifying the title of the x-axis. 31 | /// * `y_title` - An optional `Text` struct specifying the title of the y-axis. 32 | /// * `x_axis` - An optional reference to an `Axis` struct for customizing the x-axis. 33 | /// * `y_axis` - An optional reference to an `Axis` struct for customizing the y-axis. 34 | /// 35 | /// # Examples 36 | /// 37 | /// ```rust 38 | /// use plotlars::{Axis, OhlcPlot, Plot}; 39 | /// use polars::prelude::*; 40 | /// 41 | /// let stock_data = LazyCsvReader::new(PlPath::new("data/stock_prices.csv")) 42 | /// .finish() 43 | /// .unwrap() 44 | /// .collect() 45 | /// .unwrap(); 46 | /// 47 | /// OhlcPlot::builder() 48 | /// .data(&stock_data) 49 | /// .dates("date") 50 | /// .open("open") 51 | /// .high("high") 52 | /// .low("low") 53 | /// .close("close") 54 | /// .plot_title("OHLC Plot") 55 | /// .y_title("Price ($)") 56 | /// .y_axis( 57 | /// &Axis::new() 58 | /// .show_axis(true) 59 | /// ) 60 | /// .build() 61 | /// .plot(); 62 | /// ``` 63 | /// ![Exmple](https://imgur.com/Sv8r9VN.png) 64 | #[derive(Clone, Serialize)] 65 | pub struct OhlcPlot { 66 | traces: Vec>, 67 | layout: LayoutPlotly, 68 | } 69 | 70 | #[bon] 71 | impl OhlcPlot { 72 | #[builder(on(String, into), on(Text, into))] 73 | pub fn new( 74 | data: &DataFrame, 75 | dates: &str, 76 | open: &str, 77 | high: &str, 78 | low: &str, 79 | close: &str, 80 | tick_width: Option, 81 | plot_title: Option, 82 | x_title: Option, 83 | y_title: Option, 84 | x_axis: Option<&Axis>, 85 | y_axis: Option<&Axis>, 86 | ) -> Self { 87 | let z_title = None; 88 | let y_title2 = None; 89 | let z_axis = None; 90 | let y_axis2 = None; 91 | let legend_title = None; 92 | let legend = None; 93 | 94 | let layout = Self::create_layout( 95 | plot_title, 96 | x_title, 97 | y_title, 98 | y_title2, 99 | z_title, 100 | legend_title, 101 | x_axis, 102 | y_axis, 103 | y_axis2, 104 | z_axis, 105 | legend, 106 | None, 107 | ); 108 | 109 | let traces = Self::create_traces(data, dates, open, high, low, close, tick_width); 110 | 111 | Self { traces, layout } 112 | } 113 | 114 | fn create_traces( 115 | data: &DataFrame, 116 | dates_col: &str, 117 | open_col: &str, 118 | high_col: &str, 119 | low_col: &str, 120 | close_col: &str, 121 | tick_width: Option, 122 | ) -> Vec> { 123 | let mut traces: Vec> = Vec::new(); 124 | 125 | let trace = Self::create_trace( 126 | data, dates_col, open_col, high_col, low_col, close_col, tick_width, 127 | ); 128 | 129 | traces.push(trace); 130 | traces 131 | } 132 | 133 | fn create_trace( 134 | data: &DataFrame, 135 | dates_col: &str, 136 | open_col: &str, 137 | high_col: &str, 138 | low_col: &str, 139 | close_col: &str, 140 | tick_width: Option, 141 | ) -> Box { 142 | let dates_data = Self::get_string_column(data, dates_col); 143 | let open_data = Self::get_numeric_column(data, open_col); 144 | let high_data = Self::get_numeric_column(data, high_col); 145 | let low_data = Self::get_numeric_column(data, low_col); 146 | let close_data = Self::get_numeric_column(data, close_col); 147 | 148 | // Convert Option to f32 for OHLC trace 149 | let open_values: Vec = open_data.into_iter().map(|v| v.unwrap_or(0.0)).collect(); 150 | let high_values: Vec = high_data.into_iter().map(|v| v.unwrap_or(0.0)).collect(); 151 | let low_values: Vec = low_data.into_iter().map(|v| v.unwrap_or(0.0)).collect(); 152 | let close_values: Vec = close_data.into_iter().map(|v| v.unwrap_or(0.0)).collect(); 153 | let dates_values: Vec = dates_data 154 | .into_iter() 155 | .map(|v| v.unwrap_or_default()) 156 | .collect(); 157 | 158 | let mut trace = *OhlcPlotly::new( 159 | dates_values, 160 | open_values, 161 | high_values, 162 | low_values, 163 | close_values, 164 | ); 165 | 166 | // Set tick width 167 | if let Some(tick_w) = tick_width { 168 | trace = trace.tick_width(tick_w); 169 | } 170 | 171 | // Return trace as Box 172 | Box::new(trace) 173 | } 174 | } 175 | 176 | impl Layout for OhlcPlot {} 177 | impl Polar for OhlcPlot {} 178 | 179 | impl PlotHelper for OhlcPlot { 180 | fn get_layout(&self) -> &LayoutPlotly { 181 | &self.layout 182 | } 183 | 184 | fn get_traces(&self) -> &Vec> { 185 | &self.traces 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/plots/table.rs: -------------------------------------------------------------------------------- 1 | use bon::bon; 2 | 3 | use plotly::{ 4 | traces::table::{Cells as CellsPlotly, Header as HeaderPlotly}, 5 | Layout as LayoutPlotly, Table as TablePlotly, Trace, 6 | }; 7 | 8 | use polars::frame::DataFrame; 9 | use serde::Serialize; 10 | 11 | use crate::{ 12 | common::{Layout, PlotHelper, Polar}, 13 | components::{Cell, Header, Text}, 14 | }; 15 | 16 | /// A structure representing a table plot. 17 | /// 18 | /// The `Table` struct allows for the creation and customization of tables with support 19 | /// for custom headers, cell formatting, column widths, and various styling options. 20 | /// 21 | /// # Arguments 22 | /// 23 | /// * `data` - A reference to the `DataFrame` containing the data to be displayed. 24 | /// * `columns` - A vector of column names to be displayed in the table. 25 | /// * `header` - An optional `Header` component for custom header values and formatting. 26 | /// * `cell` - An optional `Cell` component for cell formatting. 27 | /// * `column_width` - An optional column width ratio. Columns fill the available width in proportion. 28 | /// * `plot_title` - An optional `Text` struct specifying the title of the plot. 29 | /// 30 | /// # Example 31 | /// 32 | /// ```rust 33 | /// use polars::prelude::*; 34 | /// use plotlars::{Table, Header, Cell, Plot, Text, Rgb}; 35 | /// 36 | /// let dataset = LazyCsvReader::new(PlPath::new("data/employee_data.csv")) 37 | /// .finish() 38 | /// .unwrap() 39 | /// .collect() 40 | /// .unwrap(); 41 | /// 42 | /// let header = Header::new() 43 | /// .values(vec![ 44 | /// "Employee Name", 45 | /// "Department", 46 | /// "Annual Salary ($)", 47 | /// "Years of Service", 48 | /// ]) 49 | /// .align("center") 50 | /// .font("Arial Bold") 51 | /// .fill(Rgb(70, 130, 180)); 52 | /// 53 | /// let cell = Cell::new() 54 | /// .align("center") 55 | /// .height(25.0) 56 | /// .font("Arial") 57 | /// .fill(Rgb(240, 248, 255)); 58 | /// 59 | /// Table::builder() 60 | /// .data(&dataset) 61 | /// .columns(vec![ 62 | /// "name", 63 | /// "department", 64 | /// "salary", 65 | /// "years", 66 | /// ]) 67 | /// .header(&header) 68 | /// .cell(&cell) 69 | /// .plot_title( 70 | /// Text::from("Table") 71 | /// .font("Arial") 72 | /// .size(20) 73 | /// .color(Rgb(25, 25, 112)) 74 | /// ) 75 | /// .build() 76 | /// .plot(); 77 | /// ``` 78 | /// 79 | /// ![Example](https://imgur.com/QDKTeFX.png) 80 | #[derive(Clone, Serialize)] 81 | pub struct Table { 82 | traces: Vec>, 83 | layout: LayoutPlotly, 84 | } 85 | 86 | #[bon] 87 | impl Table { 88 | #[builder(on(String, into), on(Text, into))] 89 | pub fn new( 90 | data: &DataFrame, 91 | columns: Vec<&str>, 92 | header: Option<&Header>, 93 | cell: Option<&Cell>, 94 | column_width: Option, 95 | plot_title: Option, 96 | ) -> Self { 97 | let x_title = None; 98 | let y_title = None; 99 | let y2_title = None; 100 | let z_title = None; 101 | let legend_title = None; 102 | let x_axis = None; 103 | let y_axis = None; 104 | let y2_axis = None; 105 | let z_axis = None; 106 | let legend = None; 107 | 108 | let layout = Self::create_layout( 109 | plot_title, 110 | x_title, 111 | y_title, 112 | y2_title, 113 | z_title, 114 | legend_title, 115 | x_axis, 116 | y_axis, 117 | y2_axis, 118 | z_axis, 119 | legend, 120 | None, 121 | ); 122 | 123 | let traces = Self::create_traces(data, &columns, header, cell, column_width); 124 | 125 | Self { traces, layout } 126 | } 127 | 128 | fn create_traces( 129 | data: &DataFrame, 130 | columns: &[&str], 131 | header: Option<&Header>, 132 | cell: Option<&Cell>, 133 | column_width: Option, 134 | ) -> Vec> { 135 | let mut traces: Vec> = Vec::new(); 136 | 137 | let trace = Self::create_trace(data, columns, header, cell, column_width); 138 | 139 | traces.push(trace); 140 | traces 141 | } 142 | 143 | fn create_trace( 144 | data: &DataFrame, 145 | columns: &[&str], 146 | header: Option<&Header>, 147 | cell: Option<&Cell>, 148 | column_width: Option, 149 | ) -> Box { 150 | // Determine header values 151 | let header_values = if let Some(h) = header { 152 | if let Some(custom_values) = &h.values { 153 | custom_values.clone() 154 | } else { 155 | columns.iter().map(|&c| c.to_string()).collect() 156 | } 157 | } else { 158 | columns.iter().map(|&c| c.to_string()).collect() 159 | }; 160 | 161 | // Extract cell values from DataFrame 162 | let mut cell_values: Vec> = Vec::new(); 163 | 164 | for column_name in columns { 165 | let column_data = Self::get_string_column(data, column_name); 166 | let column_strings: Vec = column_data 167 | .iter() 168 | .map(|opt| opt.clone().unwrap_or_default()) 169 | .collect(); 170 | cell_values.push(column_strings); 171 | } 172 | 173 | // Create header 174 | let plotly_header = if let Some(h) = header { 175 | h.to_plotly(header_values) 176 | } else { 177 | HeaderPlotly::new(header_values) 178 | }; 179 | 180 | // Create cells 181 | let plotly_cells = if let Some(c) = cell { 182 | c.to_plotly(cell_values) 183 | } else { 184 | CellsPlotly::new(cell_values) 185 | }; 186 | 187 | // Create table 188 | let mut table = TablePlotly::new(plotly_header, plotly_cells); 189 | 190 | // Set column width if provided 191 | if let Some(width) = column_width { 192 | table = table.column_width(width); 193 | } 194 | 195 | table 196 | } 197 | } 198 | 199 | impl Layout for Table {} 200 | impl Polar for Table {} 201 | 202 | impl PlotHelper for Table { 203 | fn get_layout(&self) -> &LayoutPlotly { 204 | &self.layout 205 | } 206 | 207 | fn get_traces(&self) -> &Vec> { 208 | &self.traces 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/components/dimensions.rs: -------------------------------------------------------------------------------- 1 | /// A structure representing plot dimensions and sizing behavior. 2 | /// 3 | /// The `Dimensions` struct allows customization of plot size including width, height, 4 | /// and auto-sizing behavior. It is particularly useful when creating subplot grids or 5 | /// when you need precise control over plot dimensions. 6 | /// 7 | /// # Example 8 | /// 9 | /// ```rust 10 | /// use plotlars::{ 11 | /// Axis, BarPlot, BoxPlot, Dimensions, Legend, Line, Orientation, Plot, Rgb, ScatterPlot, Shape, 12 | /// SubplotGrid, Text, TickDirection, TimeSeriesPlot, 13 | /// }; 14 | /// use polars::prelude::*; 15 | /// 16 | /// let penguins_dataset = LazyCsvReader::new(PlPath::new("data/penguins.csv")) 17 | /// .finish() 18 | /// .unwrap() 19 | /// .select([ 20 | /// col("species"), 21 | /// col("sex").alias("gender"), 22 | /// col("flipper_length_mm").cast(DataType::Int16), 23 | /// col("body_mass_g").cast(DataType::Int16), 24 | /// ]) 25 | /// .collect() 26 | /// .unwrap(); 27 | /// 28 | /// let temperature_dataset = LazyCsvReader::new(PlPath::new("data/debilt_2023_temps.csv")) 29 | /// .with_has_header(true) 30 | /// .with_try_parse_dates(true) 31 | /// .finish() 32 | /// .unwrap() 33 | /// .with_columns(vec![ 34 | /// (col("tavg") / lit(10)).alias("tavg"), 35 | /// (col("tmin") / lit(10)).alias("tmin"), 36 | /// (col("tmax") / lit(10)).alias("tmax"), 37 | /// ]) 38 | /// .collect() 39 | /// .unwrap(); 40 | /// 41 | /// let animals_dataset = LazyCsvReader::new(PlPath::new("data/animal_statistics.csv")) 42 | /// .finish() 43 | /// .unwrap() 44 | /// .collect() 45 | /// .unwrap(); 46 | /// 47 | /// let axis = Axis::new() 48 | /// .show_line(true) 49 | /// .tick_direction(TickDirection::OutSide) 50 | /// .value_thousands(true); 51 | /// 52 | /// let plot1 = TimeSeriesPlot::builder() 53 | /// .data(&temperature_dataset) 54 | /// .x("date") 55 | /// .y("tavg") 56 | /// .additional_series(vec!["tmin", "tmax"]) 57 | /// .colors(vec![Rgb(128, 128, 128), Rgb(0, 122, 255), Rgb(255, 128, 0)]) 58 | /// .lines(vec![Line::Solid, Line::Dot, Line::Dot]) 59 | /// .plot_title( 60 | /// Text::from("De Bilt Temperature 2023") 61 | /// .font("Arial Bold") 62 | /// .size(16), 63 | /// ) 64 | /// .y_title(Text::from("temperature (°C)").size(13).x(-0.08)) 65 | /// .legend(&Legend::new().x(0.1).y(0.9)) 66 | /// .build(); 67 | /// 68 | /// let plot2 = ScatterPlot::builder() 69 | /// .data(&penguins_dataset) 70 | /// .x("body_mass_g") 71 | /// .y("flipper_length_mm") 72 | /// .group("species") 73 | /// .sort_groups_by(|a, b| { 74 | /// if a.len() == b.len() { 75 | /// a.cmp(b) 76 | /// } else { 77 | /// a.len().cmp(&b.len()) 78 | /// } 79 | /// }) 80 | /// .opacity(0.6) 81 | /// .size(10) 82 | /// .colors(vec![Rgb(178, 34, 34), Rgb(65, 105, 225), Rgb(255, 140, 0)]) 83 | /// .shapes(vec![Shape::Circle, Shape::Square, Shape::Diamond]) 84 | /// .plot_title(Text::from("Penguin Morphology").font("Arial Bold").size(16)) 85 | /// .x_title(Text::from("body mass (g)").size(13)) 86 | /// .y_title(Text::from("flipper length (mm)").size(13).x(-0.11)) 87 | /// .legend_title(Text::from("Species").size(12)) 88 | /// .x_axis(&axis.clone().value_range(vec![2500.0, 6500.0])) 89 | /// .y_axis(&axis.clone().value_range(vec![170.0, 240.0])) 90 | /// .legend(&Legend::new().x(0.85).y(0.4)) 91 | /// .build(); 92 | /// 93 | /// let plot3 = BarPlot::builder() 94 | /// .data(&animals_dataset) 95 | /// .labels("animal") 96 | /// .values("value") 97 | /// .orientation(Orientation::Vertical) 98 | /// .group("gender") 99 | /// .sort_groups_by(|a, b| a.len().cmp(&b.len())) 100 | /// .error("error") 101 | /// .colors(vec![Rgb(255, 127, 80), Rgb(64, 224, 208)]) 102 | /// .plot_title(Text::from("Animal Statistics").font("Arial Bold").size(16)) 103 | /// .x_title(Text::from("animal").size(13)) 104 | /// .y_title(Text::from("value").size(13)) 105 | /// .legend_title(Text::from("Gender").size(12)) 106 | /// .legend( 107 | /// &Legend::new() 108 | /// .orientation(Orientation::Horizontal) 109 | /// .x(0.35) 110 | /// .y(0.9), 111 | /// ) 112 | /// .build(); 113 | /// 114 | /// let plot4 = BoxPlot::builder() 115 | /// .data(&penguins_dataset) 116 | /// .labels("species") 117 | /// .values("body_mass_g") 118 | /// .orientation(Orientation::Vertical) 119 | /// .group("gender") 120 | /// .box_points(true) 121 | /// .point_offset(-1.5) 122 | /// .jitter(0.01) 123 | /// .opacity(0.15) 124 | /// .colors(vec![Rgb(0, 191, 255), Rgb(57, 255, 20), Rgb(255, 105, 180)]) 125 | /// .plot_title( 126 | /// Text::from("Body Mass Distribution") 127 | /// .font("Arial Bold") 128 | /// .size(16), 129 | /// ) 130 | /// .x_title(Text::from("species").size(13)) 131 | /// .y_title(Text::from("body mass (g)").size(13).x(-0.12)) 132 | /// .legend_title(Text::from("Gender").size(12)) 133 | /// .y_axis(&Axis::new().value_thousands(true)) 134 | /// .legend(&Legend::new().x(0.85).y(0.9)) 135 | /// .build(); 136 | /// 137 | /// let dimensions = Dimensions::new().width(1400).height(850).auto_size(false); 138 | /// 139 | /// SubplotGrid::regular() 140 | /// .plots(vec![&plot1, &plot2, &plot3, &plot4]) 141 | /// .rows(2) 142 | /// .cols(2) 143 | /// .v_gap(0.3) 144 | /// .h_gap(0.2) 145 | /// .dimensions(&dimensions) 146 | /// .title( 147 | /// Text::from("Scientific Data Visualization Dashboard") 148 | /// .size(26) 149 | /// .font("Arial Bold"), 150 | /// ) 151 | /// .build() 152 | /// .plot(); 153 | /// ``` 154 | /// 155 | /// ![Example](https://imgur.com/hxwInxB.png) 156 | #[derive(Clone, Default)] 157 | pub struct Dimensions { 158 | pub(crate) width: Option, 159 | pub(crate) height: Option, 160 | pub(crate) auto_size: Option, 161 | } 162 | 163 | impl Dimensions { 164 | /// Creates a new `Dimensions` instance with default values. 165 | pub fn new() -> Self { 166 | Self::default() 167 | } 168 | 169 | /// Sets the width of the plot in pixels. 170 | pub fn width(mut self, width: usize) -> Self { 171 | self.width = Some(width); 172 | self 173 | } 174 | 175 | /// Sets the height of the plot in pixels. 176 | pub fn height(mut self, height: usize) -> Self { 177 | self.height = Some(height); 178 | self 179 | } 180 | 181 | /// Sets whether the plot should automatically resize. 182 | pub fn auto_size(mut self, auto_size: bool) -> Self { 183 | self.auto_size = Some(auto_size); 184 | self 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.11.4] - 2025-12-02 6 | 7 | ### 🚀 Features 8 | 9 | - Support mixed subplot grids combining different plot types (2D, 3D, polar, geo, mapbox, domain-based) 10 | - Auto-generate legends for subplot grids when not explicitly configured 11 | 12 | ## [0.11.3] - 2025-12-02 13 | 14 | ### 🐛 Bug Fixes 15 | 16 | - Fix colorbar positioning in subplot grids to avoid overlapping charts 17 | - Fix colorbar height scaling for traces without explicit colorbar configuration 18 | 19 | ## [0.11.2] - 2025-11-23 20 | 21 | ### ⚠️ BREAKING CHANGES 22 | 23 | - Remove `ndarray` dependency 24 | - No longer required as a direct dependency 25 | - Users relying on ndarray integration should add it to their own Cargo.toml 26 | - Remove `plotly_static` dependency 27 | - Export features now use plotly's built-in static export capabilities 28 | - Rename export features for consistency 29 | - `static_export_chromedriver` → `export-chrome` 30 | - `static_export_geckodriver` → `export-firefox` 31 | - `static_export_wd_download` → `export-download` 32 | - `static_export_default` → `export-default` 33 | 34 | ## [0.11.0] - 2025-11-15 35 | 36 | ### 🚀 Features 37 | 38 | - Dimensions: New component for controlling plot sizing with width, height, and auto_size parameters 39 | - Integrated into all plot types and SubplotGrid 40 | - Enables precise control over plot dimensions, particularly useful for multi-plot layouts 41 | - SubplotGrid: Create multi-plot grid layouts with automatic positioning and configurable spacing (cartesian 2D plots only) 42 | - Regular grids with automatic plot arrangement 43 | - Irregular grids with custom row/column spanning support 44 | - Faceting: Split data by categorical variables across 14 plot types (BarPlot, BoxPlot, ContourPlot, HeatMap, Histogram, LinePlot, Mesh3D, PieChart, SankeyDiagram, Scatter3dPlot, ScatterPlot, ScatterPolar, SurfacePlot, TimeSeriesPlot) 45 | - Custom Axis Title Positioning: Precisely position axis titles anywhere on the plot 46 | 47 | ### ⚠️ BREAKING CHANGES 48 | 49 | - ColorBar `length()` and `width()` now accept fractions (0.0-1.0) instead of pixels 50 | 51 | ### 🐛 Bug Fixes 52 | 53 | - Fix HeatMap colorbar length and width not being applied to plots 54 | - Fix colorbar extending beyond subplot boundaries in irregular SubplotGrid 55 | - Fix custom legend border rendering without explicit border color 56 | 57 | ## [0.10.0] - 2025-08-07 58 | 59 | ### 🚀 Features 60 | 61 | - Add CandlestickPlot with Direction styling 62 | - Add DensityMapbox plot 63 | - Add Mesh3D plot implementation 64 | - Add OHLC plot for financial data visualization 65 | - Add ScatterGeo plot with geographic visualization support 66 | - Add ScatterPolar plot implementation 67 | - Add Table plot with Header and Cell components 68 | 69 | ## [0.9.7] - 2025-08-04 70 | 71 | ### 🐛 Bug Fixes 72 | 73 | - Update dependencies and fix Polars 0.50.0 compatibility 74 | 75 | ## [0.9.6] - 2025-08-04 76 | 77 | ### 🚀 Features 78 | 79 | - Re-enable write_image with plotly 0.13 (incl features) 80 | 81 | ### 🐛 Bug Fixes 82 | 83 | - Fix incorrect import in the README example (Text unused; Rgb missing) 84 | 85 | ## [0.9.5] - 2025-07-05 86 | 87 | ### 📚 Documentation 88 | 89 | - Update dependencies to latest versions 90 | 91 | ## [0.9.4] - 2025-05-29 92 | 93 | ### 🐛 Bug Fixes 94 | 95 | - Update lineplot.rs to use Column instead of Series 96 | - Update polars version 97 | 98 | ### 📚 Documentation 99 | 100 | - Add dependency on plotters in LinePlot doc 101 | 102 | ## [0.9.3] - 2025-05-17 103 | 104 | ### 🐛 Bug Fixes 105 | 106 | - A workaround fix for polars 0.47.1 107 | 108 | ## [0.9.1] - 2025-05-02 109 | 110 | ### 🐛 Bug Fixes 111 | 112 | - Several time series with only one y axis 113 | 114 | ### 📚 Documentation 115 | 116 | - Add another example 117 | 118 | ## [0.9.0] - 2025-05-02 119 | 120 | ### 🚀 Features 121 | 122 | - Add Contour plot 123 | - Add Sankey diagram 124 | - Add surface plot 125 | - Add the secondary y axis 126 | 127 | ### 📚 Documentation 128 | 129 | - Add implemented plots overview with examples to README 130 | 131 | ### Feat 132 | 133 | - Additional trait methods to provide html string 134 | 135 | ## [0.8.0] - 2025-01-05 136 | 137 | ### 🚀 Features 138 | 139 | - Convert plots into JSON with the `to_json`method 140 | - Add Image plot support for visualizing raster data 141 | - Add PieChart support for visualizing categorical data 142 | - Add Array2DPlot for visualizing 2D arrays of RGB color values 143 | - Add ScatterMap for visualizing geographical data points on an interactive map 144 | 145 | ### 🐛 Bug Fixes 146 | 147 | - Rename Array2DPlot to Array2dPlot for consistency 148 | 149 | ## [0.7.0] - 2024-11-06 150 | 151 | ### 🚀 Features 152 | 153 | - Add Scatter3dPlot 154 | 155 | ## [0.6.0] - 2024-11-01 156 | 157 | ### 🚀 Features 158 | 159 | - New `axis_position` method and the old one has been renamed to `axis_side` and the corresponding enum values have been updated 160 | - Add HeatMap 161 | 162 | ### 📚 Documentation 163 | 164 | - Update documentation examples 165 | - Update documentation examples 166 | - Remove reference to vertical and horizontal bar/box plots 167 | - Add important note about using GitHub version of plotlars due to polars issue 168 | - Fix github link 169 | 170 | ## [0.5.0] - 2024-09-13 171 | 172 | ### 🚀 Features 173 | 174 | - Add new BarPlot struct with orientation field; deprecate VerticalBarPlot and HorizontalBarPlot 175 | - Update BoxPlot struct to handle both vertical and horizontal box plots 176 | - Add `color` argument 177 | - Customize the shape of the marker 178 | - Add optional shape and add line width for line and time series plots 179 | 180 | ## [0.4.0] - 2024-09-10 181 | 182 | ### 🚀 Features 183 | 184 | - Add Legend module 185 | 186 | ## [0.3.0] - 2024-09-01 187 | 188 | ### 🚀 Features 189 | 190 | - Implement From trait for Text to convert from &str and String 191 | - Add plot title position 192 | - Add From trait implementation for Text to convert from &String 193 | - Add axis module for customizing plot axes 194 | - Add write_html method 195 | 196 | ### Chote 197 | 198 | - Update features 199 | 200 | ### Update 201 | 202 | - CHANGELOG.md 203 | - Add link to data and fix text 204 | - Update dataset path 205 | - Update Changelog 206 | 207 | ## [0.2.0] - 2024-08-25 208 | 209 | ### 🚀 Features 210 | 211 | - Add Bar Plot 212 | - Add Box Plot 213 | - Add Histogram plot 214 | - Add Line Plot 215 | - Add Scatter Plot 216 | - Add Time Series Plot 217 | - Add Text module with customizable content, font, size, and color 218 | - Add Rgb struct for representing RGB colors 219 | - Add Mark trait for creating and modifying markers 220 | - Add LineType enum for representing different styles of lines and Line trait 221 | - Add Layout trait for creating Plotly layouts 222 | - Add Plot trait for displaying and rendering generic plots 223 | - Add Trace trait for creating and modifying traces 224 | - Add Polar trait for working with polars dataframes 225 | - Add plot example to README.md 226 | 227 | ### Update 228 | 229 | - README.md 230 | -------------------------------------------------------------------------------- /src/plots/density_mapbox.rs: -------------------------------------------------------------------------------- 1 | use bon::bon; 2 | 3 | use plotly::{ 4 | layout::{Center, Layout as LayoutPlotly, Mapbox, MapboxStyle, Margin}, 5 | DensityMapbox as DensityMapboxPlotly, Trace, 6 | }; 7 | 8 | use polars::frame::DataFrame; 9 | use serde::Serialize; 10 | 11 | use crate::{ 12 | common::{Layout, PlotHelper, Polar}, 13 | components::{Legend, Text}, 14 | }; 15 | 16 | /// A structure representing a density mapbox visualization. 17 | /// 18 | /// The `DensityMapbox` struct enables the creation of geographic density visualizations on an interactive map. 19 | /// It displays density or intensity values at geographic locations using latitude and longitude coordinates, 20 | /// with a third dimension (z) representing the intensity at each point. This is useful for visualizing 21 | /// population density, heat maps of activity, or any geographic concentration of values. 22 | /// 23 | /// # Arguments 24 | /// 25 | /// * `data` - A reference to the `DataFrame` containing the data to be plotted. 26 | /// * `lat` - A string slice specifying the column name containing latitude values. 27 | /// * `lon` - A string slice specifying the column name containing longitude values. 28 | /// * `z` - A string slice specifying the column name containing intensity/density values. 29 | /// * `center` - An optional array `[f64; 2]` specifying the initial center point of the map ([latitude, longitude]). 30 | /// * `zoom` - An optional `u8` specifying the initial zoom level of the map. 31 | /// * `radius` - An optional `u8` specifying the radius of influence for each point. 32 | /// * `opacity` - An optional `f64` value between `0.0` and `1.0` specifying the opacity of the density layer. 33 | /// * `z_min` - An optional `f64` specifying the minimum value for the color scale. 34 | /// * `z_max` - An optional `f64` specifying the maximum value for the color scale. 35 | /// * `z_mid` - An optional `f64` specifying the midpoint value for the color scale. 36 | /// * `plot_title` - An optional `Text` struct specifying the title of the plot. 37 | /// * `legend_title` - An optional `Text` struct specifying the title of the legend. 38 | /// * `legend` - An optional reference to a `Legend` struct for customizing the legend. 39 | /// 40 | /// # Example 41 | /// 42 | /// ```rust 43 | /// use plotlars::{DensityMapbox, Plot, Text}; 44 | /// use polars::prelude::*; 45 | /// 46 | /// let data = LazyCsvReader::new(PlPath::new("data/us_city_density.csv")) 47 | /// .finish() 48 | /// .unwrap() 49 | /// .collect() 50 | /// .unwrap(); 51 | /// 52 | /// DensityMapbox::builder() 53 | /// .data(&data) 54 | /// .lat("city_lat") 55 | /// .lon("city_lon") 56 | /// .z("population_density") 57 | /// .center([39.8283, -98.5795]) 58 | /// .zoom(3) 59 | /// .plot_title( 60 | /// Text::from("Density Mapbox") 61 | /// .font("Arial") 62 | /// .size(20) 63 | /// ) 64 | /// .build() 65 | /// .plot(); 66 | /// ``` 67 | /// 68 | /// ![Example](https://imgur.com/82eLyBm.png) 69 | #[derive(Clone, Serialize)] 70 | pub struct DensityMapbox { 71 | traces: Vec>, 72 | layout: LayoutPlotly, 73 | } 74 | 75 | #[bon] 76 | impl DensityMapbox { 77 | #[builder(on(String, into), on(Text, into))] 78 | pub fn new( 79 | data: &DataFrame, 80 | lat: &str, 81 | lon: &str, 82 | z: &str, 83 | center: Option<[f64; 2]>, 84 | zoom: Option, 85 | radius: Option, 86 | opacity: Option, 87 | z_min: Option, 88 | z_max: Option, 89 | z_mid: Option, 90 | plot_title: Option, 91 | legend_title: Option, 92 | legend: Option<&Legend>, 93 | ) -> Self { 94 | let x_title = None; 95 | let y_title = None; 96 | let z_title = None; 97 | let x_axis = None; 98 | let y_axis = None; 99 | let z_axis = None; 100 | let y2_title = None; 101 | let y2_axis = None; 102 | 103 | let mut layout = Self::create_layout( 104 | plot_title, 105 | x_title, 106 | y_title, 107 | y2_title, 108 | z_title, 109 | legend_title, 110 | x_axis, 111 | y_axis, 112 | y2_axis, 113 | z_axis, 114 | legend, 115 | None, 116 | ) 117 | .margin(Margin::new().bottom(0)); 118 | 119 | let mut map_box = Mapbox::new().style(MapboxStyle::OpenStreetMap); 120 | 121 | if let Some(center) = center { 122 | map_box = map_box.center(Center::new(center[0], center[1])); 123 | } 124 | 125 | if let Some(zoom) = zoom { 126 | map_box = map_box.zoom(zoom); 127 | } else { 128 | map_box = map_box.zoom(1); 129 | } 130 | 131 | layout = layout.mapbox(map_box); 132 | 133 | let traces = Self::create_traces(data, lat, lon, z, radius, opacity, z_min, z_max, z_mid); 134 | 135 | Self { traces, layout } 136 | } 137 | 138 | #[allow(clippy::too_many_arguments)] 139 | fn create_traces( 140 | data: &DataFrame, 141 | lat: &str, 142 | lon: &str, 143 | z: &str, 144 | radius: Option, 145 | opacity: Option, 146 | z_min: Option, 147 | z_max: Option, 148 | z_mid: Option, 149 | ) -> Vec> { 150 | let mut traces: Vec> = Vec::new(); 151 | 152 | let trace = Self::create_trace(data, lat, lon, z, radius, opacity, z_min, z_max, z_mid); 153 | 154 | traces.push(trace); 155 | traces 156 | } 157 | 158 | #[allow(clippy::too_many_arguments)] 159 | fn create_trace( 160 | data: &DataFrame, 161 | lat: &str, 162 | lon: &str, 163 | z: &str, 164 | radius: Option, 165 | opacity: Option, 166 | z_min: Option, 167 | z_max: Option, 168 | z_mid: Option, 169 | ) -> Box { 170 | let lat_data = Self::get_numeric_column(data, lat); 171 | let lon_data = Self::get_numeric_column(data, lon); 172 | let z_data = Self::get_numeric_column(data, z); 173 | 174 | let mut trace = DensityMapboxPlotly::new(lat_data, lon_data, z_data); 175 | 176 | if let Some(radius) = radius { 177 | trace = trace.radius(radius); 178 | } 179 | 180 | if let Some(opacity) = opacity { 181 | trace = trace.opacity(opacity); 182 | } 183 | 184 | if let Some(z_min) = z_min { 185 | trace = trace.zmin(Some(z_min as f32)); 186 | } 187 | 188 | if let Some(z_max) = z_max { 189 | trace = trace.zmax(Some(z_max as f32)); 190 | } 191 | 192 | if let Some(z_mid) = z_mid { 193 | trace = trace.zmid(Some(z_mid as f32)); 194 | } 195 | 196 | trace 197 | } 198 | } 199 | 200 | impl Layout for DensityMapbox {} 201 | impl Polar for DensityMapbox {} 202 | 203 | impl PlotHelper for DensityMapbox { 204 | fn get_layout(&self) -> &LayoutPlotly { 205 | &self.layout 206 | } 207 | 208 | fn get_traces(&self) -> &Vec> { 209 | &self.traces 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/common/layout.rs: -------------------------------------------------------------------------------- 1 | use plotly::common::Anchor; 2 | use plotly::layout::Annotation; 3 | use plotly::Layout as LayoutPlotly; 4 | 5 | use crate::components::{Axis, Dimensions, Legend, Text}; 6 | 7 | #[allow(clippy::too_many_arguments)] 8 | pub(crate) trait Layout { 9 | fn create_layout( 10 | plot_title: Option, 11 | x_title: Option, 12 | y_title: Option, 13 | y2_title: Option, 14 | z_title: Option, 15 | legend_title: Option, 16 | x_axis: Option<&Axis>, 17 | y_axis: Option<&Axis>, 18 | y2_axis: Option<&Axis>, 19 | z_axis: Option<&Axis>, 20 | legend: Option<&Legend>, 21 | dimensions: Option<&Dimensions>, 22 | ) -> LayoutPlotly { 23 | let mut layout = LayoutPlotly::new(); 24 | let mut annotations = Vec::new(); 25 | 26 | if let Some(title) = plot_title { 27 | layout = layout.title(title.to_plotly()); 28 | } 29 | 30 | let (x_title_for_axis, x_annotation) = if let Some(text) = x_title { 31 | if text.has_custom_position() { 32 | let text_with_defaults = text.with_x_title_defaults_for_annotation(); 33 | let ann = text_with_defaults.to_axis_annotation(true, "x", false); 34 | (None, Some(ann)) 35 | } else { 36 | (Some(text.with_x_title_defaults()), None) 37 | } 38 | } else { 39 | (None, None) 40 | }; 41 | 42 | match (x_axis, x_title_for_axis) { 43 | (Some(axis), title) => { 44 | layout = layout.x_axis(Axis::set_axis(title, axis, None)); 45 | } 46 | (None, Some(title)) => { 47 | let default_axis = Axis::default(); 48 | layout = layout.x_axis(Axis::set_axis(Some(title), &default_axis, None)); 49 | } 50 | _ => {} 51 | } 52 | 53 | if let Some(ann) = x_annotation { 54 | annotations.push(ann); 55 | } 56 | 57 | let (y_title_for_axis, y_annotation) = if let Some(text) = y_title { 58 | if text.has_custom_position() { 59 | let text_with_defaults = text.with_y_title_defaults_for_annotation(); 60 | let ann = text_with_defaults.to_axis_annotation(false, "y", false); 61 | (None, Some(ann)) 62 | } else { 63 | (Some(text.with_y_title_defaults()), None) 64 | } 65 | } else { 66 | (None, None) 67 | }; 68 | 69 | match (y_axis, y_title_for_axis) { 70 | (Some(axis), title) => { 71 | layout = layout.y_axis(Axis::set_axis(title, axis, None)); 72 | } 73 | (None, Some(title)) => { 74 | let default_axis = Axis::default(); 75 | layout = layout.y_axis(Axis::set_axis(Some(title), &default_axis, None)); 76 | } 77 | _ => {} 78 | } 79 | 80 | if let Some(ann) = y_annotation { 81 | annotations.push(ann); 82 | } 83 | 84 | // Handle y2-axis 85 | if let Some(y2_axis) = y2_axis { 86 | layout = layout.y_axis2(Axis::set_axis(y2_title, y2_axis, Some("y"))); 87 | } 88 | 89 | // Handle z-axis: use provided axis or create default with title if only title exists 90 | match (z_axis, z_title) { 91 | (Some(axis), title) => { 92 | layout = layout.z_axis(Axis::set_axis(title, axis, None)); 93 | } 94 | (None, Some(title)) => { 95 | let default_axis = Axis::default(); 96 | layout = layout.z_axis(Axis::set_axis(Some(title), &default_axis, None)); 97 | } 98 | _ => {} 99 | } 100 | 101 | layout = layout.legend(Legend::set_legend(legend_title, legend)); 102 | 103 | if !annotations.is_empty() { 104 | layout = layout.annotations(annotations); 105 | } 106 | 107 | if let Some(dims) = dimensions { 108 | if let Some(width) = dims.width { 109 | layout = layout.width(width); 110 | } 111 | if let Some(height) = dims.height { 112 | layout = layout.height(height); 113 | } 114 | if let Some(auto_size) = dims.auto_size { 115 | layout = layout.auto_size(auto_size); 116 | } 117 | } 118 | 119 | layout 120 | } 121 | 122 | fn calculate_grid_dimensions( 123 | n_facets: usize, 124 | cols: Option, 125 | rows: Option, 126 | ) -> (usize, usize) { 127 | match (cols, rows) { 128 | (Some(c), Some(r)) => { 129 | if c * r < n_facets { 130 | panic!("Grid dimensions {}x{} cannot fit {} facets", c, r, n_facets); 131 | } 132 | (c, r) 133 | } 134 | (Some(c), None) => { 135 | let r = n_facets.div_ceil(c); 136 | (c, r) 137 | } 138 | (None, Some(r)) => { 139 | let c = n_facets.div_ceil(r); 140 | (c, r) 141 | } 142 | (None, None) => { 143 | let c = (n_facets as f64).sqrt().ceil() as usize; 144 | let r = n_facets.div_ceil(c); 145 | (c, r) 146 | } 147 | } 148 | } 149 | 150 | fn create_facet_annotations( 151 | categories: &[String], 152 | title_style: Option<&Text>, 153 | ) -> Vec { 154 | categories 155 | .iter() 156 | .enumerate() 157 | .map(|(i, cat)| { 158 | let x_ref = if i == 0 { 159 | "x domain".to_string() 160 | } else { 161 | format!("x{} domain", i + 1) 162 | }; 163 | let y_ref = if i == 0 { 164 | "y domain".to_string() 165 | } else { 166 | format!("y{} domain", i + 1) 167 | }; 168 | 169 | let mut ann = Annotation::new() 170 | .text(cat.as_str()) 171 | .x_ref(&x_ref) 172 | .y_ref(&y_ref) 173 | .x_anchor(Anchor::Center) 174 | .y_anchor(Anchor::Bottom) 175 | .x(0.5) 176 | .y(1.0) 177 | .show_arrow(false); 178 | 179 | if let Some(style) = title_style { 180 | ann = ann.font(style.to_font()); 181 | } 182 | 183 | ann 184 | }) 185 | .collect() 186 | } 187 | 188 | fn get_axis_reference(subplot_index: usize, axis_type: &str) -> String { 189 | if subplot_index == 0 { 190 | axis_type.to_string() 191 | } else { 192 | format!("{}{}", axis_type, subplot_index + 1) 193 | } 194 | } 195 | 196 | fn is_bottom_row(subplot_index: usize, ncols: usize, nrows: usize, n_facets: usize) -> bool { 197 | let row = subplot_index / ncols; 198 | let index_below = subplot_index + ncols; 199 | 200 | row == nrows - 1 || index_below >= n_facets 201 | } 202 | 203 | fn is_left_column(subplot_index: usize, ncols: usize) -> bool { 204 | let col = subplot_index % ncols; 205 | col == 0 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/plots/candlestick.rs: -------------------------------------------------------------------------------- 1 | use bon::bon; 2 | 3 | use plotly::{Candlestick as CandlestickPlotly, Layout as LayoutPlotly, Trace}; 4 | 5 | use polars::frame::DataFrame; 6 | use serde::Serialize; 7 | 8 | use crate::{ 9 | common::{Layout, PlotHelper, Polar}, 10 | components::{Axis, Direction, Text}, 11 | }; 12 | 13 | /// A structure representing a Candlestick financial chart. 14 | /// 15 | /// The `CandlestickPlot` struct facilitates the creation and customization of candlestick charts commonly used 16 | /// for visualizing financial data such as stock prices. It supports custom styling for increasing/decreasing 17 | /// values, whisker width configuration, hover information, and comprehensive layout customization 18 | /// including range selectors and sliders for interactive time navigation. 19 | /// 20 | /// # Arguments 21 | /// 22 | /// * `data` - A reference to the `DataFrame` containing the data to be plotted. 23 | /// * `dates` - A string slice specifying the column name for dates/timestamps. 24 | /// * `open` - A string slice specifying the column name for opening values. 25 | /// * `high` - A string slice specifying the column name for high values. 26 | /// * `low` - A string slice specifying the column name for low values. 27 | /// * `close` - A string slice specifying the column name for closing values. 28 | /// * `increasing` - An optional reference to a `Direction` struct for customizing increasing candlesticks. 29 | /// * `decreasing` - An optional reference to a `Direction` struct for customizing decreasing candlesticks. 30 | /// * `whisker_width` - An optional `f64` specifying the width of the whiskers (0-1 range). 31 | /// * `plot_title` - An optional `Text` struct specifying the title of the plot. 32 | /// * `x_title` - An optional `Text` struct specifying the title of the x-axis. 33 | /// * `y_title` - An optional `Text` struct specifying the title of the y-axis. 34 | /// * `x_axis` - An optional reference to an `Axis` struct for customizing the x-axis. 35 | /// * `y_axis` - An optional reference to an `Axis` struct for customizing the y-axis. 36 | /// 37 | /// # Examples 38 | /// 39 | /// ```rust 40 | /// use plotlars::{Axis, CandlestickPlot, Direction, Plot, Rgb}; 41 | /// use polars::prelude::*; 42 | /// 43 | /// let stock_data = LazyCsvReader::new(PlPath::new("data/stock_prices.csv")) 44 | /// .finish() 45 | /// .unwrap() 46 | /// .collect() 47 | /// .unwrap(); 48 | /// 49 | /// let increasing = Direction::new() 50 | /// .line_color(Rgb(0, 200, 100)) 51 | /// .line_width(0.5); 52 | /// 53 | /// let decreasing = Direction::new() 54 | /// .line_color(Rgb(200, 50, 50)) 55 | /// .line_width(0.5); 56 | /// 57 | /// CandlestickPlot::builder() 58 | /// .data(&stock_data) 59 | /// .dates("date") 60 | /// .open("open") 61 | /// .high("high") 62 | /// .low("low") 63 | /// .close("close") 64 | /// .increasing(&increasing) 65 | /// .decreasing(&decreasing) 66 | /// .whisker_width(0.1) 67 | /// .plot_title("Candlestick Plot") 68 | /// .y_title("Price ($)") 69 | /// .y_axis( 70 | /// &Axis::new() 71 | /// .show_axis(true) 72 | /// .show_grid(true) 73 | /// ) 74 | /// .build() 75 | /// .plot(); 76 | /// ``` 77 | /// 78 | /// ![Example](https://imgur.com/fNDRLDX.png) 79 | #[derive(Clone, Serialize)] 80 | pub struct CandlestickPlot { 81 | traces: Vec>, 82 | layout: LayoutPlotly, 83 | } 84 | 85 | #[bon] 86 | impl CandlestickPlot { 87 | #[builder(on(String, into), on(Text, into))] 88 | pub fn new( 89 | data: &DataFrame, 90 | dates: &str, 91 | open: &str, 92 | high: &str, 93 | low: &str, 94 | close: &str, 95 | increasing: Option<&Direction>, 96 | decreasing: Option<&Direction>, 97 | whisker_width: Option, 98 | plot_title: Option, 99 | x_title: Option, 100 | y_title: Option, 101 | x_axis: Option<&Axis>, 102 | y_axis: Option<&Axis>, 103 | ) -> Self { 104 | let z_title = None; 105 | let y_title2 = None; 106 | let z_axis = None; 107 | let y_axis2 = None; 108 | let legend_title = None; 109 | let legend = None; 110 | 111 | let layout = Self::create_layout( 112 | plot_title, 113 | x_title, 114 | y_title, 115 | y_title2, 116 | z_title, 117 | legend_title, 118 | x_axis, 119 | y_axis, 120 | y_axis2, 121 | z_axis, 122 | legend, 123 | None, 124 | ); 125 | 126 | let traces = Self::create_traces( 127 | data, 128 | dates, 129 | open, 130 | high, 131 | low, 132 | close, 133 | increasing, 134 | decreasing, 135 | whisker_width, 136 | ); 137 | 138 | Self { traces, layout } 139 | } 140 | 141 | #[allow(clippy::too_many_arguments)] 142 | fn create_traces( 143 | data: &DataFrame, 144 | dates_col: &str, 145 | open_col: &str, 146 | high_col: &str, 147 | low_col: &str, 148 | close_col: &str, 149 | increasing: Option<&Direction>, 150 | decreasing: Option<&Direction>, 151 | whisker_width: Option, 152 | ) -> Vec> { 153 | let mut traces: Vec> = Vec::new(); 154 | 155 | let trace = Self::create_trace( 156 | data, 157 | dates_col, 158 | open_col, 159 | high_col, 160 | low_col, 161 | close_col, 162 | increasing, 163 | decreasing, 164 | whisker_width, 165 | ); 166 | 167 | traces.push(trace); 168 | traces 169 | } 170 | 171 | #[allow(clippy::too_many_arguments)] 172 | fn create_trace( 173 | data: &DataFrame, 174 | dates_col: &str, 175 | open_col: &str, 176 | high_col: &str, 177 | low_col: &str, 178 | close_col: &str, 179 | increasing: Option<&Direction>, 180 | decreasing: Option<&Direction>, 181 | whisker_width: Option, 182 | ) -> Box { 183 | let dates_data = Self::get_string_column(data, dates_col); 184 | let open_data = Self::get_numeric_column(data, open_col); 185 | let high_data = Self::get_numeric_column(data, high_col); 186 | let low_data = Self::get_numeric_column(data, low_col); 187 | let close_data = Self::get_numeric_column(data, close_col); 188 | 189 | let open_values: Vec = open_data.into_iter().map(|v| v.unwrap_or(0.0)).collect(); 190 | let high_values: Vec = high_data.into_iter().map(|v| v.unwrap_or(0.0)).collect(); 191 | let low_values: Vec = low_data.into_iter().map(|v| v.unwrap_or(0.0)).collect(); 192 | let close_values: Vec = close_data.into_iter().map(|v| v.unwrap_or(0.0)).collect(); 193 | let dates_values: Vec = dates_data 194 | .into_iter() 195 | .map(|v| v.unwrap_or_default()) 196 | .collect(); 197 | 198 | let mut trace = *CandlestickPlotly::new( 199 | dates_values, 200 | open_values, 201 | high_values, 202 | low_values, 203 | close_values, 204 | ); 205 | 206 | // Set increasing direction style 207 | if let Some(inc) = increasing { 208 | trace = trace.increasing(inc.to_plotly_increasing()); 209 | } 210 | 211 | // Set decreasing direction style 212 | if let Some(dec) = decreasing { 213 | trace = trace.decreasing(dec.to_plotly_decreasing()); 214 | } 215 | 216 | // Set whisker width 217 | if let Some(whisker_w) = whisker_width { 218 | trace = trace.whisker_width(whisker_w); 219 | } 220 | 221 | // Return trace as Box 222 | Box::new(trace) 223 | } 224 | } 225 | 226 | impl Layout for CandlestickPlot {} 227 | impl Polar for CandlestickPlot {} 228 | 229 | impl PlotHelper for CandlestickPlot { 230 | fn get_layout(&self) -> &LayoutPlotly { 231 | &self.layout 232 | } 233 | 234 | fn get_traces(&self) -> &Vec> { 235 | &self.traces 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/plots/scattermap.rs: -------------------------------------------------------------------------------- 1 | use bon::bon; 2 | 3 | use plotly::{ 4 | common::{Marker as MarkerPlotly, Mode}, 5 | layout::{Center, Layout as LayoutPlotly, Mapbox, MapboxStyle, Margin}, 6 | ScatterMapbox, Trace, 7 | }; 8 | 9 | use polars::frame::DataFrame; 10 | use serde::Serialize; 11 | 12 | use crate::{ 13 | common::{Layout, Marker, PlotHelper, Polar}, 14 | components::{Legend, Rgb, Shape, Text}, 15 | }; 16 | 17 | /// A structure representing a scatter plot on a map. 18 | /// 19 | /// The `ScatterMap` struct allows for visualizing geographical data points on an interactive map. 20 | /// Each data point is defined by its latitude and longitude, with additional options for grouping, 21 | /// coloring, size, opacity, and map configuration such as zoom level and center coordinates. 22 | /// This struct is ideal for displaying spatial data distributions, such as city locations or geospatial datasets. 23 | /// 24 | /// # Arguments 25 | /// 26 | /// * `data` - A reference to the `DataFrame` containing the data to be plotted. 27 | /// * `latitude` - A string slice specifying the column name containing latitude values. 28 | /// * `longitude` - A string slice specifying the column name containing longitude values. 29 | /// * `center` - An optional array `[f64; 2]` specifying the initial center point of the map ([latitude, longitude]). 30 | /// * `zoom` - An optional `u8` specifying the initial zoom level of the map. 31 | /// * `group` - An optional string slice specifying the column name for grouping data points (e.g., by city or category). 32 | /// * `sort_groups_by` - Optional comparator `fn(&str, &str) -> std::cmp::Ordering` to control group ordering. Groups are sorted lexically by default. 33 | /// * `opacity` - An optional `f64` value between `0.0` and `1.0` specifying the opacity of the points. 34 | /// * `size` - An optional `usize` specifying the size of the scatter points. 35 | /// * `color` - An optional `Rgb` value specifying the color of the points (if no grouping is applied). 36 | /// * `colors` - An optional vector of `Rgb` values specifying colors for grouped points. 37 | /// * `shape` - An optional `Shape` enum specifying the marker shape for the points. 38 | /// * `shapes` - An optional vector of `Shape` enums specifying shapes for grouped points. 39 | /// * `plot_title` - An optional `Text` struct specifying the title of the plot. 40 | /// * `legend_title` - An optional `Text` struct specifying the title of the legend. 41 | /// * `legend` - An optional reference to a `Legend` struct for customizing the legend (e.g., positioning, font, etc.). 42 | /// 43 | /// # Example 44 | /// 45 | /// ## Basic Scatter Map Plot 46 | /// 47 | /// ```rust 48 | /// use plotlars::{Plot, ScatterMap, Text}; 49 | /// use polars::prelude::*; 50 | /// 51 | /// let dataset = LazyCsvReader::new(PlPath::new("data/cities.csv")) 52 | /// .finish() 53 | /// .unwrap() 54 | /// .collect() 55 | /// .unwrap(); 56 | /// 57 | /// ScatterMap::builder() 58 | /// .data(&dataset) 59 | /// .latitude("latitude") 60 | /// .longitude("longitude") 61 | /// .center([48.856613, 2.352222]) 62 | /// .zoom(4) 63 | /// .group("city") 64 | /// .opacity(0.5) 65 | /// .size(12) 66 | /// .plot_title( 67 | /// Text::from("Scatter Map") 68 | /// .font("Arial") 69 | /// .size(18) 70 | /// ) 71 | /// .legend_title("cities") 72 | /// .build() 73 | /// .plot(); 74 | /// ``` 75 | /// 76 | /// ![Example](https://imgur.com/8MCjVOd.png) 77 | #[derive(Clone, Serialize)] 78 | pub struct ScatterMap { 79 | traces: Vec>, 80 | layout: LayoutPlotly, 81 | } 82 | 83 | #[bon] 84 | impl ScatterMap { 85 | #[builder(on(String, into), on(Text, into))] 86 | pub fn new( 87 | data: &DataFrame, 88 | latitude: &str, 89 | longitude: &str, 90 | center: Option<[f64; 2]>, 91 | zoom: Option, 92 | group: Option<&str>, 93 | sort_groups_by: Option std::cmp::Ordering>, 94 | opacity: Option, 95 | size: Option, 96 | color: Option, 97 | colors: Option>, 98 | shape: Option, 99 | shapes: Option>, 100 | plot_title: Option, 101 | legend_title: Option, 102 | legend: Option<&Legend>, 103 | ) -> Self { 104 | let x_title = None; 105 | let y_title = None; 106 | let z_title = None; 107 | let x_axis = None; 108 | let y_axis = None; 109 | let z_axis = None; 110 | let y2_title = None; 111 | let y2_axis = None; 112 | 113 | let mut layout = Self::create_layout( 114 | plot_title, 115 | x_title, 116 | y_title, 117 | y2_title, 118 | z_title, 119 | legend_title, 120 | x_axis, 121 | y_axis, 122 | y2_axis, 123 | z_axis, 124 | legend, 125 | None, 126 | ) 127 | .margin(Margin::new().bottom(0)); 128 | 129 | let mut map_box = Mapbox::new().style(MapboxStyle::OpenStreetMap).zoom(0); 130 | 131 | if let Some(center) = center { 132 | map_box = map_box.center(Center::new(center[0], center[1])); 133 | } 134 | 135 | if let Some(zoom) = zoom { 136 | map_box = map_box.zoom(zoom); 137 | } 138 | 139 | layout = layout.mapbox(map_box); 140 | 141 | let traces = Self::create_traces( 142 | data, 143 | latitude, 144 | longitude, 145 | group, 146 | sort_groups_by, 147 | opacity, 148 | size, 149 | color, 150 | colors, 151 | shape, 152 | shapes, 153 | ); 154 | 155 | Self { traces, layout } 156 | } 157 | 158 | #[allow(clippy::too_many_arguments)] 159 | fn create_traces( 160 | data: &DataFrame, 161 | latitude: &str, 162 | longitude: &str, 163 | group: Option<&str>, 164 | sort_groups_by: Option std::cmp::Ordering>, 165 | opacity: Option, 166 | size: Option, 167 | color: Option, 168 | colors: Option>, 169 | shape: Option, 170 | shapes: Option>, 171 | ) -> Vec> { 172 | let mut traces: Vec> = Vec::new(); 173 | 174 | match group { 175 | Some(group_col) => { 176 | let groups = Self::get_unique_groups(data, group_col, sort_groups_by); 177 | 178 | let groups = groups.iter().map(|s| s.as_str()); 179 | 180 | for (i, group) in groups.enumerate() { 181 | let marker = Self::create_marker( 182 | i, 183 | opacity, 184 | size, 185 | color, 186 | colors.clone(), 187 | shape, 188 | shapes.clone(), 189 | ); 190 | 191 | let subset = Self::filter_data_by_group(data, group_col, group); 192 | 193 | let trace = 194 | Self::create_trace(&subset, latitude, longitude, Some(group), marker); 195 | 196 | traces.push(trace); 197 | } 198 | } 199 | None => { 200 | let group = None; 201 | 202 | let marker = Self::create_marker( 203 | 0, 204 | opacity, 205 | size, 206 | color, 207 | colors.clone(), 208 | shape, 209 | shapes.clone(), 210 | ); 211 | 212 | let trace = Self::create_trace(data, latitude, longitude, group, marker); 213 | 214 | traces.push(trace); 215 | } 216 | } 217 | 218 | traces 219 | } 220 | 221 | fn create_trace( 222 | data: &DataFrame, 223 | latitude: &str, 224 | longitude: &str, 225 | group_name: Option<&str>, 226 | marker: MarkerPlotly, 227 | ) -> Box { 228 | let latitude = Self::get_numeric_column(data, latitude); 229 | let longitude = Self::get_numeric_column(data, longitude); 230 | 231 | let mut trace = ScatterMapbox::default() 232 | .lat(latitude) 233 | .lon(longitude) 234 | .mode(Mode::Markers); 235 | 236 | trace = trace.marker(marker); 237 | 238 | if let Some(name) = group_name { 239 | trace = trace.name(name); 240 | } 241 | 242 | trace 243 | } 244 | } 245 | 246 | impl Layout for ScatterMap {} 247 | impl Marker for ScatterMap {} 248 | impl Polar for ScatterMap {} 249 | 250 | impl PlotHelper for ScatterMap { 251 | fn get_layout(&self) -> &LayoutPlotly { 252 | &self.layout 253 | } 254 | 255 | fn get_traces(&self) -> &Vec> { 256 | &self.traces 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /data/debilt_2023_temps.csv: -------------------------------------------------------------------------------- 1 | date,tavg,tmin,tmax 2 | 2023-01-01,124,104,156 3 | 2023-01-02,88,43,128 4 | 2023-01-03,63,11,91 5 | 2023-01-04,116,85,128 6 | 2023-01-05,108,96,120 7 | 2023-01-06,106,93,117 8 | 2023-01-07,104,84,117 9 | 2023-01-08,84,66,101 10 | 2023-01-09,70,50,88 11 | 2023-01-10,70,51,104 12 | 2023-01-11,100,84,116 13 | 2023-01-12,103,87,114 14 | 2023-01-13,87,66,98 15 | 2023-01-14,84,57,115 16 | 2023-01-15,62,43,78 17 | 2023-01-16,41,28,54 18 | 2023-01-17,14,-15,48 19 | 2023-01-18,17,-35,51 20 | 2023-01-19,30,-1,62 21 | 2023-01-20,4,-7,15 22 | 2023-01-21,-14,-39,1 23 | 2023-01-22,4,-22,15 24 | 2023-01-23,26,12,41 25 | 2023-01-24,17,3,31 26 | 2023-01-25,-7,-13,3 27 | 2023-01-26,41,-5,81 28 | 2023-01-27,40,24,54 29 | 2023-01-28,32,15,62 30 | 2023-01-29,43,30,66 31 | 2023-01-30,67,36,86 32 | 2023-01-31,64,32,97 33 | 2023-02-01,78,56,90 34 | 2023-02-02,83,65,94 35 | 2023-02-03,92,75,107 36 | 2023-02-04,73,41,99 37 | 2023-02-05,65,27,83 38 | 2023-02-06,31,-18,86 39 | 2023-02-07,-3,-46,59 40 | 2023-02-08,1,-54,67 41 | 2023-02-09,18,-18,53 42 | 2023-02-10,36,-30,80 43 | 2023-02-11,77,53,104 44 | 2023-02-12,71,61,84 45 | 2023-02-13,67,1,115 46 | 2023-02-14,40,-8,122 47 | 2023-02-15,53,-9,112 48 | 2023-02-16,83,47,110 49 | 2023-02-17,104,88,119 50 | 2023-02-18,103,86,123 51 | 2023-02-19,78,51,100 52 | 2023-02-20,81,59,92 53 | 2023-02-21,79,60,88 54 | 2023-02-22,70,43,95 55 | 2023-02-23,64,-1,92 56 | 2023-02-24,44,-16,82 57 | 2023-02-25,35,-21,72 58 | 2023-02-26,25,-16,65 59 | 2023-02-27,17,-44,73 60 | 2023-02-28,23,-37,68 61 | 2023-03-01,16,-58,78 62 | 2023-03-02,35,-5,89 63 | 2023-03-03,43,1,79 64 | 2023-03-04,58,37,78 65 | 2023-03-05,40,18,64 66 | 2023-03-06,40,12,68 67 | 2023-03-07,21,-12,51 68 | 2023-03-08,4,-25,30 69 | 2023-03-09,16,3,25 70 | 2023-03-10,17,0,26 71 | 2023-03-11,17,-43,71 72 | 2023-03-12,70,14,116 73 | 2023-03-13,127,91,166 74 | 2023-03-14,66,13,108 75 | 2023-03-15,49,5,88 76 | 2023-03-16,86,44,141 77 | 2023-03-17,116,72,164 78 | 2023-03-18,122,91,154 79 | 2023-03-19,94,78,126 80 | 2023-03-20,83,66,97 81 | 2023-03-21,106,88,133 82 | 2023-03-22,108,92,120 83 | 2023-03-23,127,105,153 84 | 2023-03-24,113,97,140 85 | 2023-03-25,94,77,126 86 | 2023-03-26,72,28,91 87 | 2023-03-27,44,6,81 88 | 2023-03-28,45,-19,89 89 | 2023-03-29,106,56,142 90 | 2023-03-30,129,99,154 91 | 2023-03-31,107,98,130 92 | 2023-04-01,93,63,117 93 | 2023-04-02,58,24,100 94 | 2023-04-03,46,-4,98 95 | 2023-04-04,47,-28,109 96 | 2023-04-05,48,-25,116 97 | 2023-04-06,67,7,88 98 | 2023-04-07,86,72,102 99 | 2023-04-08,91,48,141 100 | 2023-04-09,109,76,156 101 | 2023-04-10,105,68,151 102 | 2023-04-11,104,77,133 103 | 2023-04-12,90,62,124 104 | 2023-04-13,84,30,125 105 | 2023-04-14,91,17,152 106 | 2023-04-15,89,47,131 107 | 2023-04-16,89,77,109 108 | 2023-04-17,102,71,155 109 | 2023-04-18,96,52,139 110 | 2023-04-19,109,69,150 111 | 2023-04-20,89,60,121 112 | 2023-04-21,107,57,162 113 | 2023-04-22,89,36,172 114 | 2023-04-23,115,69,169 115 | 2023-04-24,86,55,109 116 | 2023-04-25,64,21,107 117 | 2023-04-26,52,-7,103 118 | 2023-04-27,79,-12,133 119 | 2023-04-28,106,82,136 120 | 2023-04-29,111,47,159 121 | 2023-04-30,110,25,179 122 | 2023-05-01,118,63,170 123 | 2023-05-02,92,33,128 124 | 2023-05-03,93,6,156 125 | 2023-05-04,149,59,221 126 | 2023-05-05,138,99,188 127 | 2023-05-06,148,88,206 128 | 2023-05-07,152,109,212 129 | 2023-05-08,148,92,201 130 | 2023-05-09,137,123,149 131 | 2023-05-10,130,114,159 132 | 2023-05-11,132,100,165 133 | 2023-05-12,155,113,220 134 | 2023-05-13,164,108,222 135 | 2023-05-14,146,105,200 136 | 2023-05-15,113,85,150 137 | 2023-05-16,107,56,152 138 | 2023-05-17,105,47,142 139 | 2023-05-18,102,35,155 140 | 2023-05-19,138,66,192 141 | 2023-05-20,159,104,202 142 | 2023-05-21,169,140,214 143 | 2023-05-22,180,121,243 144 | 2023-05-23,124,68,155 145 | 2023-05-24,116,44,163 146 | 2023-05-25,120,49,172 147 | 2023-05-26,124,65,182 148 | 2023-05-27,147,60,221 149 | 2023-05-28,157,105,218 150 | 2023-05-29,128,87,177 151 | 2023-05-30,127,73,162 152 | 2023-05-31,154,92,224 153 | 2023-06-01,125,100,165 154 | 2023-06-02,139,104,186 155 | 2023-06-03,166,97,219 156 | 2023-06-04,159,99,218 157 | 2023-06-05,145,93,200 158 | 2023-06-06,157,99,219 159 | 2023-06-07,159,102,224 160 | 2023-06-08,178,107,247 161 | 2023-06-09,218,124,291 162 | 2023-06-10,236,171,298 163 | 2023-06-11,245,152,303 164 | 2023-06-12,243,159,299 165 | 2023-06-13,224,146,275 166 | 2023-06-14,203,135,254 167 | 2023-06-15,200,109,261 168 | 2023-06-16,191,103,265 169 | 2023-06-17,205,87,276 170 | 2023-06-18,219,152,251 171 | 2023-06-19,213,187,257 172 | 2023-06-20,221,186,296 173 | 2023-06-21,208,149,257 174 | 2023-06-22,189,130,242 175 | 2023-06-23,202,148,256 176 | 2023-06-24,213,127,283 177 | 2023-06-25,245,157,313 178 | 2023-06-26,192,130,228 179 | 2023-06-27,177,128,222 180 | 2023-06-28,184,142,218 181 | 2023-06-29,186,115,235 182 | 2023-06-30,176,85,229 183 | 2023-07-01,176,158,198 184 | 2023-07-02,173,125,208 185 | 2023-07-03,166,130,197 186 | 2023-07-04,168,125,213 187 | 2023-07-05,143,116,189 188 | 2023-07-06,169,110,220 189 | 2023-07-07,210,93,273 190 | 2023-07-08,247,171,322 191 | 2023-07-09,220,174,296 192 | 2023-07-10,205,150,254 193 | 2023-07-11,215,140,279 194 | 2023-07-12,188,160,226 195 | 2023-07-13,178,131,218 196 | 2023-07-14,193,131,230 197 | 2023-07-15,210,166,262 198 | 2023-07-16,191,165,226 199 | 2023-07-17,173,132,206 200 | 2023-07-18,180,112,238 201 | 2023-07-19,177,132,214 202 | 2023-07-20,171,117,216 203 | 2023-07-21,156,108,211 204 | 2023-07-22,154,102,195 205 | 2023-07-23,180,145,229 206 | 2023-07-24,169,100,214 207 | 2023-07-25,150,84,197 208 | 2023-07-26,154,85,207 209 | 2023-07-27,170,138,190 210 | 2023-07-28,198,166,238 211 | 2023-07-29,188,162,227 212 | 2023-07-30,174,147,212 213 | 2023-07-31,171,152,187 214 | 2023-08-01,171,146,205 215 | 2023-08-02,173,145,227 216 | 2023-08-03,170,143,202 217 | 2023-08-04,165,120,203 218 | 2023-08-05,152,124,186 219 | 2023-08-06,146,133,158 220 | 2023-08-07,154,133,186 221 | 2023-08-08,159,132,189 222 | 2023-08-09,162,117,210 223 | 2023-08-10,178,115,238 224 | 2023-08-11,209,137,275 225 | 2023-08-12,195,168,235 226 | 2023-08-13,180,125,223 227 | 2023-08-14,195,116,261 228 | 2023-08-15,186,118,241 229 | 2023-08-16,187,106,239 230 | 2023-08-17,181,159,203 231 | 2023-08-18,210,159,268 232 | 2023-08-19,218,155,264 233 | 2023-08-20,198,136,260 234 | 2023-08-21,197,146,264 235 | 2023-08-22,182,122,240 236 | 2023-08-23,188,117,259 237 | 2023-08-24,205,139,251 238 | 2023-08-25,192,139,230 239 | 2023-08-26,159,127,202 240 | 2023-08-27,150,125,182 241 | 2023-08-28,158,93,216 242 | 2023-08-29,141,91,196 243 | 2023-08-30,139,110,193 244 | 2023-08-31,152,116,198 245 | 2023-09-01,159,117,206 246 | 2023-09-02,165,113,226 247 | 2023-09-03,161,103,234 248 | 2023-09-04,178,86,252 249 | 2023-09-05,210,144,288 250 | 2023-09-06,217,146,296 251 | 2023-09-07,207,123,293 252 | 2023-09-08,216,140,300 253 | 2023-09-09,207,127,295 254 | 2023-09-10,233,162,311 255 | 2023-09-11,220,177,279 256 | 2023-09-12,197,159,243 257 | 2023-09-13,166,104,202 258 | 2023-09-14,141,77,209 259 | 2023-09-15,149,75,220 260 | 2023-09-16,181,91,248 261 | 2023-09-17,166,142,193 262 | 2023-09-18,186,137,222 263 | 2023-09-19,166,136,189 264 | 2023-09-20,185,165,218 265 | 2023-09-21,155,133,194 266 | 2023-09-22,140,115,177 267 | 2023-09-23,134,93,177 268 | 2023-09-24,139,73,197 269 | 2023-09-25,158,115,217 270 | 2023-09-26,159,116,215 271 | 2023-09-27,174,104,231 272 | 2023-09-28,170,129,195 273 | 2023-09-29,158,89,207 274 | 2023-09-30,147,94,195 275 | 2023-10-01,176,134,236 276 | 2023-10-02,182,130,236 277 | 2023-10-03,159,126,200 278 | 2023-10-04,146,117,177 279 | 2023-10-05,154,132,188 280 | 2023-10-06,161,130,198 281 | 2023-10-07,181,154,209 282 | 2023-10-08,150,116,176 283 | 2023-10-09,176,146,202 284 | 2023-10-10,174,125,227 285 | 2023-10-11,174,155,196 286 | 2023-10-12,155,131,173 287 | 2023-10-13,178,138,215 288 | 2023-10-14,109,69,141 289 | 2023-10-15,75,49,113 290 | 2023-10-16,88,46,131 291 | 2023-10-17,84,56,131 292 | 2023-10-18,94,54,136 293 | 2023-10-19,150,106,181 294 | 2023-10-20,116,93,146 295 | 2023-10-21,130,111,156 296 | 2023-10-22,113,55,157 297 | 2023-10-23,105,56,133 298 | 2023-10-24,114,93,134 299 | 2023-10-25,92,64,103 300 | 2023-10-26,94,74,108 301 | 2023-10-27,104,74,119 302 | 2023-10-28,118,86,148 303 | 2023-10-29,128,102,144 304 | 2023-10-30,110,96,136 305 | 2023-10-31,110,96,142 306 | 2023-11-01,122,96,154 307 | 2023-11-02,109,80,142 308 | 2023-11-03,95,76,122 309 | 2023-11-04,94,79,106 310 | 2023-11-05,104,92,122 311 | 2023-11-06,98,87,121 312 | 2023-11-07,96,75,125 313 | 2023-11-08,93,77,106 314 | 2023-11-09,94,68,115 315 | 2023-11-10,74,57,86 316 | 2023-11-11,75,55,102 317 | 2023-11-12,53,23,84 318 | 2023-11-13,102,35,153 319 | 2023-11-14,119,100,135 320 | 2023-11-15,110,99,136 321 | 2023-11-16,68,30,102 322 | 2023-11-17,51,22,103 323 | 2023-11-18,83,41,138 324 | 2023-11-19,124,112,139 325 | 2023-11-20,98,43,123 326 | 2023-11-21,72,22,105 327 | 2023-11-22,46,7,89 328 | 2023-11-23,112,84,133 329 | 2023-11-24,64,50,89 330 | 2023-11-25,50,5,84 331 | 2023-11-26,50,-6,74 332 | 2023-11-27,42,18,56 333 | 2023-11-28,21,-2,45 334 | 2023-11-29,24,-4,46 335 | 2023-11-30,-3,-18,9 336 | 2023-12-01,-12,-34,8 337 | 2023-12-02,-4,-41,26 338 | 2023-12-03,8,-13,25 339 | 2023-12-04,18,8,26 340 | 2023-12-05,18,14,24 341 | 2023-12-06,25,-27,66 342 | 2023-12-07,3,-34,26 343 | 2023-12-08,54,21,85 344 | 2023-12-09,81,57,111 345 | 2023-12-10,97,80,112 346 | 2023-12-11,92,77,105 347 | 2023-12-12,80,39,106 348 | 2023-12-13,82,55,99 349 | 2023-12-14,56,44,71 350 | 2023-12-15,84,66,102 351 | 2023-12-16,87,66,105 352 | 2023-12-17,86,63,103 353 | 2023-12-18,75,63,92 354 | 2023-12-19,79,65,89 355 | 2023-12-20,86,63,105 356 | 2023-12-21,98,57,122 357 | 2023-12-22,90,55,109 358 | 2023-12-23,103,73,112 359 | 2023-12-24,116,95,129 360 | 2023-12-25,116,102,125 361 | 2023-12-26,76,15,113 362 | 2023-12-27,70,6,107 363 | 2023-12-28,109,100,115 364 | 2023-12-29,93,55,114 365 | 2023-12-30,81,55,97 366 | 2023-12-31,88,79,95 367 | -------------------------------------------------------------------------------- /src/components/text.rs: -------------------------------------------------------------------------------- 1 | use plotly::common::{Anchor, Font, Title}; 2 | use plotly::layout::Annotation; 3 | 4 | use crate::components::Rgb; 5 | 6 | /// A structure representing text with customizable content, font, size, and color. 7 | /// 8 | /// # Example 9 | /// 10 | /// ```rust 11 | /// use polars::prelude::*; 12 | /// use plotlars::{Axis, BarPlot, Plot, Text, Rgb}; 13 | /// 14 | /// let dataset = df![ 15 | /// "label" => &[""], 16 | /// "value" => &[0], 17 | /// ] 18 | /// .unwrap(); 19 | /// 20 | /// let axis = Axis::new() 21 | /// .tick_values(vec![]); 22 | /// 23 | /// BarPlot::builder() 24 | /// .data(&dataset) 25 | /// .labels("label") 26 | /// .values("value") 27 | /// .plot_title( 28 | /// Text::from("Title") 29 | /// .x(0.1) 30 | /// .color(Rgb(178, 34, 34)) 31 | /// .size(30) 32 | /// .font("Zapfino") 33 | /// ) 34 | /// .x_title( 35 | /// Text::from("X") 36 | /// .color(Rgb(65, 105, 225)) 37 | /// .size(20) 38 | /// .font("Marker Felt") 39 | /// ) 40 | /// .y_title( 41 | /// Text::from("Y") 42 | /// .color(Rgb(255, 140, 0)) 43 | /// .size(20) 44 | /// .font("Arial Black") 45 | /// ) 46 | /// .x_axis(&axis) 47 | /// .y_axis(&axis) 48 | /// .build() 49 | /// .plot(); 50 | /// ``` 51 | /// ![Example](https://imgur.com/4outoUQ.png) 52 | #[derive(Clone)] 53 | pub struct Text { 54 | pub(crate) content: String, 55 | pub(crate) font: String, 56 | pub(crate) size: usize, 57 | pub(crate) color: Rgb, 58 | pub(crate) x: f64, 59 | pub(crate) y: f64, 60 | } 61 | 62 | impl Default for Text { 63 | /// Provides default values for the `Text` struct. 64 | /// 65 | /// - `content`: An empty string. 66 | /// - `font`: An empty string. 67 | /// - `size`: `12` (reasonable default for visibility). 68 | /// - `color`: Default `Rgb` value. 69 | /// - `x`: `0.5`. 70 | /// - `y`: `0.9`. 71 | fn default() -> Self { 72 | Text { 73 | content: String::new(), 74 | font: String::new(), 75 | size: 12, 76 | color: Rgb::default(), 77 | x: 0.5, 78 | y: 0.9, 79 | } 80 | } 81 | } 82 | 83 | impl Text { 84 | /// Creates a new `Text` instance from the given content. 85 | /// 86 | /// # Argument 87 | /// 88 | /// * `content` - A value that can be converted into a `String`, representing the textual content. 89 | pub fn from(content: impl Into) -> Self { 90 | Self { 91 | content: content.into(), 92 | ..Default::default() 93 | } 94 | } 95 | 96 | /// Sets the font of the text. 97 | /// 98 | /// # Argument 99 | /// 100 | /// * `font` - A value that can be converted into a `String`, representing the font name. 101 | pub fn font(mut self, font: impl Into) -> Self { 102 | self.font = font.into(); 103 | self 104 | } 105 | 106 | /// Sets the size of the text. 107 | /// 108 | /// # Argument 109 | /// 110 | /// * `size` - A `usize` value specifying the font size. 111 | pub fn size(mut self, size: usize) -> Self { 112 | self.size = size; 113 | self 114 | } 115 | 116 | /// Sets the color of the text. 117 | /// 118 | /// # Argument 119 | /// 120 | /// * `color` - An `Rgb` value specifying the color of the text. 121 | pub fn color(mut self, color: Rgb) -> Self { 122 | self.color = color; 123 | self 124 | } 125 | 126 | /// Sets the x-coordinate position of the text. 127 | /// 128 | /// # Argument 129 | /// 130 | /// * `x` - A `f64` value specifying the horizontal position. 131 | pub fn x(mut self, x: f64) -> Self { 132 | self.x = x; 133 | self 134 | } 135 | 136 | /// Sets the y-coordinate position of the text. 137 | /// 138 | /// # Argument 139 | /// 140 | /// * `y` - A `f64` value specifying the vertical position. 141 | pub fn y(mut self, y: f64) -> Self { 142 | self.y = y; 143 | self 144 | } 145 | 146 | pub(crate) fn to_plotly(&self) -> Title { 147 | Title::with_text(&self.content) 148 | .font( 149 | Font::new() 150 | .family(self.font.as_str()) 151 | .size(self.size) 152 | .color(self.color.to_plotly()), 153 | ) 154 | .x(self.x) 155 | .y(self.y) 156 | } 157 | 158 | pub(crate) fn to_font(&self) -> Font { 159 | Font::new() 160 | .family(self.font.as_str()) 161 | .size(self.size) 162 | .color(self.color.to_plotly()) 163 | } 164 | 165 | pub(crate) fn has_custom_position(&self) -> bool { 166 | const EPSILON: f64 = 1e-6; 167 | (self.x - 0.5).abs() > EPSILON || (self.y - 0.9).abs() > EPSILON 168 | } 169 | 170 | /// Apply default positioning for plot titles (x=0.5, y=0.95 - centered above) 171 | pub(crate) fn with_plot_title_defaults(mut self) -> Self { 172 | const EPSILON: f64 = 1e-6; 173 | let y_is_default = (self.y - 0.9).abs() < EPSILON; 174 | 175 | if y_is_default { 176 | self.y = 0.95; 177 | } 178 | 179 | self 180 | } 181 | 182 | /// Apply default positioning for subplot titles (x=0.5, y=1.1 - centered above, higher than overall) 183 | pub(crate) fn with_subplot_title_defaults(mut self) -> Self { 184 | const EPSILON: f64 = 1e-6; 185 | let y_is_default = (self.y - 0.9).abs() < EPSILON; 186 | let y_is_plot_default = (self.y - 0.95).abs() < EPSILON; 187 | 188 | // Override both Text::default (0.9) and plot_title default (0.95) 189 | if y_is_default || y_is_plot_default { 190 | self.y = 1.1; 191 | } 192 | 193 | self 194 | } 195 | 196 | /// Apply default positioning for x-axis titles (x=0.5, y=-0.15 - centered below) 197 | pub(crate) fn with_x_title_defaults(mut self) -> Self { 198 | const EPSILON: f64 = 1e-6; 199 | let y_is_default = (self.y - 0.9).abs() < EPSILON; 200 | 201 | if y_is_default { 202 | self.y = -0.15; 203 | } 204 | 205 | self 206 | } 207 | 208 | /// Apply default positioning for y-axis titles (x=-0.08, y=0.5 - left side, vertically centered) 209 | pub(crate) fn with_y_title_defaults(mut self) -> Self { 210 | const EPSILON: f64 = 1e-6; 211 | let x_is_default = (self.x - 0.5).abs() < EPSILON; 212 | let y_is_default = (self.y - 0.9).abs() < EPSILON; 213 | 214 | if x_is_default { 215 | self.x = -0.08; 216 | } 217 | 218 | if y_is_default { 219 | self.y = 0.5; 220 | } 221 | 222 | self 223 | } 224 | 225 | /// Apply default positioning for x-axis title annotations 226 | /// Used when user sets custom position and annotation mode is triggered 227 | /// Ensures unset coordinates get appropriate axis defaults 228 | pub(crate) fn with_x_title_defaults_for_annotation(mut self) -> Self { 229 | const EPSILON: f64 = 1e-6; 230 | let x_is_default = (self.x - 0.5).abs() < EPSILON; 231 | let y_is_default = (self.y - 0.9).abs() < EPSILON; 232 | 233 | if x_is_default { 234 | self.x = 0.5; 235 | } 236 | 237 | if y_is_default { 238 | self.y = -0.15; 239 | } 240 | 241 | self 242 | } 243 | 244 | /// Apply default positioning for y-axis title annotations 245 | /// Used when user sets custom position and annotation mode is triggered 246 | /// Ensures unset coordinates get appropriate axis defaults 247 | pub(crate) fn with_y_title_defaults_for_annotation(mut self) -> Self { 248 | const EPSILON: f64 = 1e-6; 249 | let x_is_default = (self.x - 0.5).abs() < EPSILON; 250 | let y_is_default = (self.y - 0.9).abs() < EPSILON; 251 | 252 | if x_is_default { 253 | self.x = -0.08; 254 | } 255 | 256 | if y_is_default { 257 | self.y = 0.5; 258 | } 259 | 260 | self 261 | } 262 | 263 | pub(crate) fn to_axis_annotation( 264 | &self, 265 | is_x_axis: bool, 266 | axis_ref: &str, 267 | use_domain: bool, 268 | ) -> Annotation { 269 | let (x_ref, y_ref) = if use_domain { 270 | let axis_num = axis_ref.trim_start_matches(['x', 'y']); 271 | 272 | let x_axis = if axis_num.is_empty() { 273 | "x" 274 | } else { 275 | &format!("x{}", axis_num) 276 | }; 277 | let y_axis = if axis_num.is_empty() { 278 | "y" 279 | } else { 280 | &format!("y{}", axis_num) 281 | }; 282 | 283 | (format!("{} domain", x_axis), format!("{} domain", y_axis)) 284 | } else { 285 | ("paper".to_string(), "paper".to_string()) 286 | }; 287 | 288 | let y_anchor = Anchor::Middle; 289 | let x_anchor = if is_x_axis { 290 | Anchor::Center 291 | } else { 292 | Anchor::Left 293 | }; 294 | 295 | let effective_size = if self.size == 0 { 12 } else { self.size }; 296 | 297 | let mut annotation = Annotation::new() 298 | .text(&self.content) 299 | .font( 300 | Font::new() 301 | .family(self.font.as_str()) 302 | .size(effective_size) 303 | .color(self.color.to_plotly()), 304 | ) 305 | .x_ref(&x_ref) 306 | .y_ref(&y_ref) 307 | .x(self.x) 308 | .y(self.y) 309 | .x_anchor(x_anchor) 310 | .y_anchor(y_anchor) 311 | .show_arrow(false); 312 | 313 | if !is_x_axis { 314 | annotation = annotation.text_angle(-90.0); 315 | } 316 | 317 | annotation 318 | } 319 | } 320 | 321 | impl From<&str> for Text { 322 | fn from(content: &str) -> Self { 323 | Self::from(content.to_string()) 324 | } 325 | } 326 | 327 | impl From for Text { 328 | fn from(content: String) -> Self { 329 | Self::from(content) 330 | } 331 | } 332 | 333 | impl From<&String> for Text { 334 | fn from(content: &String) -> Self { 335 | Self::from(content) 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /src/components/facet.rs: -------------------------------------------------------------------------------- 1 | use crate::components::{Rgb, Text}; 2 | use std::cmp::Ordering; 3 | 4 | /// Controls axis scaling behavior across facets in a faceted plot. 5 | /// 6 | /// This enum determines whether facets share the same axis ranges or have independent scales. 7 | /// The behavior is similar to ggplot2's `scales` parameter in `facet_wrap()`. 8 | #[derive(Clone, Default)] 9 | pub enum FacetScales { 10 | #[default] 11 | Fixed, 12 | Free, 13 | FreeX, 14 | FreeY, 15 | } 16 | 17 | /// A structure representing facet configuration for creating small multiples. 18 | /// 19 | /// The `FacetConfig` struct allows customization of faceted plots including grid layout, 20 | /// scale behavior, spacing, title styling, custom ordering, and highlighting options. 21 | /// Faceting splits data by a categorical variable to create multiple subplots arranged 22 | /// in a grid, making it easy to compare patterns across categories. 23 | /// 24 | /// # Example 25 | /// 26 | /// ```rust 27 | /// use plotlars::{SurfacePlot, FacetConfig, Plot, Palette, Text}; 28 | /// use polars::prelude::*; 29 | /// use ndarray::Array; 30 | /// 31 | /// let n: usize = 50; 32 | /// let (x_base, _): (Vec, Option) = 33 | /// Array::linspace(-5., 5., n).into_raw_vec_and_offset(); 34 | /// let (y_base, _): (Vec, Option) = 35 | /// Array::linspace(-5., 5., n).into_raw_vec_and_offset(); 36 | /// 37 | /// let mut x_all = Vec::new(); 38 | /// let mut y_all = Vec::new(); 39 | /// let mut z_all = Vec::new(); 40 | /// let mut category_all = Vec::new(); 41 | /// 42 | /// type SurfaceFunction = Box f64>; 43 | /// let functions: Vec<(&str, SurfaceFunction)> = vec![ 44 | /// ( 45 | /// "Sine Wave", 46 | /// Box::new(|xi: f64, yj: f64| (xi * xi + yj * yj).sqrt().sin()), 47 | /// ), 48 | /// ("Saddle", Box::new(|xi: f64, yj: f64| xi * xi - yj * yj)), 49 | /// ( 50 | /// "Gaussian", 51 | /// Box::new(|xi: f64, yj: f64| (-0.5 * (xi * xi + yj * yj)).exp()), 52 | /// ), 53 | /// ]; 54 | /// 55 | /// for (name, func) in &functions { 56 | /// for &xi in x_base.iter() { 57 | /// for &yj in y_base.iter() { 58 | /// x_all.push(xi); 59 | /// y_all.push(yj); 60 | /// z_all.push(func(xi, yj)); 61 | /// category_all.push(*name); 62 | /// } 63 | /// } 64 | /// } 65 | /// 66 | /// let dataset = df![ 67 | /// "x" => &x_all, 68 | /// "y" => &y_all, 69 | /// "z" => &z_all, 70 | /// "function" => &category_all, 71 | /// ] 72 | /// .unwrap(); 73 | /// 74 | /// SurfacePlot::builder() 75 | /// .data(&dataset) 76 | /// .x("x") 77 | /// .y("y") 78 | /// .z("z") 79 | /// .facet("function") 80 | /// .facet_config(&FacetConfig::new().cols(3).rows(1).h_gap(0.08).v_gap(0.12)) 81 | /// .plot_title( 82 | /// Text::from("3D Mathematical Functions") 83 | /// .font("Arial") 84 | /// .size(20), 85 | /// ) 86 | /// .color_scale(Palette::Viridis) 87 | /// .opacity(0.9) 88 | /// .build() 89 | /// .plot(); 90 | /// ``` 91 | /// 92 | /// ![Example](https://imgur.com/nHdLCAB.png) 93 | #[derive(Clone, Default)] 94 | pub struct FacetConfig { 95 | pub(crate) rows: Option, 96 | pub(crate) cols: Option, 97 | pub(crate) scales: FacetScales, 98 | pub(crate) h_gap: Option, 99 | pub(crate) v_gap: Option, 100 | pub(crate) title_style: Option, 101 | pub(crate) sorter: Option Ordering>, 102 | pub(crate) highlight_facet: bool, 103 | pub(crate) unhighlighted_color: Option, 104 | } 105 | 106 | impl FacetConfig { 107 | /// Creates a new `FacetConfig` instance with default values. 108 | /// 109 | /// By default, the grid dimensions are automatically calculated, scales are fixed 110 | /// across all facets, and highlighting is disabled. 111 | pub fn new() -> Self { 112 | Self::default() 113 | } 114 | 115 | /// Sets the number of rows in the facet grid. 116 | /// 117 | /// When specified, the grid will have exactly this many rows, and the number 118 | /// of columns will be calculated automatically based on the number of facets. If not 119 | /// specified, both dimensions are calculated automatically. 120 | /// 121 | /// # Argument 122 | /// 123 | /// * `rows` - A `usize` value specifying the number of rows (must be greater than 0). 124 | /// 125 | /// # Panics 126 | /// 127 | /// Panics if `rows` is 0. 128 | pub fn rows(mut self, rows: usize) -> Self { 129 | if rows == 0 { 130 | panic!("rows must be greater than 0"); 131 | } 132 | self.rows = Some(rows); 133 | self 134 | } 135 | 136 | /// Sets the number of columns in the facet grid. 137 | /// 138 | /// When specified, the grid will have exactly this many columns, and the number 139 | /// of rows will be calculated automatically based on the number of facets. If not 140 | /// specified, both dimensions are calculated automatically. 141 | /// 142 | /// # Argument 143 | /// 144 | /// * `cols` - A `usize` value specifying the number of columns (must be greater than 0). 145 | /// 146 | /// # Panics 147 | /// 148 | /// Panics if `cols` is 0. 149 | pub fn cols(mut self, cols: usize) -> Self { 150 | if cols == 0 { 151 | panic!("cols must be greater than 0"); 152 | } 153 | self.cols = Some(cols); 154 | self 155 | } 156 | 157 | /// Sets the axis scale behavior across facets. 158 | /// 159 | /// Controls whether facets share the same axis ranges (`Fixed`) or have independent 160 | /// scales (`Free`, `FreeX`, or `FreeY`). Fixed scales make it easier to compare values 161 | /// across facets, while free scales allow each facet to use its optimal range. 162 | /// 163 | /// # Argument 164 | /// 165 | /// * `scales` - A `FacetScales` enum value specifying the scale behavior. 166 | pub fn scales(mut self, scales: FacetScales) -> Self { 167 | self.scales = scales; 168 | self 169 | } 170 | 171 | /// Sets the horizontal spacing between columns. 172 | /// 173 | /// The gap is specified as a proportion of the plot width, with typical values 174 | /// ranging from 0.0 (no gap) to 0.2 (20% gap). If not specified, plotly's default 175 | /// spacing is used. 176 | /// 177 | /// # Argument 178 | /// 179 | /// * `gap` - A `f64` value from 0.0 to 1.0 representing the relative gap size. 180 | /// 181 | /// # Panics 182 | /// 183 | /// Panics if `gap` is negative, NaN, or infinite. 184 | pub fn h_gap(mut self, gap: f64) -> Self { 185 | if !gap.is_finite() || gap < 0.0 { 186 | panic!("h_gap must be a finite non-negative number"); 187 | } 188 | self.h_gap = Some(gap); 189 | self 190 | } 191 | 192 | /// Sets the vertical spacing between rows. 193 | /// 194 | /// The gap is specified as a proportion of the plot height, with typical values 195 | /// ranging from 0.0 (no gap) to 0.2 (20% gap). If not specified, plotly's default 196 | /// spacing is used. 197 | /// 198 | /// # Argument 199 | /// 200 | /// * `gap` - A `f64` value from 0.0 to 1.0 representing the relative gap size. 201 | /// 202 | /// # Panics 203 | /// 204 | /// Panics if `gap` is negative, NaN, or infinite. 205 | pub fn v_gap(mut self, gap: f64) -> Self { 206 | if !gap.is_finite() || gap < 0.0 { 207 | panic!("v_gap must be a finite non-negative number"); 208 | } 209 | self.v_gap = Some(gap); 210 | self 211 | } 212 | 213 | /// Sets the styling for facet labels. 214 | /// 215 | /// Controls the font, size, and color of the category labels that appear above each 216 | /// facet. If not specified, plotly's default text styling is used. 217 | /// 218 | /// # Argument 219 | /// 220 | /// * `style` - A `Text` component or any type that can be converted into `Text`, 221 | /// specifying the styling options for facet titles. 222 | pub fn title_style>(mut self, style: T) -> Self { 223 | self.title_style = Some(style.into()); 224 | self 225 | } 226 | 227 | /// Sets a custom sorting function for facet order. 228 | /// 229 | /// By default, facets are ordered alphabetically by category name. This method allows 230 | /// you to specify a custom comparison function to control the order in which facets 231 | /// appear in the grid. 232 | /// 233 | /// # Argument 234 | /// 235 | /// * `f` - A function that takes two string slices and returns an `Ordering`, 236 | /// following the same signature as `str::cmp`. 237 | /// 238 | /// # Example 239 | /// 240 | /// ```rust 241 | /// use plotlars::FacetConfig; 242 | /// use std::cmp::Ordering; 243 | /// 244 | /// // Reverse alphabetical order 245 | /// let config = FacetConfig::new() 246 | /// .sorter(|a, b| b.cmp(a)); 247 | /// ``` 248 | pub fn sorter(mut self, f: fn(&str, &str) -> Ordering) -> Self { 249 | self.sorter = Some(f); 250 | self 251 | } 252 | 253 | /// Enables or disables facet highlighting mode. 254 | /// 255 | /// When enabled, each facet shows all data from all categories, but emphasizes 256 | /// the data for the current facet's category while displaying other categories 257 | /// in a muted color. This provides visual context by showing the full data 258 | /// distribution while focusing attention on the current facet. 259 | /// 260 | /// # Argument 261 | /// 262 | /// * `highlight` - A boolean value: `true` to enable highlighting, `false` to disable. 263 | pub fn highlight_facet(mut self, highlight: bool) -> Self { 264 | self.highlight_facet = highlight; 265 | self 266 | } 267 | 268 | /// Sets the color for unhighlighted data points in highlighting mode. 269 | /// 270 | /// This setting only takes effect when `highlight_facet` is enabled. It specifies 271 | /// the color used for data points that belong to other categories (not the current 272 | /// facet's category). If not specified, a default grey color is used. 273 | /// 274 | /// # Argument 275 | /// 276 | /// * `color` - An `Rgb` value specifying the color for unhighlighted data. 277 | pub fn unhighlighted_color(mut self, color: Rgb) -> Self { 278 | self.unhighlighted_color = Some(color); 279 | self 280 | } 281 | } 282 | --------------------------------------------------------------------------------