Search code examples
pythonmatplotlibtogglephysics

Unable to create a toggle for gridlines on MatplotLib Python


I've been trying to make a toggle button to turn on and off the gridlines on my graph every time it's clicked. I understand how the toggle works as I was able to do it for some lines being animated onto my graph. I'm unsure if it's a problem with how MatplotLib sets axis and zorder etc. Or if it's to do with the fact i'm animating the graph and it updates the whole figure every frame. I didn't think that would be a problem though considering that everything is redrawn and not just the changing plots.

I've tried many different ways of doing this widget and none of it seemed to work. I've changed the visibility of the lines by using alpha, the colour since my background is black, switching ax.grid() to ax.grid(False). I'm not sure anymore, this is what I've got currently:

ax_grid = fig.add_axes([0.85, 0.2, 0.1, 0.04])
gbutton = Button(ax_grid, 'Grid', color = '0.3', hovercolor='0.7')
# Define the grid visibility state
grid_visible = False  # Initialize the grid visibility state

def grid_lines(event):
    global grid_visible
    grid_visible = not grid_visible  # Toggle the state
    plt.sca(ax)  # Ensure we are modifying the main plot's axes
    ax.set_axisbelow(False) # trying to set it above
    fig.canvas.draw()
    if grid_visible:
        ax.grid(color='white')  # Show grid with properties
    else:
        ax.grid(color='black')  # Simply turn off the grid without extra arguments

    fig.canvas.draw_idle()  # Redraw the canvas

Another version:

ax_grid = fig.add_axes([0.85, 0.2, 0.1, 0.04])
gbutton = Button(ax_grid, 'Grid', color = '0.3', hovercolor='0.7')
# Define the grid visibility state
grid_visible = False  # Initialize the grid visibility state

def grid_lines(event):
    global grid_visible
    grid_visible = not grid_visible  # Toggle state

    # Access grid lines directly and toggle their visibility
    for line in ax.get_xgridlines() + ax.get_ygridlines():
        line.set_visible(grid_visible)

    fig.canvas.draw_idle()  # Redraw the canvas

My whole code incase it's needed:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.widgets import Slider, Button
import scipy
from math import sqrt

plt.rcParams["figure.autolayout"] = True

print("Default text color is: ", plt.rcParams['text.color'])
plt.rcParams.update({'text.color': "white"})  # changing default text colour to white

dt = 0.1
numsteps = 10000
pi = scipy.constants.pi
G = 4.30091e-3  # AU^3 * M_sun^-1 * yr^-2
wA = 3.0
wB = 3.0

thetaA = 0
thetaB = thetaA + pi  # to put the other store on the opposite end of starA

# initialise variables
r = 5
mA = 10  # mass in solar mass
mB = 10
M = mA + mB

x_valA, y_valA = [], []
x_valB, y_valB = [], []

# Create the animation, fig represents the object/canvas and ax means it is the area being plotted on
fig, ax = plt.subplots(figsize=(8, 8), dpi=100)
ax.set_xlim(-10, 10)
ax.set_ylim(-10, 10)
ax.set_aspect('equal')
ax.set_facecolor("black")
fig.patch.set_facecolor("k")

# Create the stars and COM plot
starA, = ax.plot([], [], 'o', color='blue', markersize=10, label='Star A',zorder=9.5)
starB, = ax.plot([], [], 'o', color='red', markersize=10, label='Star B',zorder=9.5)
COM = ax.plot([0], [0], '+', color='white', markersize=5, label='COM',zorder=10)
orbitA, = ax.plot([], [], '-', color='cyan', alpha=0.5, label='Orbit A', zorder=5)
orbitB, = ax.plot([], [], '-', color='pink', alpha=0.5, label='Orbit B', zorder=5)
ax.grid(color='white', linestyle='--', linewidth=0.5, zorder=1)
leg = ax.legend(facecolor='k', labelcolor='w', fancybox=True, framealpha=0.6, loc='upper right', bbox_to_anchor=(1,1))
leg.set_zorder(20)

def orbit(r, mA, mB, M):
    global x_valA, y_valA, x_valB, y_valB, thetaA, thetaB, G, dt

    # Reset variables
    x_valA, y_valA = [], []
    x_valB, y_valB = [], []

    M = mA + mB

    rA = r * (mB / M)
    rB = r * (mA / M)

    # initial positions
    positionA = np.array([rA * np.cos(thetaA), rA * np.sin(thetaA)])  # Star A initial position
    positionB = np.array([rB * np.cos(thetaB), rB * np.sin(thetaB)])  # Star B initial position

    # SIMULATION LOOP
    for _ in range(numsteps):
        # Store positions for both stars
        x_valA.append(positionA[0])
        y_valA.append(positionA[1])
        x_valB.append(positionB[0])
        y_valB.append(positionB[1])

        # Update speed and angles for next positions
        wA = sqrt(G * M / rA * rA)
        wB = sqrt(G * M / rB * rB)
        thetaA += wA * dt  # update angle for starA
        thetaB += wB * dt  # update angle for starB

        # Calculate new positions based on updated angles
        positionA = np.array([rA * np.cos(thetaA), rA * np.sin(thetaA)])
        positionB = np.array([rB * np.cos(thetaB), rB * np.sin(thetaB)])

    # After simulation loop, update the orbit lines with the recorded paths
    orbitA.set_data(x_valA, y_valA)
    orbitB.set_data(x_valB, y_valB)

