Reason or Problem
When you write a categorical raster with arr.xrs.to_geotiff(), xarray-spatial drops a PAM .aux.xml sidecar holding the class names and colors, so QGIS opens the file already showing labelled, coloured classes. Continuous rasters (DEMs, slope, NDVI, and so on) get no such treatment. They open in QGIS as a flat grayscale stretch with whatever default min/max GDAL guesses, and the user has to set up a colour ramp by hand every time.
Proposal
Add an opt-in color_ramp parameter to to_geotiff that, for continuous single-band rasters, writes best-practice symbology sidecars so QGIS applies a sensible colour ramp on open. This is the continuous-data counterpart to the categorical RAT sidecar we already write.
Design:
Two sidecars are written when color_ramp is set and the raster is continuous (no category_names attr) and single-band:
- A QGIS
.qml style file (dem.qml, replacing the extension, which is the name QGIS auto-loads) with a singlebandpseudocolor renderer and an interpolated colour ramp. This is what gives QGIS real colours.
STATISTICS_MINIMUM/MAXIMUM/MEAN/STDDEV in the existing PAM .aux.xml (dem.tif.aux.xml), which GDAL tools and QGIS use for a min/max stretch even if the .qml is ignored.
Colour ramps ship as hardcoded control points (viridis default, plus a small curated set) so there is no matplotlib runtime dependency. Statistics are computed with a single backend-aware reduction pass covering numpy, cupy, dask+numpy and dask+cupy. A color_ramp_range=(min, max) escape hatch skips the reduction for large dask graphs.
Usage:
dem.xrs.to_geotiff('dem.tif', color_ramp='viridis')
# writes dem.tif, dem.qml, dem.tif.aux.xml
Open dem.tif in QGIS and it renders with the viridis ramp stretched across the data range.
Value: Continuous rasters written by xarray-spatial open in QGIS looking right without manual styling, matching the experience categorical rasters already get.
Stakeholders and Impacts
Anyone writing continuous rasters for use in QGIS. Touches the geotiff writer (to_geotiff wrapper) and a new _symbology module plus the existing _pam sidecar module. Default behaviour is unchanged because the feature is opt-in.
Drawbacks
Writes extra sidecar files. The .qml format is QGIS-specific (other GIS tools ignore it, though they still read the GDAL statistics). For the dask streaming write path, computing statistics adds a separate reduction pass unless color_ramp_range is supplied.
Alternatives
- Statistics only, no
.qml: works in all GDAL tools but only gives a grayscale stretch, no colour.
- Automatic for every continuous write: rejected to avoid surprise side-files and a forced statistics pass on writes that do not want symbology.
- A GDAL colour table embedded in the TIFF: discrete/paletted only, not suited to a continuous ramp.
Unresolved Questions
On a constant raster (min == max) the statistics sidecar is still written but the .qml is skipped, since a single-stop ramp is degenerate.
Additional Notes or Context
Follows the categorical sidecar work (#3482 / #3483).
Reason or Problem
When you write a categorical raster with
arr.xrs.to_geotiff(), xarray-spatial drops a PAM.aux.xmlsidecar holding the class names and colors, so QGIS opens the file already showing labelled, coloured classes. Continuous rasters (DEMs, slope, NDVI, and so on) get no such treatment. They open in QGIS as a flat grayscale stretch with whatever default min/max GDAL guesses, and the user has to set up a colour ramp by hand every time.Proposal
Add an opt-in
color_rampparameter toto_geotiffthat, for continuous single-band rasters, writes best-practice symbology sidecars so QGIS applies a sensible colour ramp on open. This is the continuous-data counterpart to the categorical RAT sidecar we already write.Design:
Two sidecars are written when
color_rampis set and the raster is continuous (nocategory_namesattr) and single-band:.qmlstyle file (dem.qml, replacing the extension, which is the name QGIS auto-loads) with asinglebandpseudocolorrenderer and an interpolated colour ramp. This is what gives QGIS real colours.STATISTICS_MINIMUM/MAXIMUM/MEAN/STDDEVin the existing PAM.aux.xml(dem.tif.aux.xml), which GDAL tools and QGIS use for a min/max stretch even if the.qmlis ignored.Colour ramps ship as hardcoded control points (viridis default, plus a small curated set) so there is no matplotlib runtime dependency. Statistics are computed with a single backend-aware reduction pass covering numpy, cupy, dask+numpy and dask+cupy. A
color_ramp_range=(min, max)escape hatch skips the reduction for large dask graphs.Usage:
Open
dem.tifin QGIS and it renders with the viridis ramp stretched across the data range.Value: Continuous rasters written by xarray-spatial open in QGIS looking right without manual styling, matching the experience categorical rasters already get.
Stakeholders and Impacts
Anyone writing continuous rasters for use in QGIS. Touches the geotiff writer (
to_geotiffwrapper) and a new_symbologymodule plus the existing_pamsidecar module. Default behaviour is unchanged because the feature is opt-in.Drawbacks
Writes extra sidecar files. The
.qmlformat is QGIS-specific (other GIS tools ignore it, though they still read the GDAL statistics). For the dask streaming write path, computing statistics adds a separate reduction pass unlesscolor_ramp_rangeis supplied.Alternatives
.qml: works in all GDAL tools but only gives a grayscale stretch, no colour.Unresolved Questions
On a constant raster (min == max) the statistics sidecar is still written but the
.qmlis skipped, since a single-stop ramp is degenerate.Additional Notes or Context
Follows the categorical sidecar work (#3482 / #3483).