The Let's-Plot library allows to easily visualize geospatial features from GeoTools SimpleFeatureCollection
.
SimpleFeatureCollection
is a collection of SimpleFeature
-s. Each SimpleFeature
have a "geometry" attribute as well as optional "data" attributes.
The Let's-Plot library understands the following three geometry types:
These shapes can be plotted using various geometry layers, depending on the type of the shape:
geomPoint, geom_text
with Points / Multi-PointsgeomPath
with Lines / Multi-LinesgeomPolygon, geom_map
with Polygons / Multi-PolygonsgeomRect
when used with Polygon shapes will display corresponding bounding boxesApart from SimpleFeatureCollection
the Lets-Plot library can also plot an individual Geometry
(org.locationtech.jts.geom) and a ReferencedEnvelope
(org.geotools.geometry.jts).
Before passing to a Lets-Plot geometry layer (via map
or data
parameters) any 'foreign' object must be converted to a Lets-Plot SpatialDataset
object. This is done by the toSpatialDataset()
extension method provided by Lets-Plot GeoTools extension (see the %use lets-plot-gt
'magic').
Shapfiles used in this tutorial:
all are the copies of shapefiles distributed with the GeoPandas Python package.
%useLatestDescriptors
%use lets-plot
// Initialize Lets-Plot GeoTools extension.
%use lets-plot-gt(gt="[23,)")
LetsPlot.getInfo()
Lets-Plot Kotlin API v.4.1.1. Frontend: Notebook with dynamically loaded JS. Lets-Plot JS v.2.5.1.
@file:DependsOn("org.geotools:gt-shapefile:[23,)")
@file:DependsOn("org.geotools:gt-cql:[23,)")
import org.geotools.data.shapefile.ShapefileDataStoreFactory
import org.geotools.data.simple.SimpleFeatureCollection
import java.net.URL
val factory = ShapefileDataStoreFactory()
val worldFeatures : SimpleFeatureCollection = with("naturalearth_lowres") {
val url = "https://raw.githubusercontent.com/JetBrains/lets-plot-kotlin/master/docs/examples/shp/${this}/${this}.shp"
factory.createDataStore(URL(url)).featureSource.features
}
// Convert Feature Collection to SpatialDataset.
// Use 10 decimals to encode floating point numbers (this is the default).
val world = worldFeatures.toSpatialDataset(10)
world["continent"]?.distinct()
[Oceania, Africa, North America, Asia, South America, Europe, Seven seas (open ocean), Antarctica]
val voidTheme = theme(axis="blank", panelGrid="blank")
// Use the parameter `map` in `geomPolygon()` to display Polygons / Multi-Polygons
letsPlot() +
geomPolygon(map = world, fill = "white", color = "gray") +
ggsize(700, 400) +
voidTheme
geom_map() is very similar to geomPolygon() but it automatically applies the Mercator
projection and other defaults that are more suitable for displaying blank maps.
letsPlot() +
geomMap(map = world) +
ggsize(700, 400) +
voidTheme
// When applying Mercator projection to the world map, Antarctica becomes disproportionally large so
// in the future let's show only part of it above 85-th parallel south:
val worldLimits = coordMap(ylim = -70 to 85)
val cityFeatures : SimpleFeatureCollection = with("naturalearth_cities") {
val url = "https://raw.githubusercontent.com/JetBrains/lets-plot-kotlin/master/docs/examples/shp/${this}/${this}.shp"
factory.createDataStore(URL(url)).featureSource.features
}
// Use parameter `map` in `geomPoint()` to display Point shapes
val cities = cityFeatures.toSpatialDataset(10)
letsPlot() +
geomMap(map = world) +
geomPoint(map = cities, color = "red") +
ggsize(800, 600) + voidTheme + worldLimits
The situation with geomText() is different because in order to display labels we have to specify mapping for the aesthetic "label".
Aesthetic mapping binds a variable in data (passed via data
parameter) with its representation on the screen.
Variables in a SpatialDataset passed via the map
parameter can not be used in the aesthetic mapping.
Fortunately, such a SpatialDataset can as well be passed via the data
parameter and Lets-Plot will undersand that its geometries should be mapped to the "x" and "y" aesthetic automatically.
In the next example we are going to show names of cities as labels on map.
Let's only show South American capitals because too many labels on the entire world map would quickly become not legible.
import org.geotools.filter.text.cql2.CQL
// Obtain bounding box of South America and use it to set the limits.
val southAm = worldFeatures.subCollection(
CQL.toFilter("continent = 'South America'")
)
val southAmBounds = southAm.bounds
// Let's use slightly expanded boundind box.
southAmBounds.expandBy(4.0)
// Define limits to use later with city markers and labels.
val southAmLimits = coordMap(
xlim = southAmBounds.minX to southAmBounds.maxX,
ylim = southAmBounds.minY to southAmBounds.maxY
)
letsPlot() +
geomMap(map = southAm.toSpatialDataset()) +
geomRect(map = southAmBounds.toSpatialDataset(), alpha = 0, color = "#EFC623")
// Add `text` layer and use the `data` parameter to pass `cities` SpartialDataset.
// Also configure `tooltip` in the points layer to show the city name.
letsPlot() +
geomMap(map = southAm.toSpatialDataset(), fill="#e5f5e0") +
geomPoint(data = cities, color = "red", size = 3, tooltips = layerTooltips().line("@name")) +
geomText(data = cities, vjust = 1, position = positionNudge(y = -.5)) { label = "name" } +
geomRect(map = southAmBounds.toSpatialDataset(), alpha = 0, color="#EFC623", size=16) +
southAmLimits +
ggsize(450, 691) +
voidTheme
As we saw earlier, Lets-Plot geom-layers accept SpatialDataset in their data
parameter.
This makes it easy to bind aesthetics with variables in SpatialDataset.
// Create choropleth by mapping the `continent` variable to the `fill` aesthetic.
letsPlot() +
geomMap(data = world, color = "white") { fill = "continent" } +
ggsize(900, 400) + voidTheme + worldLimits
// Create another choropleth by mapping the `GDP estimate` variable to the `fill` aesthetic.
letsPlot() +
geomMap(data = world, color = "white") { fill = "gdp_md_est" } +
ggsize(800, 400) + voidTheme + worldLimits
data
and geometry
datasets¶In this example we will use both: the data
and the map
parameters.
We will use the data
parameter to pass "average temperature per continent" dataset to the geom layer.
The continent geometries SpatialDataset is passed via the map
parameter as before.
For this to work it is also necessary to specify fields by which Lets-Plot will join data
and map
datasets. We will do that using the mapJoin parameter.
// Average temperatures
val climateData = mapOf(
"region" to listOf("Europe", "Asia", "North America", "Africa", "Australia", "Oceania"),
"avg_temp" to listOf(8.6, 16.6, 11.7, 21.9, 14.9, 23.9)
)
// Join `data` and `map` using the `mapJoin` parameter.
// For the sake of the demo let's use `geom_rect` and customize the tooltip.
letsPlot() + geomRect(data = climateData,
map = world,
mapJoin = "region" to "continent",
color = "white",
tooltips=layerTooltips().line("^fill C\u00b0")) { fill = "avg_temp" } +
scaleFillGradient(low = "light_blue", high = "dark_green", name="Average t[C\u00b0]") +
ggsize(800, 400) + voidTheme