Skip to content

Plotting

Static plots, animation videos, and a stitched PDF report — every review-grade output PaddockTS produces.

Per-paddock thumbnail calendar — red boxes mark interpolated slots

Per-paddock thumbnail calendar; red = interpolated, orange = cloud gaps filled
Function Output What it shows
sentinel2_video {stub}_sentinel2.mp4 True-colour Sentinel-2 timeline, date stamped per frame.
sentinel2_video_with_paddocks {stem}_sentinel2_paddocks.mp4 Same, with red paddock boundaries and IDs overlaid.
fractional_cover_video {stub}_fractional_cover.mp4 False-colour cover timeline (R=bg, G=pv, B=npv).
fractional_cover_paddocks_video {stem}_fractional_cover_paddocks.mp4 Same, with paddock overlays.
calendar_plot {stem}_calendar_<year>_p01.png Per-paddock thumbnail calendar (48 slots/year, paddock-masked).
iter_calendar_figures (no file — yields matplotlib Figures) Used by make_pdf to embed calendar pages with vector text.
phenology_plot {stem}_phenology_p01.png Per-paddock × per-year NDVI curves with SoS / PoS / EoS markers.
ozwald_daily_plot {stub}_ozwald_daily_*.png OzWALD climate panels (temperature, precipitation, wind, radiation).
silo_plot {stub}_silo_*.png SILO climate panels (temperature, rainfall, radiation, ET, humidity).
terrain_tiles_plot {stub}_topography.png 2 × 2 panel: elevation, flow accumulation, aspect, slope.
make_pdf {stub}_report.pdf Single PDF stitching every plot above, with section headers + PDF metadata.

All static plots and videos write to {troi.out_dir}/.


Global font defaults

Importing any module from PaddockTS.Plotting runs the package __init__.py, which bumps matplotlib's default font sizes so ticks, axis labels, titles, and legends remain readable after the PDF report's ~0.4× shrink:

# PaddockTS/Plotting/__init__.py — applied at import time
matplotlib.rcParams.update({
    'font.size':       16,
    'axes.titlesize':  20,
    'figure.titlesize': 22,
    'axes.labelsize':  18,
    'xtick.labelsize': 14,
    'ytick.labelsize': 14,
    'legend.fontsize': 14,
    'legend.title_fontsize': 16,
})

Override with plt.rcParams.update(...) at the call site if you need different sizing for a one-off figure. Calendar text is vector in the PDF (see below) and uses its own fontsize-in-points constants — adjusting matplotlib globals there has no effect.


Sentinel-2 videos

Each frame is a normalised RGB composite from nbart_red, nbart_green, nbart_blue, with the acquisition date stamped in the top-right corner. Frames are written as PNGs to a temporary directory then encoded to H.264 with ffmpeg (libopenh264). H.264 requires even dimensions, so the final size is rounded down to even after scaling to min_size.

Example

from datetime import date
from troi.troi import Troi
from PaddockTS.Plotting.sentinel2_video import sentinel2_video
from PaddockTS.Plotting.sentinel2_paddocks_video import sentinel2_video_with_paddocks

q = Troi(
    bbox=[148.36265, -33.52606, 148.38265, -33.50606],
    start=date(2024, 1, 1),
    end=date(2024, 12, 31),
    stub="vid_demo",
)

sentinel2_video(q, fps=4, min_size=1080)
# -> {out_dir}/vid_demo_sentinel2.mp4

# With auto SAM paddocks overlaid
sentinel2_video_with_paddocks(q)
# -> {out_dir}/<sam_stem>_sentinel2_paddocks.mp4

# With user paddocks overlaid + custom labels
sentinel2_video_with_paddocks(
    q,
    paddocks_filepath="/path/to/paddocks.gpkg",
    label_col="paddock_name",
)

Reference

PaddockTS.Plotting.sentinel2_video.sentinel2_video

sentinel2_video(troi: Troi, ds_sentinel2=None, fps: int = 4, min_size: int = 1080)

Encode the Sentinel-2 cube as a true-colour H.264 video.

Parameters:

Name Type Description Default
troi Troi

The :class:troi.Troi. Output is written to {troi.out_dir}/{troi.stub}_sentinel2.mp4.

required
ds_sentinel2

Optional in-memory Sentinel-2 dataset. If None, the cloud-masked window is read from the pysentinel2 cube.

