ContextLab / hypertools

A Python toolbox for gaining geometric insights into high-dimensional data
http://hypertools.readthedocs.io/en/latest/
MIT License
1.81k stars 161 forks source link

(almost) no interactive output on MacOS #258

Closed rob-miller closed 2 years ago

rob-miller commented 2 years ago

Apologies for the truly helpless startup question, but I am really struggling just getting to basic functionality on Mac Monterey.

Sample:

geo = hyp.load("weights_avg")
geo.plot(title="bar")
input("press a key")

If I debug this in VSCode, and set a breakpoint on any line, I seeBackend MacOSX is interactive backend. Turning interactive mode on. and get a widget that I can twirl the graph on. If I run in debug mode from VSCode without a breakpoint, or just run the file in VSCode, or run in a terminal, I get no message, no widget, no plot, and no error. Based on trying to catch the commands in the animations in #245 (because the API docs are still at 0.6 on the website), I've tried setting mpl_backend to various values, with no effect. I have the same negative result running from a linux terminal with a remote desktop display, but don't have vscode set up on that host.

My first goal at this point is to get interactive output working from a script run from the MacOS terminal.

When I try these from JupyterLab:

ndt = cur.fetchall()
dat = np.array(ndt)
%matplotlib inline
hyp.plot(dat, ".")

and

geo = hyp.load("weights_avg")
geo.plot( title="bar")

I get only static images of the plots. If I try geo.plot(Interactive=True, title="bar"), the first error is Javascript Error: IPython is not defined, and after that I have to restart the JupyterLab kernel to get anything working again.

I have hypertools 0.8.0 installed, with python 3.9.13 on the mac and 3.10.4 on the linux box running JupyterLab.

Any suggestions would be very much appreciated.

paxtonfitzpatrick commented 2 years ago

