Skip to content

Commit

Permalink
Merge pull request matplotlib#1878 from pelson/webagg_changes
Browse files Browse the repository at this point in the history
Webagg changes
  • Loading branch information
mdboom committed Apr 12, 2013
2 parents 1ccf29d + 786a6b4 commit 9e477b3
Show file tree
Hide file tree
Showing 7 changed files with 521 additions and 257 deletions.
1 change: 0 additions & 1 deletion lib/matplotlib/_pylab_helpers.py
Expand Up @@ -6,7 +6,6 @@
import sys, gc

import atexit
import traceback


def error_msg(msg):
Expand Down
243 changes: 160 additions & 83 deletions lib/matplotlib/backends/backend_webagg.py
Expand Up @@ -44,14 +44,16 @@ def draw_if_interactive():
class Show(backend_bases.ShowBase):
def mainloop(self):
WebAggApplication.initialize()
for manager in Gcf.get_all_fig_managers():
url = "http://127.0.0.1:{0}/{1}/".format(
WebAggApplication.port, manager.num)
if rcParams['webagg.open_in_browser']:
import webbrowser
webbrowser.open(url)
else:
print("To view figure, visit {0}".format(url))

url = "http://127.0.0.1:{port}{prefix}".format(
port=WebAggApplication.port,
prefix=WebAggApplication.url_prefix)

if rcParams['webagg.open_in_browser']:
import webbrowser
webbrowser.open(url)
else:
print("To view figure, visit {0}".format(url))

WebAggApplication.start()