None
fps int

Frames per second. Default 4.

4
min_size int

Minimum dimension (height or width) of the output video, in pixels. Smaller cubes are upscaled with nearest-neighbour to ensure legibility; H.264 requires even dimensions, so the final size is rounded down to even.

1080

Returns:

Name Type Description
str

Filesystem path of the generated MP4.

Raises:

Type Description
RuntimeError

If the ffmpeg invocation returns a non-zero exit code.

PaddockTS.Plotting.sentinel2_paddocks_video.sentinel2_video_with_paddocks

sentinel2_video_with_paddocks(troi: Troi, paddocks_filepath: str | None = None, ds_sentinel2=None, fps: int = 4, min_size: int = 1080, label_col: str | None = None)

Encode a true-colour Sentinel-2 video with paddock outlines + labels.

Parameters:

Name Type Description Default
troi Troi

The :class:troi.Troi. Output is written to {troi.out_dir}/{paddocks_stem}_sentinel2_paddocks.mp4.

required
paddocks_filepath str | None

Path to the paddocks file. If None, uses SAM paddocks from {troi.tmp_dir}/{troi.stub}_sam_paddocks.gpkg.

None
ds_sentinel2

Optional in-memory Sentinel-2 dataset. If None, the cloud-masked window is read from the pysentinel2 cube.

None
fps int

Frames per second. Default 4.

4
min_size int

Minimum dimension (height or width) of the output video. See :func:sentinel2_video for sizing notes.

1080

Returns:

Name Type Description
str

Filesystem path of the generated MP4.

Raises:

Type Description
RuntimeError

If the ffmpeg invocation returns a non-zero exit code.


Fractional cover videos

Each frame is a false-colour composite mapping the three fractional cover bands to RGB channels:

  • R = bg (bare ground)
  • G = pv (green vegetation)
  • B = npv (non-green vegetation)

Fractions are renormalised to sum to 1 before display, so a fully bare pixel is pure red, fully green vegetation is pure green, and so on.

Example

from PaddockTS.Plotting.fractional_cover_video import fractional_cover_video
from PaddockTS.Plotting.fractional_cover_paddocks_video import fractional_cover_paddocks_video

fractional_cover_video(q, fps=4, min_size=1080)
# -> {out_dir}/vid_demo_fractional_cover.mp4

fractional_cover_paddocks_video(q)
# -> {out_dir}/<sam_stem>_fractional_cover_paddocks.mp4

Reference

PaddockTS.Plotting.fractional_cover_video.fractional_cover_video

fractional_cover_video(troi: Troi, ds_fractional_cover=None, fps: int = 4, min_size: int = 1080)

Encode the fractional-cover cube as a false-colour H.264 video.

Parameters:

Name Type Description Default
troi Troi

The :class:troi.Troi. Output is written to {troi.out_dir}/{troi.stub}_fractional_cover.mp4.

required
ds_fractional_cover

Optional in-memory fractional cover dataset (with bg, pv, npv variables). If None, Paths(troi).fractional_cover is opened (or generated first).

None
fps int

Frames per second. Default 4.

4
min_size int

Minimum dimension of the output video in pixels. Smaller cubes are upscaled with nearest-neighbour. H.264 requires even dimensions, so the final size is rounded down.

1080

Returns:

Name Type Description
str

Filesystem path of the generated MP4.

Raises:

Type Description
RuntimeError

If the ffmpeg invocation returns a non-zero exit code.

PaddockTS.Plotting.fractional_cover_paddocks_video.fractional_cover_paddocks_video

fractional_cover_paddocks_video(troi: Troi, paddocks_filepath: str | None = None, ds_fractional_cover=None, ds_sentinel2=None, fps: int = 4, min_size: int = 1080, label_col: str | None = None)

Encode a fractional-cover video with paddock outlines + labels.

Parameters:

Name Type Description Default
troi Troi

The :class:troi.Troi. Output is written to {troi.out_dir}/{paddocks_stem}_fractional_cover_paddocks.mp4.

required
paddocks_filepath str | None

Path to the paddocks file. If None, uses SAM paddocks from {troi.tmp_dir}/{troi.stub}_sam_paddocks.gpkg.

None
ds_fractional_cover

Optional in-memory fractional cover dataset. If None, opens (or generates, then opens) Paths(troi).fractional_cover.

