This document records the design invariants that govern ggpatchy’s internals. It is written for contributors, not users. If you are adding a pattern or modifying the rendering pipeline, read this first.
The user contract
The pattern reference vignette
(vignettes/pattern-reference.Rmd) is the authoritative
definition of what “correct” looks like. At the default
pattern_spacing, every standard pattern should be visually
consistent and readable on a typical chart element. Any deviation from
that baseline is explicitly the user’s choice, made by setting
pattern_spacing explicitly.
The strongest form of this contract is physical uniformity
across all shapes in the same plot: a small polygon and a large
polygon with the same pattern_spacing have the same
physical distance between hatch lines, the same physical dot size, and
the same physical dot pitch. Users never see a map where small counties
have 2 dots and large counties have 50.
Coordinate system contract (mm)
Pattern spacing is in millimetres.
pattern_spacing = 5 means “5mm between line centres” or
“5mm between dot centres” everywhere — bar, tile, polygon, sf geometry,
violin, density curve, and legend key. Grid resolves millimetres to
device pixels at draw time, independently of viewport size. This
produces physically consistent patterns across all shapes.
Why mm, not npc? npc resolves relative to the
current viewport’s pixel dimensions. A tall bar at 200px and a short bar
at 40px would get very different physical spacings with the same npc
fraction. mm is resolved by grid against device physical dimensions,
independently of viewport size. spacing = 5mm is 5mm
whether the viewport is 200px or 40px.
Implementation constraint: mm → npc
conversion must happen inside a makeContent method — a grid
callback that fires at draw time when the correct viewport is active. It
cannot happen in the pattern function body, which executes before the
shape’s viewport is pushed. Pattern functions construct a custom
gTree (DotPatternTree,
LinePatternTree) that defers conversion to
makeContent. See
specs/SPEC_makecontent_infrastructure.md.
Previous model (v0.5.x and earlier)
In prior versions, pattern spacing was
viewport-relative (bounding-box fraction).
pattern_spacing = 0.08 meant 8% of the shape’s bounding
box. This produced different visual densities across shapes of different
physical sizes — most visibly on choropleth maps where small and large
regions received different dot counts.
Legend key spacing
The legend key communicates pattern identity — what type of
pattern is this? Because pattern_spacing is now in mm, the
key correctly shows the same physical density as the data. No override
is needed or applied.
pattern_size and pattern_angle are passed
through from user data, as they always have been.
Pattern functions are pure
Pattern functions fn(x, y, width, height, gp, params)
have no side effects and make no assumptions about rendering context
beyond what is passed to them. They must not inspect global state,
device dimensions, or parent viewport dimensions.
Dot radius uses "mm".
unit(r, "mm") is physically circular on all devices at all
scales. Do not use "npc" for dot radius — it resolves using
the x-axis of the viewport only and produces ovals on non-square
viewports.
Diagnostic checklist for pattern rendering issues
If patterns look wrong (wrong density, oval dots, legend mismatch), check in this order:
Are dots oval instead of circular? Dot radius must use
"mm", never"npc".unit(r, "npc")resolves anisotropically on non-square viewports.Is density inconsistent across shapes of different sizes? Check that
makeContent.DotPatternTreeandmakeContent.LinePatternTreeare callingconvertWidth/convertHeightinsidemakeContent, not in the pattern function body.Is
draw_key_patternpassingwidth = 0.9, height = 0.9? It should be — verifypattern_fn(x = 0.05, y = 0.05, width = 0.9, height = 0.9, ...).Does the legend key look wrong for sparse/dense variants? In the mm model, the legend shows the same physical spacing as the data — a dense pattern swatch has denser lines. This is correct and expected.
Is there a
bbox_scale,sqrt(width * height), or panel-dimension division in a pattern function? Remove it. Pattern functions receive bbox-normalised coordinates; they must not apply additional scaling.