""" ================ Embedding WebAgg ================ This example demonstrates how to embed Matplotlib WebAgg interactive plotting in your own web application and framework. It is not necessary to do all this if you merely want to display a plot in a browser or use Matplotlib's built-in Tornado-based server "on the side". The framework being used must support web sockets. """ import argparse import io import json import mimetypes from pathlib import Path import signal import socket 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 import ( FigureManagerWebAgg, new_figure_manager_given_figure) from matplotlib.figure import Figure import numpy as np def create_figure(): """ Creates a simple example figure. """ fig = Figure() ax = fig.add_subplot() t = np.arange(0.0, 3.0, 0.01) s = np.sin(2 * np.pi * t) ax.plot(t, s) 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 = """ matplotlib
""" 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), ]) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument('-p', '--port', type=int, default=8080, help='Port to listen on (0 for a random port).') args = parser.parse_args() figure = create_figure() application = MyApplication(figure) http_server = tornado.httpserver.HTTPServer(application) sockets = tornado.netutil.bind_sockets(args.port, '') http_server.add_sockets(sockets) for s in sockets: addr, port = s.getsockname()[:2] if s.family is socket.AF_INET6: addr = f'[{addr}]' print(f"Listening on http://{addr}:{port}/") print("Press Ctrl+C to quit") ioloop = tornado.ioloop.IOLoop.instance() def shutdown(): ioloop.stop() print("Server stopped") old_handler = signal.signal( signal.SIGINT, lambda sig, frame: ioloop.add_callback_from_signal(shutdown)) try: ioloop.start() finally: signal.signal(signal.SIGINT, old_handler)