Break axis in Makie

Hi, I am pretty new to Makie, but I am loving the package so far! Is it possible to have a broken y-axis in Makie? E.g. I want to plot:

xs1 = randn(10)
ys1 = randn(10)

xs2 = randn(10) .+ 3
ys2 = randn(10) .+ 10

fig = Figure()
ax = fig[1,1] = Axis(fig)
scatter!(ax, xs1, ys1)
scatter!(ax, xs2, ys2)
fig

without resorting to a log scale and have the clusters on the same height. Any help would be much appreciated!

So I asked about this on the Makie Slack channel and apparently this is not implemented yet. However, Julius Krumbiegel suggested I try the following to manually implement what I want:

possible manually, yes, but there’s no premade function for it. you would currently have to make two axes, plot the same thing in them, set limits how you want, hide the adjacent spines so it looks like one axis, then the only “tricky” thing is adding diagonal lines at the spine ends. this is best made dependent on the axis position, so that it stays correct

you can grab the ends of the spines from the lineaxis objects in ax.elements , they have an attribute called endpoints . grab the correct points and calculate your diagonals in figure space, then plot the lines into figure.scene

For anyone else who needs to implement this manually, here is the code. This does everything except add that “broken” y axis. I could not manage to implement those diagonal “broken axis indicator” lines. However, if you are in a pinch for time, just add them manually using Inkscape :slight_smile:

If anyone figures out how to add them, please reply to this topic.

using CairoMakie

xs1 = randn(10)
ys1 = randn(10)

xs2 = randn(10) .+ 3
ys2 = randn(10) .+ 10

fig = Figure();
ax1 = fig[1,1] = Axis(fig)
scatter!(ax1, [xs1; xs2], [ys1; ys2])
hidexdecorations!(ax1)
hideydecorations!(ax1, ticklabels=false)
ylims!(ax1, 8.5, 11.0)
ax1.yticks = 9:1:11

ax2 = fig[2,1] = Axis(fig)
scatter!(ax2, [xs1; xs2], [ys1; ys2])
hideydecorations!(ax2, ticklabels=false)
hidexdecorations!(ax2, ticklabels=false)
ylims!(ax2, 0.0, 2.5)     
ax2.yticks = 0:1:2

rowsize!(fig.layout, 2, Relative(2/3))
rowgap!(fig.layout, 1, Relative(0.0))
ax2.topspinevisible=false
ax1.bottomspinevisible=false

Label(fig[:, 0], "Manually broken y", rotation=pi/2)

fig

@Elmo, you could draw a horizontal grey line where the axis is broken, with an annotation of the gap used. E.g., y = (2.5, 8.5)

it looks like your y-scale changes, say 0-1 versus 10-11, is that desired?

Yes, so the type of figure I was going for looks like this:

Do you mean a horizontal line spanning the entire figure? If so, yes that would be a programmatic way of indicating where it breaks, however it would not look as pretty as e.g. the matplotlib version (see here)

I mean that the distance between 0 and 1 and between 10 and 11, along the y axis, does not look the same. The distance between 0 and 1 looks bigger. The image you showed with the squiggly broken axis has the same metric near 0 and near 1. Some problem when creating your figure maybe?

Ah, now I see what you mean. Nope, that is not desired, but I suppose it could be fixed by just setting the y limits and the y ticks of both figures?

You could fix the relative scaling by giving both rows an Auto(ratio) size where ratio is the relative length of each axis. For the diagonal lines, you just need to do linesegments!(fig.scene, diagonal_points). The points you have to calculate given the endpoints of the axis lines, an angle that you like, and a line length that you like. You should calculate the points using lift(endpoints) do points... etc so that they update whenever the axis shifts.

2 Likes

Here’s a possible implementation, although it uses some rather internal things:

f = Figure()

lims = Node(((0.0, 1.3), (7.6, 10.0)))

g = f[1, 1] = GridLayout()

