Search code examples
pythonmatplotlibinteractivegraphingipywidgets

struggling in calling multiple interactive functions for a graph using ipywidgets


I'm looking to have a main image upon which I draw either spirals, ellipses etc with variables that change the shape on the imposed drawing. The main image also needs to have a contrast variable.

My code currently looks like this;




###############################################BASIC FIGURE PLOT####################################
plt.figure(figsize=(24,24))
@interact
def spiral(Spiral=False,n=2000,x1=50,y1=50,z1=50,k1=300):
    if Spiral == False:
        x = 0;
        y = 0;
        plt.scatter(x,y,s = 3, c = 'black');
    else:
        angle = np.linspace(x1,y1*1*np.pi, n)
        radius = np.linspace(z1,k1,n)
        x = radius * np.cos(angle) + 150
        y = radius * np.sin(angle) + 150
        plt.scatter(x,y,s = 3, c = 'black');
@interact        
def contrast(vuc=(0.2,1,0.01),vlc=(0.1,1,0.01)):  
    vu = np.quantile(qphi, vuc);
    vl = np.quantile(qphi, vlc);
    print("upper =",vu, " lower=",vl);


plt.imshow(qphi, origin='lower',vmin=vl,vmax=vu);
plt.show() 

This produces two plots; visible here One plot which creates a spiral I can edit freely and one plot that is the main image with variable contrast.

Any advise on how to combine the two plots would be much appreciated; Thank you!


Solution

  • There are several ways to approach controlling a matplotlib plot using ipywidgets. Below I've created the output I think you're looking for using each of the options. The methods are listed in what feels like the natural order of discovery, however, I would recommend trying them in this order: 4, 2, 1, 3

    Approach 1 - inline backend

    If you use %matplotlib inline then matplotlib figures will not be interactive and you will need to recreate the entire plot every time

    %matplotlib inline
    import matplotlib.pyplot as plt
    import numpy as np
    from ipywidgets import interact
    
    # load fake image data
    from matplotlib import cbook
    img = plt.imread(cbook.get_sample_data("grace_hopper.jpg")).mean(axis=-1)
    
    
    @interact
    def graph(
        Spiral=True,
        n=2000,
        x1=50,
        y1=50,
        z1=50,
        k1=300,
        vlc=(0.1, 1, 0.01),
        vuc=(0.1, 1, 0.01),
    ):
        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12,5))
        if Spiral == False:
            x = 0
            y = 0
        else:
            angle = np.linspace(x1, y1 * 1 * np.pi, n)
            radius = np.linspace(z1, k1, n)
            x = radius * np.cos(angle) + 150
            y = radius * np.sin(angle) + 150
        ax1.scatter(x, y, s=3, color="k")
        vu = np.quantile(img, vuc)
        vl = np.quantile(img, vlc)
        ax2.imshow(img, vmin=vl, vmax=vu)
    

    Approach 2 - interactive backend + cla

    You can use one of the interactive maptlotlib backends to avoid having to completely regenerate the figure every time you change. To do this the first approach is to simply clear the axes everytime the sliders change using the cla method.

    This will work with either %matplotlib notebook or %matplotlib ipympl. The former will only work in jupyter notebook and the latter will work in both jupyter notebook and juptyerlab. (Installation info for ipympl here: https://github.com/matplotlib/ipympl#installation)

    %matplotlib ipympl
    
    import matplotlib.pyplot as plt
    import numpy as np
    from ipywidgets import interact, interactive, interactive_output
    
    # load fake image data
    from matplotlib import cbook
    img = plt.imread(cbook.get_sample_data("grace_hopper.jpg")).mean(axis=-1)
    
    
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12,5))
    
    
    @interact
    def graph(
        Spiral=True,
        n=2000,
        x1=50,
        y1=50,
        z1=50,
        k1=300,
        vlc=(0.1, 1, 0.01),
        vuc=(0.1, 1, 0.01),
    ):
        ax1.cla()
        ax2.cla()
        if Spiral == False:
            x = 0
            y = 0
        else:
            angle = np.linspace(x1, y1 * 1 * np.pi, n)
            radius = np.linspace(z1, k1, n)
            x = radius * np.cos(angle) + 150
            y = radius * np.sin(angle) + 150
        ax1.scatter(x, y, s=3, color="k")
        vu = np.quantile(img, vuc)
        vl = np.quantile(img, vlc)
        ax2.imshow(img, vmin=vl, vmax=vu)
    

    Approach 3 - interactive backend + set_data

    Totally clearing the axes can be inefficient when you are plotting larger datasets or have some parts of the plot that you want to persist from one interaction to the next. So you can instead use the set_data and set_offsets methods to update what you have already drawn.

    %matplotlib ipympl
    
    import matplotlib.pyplot as plt
    import numpy as np
    from ipywidgets import interact, interactive, interactive_output
    
    # load fake image data
    from matplotlib import cbook
    img = plt.imread(cbook.get_sample_data("grace_hopper.jpg")).mean(axis=-1)
    
    
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12,5))
    scat = ax1.scatter([0]*2000,[0]*2000,s=3, color='k')
    im = ax2.imshow(img)
    
    out = widgets.Output()
    display(out)
    
    @interact
    def graph(
        Spiral=True,
        n=2000,
        x1=50,
        y1=50,
        z1=50,
        k1=300,
        vlc=(0.1, 1, 0.01),
        vuc=(0.1, 1, 0.01),
    ):
        if Spiral == False:
            x = 0
            y = 0
        else:
            angle = np.linspace(x1, y1 * 1 * np.pi, n)
            radius = np.linspace(z1, k1, n)
            x = radius * np.cos(angle) + 150
            y = radius * np.sin(angle) + 150
        scat.set_offsets(np.c_[x, y])
        # correctly scale the x and y limits
        ax1.dataLim = scat.get_datalim(ax1.transData)
        ax1.autoscale_view()
    
        vu = np.quantile(img, vuc)
        vl = np.quantile(img, vlc)
        im.norm.vmin = vl
        im.norm.vmax = vu
    

    Approach 4 - mpl_interactions

    Using set_offsets and equivalent set_data will be the most performant solution, but can also be tricky to figure out how to get it work and even trickier to remember. To make it easier I've creted a library (mpl-interactions) that automates the boilerplate of approach 3.

    In addition to being easy and performant this has the advantage that you aren't responsible for updating the plots, only for returning the correct values. Which then has the ancillary benefit that now functions like spiral can be used in other parts of your code as they just return values rather than handle plotting.

    The other advantage is that mpl-interactions can also create matplotlib widgets so this is the only approach that will also work outside of a notebook.

    %matplotlib ipympl
    import ipywidgets as widgets
    import matplotlib.pyplot as plt
    import numpy as np
    import mpl_interactions.ipyplot as iplt
    
    img = plt.imread(cbook.get_sample_data("grace_hopper.jpg")).mean(axis=-1)
    
    # define the functions to be plotted
    def spiral(Spiral=False, n=2000, x1=50, y1=50, z1=50, k1=300):
        if Spiral == False:
            x = 0
            y = 0
            return x, y
        else:
            angle = np.linspace(x1, y1 * 1 * np.pi, n)
            radius = np.linspace(z1, k1, n)
            x = radius * np.cos(angle) + 150
            y = radius * np.sin(angle) + 150
            return x, y
    
    
    def vmin(vuc, vlc):
        return np.quantile(img, vlc)
    
    def vmax(vlc, vuc):
        return np.quantile(img, vuc)
    
    
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
    controls = iplt.scatter(
        spiral,
        Spiral={(True, False)},
        n=np.arange(1800, 2200),
        x1=(25, 75),
        y1=(25, 75),
        z1=(25, 75),
        k1=(200, 400),
        parametric=True,
        s=3,
        c="black",
        ax=ax1,
    )
    controls = iplt.imshow(
        img,
        vmin=vmin,
        vmax=vmax,
        vuc=(0.1, 1, 1000),
        vlc=(0.1, 1, 1000),
        controls=controls[None],
        ax=ax2,
    )