Converts a ggplot2 plot whose layers are drawn from sf objects (or, for
ggplot2::geom_point(), ggplot2::geom_path(), ggplot2::geom_line()
and ggplot2::geom_polygon(), from plain data frames, or from a
SpatRaster via tidyterra::geom_spatraster(),
tidyterra::geom_spatraster_rgb(),
tidyterra::geom_spatraster_contour(),
tidyterra::geom_spatraster_contour_text() or
tidyterra::geom_spatraster_contour_filled()) into a QGIS project
(.qgs) file. A SpatVector layer, drawn with
tidyterra::geom_spatvector() or with geom_sf() itself, counts as an
sf layer (see SpatVector layers). The data of each layer is saved as
a GeoPackage (a GeoTIFF for raster layers) under
<path minus extension>_data/, and the layer is styled after the
plot's trained color scale:
Usage
# S3 method for class 'tmap'
write_qgs(
plot,
path,
use_plot_crs = TRUE,
gradient_style = c("graduated", "continuous"),
overwrite = FALSE,
layer_names = NULL,
basemap = NULL,
create_na_layer = TRUE,
...
)
write_qgs(plot, path, ...)
# S3 method for class 'ggplot'
write_qgs(
plot,
path,
use_plot_crs = FALSE,
gradient_style = c("graduated", "continuous"),
overwrite = FALSE,
layer_names = NULL,
basemap = NULL,
...
)Arguments
- plot
A ggplot object whose layers are backed by sf data (or SpatVector data, see SpatVector layers), one of the supported data.frame geoms (see Data frame layers),
tidyterra::geom_spatraster(),tidyterra::geom_spatraster_rgb(),tidyterra::geom_spatraster_contour(),tidyterra::geom_spatraster_contour_text()ortidyterra::geom_spatraster_contour_filled()(see SpatRaster layers), or a tmap object (see tmap plots and tmap raster layers).- path
Path of the
.qgsfile to write. Tilde paths (e.g.~/x.qgs) are expanded.- use_plot_crs
If
TRUE, the project (map canvas) CRS is the plot's display CRS: for a ggplot, resolved the wayggplot2::coord_sf()does (itscrsargument if specified, otherwise the CRS of the first layer that defines one); for a tmap object, tmap's own display CRS (see tmap plots). IfFALSE, the project CRS is EPSG:3857 (Web Mercator). The default isFALSEfor ggplot plots andTRUEfor tmap objects. Either way the layers keep the CRS of their data; QGIS reprojects them on the fly.- gradient_style
How a continuous
fill/colourscale is rendered:"graduated"(the default): a graduated renderer with 25 equal-interval classes. The gradient is slightly banded, but the legend shows the classes with their value ranges."continuous": the exact ggplot2 look. The color is interpolated per feature by a data-defined expression on the symbol color (ramp_color(create_ramp(...), ...)). Caveats: QGIS cannot display a color ramp in the legend for a data-defined color, so the legend is a single swatch without any value labels, and the gradient is only discoverable in the layer styling panel behind the data-defined override of the symbol color, not in the renderer dropdown.
Binned scales are unaffected: their bins are exact in a graduated renderer, so there is nothing to trade off. Requesting
"continuous"for a layer with a binned scale keeps the bins, with a warning. Raster layers are unaffected too: their shader is exact and legend-friendly, so there is no trade-off to make.- overwrite
If
FALSE(the default), writing to apaththat already exists is an error. Set toTRUEto overwrite it.- layer_names
Names for the layers, used for the GeoPackage (or GeoTIFF) files and in the QGIS layer tree: a character vector with one name per layer, bottom-most first (layers that are skipped because they draw nothing — see SpatRaster layers — do not count).
/,\,|,:,*,?,",<,>and control characters cannot be used (the name becomes a file name). IfNULL(the default), each layer is named after the first of these that applies:the layer's own name (e.g.
geom_sf(name = "counties")),the variable its data came from (e.g.
ncforgeom_sf(data = nc), or forggplot(nc)when the variable is unambiguous),the geom (e.g.
geom_sf),
with a numbered suffix (
nc_2) on collision.- basemap
An XYZ tile layer to add below the vector layers, or
NULL(the default) for none. Either a predefined key or an arbitrary XYZ URL template (a string containing the{z},{x}and{y}placeholders, e.g."https://tile.openstreetmap.org/{z}/{x}/{y}.png"). The predefined keys are:"osm": OpenStreetMap."gsi_standard": the GSI standard map (https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png)."gsi_pale": the GSI pale map (https://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png).
XYZ tiles are in EPSG:3857; QGIS reprojects them to the project CRS on the fly.
- create_na_layer
tmap vector layers only. If
TRUE(the default), features whose mapped value is missing become a separate"<layer> (missing value)"layer in tmap'svalue.nacolor (see tmap plots). IfFALSE, they are not drawn. Raster layers express missing cells exactly and ignore this (see tmap raster layers).- ...
Passed on to the methods.
Details
a continuous
fill/colourscale becomes a graduated renderer with fine-grained equal-interval classes (or a continuously interpolated color, seegradient_style),a binned one (e.g.
ggplot2::scale_fill_steps()) becomes a graduated renderer with one class per bin, using the scale's exact bin boundaries and colors,a discrete one becomes a categorized renderer,
a layer with no
fill/colourmapping becomes a single symbol with the color ggplot2 would have used.
Following ggplot2's semantics for polygons, fill is the interior and
colour is the border: a colour scale on a polygon layer colors the
outlines while the interior keeps the constant fill. Constant outline
colors and widths are taken from the plot as well, as are the marker
constants of a point layer (see Point symbols) and alpha (see
Opacity). Mapping both fill and colour on the same layer is not
supported.
A constant fill/colour of NA renders as "not drawn", the way
ggplot2 draws it: an unfilled polygon keeps its border, a colour = NA
one loses it, and a layer left with nothing to draw at all (a polygon
with neither color, a line without its own) is an error.
Only a bare column name is supported for the fill/colour aesthetics;
a constant or a computed expression (e.g. aes(fill = AREA * 2)) is an
error.
Line types
A constant linetype (tmap: lty) is carried over to the symbol's
stroke — the body of a line, the border of a polygon, the ring around
a point marker. "solid", "dashed", "dotted" and "dotdash"
(1-4 in numeric form) map to the matching QGIS pen styles.
"longdash", "twodash" and the hex on/off patterns (e.g. "1343")
have no QGIS preset: on a line layer they become the equivalent custom
dash pattern (scaled by the line width, like in R), while polygon
borders and marker rings, which only support the preset pen styles,
get the nearest preset instead. "blank" (0) turns a polygon border
or marker ring off; a blank line layer would draw nothing and is an
error. A linetype that varies by feature (a mapped linetype/lty)
cannot be represented by these renderers: the symbols are drawn with
solid lines, with a warning.
Point symbols
A point layer's shape becomes the QGIS marker of the same outline:
pch 0/15/22 a square, 1/16/19/20/21 a circle, 5/18/23 a diamond,
2/17/24 an equilateral_triangle (6/25 the same, rotated 180 degrees),
3 a cross and 4 a cross2; ggplot2's shape names ("circle filled", ...) are those same symbols. The composite symbols
(pch 7-14), R's thin asterisk (8), a single-character shape (which
draws a text glyph) and tmap's grob shapes have no QGIS counterpart and
are errors rather than a silently different marker.
How R colors the shape decides where the layer's colors go: pch 0-6 are
stroke-only, so the marker gets a transparent interior and its
colour; pch 15-20 are filled with a single color (ggplot2's colour,
tmap's fill, see tmap symbol constants) and get no distinct border;
pch 21-25 fill with fill and stroke with colour. Mapping a color to
the slot the shape does not draw is an error.
The marker's size is the symbol's extent, which each package computes
its own way: ggplot2 spans size millimeters plus two thirds of
stroke, tmap a multiple of the text line height (see tmap symbol
constants). QGIS is then given the width the shape's ink actually
spans — three quarters of that extent for a circle — so a default
ggplot2 point (size = 1.5, stroke = 0.5, pch 19) is a 1.38 mm
circle. Its ring is stroke wide in R's line-width units, i.e.
0.25 mm by default; linewidth, which the point geoms do not have,
plays no part.
A size, shape or stroke that varies by feature (a mapped
aesthetic) is drawn with the first feature's value, with a warning: the
QGIS renderers vary the color and nothing else.
Opacity
A constant alpha (tmap: fill_alpha and col_alpha) becomes the
alpha component of the colors it applies to, rather than QGIS's
symbol-wide opacity — which could not render a translucent fill under
an opaque border. Which colors it applies to is what the plotting
package draws with it: ggplot2 gives a polygon's interior its alpha
and leaves the border opaque, applies it to a line's own color, and to
both colors of a point marker; tmap's two alphas belong to fill and
col respectively. Like the other constants, an alpha that varies by
feature is dropped down to the first feature's value, with a warning.
An alpha of 0 means "not drawn", like an NA color, and a layer with
nothing left to draw is an error. The classes of a graduated or
categorized renderer carry the alpha too, as does the data-defined
color expression of gradient_style = "continuous", but the
renderer's [source] color ramp stays opaque: it is the palette rather
than the rendering opacity, so re-classifying the layer in QGIS drops
the transparency. An alpha a color carries itself
(colour = "#FF000080") is not carried over — only its RGB part is.
Text and labels
A ggplot2::geom_sf_text() or ggplot2::geom_sf_label() layer becomes
a separate labels-only QGIS layer: its features are not drawn (a
null-symbol renderer) and the label aesthetic — a bare column name,
like fill/colour — becomes the labeled field, rendered by QGIS's
own labeling engine. ggplot2::geom_text() and ggplot2::geom_label()
work the same way for plain data frames, under the Data frame layers
rules (the plot must use ggplot2::coord_sf(), x/y must be bare
untransformed columns, identity stat and position — so nudging is not
supported).
The carried-over styles are the text size (in the layer's size.unit),
the font family and the text color and, for the label geoms, the
background fill color (geom_label()'s fill; drawn as a plain
rectangle, fill = NA disables it). R's default sans-serif family
("" or "sans") is a device alias rather than a font name, so it
leaves QGIS its own default font. Everything else (fontface, rounded
corners, hjust/vjust, alpha, ...) keeps the QGIS labeling defaults, and
the label placement is QGIS's: point labels are drawn over the point,
line labels along and on the line (rather than QGIS's default of
above it, since ggplot2 centers its text on the geometry), polygon
labels around the centroid.
A layer whose features are drawn under its own labels — that is
geom_spatraster_contour_text(), see SpatRaster layers — masks
them, so the text is not overdrawn by the line it labels. The
labels-only layers of the text/label geoms have nothing to mask.
Data frame layers
A geom_point(), geom_path(), geom_line() or geom_polygon() layer
drawn from a plain data frame is converted to an sf layer: one point per
row, or one linestring/polygon per group (ggplot2's grouping — an
explicit group aesthetic or the interaction of the discrete
aesthetics; geom_line() orders each line by x like ggplot2 does,
and polygon rings are closed). The plot must use ggplot2::coord_sf(),
and the x/y values are taken to be coordinates in the panel CRS:
coord_sf()'s crs argument if given, otherwise the CRS of the first
sf layer (coord_sf(default_crs = ) is not supported). Like
fill/colour, the x/y aesthetics must be bare column names, and
the layer must use the identity stat and position.
A point layer keeps every column of the data frame as attributes; a
line/polygon layer keeps the columns that are constant within every
group, one feature per group (a mapped fill/colour column must be
constant within each group). geom_polygon()'s default colour is
NA, so such a layer is written without a border. A
geom_point() layer's size, shape, stroke and alpha are
carried over like a geom_sf() point layer's (see Point symbols
and Opacity).
The project opens zoomed to the plot's displayed range (the panel range,
including the default expansion and any ggplot2::coord_sf() xlim/
ylim), reprojected to the project CRS, rather than the whole world.
SpatRaster layers
A tidyterra::geom_spatraster() layer becomes a QGIS raster layer:
the SpatRaster is written as a GeoTIFF (<layer name>.tif, named
after the band by default) next to the GeoPackages, and rendered by a
single-band pseudocolor renderer reproducing the plot's continuous
fill scale as an interpolated color ramp. Unlike for vector layers
the ramp is exact and the legend shows it as a continuous ramp, so
the gradient_style option does not apply to raster layers. Cells
with missing values are transparent (the GeoTIFF's nodata value).
A tidyterra::geom_spatraster_rgb() layer becomes a QGIS raster
layer with a multiband (true color) renderer. The written GeoTIFF
holds the three bands in red-green-blue order, with the layer's
r/g/b band selection, zlim/stretch rescaling and the
constant alpha (the renderer's opacity) carried over. A
max_col_value other than 255 becomes a linear contrast stretch
from 0..max_col_value, matching how tidyterra scales the channel
values.
What is written is the data the plot draws: a raster larger than
the geom's maxcell argument (500,000 cells by default)
has already been downsampled by tidyterra when the layer was created
and the original is not recoverable from the plot object — so the
GeoTIFF stays small, but is not the full-resolution source. For
geom_spatraster(), only a single-band SpatRaster with a continuous
(not binned) fill scale and the default fill mapping (the band
value) is supported for now; tidyterra's color tables
(scale_fill_coltab()) are not.
The contour geoms are the exception among the SpatRaster geoms: they
draw vector shapes, so they become ordinary GeoPackage-backed layers.
tidyterra::geom_spatraster_contour() becomes a LineString layer with
one feature per contour line (per level and per piece), and
tidyterra::geom_spatraster_contour_filled() a Polygon layer with one
MULTIPOLYGON feature per band, the holes punched by the bands above it
kept as holes. Both keep the band name as a lyr attribute and the
contour value as a level one — a number for the lines, the band's
label (e.g. "(70, 80]") for the filled bands. The shapes are the ones
the layer's stat computed, so the geom's breaks/bins/binwidth are
reproduced as they are — and, since the stat reprojects the raster to
the plot's CRS before contouring, the layer's CRS is the plot's, not
the raster's.
tidyterra::geom_spatraster_contour_text() draws the same isolines with
their value written along them, so it becomes that same LineString
layer with QGIS labeling enabled (see Text and labels). The text is
written as a label attribute — the label aesthetic (the contour
value by default) run through the geom's label_format, so a custom
format or a vector of labels is reproduced as it is; label_format = NULL, which tells the geom to place no labels, leaves the layer
unlabeled. The labels mask the lines they are written into, which is
how the gap ggplot2 breaks in each line under its label is reproduced.
On a geom_spatraster_contour() layer, colour may be mapped to
after_stat(level), which becomes a renderer on the level attribute
under the usual gradient_style rules. A
geom_spatraster_contour_filled() layer always varies its fill (the
stat maps it to after_stat(level)), which becomes a categorized
renderer, one category per level of the fill scale. level is the only
computed value written, so any other colour/fill expression is an
error, as is a mapping the geom does not draw (fill on the lines and
on the text, colour on the bands). On the text geom, colour colors
the labels as well as the lines, and a QGIS labeling carries a single
text color: the lines follow the scale and every label is drawn in the
first feature's color, with a warning (silently when the layer has no
labels to begin with). The constant colour, linewidth, linetype
and alpha are carried over like any other line or polygon layer's. A
multi-band SpatRaster is not supported.
The layers are named "<band>_contour", "<band>_contour_text" and
"<band>_contour_filled" by default, so overlaying the contours on the
raster itself gives distinguishable layers.
geom_spatraster() appends an invisible helper layer (a single empty
point carrying the raster's CRS to coord_sf()). Such a layer draws
nothing, so it is not written to the project: in general, an sf layer
whose geometries are all empty is skipped and does not count for
layer_names.
SpatVector layers
tidyterra::geom_spatvector(), tidyterra::geom_spatvector_text() and
tidyterra::geom_spatvector_label() are wrappers of
ggplot2::geom_sf(), ggplot2::geom_sf_text() and
ggplot2::geom_sf_label(), and tidyterra registers a fortify() method
that turns a SpatVector into an sf object when the plot or the layer is
created. Such a layer is therefore an ordinary sf layer here, converted
by all the rules above, and so is a plain geom_sf() layer given a
SpatVector as its data. terra::vect() typically produces a geometry
column mixing the single and MULTI variants of one type (e.g.
POLYGON and MULTIPOLYGON), which is cast to the MULTI variant
since a GeoPackage table holds a single geometry type.
One consequence of the wrapping shows up in the derived layer names:
ggplot2 records the wrapped geom_sf() call as the layer's
constructor, so the name comes from the variable the SpatVector is
bound to rather than from the call. When there is no such variable (e.g.
geom_spatvector(data = terra::vect(path))), the geom fallback names
the layer geom_sf, not geom_spatvector — the wrapper's own name is
not recorded anywhere in the layer. Pass layer_names for full control.
tmap plots
A tmap (>= 4.4) object is converted the same way:
tm_polygons()/tm_fill()/tm_borders(), tm_lines(), and
tm_symbols()/tm_dots()/tm_bubbles()/tm_squares() (on point data)
are supported, including tmap::qtm() maps, as is tmap::tm_raster()
on a raster shape (see tmap raster layers). The color scales are
reproduced from tmap's own trained scales:
tmap::tm_scale_intervals()(any classification style) becomes a graduated renderer with tmap's exact break boundaries and colors (zero-width bins from tied breaks are collapsed; the continuous-style legend variants,label.style, are not supported),tmap::tm_scale_categorical()andtmap::tm_scale_ordinal()become a categorized renderer keyed by the raw data values (missing values become the "all other values" category); tmap's formatted legend labels are not carried over,tmap::tm_scale_continuous()(including the transformed variants, approximated with piecewise-linear color stops) becomes a graduated renderer with 25 equal-interval classes, or an exact continuous gradient withgradient_style = "continuous".
A layer maps either fill or col to a data column (not both). The
constants tmap computed for the layer — lwd, lty (see Line
types), size and shape (see Point symbols and tmap symbol
constants), fill_alpha and col_alpha (see Opacity) — are carried
over; a constant that varies between features is dropped down to its
first value, with a warning.
Layers sharing one tmap::tm_shape() share one GeoPackage: the data is
written once and every layer of the shape references the same table.
QGIS's graduated renderers have no missing-value class, so for an
intervals/continuous scale the features whose value is missing go to a
separate layer named "<layer> (missing value)", drawn in tmap's
value.na color directly below its parent layer (the same GeoPackage
table, filtered with the QGIS provider filter "column" IS NULL). Set
create_na_layer = FALSE to leave the missing features undrawn
instead. Categorical scales render missing values within their own
layer (the "all other values" category), so they never get the extra
layer. tmap::tm_basemap() layers become XYZ tile layers
(overriding the basemap argument): a URL template is used as is, a
provider name is resolved via maptiles::get_providers(), and with
several basemaps only the first one is checked (visible) in the layer
tree. Facets, tm_text() and the other scale types are errors.
Map decorations are dropped: tmap::tm_graticules()/tmap::tm_grid(),
and the components tmap draws around the map
(tmap::tm_compass(), tmap::tm_scalebar(), tmap::tm_title(),
tmap::tm_credits(), ...). They carry no data of their own, and QGIS
offers its own equivalents. tmap::tm_tiles() is not dropped but an
error: it draws content that would silently go missing.
A caveat on the bin edges: tmap's interval bins are left-closed
([a, b)) while every QGIS classed renderer is right-closed
((a, b]), so a feature or cell whose value falls exactly on an
interior break renders one class lower in QGIS than in tmap.
The project CRS defaults to the tmap display CRS (use_plot_crs = TRUE
for tmap objects): [tmap::tm_crs()] or the main shape's CRS — or
EPSG:3857 when the map has basemaps, which is how tmap itself resolves
it — so the project opens looking like the tmap plot, zoomed to the
main shape's bounding box. use_plot_crs = FALSE forces EPSG:3857.
The conversion relies on tmap internals that are not part of its public API, so a tmap version older than 4.4 is rejected.
tmap symbol constants
What Point symbols leaves to each package is the symbol's extent:
tmap's size is a multiple of the text line height, which is 5.08 mm
on an R device with the default 12 pt font, so a default
tm_symbols() marker is a 3.81 mm circle (three quarters of it) and a
tm_dots() one 1.143 mm. A device opened with a different pointsize
would draw tmap's symbols at a different physical size; the conversion
assumes the default. [tmap::tm_layout()]'s scale is already part of
the size tmap computes, so it carries over.
tmap moves col into fill before drawing a pch 15-20 symbol (R fills
those with col), so the single color of such a marker is tmap's
fill — which is also why mapping col on one is an error.
tmap raster layers
tmap::tm_raster() on a raster shape (a stars object, a SpatRaster or
a RasterLayer passed to tmap::tm_shape()) becomes a QGIS raster
layer, written as a single-band GeoTIFF next to the project. The
renderer comes from the same trained scale as for vector layers:
tmap::tm_scale_intervals()(the default for a numeric variable) becomes asinglebandpseudocolorrenderer with a DISCRETE color-ramp shader holding tmap's exact breaks, colors and bin labels,tmap::tm_scale_categorical()andtmap::tm_scale_ordinal()become apalettedrenderer with one entry per value. A factor variable is written as its integer codes and labeled with the level names; values that no cell has are dropped,tmap::tm_scale_continuous()becomes asinglebandpseudocolorrenderer with an INTERPOLATED shader, which reproduces the gradient exactly and shows a continuous legend ramp — sogradient_styledoes not apply to raster layers.
What is written is the grid tmap draws: tmap downsamples a raster
beyond tmap_options(raster.max_cells =) and reprojects it to the
display CRS before the scales are trained, and the original is not
recoverable from the plot object.
Missing cells become the GeoTIFF's nodata value and are painted in
tmap's value.na color through the renderer's nodataColor — exactly,
so raster layers never get the separate "(missing value)" layer that
vector layers do and create_na_layer does not apply to them. A
constant col_alpha becomes the layer opacity; a per-cell one is an
error. Unlike vector layers, raster layers of one tmap::tm_shape() do
not share a file: each writes its own single-band GeoTIFF, since two
variables can differ in value type and nodata value.
[tmap::tm_rgb()]/[tmap::tm_rgba()], [tmap::tm_scale_discrete()], and curvilinear, rotated or irregularly spaced grids are errors.
[a, b)) while every QGIS classed renderer is right-closed ((a, b]: R:a,%20b)%60)%20while%20every%20QGIS%20classed%20renderer%20is%20right-closed%0A(%60(a,%20b
[tmap::tm_crs()]: R:tmap::tm_crs()
[tmap::tm_layout()]: R:tmap::tm_layout()
tmap::tm_raster(): R:tmap::tm_raster()
tmap::tm_shape(): R:tmap::tm_shape()
tmap::tm_scale_intervals(): R:tmap::tm_scale_intervals()
tmap::tm_scale_categorical(): R:tmap::tm_scale_categorical()
tmap::tm_scale_ordinal(): R:tmap::tm_scale_ordinal()
tmap::tm_scale_continuous(): R:tmap::tm_scale_continuous()
tmap::tm_shape(): R:tmap::tm_shape()
[tmap::tm_rgb()]: R:tmap::tm_rgb()
[tmap::tm_rgba()]: R:tmap::tm_rgba()
[tmap::tm_scale_discrete()]: R:tmap::tm_scale_discrete()
Examples
library(ggplot2)
nc <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE)
p <- ggplot(nc) +
geom_sf(aes(fill = AREA))
write_qgs(p, tempfile(fileext = ".qgs"))
# tmap objects work the same way
if (requireNamespace("tmap", quietly = TRUE)) {
x <- tmap::tm_shape(nc) + tmap::tm_polygons(fill = "AREA")
write_qgs(x, tempfile(fileext = ".qgs"))
}