Skip to content

Fix axis spine corner overextension - #5765

Open
thchr wants to merge 3 commits into
MakieOrg:masterfrom
thchr:fix-axis-spine-corner-overextension
Open

Fix axis spine corner overextension#5765
thchr wants to merge 3 commits into
MakieOrg:masterfrom
thchr:fix-axis-spine-corner-overextension

Conversation

@thchr

@thchr thchr commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

🙋‍♂️ As discussed on Slack with @jkrumbiegel, this PR seeks to fix two interrelated things:

  1. The axis lines previously (intentionally) overshot the axis corners by spinewidth / 2 to avoid the appearance of a "notch" in the corner. A more elegant way to do this is just to draw the axis lines with a :square cap instead of a :butt cap.
  2. Ticks previously started their stroke spinewidth / 2 away from the actual axis centerline. This gives some real surprises, especially with tickalign ≠ 0, since the stroke is then not placed in a natural position at e.g., tickalign = 0.5 or 1. Fix this by starting/anchoring the tickalign = 0 (and tickalign = 1) strokes to the axis' centerline.

These changes are - for me - chiefly motivated by the fact that the existing drawing approach gives headaches if one wants to restyle an exported CairoMakie figure manually in e.g. Illustrator or Inkscape afterwards: if any axis or tick linewidth is changed, or if the figure itself scaled, the "absolute" positioning hacks lead to very visible artifacts (that I have to admit to historically spending way to much time on fixing, by manually realigning all these little lines).

A fuller Claude-generated description below (especially of the ticks-change).


🤖 CairoMakie-exported axes don't survive being restroked in Illustrator/Inkscape. Two separate causes, one commit each — the first is a straightforward fix, the second involves a judgement call where input/agreement is needed.

1. Spines overshoot the corners (a48775e)

The spine geometry was extended by 0.5 * spinewidth past each axis corner to fill the corner join. That extension is baked in at axis creation, so restroking the exported figure at a different width leaves the overshoot wrong in both directions.

Now the spine ends exactly at the corner and uses linecap = :square, which fills the corner and scales with whatever stroke width the file is rendered at. Rendering is unchanged.

2. Ticks are anchored to the spine's outer edge (1d0974d)

Tick marks started at 0.5 * spinewidth outside the spine centerline and ran ticksize outward from there (shifted inward by tickalign * ticksize). Same coupling problem: widening the spine's stroke in an editor swallows the tick, narrowing it opens a gap.

Ticks are now anchored on the spine centerline — the one place no stroke width can uncover:

outer_len = (1 - tickalign) * ticksize
inner_len =      tickalign  * ticksize

The drawn mark is exactly ticksize long for every tickalign, which simply slides it across the centerline. spinewidth drops out of the tick geometry entirely.

Both changes live in Makie/, so this fixes all backends, not just Cairo.

The judgement call

Because the mark now starts on the centerline rather than the outer spine edge, half a spine width of it is covered by the spine. To keep existing figures looking the same, default tick sizes are raised by half the default spine width — ticksize 5 → 5.5 and minorticksize 3 → 3.5, for Axis, Colorbar and LineAxis — and tickspace (which drives the tick-label gap and the axis protrusion) subtracts that same half width so labels and the plot area stay put.

Default axes therefore render byte-identically to master (verified on CairoMakie: default, mirrored, trimspine, flipped, and Colorbar, majors and minors). But a figure that sets ticksize explicitly gets marks that appear spinewidth / 2 shorter than before.

Open question: the 5 → 5.5 default is a literal, so it only cancels the change when spinewidth is also default. An axis with spinewidth = 8 and untouched ticksize gets marks appearing 1.5 long (=8/2 - 5.5) instead of 5.0. One alternative is ticksize = automatic, resolved as 5f0 + 0.5f0 * spinewidth, which cancels it for any spine width. I went with the literal because the point of this change is to make ticksize an unconditional stroke length, and a spinewidth-coupled default reintroduces exactly the coupling being removed — but there's a definite trade-off.

What changing tickalign looks like before/after tickalign_before_after

spinewidth = 12, ticksize = 18. After: the drawn mark is a constant length and tickalign slides it across the centerline; at 1.0 it sits fully inside with its outer tip on the centerline. Before: the mark hung off the spine's outer edge and the buried fraction varied with tickalign.

