Fix axis spine corner overextension - #5765
Conversation
…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 <[email protected]>
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 <[email protected]>
1d0974d to
40f7c76
Compare
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 <[email protected]>
6bb16eb to
98e4d14
Compare
|
Gentle bump: open to making any changes here (personally currently leaning more towards a |
|
Might be nice to have |
| "The size of the xtick marks. Marks are drawn from the spine centerline, so `spinewidth / 2` of this is covered by the spine when `xtickalign = 0`." | ||
| xticksize::Float64 = 5.5f0 | ||
| "The size of the ytick marks. Marks are drawn from the spine centerline, so `spinewidth / 2` of this is covered by the spine when `ytickalign = 0`." | ||
| yticksize::Float64 = 5.5f0 |
There was a problem hiding this comment.
These are changes we probably don't want, for two reasons:
- They change how you have to scale ticksize in relation to spinewidth. I.e. if you scale spinewidth, you now have to scale ticksize too to keep the same amount of the tick visible
- When the frame is (partially) transparent, you now see some of the tick under it.
There was a problem hiding this comment.
On 1: As suggested below and noted as alternative in OP, a way to overcome this could be to use an automatic mode for ticksize, resolving to 5.0 + spinewidth/2.
On 2: Agreed, no way to avoid that with a partially transparent spine. I do think there are very few reasonable cases, if any, where one would want a transparent spine though.
|
I think the solution for the ticks is to fix what align means, the outside is already correct but inside isn't, if we have inside the mirror of outside then 0.5 will also give correct centering. And for someone who wants to have the tick start at the spine center they'll be able to set the align to something slightly below 1, depending on the spine width and tick size, and that will be exact, too. That will need a small computation once and can then be set in the theme. It doesn't change the meaning of tick length and doesn't churn refimages for the default outside ticks. For inside ticks it's a visual fix. |
I can do that - before I do though, I just want to confirm you want that as API? E.g., I could imagine one might eventually want to not draw the axis as a set of four disconnected lines, but as a single connected rectangle. In that case, it'd be nicer to be able to control how the corner was drawn (I think |
I don't like this ambiguity about ticksize. Actually thinking more about this, I think just adding two symbols for tick aligns could work here which simply take strokewidth into account. The numbers keep meaning the stroke-corrected thing they do now (with the inward one fixed because that's currently off), and then we just add And the framelines we can switch to |
|
I only meant align, not ticksize. If you want to align your ticks with the center of the spine and have them look the same as when they're aligned with the edge, you need to bump the ticksize by half the spine width. I think that's acceptable and it can be easily themed. |
Ticks are anchored on the spine centerline, so half a spine width of each
mark is covered by the spine. The previous commit compensated with a literal
default (`ticksize` 5 -> 5.5), which only cancels out at the default
`spinewidth = 1`: an axis with a thicker spine and an untouched `ticksize`
lost visible tick length, badly so once `spinewidth` approached `2 * ticksize`.
Make `ticksize` and `minorticksize` default to `automatic` instead, resolving
to `5 + spinewidth / 2` and `3 + spinewidth / 2`. The mark left visible
outside the spine then keeps its size at any spine width, so default axes
render exactly as they did before this PR regardless of `spinewidth`. Set to
a number, `ticksize` still means one thing -- the drawn stroke length.
`LineAxis` resolves both sizes once, up front, so the rest of the function
works with plain numbers; `Axis` does the same before handing them to
`mirror_ticks`. `resolve_ticksize_with_base` dispatches on `::Automatic`, and
its `::Float32` return annotation is what lets the resolved observables be
asserted `Observable{Float32}`, since `0.5f0 * spinewidth` is otherwise
`Float64`. The tick size attributes are left unannotated, since
`Union{Automatic, Float64}` will not convert an `Int` and half of them were
unannotated already.
`generic_plot_attributes(LineAxis)` gets the same `automatic` default, which
also settles a stale value there. `Axis`, `Colorbar` and this fallback were
last in sync in MakieOrg#721, at `ticksize = 6` / `minorticksize = 4`. The 2023
figure-size rescale then took `Axis` and `Colorbar` to 5 and 3 without
touching `defaultattributes.jl`, so the fallback kept the older numbers by
oversight -- its `spinewidth` is 1 like everywhere else, so nothing justified
the difference. A directly constructed `LineAxis(scene; ...)` therefore ends
up with visibly 5 and 3 units of tick rather than 6 and 4; the anchor change
contributes none of that, it is the rescale finally arriving. `Axis` and
`Colorbar` always pass their own tick sizes and never consult this fallback.
Verified against master on CairoMakie: `trimspine` (spinewidth 8, default
ticksize) is geometrically identical, tick columns and tips on the same
pixels, differing only in ~0.09% antialiasing. The two `ticksmirrored` tests
still change, since they set `ticksize = 10` explicitly.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Bz52CGcoLBg8Pgevw5b9ER
15290a5 to
6638f81
Compare
|
Okay - I think I'm following now, but admit to some confusion earlier. Perhaps we're closer than I imagined? I'd been reading the earlier comment as objecting to the Aside from that: I've pushed the On ticks being spine-center-aligned by default rather than opt-in: my motivation is post-processing figures, very often ones I didn't make. An opt-in would help whoever knows to reach for it, but I'd guess that number will never climb very high. So I'd still be sent figures by collaborators in the old floating-ticks mode, unless the default is changed. I think it's a sensible default, since it allows restyling the axis lines in post-production without introducing artifacts (and obliquely supported also by 3 of 4 tools above doing this as well). On bumping `tickalign` value that puts a tick tip on the spine centre, if default is tip-placement at spine edgeAs context, if the default is chosen as spine-edge-anchored behavior at tickalign (current master), the
On refimages: with Drive-by change: The last commit also made a small fix in On framelines: Agreed - leave it at |
Yes but I don't want the normal numbers to change meaning, they should stay edge aligned (and the inward case fixed if it's currently not correctly aligned). People who care about this specific behavior can choose the spine center alignment, for default users it will edge align as usual. So your PR should only add the symbols with the special center-aligned meaning. |
|
Between catching up on Slack comments and comments here I'm getting a bit confused. My understanding of what Julius suggested is:
Is that correct?
The problem with that is that it still changes the meaning of
You can define something like my_theme = Attributes(
Axis = Attributes(
xtickalign = 0,
ytickalign = 0
)
)and apply it globally with |
My understanding was not quite that, rather:
So, It's not my preference, but would still be an improvement. I'll try to get to it later this week. On the |




