1

I'm using the matplotlib backend 'notebook', because I am making some interactive figures, and this works well with the notebook backend (in particular, I serve them via Jupyter Notebooks). I use ipywidgets to design the GUI and interactivity.

However, using this backend, there are all sorts of buttons that can interfere with my interactive figure. Especially, resizing, zooming, panning, or the power button, will lead to much confusion for my students...

I want to disable them. See this illustration on what I want to disable. enter image description here

Can anyone point me to the relevant API pages or does anyone know how to disable/remove these buttons? I tried some other backends, but these typically will not work so well for interactive figures in Jupyter notebooks, so I want to stick to the notebook backend if possible.

This is the contents of svm_helper:

from matplotlib import pyplot as plt
from matplotlib.backend_bases import MouseButton as mb
import ipywidgets as widgets
import sklearn.linear_model
import sklearn.metrics
import sklearn.svm
import numpy as np

def plot_decision_boundary_margin(X, y, model):
    Xmin = np.min(X[:,:],axis=0)
    Xmax = np.max(X[:,:],axis=0)
    Xmin = np.array([-3, -3])
    Xmax = np.array([3, 3])

    x0, x1 = np.meshgrid(
            np.linspace(Xmin[0], Xmax[0], 500).reshape(-1, 1),
            np.linspace(Xmin[1], Xmax[1], 200).reshape(-1, 1),
        )
    X_new = np.c_[x0.ravel(), x1.ravel()]

    y_new = model.decision_function(X_new)

    #plot_dataset(X,y)

    zz = y_new.reshape(x0.shape)
    C1 = plt.contour(x0, x1, zz, levels=np.array([0]),colors='k')
    C2 = plt.contour(x0, x1, zz, levels=np.array([-1,1]),colors='k',linestyles='dashed')
    
    return (C1, C2)

