Skip to content

API reference

Style

Apply a journal style to matplotlib.

This is the only eofigs function that changes global state (matplotlib's rcParams and the eofigs defaults for panel labels). Use :func:style to apply a style temporarily.

Parameters:

Name Type Description Default
journal str or None

Journal preset: "elsevier", "springer", "mdpi", "wiley", "frontiers", "taylor_francis" or "custom". None restores the settings that were active before the first call.

"elsevier"
font str

"serif", "sans-serif" or a font name such as "Times New Roman". Defaults to the journal's preferred family. Missing fonts fall back to metric-compatible free fonts (Liberation Serif, TeX Gyre Termes, Liberation Sans) with a warning.

None
bold bool

Bold text for labels, titles and tick labels.

False
base_size float

Base font size in points. Defaults to the journal preset; tick labels are one point smaller but never below the journal minimum.

None
labels str or None

Default panel-label format: "(a)", "a", "(A)", "A", "a)" or None for no labels.

"(a)"
label_bold bool

Bold panel labels.

True
max_ticks int

A house rule for the largest number of tick labels per axis (for example 5). When set, :func:check_figure warns about axes with more tick labels, and :func:tidy_axes and the recipes use this number. None (default): no tick check; recipes still draw a few ticks at round values.

None
panel_titles bool

Whether recipes put a short title above each panel (for example "Before (3 months)" in :func:event_composite). False follows the house rule that panels carry letters only: the recipes then name the panels in their caption sentence instead.

True

Raises:

Type Description
ValueError

For an unknown journal or label format, or max_ticks below 2.

Examples:

>>> import eofigs
>>> eofigs.set_style("elsevier", font="serif")
>>> eofigs.set_style(None)  # restore the previous matplotlib settings

Apply a journal style inside a with block and restore all settings afterwards.

Takes the same arguments as :func:set_style.

Examples:

>>> import eofigs
>>> with eofigs.style("frontiers", font="DejaVu Sans"):
...     fig, ax = eofigs.new_figure("single")

Create a figure with its printed size in millimetres.

Parameters:

Name Type Description Default
width str or float

A named width of the journal preset ("single", "onehalf", "double"; Springer also "small_journal") or a width in mm.

"single"
height_mm float

Height in mm. If omitted, the height is width * aspect.

None
aspect float

Height-to-width ratio. Defaults to 0.75 * nrows / ncols.

None
nrows int

Panel grid, passed to :func:matplotlib.pyplot.subplots.

1
ncols int

Panel grid, passed to :func:matplotlib.pyplot.subplots.

1
journal str

Journal preset for the widths; defaults to the one set by :func:set_style.

None
**kwargs Any

Passed to :func:matplotlib.pyplot.subplots (for example sharex=True or subplot_kw={"projection": crs}).

{}

Returns:

Name Type Description
fig Figure
axes Axes or numpy.ndarray of Axes

Raises:

Type Description
ValueError

If the width is unknown or too wide, or the height exceeds the journal's maximum figure height.

Examples:

>>> import eofigs
>>> fig, ax = eofigs.new_figure("single", height_mm=60)
>>> float(round(fig.get_figwidth() * 25.4, 1))
90.0

Add panel labels such as (a), (b), (c) to axes, in row-major order.

Parameters:

Name Type Description Default
axes Axes or sequence of Axes

The panels to label, for example the array returned by :func:new_figure.

required
labels str or sequence of str

A format ("(a)", "a", "(A)", "A", "a)"), an explicit list of labels, or None to use the default from :func:set_style. If the default is also None, no labels are drawn.

None
bold bool

Bold labels; defaults to the :func:set_style setting.

None
loc ('outside', 'inside')

Place labels just above the top-left corner of each panel, or inside it.

"outside"
fontsize float

Label size in points; defaults to the base font size.

None

Returns:

Type Description
list of matplotlib.text.Text

The label artists (empty when labelling is switched off).

Raises:

Type Description
ValueError

For an unknown label format or location, or fewer explicit labels than panels.

Examples:

>>> import eofigs
>>> fig, axes = eofigs.new_figure("double", ncols=2)
>>> [t.get_text() for t in eofigs.label_panels(axes, "(a)")]
['(a)', '(b)']

Limit axes and colour bars to a few ticks at round values.

Every eofigs recipe calls this; call it on your own plots for the same look. Only automatically placed ticks are changed: ticks set explicitly (ax.set_xticks), log axes and map axes (cartopy GeoAxes, which use graticules) are left as they are.

Parameters:

Name Type Description Default
*items Axes, Colorbar or sequence of them

Axes (or an array of axes from :func:new_figure) and colour bars.

()
max_ticks int

Largest number of tick labels per axis. Defaults to the max_ticks set with :func:set_style, or 5.

None

Raises:

Type Description
ValueError

If max_ticks is smaller than 2.

Examples:

>>> import eofigs
>>> fig, ax = eofigs.new_figure("single")
>>> _ = ax.plot([0, 0.95], [-3.3, 7.1])
>>> eofigs.tidy_axes(ax)
>>> len(ax.get_yticks()) <= 5
True

Colours

Colour palettes and colour maps built into eofigs.

Each palette is a tuple of hex colours, chosen for one job:

Palette Use
okabe_ito lines, points, error bars and small symbols (Okabe & Ito 2008; black last)
muted filled areas: bars, stacked areas, class maps with up to five classes plus grey
slate ordered magnitude in a few classes, light (low) to dark (high)
terra_slate signed values around zero: terracotta (negative), off-white (zero), slate teal

slate and terra_slate are also registered as matplotlib colour maps "eofigs.slate" and "eofigs.terra_slate" (and their "_r" reversals), so they can be passed as cmap= anywhere. neutrals holds the colours for text, secondary lines, grid lines and no-data areas.

Examples:

>>> from eofigs import palettes
>>> palettes.muted[0]
'#768EAE'
>>> import matplotlib as mpl
>>> mpl.colormaps["eofigs.terra_slate"].N
255

Write one image showing a figure as seen with colour-vision deficiencies.

The image has five panels: normal vision, protan, deutan and tritan simulations (Machado et al. 2009, full severity, the same as :func:check_figure) and greyscale (CIELAB lightness). Use it to confirm that every series, class and colour-map step is still distinguishable, and that sequential data keep their order in greyscale.

Parameters:

Name Type Description Default
fig_or_path Figure or path

A figure, or a PNG, TIFF or JPEG file.

required
out str or Path

Output PNG. Defaults to <name>_cvd.png next to an input file; required for a figure.

None
dpi float

Resolution at which a figure is rendered for the preview.

150

Returns:

Type Description
Path

The written PNG.

Raises:

Type Description
ValueError

For a figure without out, or an unsupported file type (render PDFs to PNG first).

FileNotFoundError

If the input file does not exist.

Examples:

>>> import eofigs
>>> fig, ax = eofigs.new_figure("single")
>>> _ = ax.plot([0, 1], [0, 1])
>>> eofigs.cvd_preview(fig, "fig1_cvd.png")

Variables

Look up a variable by name, alias or xarray attributes.

Parameters:

Name Type Description Default
name (str, Variable or DataArray)

A library name or alias (case-insensitive, "NDVI", "leaf area index"), a :class:Variable (returned unchanged), or a DataArray recognised from its standard_name, long_name or name. For a DataArray whose units attribute differs from the library units, the returned variable carries the data's units (CF strings such as "kg m-2 s-1" are written in mathtext).

required

Returns:

Type Description
Variable

Raises:

Type Description
ValueError

If nothing matches; the message suggests close names.

Examples:

>>> import eofigs
>>> eofigs.variable("GPP").label
'Gross primary productivity (gC m$^{-2}$ d$^{-1}$)'
>>> eofigs.variable("ndvi").trend("decade").units
'decade$^{-1}$'

Add a variable to the library, or replace one with overwrite=True.

Registered variables are found by :func:variable and by every recipe for the rest of the session. This is the only way the library changes.

Parameters:

Name Type Description Default
name str

Key of the new variable.

required
overwrite bool

Replace an existing variable (bundled or registered) of the same name.

False
**fields Any

:class:Variable fields: long_name (required), symbol (defaults to the long name), units, cmap, anomaly_cmap, kind, vmin, vmax, standard_name, aliases.

{}

Returns:

Type Description
Variable

Raises:

Type Description
ValueError

If the name exists and overwrite is False, or a field is invalid.

Examples:

>>> import eofigs
>>> v = eofigs.register_variable(
...     "cci", long_name="Chlorophyll carotenoid index", symbol="CCI", vmin=-0.5, vmax=0.5,
...     overwrite=True)
>>> eofigs.variable("CCI").long_name
'Chlorophyll carotenoid index'

How one variable is named, labelled and coloured.

Attributes:

Name Type Description
name str

Key in the library, for example "ndvi".

long_name str

Full name, for example "Normalized Difference Vegetation Index".

symbol str

Short symbol in matplotlib mathtext, for example "NDVI" or "T$_{a}$".

units str

Units in mathtext ("gC m$^{-2}$ d$^{-1}$"); empty for unitless variables. These are the units the data are expected in; eofigs never converts units.

cmap str

Registered matplotlib colour map name (cmcrameri maps are "cmc.<name>").

anomaly_cmap str

Diverging colour map for anomalies and trends, centred on zero.

kind {'continuous', 'diverging', 'categorical', 'mask'}

How the data behave; sets the colour scaling and the raster reduction.

vmin, vmax float or None

Default colour limits; None lets the data decide.

standard_name str or None

CF standard name used to recognise the variable in xarray attributes.

aliases tuple of str

Other names that refer to the variable.

label property

Axis or colour-bar label: long name with units in brackets.

short_label property

Compact label: symbol with units in brackets.

__post_init__()

Validate the fields.

anomaly()

Return the variable as an anomaly: zero-centred diverging colours, same units.

get_cmap()

Return the colour map as a matplotlib object.

trend(per='decade')

Return the variable as a trend per year or per decade.

Parameters:

Name Type Description Default
per ('decade', 'year')

Time unit of the trend; units become for example "NDVI decade$^{-1}$".

"decade"

Raises:

Type Description
ValueError

For any other per.

Large rasters

Reduce a raster to the resolution of the printed figure.

A map printed 90 mm wide at 600 dpi shows about 2 100 pixels across, however large the raster is. fit_raster reads or aggregates the raster to that size before plotting, so large rasters fit in memory, plot quickly and give small vector PDFs. It never upsamples.

Parameters:

Name Type Description Default
data (str, Path, DataArray or Dataset)

A GeoTIFF/COG path or URL (https://, http:// or s3://; a cloud-optimised GeoTIFF is then read only at the needed resolution), or a georeferenced DataArray/Dataset (rioxarray CRS set). Arrays may be dask-backed; the result then stays lazy.

required
width_mm float

Printed width of the map area in mm.

required
height_mm float

Printed height in mm; if given, the raster fits inside width x height.

None
dpi float

Output resolution.

600
kind ('continuous', 'categorical', 'mask')

Reduction: mean of valid pixels (continuous), most frequent class (categorical) or fraction of True pixels (mask, e.g. p < 0.05). Inferred if omitted: boolean data are masks; data with flag_values attributes, a GeoTIFF colour table or a categorical library variable are categorical; everything else is continuous.

"continuous"
min_valid float

Minimum fraction of valid (not nodata/NaN) source pixels; output pixels with fewer valid pixels are NaN.

0.5
band int

Band to read from a file.

1

Returns:

Type Description
DataArray or Dataset

The reduced raster with CRS and transform. attrs["eofigs_reduction"] records the method, factor and source shape.

Raises:

Type Description
ValueError

For a raster without a CRS, an unknown kind, min_valid outside 0-1, or a rotated GeoTIFF.

FileNotFoundError

If the file does not exist.

Notes

Files are read with rasterio at the output size, so GDAL uses overviews when the file has them and never loads the full-resolution array. Pre-built overviews keep the resampling they were built with (often nearest or average), which then applies instead of kind. Integer files with continuous or mask data are averaged exactly by eofigs, a few rows at a time, because some GDAL versions round integer averages. Arrays are aggregated by an integer factor; up to factor - 1 edge rows and columns (less than one printed pixel) are dropped so the grid stays regular.

Examples:

>>> import numpy as np, xarray as xr, rioxarray  # noqa: F401
>>> from eofigs import fit_raster
>>> da = xr.DataArray(np.ones((4000, 6000)), dims=("y", "x"),
...                   coords={"y": np.arange(4000.0)[::-1], "x": np.arange(6000.0)})
>>> da = da.rio.write_crs("EPSG:32643")
>>> fit_raster(da, width_mm=90, dpi=300).shape  # 1063 px needed; factor 5 keeps 1200
(800, 1200)

Maps

Draw a publication-ready study area map.

The map shows the study area outline over a grey hillshade with semi-transparent elevation colours, an optional single-colour overlay (for example forest), labelled points, a graticule, a north arrow pointing to true north, a scale bar, a legend and a locator inset. Rasters are reduced to the printed resolution first (:func:fit_raster), so large DEMs are fine. The figure height follows the shape of the study area, up to three quarters of the journal's maximum height.

Parameters:

Name Type Description Default
aoi (GeoDataFrame, GeoSeries or path)

Study area outline(s), with a CRS. The map uses the AOI's CRS if it is projected, otherwise the AOI's UTM zone.

required
dem (path, URL or DataArray)

Elevation in metres, with a CRS; drawn as hillshade plus elevation colours with a colour bar. The colours are cmcrameri batlow, or the light half of lapaz when an overlay is drawn, so the overlay stands out.

None
overlay (path, URL or DataArray)

A raster drawn in one colour where it is True, or where it reaches overlay_threshold (default 50 for percentages, 0.5 for fractions), for example tree cover. The hillshade shows through it.

None
points GeoDataFrame or path

Places to mark, with a CRS.

None
labels str or None

Column of points with the labels; None for no labels. Labels are bold with a white halo and placed so that they overlap neither each other nor the outline.

"name"
aoi_label str

Legend entries. overlay_label defaults to the overlay's long name.

'Study area'
overlay_label str

Legend entries. overlay_label defaults to the overlay's long name.

'Study area'
points_label str

Legend entries. overlay_label defaults to the overlay's long name.

'Study area'
overlay_threshold float

Value from which the overlay is drawn.

None
overlay_colour str

Overlay colour; check_figure tests that it stays distinguishable from the elevation colours.

"#009E73" (Okabe-Ito bluish green)
inset ('country', GeoDataFrame or None)

Locator inset with the surrounding country (or the given outlines); None for no inset.

"country"
worldview str

Natural Earth worldview for the inset boundaries (see :func:add_inset); None uses the bundled default (de facto) boundaries.

None
graticule bool

Map elements to draw. The graticule has at most four labelled lines per side.

True
north_arrow bool

Map elements to draw. The graticule has at most four labelled lines per side.

True
scale_bar bool

Map elements to draw. The graticule has at most four labelled lines per side.

True
legend bool

Map elements to draw. The graticule has at most four labelled lines per side.

True
layout ('auto', 'inside', 'side')

"inside" puts the inset, legend, north arrow and scale bar in empty parts of the map, "side" puts inset and legend in a column beside it. "auto" uses "side" for tall study areas when the figure is at least 120 mm wide. Items inside the map keep 2 mm from the outline and clear of points and labels; a legend that finds no such place goes below the map, and an inset moves the map to the side layout.

"auto"
width str or float

Named journal width or width in mm.

"single"
height_mm float

Figure height; by default it follows the study area's shape, up to 75 % of the journal's maximum height.

None
journal str

Journal preset (defaults to the one set by :func:set_style).

None
ax cartopy GeoAxes

Draw into existing map axes (inside layout, no automatic size).

None
subfigure matplotlib SubFigure

Draw into a subfigure, for example one panel of fig.subfigures(1, 2): the map and its legend, inset and colour bar are laid out within the subfigure's area (width and height_mm are then taken from it).

None

Returns:

Type Description
Figure

The map (the whole figure when drawn into a subfigure). fig.eofigs_layout records the layout chosen and where each element went.

Raises:

Type Description
ValueError

For inputs without a CRS, an unknown layout, ax that is not a map, or both ax and subfigure.

Notes

Boundaries and names shown do not imply any opinion on the legal status of any country or territory.

Add a north arrow that points to true north.

On a cartopy map the arrow is rotated by the grid convergence at the map centre, so it points to true north rather than to grid north (in UTM the difference is a few degrees away from the central meridian). On ordinary axes it points up.

Parameters:

Name Type Description Default
ax Axes

A cartopy GeoAxes (or ordinary axes).

required
loc str

Matplotlib location code, for example "upper left" or "lower right".

"upper right"
size_mm float

Length of the arrow in mm (the bold "N" is added above it).

7
bbox_to_anchor tuple

Box in axes coordinates that loc refers to (default: the axes).

None

Returns:

Type Description
AnchoredOffsetbox

The arrow artist; artist.angle holds the rotation in degrees.

Examples:

>>> import cartopy.crs as ccrs
>>> import eofigs
>>> fig, ax = eofigs.new_figure("single", subplot_kw={"projection": ccrs.UTM(43)})
>>> ax.set_extent([240_000, 300_000, 3_820_000, 3_960_000], crs=ccrs.UTM(43))  # Swat
>>> arrow = eofigs.add_north_arrow(ax)
>>> round(arrow.angle, 1)  # west of the central meridian: true north is clockwise of up
1.5

Add a scale bar that shows true ground distance at the map centre.

The map's projection scale factor at its centre is taken into account, so the bar is correct in any projection near the centre (for UTM the correction is below 0.1 %).

Parameters:

Name Type Description Default
ax cartopy GeoAxes

The map.

required
loc str

Matplotlib location code.

"lower left"
length_km float

Bar length in km. Defaults to a round length (1, 2, 2.5 or 5 x 10^n) near fraction of the map width.

None
fraction float

Target share of the map width when the length is chosen automatically.

0.22
bbox_to_anchor tuple

Box in axes coordinates that loc refers to (default: the axes).

None

Returns:

Type Description
ScaleBar

The scale bar; bar.length_km holds its length.

Raises:

Type Description
ValueError

For axes that are not a map.

Examples:

>>> import cartopy.crs as ccrs
>>> import eofigs
>>> fig, ax = eofigs.new_figure("single", subplot_kw={"projection": ccrs.UTM(43)})
>>> ax.set_extent([240_000, 300_000, 3_820_000, 3_960_000], crs=ccrs.UTM(43))
>>> eofigs.add_scale_bar(ax).length_km  # about 22 % of a 60 km wide map
10

Add a locator inset: the country (or countries) around the AOI, with the AOI marked.

Parameters:

Name Type Description Default
ax Axes

The main map; the inset is placed inside it unless rect is given.

required
aoi (GeoDataFrame, GeoSeries or path)

The area of interest, with a CRS.

required
loc str

Corner of ax for the inset ("upper left", "lower right", ...).

"upper right"
width_mm float

Inset width in mm; defaults to a third of the map width.

None
rect (left, bottom, width, height)

Inset position in figure coordinates instead of loc (for example beside the map).

None
worldview str

None (default): the bundled Natural Earth 1:110m countries with de facto boundaries. A Natural Earth worldview code such as "PAK" or "IND" shows that country's view of disputed boundaries; it is downloaded once (about 5 MB) and cached.

None
countries GeoDataFrame

Your own country outlines (any CRS), instead of Natural Earth.

None

Returns:

Type Description
cartopy GeoAxes

The inset axes; inset.focus lists the countries shown in grey.

Notes

Boundaries and names shown do not imply any opinion on the legal status of any country or territory. Choose the worldview that suits the journal and readership.

Theil–Sen slope and Mann–Kendall significance for every pixel of a time series.

Parameters:

Name Type Description Default
data DataArray

Values with a datetime coordinate along dim; any other dimensions (for example y and x) are treated as independent series. numpy or dask.

required
dim str

Time dimension. Its coordinate must be datetime64; slopes are computed against the real dates in years, so gaps and irregular steps are handled.

"time"
per ('decade', 'year')

Unit of the slope.

"decade"
alpha float

Significance level for the significant mask.

0.05
min_valid int

Minimum number of valid (non-NaN) observations; pixels with fewer get NaN.

8
memory_mb float

Memory budget for the pairwise slopes. Pixels are processed in blocks that fit this budget, so any series length works; long series are only slower.

256
seasonal ('month', 'none')

Seasonal Mann–Kendall test and seasonal Kendall slope (Hirsch et al. 1982) for sub-annual series: the test and the slope compare only values of the same season in different years, so a seasonal cycle does not distort them. "month" uses the calendar month of each date; an array gives one season label per time step (for 16- or 8-day composites, for example the composite's day of year). Each season may have one value per calendar year. "none" (or None for annual data) keeps the plain test; None with sub-annual data also keeps it but warns.

"month"
seasonal_correction bool

With seasonal: correct the variance of the test statistic for correlation between seasons (Hirsch and Slack 1984, as in the R package rkt). Needs at least 10 complete years; with fewer the uncorrected test is used, with a warning.

True

Returns:

Type Description
Dataset

slope (data units per per), intercept (value at the first date, stored as reference_time), pvalue (two-sided Mann–Kendall), significant (pvalue < alpha) and n_valid (int32). Spatial coordinates and CRS are kept. slope is labelled as a trend of the input variable (no standard_name), so eofigs.variable(result.slope) returns the variable's trend with zero-centred colours.

Raises:

Type Description
ValueError

If dim is missing or not a datetime coordinate, an argument is invalid, the seasonal labels do not match the time steps, there are fewer than 2 seasons, or a season has more than one value in a calendar year.

Warns:

Type Description
TrendWarning

For sub-annual data without seasonal, and when the record is too short for the Hirsch–Slack correction.

Notes

The slope is the median of all pairwise slopes (Sen 1968) and the intercept is the median of y - slope * t (scipy.stats.theilslopes(method="joint")). The Mann–Kendall statistic S uses the variance with the tie correction and the continuity correction for Z (Kendall 1975). Observations are assumed serially independent: no correction for autocorrelation is applied (modified Mann–Kendall or prewhitening are not part of v0.1), and seasonality is not removed, so monthly data should be deseasonalised or aggregated to annual values first. Memory grows with the number of pairs, n(n-1)/2, per pixel; time grows with pixels x pairs.

Examples:

>>> import numpy as np, pandas as pd, xarray as xr
>>> from eofigs import trend
>>> time = pd.date_range("2001-01-01", periods=20, freq="YS")
>>> da = xr.DataArray(0.02 * np.arange(20.0), dims="time", coords={"time": time})
>>> result = trend(da, per="decade")
>>> round(float(result.slope), 3), bool(result.significant)
(0.2, True)

Map per-pixel trends with zero-centred colours and significance marks.

The trend is computed at the data's full resolution (Theil–Sen slope, Mann–Kendall test, :func:eofigs.trend) and only then reduced to the printed size: slopes are averaged, p-values never are. Significance is shown at print size: a dot (or hatching) marks each cell of about dot_spacing_mm where at least sig_fraction of the valid full-resolution pixels are significant.

Parameters:

Name Type Description Default
data DataArray

Time-series cube with a datetime dimension dim and a CRS; :func:trend is run on it. Give either data or slope.

None
slope DataArray

A precomputed slope grid (with a CRS) and, optionally, its p-values on the same grid (same coordinates; dimensions in another order are transposed to the slope's, and a p-value grid with a different CRS is refused). Without p-values no significance marks are drawn.

None
pvalue DataArray

A precomputed slope grid (with a CRS) and, optionally, its p-values on the same grid (same coordinates; dimensions in another order are transposed to the slope's, and a p-value grid with a different CRS is refused). Without p-values no significance marks are drawn.

None
alpha float

Significance level of each pixel's test.

0.05
fdr 'bh'

"bh": Benjamini–Hochberg false discovery rate control over the map's valid pixels (finite slope and p-value, not excluded) before the marks are drawn, with :func:eofigs.stats.fdr_mask. The family of tests is the grid passed in: to make the study area the family, clip the data to it first.

"bh"
fdr_q float

False discovery rate for fdr="bh"; default 2 * alpha (Wilks 2016, BAMS 97: 2263–2273, for spatially correlated fields).

None
dim str

Time dimension of data.

"time"
per ('decade', 'year')

Unit of the slope computed from data.

"decade"
seasonal Any

Passed to :func:eofigs.trend with data: seasonal="month" (or season labels) for the seasonal Mann–Kendall test and seasonal Kendall slope, with the Hirsch–Slack correction for correlation between seasons unless seasonal_correction=False. The caption names the test.

None
seasonal_correction Any

Passed to :func:eofigs.trend with data: seasonal="month" (or season labels) for the seasonal Mann–Kendall test and seasonal Kendall slope, with the Hirsch–Slack correction for correlation between seasons unless seasonal_correction=False. The caption names the test.

None
variable str or Variable

The variable (for example "ndvi") or its trend; sets the colours and the colour-bar label. By default it is recognised from the data.

None
limits ('p98', float or None)

Symmetric colour limits: the 98th percentile of the absolute slope rounded up to a round value, a number, or None for the largest absolute slope. Clipped values get arrows on the colour bar, and the caption states their share.

"p98"
stipple ('dots', 'hatch')

How significance is marked.

"dots"
sig_fraction float

Share of significant pixels a cell needs for a mark.

0.5
dot_spacing_mm float

Size of the significance cells on the page.

1.6
exclude ('auto', float, DataArray or None)

Pixels drawn in grey and left out of the colours, marks and profile. "auto" greys out vegetation-index pixels whose mean over the record is below 0.1 (rock, snow, ice; only with data); a number sets that threshold; a boolean grid (True = excluded) is used as given.

"auto"
exclude_label str

Legend entry for the excluded pixels.

"Not vegetated"
aoi (GeoDataFrame, GeoSeries or path)

Study area outline drawn on top; the map then shows the study area with a margin.

None
aoi_label str

Legend entry for the outline.

"Study area"
latitude_profile bool

Add a panel beside the map with the median and interquartile range of the full-resolution slope in latitude bands (valid pixels only; bands with fewer than 20 pixels are skipped).

False
graticule bool

Map elements to draw.

True
north_arrow bool

Map elements to draw.

True
scale_bar bool

Map elements to draw.

True
legend bool

Map elements to draw.

True
labels str or list of str

Panel labels for the map and profile (style default when omitted).

None
width str or float

Named journal width or width in mm.

"onehalf"
height_mm float

Figure height; by default it follows the map's shape, up to 75 % of the journal's maximum height.

None
journal str

Journal preset (defaults to the one set by :func:set_style).

None
ax cartopy GeoAxes

Draw into existing map axes (no profile, no automatic size).

None
subfigure matplotlib SubFigure

Draw within a subfigure, for example one panel of fig.subfigures(1, 2).

None

Returns:

Type Description
Figure

The figure. fig.eofigs_layout records the colour limits, the clipped and significant shares, the number of marks and a ready caption sentence (fig.eofigs_layout["caption"]).

Raises:

Type Description
ValueError

For missing or conflicting inputs, grids without a CRS, mismatched grids, an unknown stipple, sig_fraction outside (0, 1], or a profile with ax.

Examples:

>>> import numpy as np, pandas as pd, xarray as xr, rioxarray
>>> import eofigs
>>> time = pd.date_range("2001-01-01", periods=12, freq="YS")
>>> y, x = np.arange(3_900_000, 3_880_000, -500.0), np.arange(500_000, 520_000, 500.0)
>>> values = 0.5 + 0.01 * np.arange(12)[:, None, None] * np.ones((12, y.size, x.size))
>>> cube = xr.DataArray(values, dims=("time", "y", "x"),
...                     coords={"time": time, "y": y, "x": x}, name="ndvi")
>>> fig = eofigs.trend_map(cube.rio.write_crs("EPSG:32643"))
>>> fig.eofigs_layout["significant_percent"]
100.0

Benjamini–Hochberg false discovery rate control over a field of p-values.

The family of tests is every finite p-value (within valid when given); NaN is "not tested". A test is significant when its Benjamini–Hochberg adjusted p-value is at most q (Benjamini and Hochberg 1995; computed with scipy.stats.false_discovery_control). For spatially correlated fields Wilks (2016, BAMS 97: 2263–2273) recommends q = 2 × alpha, for example 0.10 for a field test at 0.05.

Parameters:

Name Type Description Default
pvalues array - like or DataArray

P-values of any shape.

required
q float

False discovery rate, in (0, 1).

0.10
valid array-like of bool

Pixels that belong to the family of tests (same shape as pvalues); others are not tested.

None

Returns:

Type Description
FdrResult

mask (same shape as pvalues), p_threshold (the equivalent per-test p-value, or None), n_tests and n_rejected.

Examples:

>>> import numpy as np
>>> from eofigs.stats import fdr_mask
>>> p = np.array([0.001, 0.008, 0.039, 0.041, 0.042, 0.06, 0.074, 0.205, 0.212, 0.216])
>>> result = fdr_mask(p, q=0.10)
>>> result.n_rejected, result.p_threshold
(6, 0.06)

Bases: NamedTuple

Result of :func:fdr_mask.

Bases: UserWarning

Warning from :func:trend about seasonal data.

Raised for sub-annual data without seasonal, and for records too short for the Hirsch-Slack correction.

Event composites

Composite anomaly maps before, during and after a set of events, with their series.

For each event the anomalies are averaged over the steps before the event (the whole pre-event part of the window), during it and after it (the post-event part); the maps show the mean of these per-event means across events. Anomalies are computed at full resolution against a baseline climatology from the steps outside every event window, after removing a per-pixel linear trend fitted to those steps. Significance is tested across events per pixel and marked at print size as in :func:trend_map.

Parameters:

Name Type Description Default
data DataArray

Time series cube (time, y, x) with a datetime time coordinate and a CRS.

required
events list of (start, end) or boolean time series

Event periods as date pairs (inclusive), or a boolean series on the data's time steps (True during an event; each run of True values is one event).

required
window (int, int)

Steps before the event start and after the event end.

(-3, 6)
freq str

A pandas offset alias (for example "MS") to resample the data to first (mean); window then counts these steps.

None
baseline (start, end)

Period for the climatology; only its steps outside every event window are used. Default: all steps outside every event window.

None
kind ('anomaly', 'zscore')

Anomalies, or anomalies divided by the baseline standard deviation of the same calendar month (pixels with fewer than 5 baseline values for it get NaN).

"anomaly"
climatology ('auto', 'month')

Remove a climatology per calendar month ("month"), one mean (None), or per month for sub-annual data and one mean for annual data ("auto").

"auto"
detrend 'linear'

Remove a per-pixel linear trend fitted to the baseline steps first, so that a long greening or warming trend does not make late events look better than early ones.

"linear"
test ('ttest', 'wilcoxon')

Significance across events per pixel: one-sample t-test or Wilcoxon signed-rank test of the event-mean anomalies against zero. Both need at least 3 events, and a two-sided Wilcoxon test at 0.05 can only reach significance with at least 6 events. None draws no marks.

"ttest"
variable str or Variable

The variable (for example "ndvi"); sets the anomaly colours and labels. Recognised from the data by default.

None
maps tuple of {"before", "during", "after"}

Phase maps to draw, in order.

all three
series bool

Add the composite series: the area mean for each event (thin grey), their mean (black) and spread (interquartile range, or the range with fewer than 4 events). Shading marks the event period: darker while all events are ongoing (up to the shortest event), lighter while only some are (up to the longest).

True
event_rule str

How the events were chosen, for the caption sentence.

None
alpha float

As in :func:trend_map; the colour limits are shared by all phase maps.

0.05
sig_fraction float

As in :func:trend_map; the colour limits are shared by all phase maps.

0.05
dot_spacing_mm float

As in :func:trend_map; the colour limits are shared by all phase maps.

0.05
stipple float

As in :func:trend_map; the colour limits are shared by all phase maps.

0.05
limits float

As in :func:trend_map; the colour limits are shared by all phase maps.

0.05
exclude float

As in :func:trend_map; the colour limits are shared by all phase maps.

0.05
exclude_label float

As in :func:trend_map; the colour limits are shared by all phase maps.

0.05
fdr str | None

As in :func:trend_map: fdr="bh" applies Benjamini–Hochberg false discovery rate control at fdr_q (default 2 * alpha) separately to each map, over its valid pixels.

None
fdr_q str | None

As in :func:trend_map: fdr="bh" applies Benjamini–Hochberg false discovery rate control at fdr_q (default 2 * alpha) separately to each map, over its valid pixels.

None
aoi (GeoDataFrame, GeoSeries or path)

Study area outline: drawn on each map, and the area the series averages over.

None
aoi_label str

Legend entry for the outline.

"Study area"
graticule bool

Map elements; the north arrow and scale bar go on the first map.

True
north_arrow bool

Map elements; the north arrow and scale bar go on the first map.

True
scale_bar bool

Map elements; the north arrow and scale bar go on the first map.

True
legend bool

Map elements; the north arrow and scale bar go on the first map.

True
labels str or list of str

Panel labels (style default when omitted). The phase titles above the maps follow the panel_titles setting of :func:set_style; without titles the caption sentence names the phases in panel order.

None
width str or float

Named journal width or width in mm.

"double"
height_mm float

Figure height; by default it follows the maps' shape, up to 75 % of the journal's maximum height.

None
journal str

Journal preset (defaults to the one set by :func:set_style).

None
ax None

Not supported: this recipe has several panels; use subfigure=.

None
subfigure matplotlib SubFigure

Draw within a subfigure of a larger figure.

None

Returns:

Type Description
Figure

The figure. fig.eofigs_layout records the events used and dropped, the steps of each phase, the colour limits, the clipped and significant shares, and a ready caption sentence (fig.eofigs_layout["caption"]).

Raises:

Type Description
ValueError

For data without a CRS or datetime axis, no usable events, an unknown kind, climatology, detrend, test or phase name, a malformed window, or ax.

Examples:

>>> import numpy as np, pandas as pd, xarray as xr, rioxarray
>>> import eofigs
>>> time = pd.date_range("2001-01-01", periods=120, freq="MS")
>>> y, x = np.arange(3_900_000, 3_880_000, -1000.0), np.arange(500_000, 520_000, 1000.0)
>>> values = 0.5 + np.zeros((120, y.size, x.size))
>>> cube = xr.DataArray(values, dims=("time", "y", "x"),
...                     coords={"time": time, "y": y, "x": x}, name="ndvi")
>>> events = [("2003-06-01", "2003-07-01"), ("2006-06-01", "2006-06-01"),
...           ("2009-07-01", "2009-08-01")]
>>> fig = eofigs.event_composite(cube.rio.write_crs("EPSG:32643"), events)
>>> len(fig.eofigs_layout["events"])
3

Accuracy scatters

Scatter of observed against predicted values with the 1:1 line, a fit and statistics.

By default the predicted values go on the x axis and the observed values on the y axis, and the fitted line is the ordinary least-squares regression of observed on predicted, so that its slope and intercept read directly as the model's consistency and bias (Piñeiro et al. 2008, Ecological Modelling 216: 316–322).

Parameters:

Name Type Description Default
observed array-like, pandas Series or xarray DataArray

Paired values of the same shape; they are flattened, and pairs with a missing value in either are left out (and counted in the record). Two pandas objects are paired by their index labels and two DataArrays by their coordinates (predicted is put in the order of observed, dimensions and labels); both raise an error when the labels differ or repeat. Arrays, and a pandas object or DataArray together with an array, are paired by position.

required
predicted array-like, pandas Series or xarray DataArray

Paired values of the same shape; they are flattened, and pairs with a missing value in either are left out (and counted in the record). Two pandas objects are paired by their index labels and two DataArrays by their coordinates (predicted is put in the order of observed, dimensions and labels); both raise an error when the labels differ or repeat. Arrays, and a pandas object or DataArray together with an array, are paired by position.

required
variable str or Variable

The variable (for example "agb"); sets the axis labels, units and the number format of the statistics. Taken from the name of observed when it matches the variable library.

None
groups array - like

A category for each pair (up to 6), drawn in Okabe-Ito colours with distinct markers. Statistics for each group are in the record and the caption. When the inputs are paired by labels, a pandas Series or DataArray of groups is aligned with them in the same way; a plain array follows the order of observed.

None
density ('auto', True, False)

Colour points by the number of points in each cell of a 60 × 60 grid (log scale, densest on top, rasterised in vector files). "auto": above 1 000 pairs and without groups.

"auto"
stats sequence of str

Statistics in the box: "r2" (squared Pearson correlation), "r", "nse" (Nash–Sutcliffe efficiency), "rmse", "rrmse" (% of the observed mean), "nmad" (normalised median absolute deviation of predicted − observed, a robust spread measure common in DEM validation; Höhle and Höhle 2009), "mae", "bias" (mean of predicted − observed), "rbias" (%), "slope" and "intercept" (of the fitted line) and "n". All of them are in the record whatever is shown.

("r2", "rmse", "bias", "n")
fit ('ols', 'rma', None)

Fitted line: ordinary least squares of the y-axis values on the x-axis values, reduced major axis, or none.

"ols"
orientation ('observed_y', 'observed_x')

Observed values on the y axis (Piñeiro et al. 2008) or on the x axis.

"observed_y"
one_to_one bool

Draw the 1:1 line.

True
limits (float, float)

Shared limits of both axes. By default the data range, rounded outwards to ticks.

None
observed_label str

Axis labels; by default "Observed" and "Predicted" with the variable and units.

None
predicted_label str

Axis labels; by default "Observed" and "Predicted" with the variable and units.

None
stats_loc str

Corner of the statistics box: "auto" (the corner with the fewest points) or "upper left", "lower right", "upper right", "lower left".

"auto"
legend bool

Legend for the lines and groups, above the plot.

True
labels str or list of str

Panel label. A single-panel figure has none unless given.

None
width str or float

Named journal width or width in mm.

"single"
height_mm float

Figure height; by default it follows the square plot.

None
journal str

Journal preset (defaults to the one set by :func:set_style).

None
ax matplotlib Axes

Draw into an existing axes, for example one panel of a larger figure.

None

Returns:

Type Description
Figure

The figure. fig.eofigs_layout (or ax.eofigs_layout with ax=) records every statistic at full precision, the statistics of each group, the fitted line, the limits, how the pairs were matched (["pairing"]: "index", "coordinates" or "position") and a ready caption sentence (["caption"]).

Raises:

Type Description
ValueError

For pandas inputs whose index labels differ or repeat, DataArrays whose dimensions or coordinates differ, inputs of different shapes, fewer than 3 pairs, observed values that are all equal, an unknown statistic, fit, orientation, density or stats_loc, more than 6 groups, or groups together with density=True.

Examples:

>>> import numpy as np
>>> import eofigs
>>> rng = np.random.default_rng(0)
>>> observed = rng.uniform(20, 300, 200)
>>> predicted = observed + rng.normal(0, 25, 200)
>>> fig = eofigs.accuracy_scatter(observed, predicted, variable="agb")
>>> fig.eofigs_layout["stats"]["n"]
200

Figure sets

Open (or start) the figure set of a manuscript in a folder.

Parameters:

Name Type Description Default
folder str or Path

Output folder; created if needed. An existing figures_manifest.csv is read, so numbering continues across sessions.

required
journal str

Journal preset for formats, resolution and checks; defaults to the one set by :func:eofigs.set_style.

None
formats sequence of str

Output formats; default from the journal preset (for example PDF + TIFF).

None
dpi float

Raster resolution; default from the journal preset and artwork type.

None
art ('combination', 'line', 'halftone')

Default artwork type of the figures.

"combination"

Returns:

Type Description
FigureSet

Examples:

>>> import tempfile, eofigs
>>> folder = tempfile.mkdtemp()
>>> figs = eofigs.figure_set(folder, journal="custom", formats=("png",), dpi=100)
>>> fig, ax = eofigs.new_figure("single", journal="custom")
>>> [p.name for p in figs.add("study_area", fig, caption="Study area.")]
['Fig01_study_area.png']
>>> [p.name for p in figs.add("accuracy", fig, supplementary=True)]
['FigS01_accuracy.png']

The figures of one manuscript, numbered and saved in one folder.

Create it with :func:figure_set. Main figures are saved as Fig01_<name> and supplementary figures as FigS01_<name>, numbered in the order they are added (change it with :meth:order). Every change rewrites figures_manifest.csv, so reopening the folder in a new session continues where the last one stopped.

entries property

Manifest rows: main figures first, then supplementary figures.

manifest_path property

Path of figures_manifest.csv.

__len__()

Return the number of figures in the set.

__repr__()

Short description.

add(name, fig, supplementary=False, caption='', art=None, check=True)

Save a figure into the set and record it in the manifest.

Adding a name that is already in the set replaces that figure (its number stays the same unless it moves between main and supplementary).

Parameters:

Name Type Description Default
name str

Short name used in the file names: letters, digits, _ and -.

required
fig Figure

The figure to save.

required
supplementary bool

Number it as a supplementary figure (FigS01).

False
caption str

Caption kept in the manifest.

""
art ('combination', 'line', 'halftone')

Artwork type for this figure; defaults to the set's art.

"combination"
check bool

Run the figure check and record the result in the manifest.

True

Returns:

Type Description
list of pathlib.Path

The files written.

Raises:

Type Description
ValueError

For an invalid name.

exists(name)

Check whether the figure is in the set and all its files are on disk.

Use it to skip rebuilding expensive figures when a notebook is run again.

order(names, supplementary=False)

Set the order of the main (or supplementary) figures and rename their files.

Parameters:

Name Type Description Default
names sequence of str

All names of that group, in the new order.

required
supplementary bool

Reorder the supplementary figures instead of the main ones.

False

Raises:

Type Description
ValueError

If names are not exactly the figures of that group.

Export

Save a figure in one or more formats at its exact printed size.

The figure is never trimmed (no bbox_inches="tight"), so the saved size is the size set with :func:eofigs.new_figure. PDFs embed TrueType fonts and carry no creation date, so the same figure always gives the same file. In vector files (PDF, EPS, SVG), dense layers are rasterised at the output resolution while text, axes and lines stay vector, which keeps files small and fast to open.

Parameters:

Name Type Description Default
fig Figure

The figure to save.

required
path str or Path

Output path. With a suffix ("fig1.png") and no formats, only that format is written; without a suffix ("figures/fig1") the journal's default formats are written next to each other.

required
formats sequence of str

Formats such as ("pdf", "png", "tiff"). Defaults to the journal preset (for example PDF + TIFF for Elsevier, PDF + PNG for "custom").

None
dpi float

Resolution of raster outputs; defaults to the preset's export resolution (600) or the resolution the journal requires for art, whichever is higher.

None
size_px int or (int, int)

Exact pixel width (or width and height) of raster outputs; sets the dpi.

None
check bool

Run :func:eofigs.check_figure on the figure and on each written PDF, PNG, TIFF or JPEG file.

True
journal str

Journal preset; defaults to the one used to create the figure.

None
art ('combination', 'line', 'halftone')

Artwork type. Line art (plots without images) needs more resolution in raster files: for example 1000 dpi for Elsevier and 1200 dpi for Springer and Taylor & Francis. Vector PDF output is not affected.

"combination"
rasterize_dense bool

In vector files, rasterise mesh plots (pcolormesh) and collections with more than 2 000 elements (dense scatter plots, many polygons). Images are always raster. The artists are restored after saving.

True

Returns:

Type Description
CheckReport or None

The combined check report, or None when check=False.

Raises:

Type Description
ValueError

For an unsupported format, or a size_px height that does not match the figure's aspect ratio.

Examples:

>>> import eofigs
>>> fig, ax = eofigs.new_figure("single", journal="custom")
>>> _ = ax.plot([0, 1], [0, 1])
>>> report = eofigs.save_figure(fig, "fig1", formats=("pdf", "png"))

Figure check

Check a figure, or an exported PDF/PNG/TIFF, against a journal preset.

For a live figure the checks are: printed size, resolution, smallest font size, fonts in use, colour-blind safety of categorical colours, colour maps, and, when a tick limit is set with set_style(max_ticks=...), the number of tick labels per axis. For files: size and resolution (raster files), size and embedded fonts (PDF).

Parameters:

Name Type Description Default
fig_or_path Figure or path

A figure, or a .pdf, .png, .tif/.tiff or .jpg file.

required
journal str

Journal preset; defaults to the one used to create the figure, or the one set by :func:eofigs.set_style.

None
dpi float

Export resolution to check for a live figure; defaults to the resolution :func:eofigs.save_figure writes by default for this journal and art.

None
art ('combination', 'line', 'halftone')

Artwork type, which sets the resolution the journal requires: "line" for plots and drawings (for example 1000 dpi for Elsevier, 1200 dpi for Springer), "halftone" for photographs and images, "combination" for both.

"combination"

Returns:

Type Description
CheckReport

report.passed is True when no check failed; print the report for details.

Raises:

Type Description
ValueError

For an unsupported file type.

FileNotFoundError

If the file does not exist.

Notes

Categorical colours (lines, markers, bars, filled areas) are compared pairwise with CIEDE2000 in normal vision (warning below 10) and under simulated protan, deutan and tritan vision (Machado et al. 2009, full severity; warning below 6). Close pairs are a warning, not a failure: they are safe when series also differ by marker, line style or direct labels. Colour maps are classified first; sequential maps need monotonic lightness, diverging maps monotonic lightness on each side of the centre, cyclic and categorical maps are exempt, and rainbow-type maps (jet, rainbow, hsv, turbo, ...) fail. Overlays drawn in one colour (for example forest on a map) are compared with the colour maps drawn beneath them, as they appear on the page with their transparency, using the same thresholds; greyscale shading is not compared.

Examples:

>>> import eofigs
>>> fig, ax = eofigs.new_figure("single", journal="elsevier")
>>> _ = ax.plot([0, 1], [0, 1])
>>> eofigs.check_figure(fig, journal="elsevier").passed
True

All check results for a figure or file.

failures property

Checks with status "fail".

passed property

True when no check failed (warnings are allowed).

warnings property

Checks with status "warn".

__str__()

Readable multi-line report.

to_dict()

JSON-serialisable form of the report.

Outcome of one check.