diff --git a/CHANGELOG.md b/CHANGELOG.md index e0d69dec..397f35ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,15 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - [[#281]((https://github.com/plotly/plotly.rs/pull/xxx))] Update to askama 0.13.0 - [[#287]](https://github.com/plotly/plotly.rs/pull/287) Added functionality for callbacks (using wasm) - [[#289]](https://github.com/plotly/plotly.rs/pull/289) Fixes Kaleido static export for MacOS targets by removing `--disable-gpu` flag for MacOS -- [[#290]](https://github.com/plotly/plotly.rs/pull/289) Remove `--disable-gpu` flag for Kaleido static-image generation for all targets. +- [[#291]](https://github.com/plotly/plotly.rs/pull/291) Remove `--disable-gpu` flag for Kaleido static-image generation for all targets. +- [[#299]](https://github.com/plotly/plotly.rs/pull/299) Added customdata field to HeatMap +- [[#303]](https://github.com/plotly/plotly.rs/pull/303) Split layout mod.rs into modules +- [[#304]](https://github.com/plotly/plotly.rs/pull/304) Refactored examples to allow fo generation of full html files ### Fixed - [[#284](https://github.com/plotly/plotly.rs/pull/284)] Allow plotly package to be compiled for android +- [[#298](https://github.com/plotly/plotly.rs/pull/298)] Added support for layout axis scaleratio +- [[#301](https://github.com/plotly/plotly.rs/pull/301)] Added ScatterGeo trace and LayoutGeo support ## [0.12.1] - 2025-01-02 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b35e4fbd..3b37a10f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing to Ploty.rs +# Contributing to Plotly.rs Contribution in the form of suggestions, bug reports, pull requests and feedback is welcome from everyone. In this document you'll find guidance if you are considering to offer your help to this project. diff --git a/examples/maps/Cargo.toml b/examples/maps/Cargo.toml index 6d3bf304..709423cc 100644 --- a/examples/maps/Cargo.toml +++ b/examples/maps/Cargo.toml @@ -6,3 +6,6 @@ edition = "2021" [dependencies] plotly = { path = "../../plotly" } +csv = "1.3" +reqwest = { version = "0.11", features = ["blocking"] } + diff --git a/examples/maps/src/main.rs b/examples/maps/src/main.rs index 7da047ec..c41ab860 100644 --- a/examples/maps/src/main.rs +++ b/examples/maps/src/main.rs @@ -1,9 +1,12 @@ #![allow(dead_code)] use plotly::{ - common::Marker, - layout::{Center, DragMode, Mapbox, MapboxStyle, Margin}, - DensityMapbox, Layout, Plot, ScatterMapbox, + color::Rgb, + common::{Line, Marker, Mode}, + layout::{ + Axis, Center, DragMode, LayoutGeo, Mapbox, MapboxStyle, Margin, Projection, Rotation, + }, + DensityMapbox, Layout, Plot, ScatterGeo, ScatterMapbox, }; fn scatter_mapbox(show: bool, file_name: &str) { @@ -30,6 +33,109 @@ fn scatter_mapbox(show: bool, file_name: &str) { } } +/// Reproduce the Earth from https://plotly.com/javascript/lines-on-maps/#lines-on-an-orthographic-map +fn scatter_geo(show: bool, file_name: &str) { + use csv; + use reqwest; + + // Download and parse the CSV + let url = "https://raw.githubusercontent.com/plotly/datasets/master/globe_contours.csv"; + let req = reqwest::blocking::get(url).unwrap().text().unwrap(); + let mut rdr = csv::Reader::from_reader(req.as_bytes()); + let headers = rdr.headers().unwrap().clone(); + let mut rows = vec![]; + for result in rdr.records() { + let record = result.unwrap(); + rows.push(record); + } + + // Color scale + let scl = [ + "rgb(213,62,79)", + "rgb(244,109,67)", + "rgb(253,174,97)", + "rgb(254,224,139)", + "rgb(255,255,191)", + "rgb(230,245,152)", + "rgb(171,221,164)", + "rgb(102,194,165)", + "rgb(50,136,189)", + ]; + + // Unpack lat/lon columns + let mut all_lats: Vec> = vec![]; + let mut all_lons: Vec> = vec![]; + for i in 0..scl.len() { + let lat_head = format!("lat-{}", i + 1); + let lon_head = format!("lon-{}", i + 1); + let lat: Vec = rows + .iter() + .map(|row| { + row.get(headers.iter().position(|h| h == lat_head).unwrap()) + .unwrap() + .parse() + .unwrap_or(f64::NAN) + }) + .collect(); + let lon: Vec = rows + .iter() + .map(|row| { + row.get(headers.iter().position(|h| h == lon_head).unwrap()) + .unwrap() + .parse() + .unwrap_or(f64::NAN) + }) + .collect(); + all_lats.push(lat); + all_lons.push(lon); + } + + // Build traces + let mut plot = Plot::new(); + for i in 0..scl.len() { + let trace = ScatterGeo::new(all_lats[i].clone(), all_lons[i].clone()) + .mode(Mode::Lines) + .line(Line::new().width(2.0).color(scl[i])); + plot.add_trace(trace); + } + + let layout = Layout::new() + .drag_mode(DragMode::Zoom) + .margin(Margin::new().top(0).left(0).bottom(0).right(0)) + .geo( + LayoutGeo::new() + .showocean(true) + .showlakes(true) + .showcountries(true) + .showland(true) + .oceancolor(Rgb::new(0, 255, 255)) + .lakecolor(Rgb::new(0, 255, 255)) + .landcolor(Rgb::new(230, 145, 56)) + .lataxis( + Axis::new() + .show_grid(true) + .grid_color(Rgb::new(102, 102, 102)), + ) + .lonaxis( + Axis::new() + .show_grid(true) + .grid_color(Rgb::new(102, 102, 102)), + ) + .projection( + Projection::new() + .projection_type(plotly::layout::ProjectionType::Orthographic) + .rotation(Rotation::new().lon(-100.0).lat(40.0)), + ), + ); + + plot.set_layout(layout); + + let path = write_example_to_html(&plot, file_name); + if show { + plot.show_html(path); + } +} + fn density_mapbox(show: bool, file_name: &str) { let trace = DensityMapbox::new(vec![45.5017], vec![-73.5673], vec![0.75]).zauto(true); @@ -63,5 +169,6 @@ fn write_example_to_html(plot: &Plot, name: &str) -> String { fn main() { // Change false to true on any of these lines to display the example. scatter_mapbox(false, "scatter_mapbox"); + scatter_geo(false, "scatter_geo"); density_mapbox(false, "density_mapbox"); } diff --git a/plotly/src/common/mod.rs b/plotly/src/common/mod.rs index 7816a41a..96a5f8cd 100644 --- a/plotly/src/common/mod.rs +++ b/plotly/src/common/mod.rs @@ -215,6 +215,7 @@ pub enum PlotType { ScatterGL, Scatter3D, ScatterMapbox, + ScatterGeo, ScatterPolar, ScatterPolarGL, Bar, @@ -1750,6 +1751,7 @@ mod tests { assert_eq!(to_value(PlotType::Scatter).unwrap(), json!("scatter")); assert_eq!(to_value(PlotType::ScatterGL).unwrap(), json!("scattergl")); assert_eq!(to_value(PlotType::Scatter3D).unwrap(), json!("scatter3d")); + assert_eq!(to_value(PlotType::ScatterGeo).unwrap(), json!("scattergeo")); assert_eq!(to_value(PlotType::ScatterPolar).unwrap(), json!("scatterpolar")); assert_eq!(to_value(PlotType::ScatterPolarGL).unwrap(), json!("scatterpolargl")); assert_eq!(to_value(PlotType::Bar).unwrap(), json!("bar")); diff --git a/plotly/src/layout/geo.rs b/plotly/src/layout/geo.rs new file mode 100644 index 00000000..03a31d93 --- /dev/null +++ b/plotly/src/layout/geo.rs @@ -0,0 +1,95 @@ +use plotly_derive::FieldSetter; +use serde::Serialize; + +use crate::color::Color; +use crate::layout::{Axis, Center, Projection}; + +#[derive(Serialize, Clone, Debug, FieldSetter)] + +pub struct LayoutGeo { + /// Sets the latitude and longitude of the center of the map. + center: Option
, + /// Sets the domain within which the mapbox will be drawn. + /// Sets the zoom level of the map. + zoom: Option, + /// Sets the projection of the map + #[field_setter(default = "Projection::new().projection_type(ProjectionType::Orthographic)")] + projection: Option, + /// If to show the ocean or not + #[field_setter(default = "Some(true)")] + showocean: Option, + /// Sets the color of the ocean + #[field_setter(default = "'rgb(0, 255, 255)'")] + oceancolor: Option>, + /// If to show the land or not + showland: Option, + /// Sets the color of the land + landcolor: Option>, + /// If to show lakes or not + showlakes: Option, + /// Sets the color of the lakes + lakecolor: Option>, + /// If to show countries (borders) or not + showcountries: Option, + /// Configures the longitude axis + lonaxis: Option, + /// Configures the latitude axis + lataxis: Option, + // Sets the coastline stroke width (in px). + #[field_setter(default = "Some(1)")] + coastlinewidth: Option, +} + +impl LayoutGeo { + pub fn new() -> Self { + Default::default() + } +} + +#[cfg(test)] +mod tests { + use serde_json::{json, to_value}; + + use super::*; + use crate::color::Rgb; + use crate::layout::{Axis, Center, Projection, ProjectionType, Rotation}; + + #[test] + fn serialize_layout_geo() { + let geo = LayoutGeo::new() + .center(Center::new(10.0, 20.0)) + .zoom(5) + .projection( + Projection::new() + .projection_type(ProjectionType::Mercator) + .rotation(Rotation::new().lat(1.0).lon(2.0).roll(4.0)), + ) + .showocean(true) + .oceancolor(Rgb::new(0, 255, 255)) + .showland(true) + .landcolor(Rgb::new(100, 200, 100)) + .showlakes(false) + .lakecolor(Rgb::new(50, 50, 200)) + .showcountries(true) + .lonaxis(Axis::new().title("Longitude")) + .lataxis(Axis::new().title("Latitude")) + .coastlinewidth(2); + + let expected = json!({ + "center": {"lat": 10.0, "lon": 20.0}, + "zoom": 5, + "projection": {"type": "mercator", "rotation": {"lat": 1.0, "lon": 2.0, "roll": 4.0}}, + "showocean": true, + "oceancolor": "rgb(0, 255, 255)", + "showland": true, + "landcolor": "rgb(100, 200, 100)", + "showlakes": false, + "lakecolor": "rgb(50, 50, 200)", + "showcountries": true, + "lataxis": { "title": { "text": "Latitude" } }, + "lonaxis": { "title": { "text": "Longitude" } }, + "coastlinewidth": 2 + }); + assert_eq!(to_value(geo).unwrap(), expected); + } +} diff --git a/plotly/src/layout/mod.rs b/plotly/src/layout/mod.rs index ddf3cf82..197bc80a 100644 --- a/plotly/src/layout/mod.rs +++ b/plotly/src/layout/mod.rs @@ -13,6 +13,7 @@ pub mod update_menu; mod annotation; mod axis; +mod geo; mod grid; mod legend; mod mapbox; @@ -27,6 +28,7 @@ pub use self::axis::{ RangeMode, RangeSelector, RangeSlider, RangeSliderYAxis, SelectorButton, SelectorStep, SliderRangeMode, StepMode, TicksDirection, TicksPosition, }; +pub use self::geo::LayoutGeo; pub use self::grid::{GridDomain, GridPattern, GridXSide, GridYSide, LayoutGrid, RowOrder}; pub use self::legend::{Legend, TraceOrder}; pub use self::mapbox::{Center, Mapbox, MapboxStyle}; @@ -35,6 +37,7 @@ pub use self::modes::{ }; pub use self::scene::{ Camera, CameraCenter, DragMode, DragMode3D, HoverMode, LayoutScene, Projection, ProjectionType, + Rotation, }; pub use self::shape::{ ActiveShape, DrawDirection, FillRule, NewShape, Shape, ShapeLayer, ShapeLine, ShapeSizeMode, @@ -274,7 +277,10 @@ pub struct LayoutFields { y_axis8: Option>, #[serde(rename = "zaxis8")] z_axis8: Option>, + // ternary: Option, scene: Option, + geo: Option, + // polar: Option, annotations: Option>, shapes: Option>, #[serde(rename = "newshape")] diff --git a/plotly/src/layout/scene.rs b/plotly/src/layout/scene.rs index f1737008..cd721e39 100644 --- a/plotly/src/layout/scene.rs +++ b/plotly/src/layout/scene.rs @@ -156,22 +156,210 @@ pub enum ProjectionType { Perspective, #[serde(rename = "orthographic")] Orthographic, + #[serde(rename = "airy")] + Airy, + #[serde(rename = "aitoff")] + Aitoff, + #[serde(rename = "albers")] + Albers, + #[serde(rename = "albers usa")] + AlbersUsa, + #[serde(rename = "august")] + August, + #[serde(rename = "azimuthal equal area")] + AzimuthalEqualArea, + #[serde(rename = "azimuthal equidistant")] + AzimuthalEquidistant, + #[serde(rename = "baker")] + Baker, + #[serde(rename = "bertin1953")] + Bertin1953, + #[serde(rename = "boggs")] + Boggs, + #[serde(rename = "bonne")] + Bonne, + #[serde(rename = "bottomley")] + Bottomley, + #[serde(rename = "bromley")] + Bromley, + #[serde(rename = "collignon")] + Collignon, + #[serde(rename = "conic conformal")] + ConicConformal, + #[serde(rename = "conic equal area")] + ConicEqualArea, + #[serde(rename = "conic equidistant")] + ConicEquidistant, + #[serde(rename = "craig")] + Craig, + #[serde(rename = "craster")] + Craster, + #[serde(rename = "cylindrical equal area")] + CylindricalEqualArea, + #[serde(rename = "cylindrical stereographic")] + CylindricalStereographic, + #[serde(rename = "eckert1")] + Eckert1, + #[serde(rename = "eckert2")] + Eckert2, + #[serde(rename = "eckert3")] + Eckert3, + #[serde(rename = "eckert4")] + Eckert4, + #[serde(rename = "eckert5")] + Eckert5, + #[serde(rename = "eckert6")] + Eckert6, + #[serde(rename = "eisenlohr")] + Eisenlohr, + #[serde(rename = "equal earth")] + EqualEarth, + #[serde(rename = "equirectangular")] + Equirectangular, + #[serde(rename = "fahey")] + Fahey, + #[serde(rename = "foucaut")] + Foucaut, + #[serde(rename = "foucaut sinusoidal")] + FoucautSinusoidal, + #[serde(rename = "ginzburg4")] + Ginzburg4, + #[serde(rename = "ginzburg5")] + Ginzburg5, + #[serde(rename = "ginzburg6")] + Ginzburg6, + #[serde(rename = "ginzburg8")] + Ginzburg8, + #[serde(rename = "ginzburg9")] + Ginzburg9, + #[serde(rename = "gnomonic")] + Gnomonic, + #[serde(rename = "gringorten")] + Gringorten, + #[serde(rename = "gringorten quincuncial")] + GringortenQuincuncial, + #[serde(rename = "guyou")] + Guyou, + #[serde(rename = "hammer")] + Hammer, + #[serde(rename = "hill")] + Hill, + #[serde(rename = "homolosine")] + Homolosine, + #[serde(rename = "hufnagel")] + Hufnagel, + #[serde(rename = "hyperelliptical")] + Hyperelliptical, + #[serde(rename = "kavrayskiy7")] + Kavrayskiy7, + #[serde(rename = "lagrange")] + Lagrange, + #[serde(rename = "larrivee")] + Larrivee, + #[serde(rename = "laskowski")] + Laskowski, + #[serde(rename = "loximuthal")] + Loximuthal, + #[serde(rename = "mercator")] + Mercator, + #[serde(rename = "miller")] + Miller, + #[serde(rename = "mollweide")] + Mollweide, + #[serde(rename = "mt flat polar parabolic")] + MtFlatPolarParabolic, + #[serde(rename = "mt flat polar quartic")] + MtFlatPolarQuartic, + #[serde(rename = "mt flat polar sinusoidal")] + MtFlatPolarSinusoidal, + #[serde(rename = "natural earth")] + NaturalEarth, + #[serde(rename = "natural earth1")] + NaturalEarth1, + #[serde(rename = "natural earth2")] + NaturalEarth2, + #[serde(rename = "nell hammer")] + NellHammer, + #[serde(rename = "nicolosi")] + Nicolosi, + #[serde(rename = "patterson")] + Patterson, + #[serde(rename = "peirce quincuncial")] + PeirceQuincuncial, + #[serde(rename = "polyconic")] + Polyconic, + #[serde(rename = "rectangular polyconic")] + RectangularPolyconic, + #[serde(rename = "robinson")] + Robinson, + #[serde(rename = "satellite")] + Satellite, + #[serde(rename = "sinu mollweide")] + SinuMollweide, + #[serde(rename = "sinusoidal")] + Sinusoidal, + #[serde(rename = "stereographic")] + Stereographic, + #[serde(rename = "times")] + Times, + #[serde(rename = "transverse mercator")] + TransverseMercator, + #[serde(rename = "van der grinten")] + VanDerGrinten, + #[serde(rename = "van der grinten2")] + VanDerGrinten2, + #[serde(rename = "van der grinten3")] + VanDerGrinten3, + #[serde(rename = "van der grinten4")] + VanDerGrinten4, + #[serde(rename = "wagner4")] + Wagner4, + #[serde(rename = "wagner6")] + Wagner6, + #[serde(rename = "wiechel")] + Wiechel, + #[serde(rename = "winkel tripel")] + WinkelTripel, + #[serde(rename = "winkel3")] + Winkel3, } impl From for Projection { fn from(projection_type: ProjectionType) -> Self { Projection { projection_type: Some(projection_type), + rotation: None, } } } +/// Sets the rotation of the map projection. +#[derive(Serialize, Clone, Debug, FieldSetter)] +pub struct Rotation { + /// Rotates the map along meridians (in degrees North). + lat: Option, + /// Rotates the map along parallels (in degrees East). + lon: Option, + /// Roll the map (in degrees). For example, a roll of "180" makes the map + /// appear upside down. + roll: Option, +} + +impl Rotation { + pub fn new() -> Self { + Default::default() + } +} + #[serde_with::skip_serializing_none] #[derive(Serialize, Debug, Clone, FieldSetter)] /// Container for Projection options. pub struct Projection { #[serde(rename = "type")] projection_type: Option, + /// Sets the rotation of the map projection. See https://plotly.com/python/reference/layout/geo/#layout-geo-projection-rotation + #[serde(rename = "rotation")] + rotation: Option, } impl Projection { @@ -425,24 +613,129 @@ mod tests { #[test] fn serialize_projection() { let projection = Projection::new().projection_type(ProjectionType::default()); - - let expected = json!({ - "type": "perspective", - }); - - assert_eq!(to_value(projection).unwrap(), expected); - - let projection = Projection::new().projection_type(ProjectionType::Orthographic); - - let expected = json!({ - "type": "orthographic", - }); - - assert_eq!(to_value(projection).unwrap(), expected); + assert_eq!( + to_value(projection).unwrap(), + json!({"type": "perspective"}) + ); let projection: Projection = ProjectionType::Orthographic.into(); + assert_eq!( + to_value(projection).unwrap(), + json!({ "type": "orthographic" }) + ); - assert_eq!(to_value(projection).unwrap(), expected); + let projections = vec![ + (ProjectionType::Orthographic, "orthographic"), + (ProjectionType::Perspective, "perspective"), + (ProjectionType::Airy, "airy"), + (ProjectionType::Aitoff, "aitoff"), + (ProjectionType::Albers, "albers"), + (ProjectionType::AlbersUsa, "albers usa"), + (ProjectionType::August, "august"), + (ProjectionType::AzimuthalEqualArea, "azimuthal equal area"), + ( + ProjectionType::AzimuthalEquidistant, + "azimuthal equidistant", + ), + (ProjectionType::Baker, "baker"), + (ProjectionType::Bertin1953, "bertin1953"), + (ProjectionType::Boggs, "boggs"), + (ProjectionType::Bonne, "bonne"), + (ProjectionType::Bottomley, "bottomley"), + (ProjectionType::Bromley, "bromley"), + (ProjectionType::Collignon, "collignon"), + (ProjectionType::ConicConformal, "conic conformal"), + (ProjectionType::ConicEqualArea, "conic equal area"), + (ProjectionType::ConicEquidistant, "conic equidistant"), + (ProjectionType::Craig, "craig"), + (ProjectionType::Craster, "craster"), + ( + ProjectionType::CylindricalEqualArea, + "cylindrical equal area", + ), + ( + ProjectionType::CylindricalStereographic, + "cylindrical stereographic", + ), + (ProjectionType::Eckert1, "eckert1"), + (ProjectionType::Eckert2, "eckert2"), + (ProjectionType::Eckert3, "eckert3"), + (ProjectionType::Eckert4, "eckert4"), + (ProjectionType::Eckert5, "eckert5"), + (ProjectionType::Eckert6, "eckert6"), + (ProjectionType::Eisenlohr, "eisenlohr"), + (ProjectionType::EqualEarth, "equal earth"), + (ProjectionType::Equirectangular, "equirectangular"), + (ProjectionType::Fahey, "fahey"), + (ProjectionType::Foucaut, "foucaut"), + (ProjectionType::FoucautSinusoidal, "foucaut sinusoidal"), + (ProjectionType::Ginzburg4, "ginzburg4"), + (ProjectionType::Ginzburg5, "ginzburg5"), + (ProjectionType::Ginzburg6, "ginzburg6"), + (ProjectionType::Ginzburg8, "ginzburg8"), + (ProjectionType::Ginzburg9, "ginzburg9"), + (ProjectionType::Gnomonic, "gnomonic"), + (ProjectionType::Gringorten, "gringorten"), + ( + ProjectionType::GringortenQuincuncial, + "gringorten quincuncial", + ), + (ProjectionType::Guyou, "guyou"), + (ProjectionType::Hammer, "hammer"), + (ProjectionType::Hill, "hill"), + (ProjectionType::Homolosine, "homolosine"), + (ProjectionType::Hufnagel, "hufnagel"), + (ProjectionType::Hyperelliptical, "hyperelliptical"), + (ProjectionType::Kavrayskiy7, "kavrayskiy7"), + (ProjectionType::Lagrange, "lagrange"), + (ProjectionType::Larrivee, "larrivee"), + (ProjectionType::Laskowski, "laskowski"), + (ProjectionType::Loximuthal, "loximuthal"), + (ProjectionType::Mercator, "mercator"), + (ProjectionType::Miller, "miller"), + (ProjectionType::Mollweide, "mollweide"), + ( + ProjectionType::MtFlatPolarParabolic, + "mt flat polar parabolic", + ), + (ProjectionType::MtFlatPolarQuartic, "mt flat polar quartic"), + ( + ProjectionType::MtFlatPolarSinusoidal, + "mt flat polar sinusoidal", + ), + (ProjectionType::NaturalEarth, "natural earth"), + (ProjectionType::NaturalEarth1, "natural earth1"), + (ProjectionType::NaturalEarth2, "natural earth2"), + (ProjectionType::NellHammer, "nell hammer"), + (ProjectionType::Nicolosi, "nicolosi"), + (ProjectionType::Patterson, "patterson"), + (ProjectionType::PeirceQuincuncial, "peirce quincuncial"), + (ProjectionType::Polyconic, "polyconic"), + ( + ProjectionType::RectangularPolyconic, + "rectangular polyconic", + ), + (ProjectionType::Robinson, "robinson"), + (ProjectionType::Satellite, "satellite"), + (ProjectionType::SinuMollweide, "sinu mollweide"), + (ProjectionType::Sinusoidal, "sinusoidal"), + (ProjectionType::Stereographic, "stereographic"), + (ProjectionType::Times, "times"), + (ProjectionType::TransverseMercator, "transverse mercator"), + (ProjectionType::VanDerGrinten, "van der grinten"), + (ProjectionType::VanDerGrinten2, "van der grinten2"), + (ProjectionType::VanDerGrinten3, "van der grinten3"), + (ProjectionType::VanDerGrinten4, "van der grinten4"), + (ProjectionType::Wagner4, "wagner4"), + (ProjectionType::Wagner6, "wagner6"), + (ProjectionType::Wiechel, "wiechel"), + (ProjectionType::WinkelTripel, "winkel tripel"), + (ProjectionType::Winkel3, "winkel3"), + ]; + for (variant, name) in projections { + let projection = Projection::new().projection_type(variant.clone()); + assert_eq!(to_value(projection).unwrap(), json!({"type": name})); + } } #[test] @@ -498,4 +791,21 @@ mod tests { assert_eq!(to_value(up).unwrap(), expected); } + + #[test] + fn serialize_projection_with_rotation() { + let projection = Projection { + projection_type: Some(ProjectionType::Mercator), + rotation: Some(Rotation { + lat: Some(10.0), + lon: Some(20.0), + roll: Some(30.0), + }), + }; + let expected = json!({ + "type": "mercator", + "rotation": {"lat": 10.0, "lon": 20.0, "roll": 30.0} + }); + assert_eq!(to_value(projection).unwrap(), expected); + } } diff --git a/plotly/src/lib.rs b/plotly/src/lib.rs index e22a1482..8d112b23 100644 --- a/plotly/src/lib.rs +++ b/plotly/src/lib.rs @@ -40,7 +40,7 @@ pub use traces::{ // Bring the different trace types into the top-level scope pub use traces::{ Bar, BoxPlot, Candlestick, Contour, DensityMapbox, HeatMap, Histogram, Image, Mesh3D, Ohlc, - Pie, Sankey, Scatter, Scatter3D, ScatterMapbox, ScatterPolar, Surface, Table, + Pie, Sankey, Scatter, Scatter3D, ScatterGeo, ScatterMapbox, ScatterPolar, Surface, Table, }; pub trait Restyle: serde::Serialize {} diff --git a/plotly/src/traces/mod.rs b/plotly/src/traces/mod.rs index 2a2d3042..85b0a294 100644 --- a/plotly/src/traces/mod.rs +++ b/plotly/src/traces/mod.rs @@ -14,6 +14,7 @@ pub mod pie; pub mod sankey; pub mod scatter; pub mod scatter3d; +pub mod scatter_geo; pub mod scatter_mapbox; mod scatter_polar; pub mod surface; @@ -32,6 +33,7 @@ pub use pie::Pie; pub use sankey::Sankey; pub use scatter::Scatter; pub use scatter3d::Scatter3D; +pub use scatter_geo::ScatterGeo; pub use scatter_mapbox::ScatterMapbox; pub use scatter_polar::ScatterPolar; pub use surface::Surface; diff --git a/plotly/src/traces/scatter_geo.rs b/plotly/src/traces/scatter_geo.rs new file mode 100644 index 00000000..6445a3cb --- /dev/null +++ b/plotly/src/traces/scatter_geo.rs @@ -0,0 +1,382 @@ +//! Geo scatter plot + +use plotly_derive::FieldSetter; +use serde::Serialize; + +use crate::common::{ + color::Color, Dim, Font, HoverInfo, Label, LegendGroupTitle, Line, Marker, Mode, PlotType, + Position, Visible, +}; +use crate::private::{NumOrString, NumOrStringCollection}; +use crate::Trace; + +#[derive(Serialize, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum Fill { + None, + ToSelf, +} + +#[serde_with::skip_serializing_none] +#[derive(Serialize, Clone, Debug, Default)] +pub struct SelectionMarker { + color: Option>, + opacity: Option, + size: Option>, +} + +#[derive(Serialize, Clone, Debug, Default)] +pub struct Selection { + marker: SelectionMarker, +} + +impl Selection { + pub fn new() -> Self { + Default::default() + } + + /// Sets the marker color of un/selected points. + pub fn color(mut self, color: C) -> Self { + self.marker.color = Some(Box::new(color)); + self + } + + /// Sets the marker opacity of un/selected points. + pub fn opacity(mut self, opacity: f64) -> Self { + self.marker.opacity = Some(opacity); + self + } + + /// Sets the marker size of un/selected points. + pub fn size(mut self, size: usize) -> Self { + self.marker.size = Some(Dim::Scalar(size)); + self + } +} + +#[serde_with::skip_serializing_none] +#[derive(Serialize, Clone, Debug, FieldSetter)] +#[field_setter(box_self, kind = "trace")] +pub struct ScatterGeo +where + Lat: Serialize + Clone, + Lon: Serialize + Clone, +{ + #[field_setter(default = "PlotType::ScatterGeo")] + r#type: PlotType, + /// Sets the trace name. The trace name appear as the legend item and on + /// hover. + name: Option, + /// Determines whether or not this trace is visible. If + /// `Visible::LegendOnly`, the trace is not drawn, but can appear as a + /// legend item (provided that the legend itself is visible). + visible: Option, + + /// Determines whether or not an item corresponding to this trace is shown + /// in the legend. + #[serde(rename = "showlegend")] + show_legend: Option, + /// Sets the legend rank for this trace. Items and groups with smaller ranks + /// are presented on top/left side while with `"reversed" + /// `legend.trace_order` they are on bottom/right side. The default + /// legendrank is 1000, so that you can use ranks less than 1000 to + /// place certain items before all unranked items, and ranks greater + /// than 1000 to go after all unranked items. + #[serde(rename = "legendrank")] + legend_rank: Option, + /// Sets the legend group for this trace. Traces part of the same legend + /// group show/hide at the same time when toggling legend items. + #[serde(rename = "legendgroup")] + legend_group: Option, + /// Set and style the title to appear for the legend group. + #[serde(rename = "legendgrouptitle")] + legend_group_title: Option, + + /// Sets the opacity of the trace. + opacity: Option, + /// Determines the drawing mode for this scatter trace. If the provided + /// `Mode` includes "Text" then the `text` elements appear at the + /// coordinates. Otherwise, the `text` elements appear on hover. If + /// there are less than 20 points and the trace is not stacked then the + /// default is `Mode::LinesMarkers`, otherwise it is `Mode::Lines`. + mode: Option, + /// Assigns id labels to each datum. These ids for object constancy of data + /// points during animation. Should be an array of strings, not numbers + /// or any other type. + ids: Option>, + + lat: Option>, + lon: Option>, + + /// Sets text elements associated with each (x,y) pair. If a single string, + /// the same string appears over all the data points. If an array of + /// strings, the items are mapped in order to the this trace's (x,y) + /// coordinates. If the trace `HoverInfo` contains a "text" flag and + /// `hover_text` is not set, these elements will be seen in the hover + /// labels. + text: Option>, + /// Sets the positions of the `text` elements with respects to the (x,y) + /// coordinates. + #[serde(rename = "textposition")] + text_position: Option>, + /// Template string used for rendering the information text that appear on + /// points. Note that this will override `textinfo`. Variables are + /// inserted using %{variable}, for example "y: %{y}". Numbers are + /// formatted using d3-format's syntax %{variable:d3-format}, for example "Price: %{y:$.2f}". See [format](https://github.com/d3/d3-3.x-api-reference/blob/master/Formatting.md#d3) + /// for details on the formatting syntax. Dates are formatted using + /// d3-time-format's syntax %{variable|d3-time-format}, for example + /// "Day: %{2019-01-01|%A}". See [format](https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Formatting.md#format) for details + /// on the date formatting syntax. Every attributes that can be specified + /// per-point (the ones that are `arrayOk: true`) are available. + #[serde(rename = "texttemplate")] + text_template: Option>, + /// Sets hover text elements associated with each (x,y) pair. If a single + /// string, the same string appears over all the data points. If an + /// array of string, the items are mapped in order to the this trace's + /// (x,y) coordinates. To be seen, trace `HoverInfo` must contain a + /// "Text" flag. + #[serde(rename = "hovertext")] + hover_text: Option>, + /// Determines which trace information appear on hover. If `HoverInfo::None` + /// or `HoverInfo::Skip` are set, no information is displayed upon + /// hovering. But, if `HoverInfo::None` is set, click and hover events + /// are still fired. + #[serde(rename = "hoverinfo")] + hover_info: Option, + /// Template string used for rendering the information that appear on hover + /// box. Note that this will override `HoverInfo`. Variables are + /// inserted using %{variable}, for example "y: %{y}". Numbers are + /// formatted using d3-format's syntax %{variable:d3-format}, for example + /// "Price: %{y:$.2f}". + /// for details + /// on the formatting syntax. Dates are formatted using d3-time-format's + /// syntax %{variable|d3-time-format}, for example "Day: + /// %{2019-01-01|%A}". for details + /// on the date formatting syntax. The variables available in + /// `hovertemplate` are the ones emitted as event data described at this link . + /// Additionally, every attributes that can be specified per-point (the ones + /// that are `arrayOk: true`) are available. Anything contained in tag + /// `` is displayed in the secondary box, for example + /// "{fullData.name}". To hide the secondary box + /// completely, use an empty tag ``. + #[serde(rename = "hovertemplate")] + hover_template: Option>, + + /// Assigns extra meta information associated with this trace that can be + /// used in various text attributes. Attributes such as trace `name`, + /// graph, axis and colorbar `title.text`, annotation `text` + /// `rangeselector`, `updatemenues` and `sliders` `label` text all support + /// `meta`. To access the trace `meta` values in an attribute in the same + /// trace, simply use `%{meta[i]}` where `i` is the index or key of the + /// `meta` item in question. To access trace `meta` in layout + /// attributes, use `%{data[n[.meta[i]}` where `i` is the index or key of + /// the `meta` and `n` is the trace index. + meta: Option, + /// Assigns extra data each datum. This may be useful when listening to + /// hover, click and selection events. Note that, "scatter" traces also + /// appends customdata items in the markers DOM elements. + #[serde(rename = "customdata")] + custom_data: Option, + + /// Sets a reference between this trace's data coordinates and a geo + /// subplot. If "geo" (the default value), the data refer to + /// `layout.geo`. If "geo2", the data refer to `layout.geo2`, and + /// so on. + subplot: Option, + /// Determines how points are displayed and joined. + marker: Option, + + /// Line display properties. + line: Option, + + /// Sets the text font. + #[serde(rename = "textfont")] + text_font: Option, + + /// Vector containing integer indices of selected points. Has an effect only + /// for traces that support selections. Note that an empty vector means + /// an empty selection where the `unselected` are turned on for all + /// points. + #[serde(rename = "selectedpoints")] + selected_points: Option>, + + /// Sets the style of selected points. + selected: Option, + /// Sets the style of unselected points. + unselected: Option, + + /// Determines if this scattergeo trace's layers are to be inserted + /// before the layer with the specified ID. By default, scattergeo + /// layers are inserted above all the base layers. To place the + /// scattergeo layers above every other layer, set `below` to "''". + below: Option, + /// Determines whether or not gaps (i.e. {nan} or missing values) in the + /// provided data arrays are connected. + #[serde(rename = "connectgaps")] + connect_gaps: Option, + + /// Sets the area to fill with a solid color. Defaults to "none" unless this + /// trace is stacked, then it gets "tonexty" ("tonextx") if + /// `orientation` is "v" ("h") Use with `fillcolor` if not + /// "none". "tozerox" and "tozeroy" fill to x=0 and y=0 respectively. + /// "tonextx" and "tonexty" fill between the endpoints of this trace and + /// the endpoints of the trace before it, connecting those endpoints + /// with straight lines (to make a stacked area graph); if there is + /// no trace before it, they behave like "tozerox" and "tozeroy". "toself" + /// connects the endpoints of the trace (or each segment of the trace if + /// it has gaps) into a closed shape. "tonext" fills the space between + /// two traces if one completely encloses the other (eg consecutive + /// contour lines), and behaves like "toself" if there is no trace before + /// it. "tonext" should not be used if one trace does not enclose the + /// other. Traces in a `stackgroup` will only fill to (or be filled to) + /// other traces in the same group. With multiple `stackgroup`s or some + /// traces stacked and some not, if fill-linked traces are not + /// already consecutive, the later ones will be pushed down in the drawing + /// order. + fill: Option, + /// Sets the fill color. Defaults to a half-transparent variant of the line + /// color, marker color, or marker line color, whichever is available. + #[serde(rename = "fillcolor")] + fill_color: Option>, + /// Properties of label displayed on mouse hover. + #[serde(rename = "hoverlabel")] + hover_label: Option