Skip to content

Support non-identity axis scales in ablines - #5685

Open
jkrumbiegel wants to merge 2 commits into
masterfrom
jk/ablines-transformed-axes
Open

Support non-identity axis scales in ablines#5685
jkrumbiegel wants to merge 2 commits into
masterfrom
jk/ablines-transformed-axes

Conversation

@jkrumbiegel

Copy link
Copy Markdown
Member

ablines used to error on any non-identity axis scale (ABLines is only defined for the identity transform). But there's no real reason for that: f(x) = slope * x + intercept is defined in data coordinates, so under a transform it just becomes a curve in screen space. This draws it as a subdivided curve (256 points sampled evenly across the visible range), and keeps the cheap 2-point line for the identity transform.

A few details fall out of this:

  • On log-log axes a zero-intercept line maps back to a straight line, as you'd expect; with a nonzero intercept it's genuinely curved.
  • The line value can leave the y-scale domain (e.g. go negative on a log y axis) even when the axis limits are valid. Those points become NaN, so the line breaks there. Since the visible y-range is always inside the domain, the break is always off-screen.
  • Per-line color/linewidth vectors still work, and it now draws with lines rather than linesegments.
f = Figure(size = (600, 500))

ax = Axis(f[1, 1], title = "identity")
ablines!(ax, [0.0, 1.0, 2.0], [1.0, -0.5, 0.25], color = [:orange, :red, :purple], linewidth = 3)
limits!(ax, 0, 10, -2, 6)

ax = Axis(f[1, 2], xscale = log10, title = "log x")
ablines!(ax, 0.0, 0.002, color = :orange, linewidth = 3)
limits!(ax, 1, 1000, 0, 3)

ax = Axis(f[1, 3], yscale = log10, title = "log y, crosses zero")
ablines!(ax, -50.0, 50.0, color = :orange, linewidth = 3)
limits!(ax, 1, 4, 1, 200)

ax = Axis(f[2, 1], xscale = log10, yscale = log10, title = "log-log, zero intercept")
ablines!(ax, [0.0, 0.0, 0.0], [1.0, 3.0, 0.3], color = [:orange, :red, :purple], linewidth = [2, 4, 6])
limits!(ax, 1, 100, 0.1, 1000)

ax = Axis(f[2, 2], xscale = log10, yscale = log10, title = "log-log, nonzero intercept")
ablines!(ax, [1.0, 10.0, 100.0], 1.0, color = [:orange, :red, :purple], linewidth = 3)
limits!(ax, 1, 100, 1, 1000)

ax = Axis(f[2, 3], xscale = sqrt, title = "sqrt x")
ablines!(ax, 0.0, 1.0, color = :orange, linewidth = 3)
limits!(ax, 0, 100, 0, 100)

f
ablines under different axis scales

ablines previously errored on any non-identity transform. It now draws
the line as a subdivided curve in transformed space (256 points),
keeping the efficient 2-point path for the identity transform. Points
whose y leaves the y-scale domain become NaN, so a line crossing into
e.g. negative territory on a log axis breaks off-screen instead of
throwing. Per-line vector color/linewidth are expanded to match. Drawn
with lines instead of linesegments.
@github-project-automation github-project-automation Bot moved this to Work in progress in PR review Jun 25, 2026
scene = Makie.parent_scene(p)
transf = transform_func(scene)
is_identity_transform(transf) || throw(ArgumentError("ABLines is only defined for the identity transform, not $(typeof(transf))."))
const N_ABLINE_SUBDIVISIONS = 256

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be an attribute?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good point!

@SimonDanisch

Copy link
Copy Markdown
Member

At some point we wanted to make this work generically by adapting the apply_transform machinery to be able to add points to lines:

apply_transform(func, vector_of_points)::Vector{Point}

Maybe worth a shot with claude being able to do the annoying interface refactor?

@ffreyer

ffreyer commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Dendrogram has something specifically for Polar:

resample_for_transform(tf, args...; step = nothing) = args
function resample_for_transform(tf::Polar, ps, args...; step = 2pi / 180)
isempty(ps) && return (ps, args...)
interpolated_points = similar(ps, 0)
push!(interpolated_points, ps[1])
interpolated_args = map(x -> similar(x, 0), args)
for (old, new) in zip(args, interpolated_args)
push!(new, old[1])
end
dim = ifelse(tf.theta_as_x, 1, 2)
for i in 2:length(ps)
p0 = ps[i - 1]
p1 = ps[i]
if isnan(p0) || isnan(p1)
push!(interpolated_points, p1)
for (old, new) in zip(args, interpolated_args)
push!(new, old[i])
end
continue
end
N = 1 + max(1, round(Int, abs(p1[dim] - p0[dim]) / step))
if N == 2
push!(interpolated_points, p1)
for (old, new) in zip(args, interpolated_args)
push!(new, old[i])
end
else
append!(interpolated_points, range(p0, p1, length = N)[2:N])
for (old, new) in zip(args, interpolated_args)
if isnan(old[i - 1]) || isnan(old[i])
append!(new, (old[i - 1] for _ in 2:N))
else
append!(new, range(old[i - 1], old[i], length = N)[2:N])
end
end
end
end
return (interpolated_points, interpolated_args...)
end

If we added resampling in an automated way it would definitely need an on/off switch. Discrete plots like scatter definitely shouldn't resample. Something like lines may also have situations where it shouldn't. For example if you make a spider chart by reducing the number of points in a PolarAxis.

@SimonDanisch

Copy link
Copy Markdown
Member

Yeah, it'd need to dispatch on the plot type as well...
We had a concept somewhere sometime I think, which thought this through a bit more than my comment 😅

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.

3 participants