2

So, I'd like to set the 2 y data of a fillbetween object, equivalent to setting the (1) y data of a Line2D with some_line.set_ydata(new_y).

A naïve attempt results in this error:

AttributeError: 'PolyCollection' object has no attribute 'set_ydata'. 

Is there a way to directly access and set the data of a PolyCollection?

Below is matplotlib's Slider demo with the added parts at lines 21 & 55 to show the issue I want to solve.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button


# The parametrized function to be plotted
def f(t, amplitude, frequency):
    return amplitude * np.sin(2 * np.pi * frequency * t)

t = np.linspace(0, 1, 1000)

# Define initial parameters
init_amplitude = 5
init_frequency = 3

# Create the figure and the line that we will manipulate
fig, ax = plt.subplots()
line, = plt.plot(t, f(t, init_amplitude, init_frequency), lw=2)

# added part
fill = plt.fill_between(t, 0, f(t, init_amplitude, init_frequency))

ax.set_xlabel('Time [s]')

# adjust the main plot to make room for the sliders
plt.subplots_adjust(left=0.25, bottom=0.25)

# Make a horizontal slider to control the frequency.
axfreq = plt.axes([0.25, 0.1, 0.65, 0.03])
freq_slider = Slider(
    ax=axfreq,
    label='Frequency [Hz]',
    valmin=0.1,
    valmax=30,
    valinit=init_frequency,
)

# Make a vertically oriented slider to control the amplitude
axamp = plt.axes([0.1, 0.25, 0.0225, 0.63])
amp_slider = Slider(
    ax=axamp,
    label="Amplitude",
    valmin=0,
    valmax=10,
    valinit=init_amplitude,
    orientation="vertical"
)


# The function to be called anytime a slider's value changes
def update(val):
    line.set_ydata(f(t, amp_slider.val, freq_slider.val))
    
    # added part 
    fill.set_ydata(0, f(t, amp_slider.val, freq_slider.val))
    
    fig.canvas.draw_idle()


# register the update function with each slider
freq_slider.on_changed(update)
amp_slider.on_changed(update)

# Create a `matplotlib.widgets.Button` to reset the sliders to initial values.
resetax = plt.axes([0.8, 0.025, 0.1, 0.04])
button = Button(resetax, 'Reset', hovercolor='0.975')


def reset(event):
    freq_slider.reset()
    amp_slider.reset()
button.on_clicked(reset)


plt.show()
Mr. T
  • 11,960
  • 10
  • 32
  • 54
Andre
  • 760
  • 3
  • 13

1 Answers1

2

Matplotlib's fill_between does not return a Line2D object but a PolyCollection. So, we have to update the vertices of the included path, and the easiest way to calculate the new vertices is to draw an invisible dummy object and extract the vertices in the modified update function:

def update(val): 
        #optional preventing autoscaling of y-axis 
        ax.autoscale(False)
        #create invisible dummy object to extract the vertices 
        dummy = ax.fill_between(t, 0, f(t, amp_slider.val, freq_slider.val), alpha=0)
        dp = dummy.get_paths()[0]
        dummy.remove()
        #update the vertices of the PolyCollection
        fill.set_paths([dp.vertices])
        
        fig.canvas.draw_idle()

Apart from the modified update function, I have only removed the line plot because it does not add anything to your plot if you have the filled path object. MCVE code:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button


def f(t, amplitude, frequency):
    return amplitude * np.sin(2 * np.pi * frequency * t)

t = np.linspace(0, 1, 1000)

# Define initial parameters
init_amplitude = 5
init_frequency = 3

# Create the figure and the line that we will manipulate
fig, ax = plt.subplots()

# added part
fill = plt.fill_between(t, 0, f(t, init_amplitude, init_frequency))

ax.set_xlabel('Time [s]')

# adjust the main plot to make room for the sliders
plt.subplots_adjust(left=0.25, bottom=0.25)

# Make a horizontal slider to control the frequency.
axfreq = plt.axes([0.25, 0.1, 0.65, 0.03])
freq_slider = Slider(
    ax=axfreq,
    label='Frequency [Hz]',
    valmin=0.1,
    valmax=30,
    valinit=init_frequency,
)

# Make a vertically oriented slider to control the amplitude
axamp = plt.axes([0.1, 0.25, 0.0225, 0.63])
amp_slider = Slider(
    ax=axamp,
    label="Amplitude",
    valmin=0,
    valmax=10,
    valinit=init_amplitude,
    orientation="vertical"
)


# The function to be called anytime a slider's value changes
def update(val): 
    #optional preventing autoscaling of y-axis 
    ax.autoscale(False)
    #create invisible dummy object to extract the vertices 
    dummy = ax.fill_between(t, 0, f(t, amp_slider.val, freq_slider.val), alpha=0)
    dp = dummy.get_paths()[0]
    dummy.remove()
    #update the vertices of the PolyCollection
    fill.set_paths([dp.vertices])
    
    fig.canvas.draw_idle()


# register the update function with each slider
freq_slider.on_changed(update)
amp_slider.on_changed(update)

# Create a `matplotlib.widgets.Button` to reset the sliders to initial values.
resetax = plt.axes([0.8, 0.025, 0.1, 0.04])
button = Button(resetax, 'Reset', hovercolor='0.975')


def reset(event):
    freq_slider.reset()
    amp_slider.reset()
button.on_clicked(reset)


plt.show()
Mr. T
  • 11,960
  • 10
  • 32
  • 54