Skip to content

Commit bdfdbdf

Browse files
committed
Merge pull request #2054 from mdboom/ipython-webagg-integration
Ipython/Webagg integration
2 parents b44bdfe + bdb85ec commit bdfdbdf

File tree

8 files changed

+356
-300
lines changed

8 files changed

+356
-300
lines changed

lib/matplotlib/backends/backend_webagg.py

+109-53
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import os
1111
import random
1212
import socket
13+
import threading
1314

1415
import numpy as np
1516

@@ -20,7 +21,6 @@
2021
import tornado.web
2122
import tornado.ioloop
2223
import tornado.websocket
23-
import tornado.template
2424

2525
import matplotlib
2626
from matplotlib import rcParams
@@ -30,6 +30,15 @@
3030
from matplotlib._pylab_helpers import Gcf
3131
from matplotlib import _png
3232

33+
# TODO: This should really only be set for the IPython notebook, but
34+
# I'm not sure how to detect that.
35+
try:
36+
__IPYTHON__
37+
except:
38+
_in_ipython = False
39+
else:
40+
_in_ipython = True
41+
3342

3443
def draw_if_interactive():
3544
"""
@@ -46,8 +55,8 @@ def mainloop(self):
4655
WebAggApplication.initialize()
4756

4857
url = "http://127.0.0.1:{port}{prefix}".format(
49-
port=WebAggApplication.port,
50-
prefix=WebAggApplication.url_prefix)
58+
port=WebAggApplication.port,
59+
prefix=WebAggApplication.url_prefix)
5160

5261
if rcParams['webagg.open_in_browser']:
5362
import webbrowser
@@ -57,7 +66,25 @@ def mainloop(self):
5766

5867
WebAggApplication.start()
5968

60-
show = Show()
69+
70+
if not _in_ipython:
71+
show = Show()
72+
else:
73+
def show():
74+
from IPython.display import display_html
75+
76+
result = []
77+
import matplotlib._pylab_helpers as pylab_helpers
78+
for manager in pylab_helpers.Gcf().get_all_fig_managers():
79+
result.append(ipython_inline_display(manager.canvas.figure))
80+
return display_html('\n'.join(result), raw=True)
81+
82+
83+
class ServerThread(threading.Thread):
84+
def run(self):
85+
tornado.ioloop.IOLoop.instance().start()
86+
87+
server_thread = ServerThread()
6188

6289

6390
def new_figure_manager(num, *args, **kwargs):
@@ -127,6 +154,16 @@ def __init__(self, *args, **kwargs):
127154
# messages from piling up.
128155
self._pending_draw = None
129156

157+
# TODO: I'd like to dynamically add the _repr_html_ method
158+
# to the figure in the right context, but then IPython doesn't
159+
# use it, for some reason.
160+
161+
# Add the _repr_html_ member to the figure for IPython inline
162+
# support
163+
# if _in_ipython:
164+
# self.figure._repr_html_ = types.MethodType(
165+
# ipython_inline_display, self.figure, self.figure.__class__)
166+
130167
def show(self):
131168
# show the figure window
132169
show()
@@ -199,7 +236,7 @@ def get_diff_image(self):
199236
self._png_is_old = False
200237
return self._png_buffer.getvalue()
201238

202-
def get_renderer(self, cleared=False):
239+
def get_renderer(self, cleared=None):
203240
# Mirrors super.get_renderer, but caches the old one
204241
# so that we can do things such as prodce a diff image
205242
# in get_diff_image
@@ -269,12 +306,12 @@ def start_event_loop(self, timeout):
269306
backend_bases.FigureCanvasBase.start_event_loop_default(
270307
self, timeout)
271308
start_event_loop.__doc__ = \
272-
backend_bases.FigureCanvasBase.start_event_loop_default.__doc__
309+
backend_bases.FigureCanvasBase.start_event_loop_default.__doc__
273310

274311
def stop_event_loop(self):
275312
backend_bases.FigureCanvasBase.stop_event_loop_default(self)
276313
stop_event_loop.__doc__ = \
277-
backend_bases.FigureCanvasBase.stop_event_loop_default.__doc__
314+
backend_bases.FigureCanvasBase.stop_event_loop_default.__doc__
278315

279316

280317
class FigureManagerWebAgg(backend_bases.FigureManagerBase):
@@ -313,26 +350,29 @@ def resize(self, w, h):
313350

314351

315352
class NavigationToolbar2WebAgg(backend_bases.NavigationToolbar2):
316-
_jquery_icon_classes = {'home': 'ui-icon ui-icon-home',
317-
'back': 'ui-icon ui-icon-circle-arrow-w',
318-
'forward': 'ui-icon ui-icon-circle-arrow-e',
319-
'zoom_to_rect': 'ui-icon ui-icon-search',
320-
'move': 'ui-icon ui-icon-arrow-4',
321-
'download': 'ui-icon ui-icon-disk',
322-
None: None
323-
}
353+
_jquery_icon_classes = {
354+
'home': 'ui-icon ui-icon-home',
355+
'back': 'ui-icon ui-icon-circle-arrow-w',
356+
'forward': 'ui-icon ui-icon-circle-arrow-e',
357+
'zoom_to_rect': 'ui-icon ui-icon-search',
358+
'move': 'ui-icon ui-icon-arrow-4',
359+
'download': 'ui-icon ui-icon-disk',
360+
None: None
361+
}
324362

325363
def _init_toolbar(self):
326364
# Use the standard toolbar items + download button
327-
toolitems = (backend_bases.NavigationToolbar2.toolitems +
328-
(('Download', 'Download plot', 'download', 'download'),))
365+
toolitems = (
366+
backend_bases.NavigationToolbar2.toolitems +
367+
(('Download', 'Download plot', 'download', 'download'),)
368+
)
329369

330370
NavigationToolbar2WebAgg.toolitems = \
331371
tuple(
332-
(text, tooltip_text, self._jquery_icon_classes[image_file],
333-
name_of_method)
334-
for text, tooltip_text, image_file, name_of_method
335-
in toolitems if image_file in self._jquery_icon_classes)
372+
(text, tooltip_text, self._jquery_icon_classes[image_file],
373+
name_of_method)
374+
for text, tooltip_text, image_file, name_of_method
375+
in toolitems if image_file in self._jquery_icon_classes)
336376

337377
self.message = ''
338378
self.cursor = 0
@@ -388,22 +428,18 @@ def __init__(self, application, request, **kwargs):
388428
request, **kwargs)
389429

390430
def get(self, fignum):
391-
with open(os.path.join(WebAggApplication._mpl_dirs['web_backend'],
392-
'single_figure.html')) as fd:
393-
tpl = fd.read()
394-
395431
fignum = int(fignum)
396432
manager = Gcf.get_fig_manager(fignum)
397433

398434
ws_uri = 'ws://{req.host}{prefix}/'.format(req=self.request,
399435
prefix=self.url_prefix)
400-
t = tornado.template.Template(tpl)
401-
self.write(t.generate(
436+
self.render(
437+
"single_figure.html",
402438
prefix=self.url_prefix,
403439
ws_uri=ws_uri,
404440
fig_id=fignum,
405441
toolitems=NavigationToolbar2WebAgg.toolitems,
406-
canvas=manager.canvas))
442+
canvas=manager.canvas)
407443

408444
class AllFiguresPage(tornado.web.RequestHandler):
409445
def __init__(self, application, request, **kwargs):
@@ -412,34 +448,27 @@ def __init__(self, application, request, **kwargs):
412448
request, **kwargs)
413449

414450
def get(self):
415-
with open(os.path.join(WebAggApplication._mpl_dirs['web_backend'],
416-
'all_figures.html')) as fd:
417-
tpl = fd.read()
418-
419451
ws_uri = 'ws://{req.host}{prefix}/'.format(req=self.request,
420452
prefix=self.url_prefix)
421-
t = tornado.template.Template(tpl)
422-
423-
self.write(t.generate(
453+
self.render(
454+
"all_figures.html",
424455
prefix=self.url_prefix,
425456
ws_uri=ws_uri,
426-
figures = sorted(list(Gcf.figs.items()), key=lambda item: item[0]),
427-
toolitems=NavigationToolbar2WebAgg.toolitems))
428-
457+
figures=sorted(
458+
list(Gcf.figs.items()), key=lambda item: item[0]),
459+
toolitems=NavigationToolbar2WebAgg.toolitems)
429460

430461
class MPLInterfaceJS(tornado.web.RequestHandler):
431-
def get(self, fignum):
432-
with open(os.path.join(WebAggApplication._mpl_dirs['web_backend'],
433-
'mpl_interface.js')) as fd:
434-
tpl = fd.read()
462+
def get(self):
463+
manager = Gcf.get_fig_manager(1)
464+
canvas = manager.canvas
435465

436-
fignum = int(fignum)
437-
manager = Gcf.get_fig_manager(fignum)
466+
self.set_header('Content-Type', 'application/javascript')
438467

439-
t = tornado.template.Template(tpl)
440-
self.write(t.generate(
468+
self.render(
469+
"mpl_interface.js",
441470
toolitems=NavigationToolbar2WebAgg.toolitems,
442-
canvas=manager.canvas))
471+
canvas=canvas)
443472

444473
class Download(tornado.web.RequestHandler):
445474
def get(self, fignum, fmt):
@@ -516,7 +545,7 @@ def send_diff_image(self, diff):
516545
def __init__(self, url_prefix=''):
517546
if url_prefix:
518547
assert url_prefix[0] == '/' and url_prefix[-1] != '/', \
519-
'url_prefix must start with a "/" and not end with one.'
548+
'url_prefix must start with a "/" and not end with one.'
520549

521550
super(WebAggApplication, self).__init__([
522551
# Static files for the CSS and JS
@@ -539,11 +568,13 @@ def __init__(self, url_prefix=''):
539568
{'path': os.path.join(self._mpl_dirs['web_backend'], 'jquery',
540569
'css', 'themes', 'base', 'images')}),
541570

542-
(url_prefix + r'/_static/jquery/js/(.*)', tornado.web.StaticFileHandler,
571+
(url_prefix + r'/_static/jquery/js/(.*)',
572+
tornado.web.StaticFileHandler,
543573
{'path': os.path.join(self._mpl_dirs['web_backend'],
544574
'jquery', 'js')}),
545575

546-
(url_prefix + r'/_static/css/(.*)', tornado.web.StaticFileHandler,
576+
(url_prefix + r'/_static/css/(.*)',
577+
tornado.web.StaticFileHandler,
547578
{'path': os.path.join(self._mpl_dirs['web_backend'], 'css')}),
548579

549580
# An MPL favicon
@@ -553,19 +584,20 @@ def __init__(self, url_prefix=''):
553584
(url_prefix + r'/([0-9]+)', self.SingleFigurePage,
554585
{'url_prefix': url_prefix}),
555586

556-
(url_prefix + r'/([0-9]+)/mpl_interface.js', self.MPLInterfaceJS),
587+
(url_prefix + r'/mpl_interface.js', self.MPLInterfaceJS),
557588

558589
# Sends images and events to the browser, and receives
559590
# events from the browser
560591
(url_prefix + r'/([0-9]+)/ws', self.WebSocket),
561592

562593
# Handles the downloading (i.e., saving) of static images
563-
(url_prefix + r'/([0-9]+)/download.([a-z]+)', self.Download),
594+
(url_prefix + r'/([0-9]+)/download.([a-z0-9.]+)', self.Download),
564595

565596
# The page that contains all of the figures
566597
(url_prefix + r'/?', self.AllFiguresPage,
567598
{'url_prefix': url_prefix}),
568-
])
599+
],
600+
template_path=self._mpl_dirs['web_backend'])
569601

570602
@classmethod
571603
def initialize(cls, url_prefix=''):
@@ -623,3 +655,27 @@ def start(cls):
623655
print("Server stopped")
624656

625657
cls.started = True
658+
659+
660+
def ipython_inline_display(figure):
661+
import matplotlib._pylab_helpers as pylab_helpers
662+
import tornado.template
663+
664+
WebAggApplication.initialize()
665+
if not server_thread.is_alive():
666+
server_thread.start()
667+
668+
with open(os.path.join(
669+
WebAggApplication._mpl_dirs['web_backend'],
670+
'ipython_inline_figure.html')) as fd:
671+
tpl = fd.read()
672+
673+
fignum = figure.number
674+
675+
t = tornado.template.Template(tpl)
676+
return t.generate(
677+
prefix=WebAggApplication.url_prefix,
678+
fig_id=fignum,
679+
toolitems=NavigationToolbar2WebAgg.toolitems,
680+
canvas=figure.canvas,
681+
port=WebAggApplication.port)

0 commit comments

Comments
 (0)