None
ds_sentinel2

Optional in-memory Sentinel-2 dataset, used only to read the rasterisation transform. If None, opens the pysentinel2 cube.

None
fps int

Frames per second. Default 4.

4
min_size int

Minimum dimension of the output video in pixels.

1080

Returns:

Name Type Description
str

Filesystem path of the generated MP4.

Raises:

Type Description
RuntimeError

If the ffmpeg invocation returns a non-zero exit code.


Diagnostic plots

Calendar plot

One page per year (split into multiple pages if there are more paddocks than max_paddocks_per_page). Rows are paddocks (largest area at top); columns are 48 evenly-spaced slots across the year (4 per month). Each cell shows the Sentinel-2 RGB thumbnail of that paddock at the observation closest to the slot's day-of-year, with non-paddock pixels masked black.

Now built with matplotlib (was PIL in earlier releases). The thumbnail grid is composited into a single numpy array and drawn with one imshow; titles, month names, and paddock labels are matplotlib text — vector when the page is written into the PDF report, so labels remain readable at any zoom and aren't subject to the PNG-embed shrink that capped text at ~13 pt in the previous PIL design.

Two public entry points:

  • calendar_plot(troi, ...) — saves one rasterised PNG per page under troi.out_dir (standalone view).
  • iter_calendar_figures(troi, ...) — generator yielding (year, page_idx, fig) triples without touching disk. Used by make_pdf to embed each page directly into the PDF as vector text.
from PaddockTS.Plotting.calendar_plot import calendar_plot

# Standalone PNGs (rasterised)
calendar_plot(q, thumb_size=64, max_paddocks_per_page=20)
# -> {out_dir}/<stem>_calendar_<year>_p01.png  (one per year × page chunk)

To consume figures programmatically (e.g. embed in your own PDF):

from PaddockTS.Plotting.calendar_plot import iter_calendar_figures
import matplotlib.pyplot as plt

for year, page_idx, fig in iter_calendar_figures(q):
    fig.savefig(f"/tmp/cal_{year}_p{page_idx}.svg")  # vector
    plt.close(fig)

PaddockTS.Plotting.calendar_plot.calendar_plot

calendar_plot(troi: Troi, ds_sentinel2: Dataset | None = None, paddocks_filepath: str | None = None, thumb_size: int = 64, label_col: str | None = None) -> list[str]

Render and save one calendar PNG per paddock.

Each PNG shows that paddock across every year in the troi. They are matplotlib-rasterized at 200 dpi for standalone viewing; for the PDF report, :mod:PaddockTS.Plotting.make_pdf calls :func:iter_calendar_figures directly so the text stays vector.

Parameters:

Name Type Description Default
troi Troi

The :class:troi.Troi.

required
ds_sentinel2 Dataset | None

Optional in-memory cleaned Sentinel-2 dataset. If None, opened (or downloaded + cleaned) from the pysentinel2 cube (cloud-masked, on read).

None
paddocks_filepath str | None

Path to the paddocks file. If None, defaults to Paths(troi).sam_paddocks.

None
thumb_size int

Edge length of each thumbnail in pixels (input resolution; matplotlib resizes for display). Default 64.

64
label_col str | None

Column in the paddocks GeoDataFrame to use for the per-page title. None"P{id} {area:.0f}ha".

None

Returns:

Type Description
list[str]

list[str]: Paths of the generated PNGs (one per paddock).

PaddockTS.Plotting.calendar_plot.iter_calendar_figures

iter_calendar_figures(troi: Troi, paddocks_filepath: str | None = None, ds_sentinel2: Dataset | None = None, thumb_size: int = 64, label_col: str | None = None) -> Iterator[tuple[int, plt.Figure]]

Yield (paddock_id, fig) — one page per paddock.

Each figure shows that paddock across every year in the troi (years as rows, months as columns), so the same paddock can be compared year-over-year, and every paddock starts on a fresh page. Paddocks are yielded largest-area first.

Does not write to disk. Used by :mod:PaddockTS.Plotting.make_pdf to embed each page directly into the report PDF as a vector-text page. The caller is responsible for plt.close(fig) after consuming each Figure.

Phenology plot

