8000 Merge pull request #2420 from mdboom/webagg-factor · matplotlib/matplotlib@9804c5d · GitHub
[go: up one dir, main page]

Skip to content

Commit 9804c5d

Browse files
committed
Merge pull request #2420 from mdboom/webagg-factor
Refactor WebAgg so it can communicate over another web server
2 parents 2b614cf + 6389d14 commit 9804c5d

File tree

10 files changed

+934
-581
lines changed

10 files changed

+934
-581
lines changed
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
"""
2+
This example demonstrates how to embed matplotlib WebAgg interactive
3+
plotting in your own web application and framework. It is not
4+
necessary to do all this if you merely want to display a plot in a
5+
browser or use matplotlib's built-in Tornado-based server "on the
6+
side".
7+
8+
The framework being used must support web sockets.
9+
"""
10+
11+
import io
12+
13+
try:
14+
import tornado
15+
except ImportError:
16+
raise RuntimeError("This example requires tornado.")
17+
import tornado.web
18+
import tornado.httpserver
19+
import tornado.ioloop
20+
import tornado.websocket
21+
22+
23+
from matplotlib.backends.backend_webagg_core import (
24+
FigureManagerWebAgg, new_figure_manager_given_figure)
25+
from matplotlib.figure import Figure
26+
27+
import numpy as np
28+
29+
import json
30+
31+
32+
def create_figure():
33+
"""
34+
Creates a simple example figure.
35+
"""
36+
fig = Figure()
37+
a = fig.add_subplot(111)
38+
t = np.arange(0.0, 3.0, 0.01)
39+
s = np.sin(2 * np.pi * t)
40+
a.plot(t, s)
41+
return fig
42+
43+
44+
# The following is the content of the web page. You would normally
45+
# generate this using some sort of template facility in your web
46+
# framework, but here we just use Python string formatting.
47+
html_content = """
48+
<html>
49+
<head>
50+
<!-- TODO: There should be a way to include all of the required javascript
51+
and CSS so matplotlib can add to the set in the future if it
52+
needs to. -->
53+
<link rel="stylesheet" href="_static/css/page.css" type="text/css">
54+
<link rel="stylesheet" href="_static/css/boilerplate.css" type="text/css" />
55+
<link rel="stylesheet" href="_static/css/fbm.css" type="text/css" />
56+
<link rel="stylesheet" href="_static/jquery/css/themes/base/jquery-ui.min.css" >
57+
<script src="_static/jquery/js/jquery-1.7.1.min.js"></script>
58+
<script src="_static/jquery/js/jquery-ui.min.js"></script>
59+
<script src="mpl.js"></script>
60+
61+
<script>
62+
/* This is a callback that is called when the user saves
63+
(downloads) a file. Its purpose is really to map from a
64+
figure and file format to a url in the application. */
65+
function ondownload(figure, format) {
66+
window.open('download.' + format, '_blank');
67+
};
68+
69+
$(document).ready(
70+
function() {
71+
/* It is up to the application to provide a websocket that the figure
72+
will use to communicate to the server. This websocket object can
73+
also be a "fake" websocket that underneath multiplexes messages
74+
from multiple figures, if necessary. */
75+
var websocket_type = mpl.get_websocket_type();
76+
var websocket = new websocket_type("%(ws_uri)sws");
77+
78+
// mpl.figure creates a new figure on the webpage.
79+
var fig = new mpl.figure(
80+
// A unique numeric identifier for the figure
81+
%(fig_id)s,
82+
// A websocket object (or something that behaves like one)
83+
websocket,
84+
// A function called when a file type is selected for download
85+
ondownload,
86+
// The HTML element in which to place the figure
87+
$('div#figure'));
88+
}
89+
);
90+
</script>
91+
92+
<title>matplotlib</title>
93+
</head>
94+
95+
<body>
96+
<div id="figure">
97+
</div>
98+
</body>
99+
</html>
100+
"""
101+
102+
103+
class MyApplication(tornado.web.Application):
104+
class MainPage(tornado.web.RequestHandler):
105+
"""
106+
Serves the main HTML page.
107+
"""
108+
def get(self):
109+
manager = self.application.manager
110+
ws_uri = "ws://{req.host}/".format(req=self.request)
111+
content = html_content % {
112+
"ws_uri": ws_uri, "fig_id": manager.num}
113+
self.write(content)
114+
115+
class MplJs(tornado.web.RequestHandler):
116+
"""
117+
Serves the generated matplotlib javascript file. The content
118+
is dynamically generated based on which toolbar functions the
119+
user has defined. Call `FigureManagerWebAgg` to get its
120+
content.
121+
"""
122+
def get(self):
123+
self.set_header('Content-Type', 'application/javascript')
124+
js_content = FigureManagerWebAgg.get_javascript()
125+
126+
self.write(js_content)
127+
128+
class Download(tornado.web.RequestHandler):
129+
"""
130+
Handles downloading of the figure in various file formats.
131+
"""
132+
def get(self, fmt):
133+
manager = self.application.manager
134+
135+
mimetypes = {
136+
'ps': 'application/postscript',
137+
'eps': 'application/postscript',
138+
'pdf': 'application/pdf',
139+
'svg': 'image/svg+xml',
140+
'png': 'image/png',
141+
'jpeg': 'image/jpeg',
142+
'tif': 'image/tiff',
143+
'emf': 'application/emf'
144+
}
145+
146+
self.set_header('Content-Type', mimetypes.get(fmt, 'binary'))
147+
148+
buff = io.BytesIO()
149+
manager.canvas.print_figure(buff, format=fmt)
150+
self.write(buff.getvalue())
151+
152+
class WebSocket(tornado.websocket.WebSocketHandler):
153+
"""
154+
A websocket for interactive communication between the plot in
155+
the browser and the server.
156+
157+
In addition to the methods required by tornado, it is required to
158+
have two callback methods:
159+
160+
- ``send_json(json_content)`` is called by matplotlib when
161+
it needs to send json to the browser. `json_content` is
162+
a JSON tree (Python dictionary), and it is the responsibility
163+
of this implementation to encode it as a string to send over
164+
the socket.
165+
166+
- ``send_binary(blob)`` is called to send binary image data
167+
to the browser.
168+
"""
169+
supports_binary = True
170+
171+
def open(self):
172+
# Register the websocket with the FigureManager.
173+
manager = self.application.manager
174+
manager.add_web_socket(self)
175+
if hasattr(self, 'set_nodelay'):
176+
self.set_nodelay(True)
177+
178+
def on_close(self):
179+
# When the socket is closed, deregister the websocket with
180+
# the FigureManager.
181+
manager = self.application.manager
182+
manager.remove_web_socket(self)
183+
184+
def on_message(self, message):
185+
# The 'supports_binary' message is relevant to the
186+
# websocket itself. The other messages get passed along
187+
# to matplotlib as-is.
188+
189+
# Every message has a "type" and a "figure_id".
190+
message = json.loads(message)
191+
if message['type'] == 'supports_binary':
192+
self.supports_binary = message['value']
193+
else:
194+
manager = self.application.manager
195+
manager.handle_json(message)
196+
197+
def send_json(self, content):
198+
self.write_message(json.dumps(content))
199+
200+
def send_binary(self, blob):
201+
if self.supports_binary:
202+
self.write_message(blob, binary=True)
203+
else:
204+
data_uri = "data:image/png;base64,{0}".format(
205+
blob.encode('base64').replace('\n', ''))
206+
self.write_message(data_uri)
207+
208+
def __init__(self, figure):
209+
self.figure = figure
210+
self.manager = new_figure_manager_given_figure(
211+
id(figure), figure)
212+
213+
super(MyApplication, self).__init__([
214+
# Static files for the CSS and JS
215+
(r'/_static/(.*)',
216+
tornado.web.StaticFileHandler,
217+
{'path': FigureManagerWebAgg.get_static_file_path()}),
218+
219+
# The page that contains all of the pieces
220+
('/', self.MainPage),
221+
222+
('/mpl.js', self.MplJs),
223+
224+
# Sends images and events to the browser, and receives
225+
# events from the browser
226+
('/ws', self.WebSocket),
227+
228+
# Handles the downloading (i.e., saving) of static images
229+
(r'/download.([a-z0-9.]+)', self.Download),
230+
])
231+
232+
233+
if __name__ == "__main__":
234+
figure = create_figure()
235+
application = MyApplication(figure)
236+
237+
http_server = tornado.httpserver.HTTPServer(application)
238+
http_server.listen(8080)
239+
240+
print("http://127.0.0.1:8080/")
241+
print("Press Ctrl+C to quit")
242+
243+
tornado.ioloop.IOLoop.instance().start()

lib/matplotlib/backend_bases.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2038,17 +2038,19 @@ def print_tif(self, filename_or_obj, *args, **kwargs):
20382038
dpi=dpi)
20392039
print_tiff = print_tif
20402040

2041-
def get_supported_filetypes(self):
2041+
@classmethod
2042+
def get_supported_filetypes(cls):
20422043
"""Return dict of savefig file formats supported by this backend"""
2043-
return self.filetypes
2044+
return cls.filetypes
20442045

2045-
def get_supported_filetypes_grouped(self):
2046+
@classmethod
2047+
def get_supported_filetypes_grouped(cls):
20462048
"""Return a dict of savefig file formats supported by this backend,
20472049
where the keys are a file type name, such as 'Joint Photographic
20482050
Experts Group', and the values are a list of filename extensions used
20492051
for that filetype, such as ['jpg', 'jpeg']."""
20502052
groupings = {}
2051-
for ext, name in six.iteritems(self.filetypes):
2053+
for ext, name in six.iteritems(cls.filetypes):
20522054
groupings.setdefault(name, []).append(ext)
20532055
groupings[name].sort()
20542056
return groupings
@@ -2236,7 +2238,8 @@ def print_figure(self, filename, dpi=None, facecolor='w', edgecolor='w',
22362238
#self.figure.canvas.draw() ## seems superfluous
22372239
return result
22382240

2239-
def get_default_filetype(self):
2241+
@classmethod
2242+
def get_default_filetype(cls):
22402243
"""
22412244
Get the default savefig file format as specified in rcParam
22422245
``savefig.format``. Returned string excludes period. Overridden

0 commit comments

Comments
 (0)
0