class LineBuilder2:
    def __init__(self, lineR, lineB, widgetcolor, widgetC, my_out, need_seperable):
        self.lineR = lineR
        self.xsR = list(lineR.get_xdata())
        self.ysR = list(lineR.get_ydata())

        self.lineB = lineB
        self.xsB = list(lineB.get_xdata())
        self.ysB = list(lineB.get_ydata())

        self.mywidgetcolor = widgetcolor
        self.cid = lineR.figure.canvas.mpl_connect('button_press_event', self)
        self.cid = lineR.figure.canvas.mpl_connect('motion_notify_event', self)
        
        self.widgetC = widgetC
        self.my_out = my_out

        self.dragging_timer = 0
        self.trained = False
        
        self.model = None
        self.C1 = None
        self.C2 = None
        
        self.need_seperable = need_seperable
    
    def remove_decision_boundary(self):
        
        if (self.C1 == None) or (self.C2 == None):
            return
        
        for coll in self.C1.collections: 
            plt.gca().collections.remove(coll) 
            
        for coll in self.C2.collections: 
            plt.gca().collections.remove(coll) 

    def __call__(self, event):
        #print('click', event)

        currently_dragging = False
        if event.name == 'motion_notify_event':
            currently_dragging = True
            self.dragging_timer = self.dragging_timer+1
            if self.dragging_timer > 5:
                self.dragging_timer = 0

        if not (event.button == mb.LEFT or event.button == mb.MIDDLE or event.button == mb.RIGHT):
            return

        if event.inaxes != self.lineB.axes:
            return

        #print(widgetcolor.value)
        if self.mywidgetcolor.value == 'green':
            self.xsR.append(event.xdata)
            self.ysR.append(event.ydata)
            if (not currently_dragging) or (currently_dragging and self.dragging_timer == 0):
                self.lineR.set_data(self.xsR, self.ysR)
            #self.lineR.figure.canvas.draw()

        if self.mywidgetcolor.value == 'blue':
            self.xsB.append(event.xdata)
            self.ysB.append(event.ydata)
            if (not currently_dragging) or (currently_dragging and self.dragging_timer == 0):
                self.lineB.set_data(self.xsB, self.ysB)
            #self.lineB.figure.canvas.draw()

        #if self.dragging_timer == 0:
        #    self.lineR.figure.canvas.draw()
        
    def clear(self, button):
        
        if self.trained == False:
            with self.my_out:
                print('can only reset if trained')
            return
        
        with self.my_out:
            print('resetted the widget')
            
        self.trained = False
        
        self.remove_decision_boundary()
        self.C1 = None
        self.C2 = None
        self.model = None
        self.xsR = []
        self.ysR = []
        self.xsB = []
        self.ysB = []
        self.lineR.set_data(self.xsR, self.ysR)
        self.lineB.set_data(self.xsB, self.ysB)
        self.lineB.figure.canvas.draw()
        self.lineR.figure.canvas.draw()
        
        
    def export(self):
        
        dataR = np.array([self.xsR,self.ysR]).transpose()
        dataB = np.array([self.xsB,self.ysB]).transpose()
        yR = np.ones((dataR.shape[0], 1))
        yB = -np.ones((dataB.shape[0], 1))
        X = np.concatenate((dataR,dataB))
        y = np.concatenate((yR,yB))
        y = np.reshape(y,y.shape[0])
        return (X,y)
    
    def train(self, button):
        
        self.my_out.clear_output()
        
        if len(self.xsR) < 1 or len(self.xsB) < 1:
            with self.my_out:
                print('need at least one object in both classes to train')
            return
        
        (X,y) = self.export()
        
        if self.need_seperable:
            C = float('inf')
        else:
            C = self.widgetC.value
        
        model = sklearn.svm.LinearSVC(loss='hinge',C=C)
        model.fit(X,y)
        
        if self.need_seperable:
            acc = model.score(X,y)
            if acc < 0.99999:
                with self.my_out:
                    print('this dataset is not seperable')
                return
                
        self.remove_decision_boundary()
        
        train_error = model.score(X,y)
        
        (C1, C2) = plot_decision_boundary_margin(X,y,model)
        self.C1 = C1
        self.C2 = C2
        
        self.model = model
        
        self.trained = True
        
        with self.my_out:
            if self.need_seperable:
                print('trained hard margin SVM')
            else:
                print('trained soft margin SVM with C %f' % C)
        

def init(need_seperable = True):

    # Turn off interactivity, for now
    plt.ioff()

    fig = plt.figure(figsize = (4,4))
    ax = fig.add_subplot(111)

    # Make some nice axes
    ax.set_xlim(-3, 3)
    ax.set_ylim(-3, 3)
    ax.set_title('click to add points')
    ax.set_xlabel('Feature 1')
    ax.set_ylabel('Feature 2')

    # Remove some stuff from the backend
    #fig.canvas.toolbar_visible = False # Hide toolbar
    #fig.canvas.header_visible = False # Hide the Figure name at the top of the figure
    #fig.canvas.footer_visible = False
    #fig.canvas.resizable = False

    # These items will contain the objects
    lineR, = ax.plot([], [], linestyle="none", marker="s", color="g", markersize=10)
    lineB, = ax.plot([], [], linestyle="none", marker="^", color="b", markersize=10)

    # Make the GUI
    w_clear = widgets.Button(
        description='Clear all',
        disabled=False,
        button_style='danger', # 'success', 'info', 'warning', 'danger' or ''
        tooltip='Remove all data and start from scratch',
        icon='check' # (FontAwesome names without the `fa-` prefix)
    )

    w_color = widgets.ToggleButtons(
        options=['green', 'blue'],
        description='Class:',
        disabled=False,
        button_style='', # 'success', 'info', 'warning', 'danger' or ''
        tooltips=['Description of slow', 'Description of regular'],
    #     icons=['check'] * 3
    )

    if not need_seperable:
        w_C = widgets.FloatLogSlider(
            value=1,
            base=10,
            min=-10, # max exponent of base
            max=10, # min exponent of base
            step=0.2, # exponent step
            #description='Log Slider',
            description='C:',
            continuous_update=False,
            orientation='horizontal',
            readout=True,
            #readout_format='.2f',
        )
    else:
        w_C = None

    w_train = widgets.Button(
        description='Train SVM',
        disabled=False,
        button_style='warning', # 'success', 'info', 'warning', 'danger' or ''
        tooltip='...',
        icon='check' # (FontAwesome names without the `fa-` prefix)
    )

    out = widgets.Output(layout={'border': '1px solid black'})
    out.layout.height = '40px'
    out.layout.width = '600px'


    if need_seperable:
        b1 = widgets.HBox([w_color,w_train])
        bar = widgets.VBox([b1, out])
    else:
        b1 = widgets.HBox([w_color,w_C,w_train])
        #b2 = widgets.HBox([w_train,w_C])
        bar = widgets.VBox([b1, out])

    linebuilder = LineBuilder2(lineR, lineB, w_color, w_C, out, need_seperable)
    w_clear.on_click(linebuilder.clear)
    w_train.on_click(linebuilder.train)

    # Turn interactivity back on
    plt.ion()

    out = fig
    ui = bar
    return display(ui, out)

