Skip to content

Jupyter PlotWidget example#2055

Merged
j9ac9k merged 1 commit intopyqtgraph:masterfrom
pijyoi:jupyter_rfb
Nov 15, 2021
Merged

Jupyter PlotWidget example#2055
j9ac9k merged 1 commit intopyqtgraph:masterfrom
pijyoi:jupyter_rfb

Conversation

@pijyoi
Copy link
Copy Markdown
Contributor

@pijyoi pijyoi commented Nov 2, 2021

This PR is a continuation of #1963 (comment). Github does not allow attaching ipynb, so creating a PR seemed the easier way to continue the discussion.

A standalone Jupyter Notebook is in this PR that implements a Jupyter PlotWidget and example usage.
What works:

  1. resizing
  2. mouse events (panning / zooming)
  3. running under WSL2. Set the environment variable QT_QPA_PLATFORM=offscreen before running jupyter-notebook

What doesn't work well:

  1. mouse wheel zoom
    • easy to accidentally trigger it while intending to scroll the notebook page.

What doesn't work:

  1. context menu caused a crash so is disabled
    • doesn't fit into the role of a notebook widget anyway

Eventually, the PlotWidget class could reside somewhere else within pyqtgraph codebase. But right now, the easiest way to try it out now is to just download the file and run it with your existing installation of pyqtgraph. (plus jupyter_rfb and pillow)

Some notes:

  1. show()-ing the underlying GraphicsView widget causes a window to pop up (on Windows platform anyway). Fortunately, rendering to a QImage works fine without the show().
  2. That it works without having to run the Qt event loop is nice. Note that the mouse events are sent synchronously using sendEvent

@pijyoi pijyoi force-pushed the jupyter_rfb branch 3 times, most recently from bbbe891 to 41ae281 Compare November 2, 2021 01:25
@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 2, 2021

I just tried this out, my goodness, this is amazing @pijyoi ... I had completely written off this functionality some time ago...

This works as intended in Chrome, in Safari none of the interactive bits were working, and I got the following error in the console:

