Skip to content

Commit a453445

Browse files
jqnatividadclaude
andcommitted
feat: add Choropleth and ChoroplethMap trace types
Add choropleth map support (addresses #408): - `Choropleth<Loc, Z>` on the existing `geo` subplot, with a `LocationMode` enum (ISO-3 / USA-states / country names / geojson-id), a dedicated `choropleth::Marker`, selection support, z-bounds, and the shared `ColorScale`/`ColorBar` machinery. `geojson` accepts a URL string or an inline GeoJSON object. - `ChoroplethMap<Loc, Z>`, the plotly.js 3.x `choroplethmap` trace on the MapLibre `map` subplot (GeoJSON-only). - New MapLibre `map` subplot via `LayoutMap` + `MapStyle` + `MapBounds`. - `LayoutGeo` additions: `fitbounds` (`GeoFitBounds`), `resolution` (`GeoResolution`, 1:110M/1:50M base-layer detail), and `bgcolor`. - `PlotType::Choropleth`/`ChoroplethMap` variants, re-exports, serialization tests, doctests, runnable `examples/maps` examples, and a Maps book recipe. Attribute names/types follow the plotly.js v3.6.0 plot-schema. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b514df8 commit a453445

15 files changed

Lines changed: 944 additions & 27 deletions

File tree

‎.github/scripts/build-book-examples.sh‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ BOOK_EXAMPLES=(
4040
"basic_charts"
4141
"custom_controls"
4242
"financial_charts"
43+
"maps"
4344
"scientific_charts"
4445
"shapes"
4546
"static_export"

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
88
### Added
99

1010
- [[#406](https://github.com/plotly/plotly.rs/issues/406)] Expose `plotly.js` 3.1–3.6 attributes
11+
- [[#408](https://github.com/plotly/plotly.rs/issues/408)] Add `Choropleth` (geo subplot) and `ChoroplethMap` (MapLibre `map` subplot) trace types, with a `LocationMode` enum and a dedicated `choropleth::Marker`; add the MapLibre `map` subplot via `LayoutMap`/`MapStyle`/`MapBounds`
12+
- [[#408](https://github.com/plotly/plotly.rs/issues/408)] Add `LayoutGeo` options: `fitbounds` (`GeoFitBounds`), `resolution` (`GeoResolution`, 1:110M/1:50M base-layer detail), and `bgcolor`
1113

1214
### Changed
1315

‎docs/book/src/SUMMARY.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
- [Rangebreaks](./recipes/financial_charts/rangebreaks.md)
3333
- [3D Charts](./recipes/3dcharts.md)
3434
- [Scatter 3D](./recipes/3dcharts/3dcharts.md)
35+
- [Maps](./recipes/maps.md)
36+
- [Choropleth Maps](./recipes/maps/choropleth_maps.md)
3537
- [Subplots](./recipes/subplots.md)
3638
- [Subplots](./recipes/subplots/subplots.md)
3739
- [Multiple Axes](./recipes/subplots/multiple_axes.md)

‎docs/book/src/recipes/maps.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Maps
2+
3+
The source code for the following examples can also be found [here](https://github.com/plotly/plotly.rs/tree/main/examples/maps).
4+
5+
Kind | Link
6+
:---|:----:
7+
Choropleth Maps | [Choropleth Maps](./maps/choropleth_maps.md)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Choropleth Maps
2+
3+
Choropleth maps color geographic regions (countries, states, custom GeoJSON
4+
areas) according to a data value. Two trace types are available:
5+
6+
- [`Choropleth`](https://docs.rs/plotly/latest/plotly/struct.Choropleth.html) —
7+
drawn on the built-in `geo` subplot
8+
([`LayoutGeo`](https://docs.rs/plotly/latest/plotly/layout/struct.LayoutGeo.html)).
9+
Regions are matched by `location_mode` (ISO-3 codes, USA state codes, country
10+
names, or a GeoJSON id).
11+
- [`ChoroplethMap`](https://docs.rs/plotly/latest/plotly/struct.ChoroplethMap.html) —
12+
drawn on the MapLibre `map` subplot
13+
([`LayoutMap`](https://docs.rs/plotly/latest/plotly/layout/struct.LayoutMap.html)).
14+
Regions are always matched against a GeoJSON feature collection via
15+
`feature_id_key`.
16+
17+
The following imports are used in the examples below:
18+
19+
```rust,no_run
20+
use plotly::{
21+
choropleth::{LocationMode, Marker as ChoroplethMarker},
22+
color::Rgb,
23+
common::{ColorBar, ColorScale, ColorScalePalette, Line},
24+
layout::{Center, DragMode, LayoutGeo, LayoutMap, MapStyle},
25+
Choropleth, ChoroplethMap, Configuration, Layout, Plot,
26+
};
27+
```
28+
29+
The `to_inline_html` method is used to produce the html plots displayed in this
30+
page. The rendered maps require an internet connection (the MapLibre basemap and,
31+
for the second example, the remote GeoJSON are fetched in the browser).
32+
33+
## Choropleth on a geo subplot
34+
35+
```rust,no_run
36+
{{#include ../../../../../examples/maps/src/main.rs:choropleth}}
37+
```
38+
39+
{{#include ../../../../../examples/maps/output/inline_choropleth.html}}
40+
41+
## Choropleth on a MapLibre map subplot
42+
43+
```rust,no_run
44+
{{#include ../../../../../examples/maps/src/main.rs:choropleth_map}}
45+
```
46+
47+
{{#include ../../../../../examples/maps/output/inline_choropleth_map.html}}

‎examples/maps/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ plotly = { path = "../../plotly" }
99
plotly_utils = { path = "../plotly_utils" }
1010
csv = "1.3"
1111
reqwest = { version = "0.11", features = ["blocking"] }
12+
serde_json = "1"
1213

‎examples/maps/src/main.rs‎

Lines changed: 123 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
#![allow(dead_code)]
22

33
use plotly::{
4+
choropleth::{LocationMode, Marker as ChoroplethMarker},
45
color::Rgb,
5-
common::{Line, Marker, Mode},
6-
layout::{Axis, Center, DragMode, LayoutGeo, Mapbox, MapboxStyle, Projection, Rotation},
7-
Configuration, DensityMapbox, Layout, Plot, ScatterGeo, ScatterMapbox,
6+
common::{ColorBar, ColorScale, ColorScalePalette, Line, Marker, Mode},
7+
layout::{
8+
Axis, Center, DragMode, GeoResolution, LayoutGeo, LayoutMap, MapStyle, Mapbox, MapboxStyle,
9+
Projection, Rotation,
10+
},
11+
Choropleth, ChoroplethMap, Configuration, DensityMapbox, Layout, Plot, ScatterGeo,
12+
ScatterMapbox,
813
};
914
use plotly_utils::write_example_to_html;
1015

@@ -35,15 +40,38 @@ fn scatter_geo(show: bool, file_name: &str) {
3540
use csv;
3641
use reqwest;
3742

38-
// Download and parse the CSV
43+
// Download and parse the CSV. If the fetch fails (e.g. no network during a
44+
// book/CI build), warn and skip this example rather than panicking.
3945
let url = "https://raw.githubusercontent.com/plotly/datasets/master/globe_contours.csv";
40-
let req = reqwest::blocking::get(url).unwrap().text().unwrap();
46+
let req = match reqwest::blocking::get(url)
47+
.and_then(|resp| resp.error_for_status())
48+
.and_then(|resp| resp.text())
49+
{
50+
Ok(body) => body,
51+
Err(err) => {
52+
eprintln!("warning: skipping scatter_geo example; failed to fetch {url}: {err}");
53+
return;
54+
}
55+
};
4156
let mut rdr = csv::Reader::from_reader(req.as_bytes());
42-
let headers = rdr.headers().unwrap().clone();
57+
let headers = match rdr.headers() {
58+
Ok(headers) => headers.clone(),
59+
Err(err) => {
60+
eprintln!("warning: skipping scatter_geo example; failed to read CSV headers: {err}");
61+
return;
62+
}
63+
};
4364
let mut rows = vec![];
4465
for result in rdr.records() {
45-
let record = result.unwrap();
46-
rows.push(record);
66+
match result {
67+
Ok(record) => rows.push(record),
68+
Err(err) => {
69+
eprintln!(
70+
"warning: skipping scatter_geo example; failed to parse CSV record: {err}"
71+
);
72+
return;
73+
}
74+
}
4775
}
4876

4977
// Color scale
@@ -65,23 +93,26 @@ fn scatter_geo(show: bool, file_name: &str) {
6593
for i in 0..scl.len() {
6694
let lat_head = format!("lat-{}", i + 1);
6795
let lon_head = format!("lon-{}", i + 1);
96+
let (lat_idx, lon_idx) = match (
97+
headers.iter().position(|h| h == lat_head),
98+
headers.iter().position(|h| h == lon_head),
99+
) {
100+
(Some(lat_idx), Some(lon_idx)) => (lat_idx, lon_idx),
101+
_ => {
102+
eprintln!(
103+
"warning: skipping scatter_geo example; missing expected columns \
104+
{lat_head}/{lon_head}"
105+
);
106+
return;
107+
}
108+
};
68109
let lat: Vec<f64> = rows
69110
.iter()
70-
.map(|row| {
71-
row.get(headers.iter().position(|h| h == lat_head).unwrap())
72-
.unwrap()
73-
.parse()
74-
.unwrap_or(f64::NAN)
75-
})
111+
.map(|row| row.get(lat_idx).unwrap_or("").parse().unwrap_or(f64::NAN))
76112
.collect();
77113
let lon: Vec<f64> = rows
78114
.iter()
79-
.map(|row| {
80-
row.get(headers.iter().position(|h| h == lon_head).unwrap())
81-
.unwrap()
82-
.parse()
83-
.unwrap_or(f64::NAN)
84-
})
115+
.map(|row| row.get(lon_idx).unwrap_or("").parse().unwrap_or(f64::NAN))
85116
.collect();
86117
all_lats.push(lat);
87118
all_lons.push(lon);
@@ -152,9 +183,81 @@ fn density_mapbox(show: bool, file_name: &str) {
152183
}
153184
}
154185

186+
/// Classic choropleth on the `geo` subplot, coloring countries by value using
187+
/// ISO-3 country codes.
188+
// ANCHOR: choropleth
189+
fn choropleth(show: bool, file_name: &str) {
190+
let trace = Choropleth::new(
191+
vec![
192+
"USA", "CAN", "MEX", "BRA", "ARG", "FRA", "DEU", "CHN", "IND", "AUS",
193+
],
194+
vec![10.0, 8.0, 6.0, 7.0, 4.0, 9.0, 9.5, 12.0, 11.0, 5.0],
195+
)
196+
.location_mode(LocationMode::Iso3)
197+
.color_scale(ColorScale::Palette(ColorScalePalette::Viridis))
198+
.color_bar(ColorBar::new().title("Score"))
199+
.marker(ChoroplethMarker::new().line(Line::new().width(0.5).color(Rgb::new(80, 80, 80))));
200+
201+
let layout = Layout::new().drag_mode(DragMode::Zoom).geo(
202+
LayoutGeo::new()
203+
.showcountries(true)
204+
.showland(true)
205+
.resolution(GeoResolution::OneOverFiftyMillion),
206+
);
207+
208+
let mut plot = Plot::new();
209+
plot.add_trace(trace);
210+
plot.set_layout(layout);
211+
plot.set_configuration(Configuration::default().responsive(true).fill_frame(true));
212+
213+
let path = write_example_to_html(&plot, file_name);
214+
if show {
215+
plot.show_html(path);
216+
}
217+
}
218+
// ANCHOR_END: choropleth
219+
220+
/// Choropleth on the MapLibre `map` subplot. Regions are matched against a
221+
/// GeoJSON feature collection (referenced here by URL) via `feature_id_key`.
222+
// ANCHOR: choropleth_map
223+
fn choropleth_map(show: bool, file_name: &str) {
224+
let geojson_url =
225+
"https://raw.githubusercontent.com/python-visualization/folium/main/tests/us-states.json";
226+
227+
let trace = ChoroplethMap::new(
228+
vec!["AL", "AK", "AZ", "CA", "NY", "TX"],
229+
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
230+
)
231+
.geojson(serde_json::json!(geojson_url))
232+
.feature_id_key("id")
233+
.color_scale(ColorScale::Palette(ColorScalePalette::Bluered))
234+
.show_scale(true)
235+
.marker(ChoroplethMarker::new().opacity(0.7));
236+
237+
let layout = Layout::new().drag_mode(DragMode::Zoom).map(
238+
LayoutMap::new()
239+
.style(MapStyle::CartoPositron)
240+
.center(Center::new(38.0, -96.0))
241+
.zoom(3.0),
242+
);
243+
244+
let mut plot = Plot::new();
245+
plot.add_trace(trace);
246+
plot.set_layout(layout);
247+
plot.set_configuration(Configuration::default().responsive(true).fill_frame(true));
248+
249+
let path = write_example_to_html(&plot, file_name);
250+
if show {
251+
plot.show_html(path);
252+
}
253+
}
254+
// ANCHOR_END: choropleth_map
255+
155256
fn main() {
156257
// Change false to true on any of these lines to display the example.
157258
scatter_mapbox(false, "scatter_mapbox");
158259
scatter_geo(false, "scatter_geo");
159260
density_mapbox(false, "density_mapbox");
261+
choropleth(false, "choropleth");
262+
choropleth_map(false, "choropleth_map");
160263
}

‎plotly/src/common/mod.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ pub enum PlotType {
232232
Pie,
233233
Treemap,
234234
Sunburst,
235+
Choropleth,
236+
ChoroplethMap,
235237
}
236238

237239
#[derive(Serialize, Clone, Debug)]

0 commit comments

Comments
 (0)