mirror of https://github.com/pyodide/pyodide.git
Add documentation to wasm_backend.py
This commit is contained in:
parent
663ff90695
commit
6319140bb6
|
@ -1,3 +1,17 @@
|
|||
"""
|
||||
A matplotlib backend that renders to an HTML5 canvas in the same thread.
|
||||
|
||||
The Agg backend is used for the actual rendering underneath, and renders the
|
||||
buffer to the HTML5 canvas. This happens with only a single copy of the data
|
||||
into the Canvas -- passing the data from Python to Javascript requires no
|
||||
copies.
|
||||
|
||||
See matplotlib.backend_bases for documentation for most of the methods, since
|
||||
this primarily is just overriding methods in the base class.
|
||||
"""
|
||||
|
||||
# TODO: Figure resizing support
|
||||
|
||||
import base64
|
||||
import io
|
||||
import math
|
||||
|
@ -6,75 +20,11 @@ from matplotlib.backends import backend_agg
|
|||
from matplotlib.backend_bases import _Backend
|
||||
from matplotlib import backend_bases, _png
|
||||
|
||||
from js import iodide
|
||||
from js import document
|
||||
from js import window
|
||||
from js import ImageData
|
||||
|
||||
|
||||
# http://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes
|
||||
_SHIFT_LUT = {
|
||||
59: ':',
|
||||
61: '+',
|
||||
173: '_',
|
||||
186: ':',
|
||||
187: '+',
|
||||
188: '<',
|
||||
189: '_',
|
||||
190: '>',
|
||||
191: '?',
|
||||
192: '~',
|
||||
219: '{',
|
||||
220: '|',
|
||||
221: '}',
|
||||
222: '"'
|
||||
}
|
||||
|
||||
_LUT = {
|
||||
8: 'backspace',
|
||||
9: 'tab',
|
||||
13: 'enter',
|
||||
16: 'shift',
|
||||
17: 'control',
|
||||
18: 'alt',
|
||||
19: 'pause',
|
||||
20: 'caps',
|
||||
27: 'escape',
|
||||
32: ' ',
|
||||
33: 'pageup',
|
||||
34: 'pagedown',
|
||||
35: 'end',
|
||||
36: 'home',
|
||||
37: 'left',
|
||||
38: 'up',
|
||||
39: 'right',
|
||||
40: 'down',
|
||||
45: 'insert',
|
||||
46: 'delete',
|
||||
91: 'super',
|
||||
92: 'super',
|
||||
93: 'select',
|
||||
106: '*',
|
||||
107: '+',
|
||||
109: '-',
|
||||
110: '.',
|
||||
111: '/',
|
||||
144: 'num_lock',
|
||||
145: 'scroll_lock',
|
||||
186: ':',
|
||||
187: '=',
|
||||
188: ',',
|
||||
189: '-',
|
||||
190: '.',
|
||||
191: '/',
|
||||
192: '`',
|
||||
219: '[',
|
||||
220: '\\',
|
||||
221: ']',
|
||||
222: "'"
|
||||
}
|
||||
|
||||
|
||||
class FigureCanvasWasm(backend_agg.FigureCanvasAgg):
|
||||
supports_blit = False
|
||||
|
||||
|
@ -87,12 +37,21 @@ class FigureCanvasWasm(backend_agg.FigureCanvasAgg):
|
|||
self._ratio = 1
|
||||
|
||||
def get_element(self, name):
|
||||
"""
|
||||
Looks up an HTMLElement created for this figure.
|
||||
"""
|
||||
# TODO: Should we store a reference here instead of always looking it
|
||||
# up? I'm a little concerned about weird Python/JS
|
||||
# cross-memory-management issues...
|
||||
return document.getElementById(self._id + name)
|
||||
|
||||
def get_dpi_ratio(self, context):
|
||||
"""
|
||||
Gets the ratio of physical pixels to logical pixels for the given HTML
|
||||
Canvas context.
|
||||
|
||||
This is typically 2 on a HiDPI ("Retina") display, and 1 otherwise.
|
||||
"""
|
||||
backing_store = (
|
||||
context.backingStorePixelRatio or
|
||||
context.webkitBackingStorePixel or
|
||||
|
@ -104,36 +63,52 @@ class FigureCanvasWasm(backend_agg.FigureCanvasAgg):
|
|||
)
|
||||
return (window.devicePixelRatio or 1) / backing_store
|
||||
|
||||
def create_root_element(self):
|
||||
# Designed to be overridden by subclasses for use in contexts other
|
||||
# than iodide.
|
||||
from js import iodide
|
||||
return iodide.output.element('div')
|
||||
|
||||
def show(self):
|
||||
# If we've already shown this canvas elsewhere, don't create a new one,
|
||||
# just reuse it and scroll to the existing one.
|
||||
existing = self.get_element('')
|
||||
if existing is not None:
|
||||
self.draw_idle()
|
||||
existing.scrollIntoView()
|
||||
return
|
||||
|
||||
# Disable the right-click context menu.
|
||||
# Doesn't work in all browsers.
|
||||
def ignore(event):
|
||||
event.preventDefault()
|
||||
return False
|
||||
window.addEventListener('contextmenu', ignore)
|
||||
|
||||
# Create the main canvas and determine the physical to logical pixel
|
||||
# ratio
|
||||
canvas = document.createElement('canvas')
|
||||
context = canvas.getContext('2d')
|
||||
self._ratio = self.get_dpi_ratio(context)
|
||||
if self._ratio != 1:
|
||||
self.figure.dpi *= self._ratio
|
||||
|
||||
renderer = self.get_renderer()
|
||||
width, height = self.get_width_height()
|
||||
div = iodide.output.element('div')
|
||||
div = self.create_root_element()
|
||||
div.setAttribute('style', 'width: {}px'.format(width / self._ratio))
|
||||
div.id = self._id
|
||||
|
||||
# The top bar
|
||||
top = document.createElement('div')
|
||||
top.id = self._id + 'top'
|
||||
top.setAttribute('style', 'font-weight: bold; text-align: center')
|
||||
top.textContent = self._title
|
||||
div.appendChild(top)
|
||||
|
||||
# A div containing two canvases stacked on top of one another:
|
||||
# - The bottom for rendering matplotlib content
|
||||
# - The top for rendering interactive elements, such as the zoom
|
||||
# rubberband
|
||||
canvas_div = document.createElement('div')
|
||||
canvas_div.setAttribute('style', 'position: relative')
|
||||
|
||||
|
@ -156,7 +131,10 @@ class FigureCanvasWasm(backend_agg.FigureCanvasAgg):
|
|||
'width: {}px; height: {}px'.format(
|
||||
width / self._ratio, height / self._ratio)
|
||||
)
|
||||
# Canvas must have a "tabindex" attr in order to receive keyboard events
|
||||
rubberband.setAttribute('tabindex', '0')
|
||||
# Event handlers are added to the canvas "on top", even though most of the
|
||||
# activity happens in the canvas below.
|
||||
rubberband.addEventListener('click', self.onclick)
|
||||
rubberband.addEventListener('mousemove', self.onmousemove)
|
||||
rubberband.addEventListener('mouseup', self.onmouseup)
|
||||
|
@ -172,6 +150,7 @@ class FigureCanvasWasm(backend_agg.FigureCanvasAgg):
|
|||
|
||||
div.appendChild(canvas_div)
|
||||
|
||||
# The bottom bar, with toolbar and message display
|
||||
bottom = document.createElement('div')
|
||||
toolbar = self.toolbar.get_element()
|
||||
bottom.appendChild(toolbar)
|
||||
|
@ -184,7 +163,9 @@ class FigureCanvasWasm(backend_agg.FigureCanvasAgg):
|
|||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
# Render the figure using Agg
|
||||
super().draw()
|
||||
# Copy the image buffer to the canvas
|
||||
width, height = self.get_width_height()
|
||||
canvas = self.get_element('canvas')
|
||||
image_data = ImageData.new(
|
||||
|
@ -209,6 +190,7 @@ class FigureCanvasWasm(backend_agg.FigureCanvasAgg):
|
|||
x = (event.offsetX * self._ratio)
|
||||
y = ((height / self._ratio) - event.offsetY) * self._ratio
|
||||
button = event.button + 1
|
||||
# Disable the right-click context menu in some browsers
|
||||
if button == 3:
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
@ -234,10 +216,12 @@ class FigureCanvasWasm(backend_agg.FigureCanvasAgg):
|
|||
|
||||
def onmouseenter(self, event):
|
||||
window.addEventListener('contextmenu', ignore)
|
||||
# When the mouse is over the figure, get keyboard focus
|
||||
self.get_element('rubberband').focus()
|
||||
self.enter_notify_event(guiEvent=event)
|
||||
|
||||
def onmouseleave(self, event):
|
||||
# When the mouse leaves the figure, drop keyboard focus
|
||||
self.get_element('rubberband').blur()
|
||||
self.leave_notify_event(guiEvent=event)
|
||||
|
||||
|
@ -255,12 +239,75 @@ class FigureCanvasWasm(backend_agg.FigureCanvasAgg):
|
|||
def set_cursor(self, cursor):
|
||||
self.get_element('rubberband').style.cursor = self._cursor_map.get(cursor, 0)
|
||||
|
||||
# http://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes
|
||||
_SHIFT_LUT = {
|
||||
59: ':',
|
||||
61: '+',
|
||||
173: '_',
|
||||
186: ':',
|
||||
187: '+',
|
||||
188: '<',
|
||||
189: '_',
|
||||
190: '>',
|
||||
191: '?',
|
||||
192: '~',
|
||||
219: '{',
|
||||
220: '|',
|
||||
221: '}',
|
||||
222: '"'
|
||||
}
|
||||
|
||||
_LUT = {
|
||||
8: 'backspace',
|
||||
9: 'tab',
|
||||
13: 'enter',
|
||||
16: 'shift',
|
||||
17: 'control',
|
||||
18: 'alt',
|
||||
19: 'pause',
|
||||
20: 'caps',
|
||||
27: 'escape',
|
||||
32: ' ',
|
||||
33: 'pageup',
|
||||
34: 'pagedown',
|
||||
35: 'end',
|
||||
36: 'home',
|
||||
37: 'left',
|
||||
38: 'up',
|
||||
39: 'right',
|
||||
40: 'down',
|
||||
45: 'insert',
|
||||
46: 'delete',
|
||||
91: 'super',
|
||||
92: 'super',
|
||||
93: 'select',
|
||||
106: '*',
|
||||
107: '+',
|
||||
109: '-',
|
||||
110: '.',
|
||||
111: '/',
|
||||
144: 'num_lock',
|
||||
145: 'scroll_lock',
|
||||
186: ':',
|
||||
187: '=',
|
||||
188: ',',
|
||||
189: '-',
|
||||
190: '.',
|
||||
191: '/',
|
||||
192: '`',
|
||||
219: '[',
|
||||
220: '\\',
|
||||
221: ']',
|
||||
222: "'"
|
||||
}
|
||||
|
||||
def _convert_key_event(self, event):
|
||||
code = int(event.which)
|
||||
value = chr(code)
|
||||
shift = event.shiftKey and code != 16
|
||||
ctrl = event.ctrlKey and code != 17
|
||||
alt = event.altKey and code != 18
|
||||
|
||||
# letter keys
|
||||
if 65 <= code <= 90:
|
||||
if not shift:
|
||||
|
@ -279,11 +326,11 @@ class FigureCanvasWasm(backend_agg.FigureCanvasAgg):
|
|||
elif 96 <= code <= 105:
|
||||
value = '%s' % (code - 96)
|
||||
# keys with shift alternatives
|
||||
elif code in _SHIFT_LUT and shift:
|
||||
value = _SHIFT_LUT[code]
|
||||
elif code in self._SHIFT_LUT and shift:
|
||||
value = self._SHIFT_LUT[code]
|
||||
shift = False
|
||||
elif code in _LUT:
|
||||
value = _LUT[code]
|
||||
elif code in self._LUT:
|
||||
value = self._LUT[code]
|
||||
|
||||
key = []
|
||||
if shift:
|
||||
|
@ -371,6 +418,7 @@ class NavigationToolbar2Wasm(backend_bases.NavigationToolbar2):
|
|||
pass
|
||||
|
||||
def get_element(self):
|
||||
# Creat the HTML content for the toolbar
|
||||
div = document.createElement('span')
|
||||
|
||||
def add_spacer():
|
||||
|
@ -405,6 +453,9 @@ class NavigationToolbar2Wasm(backend_bases.NavigationToolbar2):
|
|||
self.download(format, FILE_TYPES[format])
|
||||
|
||||
def download(self, format, mimetype):
|
||||
# Creates a temporary `a` element with a URL containing the image
|
||||
# content, and then virtually clicks it. Kind of magical, but it
|
||||
# works...
|
||||
element = document.createElement('a')
|
||||
data = io.BytesIO()
|
||||
try:
|
||||
|
|
Loading…
Reference in New Issue