To start the interactivity, I use the following in a Jupyter notebook:

%matplotlib notebook
from svm_helper import init
init()
  • Is your system where the students will do this up to date and do you have a particular minimal example code to share? Because if you don't want the cruft that comes with `%matplotlib notebook`, you can still have interactivity with your plot without it. A lot has changed recently, and so you need the current stuff and sometimes you have to build in the newer interact options slightly different than the original way. Alternatively, you can use Voila to render the interactive parts. – Wayne Mar 10 '22 at 20:20
  • Hi Wayne, thanks for the response. I've posted the Python code to reproduce my interactive plot (sorry for the lengthy code - I will see if I can make a minimal working example). Unfortunately, I don't have full control of the versions of the packages used, I will have a look at this to see what version I need to use, and will get back to this. – Tom Viering Mar 17 '22 at 08:27
  • Does [this](https://stackoverflow.com/a/61331386/8881141) work? (Disclaimer: I have no idea about the jupyter notebook environment) The toolbar class including source code is described [here](https://matplotlib.org/stable/api/backend_bases_api.html?highlight=navigation%20toolbar#matplotlib.backend_bases.NavigationToolbar2). – Mr. T Mar 17 '22 at 08:32
  • So far I haven't come up with the magic combination yet. I see your widget use here isn't quite fully ipywidgets and so Voila use doesn't apply as it doesn't even display the plot. And a comment [here](https://stackoverflow.com/a/15536216/8508004) says the `mpl.rcParams['toolbar'] = 'None'` trick doesn't work with the nbagg backend that seems to be only one that provides the other interactivity with your current code @TomViering . – Wayne Mar 17 '22 at 16:02

1 Answers1

0

So far, I've found adding the following code (from here) in a cell above the cell you have beginning with %matplotlib notebook works:

%%html
<style>
.output_wrapper button.btn.btn-default,
.output_wrapper .ui-dialog-titlebar {
  display: none;
}
</style>

Maybe not ideal since instead of explaining to your students to just ignore the buttons, you have to explain why they have to run this, but it's something.

Wayne
  • 6,607
  • 8
  • 36
  • 93
  • Thanks Wayne! I found there is an ipywidget called HTML. Now I've integrated that widget into my interactive plot, and so the styling is applied by running only the single code block. – Tom Viering Mar 17 '22 at 19:43
  • Interesting. Can you share a gist (or post of it on some other snippet sharing resource) of that combined with your plot code or at least how that could be combined with the HTML widget since you worked it out already? Thanks. – Wayne Mar 18 '22 at 15:36