Makie: Axis through (0,0)

Hi there!

I wondered whether the following is possible in Makie.

import math
import numpy as np
import matplotlib.pyplot as plt

def sigmoid(x):
    a = []
    for item in x:
        a.append(1/(1+math.exp(-item)))
    return a

    
x = np.arange(-10., 10., 0.2)
sig = sigmoid(x)

fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)

# Move left y-axis and bottom x-axis to centre, passing through (0,0)
ax.spines['left'].set_position('center')
ax.spines['bottom'].set_position('center')

# Eliminate upper and right axes
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')

# Show ticks in the left and lower axes only
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')

plt.plot(x,sig)
plt.show()

I have looked through the documentation but not found something to change the position of the spines such that they are centered through (0,0).

Thanks for any help!

2 Likes

Not inbuilt, although one could probably hack it in several ways. You could look at ax.xaxis, I think it has an endpoints observable. You could try setting that to the vertical middle of the axis area you have in ax.scene.px_area

1 Like

When I was using Plots.jl, I would always make my axis go through (0, 0). Nowaday, I am using Makie and not really missing that feature. But for some plots it is really nice to have the option. Are there plans to add built-in support for this?

Not really plans, but it would in principle not be that difficult to add I think. It might just get a bit more messy with the observables if there are more possible states than top and bottom for xaxisposition etc.

In a quick try I already saw that the grid lines break when there’s an additional :center state, so stuff like that would need to be cleaned up.

2 Likes

Thanks for the idea @jules, I gave it a try here’s what came out. I managed to make the axis go through the middle, however the grid is know messed up.

using GLMakie

fig = Figure()
xticks = -5:1:5
yticks = -5:1:5
ax = Axis(fig[1, 1],  aspect=AxisAspect(1), xticks=xticks, yticks=yticks)
limits!(ax, -5.5, 5.5, -5.5, 5.5)
hidespines!(ax, :t, :r)
xend = ax.xaxis.attributes.endpoints
yend = ax.yaxis.attributes.endpoints

x1, x2 = xend[]
y1, y2= yend[]

Δy = (y2[2] - y1[2])/2
Δx = (x2[1] - x1[1])/2

xend[] = ([x1[1], x1[2] + Δy], [x2[1], x2[2] + Δy])
yend[] = ([y1[1] + Δx, y1[2]], [y2[1] + Δx, y2[2]])

xs = -5:0.1:5
lines!(ax, xs, 5*sin.(xs))

1 Like

Ah sorry, did not see your last post. But yes I had the same problem :smiley:

Based on your solution, I obtained this

function cartesianaxis!(; origin=false)

    origin ? α = 1 : α = 0 
    # first include the (0,0) point into the plot
    scatter!([0],[0], marker = :circle, color = (:black, α))
    
    ax = current_axis()
    hidespines!(ax)
    ax.xticksvisible[] = false
    ax.yticksvisible[] = false

    autolimits!(ax) # TODO: Related with lifts

    # xlimitmargin = ax.xautolimitmargin[] 
    # ylimitmargin = ax.yautolimitmargin[]
    
    # # ax.xautolimitmargin[] = (0,0)
    # # ax.yautolimitmargin[] = (0,0)
    
    # data space
    xdatamin, xdatamax = ax.xaxis.attributes.limits[] 
    ydatamin, ydatamax = ax.yaxis.attributes.limits[]

    # pixel space Points
    xP1, xP2 = ax.xaxis.attributes.endpoints[]
    yP1, yP2 = ax.yaxis.attributes.endpoints[]

    # TODO: Work on lifts
    xtrs = - (xP2[1] - xP1[1]) / (xdatamax - xdatamin) * xdatamin
    ytrs = - (yP2[2] - yP1[2]) / (ydatamax - ydatamin) * ydatamin

    xP1 += [0, ytrs]
    xP2 += [0, ytrs]
    yP1 += [xtrs, 0]
    yP2 += [xtrs, 0]

    linesegments!(ax.blockscene, [xP1, xP2], color=:black, linewidth = 1.25)
    linesegments!(ax.blockscene, [yP1, yP2], color=:black, linewidth = 1.25)
    scatter!(ax.blockscene, [xP2], color =:black, marker=:rtriangle)
    scatter!(ax.blockscene, [yP2], color =:black, marker=:utriangle)

    f = current_figure()
end

This is the result for fig, ax, plt = lines(rand(10)); lines!(-1 .* rand(10)); cartesianaxis!()

2 Likes

You can hide the grid on your axis, then use a second axis just for the grid. It’s a bit messy but works ok if you can fix or don’t need zooming.

I also did a small tweak with the origin tick by excluding it from the ticks themselves and adding a text “0”.

This code could do with a tidy, but I’ll leave it here for now just as a reference for anyone else landing on this thread.

fig3 = Figure()
xlim = 7
ylim = 5
xticks = vcat([x for x in -xlim:-1], [x for x in 1:xlim])
yticks = vcat([x for x in -ylim:-1], [x for x in 1:ylim])

# Create second axis for gridlines only
ax4 = Axis(
    fig3[1, 1],
    xticks=xticks,
    yticks=yticks,
    xticklabelsvisible=false,
    xticksvisible=false,
    yticklabelsvisible=false,
    yticksvisible=false,
)
hidespines!(ax4)

# Now create axis to be transformed
ax3 = Axis(
    fig3[1, 1],
    xticks=xticks,
    yticks=yticks,
    xgridvisible=false,
    ygridvisible=false,
)
hidespines!(ax3, :t, :r)

margin = 0.5
limits!(ax3, -xlim-margin, xlim+margin, -ylim-margin, ylim+margin)
limits!(ax4, -xlim-margin, xlim+margin, -ylim-margin, ylim+margin)

# Adjust aspect ratio based on limits
ax3.aspect=xlim/ylim
ax4.aspect=xlim/ylim

# Translate spines
x1, x2 = ax3.xaxis.attributes.endpoints[]
xl, xo = x1
xr = x2[1]
y1, y2 = ax3.yaxis.attributes.endpoints[]
yo, yb = y1
yt = y2[2]
new_xo = yb + (yt - yb)/2.0
new_yo = xl + (xr - xl)/2.0

ax3.xaxis.attributes.endpoints[] = ([xl, new_xo], [xr, new_xo])
ax3.yaxis.attributes.endpoints[] = ([new_yo, yb], [new_yo, yt])

# Label origin
text!(ax3, -0.4, -0.5, text="0", color=:white)

# Test lines
xs = LinRange(-xlim, xlim, 150)
lines!(ax3, xs, x->0.25x^2 - 4)
lines!(ax3, [0, 0], [-5, 5], linestyle=:dash)
lines!(ax3, [-7, 7], [0, 0], linestyle=:dash)
lines!(ax3, [-7, 7], [-4, -4], linestyle=:dash)

save("center-spines.png", fig3)