Skip to content

Add ScatterGeo trace support #301

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
3 changes: 3 additions & 0 deletions examples/maps/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ edition = "2021"

[dependencies]
plotly = { path = "../../plotly" }
csv = "1.3"
reqwest = { version = "0.11", features = ["blocking"] }

113 changes: 110 additions & 3 deletions examples/maps/src/main.rs
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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<f64>> = vec![];
let mut all_lons: Vec<Vec<f64>> = vec![];
for i in 0..scl.len() {
let lat_head = format!("lat-{}", i + 1);
let lon_head = format!("lon-{}", i + 1);
let lat: Vec<f64> = rows
.iter()
.map(|row| {
row.get(headers.iter().position(|h| h == lat_head).unwrap())
.unwrap()
.parse()
.unwrap_or(f64::NAN)
})
.collect();
let lon: Vec<f64> = 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);

Expand Down Expand Up @@ -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");
}
2 changes: 2 additions & 0 deletions plotly/src/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ pub enum PlotType {
ScatterGL,
Scatter3D,
ScatterMapbox,
ScatterGeo,
ScatterPolar,
ScatterPolarGL,
Bar,
Expand Down Expand Up @@ -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"));
Expand Down
95 changes: 95 additions & 0 deletions plotly/src/layout/geo.rs
Original file line number Diff line number Diff line change
@@ -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<Center>,
/// Sets the domain within which the mapbox will be drawn.
/// Sets the zoom level of the map.
zoom: Option<u8>,
/// Sets the projection of the map
#[field_setter(default = "Projection::new().projection_type(ProjectionType::Orthographic)")]
projection: Option<Projection>,
/// If to show the ocean or not
#[field_setter(default = "Some(true)")]
showocean: Option<bool>,
/// Sets the color of the ocean
#[field_setter(default = "'rgb(0, 255, 255)'")]
oceancolor: Option<Box<dyn Color>>,
/// If to show the land or not
showland: Option<bool>,
/// Sets the color of the land
landcolor: Option<Box<dyn Color>>,
/// If to show lakes or not
showlakes: Option<bool>,
/// Sets the color of the lakes
lakecolor: Option<Box<dyn Color>>,
/// If to show countries (borders) or not
showcountries: Option<bool>,
/// Configures the longitude axis
lonaxis: Option<Axis>,
/// Configures the latitude axis
lataxis: Option<Axis>,
// Sets the coastline stroke width (in px).
#[field_setter(default = "Some(1)")]
coastlinewidth: Option<u8>,
}

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);
}
}
6 changes: 6 additions & 0 deletions plotly/src/layout/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod update_menu;

mod annotation;
mod axis;
mod geo;
mod grid;
mod legend;
mod mapbox;
Expand All @@ -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};
Expand All @@ -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,
Expand Down Expand Up @@ -274,7 +277,10 @@ pub struct LayoutFields {
y_axis8: Option<Box<Axis>>,
#[serde(rename = "zaxis8")]
z_axis8: Option<Box<Axis>>,
// ternary: Option<LayoutTernary>,
scene: Option<LayoutScene>,
geo: Option<LayoutGeo>,
// polar: Option<LayoutPolar>,
annotations: Option<Vec<Annotation>>,
shapes: Option<Vec<Shape>>,
#[serde(rename = "newshape")]
Expand Down
Loading
Loading