Skip to contents

Export a ggplot2 map plot (e.g. geom_sf()) or a tmap map as a QGIS project (.qgs) file.

write_qgs() takes a ggplot2 plot or a tmap object whose layers are backed by sf objects (see Vector below) or by rasters (see Raster) and writes a QGIS project. The data of each layer is saved as a GeoPackage (a GeoTIFF for raster layers) alongside the .qgs, and each layer is styled after the plot’s trained color scale:

  • a continuous fill/colour scale becomes a graduated renderer (or a continuously interpolated color, see gradient_style),
  • a binned scale (e.g. scale_fill_steps()) becomes a graduated renderer with the scale’s exact bins,
  • a discrete scale becomes a categorized renderer,
  • a layer with no fill/colour mapping becomes a single symbol with the color ggplot2 would have used.

Installation

You can install ggplot2qgis via R-universe:

install.packages("ggplot2qgis", repos = c("https://yutannihilation.r-universe.dev", "https://cloud.r-project.org"))

Vector

A vector layer’s data is saved as a GeoPackage table and the layer gets a symbol renderer. Constant outline colors and widths, and constant line types (linetype / lty), are carried over as well.

ggplot2

geom_sf() on an sf object is the base case:

library(ggplot2)
library(ggplot2qgis)

nc <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE)

p <- ggplot(nc) +
  geom_sf(aes(fill = AREA))

write_qgs(p, "nc.qgs")

Open nc.qgs in QGIS: the polygons are rendered with the same fill gradient as the ggplot2 plot, and the data lives in nc_data/.

The exported project open in QGIS, the North Carolina counties filled with the same blue gradient as the ggplot2 plot

To add an XYZ tile basemap below the layers, pass basemap a predefined key or an XYZ URL template:

write_qgs(p, "nc.qgs", basemap = "osm")

The same QGIS project with an OpenStreetMap basemap drawn below the counties

geom_sf_text() and geom_sf_label() become labels-only QGIS layers drawn by QGIS’s own labeling engine, and geom_point(), geom_path(), geom_line() and geom_polygon() on a plain data frame are converted to sf layers (the plot must use coord_sf()).

Beyond the colors, the constants ggplot2 computed for a layer are carried over: linewidth and linetype, and on a point layer size / stroke as the marker’s size in millimeters, shape as the QGIS marker of the same outline, and alpha as the alpha component of the colors ggplot2 applies it to (a polygon’s interior but not its border, a line’s color, both colors of a marker).

See ?write_qgs for the full set of options (use_plot_crs, gradient_style, basemap).

tidyterra

tidyterra’s geom_spatvector() (and geom_spatvector_text() / geom_spatvector_label()) works too — they are wrappers of geom_sf(), and tidyterra’s fortify() method turns the SpatVector into an sf object, so such a layer is styled by exactly the same rules:

library(tidyterra)

cyl <- terra::vect(system.file("extdata/cyl.gpkg", package = "tidyterra"))

p <- ggplot(cyl) +
  geom_spatvector(aes(fill = name))

write_qgs(p, "cyl.qgs")

A SpatVector of Castile and Leon provinces in QGIS, each province in its own category color

tmap

A tmap (>= 4.4) object works the same way, reproducing tmap’s own trained color scales (tm_scale_intervals() with its exact class boundaries, tm_scale_categorical(), tm_scale_continuous()) and converting tm_basemap() to an XYZ tile layer:

library(tmap)

x <- tm_basemap("OpenStreetMap") +
  tm_shape(nc) +
  tm_polygons(fill = "AREA")

write_qgs(x, "nc_tmap.qgs")

A tmap map exported to QGIS, the counties classified with tmap's own interval breaks over an OpenStreetMap basemap

tm_polygons() / tm_fill() / tm_borders(), tm_lines() and the symbol layers (tm_symbols() / tm_dots() / tm_bubbles() / tm_squares(), on point shapes) are supported. Beyond the colors, the constants tmap computed for the layer — lwd, lty, size, shape, fill_alpha and col_alpha — are carried over: the marker size in millimeters, the pch translated to the QGIS marker of the same outline, and the alphas as the alpha component of the respective colors.

Raster

A raster layer’s data is written as a GeoTIFF next to the project, and missing cells become the GeoTIFF’s nodata value.

tidyterra

geom_spatraster() becomes a single-band pseudocolor layer whose color ramp reproduces the plot’s continuous fill scale:

library(tidyterra)

volcano2 <- terra::rast(system.file("extdata/volcano2.tif", package = "tidyterra"))

p <- ggplot() +
  geom_spatraster(data = volcano2) +
  scale_fill_whitebox_c()

write_qgs(p, "volcano.qgs")

The volcano2 elevation raster in QGIS, drawn with a pseudocolor ramp matching the whitebox fill scale

geom_spatraster_rgb() becomes a multiband (true color) layer instead, with the layer’s r/g/b band selection, its zlim/stretch rescaling and its constant alpha carried over.

geom_spatraster_contour() is the exception: it draws lines, so it becomes a GeoPackage-backed line layer with one feature per contour line, keeping the contour value as a level attribute. Mapping colour to after_stat(level) renders it through the same scale machinery as a vector layer:

p <- ggplot() +
  geom_spatraster(data = volcano2) +
  geom_spatraster_contour(data = volcano2, aes(colour = after_stat(level)))

write_qgs(p, "volcano_contour.qgs")

The raster becomes elevation and the lines elevation_contour.

geom_spatraster_contour_filled() becomes a polygon layer instead: one feature per contour band, with the holes punched by the bands above it kept as holes. Its fill is always the band the stat computed, so the layer gets a categorized renderer keyed on the band label ("(70, 80]" and so on):

p <- ggplot() +
  geom_spatraster_contour_filled(data = volcano2) +
  geom_spatraster_contour(data = volcano2)

write_qgs(p, "volcano_bands.qgs")

The bands become elevation_contour_filled and the lines on top of them elevation_contour.

geom_spatraster_contour_text() draws the same lines with their value written along them, so it becomes that same line layer with QGIS labeling switched on. The text is written as a label attribute, run through the geom’s label_format — so a custom format is reproduced as it is:

p <- ggplot() +
  geom_spatraster_contour_text(
    data = volcano2,
    label_format = scales::label_number(suffix = " m")
  )

write_qgs(p, "volcano_contour_text.qgs")

The layer becomes elevation_contour_text, labeled "80 m", "90 m", … in the geom’s text size, font family and color. QGIS places each label on its line and masks the line under it, reproducing the gap ggplot2 breaks there. Mapping colour colors the lines through the same scale machinery as the plain isolines, but a QGIS labeling has a single text color, so every label is drawn in the first feature’s, with a warning.

tmap

tm_raster() on a raster shape (a stars object, a SpatRaster or a RasterLayer) becomes a QGIS raster layer, with the classes, colors and legend labels tmap trained — a discrete color ramp for tm_scale_intervals(), a paletted renderer for a categorical one, an interpolated ramp for tm_scale_continuous():

library(stars)

data(land, package = "tmap")

x <- tm_shape(land) + tm_raster("cover")

write_qgs(x, "land.qgs")

The global land cover raster in QGIS as a paletted layer, the legend listing tmap's land cover classes by name

TODOs

  • Facets
  • Vector
  • Raster