Multi-panel PNG: rows are paddocks, columns are years. Each panel overlays the raw vegetation-index series (filled blue dots) and the resampled-and-smoothed series (open blue dots) on a DOY axis, with SoS / PoS / EoS DOYs drawn as vertical reference lines.

Font sizes come from the package-wide matplotlib rcParams (see Global font defaults above).

from PaddockTS.Plotting.phenology_plot import phenology_plot

phenology_plot(q, variable="NDVI", max_paddocks_per_page=8)
# -> {out_dir}/<stem>_phenology_p01.png

PaddockTS.Plotting.phenology_plot.phenology_plot

phenology_plot(troi: Troi, phenology_results: dict[int, DataFrame] | None = None, ds_yearly: dict[int, Dataset] | None = None, ds_paddockTS: Dataset | None = None, variable: str = 'NDVI', paddocks_filepath: str | None = None, max_paddocks_per_page: int = 8, label_col: str | None = None) -> list[str]

Plot per-paddock × per-year phenology curves with SoS / PoS / EoS markers.

Parameters:

Name Type Description Default
troi Troi

The :class:troi.Troi. Output is written to {troi.out_dir}/{paddocks_stem}_phenology.png.

required
phenology_results dict[int, DataFrame] | None

Optional {year: DataFrame} from :func:PaddockTS.Phenology.estimate_phenology. If None, recomputed on demand using variable.

None
ds_yearly dict[int, Dataset] | None

Optional {year: Dataset} from :func:PaddockTS.Phenology.make_yearly_paddock_time_series. If None, built on demand.

None
ds_paddockTS Dataset | None

Deprecated and unused. Retained for backward compatibility with callers that still pass it (the raw overlay was removed in favour of plotting only the sampled + interpolated series, matching the web viewer).

None
variable str

Vegetation index column to plot. Default 'NDVI'; 'NIRv' and 'CFI' also work.

'NDVI'
paddocks_filepath str | None

Path to the paddocks file. Used to derive the output filename stem. If None, defaults to {troi.stub}_sam_paddocks.

None
max_paddocks_per_page int

Maximum number of paddocks per output image. Default 8. Prevents images from becoming too tall with many paddocks.

8
label_col str | None

Column name to use for paddock labels. If None, uses the paddock column (numeric ID).

None

Returns:

Type Description
list[str]

list[str]: Filesystem paths of the generated PNGs.

OzWALD climate plot

One PNG per group (temperature, precipitation, wind, radiation). Reads the cached daily CSV produced by download_ozwald_daily.

from PaddockTS.Plotting.ozwald_plot import ozwald_daily_plot

ozwald_daily_plot(q)
# -> {out_dir}/<stub>_ozwald_daily_temperature.png
# -> {out_dir}/<stub>_ozwald_daily_precipitation.png
# -> {out_dir}/<stub>_ozwald_daily_wind.png
# -> {out_dir}/<stub>_ozwald_daily_radiation.png

PaddockTS.Plotting.ozwald_plot.ozwald_daily_plot

ozwald_daily_plot(troi: Troi, groups: dict = None)

Plot OzWALD daily climate variables grouped by theme.

Reads the daily series from the machine-wide :mod:pyozwald store and writes one PNG per group to {troi.out_dir}/{troi.stub}_ozwald_daily_{group}.png.

Parameters:

Name Type Description Default
troi Troi

The :class:troi.Troi.

required
groups dict

Optional override of the default grouping. Maps a group name to {'vars': [...], 'ylabel': str, 'title': str, 'kind': 'line'|'bar'}. If None, uses :data:DAILY_GROUPS (temperature, precipitation, wind, radiation).

None

SILO climate plot

One PNG per group (temperature, rainfall, radiation, evapotranspiration, humidity). Reads the cached SILO CSV produced by download_silo.

from PaddockTS.Plotting.silo_plot import silo_plot

silo_plot(q)
# -> {out_dir}/<stub>_silo_temperature.png  ... etc.

PaddockTS.Plotting.silo_plot.silo_plot

silo_plot(troi: Troi, groups: dict = None)

Plot SILO climate variables grouped by theme.