🙋♂️ As discussed on Slack with @jkrumbiegel, this PR seeks to fix two interrelated things:
spinewidth / 2to 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:squarecap instead of a:buttcap.spinewidth / 2away from the actual axis centerline. This gives some real surprises, especially withtickalign ≠ 0, since the stroke is then not placed in a natural position at e.g.,tickalign = 0.5or1. Fix this by starting/anchoring thetickalign = 0(andtickalign = 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 * spinewidthpast 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 * spinewidthoutside the spine centerline and ranticksizeoutward from there (shifted inward bytickalign * 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:
The drawn mark is exactly
ticksizelong for everytickalign, which simply slides it across the centerline.spinewidthdrops 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 —
ticksize5 → 5.5 andminorticksize3 → 3.5, forAxis,ColorbarandLineAxis— andtickspace(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 setsticksizeexplicitly gets marks that appearspinewidth / 2shorter than before.Open question: the
5 → 5.5default is a literal, so it only cancels the change whenspinewidthis also default. An axis withspinewidth = 8and untouchedticksizegets marks appearing1.5long (=8/2 - 5.5) instead of5.0. One alternative isticksize = automatic, resolved as5f0 + 0.5f0 * spinewidth, which cancels it for any spine width. I went with the literal because the point of this change is to maketicksizean unconditional stroke length, and a spinewidth-coupled default reintroduces exactly the coupling being removed — but there's a definite trade-off.What changing
tickalignlooks like before/afterspinewidth = 12,ticksize = 18. After: the drawn mark is a constant length andtickalignslides it across the centerline; at1.0it 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 withtickalign.🤖 Generated with Claude Code
Checklists
Type of change
Checklist