Skip to content

Contour geoms - #1118

Merged
has2k1 merged 29 commits into
mainfrom
feat-geom-contour
Sep 3, 2026
Merged

has2k1 merged 29 commits into
mainfrom
feat-geom-contour

Conversation

@has2k1

@has2k1 has2k1 commented Sep 3, 2026

Copy link
Copy Markdown
Owner

This PR adds geom_contour, geom_contour_filled and geom_density_2d_filled, so you can draw a surface sampled on a grid as contour lines or filled bands. Today geom_density_2d only contours a density it computes itself from raw points, which leaves no way to show the shape of a dataset like faithfuld, where a density is already stored
for every waiting/eruptions pair.

(
    ggplot(faithfuld, aes("waiting", "eruptions", z="density"))
    + geom_contour_filled(bins=8)
)

This PR also gives geom_polygon a subgroup aesthetic, so a filled band can have holes.

One behaviour change to flag: stat_density_2d now splits density into ten bands instead of five and uses the same contour heights in every panel, so existing geom_density_2d plots gain contour lines. levels is deprecated in favour of bins and breaks.

closes #1084

Contour layers can now derive break values from a band count or width, accept
explicit values, or call a user-supplied break function. The default selects
values for ten bands across the surface range.

Surface coordinates can also be arranged into sorted x and y axes, with
missing cells represented by `nan`.
A bin count below one, a zero width, and a negative width previously reached
dependencies and produced inconsistent exceptions, warnings, or empty
output. Contour break selection now rejects each value immediately with a
`PlotnineError`.
Gridded surfaces can now produce contour lines and the filled regions between
neighbouring contour values. Each disconnected line or region receives its
own plot group, and filled regions retain the rings needed to represent
holes.

The results include contour values, scaled values, piece identifiers, and
band bounds suitable for computed-aesthetic mappings.
Equal break values made band labelling increase precision forever. Labelling
now stops at float64 round-trip precision and raises a `PlotnineError` when
the values remain indistinguishable.

Line and band calculations that generate no vertices now return zero-row
data with the same columns and categorical levels as populated results.
Downstream stats can process empty contours without special cases.
Contour calculation requires rows and columns of x and y values. Rotated
grids provide neither, so surface coordinates are now aligned with the axes
before contouring and restored afterwards.

Repeated coordinates retain their last value and emit a warning. Grid angle
detection uses the dominant neighbour angle, with the longest convex-hull
edge as a fallback.
The rotation test built its fixture from rounded coordinates, unlike the raw
layer data used in practice. This difference could conceal failures at large
coordinate magnitudes.

The fixture now uses direct rotation arithmetic and covers a grid offset by
100,000 units, so angle estimation is tested against its actual input
precision.
`geom_polygon` now treats each `subgroup` as one ring within a polygon.
Oppositely wound inner rings render as holes, while the first ring forms the
exterior.

`subgroup` divides an existing plot group instead of creating a new one.
Non-linear coordinates close and interpolate each ring independently.
Under non-linear coordinates, interpolation treated only plot-group changes
as path breaks. It therefore connected a polygon's exterior ring to its
holes.

Ring changes now mark the same hard boundary as group changes. Boundary
segments receive no interpolated points, so each ring ends before the next
begins.
`geom_contour` maps a gridded surface to separate paths at selected heights.
Its stat accepts x, y, and z values together with a band count, width,
explicit values, or a break function.

Facet panels share contour heights. Rotated grids are aligned before
contouring, duplicate coordinates keep their last value, and missing cells
remain unfilled.
When a rotated surface produced no contours, restoring its coordinates tried
to reduce an empty array and raised. Rotation now returns empty arrays
unchanged, so a valid empty contour result reaches the layer.

Populated and empty results now follow the same coordinate-restoration path.
`geom_contour_filled` fills each region between neighbouring contour values.
Disjoint regions receive separate polygon groups, and inner rings become
holes through the `subgroup` aesthetic.