Fetches the daily SILO series from the machine-wide pysilo store (downloading only what's missing) and writes one PNG per group to {troi.out_dir}/{troi.stub}_silo_{group}.png.

Parameters:

Name Type Description Default
troi Troi

The :class:troi.Troi.

required
groups dict

Optional override of the default grouping. If None, uses :data:PLOT_GROUPS (temperature, rainfall, radiation, evapotranspiration, humidity).

None

Terrain plot

2 × 2 panel: elevation, D8 flow accumulation, aspect, slope. Reads the Copernicus DEM downloaded by download_terrain, applies a Gaussian smoother before flow analysis (sharp DEMs produce striped artefacts), and reprojects to the Sentinel-2 grid for easy overlay.

from PaddockTS.Plotting.terrain_tiles_plot import terrain_tiles_plot

terrain_tiles_plot(q, sigma=10)
# -> {out_dir}/<stub>_topography.png

PaddockTS.Plotting.terrain_tiles_plot.terrain_tiles_plot

terrain_tiles_plot(troi: Troi, ds_sentinel2=None, sigma: int = 10)

Plot a 2 × 2 panel of elevation, flow accumulation, aspect, and slope.

Parameters:

Name Type Description Default
troi Troi

The :class:troi.Troi. Output is written to {troi.out_dir}/{troi.stub}_topography.png.

required
ds_sentinel2

Optional in-memory Sentinel-2 dataset, used as the spatial reference grid that the terrain tiles are reprojected onto. If None, the cloud-masked window is read from the pysentinel2 cube.

None
sigma int

Standard deviation of the Gaussian smoother applied to the DEM before flow analysis, in pixels. Larger values produce smoother, less-striped flow fields at the cost of losing fine drainage detail. Default 10.

10

Returns:

Name Type Description
str

Filesystem path of the generated PNG.


PDF report

make_pdf stitches the plots produced for a troi into a single A4-landscape PDF with section headers — Landscape (topography), Climate (SILO, OzWALD), Satellite Calendar (SAM + user paddocks), Phenology (SAM + user paddocks).

Output: {troi.out_dir}/{troi.stub}_report.pdf.

from PaddockTS.Plotting.make_pdf import make_pdf

# SAM-only report
make_pdf(q)

# Include user-paddocks sections too, with custom row labels in the
# user calendar
make_pdf(q, paddocks_filepath="/path/to/paddocks.gpkg",
         label_col="paddock_name")

What's in the PDF

  • PDF metadataTitle, Author, Subject (includes bbox + dates), Keywords, and Creator are set on the document so the report shows up cleanly in viewer tabs and file-manager properties.
  • Cover page with the troi stub, dates, and bbox.
  • Sections — each one has a header page followed by its content. Sections marked requires_user_paddocks=True (the User-paddocks calendar + phenology) are skipped if no paddocks_filepath is passed.
  • Calendar pages are written into the PDF as matplotlib figures directly (via iter_calendar_figures), so titles, month labels, and paddock labels are vector text — full-size in the report, not subject to the PNG-embed shrink that affects other image-based sections.
  • Other image pages (topography, climate panels, phenology) are embedded from the PNGs on disk. Rasterised at 220 DPI (was matplotlib's default 100 DPI) so small text stays crisp.

How sections are declared

The SECTIONS module-level list defines the report layout. Each entry is a 3-tuple (title, plot_patterns, requires_user_paddocks):

SECTIONS = [
    ('Landscape',                 [('Topography', '{stub}_topography.png')], False),
    ('Climate – SILO',            [...], False),
    ('Climate – OzWALD',          [...], False),
    ('Satellite Calendar (SAM)',  [...], False),
    ('Satellite Calendar (User)', [...], True),   # skipped without paddocks_filepath
    ('Phenology (SAM)',           [...], False),
    ('Phenology (User)',          [...], True),
]

The patterns use {stub} / {sam_stem} / {user_stem} placeholders, expanded to filesystem globs against troi.out_dir.

PaddockTS.Plotting.make_pdf.make_pdf

make_pdf(troi: Troi, paddocks_filepath: str | None = None, label_col: str | None = None)

Generate a PDF report combining all plots for a troi.

Parameters:

Name Type Description Default
troi Troi

The :class:troi.Troi.

required
paddocks_filepath str | None

Optional path to the user-provided paddocks file. If provided, includes user paddock calendar and phenology plots in the report.

None
label_col str | None

Column in the user paddocks file to use for per-row labels in the user-paddocks calendar pages. Ignored for the SAM calendar (which always uses the numeric paddock id).

None

Returns:

Name Type Description
str

Filesystem path of the generated PDF.