Expand Down Expand Up @@ -161,9 +163,9 @@ def get_diff_image(self):
# The buffer is created as type uint32 so that entire
# pixels can be compared in one numpy call, rather than
# needing to compare each plane separately.
buffer = np.frombuffer(
buff = np.frombuffer(
self._renderer.buffer_rgba(), dtype=np.uint32)
buffer.shape = (
buff.shape = (
self._renderer.height, self._renderer.width)

if not self._force_full:
Expand All @@ -172,10 +174,10 @@ def get_diff_image(self):
last_buffer.shape = (
self._renderer.height, self._renderer.width)

diff = buffer != last_buffer
output = np.where(diff, buffer, 0)
diff = buff != last_buffer
output = np.where(diff, buff, 0)
else:
output = buffer
output = buff

# Clear out the PNG data buffer rather than recreating it
# each time. This reduces the number of memory
Expand All @@ -198,27 +200,30 @@ def get_diff_image(self):
return self._png_buffer.getvalue()

def get_renderer(self):
l, b, w, h = self.figure.bbox.bounds
# Mirrors super.get_renderer, but caches the old one
# so that we can do things such as prodce a diff image
# in get_diff_image
_, _, w, h = self.figure.bbox.bounds
key = w, h, self.figure.dpi
try:
self._lastKey, self._renderer
except AttributeError:
need_new_renderer = True
else:
need_new_renderer = (self._lastKey != key)

if need_new_renderer:
self._renderer = backend_agg.RendererAgg(
w, h, self.figure.dpi)
self._last_renderer = backend_agg.RendererAgg(
w, h, self.figure.dpi)
self._lastKey = key

return self._renderer

def handle_event(self, event):
type = event['type']
if type in ('button_press', 'button_release', 'motion_notify'):
e_type = event['type']
if e_type in ('button_press', 'button_release', 'motion_notify'):
x = event['x']
y = event['y']
y = self.get_renderer().height - y
Expand All @@ -234,23 +239,24 @@ def handle_event(self, event):
if button == 2:
button = 3

if type == 'button_press':
if e_type == 'button_press':
self.button_press_event(x, y, button)
elif type == 'button_release':
elif e_type == 'button_release':
self.button_release_event(x, y, button)
elif type == 'motion_notify':
elif e_type == 'motion_notify':
self.motion_notify_event(x, y)
elif type in ('key_press', 'key_release'):
elif e_type in ('key_press', 'key_release'):
key = event['key']

if type == 'key_press':
if e_type == 'key_press':
self.key_press_event(key)
elif type == 'key_release':
elif e_type == 'key_release':
self.key_release_event(key)
elif type == 'toolbar_button':
elif e_type == 'toolbar_button':
print('Toolbar button pressed: ', event['name'])
# TODO: Be more suspicious of the input
getattr(self.toolbar, event['name'])()
elif type == 'refresh':
elif e_type == 'refresh':
self._force_full = True
self.draw_idle()

Expand Down Expand Up @@ -306,24 +312,27 @@ def resize(self, w, h):


class NavigationToolbar2WebAgg(backend_bases.NavigationToolbar2):
toolitems = list(backend_bases.NavigationToolbar2.toolitems[:6]) + [
('Download', 'Download plot', 'filesave', 'download')
]
_jquery_icon_classes = {'home': 'ui-icon ui-icon-home',
'back': 'ui-icon ui-icon-circle-arrow-w',
'forward': 'ui-icon ui-icon-circle-arrow-e',
'zoom_to_rect': 'ui-icon ui-icon-search',
'move': 'ui-icon ui-icon-arrow-4',
'download': 'ui-icon ui-icon-disk',
None: None
}

def _init_toolbar(self):
jqueryui_icons = [
'ui-icon ui-icon-home',
'ui-icon ui-icon-circle-arrow-w',
'ui-icon ui-icon-circle-arrow-e',
None,
'ui-icon ui-icon-arrow-4',
'ui-icon ui-icon-search',
'ui-icon ui-icon-disk'
]
for index, item in enumerate(self.toolitems):
if item[0] is not None:
self.toolitems[index] = (
item[0], item[1], jqueryui_icons[index], item[3])
# Use the standard toolbar items + download button
toolitems = (backend_bases.NavigationToolbar2.toolitems +
(('Download', 'Download plot', 'download', 'download'),))

NavigationToolbar2WebAgg.toolitems = \
tuple(
(text, tooltip_text, self._jquery_icon_classes[image_file],
name_of_method)
for text, tooltip_text, image_file, name_of_method
in toolitems if image_file in self._jquery_icon_classes)

self.message = ''
self.cursor = 0

Expand Down Expand Up @@ -356,20 +365,71 @@ def release_zoom(self, event):
class WebAggApplication(tornado.web.Application):
initialized = False
started = False

_mpl_data_path = os.path.join(os.path.dirname(os.path.dirname(__file__)),
'mpl-data')
_mpl_dirs = {'mpl-data': _mpl_data_path,
'images': os.path.join(_mpl_data_path, 'images'),
'web_backend': os.path.join(os.path.dirname(__file__),
'web_backend')}

class FavIcon(tornado.web.RequestHandler):
def get(self):
self.set_header('Content-Type', 'image/png')
with open(os.path.join(
os.path.dirname(__file__),
'../mpl-data/images/matplotlib.png')) as fd:
with open(os.path.join(WebAggApplication._mpl_dirs['images'],
'matplotlib.png')) as fd:
self.write(fd.read())

class IndexPage(tornado.web.RequestHandler):
class SingleFigurePage(tornado.web.RequestHandler):
def __init__(self, application, request, **kwargs):
self.url_prefix = kwargs.pop('url_prefix', '')
return tornado.web.RequestHandler.__init__(self, application,
request, **kwargs)

def get(self, fignum):
with open(os.path.join(WebAggApplication._mpl_dirs['web_backend'],
'single_figure.html')) as fd:
tpl = fd.read()

fignum = int(fignum)
manager = Gcf.get_fig_manager(fignum)

ws_uri = 'ws://{req.host}{prefix}/'.format(req=self.request,
prefix=self.url_prefix)
t = tornado.template.Template(tpl)
self.write(t.generate(
prefix=self.url_prefix,
ws_uri=ws_uri,
fig_id=fignum,
toolitems=NavigationToolbar2WebAgg.toolitems,
canvas=manager.canvas))

class AllFiguresPage(tornado.web.RequestHandler):
def __init__(self, application, request, **kwargs):
self.url_prefix = kwargs.pop('url_prefix', '')
return tornado.web.RequestHandler.__init__(self, application,
request, **kwargs)

def get(self):
with open(os.path.join(WebAggApplication._mpl_dirs['web_backend'],
'all_figures.html')) as fd:
tpl = fd.read()

ws_uri = 'ws://{req.host}{prefix}/'.format(req=self.request,
prefix=self.url_prefix)
t = tornado.template.Template(tpl)

self.write(t.generate(
prefix=self.url_prefix,
ws_uri=ws_uri,
figures = sorted(list(Gcf.figs.items()), key=lambda item: item[0]),
toolitems=NavigationToolbar2WebAgg.toolitems))


class MPLInterfaceJS(tornado.web.RequestHandler):
def get(self, fignum):
with open(os.path.join(
os.path.dirname(__file__),
'web_backend', 'index.html')) as fd:
with open(os.path.join(WebAggApplication._mpl_dirs['web_backend'],
'mpl_interface.js')) as fd:
tpl = fd.read()

fignum = int(fignum)
Expand All @@ -381,7 +441,7 @@ def get(self, fignum):
canvas=manager.canvas))

