struggling in calling multiple interactive functions for a graph using ipywidgets

661 views Asked by At

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!

1

There are 1 answers

0
Ianhi On BEST ANSWER

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,
)