RuntimeError: Cannot enter into task <Task pending name='Task-47' coro=<HTTP1ServerConnection._server_request_loop() running at /Users/ogi/.zinit/plugins/pyenv---pyenv/versions/3.9.7/envs/pyqtgraph-pyqt_62-jupyter-py39/lib/python3.9/site-packages/tornado/http1connection.py:823> wait_for=<Future finished result=b'GET /static...flate\r\n\r\n'> cb=[IOLoop.add_future.<locals>.<lambda>() at /Users/ogi/.zinit/plugins/pyenv---pyenv/versions/3.9.7/envs/pyqtgraph-pyqt_62-jupyter-py39/lib/python3.9/site-packages/tornado/ioloop.py:688]> while another task <Task pending name='Task-2' coro=<KernelManager._async_start_kernel() running at /Users/ogi/.zinit/plugins/pyenv---pyenv/versions/3.9.7/envs/pyqtgraph-pyqt_62-jupyter-py39/lib/python3.9/site-packages/jupyter_client/manager.py:337>> is being executed.
ERROR:asyncio:Exception in callback <TaskWakeupMethWrapper object at 0x105801a60>(<Future finis...late\r\n\r\n'>)
handle: <Handle <TaskWakeupMethWrapper object at 0x105801a60>(<Future finis...late\r\n\r\n'>)>
Traceback (most recent call last):
  File "/Users/ogi/.zinit/plugins/pyenv---pyenv/versions/3.9.7/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
RuntimeError: Cannot enter into task <Task pending name='Task-48' coro=<HTTP1ServerConnection._server_request_loop() running at /Users/ogi/.zinit/plugins/pyenv---pyenv/versions/3.9.7/envs/pyqtgraph-pyqt_62-jupyter-py39/lib/python3.9/site-packages/tornado/http1connection.py:823> wait_for=<Future finished result=b'GET /nbexte...flate\r\n\r\n'> cb=[IOLoop.add_future.<locals>.<lambda>() at /Users/ogi/.zinit/plugins/pyenv---pyenv/versions/3.9.7/envs/pyqtgraph-pyqt_62-jupyter-py39/lib/python3.9/site-packages/tornado/ioloop.py:688]> while another task <Task pending name='Task-2' coro=<KernelManager._async_start_kernel() running at /Users/ogi/.zinit/plugins/pyenv---pyenv/versions/3.9.7/envs/pyqtgraph-pyqt_62-jupyter-py39/lib/python3.9/site-packages/jupyter_client/manager.py:337>> is being executed.

More errors, I don't think this is an issue with our end of the code, so I'm mostly putting this for record keeping purposes.

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Nov 2, 2021

I found somewhere and copied it down (but now I can't find the webpage again) that the following will workaround the asyncio error. But the error doesn't seem to prevent the notebook from working, so I just launch it the simple way now.

# avoid asyncio error
jupyter notebook --NotebookApp.kernel_manager_class=notebook.services.kernels.kernelmanager.AsyncMappingKernelManager

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 2, 2021

nit-pick part 2, on macOS I'm getting the "bouncing icon" after I run the first cell...

image

This normally occurs when launching an application; I can try and investigate this one a bit more since I'm on this platform.

EDIT: a few minutes after I started this, it stopped...huh wonder what took so long.

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Nov 3, 2021

mouse wheel zoom
easy to accidentally trigger it while intending to scroll the notebook page.

Seems like there is some web standard for mouse-wheel : https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent

  1. wheel alone --> scroll
  2. wheel + CTRL --> zoom

pyqtgraph, as a Desktop application, doesn't consider the CTRL modifier for mouse wheel, which makes sense for it since GraphicsView (is a) Re-implementation of QGraphicsView that removes scrollbars

@almarklein
Copy link
Copy Markdown

pyqtgraph, as a Desktop application, doesn't consider the CTRL modifier for mouse wheel

I think most viz applications prefer to keep modifiers to themselves :) there are other ways though.

elif event_type == "wheel":
pos = QtCore.QPointF(event["x"], event["y"])
pixdel = QtCore.QPoint()
scale = -1.0 # map JavaScript wheel to Qt wheel
Copy link
Copy Markdown

@almarklein almarklein Nov 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I've seen, Qt uses steps of 120, versus 96 of JS, so using -120/96 == -1.25 here should be more precise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried out with a mouse wheel. On Qt Desktop, each step generated a delta of 120. But on jupyter_rfb (on Windows Chrome), each step generated a delta of 200 (unscaled). But you are saying that you are getting 96 per step?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I get 96 on Windows Firefox. I assumed that this was more or less standard. Arg. I'll do some more research.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got 100 on Chrome. I wonder why you get 200. Do you happen to be using a high-rez display?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, I was on a laptop with dpr 2.0.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose you "feel" the difference in scrolling too. What about using the touchpad, does it feel more sensitive too?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, I was on a laptop with dpr 2.0.

Could you try on Firefox too please? I don't have access to my hidpi screen right now :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the same Windows laptop with dpr 2.0:
Chrome: 200
Firefox: 96

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made an issue, with a little test html to get some more details: vispy/jupyter_rfb#50

inverted = False
evt = QtGui.QWheelEvent(pos, pos, pixdel, angdel, btns, mods, phase, inverted)
QtCore.QCoreApplication.sendEvent(self.gfxView.viewport(), evt)
self.request_draw()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically the event will be emitted into the application, and if the application reacts to it with changes that need a redraw, it will call request_draw. Then again, this should probably work too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation done here has jupyter_rfb using pyqtgraph as a library; rather than pyqtgraph using jupyter_rfb as a backend. i.e. pyqtgraph has no idea that it is being used in jupyter_notebook, and would not be able to call request_draw()

I did initially start off with a class inheriting from both pyqtgraph's GraphicsView and RemoteFrameBuffer; but got this error with PySide6: TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Nov 5, 2021

Based on @almarklein 's comment on not calling request_draw() upon any mouse event, I connected 2 of ViewBox's signals (sigRangeChangedManually and sigStateChanged) to request_draw(). Having a "pointer_move" trigger a request_draw() doesn't sound correct.

There are some other signals (sigYRangeChanged, sigXRangeChanged, sigRangeChanged, sigTransformChanged). Not being familiar with ViewBox, I am not sure if they should be connected.
Should I connect them all just to be safe? Or should I only connect those known to be needed?
EDIT: connected them anyway

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Nov 5, 2021

Ok, so the downside with not requesting draw on any mouse event is that ViewBox RectMode no longer works since the frame is not redrawn to draw the rubber band.

@almarklein
Copy link
Copy Markdown

I don't know the internals of pyqtgraph, but does it no have an update/request_draw method or something that you can tap into? Requesting a new draw on user interaction can be a practical solution, but its not airtight, because there are other events (thinking mostly of timers for animations) that may require a redraw.

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Nov 6, 2021

As far as I can tell, pyqtgraph internals is Qt. In Desktop Qt, update() is the equivalent of request_draw(), but is non-virtual, so we can't override it.

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Nov 7, 2021

Ok, so the downside with not requesting draw on any mouse event is that ViewBox RectMode no longer works since the frame is not redrawn to draw the rubber band.

So I thought I had a working workaround for the above, but that only handles actions taken by the ViewBox. There are other graphicItems that accept clicks and hover events. (The included ScatterPlot example is one of them) The simplest solution for now is still to request_draw() on each pointer_* event.

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Nov 8, 2021

Compared to the Desktop Qt version, the ScatterPlot example doesn't show tooltip on hover.

There are 2 main items that support clicks, PlotCurveItem and ScatterPlotItem. Clicks don't need special handling, the user, if they hook onto the click signal, could just call request_draw() within their callback.

ScatterPlotItem in addition supports hover and the library automatically creates a tooltip on hover, which is already not working in this rfb implementation.

If we reduce the scope of what we want to achieve in this implementation, we could remove the request_draw() on pointer_move.

@pijyoi pijyoi mentioned this pull request Nov 13, 2021
13 tasks
@pijyoi pijyoi force-pushed the jupyter_rfb branch 2 times, most recently from 38a5474 to 67b94e9 Compare November 13, 2021 09:27
@pijyoi pijyoi marked this pull request as ready for review November 13, 2021 09:47
@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 13, 2021

Hi @pijyoi

This looks good, I did run into one weird behavior, if you run the plotting notebook, and manipulate the linear region item in the 8th plot, the end result isn't quite right.

Also super nit-picky, but probably should remove the encoding pragma.

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Nov 13, 2021

I added the packages that pyqtgraph adds in its CI, but the kernel still crashes upon instantiation of the QApplication. You can try it out here:
https://mybinder.org/v2/gh/pijyoi/pyqtgraph/jupyter_rfb?labpath=pyqtgraph%2Fexamples%2Fnotebooks

if you run the plotting notebook, and manipulate the linear region item in the 8th plot, the end result isn't quite right.

I don't see a difference with the Desktop version though.

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 13, 2021

if you run the plotting notebook, and manipulate the linear region item in the 8th plot, the end result isn't quite right.

I don't see a difference with the Desktop version though.

I'll capture some video shortly.

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Nov 13, 2021

okay, to get it to work on binder, I had to set

import os
os.environ["QT_QPA_PLATFORM"] = "offscreen"

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Nov 13, 2021

Okay, the binder link above does work now!

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 14, 2021

The LinearRegionItem behavior I experienced was only occurring in the Safari web browser (in Chrome everything behaves as it should), which makes me think this potentially a jupyter_rfb issue, not a pyqtgraph one. Being able to replicate the issue in binder is fantastic.

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 14, 2021

I think the only thing this PR would need is to add a blurb to the README, and perhaps to the documentation somewhere, but given we don't reference jupyter anywhere in the docs, I'm not sure where the place to put it would be.

EDIT: also should we move this bit somewhere else a bit more obfuscated, such as in pyqtgraph/jupyter/GraphicsView.py ?

if "BINDER_SERVICE_HOST" in os.environ:
    os.environ["QT_QPA_PLATFORM"] = "offscreen"

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Nov 14, 2021

Just a reminder, before any merge, I would need to point the binder configuration to fetch from pyqtgraph repo.

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Nov 14, 2021

Okay, I can replicate the ROIExamples.ipynb being truncated on binder also when running on WSL2 with QT_QPA_PLATFORM=offscreen. So it probably has something to do with using offscreen.

After bumping up the css sizes, running on WSL2 no longer truncates; but it still gets truncated on binder. Although WSL2 was running PySide6 while binder was running PyQt5.

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 14, 2021

@pijyoi do you want to see if this is a PyQt5 vs. PySide6 difference or you think we should just merge? FWIW this looks good to me.

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Nov 14, 2021

Okay, ready to merging.

  1. Moved binder badge location
  2. Point to pyqtgraph repo
  3. Restore CI workflow

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 15, 2021

This LGTM, I would be interested in having a binder config that doesn't depend on conda (so we're not stuck w/ ancient versions of qt bindings); but given this works and a supported environment; let's leave it as is 👍🏻 ...I suppose notebook environments are more likely to use a conda distribution so probably for the best to replicate that environment.

@j9ac9k j9ac9k merged commit 1b0465a into pyqtgraph:master Nov 15, 2021
@pijyoi pijyoi deleted the jupyter_rfb branch November 15, 2021 14:33
@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Nov 15, 2021

do you want to see if this is a PyQt5 vs. PySide6 difference

I did some testing locally with WSL2. It's a Qt5 vs Qt6 difference.
As the provided container is Ubuntu 18.04, we cannot use Qt6.

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 15, 2021

As the provided container is Ubuntu 18.04, we cannot use Qt6.

While I believe you, this really surprised me, and I double-checked the documentation, and sure enough, [Ubuntu 20.04 is the supported configuration](As the provided container is Ubuntu 18.04, we cannot use Qt6.)

@j9ac9k j9ac9k mentioned this pull request Oct 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants