I was trying to embed matplotlib into web page via Tornado (required by matplotlib backend). It was plotting some circles but drag and drop functionality was not working and maybe blitting doesn't support in webagg core backend. I figured it out the issue because of the events were not fired. I couldn't find the solution to make it work yet. Please see the following code:
import json
import mimetypes
from pathlib import Path
try:
import tornado
except ImportError as err:
raise RuntimeError("This example requires tornado.") from err
import tornado.web
import tornado.httpserver
import tornado.ioloop
import tornado.websocket
import matplotlib as mpl
from matplotlib.backends.backend_webagg_core import (
FigureManagerWebAgg, new_figure_manager_given_figure)
from matplotlib.figure import Figure
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
def create_figure():
fig = plt.figure()
ax = fig.add_subplot(111)
drs = []
circles = [patches.Circle((0.32, 0.3), 0.03, fc='r', alpha=0.5),
patches.Circle((0.2, 0.35), 0.03, fc='g', alpha=0.5),
patches.Circle((0.1, 0.3), 0.03, fc='y', alpha=0.5),
patches.Circle((0.4, 0.4), 0.03, fc='r', alpha=0.5),
patches.Circle((0.3, 0.1), 0.03, fc='g', alpha=0.5)]
for circ in circles:
ax.add_patch(circ)
dr = DraggablePoint(circ)
dr.connect()
drs.append(dr)
return fig
# The following is the content of the web page. You would normally
# generate this using some sort of template facility in your web
# framework, but here we just use Python string formatting.
html_content = """
<html>
<head>
<!-- TODO: There should be a way to include all of the required javascript
and CSS so matplotlib can add to the set in the future if it
needs to. -->
<link rel="stylesheet" href="_static/css/page.css" type="text/css">
<link rel="stylesheet" href="_static/css/boilerplate.css"
type="text/css" />
<link rel="stylesheet" href="_static/css/fbm.css" type="text/css" />
<link rel="stylesheet" href="_static/css/mpl.css" type="text/css">
<script src="mpl.js"></script>
<script>
/* This is a callback that is called when the user saves
(downloads) a file. Its purpose is really to map from a
figure and file format to a url in the application. */
function ondownload(figure, format) {
window.open('download.' + format, '_blank');
};
function ready(fn) {
if (document.readyState != "loading") {
fn();
} else {
document.addEventListener("DOMContentLoaded", fn);
}
}
ready(
function() {
/* It is up to the application to provide a websocket that the figure
will use to communicate to the server. This websocket object can
also be a "fake" websocket that underneath multiplexes messages
from multiple figures, if necessary. */
var websocket_type = mpl.get_websocket_type();
var websocket = new websocket_type("%(ws_uri)sws");
// mpl.figure creates a new figure on the webpage.
var fig = new mpl.figure(
// A unique numeric identifier for the figure
%(fig_id)s,
// A websocket object (or something that behaves like one)
websocket,
// A function called when a file type is selected for download
ondownload,
// The HTML element in which to place the figure
document.getElementById("figure"));
}
);
</script>
<title>matplotlib</title>
</head>
<body>
<div id="figure">
</div>
</body>
</html>
"""
class MyApplication(tornado.web.Application):
class MainPage(tornado.web.RequestHandler):
"""
Serves the main HTML page.
"""
def get(self):
manager = self.application.manager
ws_uri = "ws://{req.host}/".format(req=self.request)
content = html_content % {
"ws_uri": ws_uri, "fig_id": manager.num}
self.write(content)
class MplJs(tornado.web.RequestHandler):
"""
Serves the generated matplotlib javascript file. The content
is dynamically generated based on which toolbar functions the
user has defined. Call `FigureManagerWebAgg` to get its
content.
"""
def get(self):
self.set_header('Content-Type', 'application/javascript')
js_content = FigureManagerWebAgg.get_javascript()
self.write(js_content)
class Download(tornado.web.RequestHandler):
"""
Handles downloading of the figure in various file formats.
"""
def get(self, fmt):
manager = self.application.manager
self.set_header(
'Content-Type', mimetypes.types_map.get(fmt, 'binary'))
buff = io.BytesIO()
manager.canvas.figure.savefig(buff, format=fmt)
self.write(buff.getvalue())
class WebSocket(tornado.websocket.WebSocketHandler):
"""
A websocket for interactive communication between the plot in
the browser and the server.
In addition to the methods required by tornado, it is required to
have two callback methods:
- ``send_json(json_content)`` is called by matplotlib when
it needs to send json to the browser. `json_content` is
a JSON tree (Python dictionary), and it is the responsibility
of this implementation to encode it as a string to send over
the socket.
- ``send_binary(blob)`` is called to send binary image data
to the browser.
"""
supports_binary = True
def open(self):
# Register the websocket with the FigureManager.
manager = self.application.manager
manager.add_web_socket(self)
if hasattr(self, 'set_nodelay'):
self.set_nodelay(True)
def on_close(self):
# When the socket is closed, deregister the websocket with
# the FigureManager.
manager = self.application.manager
manager.remove_web_socket(self)
def on_message(self, message):
# The 'supports_binary' message is relevant to the
# websocket itself. The other messages get passed along
# to matplotlib as-is.
# Every message has a "type" and a "figure_id".
message = json.loads(message)
if message['type'] == 'supports_binary':
self.supports_binary = message['value']
else:
manager = self.application.manager
manager.handle_json(message)
def send_json(self, content):
self.write_message(json.dumps(content))
def send_binary(self, blob):
if self.supports_binary:
self.write_message(blob, binary=True)
else:
data_uri = "data:image/png;base64,{0}".format(
blob.encode('base64').replace('\n', ''))
self.write_message(data_uri)
def __init__(self, figure):
self.figure = figure
self.manager = new_figure_manager_given_figure(id(figure), figure)
super().__init__([
# Static files for the CSS and JS
(r'/_static/(.*)',
tornado.web.StaticFileHandler,
{'path': FigureManagerWebAgg.get_static_file_path()}),
# Static images for the toolbar
(r'/_images/(.*)',
tornado.web.StaticFileHandler,
{'path': Path(mpl.get_data_path(), 'images')}),
# The page that contains all of the pieces
('/', self.MainPage),
('/mpl.js', self.MplJs),
# Sends images and events to the browser, and receives
# events from the browser
('/ws', self.WebSocket),
# Handles the downloading (i.e., saving) of static images
(r'/download.([a-z0-9.]+)', self.Download),
])
class DraggablePoint:
lock = None #only one can be animated at a time
def __init__(self, point):
self.point = point
self.press = None
cv = self.point.figure.canvas
self.background = cv.copy_from_bbox(cv.figure.bbox)
def connect(self):
'connect to all the events we need'
self.cidpress = self.point.figure.canvas.mpl_connect('button_press_event', self.on_press)
self.cidrelease = self.point.figure.canvas.mpl_connect('button_release_event', self.on_release)
self.cidmotion = self.point.figure.canvas.mpl_connect('motion_notify_event', self.on_motion)
def on_press(self, event):
print('on press...')
if event.inaxes != self.point.axes: return
if DraggablePoint.lock is not None: return
contains, attrd = self.point.contains(event)
if not contains: return
self.press = (self.point.center), event.xdata, event.ydata
DraggablePoint.lock = self
# # draw everything but the selected rectangle and store the pixel buffer
# canvas = self.point.figure.canvas
# axes = self.point.axes
# self.point.set_animated(True)
# canvas.draw()
# self.background = canvas.copy_from_bbox(self.point.axes.bbox)
# # now redraw just the rectangle
# axes.draw_artist(self.point)
# # and blit just the redrawn area
# canvas.blit(axes.bbox)
def on_motion(self, event):
if DraggablePoint.lock is not self:
return
if event.inaxes != self.point.axes: return
self.point.center, xpress, ypress = self.press
dx = event.xdata - xpress
dy = event.ydata - ypress
self.point.center = (self.point.center[0]+dx, self.point.center[1]+dy)
canvas = self.point.figure.canvas
axes = self.point.axes
# restore the background region
canvas.restore_region(self.background)
# redraw just the current rectangle
axes.draw_artist(self.point)
# blit just the redrawn area
canvas.blit(axes.bbox)
def on_release(self, event):
'on release we reset the press data'
if DraggablePoint.lock is not self:
return
self.press = None
DraggablePoint.lock = None
# turn off the rect animation property and reset the background
# self.point.set_animated(False)
# self.background = None
# redraw the full figure
# self.point.figure.canvas.draw()
def disconnect(self):
'disconnect all the stored connection ids'
self.point.figure.canvas.mpl_disconnect(self.cidpress)
self.point.figure.canvas.mpl_disconnect(self.cidrelease)
self.point.figure.canvas.mpl_disconnect(self.cidmotion)
if __name__ == "__main__":
figure = create_figure()
application = MyApplication(figure)
http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(8080)
print("http://127.0.0.1:8080/")
print("Press Ctrl+C to quit")
tornado.ioloop.IOLoop.instance().start()
解决方案
I solved my own issue by adding a call method to receive the triggered events.