🤖 Generated with Claude Code


Checklists

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Checklist

  • Added an entry in CHANGELOG.md (for new features and breaking changes)
  • Added or changed relevant sections in the documentation
  • Added unit tests for new algorithms, conversion methods, etc.
  • Added reference image tests for new plotting functions, recipes, visual options, etc.
    • My hope is that nothing will move - but we will see in CI

@github-project-automation github-project-automation Bot moved this to Work in progress in PR review Aug 28, 2026
thchr and others added 2 commits August 28, 2026 10:19
…etry

The four `Axis` spine segments (plus the opposite and mirrored spines) were
each extended by half a spine width past the axis corners so that, with the
default butt linecap, the segments would overlap into a clean corner instead
of leaving a notch. `create_linepoints` similarly padded trimmed spines by
half a tick width.

Because that extension was baked into the vertex positions as a fixed pixel
distance computed from the spine width at render time, it only lined up at
that exact stroke width. Re-stroking a CairoMakie-exported SVG/PDF in an
external editor (a common step when composing figures) moved every corner
out of alignment.

Instead, end each spine exactly at the corner (or the outer tick, when
trimmed) and set `linecap = :square` on the spine `linesegments!` calls. A
square cap extends the stroke by half its width automatically and is written
into the SVG/PDF as `stroke-linecap="square"`, so the corner coverage now
scales with the stroke when the figure is restyled. At any given stroke
width the rendered result is unchanged.

Tick marks keep their butt linecap and are untouched here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Tick marks were placed relative to the *outer edge* of the spine stroke:
the mark started at `0.5 * spinewidth` outside the centerline and ran
`ticksize` outward from there (shifted inward by `tickalign * ticksize`).
That couples the tick geometry to the spine's stroke width, which is exactly
what breaks when a figure is exported to SVG/PDF and the spine is restroked
in a vector editor -- widening the stroke swallows the tick, narrowing it
opens a gap, and a mark meant to point outward can end up poking into the
plot area.

Anchor the mark on the spine centerline instead, which no stroke width can
uncover:

    outer_len = (1 - tickalign) * ticksize
    inner_len =      tickalign  * ticksize

The drawn mark is exactly `ticksize` long for every `tickalign`, and
`tickalign` (0 = out, 1 = in) simply slides it across the centerline.
`spinewidth` drops out of the tick geometry entirely, here and in
`mirror_ticks`.

At `tickalign = 0` the mark now starts on the centerline rather than the
outer spine edge, so half a spine width of it is covered by the spine. To
keep default axes looking exactly as they did, the default tick sizes are
raised by half the default spine width: `Axis` `ticksize` 5 -> 5.5 and
`minorticksize` 3 -> 3.5, likewise for `Colorbar` and standalone `LineAxis`.
Figures that set `ticksize` explicitly get marks that appear
`spinewidth / 2` shorter than before.

`tickspace`, which feeds the tick label gap and the axis protrusion, now
means "how far the mark reaches past the outer spine edge" and subtracts
that same half spine width, so tick labels and the plot area stay put.

Verified pixel-identical to master on CairoMakie for default, mirrored,
trimmed, flipped and Colorbar axes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@thchr
thchr force-pushed the fix-axis-spine-corner-overextension branch from 1d0974d to 40f7c76 Compare August 28, 2026 08:19
Dropping the geometry extensions from `create_linepoints` also dropped the
`0.5 * tickwidth` that a trimmed spine end used to carry, so the end landed on
the outermost tick's centerline and the `:square` linecap then pushed it half a
spine width further out. With `spinewidth = 8` and a default `tickwidth = 1`
that is a 3.5px stub sticking out past the last tick.

A trimmed end should finish flush with the outer edge of that tick, i.e. half a
tick width past its center, as it did before. The cap already contributes half a
spine width, so the geometry only has to make up the difference:

    trim = 0.5 * (tickwidth - spinewidth)

Untrimmed ends are unaffected -- they still stop at the axis corner and let the
cap fill it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@thchr
thchr force-pushed the fix-axis-spine-corner-overextension branch from 6bb16eb to 98e4d14 Compare August 28, 2026 11:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Work in progress

Development

Successfully merging this pull request may close these issues.

1 participant