Hey @rob-miller, sorry for the delay getting back to you. There are a few different things going on here:

  1. geo.plot(title="bar") draws the plot to the matplotlib figure canvas, but doesn't by itself display it. After you create or "draw" a plot with either matplotlib or a matplotlib-based library like hypertools or seaborn, you'll typically (though not always -- see below) need to then call matplotlib.pyplot.show() to actually display it on the screen.

    There are a couple reasons geo.plot() doesn’t simply display the figure for you immediately after drawing it. The first is that you may want to add multiple plots to the same set of axes, apply some additional styling to the figure, etc. before it's rendered. For example:

    import hypertools as hyp
    import matplotlib.pyplot as plt
    
    # load the example dataset
    geo = hyp.load("weights_avg")
    # create a pretend 3rd set of weights
    extra_data = 1 - geo.data[0]
    
    # create a 3D Axes object...
    ax = plt.axes(projection='3d')
    # ...use it to plot the "weights_avg" data...
    geo.plot(legend=['group 1', 'group 2'], ax=ax)
    # ...and plot the additional data on the same axes
    geo.plot(extra_data, legend=['group 3'], ax=ax)
    
    # adjust the width & height of the figure
    ax.figure.set_size_inches(6, 4)
    # adjust the location of the legend
    ax.get_legend().set_bbox_to_anchor((.62, .3, .2, .4))
    # remove the outline around the legend 
    ax.get_legend().set_frame_on(False)
    
    # finally, display the figure
    plt.show()

    You should be able to run that snippet from the command line, or from VSCode in either regular or debug mode (without setting a break point) and see this plot appear in a pop-up window: Screen Shot 2022-06-05 at 4 52 16 AM

    The second reason is that, as you've encountered, there are lots of factors, both user-determined and not, that affect matplotlib's display behavior, and it's impossible to account for every combination of them.

  2. The reason you see the plot display window appears and get that "Turning interactive mode on." message when you set a break point is a special behavior of VSCode's Python debugger, and an intentional decision by its creators. You can check out microsoft/debugpy#704 and for more info, but in brief, "interactive" in matplotlib is a bit complicated and kinda has two different meanings:

    1. interactive to you (i.e., you can click on the plot, drag it around, zoom in/out, etc.)
    2. interactive to your program (i.e., your code can continue to interact with, manipulate, add elements to, etc. the plot after you display it, rather than pausing execution until the plot is closed).

    In matplotlib, "interactive mode" technically refers to and controls only the latter, while the former is controlled by the presence of a "GUI event loop". To test this out, you can try running this code:

    import hypertools as hyp
    import matplotlib.pyplot as plt
    
    # load the dataset
    geo = hyp.load("weights_avg")
    
    # turn interactive mode on
    plt.ion()
    print(plt.isinteractive())    # should print "True"
    
    # create a 3D Axes object for the plot
    ax = plt.axes(projection='3d')
    # plot the first group
    hyp.plot(geo.data[0], legend=['group 1'], ax=ax)
    
    # pause execution and wait for input
    input("Press enter to plot the second group")
    # plot the second group on the same axes
    hyp.plot(geo.data[1], legend=['group 2'], ax=ax)
    
    # pause for input
    input("Press enter to add a title")
    # add a title
    ax.set_title('My figure', pad=0.6)
    
    # pause again
    input("Press enter to close the plot and exit the program")

    Since interactive mode is turned on, the figure is automatically displayed after it's drawn by the first hyp.plot() call, without you having to run plt.show(). If you hit enter in your terminal window (in VSCode, if you ran it from there), the second hyp.plot() will be run, and the displayed plot will immediately be updated. Hit enter again to add the title, and again to reach the end of your script, at which point the figure is automatically closed.

    However, you might notice that you can't interact with the plot window at all -- if you try to drag it, nothing will happen until the next time your script updates the plot contents. This is because your script's execution is suspended by the input() statements, rather than by matplotlib's GUI event loop. The GUI event loop essentially redraws the figure constantly so that you can manipulate it with your mouse or the controls in the GUI window, and see the changes reflected in real time.

    The reason you get the interactive window when you add a breakpoint and run VSCode's debugger is that if your program imports matplotlib (which hypertools does internally), VSCode's Python debugger assumes you'll probably want to debug the output of your figures, so it both turns on interactive mode and runs a GUI event loop in the background. That way, when your program is suspended by the debugger, you can both run additional plotting commands in the "debug console" and explore the plot through the GUI, and the appearance of your plot will be updated live.

  3. The reason you get static figures in JupyterLab comes from the %matplotlib inline command. Commands prefixed with % are IPython magic commands, and the %matplotlib magic allows you to set the plotting backend used by matplotlib. The "inline" backend is a special backend that causes matplotlib to run a function called "_flush_figures()_" after each cell, which automatically plt.show()s and plt.close()s any figures you created in that cell, and displays them inline in the HTML of the page, rather than via an interactive GUI window.

    So I'd start by removing the %matplotlib inline command from your snippet and seeing what that changes. If your notebook defaults to inline plotting (as some JupyterLab/Jupyter notebooks versions do), you may want to actively set it to something else, e.g., "%matplotlib notebook" is usually a good bet, and this is probably good to add either way for consistency across Jupyter versions. I'd also recommend doing this in the cell before you run hypertools.plot(), rather than in the same cell.

    When switching to an interactive backend, hypertools internally handles removing the flush_figures() callback added by %matplotlib inline, so normally, I'd expect geo.plot(interactive=True, title="bar") (note the "i" in "interactive" is lowercase) to work. I personally use classic Jupyter notebooks rather than JupyterLab, so I haven't run into that "Javascript Error..." before, but based on some googling, it looks like JupyterLab has moved away from using the global IPython JS object that Jupyter notebooks uses to control the frontend, probably since they're gradually ditching the IPython kernel in general 😕 . If you're looking for interactive plot support for matplotlib & hypertools in JupyterLab, it looks like you may be able to add it manually by following this StackOverflow answer (you'd need to install Node.js on your linux box), which should enable the ipympl/widget backend. If that doesn't work, your best bet is probably switching to Jupyter notebooks if needed for this specific purpose. I probably won't be porting hypertools's interactive plotting backend capability to JupyterLab in the near future, as we're working on a more serious overhaul of the package that moves away from matplotlib in general.

Hope this helps! Let me know if you run into any further issues with this; otherwise, feel free to close the issue 👍

rob-miller commented 2 years ago

Thank you very much, very helpful and informative!

I encourage putting a complete example like the first one somewhere (more?) prominent on the website.