feat(styling): add a hatch encoding to Contour, with a hatch legend helper - #362
Merged
Merged
Conversation
…elper cleopatra could fill a field and draw isolines over it, but not draw a pattern over it -- no way to mark a region without spending the colour channel the data already carries. Add a hatch encoding to the Contour group plus a hatch_legend helper, so a significance / uncertainty overlay (a second, unfilled contour set hatched wherever a mask is true, with a legend entry) can be expressed. - Contour gains hatches / fill / hatch_color; to_options emits only the fields set, so passing a Contour never clobbers a glyph default - the ArrayGlyph contourf path applies hatches, and fill=False renders the bands unfilled (colors="none") so only the hatch marks draw; an unfilled set is not colour-mapped, so its vmin/vmax/norm and colorbar are dropped, and hatch_color recolours the strokes per set (no global hatch.color rcParam) - hatch_legend builds Patch proxies -- the pattern counterpart to disjoint_legend - the three keys are registered in ARRAY_DEFAULT_OPTIONS and the grouped-kwarg hints, so a loose hatches=/fill=/hatch_color= raises the same guided error levels= does - scoped to ArrayGlyph; inert on glyphs that do not model the keys Closes #354
- hatch_legend: default transparent-fill/black-edge, custom facecolor/edgecolor, legend-kwarg forwarding, and the equal-empty boundary of the length guard - contourf: hatch_color on a filled set recolours the strokes while keeping its colorbar (previously exercised only via the unfilled overlay)
The hatch_legend directive was inserted between disjoint_legend and its options: block, orphaning the block onto hatch_legend and leaving disjoint_legend rendering with mkdocstrings defaults. Give each its own block so both mirror the other legend builders.
The generic name 'fill' sat in the shared _GROUPED_KWARG_HINTS map, which gates every glyph's construction via the base __init__. A caller of another glyph (e.g. filled polygons) passing a loose fill= would be misdirected to contour=Contour(fill=False). Mirror the existing 'alpha' carve-out: drop 'fill' from the global map and reject a loose fill= locally in ArrayGlyph (construction and plot) via _reject_loose_fill, with the array-specific hint. 'hatches'/'hatch_color' stay global (unlikely to collide).
im.set_edgecolor(hatch_color) only recoloured the rendered hatch strokes when rcParams["hatch.color"] == "edge" -- the default only from matplotlib 3.11. On an older matplotlib, or whenever a user customised hatch.color, the hatches rendered black regardless of hatch_color, silently, and the band edges were recoloured as a side effect. Use QuadContourSet.set_hatchcolor (matplotlib >= 3.11), which recolours the hatch strokes independently of the rcParam and leaves the edges alone, and raise the matplotlib floor to >=3.11 accordingly. The test now asserts the rendered get_hatchcolor() under hatch.color="black" (so it fails if the recolour regresses to the edge) and that the band edges are not reddened.
hatches/fill/hatch_color are documented as contourf-only, but the block ran for kind='contour' too: set_hatchcolor recoloured the isolines and hatches was silently accepted-then-ignored. Gate all three on kind=='contourf' and warn when they are supplied with kind='contour' instead of mutating unrelated line-contour output.
- fill=False with no hatches renders an invisible contour set (colors='none' and nothing drawn) with a suppressed colorbar -- warn and point at hatches=. - hatch_color with no hatches has no effect -- warn. - an unfilled overlay drops the colorbar; warn when the caller explicitly requested one (colorbar= arg or add_colorbar in the explicit options) rather than silently ignoring the request.
The loose-reject suite only checked hatches=; add fill= (now rejected locally in ArrayGlyph) and hatch_color= (rejected via the global map), each asserting the guided contour=Contour(...) hint.
The floor bump in pyproject.toml (>=3.9 -> >=3.11) was not propagated to uv.lock, whose requires-dist still recorded matplotlib>=3.9. The resolved version (3.11.1) satisfies both, so runtime and plain 'uv run' CI are unaffected, but uv sync --locked / uv lock --check would fail on the mismatch. Regenerate: the only change is the matplotlib specifier line.
…acklevel - The contourf-only warning fired only inside the contour/contourf branch, so it caught kind='contour' but silently dropped hatch fields on the default kind='auto', imshow, pcolormesh, and in animate (imshow-only). Move it above the kind dispatch (kind is already the resolved effective kind) so every non-contourf kind reports the ignored fields, animate included. - The hatch warnings emitted from the _plot_im_get_cbar_kw helper used stacklevel=2, which points at cleopatra's own plot() frame; bump to stacklevel=3 so they blame the caller's line.
to_options() aliased the caller's hatches list into default_options and on into matplotlib. Return a defensive list() copy (as hatch_legend already does for its inputs) so the frozen dataclass does not share mutable list contents with the render options.
- S9088/S5778: hoist the Contour(...) and DataStyle(...) constructors out of the pytest.warns / pytest.raises blocks so each block wraps a single call that can emit the warning/exception. - S9083: drop the empty parentheses from the TestHatchLegend ax fixture decorator (@pytest.fixture). No behaviour change; the assertions are unchanged.
|
MAfarrag
added a commit
that referenced
this pull request
Sep 13, 2026
After merging #362 (hatch encoding), classify and Contour(fill=False) both live in the contourf path. fill=False renders colors="none" (an unfilled hatch-only overlay), so a co-passed classify draws no class colours even though it still sets the band edges and a stepped colorbar. Warn that the class colours are not drawn in that case, consistent with the color_scale / data_style conflict warnings. Test covers the combination.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Description
Adds a hatch encoding to the contour options, plus a
hatch_legendhelper — the two things #354 asks for.cleopatra could fill a field and draw isolines over it, but could not draw a pattern over it: no way to mark a
region without spending the colour channel the data already carries. This lets a caller express the significance /
uncertainty overlay — a filled field plus a second, unfilled contour set drawn as hatching wherever a mask is true,
with one legend entry explaining the pattern.
Contourgains three fields —hatches(a pattern per band),fill(False→ unfilled overlay), andhatch_color(per-set stroke colour).to_options()emits only the fields that were set, so passing aContournever clobbers a glyph default.
contourfpath — applieshatches;fill=Falserenders the bands unfilled (colors="none", soonly the hatch marks draw). An unfilled set is not colour-mapped, so its
vmin/vmax/normand its colorbar aredropped;
hatch_colorrecolours the strokes for that set only (viaset_edgecolor, avoiding the process-globalhatch.colorrcParam).hatch_legend— aPatch-proxy legend next todisjoint_legend, the pattern counterpart to it.ARRAY_DEFAULT_OPTIONSand_GROUPED_KWARG_HINTS, so a loosehatches=/fill=/hatch_color=raises the same guidedcontour=Contour(...)errorlevels=does; the newkeys ride the existing snapshot/rollback. Scoped to
ArrayGlyph; inert on glyphs that do not model the keys.Caller-facing shape (a second glyph onto the same axes, the composition path already supported):
No new dependency (
matplotlib.patches.Patchis already imported fordisjoint_legend).Issues
Type of change
Check relevant points.
How Has This Been Tested?
Contour.to_optionsemits the hatch keys only when set (test_scaling.py).kind="contourf"render carriesim.hatches, and an unfilled (fill=False) set draws no colorbar;a loose
hatches=is rejected with thecontour=Contour(...)hint (test_array_glyph.py).hatch_legendlabels, per-patch hatches, and the length-mismatchValueError(test_styles.py); itsdocstring doctests run in
test_styles_doctests.py.None(test_group_rollback.py).pytest --doctest-modules src/cleopatra/styling/params.py src/cleopatra/styling/styles.py— doctests pass.pytest -m "not e2e"→ 3325 passed;ruff check(pinned 0.15.22) clean.Checklist: