|
| 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() |
0 commit comments