ax_top = Axis(f[1, 1][1, 1], title = "Broken Y-Axis")
ax_bottom = Axis(f[1, 1][2, 1])

on(lims) do (bottom, top)
    ylims!(ax_bottom, bottom)
    ylims!(ax_top, top)
    rowsize!(g, 1, Auto(top[2] - top[1]))
    rowsize!(g, 2, Auto(bottom[2] - bottom[1]))
end

hidexdecorations!(ax_top, grid = false)
ax_top.bottomspinevisible = false
ax_bottom.topspinevisible = false

linkxaxes!(ax_top, ax_bottom)
rowgap!(g, 10)

angle = pi/8
linelength = 30

segments = lift(
        @lift($(ax_top.elements[:yaxis].attributes.endpoints)[1]),
        @lift($(ax_bottom.elements[:yaxis].attributes.endpoints)[2]),
        @lift($(ax_top.elements[:yoppositeline][1])[1]),
        @lift($(ax_bottom.elements[:yoppositeline][1])[2]),
    ) do p1, p2, p3, p4
    ps = Point2f0[p1, p2, p3, p4]

    map(ps) do p
        a = p + Point2f0(cos(angle), sin(angle)) * 0.5 * linelength
        b = p - Point2f0(cos(angle), sin(angle)) * 0.5 * linelength
        (a, b)
    end
end

linesegments!(f.scene, segments)

scatter!(ax_top, randn(100, 2) .* 0.5 .+ 9)
scatter!(ax_bottom, randn(100, 2) .* 0.2 .+ 0.25)

notify(lims)

f
7 Likes
  1. rescale data such that they fit into the same coordinate system.
  2. plot as you would
  3. re-annotate y-ticks

In your example, you have two chunks of data, share same x, but different ys, one from [0, 0.25], and one from [0.75, 1]. To set up common coordinate system, from [0, 0.5], where [0, 0.25] map to [0, 0.25], and [0.75, 1] map to [0.25, 0.5], this should be one simple broadcast. If you need log-scale, just do it.

i dont really know how to do the “//”, but given it serves as attention grabbing marker, you could just plot some shaded region, depends on the determined coordinate system

are there plans to have an API for such use cases ?

1 Like

This no longer works. Particularly I cannot access yoppositeline in the current version. Is there an alternative ?

This works for me in Makie 0.19.1:

f = Figure()

lims = Observable(((0.0, 1.3), (7.6, 10.0)))

g = f[1, 1] = GridLayout()

ax_top = Axis(f[1, 1][1, 1], title = "Broken Y-Axis")
ax_bottom = Axis(f[1, 1][2, 1])

on(lims) do (bottom, top)
    ylims!(ax_bottom, bottom)
    ylims!(ax_top, top)
    rowsize!(g, 1, Auto(top[2] - top[1]))
    rowsize!(g, 2, Auto(bottom[2] - bottom[1]))
end

hidexdecorations!(ax_top, grid = false)
ax_top.bottomspinevisible = false
ax_bottom.topspinevisible = false

linkxaxes!(ax_top, ax_bottom)
rowgap!(g, 10)

angle = pi/8
linelength = 30

segments = lift(
        @lift($(ax_top.yaxis.attributes.endpoints)[1]),
        @lift($(ax_bottom.yaxis.attributes.endpoints)[2]),
        @lift($(ax_top.elements[:yoppositeline][1])[1]),
        @lift($(ax_bottom.elements[:yoppositeline][1])[2]),
    ) do p1, p2, p3, p4
    ps = Point2f[p1, p2, p3, p4]
    
    map(ps) do p
        a = p + Point2f(cos(angle), sin(angle)) * 0.5 * linelength
        b = p - Point2f(cos(angle), sin(angle)) * 0.5 * linelength
        (a, b)
    end
end

linesegments!(f.scene, segments)

scatter!(ax_top, randn(100, 2) .* 0.5 .+ 9)
scatter!(ax_bottom, randn(100, 2) .* 0.2 .+ 0.25)

notify(lims)

f
2 Likes