class Download(tornado.web.RequestHandler):
def get(self, fignum, format):
def get(self, fignum, fmt):
self.fignum = int(fignum)
manager = Gcf.get_fig_manager(self.fignum)

Expand All @@ -397,11 +457,11 @@ def get(self, fignum, format):
'emf': 'application/emf'
}

self.set_header('Content-Type', mimetypes.get(format, 'binary'))
self.set_header('Content-Type', mimetypes.get(fmt, 'binary'))

buffer = io.BytesIO()
manager.canvas.print_figure(buffer, format=format)
self.write(buffer.getvalue())
buff = io.BytesIO()
manager.canvas.print_figure(buff, format=fmt)
self.write(buff.getvalue())

class WebSocket(tornado.websocket.WebSocketHandler):
supports_binary = True
Expand All @@ -410,7 +470,7 @@ def open(self, fignum):
self.fignum = int(fignum)
manager = Gcf.get_fig_manager(self.fignum)
manager.add_web_socket(self)
l, b, w, h = manager.canvas.figure.bbox.bounds
_, _, w, h = manager.canvas.figure.bbox.bounds
manager.resize(w, h)
self.on_message('{"type":"refresh"}')

Expand Down Expand Up @@ -443,52 +503,69 @@ def send_image(self):
diff.encode('base64').replace('\n', ''))
self.write_message(data_uri)

def __init__(self):
def __init__(self, url_prefix=''):
if url_prefix:
assert url_prefix[0] == '/' and url_prefix[-1] != '/', \
'url_prefix must start with a "/" and not end with one.'

super(WebAggApplication, self).__init__([
# Static files for the CSS and JS
(r'/static/(.*)',
(url_prefix + r'/_static/(.*)',
tornado.web.StaticFileHandler,
{'path':
os.path.join(os.path.dirname(__file__), 'web_backend')}),
{'path': self._mpl_dirs['web_backend']}),

# Static images for toolbar buttons
(r'/images/(.*)',
(url_prefix + r'/_static/images/(.*)',
tornado.web.StaticFileHandler,
{'path':
os.path.join(os.path.dirname(__file__), '../mpl-data/images')}),
(r'/static/jquery/css/themes/base/(.*)',
{'path': self._mpl_dirs['images']}),
(url_prefix + r'/_static/jquery/css/themes/base/(.*)',
tornado.web.StaticFileHandler,
{'path':
os.path.join(os.path.dirname(__file__),
'web_backend/jquery/css/themes/base')}),
(r'/static/jquery/css/themes/base/images/(.*)',
{'path': os.path.join(self._mpl_dirs['web_backend'], 'jquery',
'css', 'themes', 'base')}),
(url_prefix + r'/_static/jquery/css/themes/base/images/(.*)',
tornado.web.StaticFileHandler,
{'path':
os.path.join(os.path.dirname(__file__),
'web_backend/jquery/css/themes/base/images')}),
(r'/static/jquery/js/(.*)', tornado.web.StaticFileHandler,
{'path':
os.path.join(os.path.dirname(__file__),
'web_backend/jquery/js')}),
(r'/static/css/(.*)', tornado.web.StaticFileHandler,
{'path':
os.path.join(os.path.dirname(__file__), 'web_backend/css')}),
{'path': os.path.join(self._mpl_dirs['web_backend'], 'jquery',
'css', 'themes', 'base', 'images')}),
(url_prefix + r'/_static/jquery/js/(.*)', tornado.web.StaticFileHandler,
{'path': os.path.join(self._mpl_dirs['web_backend'],
'jquery', 'js')}),
(url_prefix + r'/_static/css/(.*)', tornado.web.StaticFileHandler,
{'path': os.path.join(self._mpl_dirs['web_backend'], 'css')}),
# An MPL favicon
(r'/favicon.ico', self.FavIcon),
(url_prefix + r'/favicon.ico', self.FavIcon),

# The page that contains all of the pieces
(r'/([0-9]+)/', self.IndexPage),
(url_prefix + r'/([0-9]+)', self.SingleFigurePage,
{'url_prefix': url_prefix}),

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

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

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

# The page that contains all of the figures
(url_prefix + r'/?', self.AllFiguresPage,
{'url_prefix': url_prefix}),
])

@classmethod
def initialize(cls):
def initialize(cls, url_prefix=''):
if cls.initialized:
return

app = cls()
# Create the class instance
app = cls(url_prefix=url_prefix)

cls.url_prefix = url_prefix

# This port selection algorithm is borrowed, more or less
# verbatim, from IPython.
Expand Down

0 comments on commit 9e477b3

Please sign in to comment.