/* Put everything inside the global mpl namespace */ /* global mpl */ window.mpl = {}; mpl.get_websocket_type = function () { if (typeof WebSocket !== 'undefined') { return WebSocket; } else if (typeof MozWebSocket !== 'undefined') { return MozWebSocket; } else { alert( 'Your browser does not have WebSocket support. ' + 'Please try Chrome, Safari or Firefox ≥ 6. ' + 'Firefox 4 and 5 are also supported but you ' + 'have to enable WebSockets in about:config.' ); } }; mpl.figure = function (figure_id, websocket, ondownload, parent_element) { this.id = figure_id; this.ws = websocket; this.supports_binary = this.ws.binaryType !== undefined; if (!this.supports_binary) { var warnings = document.getElementById('mpl-warnings'); if (warnings) { warnings.style.display = 'block'; warnings.textContent = 'This browser does not support binary websocket messages. ' + 'Performance may be slow.'; } } this.imageObj = new Image(); this.context = undefined; this.message = undefined; this.canvas = undefined; this.rubberband_canvas = undefined; this.rubberband_context = undefined; this.format_dropdown = undefined; this.image_mode = 'full'; this.root = document.createElement('div'); this.root.setAttribute('style', 'display: inline-block'); this._root_extra_style(this.root); parent_element.appendChild(this.root); this._init_header(this); this._init_canvas(this); this._init_toolbar(this); var fig = this; this.waiting = false; this.ws.onopen = function () { fig.send_message('supports_binary', { value: fig.supports_binary }); fig.send_message('send_image_mode', {}); if (fig.ratio !== 1) { fig.send_message('set_dpi_ratio', { dpi_ratio: fig.ratio }); } fig.send_message('refresh', {}); }; this.imageObj.onload = function () { if (fig.image_mode === 'full') { // Full images could contain transparency (where diff images // almost always do), so we need to clear the canvas so that // there is no ghosting. fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height); } fig.context.drawImage(fig.imageObj, 0, 0); }; this.imageObj.onunload = function () { fig.ws.close(); }; this.ws.onmessage = this._make_on_message_function(this); this.ondownload = ondownload; }; mpl.figure.prototype._init_header = function () { var titlebar = document.createElement('div'); titlebar.classList = 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix'; var titletext = document.createElement('div'); titletext.classList = 'ui-dialog-title'; titletext.setAttribute( 'style', 'width: 100%; text-align: center; padding: 3px;' ); titlebar.appendChild(titletext); this.root.appendChild(titlebar); this.header = titletext; }; mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {}; mpl.figure.prototype._root_extra_style = function (_canvas_div) {}; mpl.figure.prototype._init_canvas = function () { var fig = this; var canvas_div = (this.canvas_div = document.createElement('div')); canvas_div.setAttribute( 'style', 'border: 1px solid #ddd;' + 'box-sizing: content-box;' + 'clear: both;' + 'min-height: 1px;' + 'min-width: 1px;' + 'outline: 0;' + 'overflow: hidden;' + 'position: relative;' + 'resize: both;' ); function on_keyboard_event_closure(name) { return function (event) { return fig.key_event(event, name); }; } canvas_div.addEventListener( 'keydown', on_keyboard_event_closure('key_press') ); canvas_div.addEventListener( 'keyup', on_keyboard_event_closure('key_release') ); this._canvas_extra_style(canvas_div); this.root.appendChild(canvas_div); var canvas = (this.canvas = document.createElement('canvas')); canvas.classList.add('mpl-canvas'); canvas.setAttribute('style', 'box-sizing: content-box;'); this.context = canvas.getContext('2d'); var backingStore = this.context.backingStorePixelRatio || this.context.webkitBackingStorePixelRatio || this.context.mozBackingStorePixelRatio || this.context.msBackingStorePixelRatio || this.context.oBackingStorePixelRatio || this.context.backingStorePixelRatio || 1; this.ratio = (window.devicePixelRatio || 1) / backingStore; if (this.ratio !== 1) { fig.send_message('set_dpi_ratio', { dpi_ratio: this.ratio }); } var rubberband_canvas = (this.rubberband_canvas = document.createElement( 'canvas' )); rubberband_canvas.setAttribute( 'style', 'box-sizing: content-box; position: absolute; left: 0; top: 0; z-index: 1;' ); var resizeObserver = new ResizeObserver(function (entries) { var nentries = entries.length; for (var i = 0; i < nentries; i++) { var entry = entries[i]; var width, height; if (entry.contentBoxSize) { if (entry.contentBoxSize instanceof Array) { // Chrome 84 implements new version of spec. width = entry.contentBoxSize[0].inlineSize; height = entry.contentBoxSize[0].blockSize; } else { // Firefox implements old version of spec. width = entry.contentBoxSize.inlineSize; height = entry.contentBoxSize.blockSize; } } else { // Chrome <84 implements even older version of spec. width = entry.contentRect.width; height = entry.contentRect.height; } // Keep the size of the canvas and rubber band canvas in sync with // the canvas container. if (entry.devicePixelContentBoxSize) { // Chrome 84 implements new version of spec. canvas.setAttribute( 'width', entry.devicePixelContentBoxSize[0].inlineSize ); canvas.setAttribute( 'height', entry.devicePixelContentBoxSize[0].blockSize ); } else { canvas.setAttribute('width', width * fig.ratio); canvas.setAttribute('height', height * fig.ratio); } canvas.setAttribute( 'style', 'width: ' + width + 'px; height: ' + height + 'px;' ); rubberband_canvas.setAttribute('width', width); rubberband_canvas.setAttribute('height', height); // And update the size in Python. We ignore the initial 0/0 size // that occurs as the element is placed into the DOM, which should // otherwise not happen due to the minimum size styling. if (width != 0 && height != 0) { fig.request_resize(width, height); } } }); resizeObserver.observe(canvas_div); function on_mouse_event_closure(name) { return function (event) { return fig.mouse_event(event, name); }; } rubberband_canvas.addEventListener( 'mousedown', on_mouse_event_closure('button_press') ); rubberband_canvas.addEventListener( 'mouseup', on_mouse_event_closure('button_release') ); // Throttle sequential mouse events to 1 every 20ms. rubberband_canvas.addEventListener( 'mousemove', on_mouse_event_closure('motion_notify') ); rubberband_canvas.addEventListener( 'mouseenter', on_mouse_event_closure('figure_enter') ); rubberband_canvas.addEventListener( 'mouseleave', on_mouse_event_closure('figure_leave') ); canvas_div.addEventListener('wheel', function (event) { if (event.deltaY < 0) { event.step = 1; } else { event.step = -1; } on_mouse_event_closure('scroll')(event); }); canvas_div.appendChild(canvas); canvas_div.appendChild(rubberband_canvas); this.rubberband_context = rubberband_canvas.getContext('2d'); this.rubberband_context.strokeStyle = '#000000'; this._resize_canvas = function (width, height, forward) { if (forward) { canvas_div.style.width = width + 'px'; canvas_div.style.height = height + 'px'; } }; // Disable right mouse context menu. this.rubberband_canvas.addEventListener('contextmenu', function (_e) { event.preventDefault(); return false; }); function set_focus() { canvas.focus(); canvas_div.focus(); } window.setTimeout(set_focus, 100); }; mpl.figure.prototype._init_toolbar = function () { var fig = this; var toolbar = document.createElement('div'); toolbar.classList = 'mpl-toolbar'; this.root.appendChild(toolbar); function on_click_closure(name) { return function (_event) { return fig.toolbar_button_onclick(name); }; } function on_mouseover_closure(tooltip) { return function (event) { if (!event.currentTarget.disabled) { return fig.toolbar_button_onmouseover(tooltip); } }; } fig.buttons = {}; var buttonGroup = document.createElement('div'); buttonGroup.classList = 'mpl-button-group'; for (var toolbar_ind in mpl.toolbar_items) { var name = mpl.toolbar_items[toolbar_ind][0]; var tooltip = mpl.toolbar_items[toolbar_ind][1]; var image = mpl.toolbar_items[toolbar_ind][2]; var method_name = mpl.toolbar_items[toolbar_ind][3]; if (!name) { /* Instead of a spacer, we start a new button group. */ if (buttonGroup.hasChildNodes()) { toolbar.appendChild(buttonGroup); } buttonGroup = document.createElement('div'); buttonGroup.classList = 'mpl-button-group'; continue; } var button = (fig.buttons[name] = document.createElement('button')); button.classList = 'mpl-widget'; button.setAttribute('role', 'button'); button.setAttribute('aria-disabled', 'false'); button.addEventListener('click', on_click_closure(method_name)); button.addEventListener('mouseover', on_mouseover_closure(tooltip)); var icon_img = document.createElement('img'); icon_img.src = '_images/' + image + '.png'; icon_img.srcset = '_images/' + image + '_large.png 2x'; icon_img.alt = tooltip; button.appendChild(icon_img); buttonGroup.appendChild(button); } if (buttonGroup.hasChildNodes()) { toolbar.appendChild(buttonGroup); } var fmt_picker = document.createElement('select'); fmt_picker.classList = 'mpl-widget'; toolbar.appendChild(fmt_picker); this.format_dropdown = fmt_picker; for (var ind in mpl.extensions) { var fmt = mpl.extensions[ind]; var option = document.createElement('option'); option.selected = fmt === mpl.default_extension; option.innerHTML = fmt; fmt_picker.appendChild(option); } var status_bar = document.createElement('span'); status_bar.classList = 'mpl-message'; toolbar.appendChild(status_bar); this.message = status_bar; }; mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) { // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client, // which will in turn request a refresh of the image. this.send_message('resize', { width: x_pixels, height: y_pixels }); }; mpl.figure.prototype.send_message = function (type, properties) { properties['type'] = type; properties['figure_id'] = this.id; this.ws.send(JSON.stringify(properties)); }; mpl.figure.prototype.send_draw_message = function () { if (!this.waiting) { this.waiting = true; this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id })); } }; mpl.figure.prototype.handle_save = function (fig, _msg) { var format_dropdown = fig.format_dropdown; var format = format_dropdown.options[format_dropdown.selectedIndex].value; fig.ondownload(fig, format); }; mpl.figure.prototype.handle_resize = function (fig, msg) { var size = msg['size']; if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) { fig._resize_canvas(size[0], size[1], msg['forward']); fig.send_message('refresh', {}); } }; mpl.figure.prototype.handle_rubberband = function (fig, msg) { var x0 = msg['x0'] / fig.ratio; var y0 = (fig.canvas.height - msg['y0']) / fig.ratio; var x1 = msg['x1'] / fig.ratio; var y1 = (fig.canvas.height - msg['y1']) / fig.ratio; x0 = Math.floor(x0) + 0.5; y0 = Math.floor(y0) + 0.5; x1 = Math.floor(x1) + 0.5; y1 = Math.floor(y1) + 0.5; var min_x = Math.min(x0, x1); var min_y = Math.min(y0, y1); var width = Math.abs(x1 - x0); var height = Math.abs(y1 - y0); fig.rubberband_context.clearRect( 0, 0, fig.canvas.width / fig.ratio, fig.canvas.height / fig.ratio ); fig.rubberband_context.strokeRect(min_x, min_y, width, height); }; mpl.figure.prototype.handle_figure_label = function (fig, msg) { // Updates the figure title. fig.header.textContent = msg['label']; }; mpl.figure.prototype.handle_cursor = function (fig, msg) { var cursor = msg['cursor']; switch (cursor) { case 0: cursor = 'pointer'; break; case 1: cursor = 'default'; break; case 2: cursor = 'crosshair'; break; case 3: cursor = 'move'; break; } fig.rubberband_canvas.style.cursor = cursor; }; mpl.figure.prototype.handle_message = function (fig, msg) { fig.message.textContent = msg['message']; }; mpl.figure.prototype.handle_draw = function (fig, _msg) { // Request the server to send over a new figure. fig.send_draw_message(); }; mpl.figure.prototype.handle_image_mode = function (fig, msg) { fig.image_mode = msg['mode']; }; mpl.figure.prototype.handle_history_buttons = function (fig, msg) { for (var key in msg) { if (!(key in fig.buttons)) { continue; } fig.buttons[key].disabled = !msg[key]; fig.buttons[key].setAttribute('aria-disabled', !msg[key]); } }; mpl.figure.prototype.handle_navigate_mode = function (fig, msg) { if (msg['mode'] === 'PAN') { fig.buttons['Pan'].classList.add('active'); fig.buttons['Zoom'].classList.remove('active'); } else if (msg['mode'] === 'ZOOM') { fig.buttons['Pan'].classList.remove('active'); fig.buttons['Zoom'].classList.add('active'); } else { fig.buttons['Pan'].classList.remove('active'); fig.buttons['Zoom'].classList.remove('active'); } }; mpl.figure.prototype.updated_canvas_event = function () { // Called whenever the canvas gets updated. this.send_message('ack', {}); }; // A function to construct a web socket function for onmessage handling. // Called in the figure constructor. mpl.figure.prototype._make_on_message_function = function (fig) { return function socket_on_message(evt) { if (evt.data instanceof Blob) { /* FIXME: We get "Resource interpreted as Image but * transferred with MIME type text/plain:" errors on * Chrome. But how to set the MIME type? It doesn't seem * to be part of the websocket stream */ evt.data.type = 'image/png'; /* Free the memory for the previous frames */ if (fig.imageObj.src) { (window.URL || window.webkitURL).revokeObjectURL( fig.imageObj.src ); } fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL( evt.data ); fig.updated_canvas_event(); fig.waiting = false; return; } else if ( typeof evt.data === 'string' && evt.data.slice(0, 21) === 'data:image/png;base64' ) { fig.imageObj.src = evt.data; fig.updated_canvas_event(); fig.waiting = false; return; } var msg = JSON.parse(evt.data); var msg_type = msg['type']; // Call the "handle_{type}" callback, which takes // the figure and JSON message as its only arguments. try { var callback = fig['handle_' + msg_type]; } catch (e) { console.log( "No handler for the '" + msg_type + "' message type: ", msg ); return; } if (callback) { try { // console.log("Handling '" + msg_type + "' message: ", msg); callback(fig, msg); } catch (e) { console.log( "Exception inside the 'handler_" + msg_type + "' callback:", e, e.stack, msg ); } } }; }; // from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas mpl.findpos = function (e) { //this section is from http://www.quirksmode.org/js/events_properties.html var targ; if (!e) { e = window.event; } if (e.target) { targ = e.target; } else if (e.srcElement) { targ = e.srcElement; } if (targ.nodeType === 3) { // defeat Safari bug targ = targ.parentNode; } // pageX,Y are the mouse positions relative to the document var boundingRect = targ.getBoundingClientRect(); var x = e.pageX - (boundingRect.left + document.body.scrollLeft); var y = e.pageY - (boundingRect.top + document.body.scrollTop); return { x: x, y: y }; }; /* * return a copy of an object with only non-object keys * we need this to avoid circular references * http://stackoverflow.com/a/24161582/3208463 */ function simpleKeys(original) { return Object.keys(original).reduce(function (obj, key) { if (typeof original[key] !== 'object') { obj[key] = original[key]; } return obj; }, {}); } mpl.figure.prototype.mouse_event = function (event, name) { var canvas_pos = mpl.findpos(event); if (name === 'button_press') { this.canvas.focus(); this.canvas_div.focus(); } var x = canvas_pos.x * this.ratio; var y = canvas_pos.y * this.ratio; this.send_message(name, { x: x, y: y, button: event.button, step: event.step, guiEvent: simpleKeys(event), }); /* This prevents the web browser from automatically changing to * the text insertion cursor when the button is pressed. We want * to control all of the cursor setting manually through the * 'cursor' event from matplotlib */ event.preventDefault(); return false; }; mpl.figure.prototype._key_event_extra = function (_event, _name) { // Handle any extra behaviour associated with a key event }; mpl.figure.prototype.key_event = function (event, name) { // Prevent repeat events if (name === 'key_press') { if (event.which === this._key) { return; } else { this._key = event.which; } } if (name === 'key_release') { this._key = null; } var value = ''; if (event.ctrlKey && event.which !== 17) { value += 'ctrl+'; } if (event.altKey && event.which !== 18) { value += 'alt+'; } if (event.shiftKey && event.which !== 16) { value += 'shift+'; } value += 'k'; value += event.which.toString(); this._key_event_extra(event, name); this.send_message(name, { key: value, guiEvent: simpleKeys(event) }); return false; }; mpl.figure.prototype.toolbar_button_onclick = function (name) { if (name === 'download') { this.handle_save(this, null); } else { this.send_message('toolbar_button', { name: name }); } }; mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) { this.message.textContent = tooltip; };