Search code examples
pythonmatplotlibanimation

Changing the color scheme in a matplotlib animated gif


I'm trying to create an animated gif that takes a python graph in something like the plt.style.use('dark_background') (i.e black background with white text) and fades it to something like the default style (ie. white background with black text).

animated gif

The above is the result of running the below. You'll see that it doesn't quite work because the area around the plot area and legend area stubbornly remains white. I've tried numerous variations on the below, but can't figure it out.

I'm also trying to get it not to loop. The repeat=False doesn't seem to do it. But that's a secondary issue. The main one is: how do I get the background of the figure to change its color during an animation?

import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np
import matplotlib as mpl

# Data for the plot
x1 = np.linspace(1,100)
y1 = np.sin(x1)

#####################################
# STEP 1: FADE TO WHITE - FAILED
#####################################

# Create the figure and axis
fig, ax = plt.subplots()

# Plot the data
line1, = ax.plot(x1, y1, label='sin')

# Set the title and axis labels
ax.set_title('Title')
ax.set_xlabel('x axis')
ax.set_ylabel('y axis')

# Add a legend
#ax.legend(loc='right')
legend = ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.)
plt.subplots_adjust(right=0.79)

# Function to update the plot for the animation
def update1(frame):
    if frame>=noFrames:
        frame=noFrames-1
    startWhite = tuple([(noFrames-frame-1)/(noFrames-1)]*3)
    startBlack = tuple([frame/(noFrames-1)]*3)

    ax.cla()
    # Plot the data
    line1, = ax.plot(x1, y1, label='sin')

    # Set the title and axis labels
    ax.set_title('Title',color=startWhite)
    ax.set_xlabel('X Axis',color=startWhite)
    ax.set_ylabel('Y Axis',color=startWhite)

    # Add a legend
    #ax.legend(loc='right')
    legend = ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.)
    plt.subplots_adjust(right=0.79)

    fig.patch.set_color(None)
    fig.patch.set_facecolor(startBlack)
    ax.patch.set_facecolor(startBlack)
    fig.patch.set_edgecolor(startBlack)
    fig.patch.set_color(startBlack)

    plt.rcParams['axes.facecolor'] = startBlack
    plt.rcParams['axes.edgecolor'] = startBlack
    plt.rcParams['axes.labelcolor'] = startWhite
    plt.rcParams['axes.titlecolor'] = startWhite
    plt.rcParams['legend.facecolor'] = startBlack
    plt.rcParams['legend.edgecolor'] = startWhite
    plt.rcParams['legend.labelcolor'] = startWhite
    plt.rcParams['figure.facecolor'] = startBlack
    plt.rcParams['figure.edgecolor'] = startBlack
    plt.rcParams['xtick.color'] = startWhite
    plt.rcParams['ytick.color'] = startWhite
    plt.rcParams['text.color'] = startWhite

    fig.canvas.draw_idle()

    return fig,

noFrames = 50

# Create the animation
ani = animation.FuncAnimation(fig, update1, frames=range(noFrames*5), blit=False, repeat=False)
ani.event_source.stop() #stop the looping

# Save the animation as a GIF
ani.save('TEST01_fade.gif', writer='pillow', fps=10)

plt.close()

Solution

  • I couldn't figure out how to make the figure facecolor transition, but a workaround is to use a single subfigure, and adjust the facecolor of that. I also set the color properties directly on the artists rather than using rcParams as I think there is sometimes inconsistency about exactly when rcParams get applied (e.g. when the artist is created or when the artist is drawn). Rather than clear the axes and re-add everything, I just keep the original artists where possible. Legend does not seem to have the required set methods so I re-create that, but Legend always replaces the existing one anyway (as do the title and x-, y-labels).

    Tested with Matplotlib v3.8.2.

    import matplotlib.pyplot as plt
    import matplotlib.animation as animation
    import numpy as np
    
    # Data for the plot
    x1 = np.linspace(1,100)
    y1 = np.sin(x1)
    
    #######################
    # STEP 1: FADE TO WHITE
    #######################
    
    # Create the figure, subfigure and axis
    fig = plt.figure()
    sfig = fig.subfigures()
    ax = sfig.add_subplot()
    
    # Plot the data
    line1, = ax.plot(x1, y1, label='sin')
    
    # Set the title and axis labels
    ax.set_title('Title')
    ax.set_xlabel('x axis')
    ax.set_ylabel('y axis')
    
    # Add a legend
    legend = ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.)
    plt.subplots_adjust(right=0.79)
    
    # Function to update the plot for the animation
    def update1(frame):
        if frame>=noFrames:
            frame=noFrames-1
        startWhite = tuple([(noFrames-frame-1)/(noFrames-1)]*3)
        startBlack = tuple([frame/(noFrames-1)]*3)
    
        # Update background colours
        sfig.set_facecolor(startBlack)
        ax.set_facecolor(startBlack)
    
        # Set the title and axis labels
        ax.set_title('Title',color=startWhite)
        ax.set_xlabel('X Axis',color=startWhite)
        ax.set_ylabel('Y Axis',color=startWhite)
        
        ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.,
                  facecolor=startBlack, edgecolor=startWhite, labelcolor=startWhite)
        
        # Update tick/label colours
        ax.tick_params(color=startWhite, labelcolor=startWhite)
        
        # Update spine colours
        ax.spines[:].set_color(startWhite)
    
    noFrames = 50
    
    # Create the animation
    ani = animation.FuncAnimation(fig, update1, frames=range(noFrames*5), blit=False, repeat=False)
    
    # Save the animation as a GIF
    ani.save('TEST01_fade.gif', writer='pillow', fps=10)
    
    plt.close()
    

    enter image description here