# initialising the data
def init():
    starA.set_data([], [])
    starB.set_data([], [])
    orbitA.set_data([], [])  # Clear the initial orbit paths
    orbitB.set_data([], [])
    
    return starA, starB, orbitA, orbitB

def update(frame):
    starA.set_data([x_valA[frame]], [y_valA[frame]])  # Pass as lists
    starB.set_data([x_valB[frame]], [y_valB[frame]])
    if orbit_lines_visible:
        orbitA.set_data(x_valA[:frame+1], y_valA[:frame+1])
        orbitB.set_data(x_valB[:frame+1], y_valB[:frame+1])
    return starA, starB, orbitA, orbitB

ani = FuncAnimation(fig, update, frames=numsteps, init_func=init, blit=False, interval=50)
plt.title("Binary Star System", fontsize=20, fontweight='bold')

def up(val):
    global r, mA, mB, M
    r = seperation_slider.val
    mA = mA_slider.val
    mB = mB_slider.val
    M = mA + mB
    orbit(r, mA, mB, M)  # updated values into function
    starA.set_markersize(mA_slider.val)
    starB.set_markersize(mB_slider.val)
    ani.event_source.stop()  # Stop the current animation
    ani.event_source.start()  # Restart the animation with updated orbit
    fig.canvas.draw_idle()
    
seperation_slider = Slider(ax=plt.axes([0.125, 0.02, 0.10, 0.04]), label='Seperation', valmin=1, valmax=15, valinit=r, valstep=1.11, facecolor='w')
mA_slider = Slider(ax=plt.axes([0.45, 0.02, 0.15, 0.04]), label="Mass A", valmin=0.1, valmax=100, valinit=mA, valstep=1.11, facecolor='b')
mB_slider = Slider(ax=plt.axes([0.80, 0.02, 0.15, 0.04]), label="Mass B", valmin=0.1, valmax=100, valinit=mB, valstep=1.11, facecolor='r')

seperation_slider.label.set_size(12)
mA_slider.label.set_size(12)
mB_slider.label.set_size(12)
mA_slider.vline.set_color('cyan')
mB_slider.vline.set_color('violet')
seperation_slider.vline.set_color('black')

seperation_slider.on_changed(up)
mA_slider.on_changed(up)
mB_slider.on_changed(up)

orbit(r, mA, mB, M)

ax_reset = fig.add_axes([0.85, 0.08, 0.1, 0.04])
rbutton = Button(ax_reset, 'Reset', color='0.3', hovercolor='0.7')

def reset(event):
    seperation_slider.reset()
    mA_slider.reset()
    mB_slider.reset()

rbutton.on_clicked(reset)

# Toggle for orbit lines visibility
orbit_lines_visible = False

def lines(event):
    global orbit_lines_visible
    if orbit_lines_visible:
        orbitA.set_alpha(0)  # Hide orbit A
        orbitB.set_alpha(0)  # Hide orbit B
    else:
        orbitA.set_alpha(0.5)  # Show orbit A
        orbitB.set_alpha(0.5)  # Show orbit B
    orbit_lines_visible = not orbit_lines_visible
    fig.canvas.draw_idle()
ax_lines = fig.add_axes([0.85, 0.14, 0.1, 0.04])
button = Button(ax_lines, 'Orbit Lines', color='0.3', hovercolor='0.7')
button.on_clicked(lines)

ax_grid = fig.add_axes([0.85, 0.2, 0.1, 0.04])
gbutton = Button(ax_grid, 'Grid', color = '0.3', hovercolor='0.7')
# Define the grid visibility state
grid_visible = False  # Initialize the grid visibility state

def grid_lines(event):
    global grid_visible
    grid_visible = not grid_visible
    
    if grid_visible:
        ax.grid(True, color='white')
    else:
        ax.grid(False)
    
    # Force immediate redraw
    fig.canvas.draw()

plt.show()

Solution

  • The issue is that instead of turning the grid off, you're only changing its color to black. Even though your background is black, the grid is still active, it’s just invisible. To disable the grid, call:

    ax.grid(False)
    

    and to enable, explicitly call:

    ax.grid(True, color='white', linestyle='--', linewidth=0.5)
    

    Example working script:

    import matplotlib.pyplot as plt
    from matplotlib.widgets import Button
    
    fig, ax = plt.subplots()
    fig.set_facecolor('black')
    ax.set_facecolor('black')
    for spine in ax.spines.values():
        spine.set_color('white')
    ax.tick_params(colors='white')
    ax.grid(False)
    
    grid_state = [False]
    
    def toggle_grid(event):
        grid_state[0] = not grid_state[0]
        if grid_state[0]:
            ax.grid(True, color='white', alpha=0.5, linestyle='-', linewidth=0.5)
        else:
            ax.grid(False)
        fig.canvas.draw_idle()
    
    button_ax = plt.axes([0.8, 0.05, 0.15, 0.075])
    button = Button(button_ax, 'Grid', color='0.3', hovercolor='0.7')
    button.on_clicked(toggle_grid)
    
    plt.show()
    

    enter image description here

    enter image description here