The default mapping fills by ordered band interval. Computed variables expose
each band's bounds, midpoint, scaled upper bound, piece, and ring.
Document `subgroup` as the ring index within each polygon piece. Ring 0 is
the exterior; later rings are holes.
The group-apply utility annotated each extra positional argument as a tuple,
so Pyright rejected valid scalar and array arguments. Its variadic parameter
now accepts values of any type, matching its pass-through behaviour.
`stat_density_2d` can now contour `density`, `ndensity`, or `count`. It
computes break values across the complete layer, so facet panels use
comparable contour heights.

Callers can choose a band count, width, explicit values, or a break function.
The previous `levels` parameter is deprecated. Without contouring, the stat
exposes the density measures and observation count directly.
The observation count is constant within a density group, so the contour
stage restored it as if it were an original aesthetic. Other density-grid
columns disappeared only because they varied by point.

Contoured output now excludes every column created by density estimation,
including `n`, while retaining original constant aesthetics.
`geom_density_2d_filled` now fills the regions between neighbouring contour
values from a two-dimensional kernel density estimate. It supports the same
contour variable and break controls as `geom_density_2d`.

The default fill maps to ordered band intervals. Computed variables expose
the density values, band bounds, pieces, and polygon rings available at each
stage.
Document the density variables available before contouring and the band
variables available afterwards. The band output includes interval labels,
bounds, midpoints, scaled levels, polygon pieces, and ring indices.
Describe a piece as one disconnected polygon within a band, rather than as
the complete band. Keep every computed-aesthetic description in the same
grammatical form.
Rounding the estimated grid angle worked for small integer grids but moved
realistic coordinates off their shared rows and columns. The gaps produced
broken or missing contours.

Angle selection now uses rounded values only to find the dominant direction,
then returns the full-precision mean. The inverse rotation uses a wider
rounding tolerance, so rotated grids realign without discarding excess
precision.
Verify that facet panels share contour heights, missing surface values leave
holes, and rotated grids produce the same complete contours as axis-aligned
grids.
Document the new line, filled-surface, and filled-density contour geoms.
Describe polygon holes through the `subgroup` aesthetic and the new contour
controls and shared panel heights for two-dimensional density estimates.
Verify that a rotated grid at realistic coordinate scales recovers its
original row and column counts. Approximate angle equality alone did not
detect precision loss that separated aligned coordinates.

Also contain the expected missing-value warning and confirm that both facet
panels contribute data before checking their shared contour spacing. Clarify
why the rotation tolerance depends on grid extent.
Contouring above the surface range or over identical points produced no rows
and lost the layer's panel metadata. Plot building then raised a bare
`KeyError` instead of drawing an empty panel. Empty contour results now retain
their full schema and original constant columns.

`ndensity` now scales by the largest non-missing density, so one missing
estimate no longer makes the complete column missing.
Tables may leave `subgroup` missing for polygons that contain only one ring.
Those rings were dropped during grouping, which either removed a hole without
warning or raised an unrelated concatenation error.

Missing values now identify one ring like any other. The ring renders in
linear coordinates and remains continuous while non-linear coordinates
interpolate its edges.
Single-cell grids, flat surfaces with a requested band count, and filled
contours with only one break previously raised dependency errors or divided
by zero. They now warn that no contours were generated and return an empty
result with the expected schema.

Explicit break values are now sorted and deduplicated. Break functions always
receive a contour distance derived from the band count, the requested width,
or one tenth of the range.
Passing deprecated `levels` with `bins` or `breaks` silently discarded the
explicit replacement. The replacement now takes precedence, and the warning
identifies which parameter was used.

The deprecation now uses `FutureWarning` and states that `levels` will be
removed in a future version.
Surface contouring needs the API that calculates several contour values in
one call. The version supplied indirectly by Matplotlib does not guarantee
that API, so declare `contourpy>=1.2.0` directly.

Also document that two-dimensional density plots now use ten contour bands
by default instead of five.
A polygon without holes can either number its only ring or leave its
`subgroup` missing. Compare both encodings with the same reference image to
state that they represent identical shapes.

Cartesian coverage catches a ring dropped during grouping. Polar coverage
catches a missing value incorrectly treated as a boundary at every vertex.
@has2k1
has2k1 merged commit a8400fa into main Sep 3, 2026
14 checks passed
@has2k1
has2k1 deleted the feat-geom-contour branch September 3, 2026 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Is geom_contour available?

1 participant