Last active
October 17, 2024 14:18
-
-
Save kif/ace15da5acfa495efaf3d37feb701a17 to your computer and use it in GitHub Desktop.
Comparison of (pseudo-) random number generators
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"cells": [ | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"# Optimisation of (pseudo-) random number generation\n", | |
"\n", | |
"This story started with 5 lines of `numpy` to generate images made of pixels having each their own mean and standard deviation. Images are composed of several mega-pixels and there are thousands of them. The 5 lines of Python are providing a proper result but with its 10 frames per seconds, it is far too slow, and the client expects at least 100 to process the image-stack reasonably fast.\n", | |
"\n", | |
"Since this is intended to be production code, hence widely distributed, my favorite tool, the GPU gets discarded. So I am stucked with Python and some tools already in use within the project: Cython and a C/C++ compiler. Since the code needs to run on any platform (windows, macos and linux to the least) and on many achitectures x86, amd64, ppc64le and arm64 among other. Other fancy tools like OpenMP are neither an option since it is not supported on macos.\n", | |
"\n", | |
"Naively, I started to rewrite this function in C, sure that this would be enough ... how disapointed was I when I realized it was even slower than the initial Python !\n", | |
"\n", | |
"This triggered some rationnal benchmarking on random number generation that I would like to share with you.\n", | |
"\n", | |
"First of all, all pseudo random numbers, like the one obtained from a normal distribution are derived from an uniform distribution, and one of the most commonly used generator is based on the Mesenne-Twister. While is not cryptographically strong, it is neverthless enough for our needs.\n", | |
"\n", | |
"*Disclaimer:* I am not a guru in C nor in C++, so I would be pleased if you can help me fixing some code if you believe it is sub-optimal.\n", | |
"\n", | |
"## Some reference `Numpy` code and figures" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 1, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"data": { | |
"text/plain": [ | |
"sys.version_info(major=3, minor=9, micro=2, releaselevel='final', serial=0)" | |
] | |
}, | |
"execution_count": 1, | |
"metadata": {}, | |
"output_type": "execute_result" | |
} | |
], | |
"source": [ | |
"%matplotlib nbagg\n", | |
"import sys, time, random\n", | |
"start_time = time.perf_counter()\n", | |
"sys.version_info" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 2, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"import numpy\n", | |
"from matplotlib.pyplot import subplots\n", | |
"\n", | |
"#This is the size of one image:\n", | |
"shape = (2167,2070)" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 3, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"Reference timing from numpy: Uniform\n", | |
"35.4 ms ± 234 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", | |
"Reference timing from numpy: Normal\n", | |
"110 ms ± 593 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" | |
] | |
}, | |
{ | |
"data": { | |
"application/javascript": [ | |
"/* Put everything inside the global mpl namespace */\n", | |
"/* global mpl */\n", | |
"window.mpl = {};\n", | |
"\n", | |
"mpl.get_websocket_type = function () {\n", | |
" if (typeof WebSocket !== 'undefined') {\n", | |
" return WebSocket;\n", | |
" } else if (typeof MozWebSocket !== 'undefined') {\n", | |
" return MozWebSocket;\n", | |
" } else {\n", | |
" alert(\n", | |
" 'Your browser does not have WebSocket support. ' +\n", | |
" 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", | |
" 'Firefox 4 and 5 are also supported but you ' +\n", | |
" 'have to enable WebSockets in about:config.'\n", | |
" );\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n", | |
" this.id = figure_id;\n", | |
"\n", | |
" this.ws = websocket;\n", | |
"\n", | |
" this.supports_binary = this.ws.binaryType !== undefined;\n", | |
"\n", | |
" if (!this.supports_binary) {\n", | |
" var warnings = document.getElementById('mpl-warnings');\n", | |
" if (warnings) {\n", | |
" warnings.style.display = 'block';\n", | |
" warnings.textContent =\n", | |
" 'This browser does not support binary websocket messages. ' +\n", | |
" 'Performance may be slow.';\n", | |
" }\n", | |
" }\n", | |
"\n", | |
" this.imageObj = new Image();\n", | |
"\n", | |
" this.context = undefined;\n", | |
" this.message = undefined;\n", | |
" this.canvas = undefined;\n", | |
" this.rubberband_canvas = undefined;\n", | |
" this.rubberband_context = undefined;\n", | |
" this.format_dropdown = undefined;\n", | |
"\n", | |
" this.image_mode = 'full';\n", | |
"\n", | |
" this.root = document.createElement('div');\n", | |
" this.root.setAttribute('style', 'display: inline-block');\n", | |
" this._root_extra_style(this.root);\n", | |
"\n", | |
" parent_element.appendChild(this.root);\n", | |
"\n", | |
" this._init_header(this);\n", | |
" this._init_canvas(this);\n", | |
" this._init_toolbar(this);\n", | |
"\n", | |
" var fig = this;\n", | |
"\n", | |
" this.waiting = false;\n", | |
"\n", | |
" this.ws.onopen = function () {\n", | |
" fig.send_message('supports_binary', { value: fig.supports_binary });\n", | |
" fig.send_message('send_image_mode', {});\n", | |
" if (fig.ratio !== 1) {\n", | |
" fig.send_message('set_dpi_ratio', { dpi_ratio: fig.ratio });\n", | |
" }\n", | |
" fig.send_message('refresh', {});\n", | |
" };\n", | |
"\n", | |
" this.imageObj.onload = function () {\n", | |
" if (fig.image_mode === 'full') {\n", | |
" // Full images could contain transparency (where diff images\n", | |
" // almost always do), so we need to clear the canvas so that\n", | |
" // there is no ghosting.\n", | |
" fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", | |
" }\n", | |
" fig.context.drawImage(fig.imageObj, 0, 0);\n", | |
" };\n", | |
"\n", | |
" this.imageObj.onunload = function () {\n", | |
" fig.ws.close();\n", | |
" };\n", | |
"\n", | |
" this.ws.onmessage = this._make_on_message_function(this);\n", | |
"\n", | |
" this.ondownload = ondownload;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_header = function () {\n", | |
" var titlebar = document.createElement('div');\n", | |
" titlebar.classList =\n", | |
" 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n", | |
" var titletext = document.createElement('div');\n", | |
" titletext.classList = 'ui-dialog-title';\n", | |
" titletext.setAttribute(\n", | |
" 'style',\n", | |
" 'width: 100%; text-align: center; padding: 3px;'\n", | |
" );\n", | |
" titlebar.appendChild(titletext);\n", | |
" this.root.appendChild(titlebar);\n", | |
" this.header = titletext;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n", | |
"\n", | |
"mpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n", | |
"\n", | |
"mpl.figure.prototype._init_canvas = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var canvas_div = (this.canvas_div = document.createElement('div'));\n", | |
" canvas_div.setAttribute(\n", | |
" 'style',\n", | |
" 'border: 1px solid #ddd;' +\n", | |
" 'box-sizing: content-box;' +\n", | |
" 'clear: both;' +\n", | |
" 'min-height: 1px;' +\n", | |
" 'min-width: 1px;' +\n", | |
" 'outline: 0;' +\n", | |
" 'overflow: hidden;' +\n", | |
" 'position: relative;' +\n", | |
" 'resize: both;'\n", | |
" );\n", | |
"\n", | |
" function on_keyboard_event_closure(name) {\n", | |
" return function (event) {\n", | |
" return fig.key_event(event, name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" canvas_div.addEventListener(\n", | |
" 'keydown',\n", | |
" on_keyboard_event_closure('key_press')\n", | |
" );\n", | |
" canvas_div.addEventListener(\n", | |
" 'keyup',\n", | |
" on_keyboard_event_closure('key_release')\n", | |
" );\n", | |
"\n", | |
" this._canvas_extra_style(canvas_div);\n", | |
" this.root.appendChild(canvas_div);\n", | |
"\n", | |
" var canvas = (this.canvas = document.createElement('canvas'));\n", | |
" canvas.classList.add('mpl-canvas');\n", | |
" canvas.setAttribute('style', 'box-sizing: content-box;');\n", | |
"\n", | |
" this.context = canvas.getContext('2d');\n", | |
"\n", | |
" var backingStore =\n", | |
" this.context.backingStorePixelRatio ||\n", | |
" this.context.webkitBackingStorePixelRatio ||\n", | |
" this.context.mozBackingStorePixelRatio ||\n", | |
" this.context.msBackingStorePixelRatio ||\n", | |
" this.context.oBackingStorePixelRatio ||\n", | |
" this.context.backingStorePixelRatio ||\n", | |
" 1;\n", | |
"\n", | |
" this.ratio = (window.devicePixelRatio || 1) / backingStore;\n", | |
"\n", | |
" var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n", | |
" 'canvas'\n", | |
" ));\n", | |
" rubberband_canvas.setAttribute(\n", | |
" 'style',\n", | |
" 'box-sizing: content-box; position: absolute; left: 0; top: 0; z-index: 1;'\n", | |
" );\n", | |
"\n", | |
" // Apply a ponyfill if ResizeObserver is not implemented by browser.\n", | |
" if (this.ResizeObserver === undefined) {\n", | |
" if (window.ResizeObserver !== undefined) {\n", | |
" this.ResizeObserver = window.ResizeObserver;\n", | |
" } else {\n", | |
" var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n", | |
" this.ResizeObserver = obs.ResizeObserver;\n", | |
" }\n", | |
" }\n", | |
"\n", | |
" this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n", | |
" var nentries = entries.length;\n", | |
" for (var i = 0; i < nentries; i++) {\n", | |
" var entry = entries[i];\n", | |
" var width, height;\n", | |
" if (entry.contentBoxSize) {\n", | |
" if (entry.contentBoxSize instanceof Array) {\n", | |
" // Chrome 84 implements new version of spec.\n", | |
" width = entry.contentBoxSize[0].inlineSize;\n", | |
" height = entry.contentBoxSize[0].blockSize;\n", | |
" } else {\n", | |
" // Firefox implements old version of spec.\n", | |
" width = entry.contentBoxSize.inlineSize;\n", | |
" height = entry.contentBoxSize.blockSize;\n", | |
" }\n", | |
" } else {\n", | |
" // Chrome <84 implements even older version of spec.\n", | |
" width = entry.contentRect.width;\n", | |
" height = entry.contentRect.height;\n", | |
" }\n", | |
"\n", | |
" // Keep the size of the canvas and rubber band canvas in sync with\n", | |
" // the canvas container.\n", | |
" if (entry.devicePixelContentBoxSize) {\n", | |
" // Chrome 84 implements new version of spec.\n", | |
" canvas.setAttribute(\n", | |
" 'width',\n", | |
" entry.devicePixelContentBoxSize[0].inlineSize\n", | |
" );\n", | |
" canvas.setAttribute(\n", | |
" 'height',\n", | |
" entry.devicePixelContentBoxSize[0].blockSize\n", | |
" );\n", | |
" } else {\n", | |
" canvas.setAttribute('width', width * fig.ratio);\n", | |
" canvas.setAttribute('height', height * fig.ratio);\n", | |
" }\n", | |
" canvas.setAttribute(\n", | |
" 'style',\n", | |
" 'width: ' + width + 'px; height: ' + height + 'px;'\n", | |
" );\n", | |
"\n", | |
" rubberband_canvas.setAttribute('width', width);\n", | |
" rubberband_canvas.setAttribute('height', height);\n", | |
"\n", | |
" // And update the size in Python. We ignore the initial 0/0 size\n", | |
" // that occurs as the element is placed into the DOM, which should\n", | |
" // otherwise not happen due to the minimum size styling.\n", | |
" if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n", | |
" fig.request_resize(width, height);\n", | |
" }\n", | |
" }\n", | |
" });\n", | |
" this.resizeObserverInstance.observe(canvas_div);\n", | |
"\n", | |
" function on_mouse_event_closure(name) {\n", | |
" return function (event) {\n", | |
" return fig.mouse_event(event, name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mousedown',\n", | |
" on_mouse_event_closure('button_press')\n", | |
" );\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseup',\n", | |
" on_mouse_event_closure('button_release')\n", | |
" );\n", | |
" // Throttle sequential mouse events to 1 every 20ms.\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mousemove',\n", | |
" on_mouse_event_closure('motion_notify')\n", | |
" );\n", | |
"\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseenter',\n", | |
" on_mouse_event_closure('figure_enter')\n", | |
" );\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseleave',\n", | |
" on_mouse_event_closure('figure_leave')\n", | |
" );\n", | |
"\n", | |
" canvas_div.addEventListener('wheel', function (event) {\n", | |
" if (event.deltaY < 0) {\n", | |
" event.step = 1;\n", | |
" } else {\n", | |
" event.step = -1;\n", | |
" }\n", | |
" on_mouse_event_closure('scroll')(event);\n", | |
" });\n", | |
"\n", | |
" canvas_div.appendChild(canvas);\n", | |
" canvas_div.appendChild(rubberband_canvas);\n", | |
"\n", | |
" this.rubberband_context = rubberband_canvas.getContext('2d');\n", | |
" this.rubberband_context.strokeStyle = '#000000';\n", | |
"\n", | |
" this._resize_canvas = function (width, height, forward) {\n", | |
" if (forward) {\n", | |
" canvas_div.style.width = width + 'px';\n", | |
" canvas_div.style.height = height + 'px';\n", | |
" }\n", | |
" };\n", | |
"\n", | |
" // Disable right mouse context menu.\n", | |
" this.rubberband_canvas.addEventListener('contextmenu', function (_e) {\n", | |
" event.preventDefault();\n", | |
" return false;\n", | |
" });\n", | |
"\n", | |
" function set_focus() {\n", | |
" canvas.focus();\n", | |
" canvas_div.focus();\n", | |
" }\n", | |
"\n", | |
" window.setTimeout(set_focus, 100);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_toolbar = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var toolbar = document.createElement('div');\n", | |
" toolbar.classList = 'mpl-toolbar';\n", | |
" this.root.appendChild(toolbar);\n", | |
"\n", | |
" function on_click_closure(name) {\n", | |
" return function (_event) {\n", | |
" return fig.toolbar_button_onclick(name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" function on_mouseover_closure(tooltip) {\n", | |
" return function (event) {\n", | |
" if (!event.currentTarget.disabled) {\n", | |
" return fig.toolbar_button_onmouseover(tooltip);\n", | |
" }\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" fig.buttons = {};\n", | |
" var buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'mpl-button-group';\n", | |
" for (var toolbar_ind in mpl.toolbar_items) {\n", | |
" var name = mpl.toolbar_items[toolbar_ind][0];\n", | |
" var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", | |
" var image = mpl.toolbar_items[toolbar_ind][2];\n", | |
" var method_name = mpl.toolbar_items[toolbar_ind][3];\n", | |
"\n", | |
" if (!name) {\n", | |
" /* Instead of a spacer, we start a new button group. */\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
" buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'mpl-button-group';\n", | |
" continue;\n", | |
" }\n", | |
"\n", | |
" var button = (fig.buttons[name] = document.createElement('button'));\n", | |
" button.classList = 'mpl-widget';\n", | |
" button.setAttribute('role', 'button');\n", | |
" button.setAttribute('aria-disabled', 'false');\n", | |
" button.addEventListener('click', on_click_closure(method_name));\n", | |
" button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", | |
"\n", | |
" var icon_img = document.createElement('img');\n", | |
" icon_img.src = '_images/' + image + '.png';\n", | |
" icon_img.srcset = '_images/' + image + '_large.png 2x';\n", | |
" icon_img.alt = tooltip;\n", | |
" button.appendChild(icon_img);\n", | |
"\n", | |
" buttonGroup.appendChild(button);\n", | |
" }\n", | |
"\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
"\n", | |
" var fmt_picker = document.createElement('select');\n", | |
" fmt_picker.classList = 'mpl-widget';\n", | |
" toolbar.appendChild(fmt_picker);\n", | |
" this.format_dropdown = fmt_picker;\n", | |
"\n", | |
" for (var ind in mpl.extensions) {\n", | |
" var fmt = mpl.extensions[ind];\n", | |
" var option = document.createElement('option');\n", | |
" option.selected = fmt === mpl.default_extension;\n", | |
" option.innerHTML = fmt;\n", | |
" fmt_picker.appendChild(option);\n", | |
" }\n", | |
"\n", | |
" var status_bar = document.createElement('span');\n", | |
" status_bar.classList = 'mpl-message';\n", | |
" toolbar.appendChild(status_bar);\n", | |
" this.message = status_bar;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n", | |
" // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n", | |
" // which will in turn request a refresh of the image.\n", | |
" this.send_message('resize', { width: x_pixels, height: y_pixels });\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.send_message = function (type, properties) {\n", | |
" properties['type'] = type;\n", | |
" properties['figure_id'] = this.id;\n", | |
" this.ws.send(JSON.stringify(properties));\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.send_draw_message = function () {\n", | |
" if (!this.waiting) {\n", | |
" this.waiting = true;\n", | |
" this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_save = function (fig, _msg) {\n", | |
" var format_dropdown = fig.format_dropdown;\n", | |
" var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n", | |
" fig.ondownload(fig, format);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_resize = function (fig, msg) {\n", | |
" var size = msg['size'];\n", | |
" if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n", | |
" fig._resize_canvas(size[0], size[1], msg['forward']);\n", | |
" fig.send_message('refresh', {});\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_rubberband = function (fig, msg) {\n", | |
" var x0 = msg['x0'] / fig.ratio;\n", | |
" var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n", | |
" var x1 = msg['x1'] / fig.ratio;\n", | |
" var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n", | |
" x0 = Math.floor(x0) + 0.5;\n", | |
" y0 = Math.floor(y0) + 0.5;\n", | |
" x1 = Math.floor(x1) + 0.5;\n", | |
" y1 = Math.floor(y1) + 0.5;\n", | |
" var min_x = Math.min(x0, x1);\n", | |
" var min_y = Math.min(y0, y1);\n", | |
" var width = Math.abs(x1 - x0);\n", | |
" var height = Math.abs(y1 - y0);\n", | |
"\n", | |
" fig.rubberband_context.clearRect(\n", | |
" 0,\n", | |
" 0,\n", | |
" fig.canvas.width / fig.ratio,\n", | |
" fig.canvas.height / fig.ratio\n", | |
" );\n", | |
"\n", | |
" fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_figure_label = function (fig, msg) {\n", | |
" // Updates the figure title.\n", | |
" fig.header.textContent = msg['label'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_cursor = function (fig, msg) {\n", | |
" var cursor = msg['cursor'];\n", | |
" switch (cursor) {\n", | |
" case 0:\n", | |
" cursor = 'pointer';\n", | |
" break;\n", | |
" case 1:\n", | |
" cursor = 'default';\n", | |
" break;\n", | |
" case 2:\n", | |
" cursor = 'crosshair';\n", | |
" break;\n", | |
" case 3:\n", | |
" cursor = 'move';\n", | |
" break;\n", | |
" }\n", | |
" fig.rubberband_canvas.style.cursor = cursor;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_message = function (fig, msg) {\n", | |
" fig.message.textContent = msg['message'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_draw = function (fig, _msg) {\n", | |
" // Request the server to send over a new figure.\n", | |
" fig.send_draw_message();\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_image_mode = function (fig, msg) {\n", | |
" fig.image_mode = msg['mode'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n", | |
" for (var key in msg) {\n", | |
" if (!(key in fig.buttons)) {\n", | |
" continue;\n", | |
" }\n", | |
" fig.buttons[key].disabled = !msg[key];\n", | |
" fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n", | |
" if (msg['mode'] === 'PAN') {\n", | |
" fig.buttons['Pan'].classList.add('active');\n", | |
" fig.buttons['Zoom'].classList.remove('active');\n", | |
" } else if (msg['mode'] === 'ZOOM') {\n", | |
" fig.buttons['Pan'].classList.remove('active');\n", | |
" fig.buttons['Zoom'].classList.add('active');\n", | |
" } else {\n", | |
" fig.buttons['Pan'].classList.remove('active');\n", | |
" fig.buttons['Zoom'].classList.remove('active');\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.updated_canvas_event = function () {\n", | |
" // Called whenever the canvas gets updated.\n", | |
" this.send_message('ack', {});\n", | |
"};\n", | |
"\n", | |
"// A function to construct a web socket function for onmessage handling.\n", | |
"// Called in the figure constructor.\n", | |
"mpl.figure.prototype._make_on_message_function = function (fig) {\n", | |
" return function socket_on_message(evt) {\n", | |
" if (evt.data instanceof Blob) {\n", | |
" /* FIXME: We get \"Resource interpreted as Image but\n", | |
" * transferred with MIME type text/plain:\" errors on\n", | |
" * Chrome. But how to set the MIME type? It doesn't seem\n", | |
" * to be part of the websocket stream */\n", | |
" evt.data.type = 'image/png';\n", | |
"\n", | |
" /* Free the memory for the previous frames */\n", | |
" if (fig.imageObj.src) {\n", | |
" (window.URL || window.webkitURL).revokeObjectURL(\n", | |
" fig.imageObj.src\n", | |
" );\n", | |
" }\n", | |
"\n", | |
" fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n", | |
" evt.data\n", | |
" );\n", | |
" fig.updated_canvas_event();\n", | |
" fig.waiting = false;\n", | |
" return;\n", | |
" } else if (\n", | |
" typeof evt.data === 'string' &&\n", | |
" evt.data.slice(0, 21) === 'data:image/png;base64'\n", | |
" ) {\n", | |
" fig.imageObj.src = evt.data;\n", | |
" fig.updated_canvas_event();\n", | |
" fig.waiting = false;\n", | |
" return;\n", | |
" }\n", | |
"\n", | |
" var msg = JSON.parse(evt.data);\n", | |
" var msg_type = msg['type'];\n", | |
"\n", | |
" // Call the \"handle_{type}\" callback, which takes\n", | |
" // the figure and JSON message as its only arguments.\n", | |
" try {\n", | |
" var callback = fig['handle_' + msg_type];\n", | |
" } catch (e) {\n", | |
" console.log(\n", | |
" \"No handler for the '\" + msg_type + \"' message type: \",\n", | |
" msg\n", | |
" );\n", | |
" return;\n", | |
" }\n", | |
"\n", | |
" if (callback) {\n", | |
" try {\n", | |
" // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n", | |
" callback(fig, msg);\n", | |
" } catch (e) {\n", | |
" console.log(\n", | |
" \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n", | |
" e,\n", | |
" e.stack,\n", | |
" msg\n", | |
" );\n", | |
" }\n", | |
" }\n", | |
" };\n", | |
"};\n", | |
"\n", | |
"// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n", | |
"mpl.findpos = function (e) {\n", | |
" //this section is from http://www.quirksmode.org/js/events_properties.html\n", | |
" var targ;\n", | |
" if (!e) {\n", | |
" e = window.event;\n", | |
" }\n", | |
" if (e.target) {\n", | |
" targ = e.target;\n", | |
" } else if (e.srcElement) {\n", | |
" targ = e.srcElement;\n", | |
" }\n", | |
" if (targ.nodeType === 3) {\n", | |
" // defeat Safari bug\n", | |
" targ = targ.parentNode;\n", | |
" }\n", | |
"\n", | |
" // pageX,Y are the mouse positions relative to the document\n", | |
" var boundingRect = targ.getBoundingClientRect();\n", | |
" var x = e.pageX - (boundingRect.left + document.body.scrollLeft);\n", | |
" var y = e.pageY - (boundingRect.top + document.body.scrollTop);\n", | |
"\n", | |
" return { x: x, y: y };\n", | |
"};\n", | |
"\n", | |
"/*\n", | |
" * return a copy of an object with only non-object keys\n", | |
" * we need this to avoid circular references\n", | |
" * http://stackoverflow.com/a/24161582/3208463\n", | |
" */\n", | |
"function simpleKeys(original) {\n", | |
" return Object.keys(original).reduce(function (obj, key) {\n", | |
" if (typeof original[key] !== 'object') {\n", | |
" obj[key] = original[key];\n", | |
" }\n", | |
" return obj;\n", | |
" }, {});\n", | |
"}\n", | |
"\n", | |
"mpl.figure.prototype.mouse_event = function (event, name) {\n", | |
" var canvas_pos = mpl.findpos(event);\n", | |
"\n", | |
" if (name === 'button_press') {\n", | |
" this.canvas.focus();\n", | |
" this.canvas_div.focus();\n", | |
" }\n", | |
"\n", | |
" var x = canvas_pos.x * this.ratio;\n", | |
" var y = canvas_pos.y * this.ratio;\n", | |
"\n", | |
" this.send_message(name, {\n", | |
" x: x,\n", | |
" y: y,\n", | |
" button: event.button,\n", | |
" step: event.step,\n", | |
" guiEvent: simpleKeys(event),\n", | |
" });\n", | |
"\n", | |
" /* This prevents the web browser from automatically changing to\n", | |
" * the text insertion cursor when the button is pressed. We want\n", | |
" * to control all of the cursor setting manually through the\n", | |
" * 'cursor' event from matplotlib */\n", | |
" event.preventDefault();\n", | |
" return false;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._key_event_extra = function (_event, _name) {\n", | |
" // Handle any extra behaviour associated with a key event\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.key_event = function (event, name) {\n", | |
" // Prevent repeat events\n", | |
" if (name === 'key_press') {\n", | |
" if (event.which === this._key) {\n", | |
" return;\n", | |
" } else {\n", | |
" this._key = event.which;\n", | |
" }\n", | |
" }\n", | |
" if (name === 'key_release') {\n", | |
" this._key = null;\n", | |
" }\n", | |
"\n", | |
" var value = '';\n", | |
" if (event.ctrlKey && event.which !== 17) {\n", | |
" value += 'ctrl+';\n", | |
" }\n", | |
" if (event.altKey && event.which !== 18) {\n", | |
" value += 'alt+';\n", | |
" }\n", | |
" if (event.shiftKey && event.which !== 16) {\n", | |
" value += 'shift+';\n", | |
" }\n", | |
"\n", | |
" value += 'k';\n", | |
" value += event.which.toString();\n", | |
"\n", | |
" this._key_event_extra(event, name);\n", | |
"\n", | |
" this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n", | |
" return false;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.toolbar_button_onclick = function (name) {\n", | |
" if (name === 'download') {\n", | |
" this.handle_save(this, null);\n", | |
" } else {\n", | |
" this.send_message('toolbar_button', { name: name });\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n", | |
" this.message.textContent = tooltip;\n", | |
"};\n", | |
"\n", | |
"///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n", | |
"// prettier-ignore\n", | |
"var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\n", | |
"mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", | |
"\n", | |
"mpl.extensions = [\"eps\", \"jpeg\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n", | |
"\n", | |
"mpl.default_extension = \"png\";/* global mpl */\n", | |
"\n", | |
"var comm_websocket_adapter = function (comm) {\n", | |
" // Create a \"websocket\"-like object which calls the given IPython comm\n", | |
" // object with the appropriate methods. Currently this is a non binary\n", | |
" // socket, so there is still some room for performance tuning.\n", | |
" var ws = {};\n", | |
"\n", | |
" ws.close = function () {\n", | |
" comm.close();\n", | |
" };\n", | |
" ws.send = function (m) {\n", | |
" //console.log('sending', m);\n", | |
" comm.send(m);\n", | |
" };\n", | |
" // Register the callback with on_msg.\n", | |
" comm.on_msg(function (msg) {\n", | |
" //console.log('receiving', msg['content']['data'], msg);\n", | |
" // Pass the mpl event to the overridden (by mpl) onmessage function.\n", | |
" ws.onmessage(msg['content']['data']);\n", | |
" });\n", | |
" return ws;\n", | |
"};\n", | |
"\n", | |
"mpl.mpl_figure_comm = function (comm, msg) {\n", | |
" // This is the function which gets called when the mpl process\n", | |
" // starts-up an IPython Comm through the \"matplotlib\" channel.\n", | |
"\n", | |
" var id = msg.content.data.id;\n", | |
" // Get hold of the div created by the display call when the Comm\n", | |
" // socket was opened in Python.\n", | |
" var element = document.getElementById(id);\n", | |
" var ws_proxy = comm_websocket_adapter(comm);\n", | |
"\n", | |
" function ondownload(figure, _format) {\n", | |
" window.open(figure.canvas.toDataURL());\n", | |
" }\n", | |
"\n", | |
" var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n", | |
"\n", | |
" // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n", | |
" // web socket which is closed, not our websocket->open comm proxy.\n", | |
" ws_proxy.onopen();\n", | |
"\n", | |
" fig.parent_element = element;\n", | |
" fig.cell_info = mpl.find_output_cell(\"<div id='\" + id + \"'></div>\");\n", | |
" if (!fig.cell_info) {\n", | |
" console.error('Failed to find cell for figure', id, fig);\n", | |
" return;\n", | |
" }\n", | |
" fig.cell_info[0].output_area.element.on(\n", | |
" 'cleared',\n", | |
" { fig: fig },\n", | |
" fig._remove_fig_handler\n", | |
" );\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_close = function (fig, msg) {\n", | |
" var width = fig.canvas.width / fig.ratio;\n", | |
" fig.cell_info[0].output_area.element.off(\n", | |
" 'cleared',\n", | |
" fig._remove_fig_handler\n", | |
" );\n", | |
" fig.resizeObserverInstance.unobserve(fig.canvas_div);\n", | |
"\n", | |
" // Update the output cell to use the data from the current canvas.\n", | |
" fig.push_to_output();\n", | |
" var dataURL = fig.canvas.toDataURL();\n", | |
" // Re-enable the keyboard manager in IPython - without this line, in FF,\n", | |
" // the notebook keyboard shortcuts fail.\n", | |
" IPython.keyboard_manager.enable();\n", | |
" fig.parent_element.innerHTML =\n", | |
" '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n", | |
" fig.close_ws(fig, msg);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.close_ws = function (fig, msg) {\n", | |
" fig.send_message('closing', msg);\n", | |
" // fig.ws.close()\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.push_to_output = function (_remove_interactive) {\n", | |
" // Turn the data on the canvas into data in the output cell.\n", | |
" var width = this.canvas.width / this.ratio;\n", | |
" var dataURL = this.canvas.toDataURL();\n", | |
" this.cell_info[1]['text/html'] =\n", | |
" '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.updated_canvas_event = function () {\n", | |
" // Tell IPython that the notebook contents must change.\n", | |
" IPython.notebook.set_dirty(true);\n", | |
" this.send_message('ack', {});\n", | |
" var fig = this;\n", | |
" // Wait a second, then push the new image to the DOM so\n", | |
" // that it is saved nicely (might be nice to debounce this).\n", | |
" setTimeout(function () {\n", | |
" fig.push_to_output();\n", | |
" }, 1000);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_toolbar = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var toolbar = document.createElement('div');\n", | |
" toolbar.classList = 'btn-toolbar';\n", | |
" this.root.appendChild(toolbar);\n", | |
"\n", | |
" function on_click_closure(name) {\n", | |
" return function (_event) {\n", | |
" return fig.toolbar_button_onclick(name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" function on_mouseover_closure(tooltip) {\n", | |
" return function (event) {\n", | |
" if (!event.currentTarget.disabled) {\n", | |
" return fig.toolbar_button_onmouseover(tooltip);\n", | |
" }\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" fig.buttons = {};\n", | |
" var buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'btn-group';\n", | |
" var button;\n", | |
" for (var toolbar_ind in mpl.toolbar_items) {\n", | |
" var name = mpl.toolbar_items[toolbar_ind][0];\n", | |
" var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", | |
" var image = mpl.toolbar_items[toolbar_ind][2];\n", | |
" var method_name = mpl.toolbar_items[toolbar_ind][3];\n", | |
"\n", | |
" if (!name) {\n", | |
" /* Instead of a spacer, we start a new button group. */\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
" buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'btn-group';\n", | |
" continue;\n", | |
" }\n", | |
"\n", | |
" button = fig.buttons[name] = document.createElement('button');\n", | |
" button.classList = 'btn btn-default';\n", | |
" button.href = '#';\n", | |
" button.title = name;\n", | |
" button.innerHTML = '<i class=\"fa ' + image + ' fa-lg\"></i>';\n", | |
" button.addEventListener('click', on_click_closure(method_name));\n", | |
" button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", | |
" buttonGroup.appendChild(button);\n", | |
" }\n", | |
"\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
"\n", | |
" // Add the status bar.\n", | |
" var status_bar = document.createElement('span');\n", | |
" status_bar.classList = 'mpl-message pull-right';\n", | |
" toolbar.appendChild(status_bar);\n", | |
" this.message = status_bar;\n", | |
"\n", | |
" // Add the close button to the window.\n", | |
" var buttongrp = document.createElement('div');\n", | |
" buttongrp.classList = 'btn-group inline pull-right';\n", | |
" button = document.createElement('button');\n", | |
" button.classList = 'btn btn-mini btn-primary';\n", | |
" button.href = '#';\n", | |
" button.title = 'Stop Interaction';\n", | |
" button.innerHTML = '<i class=\"fa fa-power-off icon-remove icon-large\"></i>';\n", | |
" button.addEventListener('click', function (_evt) {\n", | |
" fig.handle_close(fig, {});\n", | |
" });\n", | |
" button.addEventListener(\n", | |
" 'mouseover',\n", | |
" on_mouseover_closure('Stop Interaction')\n", | |
" );\n", | |
" buttongrp.appendChild(button);\n", | |
" var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n", | |
" titlebar.insertBefore(buttongrp, titlebar.firstChild);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._remove_fig_handler = function (event) {\n", | |
" var fig = event.data.fig;\n", | |
" if (event.target !== this) {\n", | |
" // Ignore bubbled events from children.\n", | |
" return;\n", | |
" }\n", | |
" fig.close_ws(fig, {});\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._root_extra_style = function (el) {\n", | |
" el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._canvas_extra_style = function (el) {\n", | |
" // this is important to make the div 'focusable\n", | |
" el.setAttribute('tabindex', 0);\n", | |
" // reach out to IPython and tell the keyboard manager to turn it's self\n", | |
" // off when our div gets focus\n", | |
"\n", | |
" // location in version 3\n", | |
" if (IPython.notebook.keyboard_manager) {\n", | |
" IPython.notebook.keyboard_manager.register_events(el);\n", | |
" } else {\n", | |
" // location in version 2\n", | |
" IPython.keyboard_manager.register_events(el);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._key_event_extra = function (event, _name) {\n", | |
" var manager = IPython.notebook.keyboard_manager;\n", | |
" if (!manager) {\n", | |
" manager = IPython.keyboard_manager;\n", | |
" }\n", | |
"\n", | |
" // Check for shift+enter\n", | |
" if (event.shiftKey && event.which === 13) {\n", | |
" this.canvas_div.blur();\n", | |
" // select the cell after this one\n", | |
" var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", | |
" IPython.notebook.select(index + 1);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_save = function (fig, _msg) {\n", | |
" fig.ondownload(fig, null);\n", | |
"};\n", | |
"\n", | |
"mpl.find_output_cell = function (html_output) {\n", | |
" // Return the cell and output element which can be found *uniquely* in the notebook.\n", | |
" // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", | |
" // IPython event is triggered only after the cells have been serialised, which for\n", | |
" // our purposes (turning an active figure into a static one), is too late.\n", | |
" var cells = IPython.notebook.get_cells();\n", | |
" var ncells = cells.length;\n", | |
" for (var i = 0; i < ncells; i++) {\n", | |
" var cell = cells[i];\n", | |
" if (cell.cell_type === 'code') {\n", | |
" for (var j = 0; j < cell.output_area.outputs.length; j++) {\n", | |
" var data = cell.output_area.outputs[j];\n", | |
" if (data.data) {\n", | |
" // IPython >= 3 moved mimebundle to data attribute of output\n", | |
" data = data.data;\n", | |
" }\n", | |
" if (data['text/html'] === html_output) {\n", | |
" return [cell, data, j];\n", | |
" }\n", | |
" }\n", | |
" }\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"// Register the function which deals with the matplotlib target/channel.\n", | |
"// The kernel may be null if the page has been refreshed.\n", | |
"if (IPython.notebook.kernel !== null) {\n", | |
" IPython.notebook.kernel.comm_manager.register_target(\n", | |
" 'matplotlib',\n", | |
" mpl.mpl_figure_comm\n", | |
" );\n", | |
"}\n" | |
], | |
"text/plain": [ | |
"<IPython.core.display.Javascript object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
}, | |
{ | |
"data": { | |
"text/html": [ | |
"<img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAgAElEQVR4nO3de3TU9Z3/8Q8h95iMG9RcQFE0gBqisqy1Z3+C3MQugbZ7bJHDzXq41qC0rjdcjJwKBG/dukfEbdFzuoWgbbDrKqIBFZUEdHewBCNaUTEEtSshQSuJhrx+f3Ay5UsGTPjkHQjzfJ7z+MPJN2Hmm9G8nMkMTkREREQUU7kTfQWIiIiIqGtjABIRERHFWAxAIiIiohiLAUhEREQUYzEAiYiIiGIsBiARERFRjMUAJCIiIoqxGIBEREREMRYDkIiIiCjGYgASERERxVgMQCIiIqIYiwFIREREFGMxAImIiIhiLAYgERERUYzFACQiIiKKsRiARERERDEWA5CIiIgoxmIAEhEREcVYDEAiIiKiGIsBSERERBRjMQCJiIiIYiwGIBEREVGMxQAkIiIiirEYgEREREQxFgOQiIiIKMZiABIRERHFWAxAIiIiohiLAUhEREQUYzEAiYiIiGIsBiARERFRjMUAJCIiIoqxGIBEREREMRYDkIiIiCjGYgASERERxVgMQCIiIqIYiwFIREREFGMxAClmKy4ulnNO//d//xf14xdffLGGDRt2XF972rRp6tu3b+CyvXv3asKECTrzzDPlnNP3v//94/raJ7q+fftq2rRpkX9++eWX5ZzTyy+/3KGv88gjj+iJJ57o0OdE+7OmTZumtLS0Dn2db2vTpk0qLi7Wvn372nxs2LBhx32/8C0cDmvo0KHKyMiQc06//OUvT8j1aE8ffvihnHNyzqm0tLTNx7/t3z8iso0BSDGb5QB8//33FQ6HA5fNmzdPiYmJ+t3vfqfKykq9++67x/W1T3RHDsCGhgZVVlaqoaGhQ1/neM5vtD/LYgDef//9cs7pww8/bPOxt99+W2+//Xan/nnt7dJLL1VeXp7Wrl2ryspKffLJJyfkerSnwwdgv3799PXXXwc+zgAkOrExAClmsxyA0Ro1apQuvPDCTvt6LS0t+uqrrzrt67W3Iwfg8daR8/v111/rm2++ifqxrh6AJ7L4+HjNmTPnW4/76quv1NLS0gXX6Oi1DsDvfe97cs7p4YcfDnycAUh0YmMAUszW0QHY+vTjqlWrNH/+fOXk5Cg9PV0jR47Ujh07Ap97+FPAhz8ScrjWpzH37t2rOXPmKDc3VwkJCTrvvPM0f/58NTY2Br6mc0433nijHn30UQ0cOFAJCQl69NFH9cQTT8g5pw0bNmj69OnKzMxUenq6pkyZoi+//FKffPKJfvSjHykUCik7O1u33HJLm0djovX111/r1ltvVVZWllJSUvSP//iP2rJlS7ueAt65c6cmTJignJwcJSYm6qyzztKIESO0detWSYdG5JHno/V8tX693/72t/r5z3+u3Nxc9ejRQ++8884xnwLevn27RowYodTUVJ1xxhm68cYb9de//jVyXOv3IdrTzs45FRcXS/rb/eJo369oTwF39Hv429/+VgMHDlRKSooKCgr03//938f8XrR+j490+MdeeOEF/eQnP9EZZ5wh55wOHDiggwcPaunSpRowYIASExN15plnasqUKaqpqQl8/WHDhuniiy9WRUWFvvvd7yo5OVl9+/bV448/Lkl69tlnddlllyklJUX5+fl6/vnnj3l9Dz/f999/v8aMGaMzzzxT+/fvj3w82r9/R/ufiyPPeev9YOXKlbrtttuUnZ2ttLQ0FRYW6tNPP9X+/fs1Y8YM9erVS7169dL111+vL774Iur3Yvny5crLy1NiYqIuvPDCwNPVH374oXr27KnFixe3uU4bN26Uc05PPfXUt54LopMxBiDFbMc7AM8991xNmjRJzz33nEpLS3XOOecoLy9Pzc3NkWMPH4CNjY2qrKzUZZddpn79+qmysjLyNOaBAwdUUFCgtLQ0PfDAA3rxxRe1YMECxcfH65/+6Z8C18c5p969e6ugoECrVq3SSy+9pO3bt0cGwHnnnadbbrlFL774opYuXaqePXtq4sSJGjx4sO69916Vl5fr9ttvl3NODz744Leen2nTpqlHjx669dZb9eKLL+qhhx5S7969lZGR8a0DcMCAAbrgggv0n//5n9q4caPKysp0yy23RI4Jh8Pq16+fLrvsssj5aH3KvPXr9e7dW9dee62eeeYZPfvss9q7d+9RB2BiYqLOOeccLVq0SC+++KLuuecexcfHq7CwMHJcewdgTU2N5s6dK+ec1qxZE/h+SW3HSEe/h+eee64uv/xyPfXUU1q7dq2uuuoqxcfHa+fOnUf9XvzlL39RZWWlnHO69tprI9dJ+tsA7N27t2bOnKnnn39ef/jDH9Tc3KyZM2fKOaeioiKtW7dOy5cv15lnnqmzzz47cL8fNmyYevXqpQEDBmjFihV64YUXVFhYKOecFi5cqEGDBqm0tFRr167VFVdcoaSkJNXW1h71+h5+vu+//3699dZb6tGjhxYsWBD5eGcMwL59++r666+P3LbTTjtNw4cP1+jRo/Uv//IvgX8X5s6d2+Z7cfbZZ+uiiy5SaWmpnnnmGV1zzTVyzun3v/995Lgf/vCHOueccwL/fkvSj370I+Xm5h71kWmikz0GIMVsxzsAj/yh/tRTT8k5F/mBLEV/EUjroyyHt3z58qiPIixdulTOOb344ouRy5xzCoVCqqurCxzbOgCO/AH3gx/8QM45PfTQQ4HLL730Ug0ePDjqbW7tnXfekXNOP/vZzwKXr1y5Us65Yw7Azz//XM45/du//dsx/4yjPQXc+vWGDh161I8dOQCdc/rVr34VOHbRokVyzun111+X1P4BKB37KeAjx0hHv4dZWVmBR8I+/fRTxcXFacmSJW3+rGjX88Ybbwxc1vr9nzp1auDy1u/hT3/608DlW7ZskXNO8+fPD9wm55z+53/+J3LZ3r171bNnT6WkpATG3ltvvRX1Kd0jO3wAStKkSZOUlpYW+b3FzhiA48aNCxw3b948Oed00003BS7/wQ9+oMzMzMBlzjmlpKTo008/jVzW3NysgQMH6oILLmjzZz399NORy2praxUfH6+FCxce8xwQncwxAClmO94BuHz58sBxO3bskHNOq1evjlzW3gH44x//WGlpaW1+X+uzzz6Tc06333575DLnnH74wx+2uZ6HPwV4eHfeeaecc21ebDJx4kT16tUr6m1ubdmyZW0GgSR98803io+PP+YAbGlp0fnnn6/evXvrwQcfVDgc1sGDB9v8Gd82AI8cdNH+LOlvA/Dzzz8PHNs6QH7xi18E/rmzB2BHv4fXXXddm6+ZnZ2t2bNnt7k82vU82gD8r//6r8Dlrd/DN954o83XufDCC/Wd73wncJtycnLaHJeTk6Pvfve7gcuamprknNMtt9xyzOt65AD88MMPlZiYGLmdnTEAH3vsscBxjz322DH/XTj8aWDnXOAR4tZar9fhT5NfcsklGjVqVOSfFyxYoISEhJP6RThE3xYDkGK2X/ziF3LOBR4BOLwBAwYE/qPf+kPn8KeHpOjDor0DcOTIkTr//POj/vnx8fGaPn165J+jPZoj/W0AvPnmm4HLjzZw2/OiidZzs3v37jYfy8rK+tangD/66CPdcMMNysrKknNOmZmZmjt3buCRr28bgNF+t+poAzA+Pr7NsQcOHJBzTvPmzZNkNwA7+j08csBJ7X9hzbEG4JFDr/V7eOTv+7Ve58Mf5Yp232y9XmPHjm3X9TiyIwegdOgRuvj4eL333nudMgCP/HexI/8uOOcC35vWHn30UTnn9NZbb0UuW7FihXr06KEdO3bo66+/VnZ2tiZOnHjM2090sscApJjtP/7jP+Sc0//+7/+2+VhLS4syMjI0adKkyGUWA/DHP/6xTjvttKM+enTHHXdELjvaD12LAejzCOCRvfvuu/rFL36hnj17atasWZHLv20AHnmej/ZntfcRwE8++STqI7itT1n7PALo+z3sjAF45Pf/2x4BvOKKKwK3qSsG4Oeff66MjAxde+21Ue+fAwYMiPoI6dEejfcdgO19BPDAgQM644wzNHfu3MivQbT+agFRd40BSDHb+++/rx49eui2225r87G1a9e2GXUWA7D1Kas1a9YELm8dIOXl5ZHLunIAVldXH/fvAB6tSy+9VP/wD/8Q+efBgwfr8ssvb3Pc8Q7Ao/0O4GuvvSbp0KhPTk5u8yjqihUr2gzAhx9+WM45VVdXt7kORw7AzvgeWgzA1l9NOPL34d544w0553TXXXcFblNXDEDpb9+X1reHOfz+OWbMGF100UWB4999913Fx8ebDMCj/Q5gtEd058+fr4yMDP393/+9Lr300mPedqLuEAOQYrq5c+eqR48emjlzpv74xz/qhRde0L333qvTTjtNQ4YMUVNTU+RYiwHY+grS9PR0PfTQQyovL1dxcbESEhKivoK0qwagJE2ePDkykFtfBZybm/utrwL+05/+pCuvvFIPP/ywnn/+eW3YsEF33XWX4uLiAi88mDZtmpKSkrR69Wq98cYb2rZtW+DrdWQAHu1VwN/73vcCnz99+nQlJyfrwQcf1Pr167V48WLl5+e3GYCtf86sWbNUUVGhN998M/L09dFeBezzPbQYgJI0c+ZM9ejRQ/PmzdMLL7ygxx57TGeddZbOPvvswCOmXTkA//rXvyo3N1fOuTb3z9/97ndyzmnOnDlav369VqxYoQEDBignJ8dkAB7tVcCH/z5va7t371Z8fLycc/rNb35zzNtO1B1iAFJM19LSokcffVRDhgxRamqqEhMTlZeXp9tvv73N+4ZZDEDp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8cdyzqmmpiZweWc5e95TAADELIufrQ0NDaqpqZFzTvX19V0xOU7KYnYA1tbWyjmnTZs2BS5ftGiR+vfvH/VziouL5ZwDAACngJqamq6YHCdlMT8AKyoqApffe++9GjBgQNTPOfIRwH379mnnzp2qr683+78Tq0cXwXnmPJ96OM+c51OJ5Xmur69XTU2NDh482BWT46QsZgfg8TwF3JU1NPD7CV0R57lr4jx3TZznronz3DVxnm2L2QEoHXoRyJw5cwKXXXjhhe1+EYhl3PG7Js5z18R57po4z10T57lr4jzbFtMDsPVtYFasWKHq6mrNmzdPaWlp+uijj070VeOO30VxnrsmznPXxHnumjjPXRPn2baYHoDSoTeC7tu3rxITEzV48GBt3LjxRF8lSYd+37C4uFiNjY0n+qqc0nGeuybOc9fEee6aOM9dE+fZtpgfgERERESxFgOQiIiIKMZiABIRERHFWAxAIiIiohiLAUhEREQUYzEAT1CPPPKIzj33XCUlJWnw4MF69dVXj3n8K6+8osGDByspKUnnnXeeHn300S66pt2/jpzrsrIyjRo1SmeccYbS09N1xRVXaN26dV14bbtvHb1Pt/b666+rZ8+euuSSS4yv4alRR89zY2Oj5s+fr3POOUeJiYnq16+fVqxY0UXXtvvW0fP8u9/9TgUFBUpJSVF2drauv/56ff755110bbtnGzduVGFhoXJycuSc09NPP/2tn8PPws6LAXgCan3/wV//+teqrq7WzTffrLS0NO3atSvq8R988IFSU1N18803q7q6Wr/+9a+VkJCgP/zhD118zbtfHT3XN998s5YuXao33nhD7733nu68804lJCQoHA538TXvXnX0PLdWX1+vfv366eqrr2YAtqPjOc/jx4/Xd77zHZWXl+vDDz/Uli1b2vwd6BSso+f5tddeU1xcnH71q1/pgw8+0GuvvaaLL75YP/jBD7r4mnev1q5dq7vuuktlZWXtGoD8LOzcGIAnoMsvv1yzZ88OXDZw4MCj/g0kt912mwYOHBi4bNasWbriiivMruOpUkfPdbQuuugiLVy4sLOv2inV8Z7nCRMm6F//9V9VXFzMAGxHHT3Pzz//vEKhkPbu3dsVV++UqaPn+f7771e/fv0Clz388MPq06eP2XU81WrPAORnYefGAOzijufvIL7yyit10003BS5bs2aN4uPj9fXXX5td1+5eZ/x9zwcPHtTZZ5+tf//3f7e4iqdEx3ueH3/8cQ0ZMkTffPMNA7AdHc95njNnjkaOHKnbb79dubm5ysvL0y233KKvvvqqK65yt+x4zvOmTZuUmJio5557Ti0tLfr00081dOhQzZo1qyuu8ilRewYgPws7NwZgF1dbWyvnXJunYBYtWqT+/ftH/Zy8vDwtWrQocNmmTZvknNOePXvMrmt373jO9ZHdd999yszM1GeffWZxFU+Jjuc8v/feezrrrLP07rvvShIDsB0dz3keM2aMkpKSNHbsWG3ZskXPPfec+vbtq5/85CddcZW7Zcf7343f//73Ou200xQfHy/nnMaPH88o6UDtGYD8LOzcGIBdXOt/XCoqKgKX33vvvRowYEDUz8nLy9PixYsDl73++utyzumTTz4xu67dveM514e3atUqpaamqry83OoqnhJ19Dw3NzdryJAhgV/eZgB+e8dzfx49erSSk5NVX18fuaysrEw9evTgUcCjdDzn+e2331ZOTo7uu+8+/elPf9K6des0aNAg3XDDDV1xlU+J2jsA+VnYeTEAuzieAu66fJ4CXr16tVJSUvTss89aXsVToo6e53379sk5p549e0b06NEjctmGDRu66qp3q47n/jx16lSdf/75gcuqq6vlnNN7771ndl27c8dznidPnqxrr702cNlrr73GI1MdiKeAuz4G4Ano8ssv15w5cwKXXXjhhcd8EciFF14YuGz27Nn84ms76ui5lg498pecnNyutySgQ3XkPB88eFBVVVUBc+bM0YABA1RVVaUvv/yyq652t6uj9+fHHntMKSkp+uKLLyKX/fGPf1RcXByPAB6jjp7nf/7nf9aPf/zjwGUVFRVyzqm2ttbsep5KtfdFIPws7LwYgCeg1rcYWLFihaqrqzVv3jylpaXpo48+kiTdcccdmjJlSuT41pe+/+xnP1N1dbVWrFjBS9/bWUfP9apVqxQfH69HHnlEn3zyScThT6FR2zp6no+Mp4DbV0fP8xdffKE+ffro2muv1dtvv62NGzcqLy9P06dPP1E3oVvU0fP8xBNPKD4+XsuWLdPOnTv1+uuva8iQIbr88stP1E3oFn3xxRfaunWrtm7dKuecHnroIW3dujXydjv8LLSNAXiCeuSRR9S3b18lJiZq8ODB2rhxY+Rj06ZN07BhwwLHv/LKK7rsssuUmJioc889lze/7EAdOdfDhg2Tc66NadOmdf0V72Z19D59eAzA9tfR8/zOO+9o1KhRSklJUZ8+ffTzn/+cR//aUUfP88MPP6yLLrpIKSkpysnJ0aRJk7R79+4uvtbdq5dffvmY/73lZ6FtDEAiIiKiGIsBSERERBRjMQCJiIiIYiwGIBEREVGMxQAkIiIiirEYgEREREQxFgOQiIiIKMZiABIRERHFWAxAIiIiohiLAUhEREQUYzEAiYiIiGIsBiARERFRjMUAJCIiIoqxGIBEREREMRYDkIiIiCjGYgASERERxVgMQCIiIqIYiwFIREREFGMxAImIiIhiLAYgERERUYzFACQiIiKKsRiARERERDEWA5CIiIgoxmIAEhEREcVYDEAiIiKiGIsBSERERBRjMQCJiIiIYiwGIBEREVGMxQAkIiIiirEYgEREREQxFgOQiIiIKMZiABIRERHFWP8fvSK7MIEP1csAAAAASUVORK5CYII=\" width=\"640\">" | |
], | |
"text/plain": [ | |
"<IPython.core.display.HTML object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"#Those are reference figures obtained from Numpy\n", | |
"print(\"Reference timing from numpy: Uniform\")\n", | |
"%timeit numpy.random.random(shape)\n", | |
"print(\"Reference timing from numpy: Normal\")\n", | |
"%timeit numpy.random.normal(0, 1, size=shape)\n", | |
"fig, ax = subplots()\n", | |
"ax.hist(numpy.random.random(shape).ravel())\n", | |
"ax.set_title(\"Uniform distribution from Numpy\")\n", | |
"pass" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"With figures like 110ms for 4 Mpix of random values, it makes clear that 10 fps is a hard limit for those frame generation: Almost all the time is spent in generating those random numbers ! \n", | |
"\n", | |
"Let's see how the `rand` function from the standard C library behaves. \n", | |
"\n", | |
"## Cython implementation of the `C-rand`\n", | |
"\n", | |
"The C-standard library pseudo-random number generator is not of the greatest quality, but this is probably not of great importance in this case. We have a performance issue ! Let's look at some simple and direct call to `rand` via Cython. Nothing fancy, but the benchmarks !\n", | |
"\n", | |
"Thanks Jupyter for letting use code and profile cython code interactively." | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 4, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"%load_ext Cython" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 5, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"%%cython\n", | |
"# cython: boundscheck=False\n", | |
"# cython: cdivision=True\n", | |
"# cython: wraparound=False\n", | |
"import time\n", | |
"import numpy\n", | |
"from libc.stdint cimport uint64_t\n", | |
"from libc.stdlib cimport rand, RAND_MAX, srand\n", | |
"\n", | |
"srand(<unsigned int> (time.time_ns()%RAND_MAX))\n", | |
"\n", | |
"def cython_uniform_rand(shape):\n", | |
" cdef uint64_t size = numpy.prod(shape), idx\n", | |
" cdef double[::1] ary = numpy.empty(size)\n", | |
" with nogil:\n", | |
" for idx in range(size):\n", | |
" ary[idx] = rand()/(RAND_MAX+1.0)\n", | |
" return numpy.asarray(ary).reshape(shape)" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 6, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"Using the C-rand function from Cython\n", | |
"58.6 ms ± 501 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" | |
] | |
}, | |
{ | |
"data": { | |
"application/javascript": [ | |
"/* Put everything inside the global mpl namespace */\n", | |
"/* global mpl */\n", | |
"window.mpl = {};\n", | |
"\n", | |
"mpl.get_websocket_type = function () {\n", | |
" if (typeof WebSocket !== 'undefined') {\n", | |
" return WebSocket;\n", | |
" } else if (typeof MozWebSocket !== 'undefined') {\n", | |
" return MozWebSocket;\n", | |
" } else {\n", | |
" alert(\n", | |
" 'Your browser does not have WebSocket support. ' +\n", | |
" 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", | |
" 'Firefox 4 and 5 are also supported but you ' +\n", | |
" 'have to enable WebSockets in about:config.'\n", | |
" );\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n", | |
" this.id = figure_id;\n", | |
"\n", | |
" this.ws = websocket;\n", | |
"\n", | |
" this.supports_binary = this.ws.binaryType !== undefined;\n", | |
"\n", | |
" if (!this.supports_binary) {\n", | |
" var warnings = document.getElementById('mpl-warnings');\n", | |
" if (warnings) {\n", | |
" warnings.style.display = 'block';\n", | |
" warnings.textContent =\n", | |
" 'This browser does not support binary websocket messages. ' +\n", | |
" 'Performance may be slow.';\n", | |
" }\n", | |
" }\n", | |
"\n", | |
" this.imageObj = new Image();\n", | |
"\n", | |
" this.context = undefined;\n", | |
" this.message = undefined;\n", | |
" this.canvas = undefined;\n", | |
" this.rubberband_canvas = undefined;\n", | |
" this.rubberband_context = undefined;\n", | |
" this.format_dropdown = undefined;\n", | |
"\n", | |
" this.image_mode = 'full';\n", | |
"\n", | |
" this.root = document.createElement('div');\n", | |
" this.root.setAttribute('style', 'display: inline-block');\n", | |
" this._root_extra_style(this.root);\n", | |
"\n", | |
" parent_element.appendChild(this.root);\n", | |
"\n", | |
" this._init_header(this);\n", | |
" this._init_canvas(this);\n", | |
" this._init_toolbar(this);\n", | |
"\n", | |
" var fig = this;\n", | |
"\n", | |
" this.waiting = false;\n", | |
"\n", | |
" this.ws.onopen = function () {\n", | |
" fig.send_message('supports_binary', { value: fig.supports_binary });\n", | |
" fig.send_message('send_image_mode', {});\n", | |
" if (fig.ratio !== 1) {\n", | |
" fig.send_message('set_dpi_ratio', { dpi_ratio: fig.ratio });\n", | |
" }\n", | |
" fig.send_message('refresh', {});\n", | |
" };\n", | |
"\n", | |
" this.imageObj.onload = function () {\n", | |
" if (fig.image_mode === 'full') {\n", | |
" // Full images could contain transparency (where diff images\n", | |
" // almost always do), so we need to clear the canvas so that\n", | |
" // there is no ghosting.\n", | |
" fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", | |
" }\n", | |
" fig.context.drawImage(fig.imageObj, 0, 0);\n", | |
" };\n", | |
"\n", | |
" this.imageObj.onunload = function () {\n", | |
" fig.ws.close();\n", | |
" };\n", | |
"\n", | |
" this.ws.onmessage = this._make_on_message_function(this);\n", | |
"\n", | |
" this.ondownload = ondownload;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_header = function () {\n", | |
" var titlebar = document.createElement('div');\n", | |
" titlebar.classList =\n", | |
" 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n", | |
" var titletext = document.createElement('div');\n", | |
" titletext.classList = 'ui-dialog-title';\n", | |
" titletext.setAttribute(\n", | |
" 'style',\n", | |
" 'width: 100%; text-align: center; padding: 3px;'\n", | |
" );\n", | |
" titlebar.appendChild(titletext);\n", | |
" this.root.appendChild(titlebar);\n", | |
" this.header = titletext;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n", | |
"\n", | |
"mpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n", | |
"\n", | |
"mpl.figure.prototype._init_canvas = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var canvas_div = (this.canvas_div = document.createElement('div'));\n", | |
" canvas_div.setAttribute(\n", | |
" 'style',\n", | |
" 'border: 1px solid #ddd;' +\n", | |
" 'box-sizing: content-box;' +\n", | |
" 'clear: both;' +\n", | |
" 'min-height: 1px;' +\n", | |
" 'min-width: 1px;' +\n", | |
" 'outline: 0;' +\n", | |
" 'overflow: hidden;' +\n", | |
" 'position: relative;' +\n", | |
" 'resize: both;'\n", | |
" );\n", | |
"\n", | |
" function on_keyboard_event_closure(name) {\n", | |
" return function (event) {\n", | |
" return fig.key_event(event, name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" canvas_div.addEventListener(\n", | |
" 'keydown',\n", | |
" on_keyboard_event_closure('key_press')\n", | |
" );\n", | |
" canvas_div.addEventListener(\n", | |
" 'keyup',\n", | |
" on_keyboard_event_closure('key_release')\n", | |
" );\n", | |
"\n", | |
" this._canvas_extra_style(canvas_div);\n", | |
" this.root.appendChild(canvas_div);\n", | |
"\n", | |
" var canvas = (this.canvas = document.createElement('canvas'));\n", | |
" canvas.classList.add('mpl-canvas');\n", | |
" canvas.setAttribute('style', 'box-sizing: content-box;');\n", | |
"\n", | |
" this.context = canvas.getContext('2d');\n", | |
"\n", | |
" var backingStore =\n", | |
" this.context.backingStorePixelRatio ||\n", | |
" this.context.webkitBackingStorePixelRatio ||\n", | |
" this.context.mozBackingStorePixelRatio ||\n", | |
" this.context.msBackingStorePixelRatio ||\n", | |
" this.context.oBackingStorePixelRatio ||\n", | |
" this.context.backingStorePixelRatio ||\n", | |
" 1;\n", | |
"\n", | |
" this.ratio = (window.devicePixelRatio || 1) / backingStore;\n", | |
"\n", | |
" var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n", | |
" 'canvas'\n", | |
" ));\n", | |
" rubberband_canvas.setAttribute(\n", | |
" 'style',\n", | |
" 'box-sizing: content-box; position: absolute; left: 0; top: 0; z-index: 1;'\n", | |
" );\n", | |
"\n", | |
" // Apply a ponyfill if ResizeObserver is not implemented by browser.\n", | |
" if (this.ResizeObserver === undefined) {\n", | |
" if (window.ResizeObserver !== undefined) {\n", | |
" this.ResizeObserver = window.ResizeObserver;\n", | |
" } else {\n", | |
" var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n", | |
" this.ResizeObserver = obs.ResizeObserver;\n", | |
" }\n", | |
" }\n", | |
"\n", | |
" this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n", | |
" var nentries = entries.length;\n", | |
" for (var i = 0; i < nentries; i++) {\n", | |
" var entry = entries[i];\n", | |
" var width, height;\n", | |
" if (entry.contentBoxSize) {\n", | |
" if (entry.contentBoxSize instanceof Array) {\n", | |
" // Chrome 84 implements new version of spec.\n", | |
" width = entry.contentBoxSize[0].inlineSize;\n", | |
" height = entry.contentBoxSize[0].blockSize;\n", | |
" } else {\n", | |
" // Firefox implements old version of spec.\n", | |
" width = entry.contentBoxSize.inlineSize;\n", | |
" height = entry.contentBoxSize.blockSize;\n", | |
" }\n", | |
" } else {\n", | |
" // Chrome <84 implements even older version of spec.\n", | |
" width = entry.contentRect.width;\n", | |
" height = entry.contentRect.height;\n", | |
" }\n", | |
"\n", | |
" // Keep the size of the canvas and rubber band canvas in sync with\n", | |
" // the canvas container.\n", | |
" if (entry.devicePixelContentBoxSize) {\n", | |
" // Chrome 84 implements new version of spec.\n", | |
" canvas.setAttribute(\n", | |
" 'width',\n", | |
" entry.devicePixelContentBoxSize[0].inlineSize\n", | |
" );\n", | |
" canvas.setAttribute(\n", | |
" 'height',\n", | |
" entry.devicePixelContentBoxSize[0].blockSize\n", | |
" );\n", | |
" } else {\n", | |
" canvas.setAttribute('width', width * fig.ratio);\n", | |
" canvas.setAttribute('height', height * fig.ratio);\n", | |
" }\n", | |
" canvas.setAttribute(\n", | |
" 'style',\n", | |
" 'width: ' + width + 'px; height: ' + height + 'px;'\n", | |
" );\n", | |
"\n", | |
" rubberband_canvas.setAttribute('width', width);\n", | |
" rubberband_canvas.setAttribute('height', height);\n", | |
"\n", | |
" // And update the size in Python. We ignore the initial 0/0 size\n", | |
" // that occurs as the element is placed into the DOM, which should\n", | |
" // otherwise not happen due to the minimum size styling.\n", | |
" if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n", | |
" fig.request_resize(width, height);\n", | |
" }\n", | |
" }\n", | |
" });\n", | |
" this.resizeObserverInstance.observe(canvas_div);\n", | |
"\n", | |
" function on_mouse_event_closure(name) {\n", | |
" return function (event) {\n", | |
" return fig.mouse_event(event, name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mousedown',\n", | |
" on_mouse_event_closure('button_press')\n", | |
" );\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseup',\n", | |
" on_mouse_event_closure('button_release')\n", | |
" );\n", | |
" // Throttle sequential mouse events to 1 every 20ms.\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mousemove',\n", | |
" on_mouse_event_closure('motion_notify')\n", | |
" );\n", | |
"\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseenter',\n", | |
" on_mouse_event_closure('figure_enter')\n", | |
" );\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseleave',\n", | |
" on_mouse_event_closure('figure_leave')\n", | |
" );\n", | |
"\n", | |
" canvas_div.addEventListener('wheel', function (event) {\n", | |
" if (event.deltaY < 0) {\n", | |
" event.step = 1;\n", | |
" } else {\n", | |
" event.step = -1;\n", | |
" }\n", | |
" on_mouse_event_closure('scroll')(event);\n", | |
" });\n", | |
"\n", | |
" canvas_div.appendChild(canvas);\n", | |
" canvas_div.appendChild(rubberband_canvas);\n", | |
"\n", | |
" this.rubberband_context = rubberband_canvas.getContext('2d');\n", | |
" this.rubberband_context.strokeStyle = '#000000';\n", | |
"\n", | |
" this._resize_canvas = function (width, height, forward) {\n", | |
" if (forward) {\n", | |
" canvas_div.style.width = width + 'px';\n", | |
" canvas_div.style.height = height + 'px';\n", | |
" }\n", | |
" };\n", | |
"\n", | |
" // Disable right mouse context menu.\n", | |
" this.rubberband_canvas.addEventListener('contextmenu', function (_e) {\n", | |
" event.preventDefault();\n", | |
" return false;\n", | |
" });\n", | |
"\n", | |
" function set_focus() {\n", | |
" canvas.focus();\n", | |
" canvas_div.focus();\n", | |
" }\n", | |
"\n", | |
" window.setTimeout(set_focus, 100);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_toolbar = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var toolbar = document.createElement('div');\n", | |
" toolbar.classList = 'mpl-toolbar';\n", | |
" this.root.appendChild(toolbar);\n", | |
"\n", | |
" function on_click_closure(name) {\n", | |
" return function (_event) {\n", | |
" return fig.toolbar_button_onclick(name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" function on_mouseover_closure(tooltip) {\n", | |
" return function (event) {\n", | |
" if (!event.currentTarget.disabled) {\n", | |
" return fig.toolbar_button_onmouseover(tooltip);\n", | |
" }\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" fig.buttons = {};\n", | |
" var buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'mpl-button-group';\n", | |
" for (var toolbar_ind in mpl.toolbar_items) {\n", | |
" var name = mpl.toolbar_items[toolbar_ind][0];\n", | |
" var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", | |
" var image = mpl.toolbar_items[toolbar_ind][2];\n", | |
" var method_name = mpl.toolbar_items[toolbar_ind][3];\n", | |
"\n", | |
" if (!name) {\n", | |
" /* Instead of a spacer, we start a new button group. */\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
" buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'mpl-button-group';\n", | |
" continue;\n", | |
" }\n", | |
"\n", | |
" var button = (fig.buttons[name] = document.createElement('button'));\n", | |
" button.classList = 'mpl-widget';\n", | |
" button.setAttribute('role', 'button');\n", | |
" button.setAttribute('aria-disabled', 'false');\n", | |
" button.addEventListener('click', on_click_closure(method_name));\n", | |
" button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", | |
"\n", | |
" var icon_img = document.createElement('img');\n", | |
" icon_img.src = '_images/' + image + '.png';\n", | |
" icon_img.srcset = '_images/' + image + '_large.png 2x';\n", | |
" icon_img.alt = tooltip;\n", | |
" button.appendChild(icon_img);\n", | |
"\n", | |
" buttonGroup.appendChild(button);\n", | |
" }\n", | |
"\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
"\n", | |
" var fmt_picker = document.createElement('select');\n", | |
" fmt_picker.classList = 'mpl-widget';\n", | |
" toolbar.appendChild(fmt_picker);\n", | |
" this.format_dropdown = fmt_picker;\n", | |
"\n", | |
" for (var ind in mpl.extensions) {\n", | |
" var fmt = mpl.extensions[ind];\n", | |
" var option = document.createElement('option');\n", | |
" option.selected = fmt === mpl.default_extension;\n", | |
" option.innerHTML = fmt;\n", | |
" fmt_picker.appendChild(option);\n", | |
" }\n", | |
"\n", | |
" var status_bar = document.createElement('span');\n", | |
" status_bar.classList = 'mpl-message';\n", | |
" toolbar.appendChild(status_bar);\n", | |
" this.message = status_bar;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n", | |
" // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n", | |
" // which will in turn request a refresh of the image.\n", | |
" this.send_message('resize', { width: x_pixels, height: y_pixels });\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.send_message = function (type, properties) {\n", | |
" properties['type'] = type;\n", | |
" properties['figure_id'] = this.id;\n", | |
" this.ws.send(JSON.stringify(properties));\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.send_draw_message = function () {\n", | |
" if (!this.waiting) {\n", | |
" this.waiting = true;\n", | |
" this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_save = function (fig, _msg) {\n", | |
" var format_dropdown = fig.format_dropdown;\n", | |
" var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n", | |
" fig.ondownload(fig, format);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_resize = function (fig, msg) {\n", | |
" var size = msg['size'];\n", | |
" if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n", | |
" fig._resize_canvas(size[0], size[1], msg['forward']);\n", | |
" fig.send_message('refresh', {});\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_rubberband = function (fig, msg) {\n", | |
" var x0 = msg['x0'] / fig.ratio;\n", | |
" var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n", | |
" var x1 = msg['x1'] / fig.ratio;\n", | |
" var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n", | |
" x0 = Math.floor(x0) + 0.5;\n", | |
" y0 = Math.floor(y0) + 0.5;\n", | |
" x1 = Math.floor(x1) + 0.5;\n", | |
" y1 = Math.floor(y1) + 0.5;\n", | |
" var min_x = Math.min(x0, x1);\n", | |
" var min_y = Math.min(y0, y1);\n", | |
" var width = Math.abs(x1 - x0);\n", | |
" var height = Math.abs(y1 - y0);\n", | |
"\n", | |
" fig.rubberband_context.clearRect(\n", | |
" 0,\n", | |
" 0,\n", | |
" fig.canvas.width / fig.ratio,\n", | |
" fig.canvas.height / fig.ratio\n", | |
" );\n", | |
"\n", | |
" fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_figure_label = function (fig, msg) {\n", | |
" // Updates the figure title.\n", | |
" fig.header.textContent = msg['label'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_cursor = function (fig, msg) {\n", | |
" var cursor = msg['cursor'];\n", | |
" switch (cursor) {\n", | |
" case 0:\n", | |
" cursor = 'pointer';\n", | |
" break;\n", | |
" case 1:\n", | |
" cursor = 'default';\n", | |
" break;\n", | |
" case 2:\n", | |
" cursor = 'crosshair';\n", | |
" break;\n", | |
" case 3:\n", | |
" cursor = 'move';\n", | |
" break;\n", | |
" }\n", | |
" fig.rubberband_canvas.style.cursor = cursor;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_message = function (fig, msg) {\n", | |
" fig.message.textContent = msg['message'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_draw = function (fig, _msg) {\n", | |
" // Request the server to send over a new figure.\n", | |
" fig.send_draw_message();\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_image_mode = function (fig, msg) {\n", | |
" fig.image_mode = msg['mode'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n", | |
" for (var key in msg) {\n", | |
" if (!(key in fig.buttons)) {\n", | |
" continue;\n", | |
" }\n", | |
" fig.buttons[key].disabled = !msg[key];\n", | |
" fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n", | |
" if (msg['mode'] === 'PAN') {\n", | |
" fig.buttons['Pan'].classList.add('active');\n", | |
" fig.buttons['Zoom'].classList.remove('active');\n", | |
" } else if (msg['mode'] === 'ZOOM') {\n", | |
" fig.buttons['Pan'].classList.remove('active');\n", | |
" fig.buttons['Zoom'].classList.add('active');\n", | |
" } else {\n", | |
" fig.buttons['Pan'].classList.remove('active');\n", | |
" fig.buttons['Zoom'].classList.remove('active');\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.updated_canvas_event = function () {\n", | |
" // Called whenever the canvas gets updated.\n", | |
" this.send_message('ack', {});\n", | |
"};\n", | |
"\n", | |
"// A function to construct a web socket function for onmessage handling.\n", | |
"// Called in the figure constructor.\n", | |
"mpl.figure.prototype._make_on_message_function = function (fig) {\n", | |
" return function socket_on_message(evt) {\n", | |
" if (evt.data instanceof Blob) {\n", | |
" /* FIXME: We get \"Resource interpreted as Image but\n", | |
" * transferred with MIME type text/plain:\" errors on\n", | |
" * Chrome. But how to set the MIME type? It doesn't seem\n", | |
" * to be part of the websocket stream */\n", | |
" evt.data.type = 'image/png';\n", | |
"\n", | |
" /* Free the memory for the previous frames */\n", | |
" if (fig.imageObj.src) {\n", | |
" (window.URL || window.webkitURL).revokeObjectURL(\n", | |
" fig.imageObj.src\n", | |
" );\n", | |
" }\n", | |
"\n", | |
" fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n", | |
" evt.data\n", | |
" );\n", | |
" fig.updated_canvas_event();\n", | |
" fig.waiting = false;\n", | |
" return;\n", | |
" } else if (\n", | |
" typeof evt.data === 'string' &&\n", | |
" evt.data.slice(0, 21) === 'data:image/png;base64'\n", | |
" ) {\n", | |
" fig.imageObj.src = evt.data;\n", | |
" fig.updated_canvas_event();\n", | |
" fig.waiting = false;\n", | |
" return;\n", | |
" }\n", | |
"\n", | |
" var msg = JSON.parse(evt.data);\n", | |
" var msg_type = msg['type'];\n", | |
"\n", | |
" // Call the \"handle_{type}\" callback, which takes\n", | |
" // the figure and JSON message as its only arguments.\n", | |
" try {\n", | |
" var callback = fig['handle_' + msg_type];\n", | |
" } catch (e) {\n", | |
" console.log(\n", | |
" \"No handler for the '\" + msg_type + \"' message type: \",\n", | |
" msg\n", | |
" );\n", | |
" return;\n", | |
" }\n", | |
"\n", | |
" if (callback) {\n", | |
" try {\n", | |
" // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n", | |
" callback(fig, msg);\n", | |
" } catch (e) {\n", | |
" console.log(\n", | |
" \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n", | |
" e,\n", | |
" e.stack,\n", | |
" msg\n", | |
" );\n", | |
" }\n", | |
" }\n", | |
" };\n", | |
"};\n", | |
"\n", | |
"// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n", | |
"mpl.findpos = function (e) {\n", | |
" //this section is from http://www.quirksmode.org/js/events_properties.html\n", | |
" var targ;\n", | |
" if (!e) {\n", | |
" e = window.event;\n", | |
" }\n", | |
" if (e.target) {\n", | |
" targ = e.target;\n", | |
" } else if (e.srcElement) {\n", | |
" targ = e.srcElement;\n", | |
" }\n", | |
" if (targ.nodeType === 3) {\n", | |
" // defeat Safari bug\n", | |
" targ = targ.parentNode;\n", | |
" }\n", | |
"\n", | |
" // pageX,Y are the mouse positions relative to the document\n", | |
" var boundingRect = targ.getBoundingClientRect();\n", | |
" var x = e.pageX - (boundingRect.left + document.body.scrollLeft);\n", | |
" var y = e.pageY - (boundingRect.top + document.body.scrollTop);\n", | |
"\n", | |
" return { x: x, y: y };\n", | |
"};\n", | |
"\n", | |
"/*\n", | |
" * return a copy of an object with only non-object keys\n", | |
" * we need this to avoid circular references\n", | |
" * http://stackoverflow.com/a/24161582/3208463\n", | |
" */\n", | |
"function simpleKeys(original) {\n", | |
" return Object.keys(original).reduce(function (obj, key) {\n", | |
" if (typeof original[key] !== 'object') {\n", | |
" obj[key] = original[key];\n", | |
" }\n", | |
" return obj;\n", | |
" }, {});\n", | |
"}\n", | |
"\n", | |
"mpl.figure.prototype.mouse_event = function (event, name) {\n", | |
" var canvas_pos = mpl.findpos(event);\n", | |
"\n", | |
" if (name === 'button_press') {\n", | |
" this.canvas.focus();\n", | |
" this.canvas_div.focus();\n", | |
" }\n", | |
"\n", | |
" var x = canvas_pos.x * this.ratio;\n", | |
" var y = canvas_pos.y * this.ratio;\n", | |
"\n", | |
" this.send_message(name, {\n", | |
" x: x,\n", | |
" y: y,\n", | |
" button: event.button,\n", | |
" step: event.step,\n", | |
" guiEvent: simpleKeys(event),\n", | |
" });\n", | |
"\n", | |
" /* This prevents the web browser from automatically changing to\n", | |
" * the text insertion cursor when the button is pressed. We want\n", | |
" * to control all of the cursor setting manually through the\n", | |
" * 'cursor' event from matplotlib */\n", | |
" event.preventDefault();\n", | |
" return false;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._key_event_extra = function (_event, _name) {\n", | |
" // Handle any extra behaviour associated with a key event\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.key_event = function (event, name) {\n", | |
" // Prevent repeat events\n", | |
" if (name === 'key_press') {\n", | |
" if (event.which === this._key) {\n", | |
" return;\n", | |
" } else {\n", | |
" this._key = event.which;\n", | |
" }\n", | |
" }\n", | |
" if (name === 'key_release') {\n", | |
" this._key = null;\n", | |
" }\n", | |
"\n", | |
" var value = '';\n", | |
" if (event.ctrlKey && event.which !== 17) {\n", | |
" value += 'ctrl+';\n", | |
" }\n", | |
" if (event.altKey && event.which !== 18) {\n", | |
" value += 'alt+';\n", | |
" }\n", | |
" if (event.shiftKey && event.which !== 16) {\n", | |
" value += 'shift+';\n", | |
" }\n", | |
"\n", | |
" value += 'k';\n", | |
" value += event.which.toString();\n", | |
"\n", | |
" this._key_event_extra(event, name);\n", | |
"\n", | |
" this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n", | |
" return false;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.toolbar_button_onclick = function (name) {\n", | |
" if (name === 'download') {\n", | |
" this.handle_save(this, null);\n", | |
" } else {\n", | |
" this.send_message('toolbar_button', { name: name });\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n", | |
" this.message.textContent = tooltip;\n", | |
"};\n", | |
"\n", | |
"///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n", | |
"// prettier-ignore\n", | |
"var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\n", | |
"mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", | |
"\n", | |
"mpl.extensions = [\"eps\", \"jpeg\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n", | |
"\n", | |
"mpl.default_extension = \"png\";/* global mpl */\n", | |
"\n", | |
"var comm_websocket_adapter = function (comm) {\n", | |
" // Create a \"websocket\"-like object which calls the given IPython comm\n", | |
" // object with the appropriate methods. Currently this is a non binary\n", | |
" // socket, so there is still some room for performance tuning.\n", | |
" var ws = {};\n", | |
"\n", | |
" ws.close = function () {\n", | |
" comm.close();\n", | |
" };\n", | |
" ws.send = function (m) {\n", | |
" //console.log('sending', m);\n", | |
" comm.send(m);\n", | |
" };\n", | |
" // Register the callback with on_msg.\n", | |
" comm.on_msg(function (msg) {\n", | |
" //console.log('receiving', msg['content']['data'], msg);\n", | |
" // Pass the mpl event to the overridden (by mpl) onmessage function.\n", | |
" ws.onmessage(msg['content']['data']);\n", | |
" });\n", | |
" return ws;\n", | |
"};\n", | |
"\n", | |
"mpl.mpl_figure_comm = function (comm, msg) {\n", | |
" // This is the function which gets called when the mpl process\n", | |
" // starts-up an IPython Comm through the \"matplotlib\" channel.\n", | |
"\n", | |
" var id = msg.content.data.id;\n", | |
" // Get hold of the div created by the display call when the Comm\n", | |
" // socket was opened in Python.\n", | |
" var element = document.getElementById(id);\n", | |
" var ws_proxy = comm_websocket_adapter(comm);\n", | |
"\n", | |
" function ondownload(figure, _format) {\n", | |
" window.open(figure.canvas.toDataURL());\n", | |
" }\n", | |
"\n", | |
" var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n", | |
"\n", | |
" // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n", | |
" // web socket which is closed, not our websocket->open comm proxy.\n", | |
" ws_proxy.onopen();\n", | |
"\n", | |
" fig.parent_element = element;\n", | |
" fig.cell_info = mpl.find_output_cell(\"<div id='\" + id + \"'></div>\");\n", | |
" if (!fig.cell_info) {\n", | |
" console.error('Failed to find cell for figure', id, fig);\n", | |
" return;\n", | |
" }\n", | |
" fig.cell_info[0].output_area.element.on(\n", | |
" 'cleared',\n", | |
" { fig: fig },\n", | |
" fig._remove_fig_handler\n", | |
" );\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_close = function (fig, msg) {\n", | |
" var width = fig.canvas.width / fig.ratio;\n", | |
" fig.cell_info[0].output_area.element.off(\n", | |
" 'cleared',\n", | |
" fig._remove_fig_handler\n", | |
" );\n", | |
" fig.resizeObserverInstance.unobserve(fig.canvas_div);\n", | |
"\n", | |
" // Update the output cell to use the data from the current canvas.\n", | |
" fig.push_to_output();\n", | |
" var dataURL = fig.canvas.toDataURL();\n", | |
" // Re-enable the keyboard manager in IPython - without this line, in FF,\n", | |
" // the notebook keyboard shortcuts fail.\n", | |
" IPython.keyboard_manager.enable();\n", | |
" fig.parent_element.innerHTML =\n", | |
" '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n", | |
" fig.close_ws(fig, msg);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.close_ws = function (fig, msg) {\n", | |
" fig.send_message('closing', msg);\n", | |
" // fig.ws.close()\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.push_to_output = function (_remove_interactive) {\n", | |
" // Turn the data on the canvas into data in the output cell.\n", | |
" var width = this.canvas.width / this.ratio;\n", | |
" var dataURL = this.canvas.toDataURL();\n", | |
" this.cell_info[1]['text/html'] =\n", | |
" '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.updated_canvas_event = function () {\n", | |
" // Tell IPython that the notebook contents must change.\n", | |
" IPython.notebook.set_dirty(true);\n", | |
" this.send_message('ack', {});\n", | |
" var fig = this;\n", | |
" // Wait a second, then push the new image to the DOM so\n", | |
" // that it is saved nicely (might be nice to debounce this).\n", | |
" setTimeout(function () {\n", | |
" fig.push_to_output();\n", | |
" }, 1000);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_toolbar = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var toolbar = document.createElement('div');\n", | |
" toolbar.classList = 'btn-toolbar';\n", | |
" this.root.appendChild(toolbar);\n", | |
"\n", | |
" function on_click_closure(name) {\n", | |
" return function (_event) {\n", | |
" return fig.toolbar_button_onclick(name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" function on_mouseover_closure(tooltip) {\n", | |
" return function (event) {\n", | |
" if (!event.currentTarget.disabled) {\n", | |
" return fig.toolbar_button_onmouseover(tooltip);\n", | |
" }\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" fig.buttons = {};\n", | |
" var buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'btn-group';\n", | |
" var button;\n", | |
" for (var toolbar_ind in mpl.toolbar_items) {\n", | |
" var name = mpl.toolbar_items[toolbar_ind][0];\n", | |
" var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", | |
" var image = mpl.toolbar_items[toolbar_ind][2];\n", | |
" var method_name = mpl.toolbar_items[toolbar_ind][3];\n", | |
"\n", | |
" if (!name) {\n", | |
" /* Instead of a spacer, we start a new button group. */\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
" buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'btn-group';\n", | |
" continue;\n", | |
" }\n", | |
"\n", | |
" button = fig.buttons[name] = document.createElement('button');\n", | |
" button.classList = 'btn btn-default';\n", | |
" button.href = '#';\n", | |
" button.title = name;\n", | |
" button.innerHTML = '<i class=\"fa ' + image + ' fa-lg\"></i>';\n", | |
" button.addEventListener('click', on_click_closure(method_name));\n", | |
" button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", | |
" buttonGroup.appendChild(button);\n", | |
" }\n", | |
"\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
"\n", | |
" // Add the status bar.\n", | |
" var status_bar = document.createElement('span');\n", | |
" status_bar.classList = 'mpl-message pull-right';\n", | |
" toolbar.appendChild(status_bar);\n", | |
" this.message = status_bar;\n", | |
"\n", | |
" // Add the close button to the window.\n", | |
" var buttongrp = document.createElement('div');\n", | |
" buttongrp.classList = 'btn-group inline pull-right';\n", | |
" button = document.createElement('button');\n", | |
" button.classList = 'btn btn-mini btn-primary';\n", | |
" button.href = '#';\n", | |
" button.title = 'Stop Interaction';\n", | |
" button.innerHTML = '<i class=\"fa fa-power-off icon-remove icon-large\"></i>';\n", | |
" button.addEventListener('click', function (_evt) {\n", | |
" fig.handle_close(fig, {});\n", | |
" });\n", | |
" button.addEventListener(\n", | |
" 'mouseover',\n", | |
" on_mouseover_closure('Stop Interaction')\n", | |
" );\n", | |
" buttongrp.appendChild(button);\n", | |
" var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n", | |
" titlebar.insertBefore(buttongrp, titlebar.firstChild);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._remove_fig_handler = function (event) {\n", | |
" var fig = event.data.fig;\n", | |
" if (event.target !== this) {\n", | |
" // Ignore bubbled events from children.\n", | |
" return;\n", | |
" }\n", | |
" fig.close_ws(fig, {});\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._root_extra_style = function (el) {\n", | |
" el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._canvas_extra_style = function (el) {\n", | |
" // this is important to make the div 'focusable\n", | |
" el.setAttribute('tabindex', 0);\n", | |
" // reach out to IPython and tell the keyboard manager to turn it's self\n", | |
" // off when our div gets focus\n", | |
"\n", | |
" // location in version 3\n", | |
" if (IPython.notebook.keyboard_manager) {\n", | |
" IPython.notebook.keyboard_manager.register_events(el);\n", | |
" } else {\n", | |
" // location in version 2\n", | |
" IPython.keyboard_manager.register_events(el);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._key_event_extra = function (event, _name) {\n", | |
" var manager = IPython.notebook.keyboard_manager;\n", | |
" if (!manager) {\n", | |
" manager = IPython.keyboard_manager;\n", | |
" }\n", | |
"\n", | |
" // Check for shift+enter\n", | |
" if (event.shiftKey && event.which === 13) {\n", | |
" this.canvas_div.blur();\n", | |
" // select the cell after this one\n", | |
" var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", | |
" IPython.notebook.select(index + 1);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_save = function (fig, _msg) {\n", | |
" fig.ondownload(fig, null);\n", | |
"};\n", | |
"\n", | |
"mpl.find_output_cell = function (html_output) {\n", | |
" // Return the cell and output element which can be found *uniquely* in the notebook.\n", | |
" // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", | |
" // IPython event is triggered only after the cells have been serialised, which for\n", | |
" // our purposes (turning an active figure into a static one), is too late.\n", | |
" var cells = IPython.notebook.get_cells();\n", | |
" var ncells = cells.length;\n", | |
" for (var i = 0; i < ncells; i++) {\n", | |
" var cell = cells[i];\n", | |
" if (cell.cell_type === 'code') {\n", | |
" for (var j = 0; j < cell.output_area.outputs.length; j++) {\n", | |
" var data = cell.output_area.outputs[j];\n", | |
" if (data.data) {\n", | |
" // IPython >= 3 moved mimebundle to data attribute of output\n", | |
" data = data.data;\n", | |
" }\n", | |
" if (data['text/html'] === html_output) {\n", | |
" return [cell, data, j];\n", | |
" }\n", | |
" }\n", | |
" }\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"// Register the function which deals with the matplotlib target/channel.\n", | |
"// The kernel may be null if the page has been refreshed.\n", | |
"if (IPython.notebook.kernel !== null) {\n", | |
" IPython.notebook.kernel.comm_manager.register_target(\n", | |
" 'matplotlib',\n", | |
" mpl.mpl_figure_comm\n", | |
" );\n", | |
"}\n" | |
], | |
"text/plain": [ | |
"<IPython.core.display.Javascript object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
}, | |
{ | |
"data": { | |
"text/html": [ | |
"<img src=\"data:image/png;base64,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jRs3as2aNRo4cKAuvPDCwO2+sLBQAwYM0PDhw7V27Vo9//zzKioqknNOy5Yt08iRI1VWVqYNGzbommuuUWJiYrvhdGITJ05U37599dFHH53yuNPV9t/TIUOGaMmSJaqsrNRvfvMbScceQbznnnv09NNPa+vWrSovL1dhYaEGDhwY+PnaztGll16qRYsW6fnnn9fPf/5zfe5zn9OYMWMC36+zt3ui3hoDkGK+Mx2AJ96pP/XUU3LORe+QpY5fBNL2KMvxrVmzRs45PfXUU4HLV61aJeecNm3aFL3MOadQKKT6+vrAsW13bosWLQpc/rWvfU3OOT344IOBy7/whS9o1KhRHf7Mbf3hD3+Qc07f/e53A5c/8cQTcs6dcgD+9a9/lXNO//Iv/3LK73Gyp4Dbvl5BQcFJP3biAHTO6ac//Wng2OXLl8s5p1dffVVS5wegdOqngE8cgF39HWZmZurQoUPRyz788EPFxcVp5cqV7b5XR9fz5ptvDlzW9vufNWtW4PK23+F3vvOdwOU7duyQc0533nln4Gdyzum///u/o5cdOHBAffv2VXJycmDs/e53v5NzTg899NApr+uIESOij9T51Pbf03vuuee0x7a0tOijjz5Sampq4PbQdo5OPBf33XefnHPRv6Xsyu2eqLfGAKSY70wH4Jo1awLHvfnmm3LOqby8PHpZZwfgN7/5TaWmprb7e60///nPcs5pyZIl0cucc/r617/e7noe/xTg8d1xxx1yzrV7scm0adM0YMCADn/mtlavXt1uEEjSp59+qn79+p1yALa2tuqyyy7ToEGD9MADDygcDuvo0aPtvsfpBuCJg66j7yX9bQD+9a9/DRzbNvh+9KMfBf5zdw/Arv4Ob7zxxnZfMysrSwsWLGh3eUfX82QD8D//8z8Dl7f9Dl977bV2X+fyyy/XF7/4xcDPlJ2d3e647OxsfelLXwpc1tzcLOecbrvttlNe164MwNbWVn366acBbbX99/T3v/99u887fPiwfvCDH+iyyy5T3759A0+PH38+287Rxo0bA5+/ceNGOee0fft2SV273RP11hiAFPP96Ec/knNOH374YYcfHz58uMaPHx/9z23j48Q/XO9oWHR2AI4bN06XXXZZh9+/X79+mjNnTvQ/d/QIhvS3O7fXX389cPnJBm5nXjTRdm727dvX7mOZmZmnfQr4vffe00033aTMzEw555SRkaFFixYFHvk63QA88RG1k32v2bNnq1+/fu2OPXLkiJxzWrx4sSS7AdjV3+GJA07q/AsMTjUATxx6bb/Djl6AMW7cOA0dOjTwM51422y7XpMmTerU9TixrjwF3PZ7PV7buW+7Hf/lL39p93mTJ09WSkqKVq5cqc2bN+u1117T66+/roEDBwbO58n+O3Li7akrt3ui3hoDkGK+f//3f4/+cfmJtba2Kj09XdOnT49eZjEAv/nNb+q888476aNHt99+e/Syk93pWgxAn0cAT+ytt97Sj370I/Xt21fz58+PXn66AdjRK0R9HgH84IMPOnwEt+0pa59HAH1/h90xAE/8/Z/uEcBrrrkm8DN19wB84IEH5FznXgRy6NAhvf766wHNzc2STn47bmhoUJ8+fbR06dLA5U1NTerbt+8ZDUAeAaRYiAFIMd+f/vQn9enTRz/4wQ/afWzDhg3tRp3FAPzZz34WfbHB8bUNkMrKyuhlPTkAa2pqzvhvAE/WF77wBf3d3/1d9D+PGjVKV199dbvjznQAnuxvAF955RVJx0Z9UlJSu0dR165d224APvTQQ3LOqaampt11OHEAdsfv0GIAtv1pwi233BK4/LXXXpNzTnfddVfgZ+ruAdjQ0BB9G5iOHlGTpIqKilN+Denkt+PGxkY559r97eS//du/tbuNdnYAduV2T9RbYwASSVq0aJH69OmjefPm6Te/+Y2ef/553XvvvTrvvPM0evTo6KMQks0AbHsFaVpamh588EFVVlaqpKRE8fHxHb6CtKcGoCTNmDEjOpDbXg2Zk5Nz2lcB//73v9e1116rhx56SM8995y2bNmiu+66S3FxcYEXHsyePVuJiYkqLy/Xa6+9pl27dgW+XlcG4MleBfwP//APgc+fM2eOkpKS9MADD2jz5s1asWKF8vLy2g3Atu8zf/58VVVV6fXXX48+fX2yVwH7/A4tBqAkzZs3T3369NHixYv1/PPP62c/+5kuuOACXXjhhYFHTC0GoPS3N4IeOHCgli1bpk2bNumll17So48+qsLCwk69EfSp/la3oKBAGRkZevTRR1VZWakf/vCHys7OVv/+/c9oAEqdv90T9dYYgEQ69qjQI488otGjRyslJUUJCQnKzc3VkiVLdPjw4cCxFgNQOvZqywULFig7O1v9+vXTkCFDdMcdd5z0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H36ogoICzZ8/vyeu8jlRZwYg94XdGwOwh6urq5Nzrt1TMMuXL9ewYcM6/Jzc3FwtX748cNm2bdvknNP+/fvNrmtv70zO9Yndd999ysjI0J///GeLq3hOdCbn+e2339YFF1ygt956S5IYgJ3oTM7zddddp8TERE2aNEk7duzQs88+qyFDhujb3/52T1zlXtmZ/u/Gr371K5133nnq16+fnHOaMmUKo6QLdWYAcl/YvTEAe7i2/3GpqqoKXH7vvfdq+PDhHX5Obm6uVqxYEbjs1VdflXNOH3zwgdl17e2dybk+vnXr1iklJUWVlZVWV/GcqKvnuaWlRaNHjw788TYD8PSdye15woQJSkpKUkNDQ/SyiooK9enTh0cBT9KZnOc33nhD2dnZuu+++/T73/9eGzdu1MiRI3XTTTf1xFU+J+rsAOS+sPtiAPZwPAXcc/k8BVxeXq7k5GQ988wzllfxnKir5/ngwYNyzqlv375Rffr0iV62ZcuWnrrqvaozuT3PmjVLl112WeCympoaOef09ttvm13X3tyZnOcZM2bohhtuCFz2yiuv8MhUF+Ip4J6PAXgWuvrqq7Vw4cLAZZdffvkpXwRy+eWXBy5bsGABf/jaibp6rqVjj/wlJSV16i0J6FhdOc9Hjx5VJBIJWLhwoYYPH65IJKKPPvqop652r6urt+ef/exnSk5O1uHDh6OX/eY3v1FcXByPAJ6irp7nf/qnf9I3v/nNwGVVVVVyzqmurs7sep5LdfZFINwXdl8MwLNQ21sMrF27VjU1NVq8eLFSU1P13nvvSZJuv/12zZw5M3p820vfv/vd76qmpkZr167lpe+drKvnet26derXr58efvhhffDBB1HHP4VG7evqeT4xngLuXF09z4cPH9bgwYN1ww036I033tDWrVuVm5urOXPmnK0foVfU1fP8+OOPq1+/flq9erX27NmjV199VaNHj9bVV199tn6EXtHhw4e1c+dO7dy5U845Pfjgg9q5c2f07Xa4L7SNAXiWevjhhzVkyBAlJCRo1KhR2rp1a/Rjs2fPVmFhYeD4l156SVdddZUSEhJ08cUX8+aXXagr57qwsFDOuXZmz57d81e8l9XV2/TxMQA7X1fP8x/+8AeNHz9eycnJGjx4sL73ve/x6F8n6up5fuihh/T5z39eycnJys7O1vTp07Vv374evta9qxdffPGU/3vLfaFtDEAiIiKiGIsBSERERBRjMQCJiIiIYiwGIBEREVGMxQAkIiIiirEYgEREREQxFgOQiIiIKMZiABIRERHFWAxAIiIiohiLAUhEREQUYzEAiYiIiGIsBiARERFRjMUAJCIiIoqxGIBEREREMRYDkIiIiCjGYgASERERxVgMQCIiIqIYiwFIREREFGMxAImIiIhiLAYgERERUYzFACQiIiKKsRiARERERDEWA5CIiIgoxmIAEhEREcVYDEAiIiKiGIsBSERERBRjMQCJiIiIYiwGIBEREVGMxQAkIiIiirEYgEREREQxFgOQiIiIKMZiABIRERHFWP8PGHdtgmBr6VsAAAAASUVORK5CYII=\" width=\"640\">" | |
], | |
"text/plain": [ | |
"<IPython.core.display.HTML object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"print(\"Using the C-rand function from Cython\")\n", | |
"%timeit cython_uniform_rand(shape)\n", | |
"fig, ax = subplots()\n", | |
"ax.hist(cython_uniform_rand(shape).ravel())\n", | |
"ax.set_title(\"Uniform distribution from C-rand\")\n", | |
"pass" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"The distribution looks OK but the C-rand function is almost **twice slower** than the equivalent numpy function !\n", | |
"This explains why rewriting the function in cython with C-rand is actually slower than the initial numpy implementation (which was 5 lines long!).\n", | |
"\n", | |
"\n", | |
"## C++ bound with Cython\n", | |
"\n", | |
"A friend of mind advertised C++, a great programming language which standard library implements many random number generator, most of them of much better quality than the one from C. Moreover, the normal distribution is directly available which makes it a great candidate for my problem and created great hopes for faster processing.\n", | |
"\n", | |
"I started coding on the examples provided and discovered the PRNG-feature requires a fairly recent compiler, compatible with C++-11. This is the first cold shower, I nevertheless offered C++ a try:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 7, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"%%cython\n", | |
"# distutils: language = c++\n", | |
"# distutils: extra_compile_args = -std=c++11\n", | |
"# cython: boundscheck=False\n", | |
"# cython: cdivision=True\n", | |
"# cython: wraparound=False\n", | |
"\n", | |
"import cython\n", | |
"import numpy\n", | |
"import time\n", | |
"cdef extern from \"<random>\" namespace \"std\":\n", | |
" cdef cppclass mt19937:\n", | |
" mt19937() nogil# we need to define this constructor to stack allocate classes in Cython\n", | |
" mt19937(unsigned int seed) # not worrying about matching the exact int type for seed\n", | |
" cdef cppclass mt19937_64:\n", | |
" mt19937_64() nogil\n", | |
" mt19937_64(unsigned long long seed)\n", | |
" cdef cppclass uniform_real_distribution[T]:\n", | |
" uniform_real_distribution()\n", | |
" uniform_real_distribution(T a, T b)\n", | |
" T operator()(mt19937 gen) nogil # ignore the possibility of using other classes for \"gen\"\n", | |
" T operator()(mt19937_64 gen) nogil # ignore the possibility of using other classes for \"gen\"\n", | |
" cdef cppclass normal_distribution[T]:\n", | |
" normal_distribution() nogil\n", | |
" normal_distribution(T a, T b) nogil\n", | |
" T operator()(mt19937 gen) nogil # ignore the possibility of using other classes for \"gen\"\n", | |
" T operator()(mt19937_64 gen) nogil # ignore the possibility of using other classes for \"gen\"\n", | |
" \n", | |
"def test():\n", | |
" cdef:\n", | |
" mt19937 gen = mt19937(5)\n", | |
" uniform_real_distribution[double] dist = uniform_real_distribution[double](0.0,1.0)\n", | |
" return dist(gen)\n", | |
"\n", | |
"def cython_uniform_cpp(shape):\n", | |
" cdef: \n", | |
" Py_ssize_t size = numpy.prod(shape), idx\n", | |
" double[::1] ary = numpy.empty(size)\n", | |
" mt19937 gen = mt19937(time.time_ns()&((1<<32)-1))\n", | |
" uniform_real_distribution[double] dist = uniform_real_distribution[double](0.0, 1.0)\n", | |
" with nogil:\n", | |
" for idx in range(size):\n", | |
" ary[idx] = dist(gen)\n", | |
" return numpy.asarray(ary).reshape(shape)\n", | |
"\n", | |
"def cython_uniform64_cpp(shape):\n", | |
" cdef: \n", | |
" Py_ssize_t size = numpy.prod(shape), idx\n", | |
" double[::1] ary = numpy.empty(size)\n", | |
" mt19937_64 gen = mt19937_64(time.time_ns())\n", | |
" uniform_real_distribution[double] dist = uniform_real_distribution[double](0.0,1.0)\n", | |
" with nogil:\n", | |
" for idx in range(size):\n", | |
" ary[idx] = dist(gen)\n", | |
" return numpy.asarray(ary).reshape(shape)\n", | |
"\n", | |
"def cython_normal_cpp(mu, sigma):\n", | |
" shape = mu.shape\n", | |
" assert sigma.shape == shape\n", | |
" cdef: \n", | |
" Py_ssize_t size = numpy.prod(shape), idx\n", | |
" double[::1] ary = numpy.empty(size)\n", | |
" double[::1] cmu = numpy.ascontiguousarray(mu).ravel()\n", | |
" double[::1] csigma = numpy.ascontiguousarray(sigma).ravel()\n", | |
" mt19937 gen = mt19937(time.time_ns()&((1<<32)-1))\n", | |
" normal_distribution[double] dist\n", | |
" with nogil:\n", | |
" for idx in range(size):\n", | |
" dist = normal_distribution[double](cmu[idx], csigma[idx])\n", | |
" ary[idx] = dist(gen)\n", | |
" return numpy.asarray(ary).reshape(shape)\n", | |
"\n", | |
"def cython_normal64_cpp(mu, sigma):\n", | |
" shape = mu.shape\n", | |
" assert sigma.shape == shape\n", | |
" cdef: \n", | |
" Py_ssize_t size = numpy.prod(shape), idx\n", | |
" double[::1] ary = numpy.empty(size)\n", | |
" double[::1] cmu = numpy.ascontiguousarray(mu).ravel()\n", | |
" double[::1] csigma = numpy.ascontiguousarray(sigma).ravel()\n", | |
" mt19937_64 gen = mt19937_64(time.time_ns())\n", | |
" normal_distribution[double] dist\n", | |
" with nogil:\n", | |
" for idx in range(size):\n", | |
" dist = normal_distribution[double](cmu[idx], csigma[idx])\n", | |
" ary[idx] = dist(gen)\n", | |
" return numpy.asarray(ary).reshape(shape)" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 8, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"Execution time from C++:\n", | |
"Uniform distribution, 32 and 64 bits versions of Mersenne twisters:\n", | |
"77.6 ms ± 328 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", | |
"65.3 ms ± 25.8 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", | |
"Normal distribution, 32 and 64 bits versions of Mersenne twisters:\n", | |
"311 ms ± 596 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", | |
"240 ms ± 888 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" | |
] | |
}, | |
{ | |
"data": { | |
"application/javascript": [ | |
"/* Put everything inside the global mpl namespace */\n", | |
"/* global mpl */\n", | |
"window.mpl = {};\n", | |
"\n", | |
"mpl.get_websocket_type = function () {\n", | |
" if (typeof WebSocket !== 'undefined') {\n", | |
" return WebSocket;\n", | |
" } else if (typeof MozWebSocket !== 'undefined') {\n", | |
" return MozWebSocket;\n", | |
" } else {\n", | |
" alert(\n", | |
" 'Your browser does not have WebSocket support. ' +\n", | |
" 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", | |
" 'Firefox 4 and 5 are also supported but you ' +\n", | |
" 'have to enable WebSockets in about:config.'\n", | |
" );\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n", | |
" this.id = figure_id;\n", | |
"\n", | |
" this.ws = websocket;\n", | |
"\n", | |
" this.supports_binary = this.ws.binaryType !== undefined;\n", | |
"\n", | |
" if (!this.supports_binary) {\n", | |
" var warnings = document.getElementById('mpl-warnings');\n", | |
" if (warnings) {\n", | |
" warnings.style.display = 'block';\n", | |
" warnings.textContent =\n", | |
" 'This browser does not support binary websocket messages. ' +\n", | |
" 'Performance may be slow.';\n", | |
" }\n", | |
" }\n", | |
"\n", | |
" this.imageObj = new Image();\n", | |
"\n", | |
" this.context = undefined;\n", | |
" this.message = undefined;\n", | |
" this.canvas = undefined;\n", | |
" this.rubberband_canvas = undefined;\n", | |
" this.rubberband_context = undefined;\n", | |
" this.format_dropdown = undefined;\n", | |
"\n", | |
" this.image_mode = 'full';\n", | |
"\n", | |
" this.root = document.createElement('div');\n", | |
" this.root.setAttribute('style', 'display: inline-block');\n", | |
" this._root_extra_style(this.root);\n", | |
"\n", | |
" parent_element.appendChild(this.root);\n", | |
"\n", | |
" this._init_header(this);\n", | |
" this._init_canvas(this);\n", | |
" this._init_toolbar(this);\n", | |
"\n", | |
" var fig = this;\n", | |
"\n", | |
" this.waiting = false;\n", | |
"\n", | |
" this.ws.onopen = function () {\n", | |
" fig.send_message('supports_binary', { value: fig.supports_binary });\n", | |
" fig.send_message('send_image_mode', {});\n", | |
" if (fig.ratio !== 1) {\n", | |
" fig.send_message('set_dpi_ratio', { dpi_ratio: fig.ratio });\n", | |
" }\n", | |
" fig.send_message('refresh', {});\n", | |
" };\n", | |
"\n", | |
" this.imageObj.onload = function () {\n", | |
" if (fig.image_mode === 'full') {\n", | |
" // Full images could contain transparency (where diff images\n", | |
" // almost always do), so we need to clear the canvas so that\n", | |
" // there is no ghosting.\n", | |
" fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", | |
" }\n", | |
" fig.context.drawImage(fig.imageObj, 0, 0);\n", | |
" };\n", | |
"\n", | |
" this.imageObj.onunload = function () {\n", | |
" fig.ws.close();\n", | |
" };\n", | |
"\n", | |
" this.ws.onmessage = this._make_on_message_function(this);\n", | |
"\n", | |
" this.ondownload = ondownload;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_header = function () {\n", | |
" var titlebar = document.createElement('div');\n", | |
" titlebar.classList =\n", | |
" 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n", | |
" var titletext = document.createElement('div');\n", | |
" titletext.classList = 'ui-dialog-title';\n", | |
" titletext.setAttribute(\n", | |
" 'style',\n", | |
" 'width: 100%; text-align: center; padding: 3px;'\n", | |
" );\n", | |
" titlebar.appendChild(titletext);\n", | |
" this.root.appendChild(titlebar);\n", | |
" this.header = titletext;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n", | |
"\n", | |
"mpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n", | |
"\n", | |
"mpl.figure.prototype._init_canvas = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var canvas_div = (this.canvas_div = document.createElement('div'));\n", | |
" canvas_div.setAttribute(\n", | |
" 'style',\n", | |
" 'border: 1px solid #ddd;' +\n", | |
" 'box-sizing: content-box;' +\n", | |
" 'clear: both;' +\n", | |
" 'min-height: 1px;' +\n", | |
" 'min-width: 1px;' +\n", | |
" 'outline: 0;' +\n", | |
" 'overflow: hidden;' +\n", | |
" 'position: relative;' +\n", | |
" 'resize: both;'\n", | |
" );\n", | |
"\n", | |
" function on_keyboard_event_closure(name) {\n", | |
" return function (event) {\n", | |
" return fig.key_event(event, name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" canvas_div.addEventListener(\n", | |
" 'keydown',\n", | |
" on_keyboard_event_closure('key_press')\n", | |
" );\n", | |
" canvas_div.addEventListener(\n", | |
" 'keyup',\n", | |
" on_keyboard_event_closure('key_release')\n", | |
" );\n", | |
"\n", | |
" this._canvas_extra_style(canvas_div);\n", | |
" this.root.appendChild(canvas_div);\n", | |
"\n", | |
" var canvas = (this.canvas = document.createElement('canvas'));\n", | |
" canvas.classList.add('mpl-canvas');\n", | |
" canvas.setAttribute('style', 'box-sizing: content-box;');\n", | |
"\n", | |
" this.context = canvas.getContext('2d');\n", | |
"\n", | |
" var backingStore =\n", | |
" this.context.backingStorePixelRatio ||\n", | |
" this.context.webkitBackingStorePixelRatio ||\n", | |
" this.context.mozBackingStorePixelRatio ||\n", | |
" this.context.msBackingStorePixelRatio ||\n", | |
" this.context.oBackingStorePixelRatio ||\n", | |
" this.context.backingStorePixelRatio ||\n", | |
" 1;\n", | |
"\n", | |
" this.ratio = (window.devicePixelRatio || 1) / backingStore;\n", | |
"\n", | |
" var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n", | |
" 'canvas'\n", | |
" ));\n", | |
" rubberband_canvas.setAttribute(\n", | |
" 'style',\n", | |
" 'box-sizing: content-box; position: absolute; left: 0; top: 0; z-index: 1;'\n", | |
" );\n", | |
"\n", | |
" // Apply a ponyfill if ResizeObserver is not implemented by browser.\n", | |
" if (this.ResizeObserver === undefined) {\n", | |
" if (window.ResizeObserver !== undefined) {\n", | |
" this.ResizeObserver = window.ResizeObserver;\n", | |
" } else {\n", | |
" var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n", | |
" this.ResizeObserver = obs.ResizeObserver;\n", | |
" }\n", | |
" }\n", | |
"\n", | |
" this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n", | |
" var nentries = entries.length;\n", | |
" for (var i = 0; i < nentries; i++) {\n", | |
" var entry = entries[i];\n", | |
" var width, height;\n", | |
" if (entry.contentBoxSize) {\n", | |
" if (entry.contentBoxSize instanceof Array) {\n", | |
" // Chrome 84 implements new version of spec.\n", | |
" width = entry.contentBoxSize[0].inlineSize;\n", | |
" height = entry.contentBoxSize[0].blockSize;\n", | |
" } else {\n", | |
" // Firefox implements old version of spec.\n", | |
" width = entry.contentBoxSize.inlineSize;\n", | |
" height = entry.contentBoxSize.blockSize;\n", | |
" }\n", | |
" } else {\n", | |
" // Chrome <84 implements even older version of spec.\n", | |
" width = entry.contentRect.width;\n", | |
" height = entry.contentRect.height;\n", | |
" }\n", | |
"\n", | |
" // Keep the size of the canvas and rubber band canvas in sync with\n", | |
" // the canvas container.\n", | |
" if (entry.devicePixelContentBoxSize) {\n", | |
" // Chrome 84 implements new version of spec.\n", | |
" canvas.setAttribute(\n", | |
" 'width',\n", | |
" entry.devicePixelContentBoxSize[0].inlineSize\n", | |
" );\n", | |
" canvas.setAttribute(\n", | |
" 'height',\n", | |
" entry.devicePixelContentBoxSize[0].blockSize\n", | |
" );\n", | |
" } else {\n", | |
" canvas.setAttribute('width', width * fig.ratio);\n", | |
" canvas.setAttribute('height', height * fig.ratio);\n", | |
" }\n", | |
" canvas.setAttribute(\n", | |
" 'style',\n", | |
" 'width: ' + width + 'px; height: ' + height + 'px;'\n", | |
" );\n", | |
"\n", | |
" rubberband_canvas.setAttribute('width', width);\n", | |
" rubberband_canvas.setAttribute('height', height);\n", | |
"\n", | |
" // And update the size in Python. We ignore the initial 0/0 size\n", | |
" // that occurs as the element is placed into the DOM, which should\n", | |
" // otherwise not happen due to the minimum size styling.\n", | |
" if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n", | |
" fig.request_resize(width, height);\n", | |
" }\n", | |
" }\n", | |
" });\n", | |
" this.resizeObserverInstance.observe(canvas_div);\n", | |
"\n", | |
" function on_mouse_event_closure(name) {\n", | |
" return function (event) {\n", | |
" return fig.mouse_event(event, name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mousedown',\n", | |
" on_mouse_event_closure('button_press')\n", | |
" );\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseup',\n", | |
" on_mouse_event_closure('button_release')\n", | |
" );\n", | |
" // Throttle sequential mouse events to 1 every 20ms.\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mousemove',\n", | |
" on_mouse_event_closure('motion_notify')\n", | |
" );\n", | |
"\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseenter',\n", | |
" on_mouse_event_closure('figure_enter')\n", | |
" );\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseleave',\n", | |
" on_mouse_event_closure('figure_leave')\n", | |
" );\n", | |
"\n", | |
" canvas_div.addEventListener('wheel', function (event) {\n", | |
" if (event.deltaY < 0) {\n", | |
" event.step = 1;\n", | |
" } else {\n", | |
" event.step = -1;\n", | |
" }\n", | |
" on_mouse_event_closure('scroll')(event);\n", | |
" });\n", | |
"\n", | |
" canvas_div.appendChild(canvas);\n", | |
" canvas_div.appendChild(rubberband_canvas);\n", | |
"\n", | |
" this.rubberband_context = rubberband_canvas.getContext('2d');\n", | |
" this.rubberband_context.strokeStyle = '#000000';\n", | |
"\n", | |
" this._resize_canvas = function (width, height, forward) {\n", | |
" if (forward) {\n", | |
" canvas_div.style.width = width + 'px';\n", | |
" canvas_div.style.height = height + 'px';\n", | |
" }\n", | |
" };\n", | |
"\n", | |
" // Disable right mouse context menu.\n", | |
" this.rubberband_canvas.addEventListener('contextmenu', function (_e) {\n", | |
" event.preventDefault();\n", | |
" return false;\n", | |
" });\n", | |
"\n", | |
" function set_focus() {\n", | |
" canvas.focus();\n", | |
" canvas_div.focus();\n", | |
" }\n", | |
"\n", | |
" window.setTimeout(set_focus, 100);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_toolbar = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var toolbar = document.createElement('div');\n", | |
" toolbar.classList = 'mpl-toolbar';\n", | |
" this.root.appendChild(toolbar);\n", | |
"\n", | |
" function on_click_closure(name) {\n", | |
" return function (_event) {\n", | |
" return fig.toolbar_button_onclick(name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" function on_mouseover_closure(tooltip) {\n", | |
" return function (event) {\n", | |
" if (!event.currentTarget.disabled) {\n", | |
" return fig.toolbar_button_onmouseover(tooltip);\n", | |
" }\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" fig.buttons = {};\n", | |
" var buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'mpl-button-group';\n", | |
" for (var toolbar_ind in mpl.toolbar_items) {\n", | |
" var name = mpl.toolbar_items[toolbar_ind][0];\n", | |
" var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", | |
" var image = mpl.toolbar_items[toolbar_ind][2];\n", | |
" var method_name = mpl.toolbar_items[toolbar_ind][3];\n", | |
"\n", | |
" if (!name) {\n", | |
" /* Instead of a spacer, we start a new button group. */\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
" buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'mpl-button-group';\n", | |
" continue;\n", | |
" }\n", | |
"\n", | |
" var button = (fig.buttons[name] = document.createElement('button'));\n", | |
" button.classList = 'mpl-widget';\n", | |
" button.setAttribute('role', 'button');\n", | |
" button.setAttribute('aria-disabled', 'false');\n", | |
" button.addEventListener('click', on_click_closure(method_name));\n", | |
" button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", | |
"\n", | |
" var icon_img = document.createElement('img');\n", | |
" icon_img.src = '_images/' + image + '.png';\n", | |
" icon_img.srcset = '_images/' + image + '_large.png 2x';\n", | |
" icon_img.alt = tooltip;\n", | |
" button.appendChild(icon_img);\n", | |
"\n", | |
" buttonGroup.appendChild(button);\n", | |
" }\n", | |
"\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
"\n", | |
" var fmt_picker = document.createElement('select');\n", | |
" fmt_picker.classList = 'mpl-widget';\n", | |
" toolbar.appendChild(fmt_picker);\n", | |
" this.format_dropdown = fmt_picker;\n", | |
"\n", | |
" for (var ind in mpl.extensions) {\n", | |
" var fmt = mpl.extensions[ind];\n", | |
" var option = document.createElement('option');\n", | |
" option.selected = fmt === mpl.default_extension;\n", | |
" option.innerHTML = fmt;\n", | |
" fmt_picker.appendChild(option);\n", | |
" }\n", | |
"\n", | |
" var status_bar = document.createElement('span');\n", | |
" status_bar.classList = 'mpl-message';\n", | |
" toolbar.appendChild(status_bar);\n", | |
" this.message = status_bar;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n", | |
" // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n", | |
" // which will in turn request a refresh of the image.\n", | |
" this.send_message('resize', { width: x_pixels, height: y_pixels });\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.send_message = function (type, properties) {\n", | |
" properties['type'] = type;\n", | |
" properties['figure_id'] = this.id;\n", | |
" this.ws.send(JSON.stringify(properties));\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.send_draw_message = function () {\n", | |
" if (!this.waiting) {\n", | |
" this.waiting = true;\n", | |
" this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_save = function (fig, _msg) {\n", | |
" var format_dropdown = fig.format_dropdown;\n", | |
" var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n", | |
" fig.ondownload(fig, format);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_resize = function (fig, msg) {\n", | |
" var size = msg['size'];\n", | |
" if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n", | |
" fig._resize_canvas(size[0], size[1], msg['forward']);\n", | |
" fig.send_message('refresh', {});\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_rubberband = function (fig, msg) {\n", | |
" var x0 = msg['x0'] / fig.ratio;\n", | |
" var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n", | |
" var x1 = msg['x1'] / fig.ratio;\n", | |
" var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n", | |
" x0 = Math.floor(x0) + 0.5;\n", | |
" y0 = Math.floor(y0) + 0.5;\n", | |
" x1 = Math.floor(x1) + 0.5;\n", | |
" y1 = Math.floor(y1) + 0.5;\n", | |
" var min_x = Math.min(x0, x1);\n", | |
" var min_y = Math.min(y0, y1);\n", | |
" var width = Math.abs(x1 - x0);\n", | |
" var height = Math.abs(y1 - y0);\n", | |
"\n", | |
" fig.rubberband_context.clearRect(\n", | |
" 0,\n", | |
" 0,\n", | |
" fig.canvas.width / fig.ratio,\n", | |
" fig.canvas.height / fig.ratio\n", | |
" );\n", | |
"\n", | |
" fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_figure_label = function (fig, msg) {\n", | |
" // Updates the figure title.\n", | |
" fig.header.textContent = msg['label'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_cursor = function (fig, msg) {\n", | |
" var cursor = msg['cursor'];\n", | |
" switch (cursor) {\n", | |
" case 0:\n", | |
" cursor = 'pointer';\n", | |
" break;\n", | |
" case 1:\n", | |
" cursor = 'default';\n", | |
" break;\n", | |
" case 2:\n", | |
" cursor = 'crosshair';\n", | |
" break;\n", | |
" case 3:\n", | |
" cursor = 'move';\n", | |
" break;\n", | |
" }\n", | |
" fig.rubberband_canvas.style.cursor = cursor;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_message = function (fig, msg) {\n", | |
" fig.message.textContent = msg['message'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_draw = function (fig, _msg) {\n", | |
" // Request the server to send over a new figure.\n", | |
" fig.send_draw_message();\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_image_mode = function (fig, msg) {\n", | |
" fig.image_mode = msg['mode'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n", | |
" for (var key in msg) {\n", | |
" if (!(key in fig.buttons)) {\n", | |
" continue;\n", | |
" }\n", | |
" fig.buttons[key].disabled = !msg[key];\n", | |
" fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n", | |
" if (msg['mode'] === 'PAN') {\n", | |
" fig.buttons['Pan'].classList.add('active');\n", | |
" fig.buttons['Zoom'].classList.remove('active');\n", | |
" } else if (msg['mode'] === 'ZOOM') {\n", | |
" fig.buttons['Pan'].classList.remove('active');\n", | |
" fig.buttons['Zoom'].classList.add('active');\n", | |
" } else {\n", | |
" fig.buttons['Pan'].classList.remove('active');\n", | |
" fig.buttons['Zoom'].classList.remove('active');\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.updated_canvas_event = function () {\n", | |
" // Called whenever the canvas gets updated.\n", | |
" this.send_message('ack', {});\n", | |
"};\n", | |
"\n", | |
"// A function to construct a web socket function for onmessage handling.\n", | |
"// Called in the figure constructor.\n", | |
"mpl.figure.prototype._make_on_message_function = function (fig) {\n", | |
" return function socket_on_message(evt) {\n", | |
" if (evt.data instanceof Blob) {\n", | |
" /* FIXME: We get \"Resource interpreted as Image but\n", | |
" * transferred with MIME type text/plain:\" errors on\n", | |
" * Chrome. But how to set the MIME type? It doesn't seem\n", | |
" * to be part of the websocket stream */\n", | |
" evt.data.type = 'image/png';\n", | |
"\n", | |
" /* Free the memory for the previous frames */\n", | |
" if (fig.imageObj.src) {\n", | |
" (window.URL || window.webkitURL).revokeObjectURL(\n", | |
" fig.imageObj.src\n", | |
" );\n", | |
" }\n", | |
"\n", | |
" fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n", | |
" evt.data\n", | |
" );\n", | |
" fig.updated_canvas_event();\n", | |
" fig.waiting = false;\n", | |
" return;\n", | |
" } else if (\n", | |
" typeof evt.data === 'string' &&\n", | |
" evt.data.slice(0, 21) === 'data:image/png;base64'\n", | |
" ) {\n", | |
" fig.imageObj.src = evt.data;\n", | |
" fig.updated_canvas_event();\n", | |
" fig.waiting = false;\n", | |
" return;\n", | |
" }\n", | |
"\n", | |
" var msg = JSON.parse(evt.data);\n", | |
" var msg_type = msg['type'];\n", | |
"\n", | |
" // Call the \"handle_{type}\" callback, which takes\n", | |
" // the figure and JSON message as its only arguments.\n", | |
" try {\n", | |
" var callback = fig['handle_' + msg_type];\n", | |
" } catch (e) {\n", | |
" console.log(\n", | |
" \"No handler for the '\" + msg_type + \"' message type: \",\n", | |
" msg\n", | |
" );\n", | |
" return;\n", | |
" }\n", | |
"\n", | |
" if (callback) {\n", | |
" try {\n", | |
" // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n", | |
" callback(fig, msg);\n", | |
" } catch (e) {\n", | |
" console.log(\n", | |
" \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n", | |
" e,\n", | |
" e.stack,\n", | |
" msg\n", | |
" );\n", | |
" }\n", | |
" }\n", | |
" };\n", | |
"};\n", | |
"\n", | |
"// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n", | |
"mpl.findpos = function (e) {\n", | |
" //this section is from http://www.quirksmode.org/js/events_properties.html\n", | |
" var targ;\n", | |
" if (!e) {\n", | |
" e = window.event;\n", | |
" }\n", | |
" if (e.target) {\n", | |
" targ = e.target;\n", | |
" } else if (e.srcElement) {\n", | |
" targ = e.srcElement;\n", | |
" }\n", | |
" if (targ.nodeType === 3) {\n", | |
" // defeat Safari bug\n", | |
" targ = targ.parentNode;\n", | |
" }\n", | |
"\n", | |
" // pageX,Y are the mouse positions relative to the document\n", | |
" var boundingRect = targ.getBoundingClientRect();\n", | |
" var x = e.pageX - (boundingRect.left + document.body.scrollLeft);\n", | |
" var y = e.pageY - (boundingRect.top + document.body.scrollTop);\n", | |
"\n", | |
" return { x: x, y: y };\n", | |
"};\n", | |
"\n", | |
"/*\n", | |
" * return a copy of an object with only non-object keys\n", | |
" * we need this to avoid circular references\n", | |
" * http://stackoverflow.com/a/24161582/3208463\n", | |
" */\n", | |
"function simpleKeys(original) {\n", | |
" return Object.keys(original).reduce(function (obj, key) {\n", | |
" if (typeof original[key] !== 'object') {\n", | |
" obj[key] = original[key];\n", | |
" }\n", | |
" return obj;\n", | |
" }, {});\n", | |
"}\n", | |
"\n", | |
"mpl.figure.prototype.mouse_event = function (event, name) {\n", | |
" var canvas_pos = mpl.findpos(event);\n", | |
"\n", | |
" if (name === 'button_press') {\n", | |
" this.canvas.focus();\n", | |
" this.canvas_div.focus();\n", | |
" }\n", | |
"\n", | |
" var x = canvas_pos.x * this.ratio;\n", | |
" var y = canvas_pos.y * this.ratio;\n", | |
"\n", | |
" this.send_message(name, {\n", | |
" x: x,\n", | |
" y: y,\n", | |
" button: event.button,\n", | |
" step: event.step,\n", | |
" guiEvent: simpleKeys(event),\n", | |
" });\n", | |
"\n", | |
" /* This prevents the web browser from automatically changing to\n", | |
" * the text insertion cursor when the button is pressed. We want\n", | |
" * to control all of the cursor setting manually through the\n", | |
" * 'cursor' event from matplotlib */\n", | |
" event.preventDefault();\n", | |
" return false;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._key_event_extra = function (_event, _name) {\n", | |
" // Handle any extra behaviour associated with a key event\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.key_event = function (event, name) {\n", | |
" // Prevent repeat events\n", | |
" if (name === 'key_press') {\n", | |
" if (event.which === this._key) {\n", | |
" return;\n", | |
" } else {\n", | |
" this._key = event.which;\n", | |
" }\n", | |
" }\n", | |
" if (name === 'key_release') {\n", | |
" this._key = null;\n", | |
" }\n", | |
"\n", | |
" var value = '';\n", | |
" if (event.ctrlKey && event.which !== 17) {\n", | |
" value += 'ctrl+';\n", | |
" }\n", | |
" if (event.altKey && event.which !== 18) {\n", | |
" value += 'alt+';\n", | |
" }\n", | |
" if (event.shiftKey && event.which !== 16) {\n", | |
" value += 'shift+';\n", | |
" }\n", | |
"\n", | |
" value += 'k';\n", | |
" value += event.which.toString();\n", | |
"\n", | |
" this._key_event_extra(event, name);\n", | |
"\n", | |
" this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n", | |
" return false;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.toolbar_button_onclick = function (name) {\n", | |
" if (name === 'download') {\n", | |
" this.handle_save(this, null);\n", | |
" } else {\n", | |
" this.send_message('toolbar_button', { name: name });\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n", | |
" this.message.textContent = tooltip;\n", | |
"};\n", | |
"\n", | |
"///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n", | |
"// prettier-ignore\n", | |
"var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\n", | |
"mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", | |
"\n", | |
"mpl.extensions = [\"eps\", \"jpeg\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n", | |
"\n", | |
"mpl.default_extension = \"png\";/* global mpl */\n", | |
"\n", | |
"var comm_websocket_adapter = function (comm) {\n", | |
" // Create a \"websocket\"-like object which calls the given IPython comm\n", | |
" // object with the appropriate methods. Currently this is a non binary\n", | |
" // socket, so there is still some room for performance tuning.\n", | |
" var ws = {};\n", | |
"\n", | |
" ws.close = function () {\n", | |
" comm.close();\n", | |
" };\n", | |
" ws.send = function (m) {\n", | |
" //console.log('sending', m);\n", | |
" comm.send(m);\n", | |
" };\n", | |
" // Register the callback with on_msg.\n", | |
" comm.on_msg(function (msg) {\n", | |
" //console.log('receiving', msg['content']['data'], msg);\n", | |
" // Pass the mpl event to the overridden (by mpl) onmessage function.\n", | |
" ws.onmessage(msg['content']['data']);\n", | |
" });\n", | |
" return ws;\n", | |
"};\n", | |
"\n", | |
"mpl.mpl_figure_comm = function (comm, msg) {\n", | |
" // This is the function which gets called when the mpl process\n", | |
" // starts-up an IPython Comm through the \"matplotlib\" channel.\n", | |
"\n", | |
" var id = msg.content.data.id;\n", | |
" // Get hold of the div created by the display call when the Comm\n", | |
" // socket was opened in Python.\n", | |
" var element = document.getElementById(id);\n", | |
" var ws_proxy = comm_websocket_adapter(comm);\n", | |
"\n", | |
" function ondownload(figure, _format) {\n", | |
" window.open(figure.canvas.toDataURL());\n", | |
" }\n", | |
"\n", | |
" var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n", | |
"\n", | |
" // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n", | |
" // web socket which is closed, not our websocket->open comm proxy.\n", | |
" ws_proxy.onopen();\n", | |
"\n", | |
" fig.parent_element = element;\n", | |
" fig.cell_info = mpl.find_output_cell(\"<div id='\" + id + \"'></div>\");\n", | |
" if (!fig.cell_info) {\n", | |
" console.error('Failed to find cell for figure', id, fig);\n", | |
" return;\n", | |
" }\n", | |
" fig.cell_info[0].output_area.element.on(\n", | |
" 'cleared',\n", | |
" { fig: fig },\n", | |
" fig._remove_fig_handler\n", | |
" );\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_close = function (fig, msg) {\n", | |
" var width = fig.canvas.width / fig.ratio;\n", | |
" fig.cell_info[0].output_area.element.off(\n", | |
" 'cleared',\n", | |
" fig._remove_fig_handler\n", | |
" );\n", | |
" fig.resizeObserverInstance.unobserve(fig.canvas_div);\n", | |
"\n", | |
" // Update the output cell to use the data from the current canvas.\n", | |
" fig.push_to_output();\n", | |
" var dataURL = fig.canvas.toDataURL();\n", | |
" // Re-enable the keyboard manager in IPython - without this line, in FF,\n", | |
" // the notebook keyboard shortcuts fail.\n", | |
" IPython.keyboard_manager.enable();\n", | |
" fig.parent_element.innerHTML =\n", | |
" '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n", | |
" fig.close_ws(fig, msg);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.close_ws = function (fig, msg) {\n", | |
" fig.send_message('closing', msg);\n", | |
" // fig.ws.close()\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.push_to_output = function (_remove_interactive) {\n", | |
" // Turn the data on the canvas into data in the output cell.\n", | |
" var width = this.canvas.width / this.ratio;\n", | |
" var dataURL = this.canvas.toDataURL();\n", | |
" this.cell_info[1]['text/html'] =\n", | |
" '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.updated_canvas_event = function () {\n", | |
" // Tell IPython that the notebook contents must change.\n", | |
" IPython.notebook.set_dirty(true);\n", | |
" this.send_message('ack', {});\n", | |
" var fig = this;\n", | |
" // Wait a second, then push the new image to the DOM so\n", | |
" // that it is saved nicely (might be nice to debounce this).\n", | |
" setTimeout(function () {\n", | |
" fig.push_to_output();\n", | |
" }, 1000);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_toolbar = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var toolbar = document.createElement('div');\n", | |
" toolbar.classList = 'btn-toolbar';\n", | |
" this.root.appendChild(toolbar);\n", | |
"\n", | |
" function on_click_closure(name) {\n", | |
" return function (_event) {\n", | |
" return fig.toolbar_button_onclick(name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" function on_mouseover_closure(tooltip) {\n", | |
" return function (event) {\n", | |
" if (!event.currentTarget.disabled) {\n", | |
" return fig.toolbar_button_onmouseover(tooltip);\n", | |
" }\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" fig.buttons = {};\n", | |
" var buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'btn-group';\n", | |
" var button;\n", | |
" for (var toolbar_ind in mpl.toolbar_items) {\n", | |
" var name = mpl.toolbar_items[toolbar_ind][0];\n", | |
" var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", | |
" var image = mpl.toolbar_items[toolbar_ind][2];\n", | |
" var method_name = mpl.toolbar_items[toolbar_ind][3];\n", | |
"\n", | |
" if (!name) {\n", | |
" /* Instead of a spacer, we start a new button group. */\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
" buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'btn-group';\n", | |
" continue;\n", | |
" }\n", | |
"\n", | |
" button = fig.buttons[name] = document.createElement('button');\n", | |
" button.classList = 'btn btn-default';\n", | |
" button.href = '#';\n", | |
" button.title = name;\n", | |
" button.innerHTML = '<i class=\"fa ' + image + ' fa-lg\"></i>';\n", | |
" button.addEventListener('click', on_click_closure(method_name));\n", | |
" button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", | |
" buttonGroup.appendChild(button);\n", | |
" }\n", | |
"\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
"\n", | |
" // Add the status bar.\n", | |
" var status_bar = document.createElement('span');\n", | |
" status_bar.classList = 'mpl-message pull-right';\n", | |
" toolbar.appendChild(status_bar);\n", | |
" this.message = status_bar;\n", | |
"\n", | |
" // Add the close button to the window.\n", | |
" var buttongrp = document.createElement('div');\n", | |
" buttongrp.classList = 'btn-group inline pull-right';\n", | |
" button = document.createElement('button');\n", | |
" button.classList = 'btn btn-mini btn-primary';\n", | |
" button.href = '#';\n", | |
" button.title = 'Stop Interaction';\n", | |
" button.innerHTML = '<i class=\"fa fa-power-off icon-remove icon-large\"></i>';\n", | |
" button.addEventListener('click', function (_evt) {\n", | |
" fig.handle_close(fig, {});\n", | |
" });\n", | |
" button.addEventListener(\n", | |
" 'mouseover',\n", | |
" on_mouseover_closure('Stop Interaction')\n", | |
" );\n", | |
" buttongrp.appendChild(button);\n", | |
" var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n", | |
" titlebar.insertBefore(buttongrp, titlebar.firstChild);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._remove_fig_handler = function (event) {\n", | |
" var fig = event.data.fig;\n", | |
" if (event.target !== this) {\n", | |
" // Ignore bubbled events from children.\n", | |
" return;\n", | |
" }\n", | |
" fig.close_ws(fig, {});\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._root_extra_style = function (el) {\n", | |
" el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._canvas_extra_style = function (el) {\n", | |
" // this is important to make the div 'focusable\n", | |
" el.setAttribute('tabindex', 0);\n", | |
" // reach out to IPython and tell the keyboard manager to turn it's self\n", | |
" // off when our div gets focus\n", | |
"\n", | |
" // location in version 3\n", | |
" if (IPython.notebook.keyboard_manager) {\n", | |
" IPython.notebook.keyboard_manager.register_events(el);\n", | |
" } else {\n", | |
" // location in version 2\n", | |
" IPython.keyboard_manager.register_events(el);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._key_event_extra = function (event, _name) {\n", | |
" var manager = IPython.notebook.keyboard_manager;\n", | |
" if (!manager) {\n", | |
" manager = IPython.keyboard_manager;\n", | |
" }\n", | |
"\n", | |
" // Check for shift+enter\n", | |
" if (event.shiftKey && event.which === 13) {\n", | |
" this.canvas_div.blur();\n", | |
" // select the cell after this one\n", | |
" var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", | |
" IPython.notebook.select(index + 1);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_save = function (fig, _msg) {\n", | |
" fig.ondownload(fig, null);\n", | |
"};\n", | |
"\n", | |
"mpl.find_output_cell = function (html_output) {\n", | |
" // Return the cell and output element which can be found *uniquely* in the notebook.\n", | |
" // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", | |
" // IPython event is triggered only after the cells have been serialised, which for\n", | |
" // our purposes (turning an active figure into a static one), is too late.\n", | |
" var cells = IPython.notebook.get_cells();\n", | |
" var ncells = cells.length;\n", | |
" for (var i = 0; i < ncells; i++) {\n", | |
" var cell = cells[i];\n", | |
" if (cell.cell_type === 'code') {\n", | |
" for (var j = 0; j < cell.output_area.outputs.length; j++) {\n", | |
" var data = cell.output_area.outputs[j];\n", | |
" if (data.data) {\n", | |
" // IPython >= 3 moved mimebundle to data attribute of output\n", | |
" data = data.data;\n", | |
" }\n", | |
" if (data['text/html'] === html_output) {\n", | |
" return [cell, data, j];\n", | |
" }\n", | |
" }\n", | |
" }\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"// Register the function which deals with the matplotlib target/channel.\n", | |
"// The kernel may be null if the page has been refreshed.\n", | |
"if (IPython.notebook.kernel !== null) {\n", | |
" IPython.notebook.kernel.comm_manager.register_target(\n", | |
" 'matplotlib',\n", | |
" mpl.mpl_figure_comm\n", | |
" );\n", | |
"}\n" | |
], | |
"text/plain": [ | |
"<IPython.core.display.Javascript object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
}, | |
{ | |
"data": { | |
"text/html": [ | |
"<img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAgAElEQVR4nO3de3TU9Z3/8Q+BXDEZN4i5AOKFANYQK8tSe7Yk5e4uEbt73CKHWy9ca1BaT4uXn0aOAqFFu3WPiOui51QlaBvsRREJqFRJQHcHy2C8VFSMAe1KSNSVRCGv3x+cTPmSBBI+eQfCPJ/nPP7o5JthZjJlXs5kBiciIiIiiqnc6b4ARERERNS1MQCJiIiIYiwGIBEREVGMxQAkIiIiirEYgEREREQxFgOQiIiIKMZiABIRERHFWAxAIiIiohiLAUhEREQUYzEAiYiIiGIsBiARERFRjMUAJCIiIoqxGIBEREREMRYDkIiIiCjGYgASERERxVgMQCIiIqIYiwFIREREFGMxAImIiIhiLAYgERERUYzFACQiIiKKsRiARERERDEWA5CIiIgoxmIAEhEREcVYDEAiIiKiGIsBSERERBRjMQCJiIiIYiwGIBEREVGMxQAkIiIiirEYgEREREQxFgOQiIiIKMZiABIRERHFWAxAIiIiohiLAUhEREQUYzEAiYiIiGIsBiARERFRjMUAJCIiIoqxGIDUrSsuLpZzTv/7v//b6tcvu+wyFRQUnNJ5z5o1SwMHDgycduDAAU2ZMkV9+/aVc07XXHPNKZ336W7gwIGaNWtW9H+/8MILcs7phRde6ND53H///XrkkUc69D2t/VmzZs1S7969O3Q+J2vbtm0qLi7WwYMHW3ytoKDglO8XvoXDYeXn5ystLU3OOf3yl788LZejI3300UdavHixcnNz1bt3byUmJmrQoEG64YYb9Pbbb5/ui9fhOnq/bes+e/zfD239/+o3v/mN3wUmMogBSN06ywH4zjvvKBwOB05btGiREhIS9Nhjj6myslJvvfXWKZ336e74B6r6+npVVlaqvr6+Q+dzKrdva3+WxQD8xS9+Ieec3nvvvRZfe/311/X666936p/X3r7+9a8rJydHGzZsUGVlpfbv339aLkd727Fjh/r27avzzjtPd955p5577jm98MILWr16tb71rW/p3HPPPd0XscN19H7b3gEYDof1zjvvtPg+BiCdiTEAqVtnOQBba9y4cbr00ks77fyampr0xRdfdNr5tbfjB+Cp1pHb98svv9RXX33V6te6egCeznr16qUFCxac9LgvvvhCTU1NXXCJ2q6+vl6ZmZkaMGCAqqurWz3mZONm4MCBKi4u7vCf7Zzr8LPL7c1qALb1fQxAOhNjAFK3rqMDsPkv5LVr1+rWW29VVlaWUlNTNXbsWL355puB7z32L/j33ntPzrkWmh8QDhw4oAULFig7O1vx8fG66KKLdOutt6qhoSFwns45XX/99XrggQc0dOhQxcfH64EHHtAjjzwi55y2bNmi2bNnKz09XampqZoxY4Y+//xz7d+/X//2b/+mUCikzMxM3XTTTfryyy9Pevt8+eWX+ulPf6qMjAwlJyfrH//xH7Vjx452vQS8Z88eTZkyRVlZWUpISND555+vMWPGaOfOnZKOPrAff3s0317N5/frX/9aP/nJT5Sdna0ePXrojTfeOOFLwLt379aYMWOUkpKi8847T9dff73+7//+L3pc88+htWHgnIsOjeb7RVs/r9ZeAu7oz/DXv/61hg4dquTkZOXl5emPf/zjCX8WzT/j4x37teeee07f//73dd5558k5p0OHDunIkSNasWKFhgwZooSEBPXt21czZsxoMcgKCgp02WWXqaKiQt/85jeVlJSkgQMH6uGHH5YkPf3007riiiuUnJys3NxcPfvssye8vJK0cuVKOedUWlp60mPbqqsHoM/9VpLeeOMNTZw4UcnJyerTp4/mzZunP/zhD14vAT/66KP68Y9/rIyMDCUlJSk/P7/FqwtEXR0DkLp1pzoAL7zwQk2bNk3PPPOMSktLdcEFFygnJ0eHDx+OHnvsX/ANDQ2qrKzUFVdcoYsvvliVlZXRlzEPHTqkvLw89e7dWytXrtSmTZt0++23q1evXvrnf/7nwOVxzqlfv37Ky8vT2rVr9fzzz2v37t3RAXDRRRfppptu0qZNm7RixQr17NlTU6dO1fDhw3X33XervLxcixcvlnNO99xzz0lvn1mzZqlHjx766U9/qk2bNunee+9Vv379lJaWdtIBOGTIEA0aNEiPPvqotm7dqrKyMt10003RY8LhsC6++GJdccUV0duj+UGt+fz69euna6+9Vn/4wx/09NNP68CBA20OwISEBF1wwQVaunSpNm3apDvvvFO9evVSYWFh9Lj2DsDq6motXLhQzjmtX78+8POSWg7Ajv4ML7zwQo0cOVJPPvmkNmzYoG9/+9vq1auX9uzZ0+bP4q9//asqKyvlnNO1114bvUzS3wZgv379NHfu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gB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiAnTwAt27dqsLCQmVlZck5p6eeeirw9aamJhUXFysrK0tJSUkqKCjQ7t27A8c0NDSoqKhIffr0UUpKiq6++mpVV1cHjqmtrdX06dOVlpamtLQ0TZ8+XQcPHgwcs3fvXhUWFiolJUV9+vTRwoUL1djYGDhm165dys/PV1JSkrKzs7VkyRI1NTW1+/oyAAEAsGMVA7CTB+CGDRt02223qaysrNUBWFJSotTUVJWVlSkSiWjKlCnKysrSp59+Gj1m/vz56tevn8rLyxUOhzV69GhdfvnlOnz4cPSYq666Srm5uaqoqFBFRYVyc3NVWFgY/frhw4eVm5ur0aNHKxwOq7y8XNnZ2SoqKooeU19fr4yMDF133XWKRCIqKytTamqqVq5c2e7rywAEAMCO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AFZUVAROv/vuuzVkyJBWv+f4ZwAPHjyoPXv2qK6uzuy/TqyeXQS3M7fz2Yfbmdv5bGJ5O9fV1am6ulpHjhzpislxRhazA/BUXgLuyurr+f2ErojbuWvidu6auJ27Jm7nronb2baYHYDS0TeBLFiwIHDapZde2u43gVjGHb9r4nbumriduyZu566J27lr4na2LaYHYPPHwKxZs0ZVVVVatGiRevfurffff/90XzTu+F0Ut3PXxO3cNXE7d03czl0Tt7NtMT0ApaMfBD1w4EAlJCRo+PDh2rp16+m+SJKO/r5hcXGxGhoaTvdFOavjdu6auJ27Jm7nronbuWvidrYt5gcgERERUazFACQiIiKKsRiARERERDEWA5CIiIgoxmIAEhEREcVYDMDT1P33368LL7xQiYmJGj58uP70pz+d8PgXX3xRw4cPV2Jioi666CI98MADXXRJu38dua3Lyso0btw4nXfeeUpNTdWVV16pjRs3duGl7b519D7d3Msvv6yePXvq8ssvN76EZ0cdvZ0bGhp066236oILLlBCQoIuvvhirVmzposubfeto7fzY489pry8PCUnJyszM1Pf+9739Mknn3TRpe2ebd26VYWFhcrKypJzTk899dRJv4fHws6LAXgaav78wYceekhVVVW68cYb1bt3b+3du7fV4999912lpKToxhtvVFVVlR566CHFx8frt7/9bRdf8u5XR2/rG2+8UStWrNArr7yit99+W7fccovi4+MVDoe7+JJ3rzp6OzdXV1eniy++WBMmTGAAtqNTuZ0nT56sb3zjGyovL9d7772nHTt2tPg30ClYR2/nl156SXFxcfrVr36ld999Vy+99JIuu+wyfec73+niS9692rBhg2677TaVlZW1awDyWNi5MQBPQyNHjtT8+fMDpw0dOrTNf4HkZz/7mYYOHRo4bd68ebryyivNLuPZUkdv69b62te+piVLlnT2RTurOtXbecqUKfp//+//qbi4mAHYjjp6Oz/77LMKhUI6cOBAV1y8s6aO3s6/+MUvdPHFFwdOu++++9S/f3+zy3i21Z4ByGNh58YA7OJO5d8gHjVqlG644YbAaevXr1evXr305Zdfml3W7l5n/HvPR44c0YABA/Qf//EfFhfxrOhUb+eHH35YI0aM0FdffcUAbEencjsvWLBAY8eO1eLFi5Wdna2cnBzddNNN+uKLL7riInfLTuV23rZtmxISEvTMM8+oqalJH330kfLz8zVv3ryuuMhnRe0ZgDwWdm4MwC6upqZGzrkWL8EsXbpUgwcPbvV7cnJytHTp0sBp27Ztk3NO+/btM7us3b1Tua2P7+c//7nS09P18ccfW1zEs6JTuZ3ffvttnX/++XrrrbckiQHYjk7ldp44caISExM1adIk7dixQ88884wGDhyo73//+11xkbtlp/r3xm9+8xudc8456tWrl5xzmjx5MqOkA7VnAPJY2LkxALu45r9cKioqAqfffffdGjJkSKvfk5OTo2XLlgVOe/nll+Wc0/79+80ua3fvVG7rY1u7dq1SUlJUXl5udRHPijp6Ox8+fFgjRowI/PI2A/Dkncr9efz48UpKSlJdXV30tLKyMvXo0YNnAdvoVG7n119/XVlZWfr5z3+uP//5z9q4caOGDRumH/zgB11xkc+K2jsAeSzsvBiAXRwvAXddPi8Br1u3TsnJyXr66actL+JZUUdv54MHD8o5p549e0b16NEjetqWLVu66qJ3q07l/jxz5kxdcsklgdOqqqrknNPbb79tdlm7c6dyO0+fPl3XXntt4LSXXnqJZ6Y6EC8Bd30MwNPQyJEjtWDBgsBpl1566QnfBHLppZcGTps/fz6/+NqOOnpbS0ef+UtKSmrXRxLQ0TpyOx85ckSRSCRgwYIFGjJkiCKRiD7//POuutjdro7enx988EElJyfrs88+i572u9/9TnFxcTwDeII6ejv/67/+q7773e8GTquoqJBzTjU1NWaX82yqvW8C4bGw82IAnoaaP2JgzZo1qqqq0qJFi9S7d2+9//77kqSbb75ZM2bMiB7f/Nb3H//4x6qqqtKaNWt463s76+htvXbtWvXq1Uv333+/9u/fH3XsS2jUso7ezsfHS8Dtq6O382effab+/fvr2muv1euvv66tW7cqJydHs2fPPl1XoVvU0dv5kUceUa9evbRq1Srt2bNHL7/8skaMGKGRI0eerqvQLfrss8+0c+dO7dy5U8453Xvvvdq5c2f043Z4LLSNAXiauv/++zVw4EAlJCRo+PDh2rp1a/Rrs2bNUkFBQeD4F198UVdccYUSEhJ04YUX8uGXHagjt3VBQYGccy3MmjWr6y94N6uj9+ljYwC2v47ezm+88YbGjRun5ORk9e/fXz/5yU949q8ddfR2vu+++/S1r31NycnJysrK0rRp0/Thhx928aXuXr3wwgsn/PuWx0LbGIBEREREMRYDkIiIiCjGYgASERERxVgMQCIiIqIYiwFIREREFGMxAImIiIhiLAYgERERUYzFACQiIiKKsRiARERERDEWA5CIiIgoxmIAEhEREcVYDEAiIiKiGIsBSERERBRjMQCJiIiIYiwGIBEREVGMxQAkIiIiirEYgEREREQxFgOQiIiIKMZiABIRERHFWAxAIiIiohiLAUhEREQUYzEAiYiIiGIsBiARERFRjMUAJCIiIoqxGIBEREREMRYDkIiIiCjGYgASERERxVgMQCIiIqIYiwFIREREFGMxAImIiIhiLAYgERERUYzFACQiIiKKsf4/yGqecnuQjqEAAAAASUVORK5CYII=\" width=\"640\">" | |
], | |
"text/plain": [ | |
"<IPython.core.display.HTML object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"print(\"Execution time from C++:\")\n", | |
"print(\"Uniform distribution, 32 and 64 bits versions of Mersenne twisters:\")\n", | |
"%timeit cython_uniform_cpp(shape)\n", | |
"%timeit cython_uniform64_cpp(shape)\n", | |
"print(\"Normal distribution, 32 and 64 bits versions of Mersenne twisters:\")\n", | |
"a=numpy.ones(shape)\n", | |
"%timeit cython_normal_cpp(a,a)\n", | |
"%timeit cython_normal64_cpp(a,a)\n", | |
"fig, ax = subplots()\n", | |
"ax.hist(cython_uniform64_cpp(shape).ravel())\n", | |
"ax.set_title(\"Uniform distribution from C++ stdlib\")\n", | |
"pass" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"This is really disappointing! While C++ offers ready to use pseudo random number gernertors, which looks OK, but their performances are at least twice slower than what numpy offers !\n", | |
"\n", | |
"Note: the 32-bits version is even slower than the 64 bits version (running on a 64-bits computer)\n", | |
"\n", | |
"Sorry C++ is not the proper tool.\n", | |
"\n", | |
"## Numpy's C-API\n", | |
"\n", | |
"Since *numpy* was found to be the fastest, I descided to use the tools available within it. \n", | |
"\n", | |
"I do not especially like the idea of direct binding to the numpy ABI because it is likely to make the binaries much more difficult to distribute via `pip` and end-users are likely to experience random segmentation faults if they use a numpy version which is different from the one used for packaging.\n", | |
"\n", | |
"I found this code pretty complicated with those *PyCapsules*, but it is just a copy/paste from what was found on the numpy documentation at https://numpy.org/doc/stable/reference/random/extending.html\n", | |
"\n", | |
"The PCG generator is apparently better in quality than the Mersenne-Twister used with the C++ test." | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 9, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"%%cython\n", | |
"# cython: language_level=3\n", | |
"# cython: boundscheck=False\n", | |
"# cython: cdivision=True\n", | |
"# cython: wraparound=False\n", | |
"\n", | |
"\"\"\"\n", | |
"This file shows how the to use a BitGenerator to create a distribution.\n", | |
"\"\"\"\n", | |
"import numpy as np\n", | |
"cimport numpy as np\n", | |
"from cpython.pycapsule cimport PyCapsule_IsValid, PyCapsule_GetPointer\n", | |
"from libc.stdint cimport uint16_t, uint64_t\n", | |
"from numpy.random cimport bitgen_t\n", | |
"from numpy.random import PCG64\n", | |
"from numpy.random.c_distributions cimport (\n", | |
" random_standard_uniform_fill, random_standard_uniform_fill_f)\n", | |
"\n", | |
"def cython_uniform_cnp(shape):\n", | |
" \"\"\"\n", | |
" Create an array of `n` uniformly distributed doubles.\n", | |
" A 'real' distribution would want to process the values into\n", | |
" some non-uniform distribution\n", | |
" \"\"\"\n", | |
" cdef:\n", | |
" Py_ssize_t i, n=np.prod(shape)\n", | |
" bitgen_t *rng\n", | |
" const char *capsule_name = \"BitGenerator\"\n", | |
" double[::1] random_values\n", | |
"\n", | |
" x = PCG64()\n", | |
" capsule = x.capsule\n", | |
" # Optional check that the capsule if from a BitGenerator\n", | |
" if not PyCapsule_IsValid(capsule, capsule_name):\n", | |
" raise ValueError(\"Invalid pointer to anon_func_state\")\n", | |
" # Cast the pointer\n", | |
" rng = <bitgen_t *> PyCapsule_GetPointer(capsule, capsule_name)\n", | |
" random_values = np.empty(n, dtype='float64')\n", | |
" with x.lock, nogil:\n", | |
" for i in range(n):\n", | |
" # Call the function\n", | |
" random_values[i] = rng.next_double(rng.state)\n", | |
" randoms = np.asarray(random_values)\n", | |
"\n", | |
" return randoms.reshape(shape)" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 10, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"Performance obtained from the C-API of Numpy\n", | |
"21.3 ms ± 217 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" | |
] | |
}, | |
{ | |
"data": { | |
"application/javascript": [ | |
"/* Put everything inside the global mpl namespace */\n", | |
"/* global mpl */\n", | |
"window.mpl = {};\n", | |
"\n", | |
"mpl.get_websocket_type = function () {\n", | |
" if (typeof WebSocket !== 'undefined') {\n", | |
" return WebSocket;\n", | |
" } else if (typeof MozWebSocket !== 'undefined') {\n", | |
" return MozWebSocket;\n", | |
" } else {\n", | |
" alert(\n", | |
" 'Your browser does not have WebSocket support. ' +\n", | |
" 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", | |
" 'Firefox 4 and 5 are also supported but you ' +\n", | |
" 'have to enable WebSockets in about:config.'\n", | |
" );\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n", | |
" this.id = figure_id;\n", | |
"\n", | |
" this.ws = websocket;\n", | |
"\n", | |
" this.supports_binary = this.ws.binaryType !== undefined;\n", | |
"\n", | |
" if (!this.supports_binary) {\n", | |
" var warnings = document.getElementById('mpl-warnings');\n", | |
" if (warnings) {\n", | |
" warnings.style.display = 'block';\n", | |
" warnings.textContent =\n", | |
" 'This browser does not support binary websocket messages. ' +\n", | |
" 'Performance may be slow.';\n", | |
" }\n", | |
" }\n", | |
"\n", | |
" this.imageObj = new Image();\n", | |
"\n", | |
" this.context = undefined;\n", | |
" this.message = undefined;\n", | |
" this.canvas = undefined;\n", | |
" this.rubberband_canvas = undefined;\n", | |
" this.rubberband_context = undefined;\n", | |
" this.format_dropdown = undefined;\n", | |
"\n", | |
" this.image_mode = 'full';\n", | |
"\n", | |
" this.root = document.createElement('div');\n", | |
" this.root.setAttribute('style', 'display: inline-block');\n", | |
" this._root_extra_style(this.root);\n", | |
"\n", | |
" parent_element.appendChild(this.root);\n", | |
"\n", | |
" this._init_header(this);\n", | |
" this._init_canvas(this);\n", | |
" this._init_toolbar(this);\n", | |
"\n", | |
" var fig = this;\n", | |
"\n", | |
" this.waiting = false;\n", | |
"\n", | |
" this.ws.onopen = function () {\n", | |
" fig.send_message('supports_binary', { value: fig.supports_binary });\n", | |
" fig.send_message('send_image_mode', {});\n", | |
" if (fig.ratio !== 1) {\n", | |
" fig.send_message('set_dpi_ratio', { dpi_ratio: fig.ratio });\n", | |
" }\n", | |
" fig.send_message('refresh', {});\n", | |
" };\n", | |
"\n", | |
" this.imageObj.onload = function () {\n", | |
" if (fig.image_mode === 'full') {\n", | |
" // Full images could contain transparency (where diff images\n", | |
" // almost always do), so we need to clear the canvas so that\n", | |
" // there is no ghosting.\n", | |
" fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", | |
" }\n", | |
" fig.context.drawImage(fig.imageObj, 0, 0);\n", | |
" };\n", | |
"\n", | |
" this.imageObj.onunload = function () {\n", | |
" fig.ws.close();\n", | |
" };\n", | |
"\n", | |
" this.ws.onmessage = this._make_on_message_function(this);\n", | |
"\n", | |
" this.ondownload = ondownload;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_header = function () {\n", | |
" var titlebar = document.createElement('div');\n", | |
" titlebar.classList =\n", | |
" 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n", | |
" var titletext = document.createElement('div');\n", | |
" titletext.classList = 'ui-dialog-title';\n", | |
" titletext.setAttribute(\n", | |
" 'style',\n", | |
" 'width: 100%; text-align: center; padding: 3px;'\n", | |
" );\n", | |
" titlebar.appendChild(titletext);\n", | |
" this.root.appendChild(titlebar);\n", | |
" this.header = titletext;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n", | |
"\n", | |
"mpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n", | |
"\n", | |
"mpl.figure.prototype._init_canvas = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var canvas_div = (this.canvas_div = document.createElement('div'));\n", | |
" canvas_div.setAttribute(\n", | |
" 'style',\n", | |
" 'border: 1px solid #ddd;' +\n", | |
" 'box-sizing: content-box;' +\n", | |
" 'clear: both;' +\n", | |
" 'min-height: 1px;' +\n", | |
" 'min-width: 1px;' +\n", | |
" 'outline: 0;' +\n", | |
" 'overflow: hidden;' +\n", | |
" 'position: relative;' +\n", | |
" 'resize: both;'\n", | |
" );\n", | |
"\n", | |
" function on_keyboard_event_closure(name) {\n", | |
" return function (event) {\n", | |
" return fig.key_event(event, name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" canvas_div.addEventListener(\n", | |
" 'keydown',\n", | |
" on_keyboard_event_closure('key_press')\n", | |
" );\n", | |
" canvas_div.addEventListener(\n", | |
" 'keyup',\n", | |
" on_keyboard_event_closure('key_release')\n", | |
" );\n", | |
"\n", | |
" this._canvas_extra_style(canvas_div);\n", | |
" this.root.appendChild(canvas_div);\n", | |
"\n", | |
" var canvas = (this.canvas = document.createElement('canvas'));\n", | |
" canvas.classList.add('mpl-canvas');\n", | |
" canvas.setAttribute('style', 'box-sizing: content-box;');\n", | |
"\n", | |
" this.context = canvas.getContext('2d');\n", | |
"\n", | |
" var backingStore =\n", | |
" this.context.backingStorePixelRatio ||\n", | |
" this.context.webkitBackingStorePixelRatio ||\n", | |
" this.context.mozBackingStorePixelRatio ||\n", | |
" this.context.msBackingStorePixelRatio ||\n", | |
" this.context.oBackingStorePixelRatio ||\n", | |
" this.context.backingStorePixelRatio ||\n", | |
" 1;\n", | |
"\n", | |
" this.ratio = (window.devicePixelRatio || 1) / backingStore;\n", | |
"\n", | |
" var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n", | |
" 'canvas'\n", | |
" ));\n", | |
" rubberband_canvas.setAttribute(\n", | |
" 'style',\n", | |
" 'box-sizing: content-box; position: absolute; left: 0; top: 0; z-index: 1;'\n", | |
" );\n", | |
"\n", | |
" // Apply a ponyfill if ResizeObserver is not implemented by browser.\n", | |
" if (this.ResizeObserver === undefined) {\n", | |
" if (window.ResizeObserver !== undefined) {\n", | |
" this.ResizeObserver = window.ResizeObserver;\n", | |
" } else {\n", | |
" var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n", | |
" this.ResizeObserver = obs.ResizeObserver;\n", | |
" }\n", | |
" }\n", | |
"\n", | |
" this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n", | |
" var nentries = entries.length;\n", | |
" for (var i = 0; i < nentries; i++) {\n", | |
" var entry = entries[i];\n", | |
" var width, height;\n", | |
" if (entry.contentBoxSize) {\n", | |
" if (entry.contentBoxSize instanceof Array) {\n", | |
" // Chrome 84 implements new version of spec.\n", | |
" width = entry.contentBoxSize[0].inlineSize;\n", | |
" height = entry.contentBoxSize[0].blockSize;\n", | |
" } else {\n", | |
" // Firefox implements old version of spec.\n", | |
" width = entry.contentBoxSize.inlineSize;\n", | |
" height = entry.contentBoxSize.blockSize;\n", | |
" }\n", | |
" } else {\n", | |
" // Chrome <84 implements even older version of spec.\n", | |
" width = entry.contentRect.width;\n", | |
" height = entry.contentRect.height;\n", | |
" }\n", | |
"\n", | |
" // Keep the size of the canvas and rubber band canvas in sync with\n", | |
" // the canvas container.\n", | |
" if (entry.devicePixelContentBoxSize) {\n", | |
" // Chrome 84 implements new version of spec.\n", | |
" canvas.setAttribute(\n", | |
" 'width',\n", | |
" entry.devicePixelContentBoxSize[0].inlineSize\n", | |
" );\n", | |
" canvas.setAttribute(\n", | |
" 'height',\n", | |
" entry.devicePixelContentBoxSize[0].blockSize\n", | |
" );\n", | |
" } else {\n", | |
" canvas.setAttribute('width', width * fig.ratio);\n", | |
" canvas.setAttribute('height', height * fig.ratio);\n", | |
" }\n", | |
" canvas.setAttribute(\n", | |
" 'style',\n", | |
" 'width: ' + width + 'px; height: ' + height + 'px;'\n", | |
" );\n", | |
"\n", | |
" rubberband_canvas.setAttribute('width', width);\n", | |
" rubberband_canvas.setAttribute('height', height);\n", | |
"\n", | |
" // And update the size in Python. We ignore the initial 0/0 size\n", | |
" // that occurs as the element is placed into the DOM, which should\n", | |
" // otherwise not happen due to the minimum size styling.\n", | |
" if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n", | |
" fig.request_resize(width, height);\n", | |
" }\n", | |
" }\n", | |
" });\n", | |
" this.resizeObserverInstance.observe(canvas_div);\n", | |
"\n", | |
" function on_mouse_event_closure(name) {\n", | |
" return function (event) {\n", | |
" return fig.mouse_event(event, name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mousedown',\n", | |
" on_mouse_event_closure('button_press')\n", | |
" );\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseup',\n", | |
" on_mouse_event_closure('button_release')\n", | |
" );\n", | |
" // Throttle sequential mouse events to 1 every 20ms.\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mousemove',\n", | |
" on_mouse_event_closure('motion_notify')\n", | |
" );\n", | |
"\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseenter',\n", | |
" on_mouse_event_closure('figure_enter')\n", | |
" );\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseleave',\n", | |
" on_mouse_event_closure('figure_leave')\n", | |
" );\n", | |
"\n", | |
" canvas_div.addEventListener('wheel', function (event) {\n", | |
" if (event.deltaY < 0) {\n", | |
" event.step = 1;\n", | |
" } else {\n", | |
" event.step = -1;\n", | |
" }\n", | |
" on_mouse_event_closure('scroll')(event);\n", | |
" });\n", | |
"\n", | |
" canvas_div.appendChild(canvas);\n", | |
" canvas_div.appendChild(rubberband_canvas);\n", | |
"\n", | |
" this.rubberband_context = rubberband_canvas.getContext('2d');\n", | |
" this.rubberband_context.strokeStyle = '#000000';\n", | |
"\n", | |
" this._resize_canvas = function (width, height, forward) {\n", | |
" if (forward) {\n", | |
" canvas_div.style.width = width + 'px';\n", | |
" canvas_div.style.height = height + 'px';\n", | |
" }\n", | |
" };\n", | |
"\n", | |
" // Disable right mouse context menu.\n", | |
" this.rubberband_canvas.addEventListener('contextmenu', function (_e) {\n", | |
" event.preventDefault();\n", | |
" return false;\n", | |
" });\n", | |
"\n", | |
" function set_focus() {\n", | |
" canvas.focus();\n", | |
" canvas_div.focus();\n", | |
" }\n", | |
"\n", | |
" window.setTimeout(set_focus, 100);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_toolbar = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var toolbar = document.createElement('div');\n", | |
" toolbar.classList = 'mpl-toolbar';\n", | |
" this.root.appendChild(toolbar);\n", | |
"\n", | |
" function on_click_closure(name) {\n", | |
" return function (_event) {\n", | |
" return fig.toolbar_button_onclick(name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" function on_mouseover_closure(tooltip) {\n", | |
" return function (event) {\n", | |
" if (!event.currentTarget.disabled) {\n", | |
" return fig.toolbar_button_onmouseover(tooltip);\n", | |
" }\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" fig.buttons = {};\n", | |
" var buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'mpl-button-group';\n", | |
" for (var toolbar_ind in mpl.toolbar_items) {\n", | |
" var name = mpl.toolbar_items[toolbar_ind][0];\n", | |
" var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", | |
" var image = mpl.toolbar_items[toolbar_ind][2];\n", | |
" var method_name = mpl.toolbar_items[toolbar_ind][3];\n", | |
"\n", | |
" if (!name) {\n", | |
" /* Instead of a spacer, we start a new button group. */\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
" buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'mpl-button-group';\n", | |
" continue;\n", | |
" }\n", | |
"\n", | |
" var button = (fig.buttons[name] = document.createElement('button'));\n", | |
" button.classList = 'mpl-widget';\n", | |
" button.setAttribute('role', 'button');\n", | |
" button.setAttribute('aria-disabled', 'false');\n", | |
" button.addEventListener('click', on_click_closure(method_name));\n", | |
" button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", | |
"\n", | |
" var icon_img = document.createElement('img');\n", | |
" icon_img.src = '_images/' + image + '.png';\n", | |
" icon_img.srcset = '_images/' + image + '_large.png 2x';\n", | |
" icon_img.alt = tooltip;\n", | |
" button.appendChild(icon_img);\n", | |
"\n", | |
" buttonGroup.appendChild(button);\n", | |
" }\n", | |
"\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
"\n", | |
" var fmt_picker = document.createElement('select');\n", | |
" fmt_picker.classList = 'mpl-widget';\n", | |
" toolbar.appendChild(fmt_picker);\n", | |
" this.format_dropdown = fmt_picker;\n", | |
"\n", | |
" for (var ind in mpl.extensions) {\n", | |
" var fmt = mpl.extensions[ind];\n", | |
" var option = document.createElement('option');\n", | |
" option.selected = fmt === mpl.default_extension;\n", | |
" option.innerHTML = fmt;\n", | |
" fmt_picker.appendChild(option);\n", | |
" }\n", | |
"\n", | |
" var status_bar = document.createElement('span');\n", | |
" status_bar.classList = 'mpl-message';\n", | |
" toolbar.appendChild(status_bar);\n", | |
" this.message = status_bar;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n", | |
" // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n", | |
" // which will in turn request a refresh of the image.\n", | |
" this.send_message('resize', { width: x_pixels, height: y_pixels });\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.send_message = function (type, properties) {\n", | |
" properties['type'] = type;\n", | |
" properties['figure_id'] = this.id;\n", | |
" this.ws.send(JSON.stringify(properties));\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.send_draw_message = function () {\n", | |
" if (!this.waiting) {\n", | |
" this.waiting = true;\n", | |
" this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_save = function (fig, _msg) {\n", | |
" var format_dropdown = fig.format_dropdown;\n", | |
" var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n", | |
" fig.ondownload(fig, format);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_resize = function (fig, msg) {\n", | |
" var size = msg['size'];\n", | |
" if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n", | |
" fig._resize_canvas(size[0], size[1], msg['forward']);\n", | |
" fig.send_message('refresh', {});\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_rubberband = function (fig, msg) {\n", | |
" var x0 = msg['x0'] / fig.ratio;\n", | |
" var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n", | |
" var x1 = msg['x1'] / fig.ratio;\n", | |
" var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n", | |
" x0 = Math.floor(x0) + 0.5;\n", | |
" y0 = Math.floor(y0) + 0.5;\n", | |
" x1 = Math.floor(x1) + 0.5;\n", | |
" y1 = Math.floor(y1) + 0.5;\n", | |
" var min_x = Math.min(x0, x1);\n", | |
" var min_y = Math.min(y0, y1);\n", | |
" var width = Math.abs(x1 - x0);\n", | |
" var height = Math.abs(y1 - y0);\n", | |
"\n", | |
" fig.rubberband_context.clearRect(\n", | |
" 0,\n", | |
" 0,\n", | |
" fig.canvas.width / fig.ratio,\n", | |
" fig.canvas.height / fig.ratio\n", | |
" );\n", | |
"\n", | |
" fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_figure_label = function (fig, msg) {\n", | |
" // Updates the figure title.\n", | |
" fig.header.textContent = msg['label'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_cursor = function (fig, msg) {\n", | |
" var cursor = msg['cursor'];\n", | |
" switch (cursor) {\n", | |
" case 0:\n", | |
" cursor = 'pointer';\n", | |
" break;\n", | |
" case 1:\n", | |
" cursor = 'default';\n", | |
" break;\n", | |
" case 2:\n", | |
" cursor = 'crosshair';\n", | |
" break;\n", | |
" case 3:\n", | |
" cursor = 'move';\n", | |
" break;\n", | |
" }\n", | |
" fig.rubberband_canvas.style.cursor = cursor;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_message = function (fig, msg) {\n", | |
" fig.message.textContent = msg['message'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_draw = function (fig, _msg) {\n", | |
" // Request the server to send over a new figure.\n", | |
" fig.send_draw_message();\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_image_mode = function (fig, msg) {\n", | |
" fig.image_mode = msg['mode'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n", | |
" for (var key in msg) {\n", | |
" if (!(key in fig.buttons)) {\n", | |
" continue;\n", | |
" }\n", | |
" fig.buttons[key].disabled = !msg[key];\n", | |
" fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n", | |
" if (msg['mode'] === 'PAN') {\n", | |
" fig.buttons['Pan'].classList.add('active');\n", | |
" fig.buttons['Zoom'].classList.remove('active');\n", | |
" } else if (msg['mode'] === 'ZOOM') {\n", | |
" fig.buttons['Pan'].classList.remove('active');\n", | |
" fig.buttons['Zoom'].classList.add('active');\n", | |
" } else {\n", | |
" fig.buttons['Pan'].classList.remove('active');\n", | |
" fig.buttons['Zoom'].classList.remove('active');\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.updated_canvas_event = function () {\n", | |
" // Called whenever the canvas gets updated.\n", | |
" this.send_message('ack', {});\n", | |
"};\n", | |
"\n", | |
"// A function to construct a web socket function for onmessage handling.\n", | |
"// Called in the figure constructor.\n", | |
"mpl.figure.prototype._make_on_message_function = function (fig) {\n", | |
" return function socket_on_message(evt) {\n", | |
" if (evt.data instanceof Blob) {\n", | |
" /* FIXME: We get \"Resource interpreted as Image but\n", | |
" * transferred with MIME type text/plain:\" errors on\n", | |
" * Chrome. But how to set the MIME type? It doesn't seem\n", | |
" * to be part of the websocket stream */\n", | |
" evt.data.type = 'image/png';\n", | |
"\n", | |
" /* Free the memory for the previous frames */\n", | |
" if (fig.imageObj.src) {\n", | |
" (window.URL || window.webkitURL).revokeObjectURL(\n", | |
" fig.imageObj.src\n", | |
" );\n", | |
" }\n", | |
"\n", | |
" fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n", | |
" evt.data\n", | |
" );\n", | |
" fig.updated_canvas_event();\n", | |
" fig.waiting = false;\n", | |
" return;\n", | |
" } else if (\n", | |
" typeof evt.data === 'string' &&\n", | |
" evt.data.slice(0, 21) === 'data:image/png;base64'\n", | |
" ) {\n", | |
" fig.imageObj.src = evt.data;\n", | |
" fig.updated_canvas_event();\n", | |
" fig.waiting = false;\n", | |
" return;\n", | |
" }\n", | |
"\n", | |
" var msg = JSON.parse(evt.data);\n", | |
" var msg_type = msg['type'];\n", | |
"\n", | |
" // Call the \"handle_{type}\" callback, which takes\n", | |
" // the figure and JSON message as its only arguments.\n", | |
" try {\n", | |
" var callback = fig['handle_' + msg_type];\n", | |
" } catch (e) {\n", | |
" console.log(\n", | |
" \"No handler for the '\" + msg_type + \"' message type: \",\n", | |
" msg\n", | |
" );\n", | |
" return;\n", | |
" }\n", | |
"\n", | |
" if (callback) {\n", | |
" try {\n", | |
" // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n", | |
" callback(fig, msg);\n", | |
" } catch (e) {\n", | |
" console.log(\n", | |
" \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n", | |
" e,\n", | |
" e.stack,\n", | |
" msg\n", | |
" );\n", | |
" }\n", | |
" }\n", | |
" };\n", | |
"};\n", | |
"\n", | |
"// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n", | |
"mpl.findpos = function (e) {\n", | |
" //this section is from http://www.quirksmode.org/js/events_properties.html\n", | |
" var targ;\n", | |
" if (!e) {\n", | |
" e = window.event;\n", | |
" }\n", | |
" if (e.target) {\n", | |
" targ = e.target;\n", | |
" } else if (e.srcElement) {\n", | |
" targ = e.srcElement;\n", | |
" }\n", | |
" if (targ.nodeType === 3) {\n", | |
" // defeat Safari bug\n", | |
" targ = targ.parentNode;\n", | |
" }\n", | |
"\n", | |
" // pageX,Y are the mouse positions relative to the document\n", | |
" var boundingRect = targ.getBoundingClientRect();\n", | |
" var x = e.pageX - (boundingRect.left + document.body.scrollLeft);\n", | |
" var y = e.pageY - (boundingRect.top + document.body.scrollTop);\n", | |
"\n", | |
" return { x: x, y: y };\n", | |
"};\n", | |
"\n", | |
"/*\n", | |
" * return a copy of an object with only non-object keys\n", | |
" * we need this to avoid circular references\n", | |
" * http://stackoverflow.com/a/24161582/3208463\n", | |
" */\n", | |
"function simpleKeys(original) {\n", | |
" return Object.keys(original).reduce(function (obj, key) {\n", | |
" if (typeof original[key] !== 'object') {\n", | |
" obj[key] = original[key];\n", | |
" }\n", | |
" return obj;\n", | |
" }, {});\n", | |
"}\n", | |
"\n", | |
"mpl.figure.prototype.mouse_event = function (event, name) {\n", | |
" var canvas_pos = mpl.findpos(event);\n", | |
"\n", | |
" if (name === 'button_press') {\n", | |
" this.canvas.focus();\n", | |
" this.canvas_div.focus();\n", | |
" }\n", | |
"\n", | |
" var x = canvas_pos.x * this.ratio;\n", | |
" var y = canvas_pos.y * this.ratio;\n", | |
"\n", | |
" this.send_message(name, {\n", | |
" x: x,\n", | |
" y: y,\n", | |
" button: event.button,\n", | |
" step: event.step,\n", | |
" guiEvent: simpleKeys(event),\n", | |
" });\n", | |
"\n", | |
" /* This prevents the web browser from automatically changing to\n", | |
" * the text insertion cursor when the button is pressed. We want\n", | |
" * to control all of the cursor setting manually through the\n", | |
" * 'cursor' event from matplotlib */\n", | |
" event.preventDefault();\n", | |
" return false;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._key_event_extra = function (_event, _name) {\n", | |
" // Handle any extra behaviour associated with a key event\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.key_event = function (event, name) {\n", | |
" // Prevent repeat events\n", | |
" if (name === 'key_press') {\n", | |
" if (event.which === this._key) {\n", | |
" return;\n", | |
" } else {\n", | |
" this._key = event.which;\n", | |
" }\n", | |
" }\n", | |
" if (name === 'key_release') {\n", | |
" this._key = null;\n", | |
" }\n", | |
"\n", | |
" var value = '';\n", | |
" if (event.ctrlKey && event.which !== 17) {\n", | |
" value += 'ctrl+';\n", | |
" }\n", | |
" if (event.altKey && event.which !== 18) {\n", | |
" value += 'alt+';\n", | |
" }\n", | |
" if (event.shiftKey && event.which !== 16) {\n", | |
" value += 'shift+';\n", | |
" }\n", | |
"\n", | |
" value += 'k';\n", | |
" value += event.which.toString();\n", | |
"\n", | |
" this._key_event_extra(event, name);\n", | |
"\n", | |
" this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n", | |
" return false;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.toolbar_button_onclick = function (name) {\n", | |
" if (name === 'download') {\n", | |
" this.handle_save(this, null);\n", | |
" } else {\n", | |
" this.send_message('toolbar_button', { name: name });\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n", | |
" this.message.textContent = tooltip;\n", | |
"};\n", | |
"\n", | |
"///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n", | |
"// prettier-ignore\n", | |
"var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\n", | |
"mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", | |
"\n", | |
"mpl.extensions = [\"eps\", \"jpeg\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n", | |
"\n", | |
"mpl.default_extension = \"png\";/* global mpl */\n", | |
"\n", | |
"var comm_websocket_adapter = function (comm) {\n", | |
" // Create a \"websocket\"-like object which calls the given IPython comm\n", | |
" // object with the appropriate methods. Currently this is a non binary\n", | |
" // socket, so there is still some room for performance tuning.\n", | |
" var ws = {};\n", | |
"\n", | |
" ws.close = function () {\n", | |
" comm.close();\n", | |
" };\n", | |
" ws.send = function (m) {\n", | |
" //console.log('sending', m);\n", | |
" comm.send(m);\n", | |
" };\n", | |
" // Register the callback with on_msg.\n", | |
" comm.on_msg(function (msg) {\n", | |
" //console.log('receiving', msg['content']['data'], msg);\n", | |
" // Pass the mpl event to the overridden (by mpl) onmessage function.\n", | |
" ws.onmessage(msg['content']['data']);\n", | |
" });\n", | |
" return ws;\n", | |
"};\n", | |
"\n", | |
"mpl.mpl_figure_comm = function (comm, msg) {\n", | |
" // This is the function which gets called when the mpl process\n", | |
" // starts-up an IPython Comm through the \"matplotlib\" channel.\n", | |
"\n", | |
" var id = msg.content.data.id;\n", | |
" // Get hold of the div created by the display call when the Comm\n", | |
" // socket was opened in Python.\n", | |
" var element = document.getElementById(id);\n", | |
" var ws_proxy = comm_websocket_adapter(comm);\n", | |
"\n", | |
" function ondownload(figure, _format) {\n", | |
" window.open(figure.canvas.toDataURL());\n", | |
" }\n", | |
"\n", | |
" var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n", | |
"\n", | |
" // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n", | |
" // web socket which is closed, not our websocket->open comm proxy.\n", | |
" ws_proxy.onopen();\n", | |
"\n", | |
" fig.parent_element = element;\n", | |
" fig.cell_info = mpl.find_output_cell(\"<div id='\" + id + \"'></div>\");\n", | |
" if (!fig.cell_info) {\n", | |
" console.error('Failed to find cell for figure', id, fig);\n", | |
" return;\n", | |
" }\n", | |
" fig.cell_info[0].output_area.element.on(\n", | |
" 'cleared',\n", | |
" { fig: fig },\n", | |
" fig._remove_fig_handler\n", | |
" );\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_close = function (fig, msg) {\n", | |
" var width = fig.canvas.width / fig.ratio;\n", | |
" fig.cell_info[0].output_area.element.off(\n", | |
" 'cleared',\n", | |
" fig._remove_fig_handler\n", | |
" );\n", | |
" fig.resizeObserverInstance.unobserve(fig.canvas_div);\n", | |
"\n", | |
" // Update the output cell to use the data from the current canvas.\n", | |
" fig.push_to_output();\n", | |
" var dataURL = fig.canvas.toDataURL();\n", | |
" // Re-enable the keyboard manager in IPython - without this line, in FF,\n", | |
" // the notebook keyboard shortcuts fail.\n", | |
" IPython.keyboard_manager.enable();\n", | |
" fig.parent_element.innerHTML =\n", | |
" '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n", | |
" fig.close_ws(fig, msg);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.close_ws = function (fig, msg) {\n", | |
" fig.send_message('closing', msg);\n", | |
" // fig.ws.close()\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.push_to_output = function (_remove_interactive) {\n", | |
" // Turn the data on the canvas into data in the output cell.\n", | |
" var width = this.canvas.width / this.ratio;\n", | |
" var dataURL = this.canvas.toDataURL();\n", | |
" this.cell_info[1]['text/html'] =\n", | |
" '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.updated_canvas_event = function () {\n", | |
" // Tell IPython that the notebook contents must change.\n", | |
" IPython.notebook.set_dirty(true);\n", | |
" this.send_message('ack', {});\n", | |
" var fig = this;\n", | |
" // Wait a second, then push the new image to the DOM so\n", | |
" // that it is saved nicely (might be nice to debounce this).\n", | |
" setTimeout(function () {\n", | |
" fig.push_to_output();\n", | |
" }, 1000);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_toolbar = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var toolbar = document.createElement('div');\n", | |
" toolbar.classList = 'btn-toolbar';\n", | |
" this.root.appendChild(toolbar);\n", | |
"\n", | |
" function on_click_closure(name) {\n", | |
" return function (_event) {\n", | |
" return fig.toolbar_button_onclick(name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" function on_mouseover_closure(tooltip) {\n", | |
" return function (event) {\n", | |
" if (!event.currentTarget.disabled) {\n", | |
" return fig.toolbar_button_onmouseover(tooltip);\n", | |
" }\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" fig.buttons = {};\n", | |
" var buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'btn-group';\n", | |
" var button;\n", | |
" for (var toolbar_ind in mpl.toolbar_items) {\n", | |
" var name = mpl.toolbar_items[toolbar_ind][0];\n", | |
" var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", | |
" var image = mpl.toolbar_items[toolbar_ind][2];\n", | |
" var method_name = mpl.toolbar_items[toolbar_ind][3];\n", | |
"\n", | |
" if (!name) {\n", | |
" /* Instead of a spacer, we start a new button group. */\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
" buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'btn-group';\n", | |
" continue;\n", | |
" }\n", | |
"\n", | |
" button = fig.buttons[name] = document.createElement('button');\n", | |
" button.classList = 'btn btn-default';\n", | |
" button.href = '#';\n", | |
" button.title = name;\n", | |
" button.innerHTML = '<i class=\"fa ' + image + ' fa-lg\"></i>';\n", | |
" button.addEventListener('click', on_click_closure(method_name));\n", | |
" button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", | |
" buttonGroup.appendChild(button);\n", | |
" }\n", | |
"\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
"\n", | |
" // Add the status bar.\n", | |
" var status_bar = document.createElement('span');\n", | |
" status_bar.classList = 'mpl-message pull-right';\n", | |
" toolbar.appendChild(status_bar);\n", | |
" this.message = status_bar;\n", | |
"\n", | |
" // Add the close button to the window.\n", | |
" var buttongrp = document.createElement('div');\n", | |
" buttongrp.classList = 'btn-group inline pull-right';\n", | |
" button = document.createElement('button');\n", | |
" button.classList = 'btn btn-mini btn-primary';\n", | |
" button.href = '#';\n", | |
" button.title = 'Stop Interaction';\n", | |
" button.innerHTML = '<i class=\"fa fa-power-off icon-remove icon-large\"></i>';\n", | |
" button.addEventListener('click', function (_evt) {\n", | |
" fig.handle_close(fig, {});\n", | |
" });\n", | |
" button.addEventListener(\n", | |
" 'mouseover',\n", | |
" on_mouseover_closure('Stop Interaction')\n", | |
" );\n", | |
" buttongrp.appendChild(button);\n", | |
" var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n", | |
" titlebar.insertBefore(buttongrp, titlebar.firstChild);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._remove_fig_handler = function (event) {\n", | |
" var fig = event.data.fig;\n", | |
" if (event.target !== this) {\n", | |
" // Ignore bubbled events from children.\n", | |
" return;\n", | |
" }\n", | |
" fig.close_ws(fig, {});\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._root_extra_style = function (el) {\n", | |
" el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._canvas_extra_style = function (el) {\n", | |
" // this is important to make the div 'focusable\n", | |
" el.setAttribute('tabindex', 0);\n", | |
" // reach out to IPython and tell the keyboard manager to turn it's self\n", | |
" // off when our div gets focus\n", | |
"\n", | |
" // location in version 3\n", | |
" if (IPython.notebook.keyboard_manager) {\n", | |
" IPython.notebook.keyboard_manager.register_events(el);\n", | |
" } else {\n", | |
" // location in version 2\n", | |
" IPython.keyboard_manager.register_events(el);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._key_event_extra = function (event, _name) {\n", | |
" var manager = IPython.notebook.keyboard_manager;\n", | |
" if (!manager) {\n", | |
" manager = IPython.keyboard_manager;\n", | |
" }\n", | |
"\n", | |
" // Check for shift+enter\n", | |
" if (event.shiftKey && event.which === 13) {\n", | |
" this.canvas_div.blur();\n", | |
" // select the cell after this one\n", | |
" var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", | |
" IPython.notebook.select(index + 1);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_save = function (fig, _msg) {\n", | |
" fig.ondownload(fig, null);\n", | |
"};\n", | |
"\n", | |
"mpl.find_output_cell = function (html_output) {\n", | |
" // Return the cell and output element which can be found *uniquely* in the notebook.\n", | |
" // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", | |
" // IPython event is triggered only after the cells have been serialised, which for\n", | |
" // our purposes (turning an active figure into a static one), is too late.\n", | |
" var cells = IPython.notebook.get_cells();\n", | |
" var ncells = cells.length;\n", | |
" for (var i = 0; i < ncells; i++) {\n", | |
" var cell = cells[i];\n", | |
" if (cell.cell_type === 'code') {\n", | |
" for (var j = 0; j < cell.output_area.outputs.length; j++) {\n", | |
" var data = cell.output_area.outputs[j];\n", | |
" if (data.data) {\n", | |
" // IPython >= 3 moved mimebundle to data attribute of output\n", | |
" data = data.data;\n", | |
" }\n", | |
" if (data['text/html'] === html_output) {\n", | |
" return [cell, data, j];\n", | |
" }\n", | |
" }\n", | |
" }\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"// Register the function which deals with the matplotlib target/channel.\n", | |
"// The kernel may be null if the page has been refreshed.\n", | |
"if (IPython.notebook.kernel !== null) {\n", | |
" IPython.notebook.kernel.comm_manager.register_target(\n", | |
" 'matplotlib',\n", | |
" mpl.mpl_figure_comm\n", | |
" );\n", | |
"}\n" | |
], | |
"text/plain": [ | |
"<IPython.core.display.Javascript object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
}, | |
{ | |
"data": { | |
"text/html": [ | |
"<img src=\"data:image/png;base64,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oCVLlqhTp04aP368BgwYoHvuuUelpaW67bbb5JzTT3/608+8faZMmaKoqCj98Ic/1AsvvKD7779f3bt3V0pKymcOwD59+uiyyy7Tb37zG23cuFElJSWaO3du8JhAIKBevXrp6quvDt4ejS+ZN55f9+7dNXbsWD399NN65plndOjQoWYHYFxcnC666CItXLhQL7zwgu6++27FxMQoPz8/eFxLB2BlZaVmz54t55xWr14d8vOSmo6R1v4ML774Yg0aNEhPPPGE1qxZo69+9auKiYnRnj17mv1Z/OUvf1F5eXnIy3Tl5eWS/j48unfvrunTp+u5557T73//e9XX12v69OlyzqmgoEBr167VsmXL1LVrV1144YUh9/u8vDylp6erT58+Wr58uZ5//nnl5+fLOacFCxaoX79+Ki4u1po1a3TNNdcoPj5eVVVVzV7eU2/ve++9V2+88YaioqI0f/784NfbYgD27NlTN998c/C6denSRUOGDNGIESP07//+7yH/LcyePbvJz+LCCy/UFVdcoeLiYj399NO6/vrr5ZzT7373u+Bx3/jGN3TRRReF/PctSd/61reUlZXV7DPTe/fuVUJCgkaMGKGnnnpKL730klasWKFJkyaF/d3SxlauXCnnnB566KFmj2lpH3/8sVJTU/VP//RPkqRf/vKXcs7pV7/6VZNjww3A6upqRUdH6ytf+UrwNAYgtUUMQOrQznYAnv6g/sQTT8g5F3xAlsK/CaTxWZZTW7ZsWcizCI0tWbJEzjm98MILwdOcc0pNTVV1dXXIsY0D4PQHuBtuuEHOOd1///0hp1911VUaMGBA2Ovc2FtvvSXnnL7//e+HnL5ixQo55844AD/88EM55/Rf//VfZ/wzmnsJuPH8Bg8e3OzXTh+Azjn9/Oc/Dzl24cKFcs7p1VdfldTyASid+SXg08dIa3+G3bp1C3km7P3331d0dLQWL17c5M8KdzlPf5Bu/PlPnjw55PTGn+F3v/vdkNO3bNki55zmzZsXcp2cc/rjH/8YPO3QoUPq1KmTEhMTQ8beG2+8EfYl3dM7dQBK0oQJE5SUlBT8vcW2GICjR48OOW7OnDlyzul73/teyOk33HCD0tLSQk5zzikxMVHvv/9+8LT6+nr17dtXl112WZM/68knnwyeVlVVpZiYGC1YsKDZ6//73/9ezjm98cYbzR4TrqKiIjnntHbt2lZ9X7gee+wxOee0bNkySdLRo0fVpUsXXXvttU2ObbyvfPrpp/rkk0/01ltvBV++f/DBB4PHMQCpLWIAUod2tgOw8S/Txnbu3CnnnFatWhU8raUD8MYbb1RSUlKT39f64IMP5JzTbbfdFjzNOadvfOMbTS7nqS8Bntodd9wh51yTN5uMHz9e6enpYa9zY0uXLm0yCCTp008/VUxMzBkHYENDgy699FJ1795dP/3pTxUIBMK+Y/GzBuDpgy7cnyX9fQB++OGHIcc2DpAf//jHIf+7rQdga3+GN910U5PzzMjI0MyZM5ucHu5yNjcA//d//zfk9Maf4WuvvdbkfC6//HJ96UtfCrlOmZmZTY7LzMzUl7/85ZDTjh8/Luec5s6de8bLevoA3Lt3r+Li4oLXsy0G4MMPPxxy3MMPP3zG/xZOfRnYORfyDHFjjZfr1JfJ+/fvH/J7cPPnz1dsbOwZ34Tz9ttvKy4uToMGDdKvfvWrMz7De2qtHYD19fX69NNPg079by0vL0+JiYmqqakJnvbtb39bzjnt3r075HzC/YpBamqq/vM//zPkOAYgtUUMQOrQfvzjH8s5F/IMwKn16dMn5C/9xgedU18eksIPi5YOwGHDhunSSy8N++fHxMRo6tSpwf8d7tkc6e8D4PXXXw85vbmB25I3TTTeNvv372/ytW7dun3mS8DvvvuuvvOd76hbt25yziktLU2zZ88Oeebrswbg6c+oNfdnTZkyRTExMU2OPXbsmJxzmjNnjiS7Adjan2G437Nq6RtrzjQATx96jT/D03/fr/Eyn/osV7j7ZuPlCvdg39z1OLXTB6B08hm6mJgY7d69u00G4On/LbbmvwXnXMjPprGHHnqoyTN3y5cvV1RUlHbu3KlPPvlEGRkZGj9+/BmvvyS9/PLLys/PV1JSkpw7+W7oz3pmvLUvAZ/++7SN9+M///nPioqK0tixY3X48OGgZ599Vs453X777SHn45zTjTfeqNdff11//OMftWvXriYvezf+eQxA8o0BSB3a//zP/8g5p//7v/9r8rWGhgalpKRowoQJwdMsBuCNN96oLl26NPvs0al/STf3oGsxAH2eATy9Xbt26cc//rE6deqkGTNmBE//rAF4+u3c3J/V0mcADx48GPYZ3MaXrH2eAfT9GbbFADz95/9ZzwBec801IdepPQbghx9+qJSUFI0dOzbs/bNPnz5hnyFt7tl43wHY0mcAjx07pvPPP1+zZ88O/hpE468WtKT6+npt3rxZEyZMkHPhPxexsda+CWTbtm16/fXXgxpfrm981rM5mZmZIQOvJT9TiQFIbRMDkDq0t99+W1FRUfrRj37U5Gtr1qxpMuosBmDjS1arV68OOb1xgJSWlgZPa88BuGPHDjl3dr8D2FxXXXVV8JfRJWnAgAEaNGhQk+POdgA29zuAr7zyiqSToz4hIaHJs6jLly9vMgAfeOABOee0Y8eOJpfh9AHYFj9DiwHY+KsJp/8+3GuvvSbnnO68886Q69QeA1D6+8+l8ffLTr1/jhw5UldccUXI8bt27VJMTIzJAGzudwDDPaM7b948paSk6B//8R911VVXnfG6N1dNTY2cc/rhD394xuM+62Ng3n777TN+DEx9fb2ysrJ06aWX6sUXX2xi7ty5cs6FvPucAUjtGQOQOrzZs2crKipK06dP11NPPaXnn39e99xzj7p06aKBAwfq+PHjwWMtBmDjO0iTk5N1//33q7S0VIWFhYqNjQ37DtL2GoCSNHHixOBAbnwXcFZW1me+C/hPf/qTrr32Wj3wwAN67rnntH79et15552Kjo4OeePBlClTFB8fr1WrVum1117Ttm3bQs6vNQOwuXcBf+1rXwv5/qlTpyohIUE//elPtW7dOi1atEg5OTlNBmDjnzNjxgyVlZXp9ddfD7583dy7gH1+hhYDUJKmT5+uqKgozZkzR88//7wefvhhXXDBBbrwwgtDnjFtzwH4t7/9TVlZWcFnok69f/72t7+Vc06zZs3SunXrtHz5cvXp00eZmZkmA7C5dwGf+vu8je3fv18xMTFyzumXv/zlGa+7dPKl5G9961v61a9+pQ0bNmjNmjUaO3asnGv6O4qnd+oHQf/bv/2bfve73+nll1/W6tWrNWvWLCUkJJzxg6D/8Ic/yDmnJUuWhP36X//6V8XHx+uGG24IuT0YgNReMQCpw2toaNBDDz2kgQMHqnPnzoqLi1N2drZuu+22Jp8bZjEApZPvtpw5c6YyMzMVExOjnj176o477mj2M+ROz2oAHj9+XHPnztUFF1yghIQEXXPNNSovL//MzwH84IMPdPPNN6tv375KSkpSly5dlJubq5/97GchLzm9++67uu6665ScnCznmn4OYGsGYFJSkrZt26avfvWrSkxMVFpammbNmqWPPvoo5Ptra2s1depUdevWTUlJSRo9erTefffdJgNQOvkSWlZWlqKjo0P+zOY+B9DnZ2g1ABs/B7B3796KjY3V+eefr4kTJzb7OYDhLldbD0Dp779+cfr9s6GhQT/5yU/Uq1cvJSQkaODAgdqwYYPZ7wDecsstWrp0qS699FLFxsaqb9++WrFiRbPX6atf/arS0tJa9AHs5eXl+sY3vqGePXsqPj5e6enpysvL09NPP/2Z3yudfBbv17/+tYYOHaq0tLTgPwX3ta99TStXrjzjPwV3ww03KC4uTn/5y1+aPeamm25STExM8BlQBiC1ZwxAIiLqkFo6eBr74IMPlJCQ8Jkv3xLRZ8cAJCKiDqmlA7CyslIbN25Ufn6+OnfuHPad8UTUuhiARETUIbV0ABYWFioqKkqXXHJJyIdBE9HZxwAkIiIiirAYgEREREQRFgOQiIiIKMJiABIRERFFWAxAIiIiogiLAejRiRMnVFlZqZqaGtXW1gIAgHNATU2NKisrz/hh3l/0GIAeVVZWnvEf+gYAAJ9fp/+LPJEUA9Cjxn9UvLKyssP/3wwAAGiZxidwampqOnpKdFgMQI9qa2vlnFNtbW1HXxQiIiJqYTx+MwC94g5ERER07sXjNwPQK+5ARERE5148fjMAveIOREREdO7F4zcD0CvuQEREROdePH4zAL3iDkRERHTuxeM3A9Ar7kBERETnXjx+MwC94g5ERER07sXjNwPQK+5ARERE5148fjMAveIOREREdO7F4zcD0CvuQEREROdePH4zAL3iDkRERHTuxeM3A9Ar7kBERETnXjx+MwC94g5ERER07sXjNwPQK+5ARERE5148fjMAveIO1LSetz1zziFqro6+b0bK/bmjb7NIuZ3p7/H4zQD0yvoO1NF/wQE495yLdfRths8vqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGICGA3DRokVyzunWW28NntbQ0KDCwkJlZmYqISFBeXl52r59e8j31dXVqaCgQOnp6ercubNGjx6tysrKkGOqq6s1ceJEpaSkKCUlRRMnTtThw4dDjtm3b5/y8/PVuXNnpaena/bs2Tp+/HjIMdu2bdPgwYOVkJCgrKwsLViwQA0NDS2+jgxAAADsWMUANBqAr732mi6++GLl5uaGDMCioiIlJyerpKREFRUVGjdunDIzM3XkyJHgMTNnzlT37t1VWlqqQCCgIUOGqH///qqvrw8ec/311ysnJ0dlZWUqKytTTk6O8vPzg1+vr69XTk6OhgwZokAgoNLSUmVlZamgoCB4TG1trbp166abbrpJFRUVKikpUXJysu67774WX08GIAAAdqxiABoMwKNHjyo7O1ulpaXKy8sLDsCGhgZlZGSoqKgoeGxdXZ1SU1O1bNkySVJNTY1iY2O1atWq4DFVVVWKjo7W2rVrJUk7duyQc06bN28OHlNeXi7nnHbu3ClJWrNmjaKjo1VVVRU8pri4WPHx8cEf9tKlS5Wamqq6urrgMYsXL1ZWVlaLnwVkAAIAYMcqBqDBAJw8ebLmzJkjSSEDcM+ePXLOKRAIhBw/ZswYTZ48WZK0fv16OedUXV0dckxubq7uuusuSdLy5cuVmpra5M9NTU3VI488IkmaP3++cnNzQ75eXV0t55w2bNggSZo0aZLGjBkTckwgEJBzTu+8806LrisDEAAAO1YxANt4ABYXFysnJ0fHjh2TFDoAN23aJOdcyLNykjRt2jRdd911kqQVK1YoLi6uyfmOGDFC06dPlyQtXLhQ2dnZTY7Jzs7WokWLguc5YsSIJsfExcVp5cqVwfOcNm1ayNerqqrknFNZWVnY61dXV6fa2tqgyspKBiAAAEasYgC24QB87733dMEFF+iNN94InhZuAB44cCDk+6ZOnaqRI0dKan4A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gB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiAbTwAN27cqPz8fGVmZso5pyeffDLk6w0NDSosLFRmZqYSEhKUl5en7du3hxxTV1engoICpaenq3Pnzho9erQqKytDjqmurtbEiROVkpKilJQUTZw4UYcPHw45Zt++fcrPz1fnzp2Vnp6u2bNn6/jx4yHHbNu2TYMHD1ZCQoKysrK0YMECNTQ0tPj6MgABALBjFQOwjQfgmjVrdOedd6qkpCTsACwqKlJycrJKSkpUUVGhcePGKTMzU0eOHAkeM3PmTHXv3l2lpaUKBAIaMmSI+vfvr/r6+uAx119/vXJyclRWVqay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evVqxcTE6JNPPjG7rOd6bfHvPZ84cUIXXnih/vu//9viIn4hOtvb+ZFHHtHAgQP16aefMgBb0NnczrNmzdKwYcN02223KSsrS9nZ2Zo7d64+/vjj9rjI52Rncztv2rRJcXFxevbZZ9XQ0KD3339fgwcP1owZM9rjIn8haskA5LGwbWMAtnNVVVVyzjV5CWbhwoXq3bt32O/Jzs7WwoULQ07btGmTnHM6cOCA2WU91zub2/r0fvKTnygtLU0ffPCBxUX8QnQ2t/Pu3bt1wQUXaNeuXZLEAGxBZ3M7jxw5UvHx8Ro1apS2bNmiZ599Vj179tS3v/3t9rjI52Rn+/fG7373O3Xp0kUxMTFyzmnMmDGMklbUkgHIY2HbxgBs5xr/cikrKws5/Z577lGfPn3Cfk92drYWLVoUctqrr74q55wOHjxodlnP9c7mtj61lStXqnPnziotLbW6iF+IWns719fXa+DAgSG/vM0A/OzO5v48YsQIJSQkqKamJnhaSUmJoqKieBawmc7mdn7zzTeVmZmpn/zkJ/rTn/6ktWvXql+/fvrOd77THhf5C1FLByCPhW0XA7Cd4yXg9svnJeBVq1YpMTFRzzzzjOVF/ELU2tv58OHDcs6pU6dOQVFRUcHT1q9f314X/ZzqbO7PkydP1qWXXhpy2o4dO+Sc0+7du80u67nc2dzOEydO1NixY0NOe+WVV3hmqhXxEnD7xwDsgAYNGqRZs2aFnHb55Zef8U0gl19+echpM2fO5BdfW1Brb2vp5DN/CQkJLfpIAjpZa27nEydOqKKiIsSsWbPUp08fVVRU6KOPPmqvi33O1dr788MPP6zExEQdPXo0eNpTTz2l6OhongE8Q629nb/5zW/qxhtvDDmtrKxMzjlVVVWZXc4vUi19EwiPhW0XA7ADavyIgeXLl2vHjh2aM2eOkpKS9O6770qSbr/9dk2aNCl4fONb37///e9rx44dWr58OW99b2Gtva1XrlypmJgYPfjggzp48GDQqS+hUdNaezufHi8Bt6zW3s5Hjx5Vjx49NHbsWL355pvauHGjsrOzNXXq1I66CudErb2dH330UcXExGjp0qXas2ePXn31VQ0cOFCDBg3qqKtwTnT06FFt3bpVW7dulXNO999/v7Zu3Rr8uB0eC21jAHZQDz74oHr27Km4uDgNGDBAGzduDH5typQpysvLCzn+pZde0tVXX624uDhdfPHFfPhlK2rNbZ2XlyfnXBNTpkxp/wt+jtXa+/SpMQBbXmtv57feekvDhw9XYmKievTooR/84Ac8+9eCWns7P/DAA7riiiuUmJiozMxMTZgwQfv372/nS31u9eKLL57x71seC21jABIRERFFWAxAIiIiogiLAUhEREQUYTEAiYiIiCIsBiARERFRhMUAJCIiIoqwGIBEREREERYDkIiIiCjCYgASERERRVgMQCIiIqIIiwFIREREFGExAImIiIgiLAYgERERUYTFACQiIiKKsBiARERERBEWA5CIiIgowmIAEhEREUVYDEAiIiKiCIsBSERERBRhMQCJiIiIIiwGIBEREVGExQAkIiIiirAYgEREREQRFgOQiIiIKMJiABIRERFFWAxAIiIiogiLAUhEREQUYTEAiYiIiCIsBiARERFRhMUAJCIiIoqwGIBEREREERYDkIiIiCjC+n8G0wZyNuQR9gAAAABJRU5ErkJggg==\" width=\"640\">" | |
], | |
"text/plain": [ | |
"<IPython.core.display.HTML object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"print(\"Performance obtained from the C-API of Numpy\")\n", | |
"%timeit cython_uniform_cnp(shape)\n", | |
"fig, ax = subplots()\n", | |
"ax.hist(cython_uniform_cnp(shape).ravel())\n", | |
"ax.set_title(\"Uniform distribution from Numpy's C-API\")\n", | |
"pass" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"Finally ! this solution gives faster random number than both the C and C++ version, and even slightly faster than the numpy version. But this is normal: I removed most of the flexibility offered by numpy and tailored it to my needs.\n", | |
"\n", | |
"## Cython written Mersenne-twisters\n", | |
"\n", | |
"As an alternative, I found the same algorithm we tested in C++, already implemented in Cython:\n", | |
"https://github.com/ananswam/cython_random\n", | |
"\n", | |
"This again the Mersenne-twister. Let's see how it behaves:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 11, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"%%cython\n", | |
"# cython: boundscheck=False\n", | |
"# cython: cdivision=True\n", | |
"# cython: wraparound=False\n", | |
"import numpy\n", | |
"cimport numpy as np\n", | |
"import time\n", | |
"\n", | |
"cdef extern from \"stdlib.h\":\n", | |
" cdef int RAND_MAX # Need to this to normalize appropriately to uniform dist\n", | |
"\n", | |
"# MT Stuff\n", | |
"\n", | |
"cdef unsigned NN = 312\n", | |
"cdef unsigned MM = 156\n", | |
"cdef unsigned long long MATRIX_A = 0xB5026F5AA96619E9ULL\n", | |
"cdef unsigned long long UM = 0xFFFFFFFF80000000ULL\n", | |
"cdef unsigned long long LM = 0x7FFFFFFFULL\n", | |
"cdef unsigned long long mt[312]\n", | |
"cdef unsigned mti = NN + 1\n", | |
"cdef unsigned long long mag01[2]\n", | |
"\n", | |
"cdef mt_seed(unsigned long long seed):\n", | |
" global mt\n", | |
" global mti\n", | |
" global mag01\n", | |
" global NN\n", | |
" global MATRIX_A\n", | |
" mt[0] = seed\n", | |
" for mti in range(1,NN):\n", | |
" mt[mti] = (6364136223846793005ULL * (mt[mti-1] ^ (mt[mti-1] >> 62)) + mti)\n", | |
"\n", | |
" mag01[0] = 0ULL\n", | |
" mag01[1] = MATRIX_A\n", | |
" mti = NN\n", | |
"\n", | |
"\n", | |
"cdef unsigned long long genrand64() nogil:\n", | |
" cdef int i\n", | |
" cdef unsigned long long x\n", | |
" global mag01\n", | |
" global mti\n", | |
" global mt\n", | |
" global NN\n", | |
" global MM\n", | |
" global UM\n", | |
" global LM\n", | |
"\n", | |
" if mti >= NN:\n", | |
" for i in range(NN-MM):\n", | |
" x = (mt[i]&UM) | (mt[i+1]&LM)\n", | |
" mt[i] = mt[i+MM] ^ (x>>1) ^ mag01[int(x&1ULL)]\n", | |
"\n", | |
" for i in range(NN-MM, NN-1):\n", | |
" x = (mt[i]&UM)|(mt[i+1]&LM)\n", | |
" mt[i] = mt[i+(MM-NN)] ^ (x>>1) ^ mag01[int(x&1ULL)]\n", | |
"\n", | |
" x = (mt[NN-1]&UM)|(mt[0]&LM)\n", | |
" mt[NN-1] = mt[MM-1] ^ (x>>1) ^ mag01[int(x&1ULL)]\n", | |
" mti = 0\n", | |
"\n", | |
" x = mt[mti]\n", | |
" mti += 1\n", | |
" x ^= (x >> 29) & 0x5555555555555555ULL\n", | |
" x ^= (x << 17) & 0x71D67FFFEDA60000ULL\n", | |
" x ^= (x << 37) & 0xFFF7EEE000000000ULL\n", | |
" x ^= (x >> 43);\n", | |
"\n", | |
" return x\n", | |
"\n", | |
"def py_rand_int():\n", | |
" return genrand64()\n", | |
"\n", | |
"# Functions\n", | |
"\n", | |
"# Seed the random number generator\n", | |
"cdef seed_random(unsigned long long seed):\n", | |
" \"\"\"\n", | |
" Seed the C random number generator with the current system time.\n", | |
" :return: none\n", | |
" \"\"\"\n", | |
" if seed == 0:\n", | |
" mt_seed(time.time())\n", | |
" else:\n", | |
" mt_seed(seed)\n", | |
"\n", | |
"def py_seed_random(unsigned long long seed = 0):\n", | |
" seed_random(seed)\n", | |
"\n", | |
"cdef double uniform_rv() nogil:\n", | |
" \"\"\"\n", | |
" Generate a uniform random variable in [0,1]\n", | |
" :return: (double) a random uniform number in [0,1]\n", | |
" \"\"\"\n", | |
" return (genrand64() >> 11) * (1.0/9007199254740991.0)\n", | |
"\n", | |
"def cython_uniform_mt(shape):\n", | |
" cdef Py_ssize_t size = numpy.prod(shape), idx\n", | |
" cdef double[::1] ary = numpy.empty(size)\n", | |
" mt_seed(time.time_ns())\n", | |
" with nogil:\n", | |
" for idx in range(size):\n", | |
" ary[idx] = uniform_rv()\n", | |
" return numpy.asarray(ary).reshape(shape)" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 12, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"Performances of MT64 implemented in Cython\n", | |
"21.3 ms ± 199 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" | |
] | |
}, | |
{ | |
"data": { | |
"application/javascript": [ | |
"/* Put everything inside the global mpl namespace */\n", | |
"/* global mpl */\n", | |
"window.mpl = {};\n", | |
"\n", | |
"mpl.get_websocket_type = function () {\n", | |
" if (typeof WebSocket !== 'undefined') {\n", | |
" return WebSocket;\n", | |
" } else if (typeof MozWebSocket !== 'undefined') {\n", | |
" return MozWebSocket;\n", | |
" } else {\n", | |
" alert(\n", | |
" 'Your browser does not have WebSocket support. ' +\n", | |
" 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", | |
" 'Firefox 4 and 5 are also supported but you ' +\n", | |
" 'have to enable WebSockets in about:config.'\n", | |
" );\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n", | |
" this.id = figure_id;\n", | |
"\n", | |
" this.ws = websocket;\n", | |
"\n", | |
" this.supports_binary = this.ws.binaryType !== undefined;\n", | |
"\n", | |
" if (!this.supports_binary) {\n", | |
" var warnings = document.getElementById('mpl-warnings');\n", | |
" if (warnings) {\n", | |
" warnings.style.display = 'block';\n", | |
" warnings.textContent =\n", | |
" 'This browser does not support binary websocket messages. ' +\n", | |
" 'Performance may be slow.';\n", | |
" }\n", | |
" }\n", | |
"\n", | |
" this.imageObj = new Image();\n", | |
"\n", | |
" this.context = undefined;\n", | |
" this.message = undefined;\n", | |
" this.canvas = undefined;\n", | |
" this.rubberband_canvas = undefined;\n", | |
" this.rubberband_context = undefined;\n", | |
" this.format_dropdown = undefined;\n", | |
"\n", | |
" this.image_mode = 'full';\n", | |
"\n", | |
" this.root = document.createElement('div');\n", | |
" this.root.setAttribute('style', 'display: inline-block');\n", | |
" this._root_extra_style(this.root);\n", | |
"\n", | |
" parent_element.appendChild(this.root);\n", | |
"\n", | |
" this._init_header(this);\n", | |
" this._init_canvas(this);\n", | |
" this._init_toolbar(this);\n", | |
"\n", | |
" var fig = this;\n", | |
"\n", | |
" this.waiting = false;\n", | |
"\n", | |
" this.ws.onopen = function () {\n", | |
" fig.send_message('supports_binary', { value: fig.supports_binary });\n", | |
" fig.send_message('send_image_mode', {});\n", | |
" if (fig.ratio !== 1) {\n", | |
" fig.send_message('set_dpi_ratio', { dpi_ratio: fig.ratio });\n", | |
" }\n", | |
" fig.send_message('refresh', {});\n", | |
" };\n", | |
"\n", | |
" this.imageObj.onload = function () {\n", | |
" if (fig.image_mode === 'full') {\n", | |
" // Full images could contain transparency (where diff images\n", | |
" // almost always do), so we need to clear the canvas so that\n", | |
" // there is no ghosting.\n", | |
" fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", | |
" }\n", | |
" fig.context.drawImage(fig.imageObj, 0, 0);\n", | |
" };\n", | |
"\n", | |
" this.imageObj.onunload = function () {\n", | |
" fig.ws.close();\n", | |
" };\n", | |
"\n", | |
" this.ws.onmessage = this._make_on_message_function(this);\n", | |
"\n", | |
" this.ondownload = ondownload;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_header = function () {\n", | |
" var titlebar = document.createElement('div');\n", | |
" titlebar.classList =\n", | |
" 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n", | |
" var titletext = document.createElement('div');\n", | |
" titletext.classList = 'ui-dialog-title';\n", | |
" titletext.setAttribute(\n", | |
" 'style',\n", | |
" 'width: 100%; text-align: center; padding: 3px;'\n", | |
" );\n", | |
" titlebar.appendChild(titletext);\n", | |
" this.root.appendChild(titlebar);\n", | |
" this.header = titletext;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n", | |
"\n", | |
"mpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n", | |
"\n", | |
"mpl.figure.prototype._init_canvas = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var canvas_div = (this.canvas_div = document.createElement('div'));\n", | |
" canvas_div.setAttribute(\n", | |
" 'style',\n", | |
" 'border: 1px solid #ddd;' +\n", | |
" 'box-sizing: content-box;' +\n", | |
" 'clear: both;' +\n", | |
" 'min-height: 1px;' +\n", | |
" 'min-width: 1px;' +\n", | |
" 'outline: 0;' +\n", | |
" 'overflow: hidden;' +\n", | |
" 'position: relative;' +\n", | |
" 'resize: both;'\n", | |
" );\n", | |
"\n", | |
" function on_keyboard_event_closure(name) {\n", | |
" return function (event) {\n", | |
" return fig.key_event(event, name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" canvas_div.addEventListener(\n", | |
" 'keydown',\n", | |
" on_keyboard_event_closure('key_press')\n", | |
" );\n", | |
" canvas_div.addEventListener(\n", | |
" 'keyup',\n", | |
" on_keyboard_event_closure('key_release')\n", | |
" );\n", | |
"\n", | |
" this._canvas_extra_style(canvas_div);\n", | |
" this.root.appendChild(canvas_div);\n", | |
"\n", | |
" var canvas = (this.canvas = document.createElement('canvas'));\n", | |
" canvas.classList.add('mpl-canvas');\n", | |
" canvas.setAttribute('style', 'box-sizing: content-box;');\n", | |
"\n", | |
" this.context = canvas.getContext('2d');\n", | |
"\n", | |
" var backingStore =\n", | |
" this.context.backingStorePixelRatio ||\n", | |
" this.context.webkitBackingStorePixelRatio ||\n", | |
" this.context.mozBackingStorePixelRatio ||\n", | |
" this.context.msBackingStorePixelRatio ||\n", | |
" this.context.oBackingStorePixelRatio ||\n", | |
" this.context.backingStorePixelRatio ||\n", | |
" 1;\n", | |
"\n", | |
" this.ratio = (window.devicePixelRatio || 1) / backingStore;\n", | |
"\n", | |
" var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n", | |
" 'canvas'\n", | |
" ));\n", | |
" rubberband_canvas.setAttribute(\n", | |
" 'style',\n", | |
" 'box-sizing: content-box; position: absolute; left: 0; top: 0; z-index: 1;'\n", | |
" );\n", | |
"\n", | |
" // Apply a ponyfill if ResizeObserver is not implemented by browser.\n", | |
" if (this.ResizeObserver === undefined) {\n", | |
" if (window.ResizeObserver !== undefined) {\n", | |
" this.ResizeObserver = window.ResizeObserver;\n", | |
" } else {\n", | |
" var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n", | |
" this.ResizeObserver = obs.ResizeObserver;\n", | |
" }\n", | |
" }\n", | |
"\n", | |
" this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n", | |
" var nentries = entries.length;\n", | |
" for (var i = 0; i < nentries; i++) {\n", | |
" var entry = entries[i];\n", | |
" var width, height;\n", | |
" if (entry.contentBoxSize) {\n", | |
" if (entry.contentBoxSize instanceof Array) {\n", | |
" // Chrome 84 implements new version of spec.\n", | |
" width = entry.contentBoxSize[0].inlineSize;\n", | |
" height = entry.contentBoxSize[0].blockSize;\n", | |
" } else {\n", | |
" // Firefox implements old version of spec.\n", | |
" width = entry.contentBoxSize.inlineSize;\n", | |
" height = entry.contentBoxSize.blockSize;\n", | |
" }\n", | |
" } else {\n", | |
" // Chrome <84 implements even older version of spec.\n", | |
" width = entry.contentRect.width;\n", | |
" height = entry.contentRect.height;\n", | |
" }\n", | |
"\n", | |
" // Keep the size of the canvas and rubber band canvas in sync with\n", | |
" // the canvas container.\n", | |
" if (entry.devicePixelContentBoxSize) {\n", | |
" // Chrome 84 implements new version of spec.\n", | |
" canvas.setAttribute(\n", | |
" 'width',\n", | |
" entry.devicePixelContentBoxSize[0].inlineSize\n", | |
" );\n", | |
" canvas.setAttribute(\n", | |
" 'height',\n", | |
" entry.devicePixelContentBoxSize[0].blockSize\n", | |
" );\n", | |
" } else {\n", | |
" canvas.setAttribute('width', width * fig.ratio);\n", | |
" canvas.setAttribute('height', height * fig.ratio);\n", | |
" }\n", | |
" canvas.setAttribute(\n", | |
" 'style',\n", | |
" 'width: ' + width + 'px; height: ' + height + 'px;'\n", | |
" );\n", | |
"\n", | |
" rubberband_canvas.setAttribute('width', width);\n", | |
" rubberband_canvas.setAttribute('height', height);\n", | |
"\n", | |
" // And update the size in Python. We ignore the initial 0/0 size\n", | |
" // that occurs as the element is placed into the DOM, which should\n", | |
" // otherwise not happen due to the minimum size styling.\n", | |
" if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n", | |
" fig.request_resize(width, height);\n", | |
" }\n", | |
" }\n", | |
" });\n", | |
" this.resizeObserverInstance.observe(canvas_div);\n", | |
"\n", | |
" function on_mouse_event_closure(name) {\n", | |
" return function (event) {\n", | |
" return fig.mouse_event(event, name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mousedown',\n", | |
" on_mouse_event_closure('button_press')\n", | |
" );\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseup',\n", | |
" on_mouse_event_closure('button_release')\n", | |
" );\n", | |
" // Throttle sequential mouse events to 1 every 20ms.\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mousemove',\n", | |
" on_mouse_event_closure('motion_notify')\n", | |
" );\n", | |
"\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseenter',\n", | |
" on_mouse_event_closure('figure_enter')\n", | |
" );\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseleave',\n", | |
" on_mouse_event_closure('figure_leave')\n", | |
" );\n", | |
"\n", | |
" canvas_div.addEventListener('wheel', function (event) {\n", | |
" if (event.deltaY < 0) {\n", | |
" event.step = 1;\n", | |
" } else {\n", | |
" event.step = -1;\n", | |
" }\n", | |
" on_mouse_event_closure('scroll')(event);\n", | |
" });\n", | |
"\n", | |
" canvas_div.appendChild(canvas);\n", | |
" canvas_div.appendChild(rubberband_canvas);\n", | |
"\n", | |
" this.rubberband_context = rubberband_canvas.getContext('2d');\n", | |
" this.rubberband_context.strokeStyle = '#000000';\n", | |
"\n", | |
" this._resize_canvas = function (width, height, forward) {\n", | |
" if (forward) {\n", | |
" canvas_div.style.width = width + 'px';\n", | |
" canvas_div.style.height = height + 'px';\n", | |
" }\n", | |
" };\n", | |
"\n", | |
" // Disable right mouse context menu.\n", | |
" this.rubberband_canvas.addEventListener('contextmenu', function (_e) {\n", | |
" event.preventDefault();\n", | |
" return false;\n", | |
" });\n", | |
"\n", | |
" function set_focus() {\n", | |
" canvas.focus();\n", | |
" canvas_div.focus();\n", | |
" }\n", | |
"\n", | |
" window.setTimeout(set_focus, 100);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_toolbar = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var toolbar = document.createElement('div');\n", | |
" toolbar.classList = 'mpl-toolbar';\n", | |
" this.root.appendChild(toolbar);\n", | |
"\n", | |
" function on_click_closure(name) {\n", | |
" return function (_event) {\n", | |
" return fig.toolbar_button_onclick(name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" function on_mouseover_closure(tooltip) {\n", | |
" return function (event) {\n", | |
" if (!event.currentTarget.disabled) {\n", | |
" return fig.toolbar_button_onmouseover(tooltip);\n", | |
" }\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" fig.buttons = {};\n", | |
" var buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'mpl-button-group';\n", | |
" for (var toolbar_ind in mpl.toolbar_items) {\n", | |
" var name = mpl.toolbar_items[toolbar_ind][0];\n", | |
" var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", | |
" var image = mpl.toolbar_items[toolbar_ind][2];\n", | |
" var method_name = mpl.toolbar_items[toolbar_ind][3];\n", | |
"\n", | |
" if (!name) {\n", | |
" /* Instead of a spacer, we start a new button group. */\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
" buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'mpl-button-group';\n", | |
" continue;\n", | |
" }\n", | |
"\n", | |
" var button = (fig.buttons[name] = document.createElement('button'));\n", | |
" button.classList = 'mpl-widget';\n", | |
" button.setAttribute('role', 'button');\n", | |
" button.setAttribute('aria-disabled', 'false');\n", | |
" button.addEventListener('click', on_click_closure(method_name));\n", | |
" button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", | |
"\n", | |
" var icon_img = document.createElement('img');\n", | |
" icon_img.src = '_images/' + image + '.png';\n", | |
" icon_img.srcset = '_images/' + image + '_large.png 2x';\n", | |
" icon_img.alt = tooltip;\n", | |
" button.appendChild(icon_img);\n", | |
"\n", | |
" buttonGroup.appendChild(button);\n", | |
" }\n", | |
"\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
"\n", | |
" var fmt_picker = document.createElement('select');\n", | |
" fmt_picker.classList = 'mpl-widget';\n", | |
" toolbar.appendChild(fmt_picker);\n", | |
" this.format_dropdown = fmt_picker;\n", | |
"\n", | |
" for (var ind in mpl.extensions) {\n", | |
" var fmt = mpl.extensions[ind];\n", | |
" var option = document.createElement('option');\n", | |
" option.selected = fmt === mpl.default_extension;\n", | |
" option.innerHTML = fmt;\n", | |
" fmt_picker.appendChild(option);\n", | |
" }\n", | |
"\n", | |
" var status_bar = document.createElement('span');\n", | |
" status_bar.classList = 'mpl-message';\n", | |
" toolbar.appendChild(status_bar);\n", | |
" this.message = status_bar;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n", | |
" // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n", | |
" // which will in turn request a refresh of the image.\n", | |
" this.send_message('resize', { width: x_pixels, height: y_pixels });\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.send_message = function (type, properties) {\n", | |
" properties['type'] = type;\n", | |
" properties['figure_id'] = this.id;\n", | |
" this.ws.send(JSON.stringify(properties));\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.send_draw_message = function () {\n", | |
" if (!this.waiting) {\n", | |
" this.waiting = true;\n", | |
" this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_save = function (fig, _msg) {\n", | |
" var format_dropdown = fig.format_dropdown;\n", | |
" var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n", | |
" fig.ondownload(fig, format);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_resize = function (fig, msg) {\n", | |
" var size = msg['size'];\n", | |
" if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n", | |
" fig._resize_canvas(size[0], size[1], msg['forward']);\n", | |
" fig.send_message('refresh', {});\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_rubberband = function (fig, msg) {\n", | |
" var x0 = msg['x0'] / fig.ratio;\n", | |
" var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n", | |
" var x1 = msg['x1'] / fig.ratio;\n", | |
" var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n", | |
" x0 = Math.floor(x0) + 0.5;\n", | |
" y0 = Math.floor(y0) + 0.5;\n", | |
" x1 = Math.floor(x1) + 0.5;\n", | |
" y1 = Math.floor(y1) + 0.5;\n", | |
" var min_x = Math.min(x0, x1);\n", | |
" var min_y = Math.min(y0, y1);\n", | |
" var width = Math.abs(x1 - x0);\n", | |
" var height = Math.abs(y1 - y0);\n", | |
"\n", | |
" fig.rubberband_context.clearRect(\n", | |
" 0,\n", | |
" 0,\n", | |
" fig.canvas.width / fig.ratio,\n", | |
" fig.canvas.height / fig.ratio\n", | |
" );\n", | |
"\n", | |
" fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_figure_label = function (fig, msg) {\n", | |
" // Updates the figure title.\n", | |
" fig.header.textContent = msg['label'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_cursor = function (fig, msg) {\n", | |
" var cursor = msg['cursor'];\n", | |
" switch (cursor) {\n", | |
" case 0:\n", | |
" cursor = 'pointer';\n", | |
" break;\n", | |
" case 1:\n", | |
" cursor = 'default';\n", | |
" break;\n", | |
" case 2:\n", | |
" cursor = 'crosshair';\n", | |
" break;\n", | |
" case 3:\n", | |
" cursor = 'move';\n", | |
" break;\n", | |
" }\n", | |
" fig.rubberband_canvas.style.cursor = cursor;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_message = function (fig, msg) {\n", | |
" fig.message.textContent = msg['message'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_draw = function (fig, _msg) {\n", | |
" // Request the server to send over a new figure.\n", | |
" fig.send_draw_message();\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_image_mode = function (fig, msg) {\n", | |
" fig.image_mode = msg['mode'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n", | |
" for (var key in msg) {\n", | |
" if (!(key in fig.buttons)) {\n", | |
" continue;\n", | |
" }\n", | |
" fig.buttons[key].disabled = !msg[key];\n", | |
" fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n", | |
" if (msg['mode'] === 'PAN') {\n", | |
" fig.buttons['Pan'].classList.add('active');\n", | |
" fig.buttons['Zoom'].classList.remove('active');\n", | |
" } else if (msg['mode'] === 'ZOOM') {\n", | |
" fig.buttons['Pan'].classList.remove('active');\n", | |
" fig.buttons['Zoom'].classList.add('active');\n", | |
" } else {\n", | |
" fig.buttons['Pan'].classList.remove('active');\n", | |
" fig.buttons['Zoom'].classList.remove('active');\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.updated_canvas_event = function () {\n", | |
" // Called whenever the canvas gets updated.\n", | |
" this.send_message('ack', {});\n", | |
"};\n", | |
"\n", | |
"// A function to construct a web socket function for onmessage handling.\n", | |
"// Called in the figure constructor.\n", | |
"mpl.figure.prototype._make_on_message_function = function (fig) {\n", | |
" return function socket_on_message(evt) {\n", | |
" if (evt.data instanceof Blob) {\n", | |
" /* FIXME: We get \"Resource interpreted as Image but\n", | |
" * transferred with MIME type text/plain:\" errors on\n", | |
" * Chrome. But how to set the MIME type? It doesn't seem\n", | |
" * to be part of the websocket stream */\n", | |
" evt.data.type = 'image/png';\n", | |
"\n", | |
" /* Free the memory for the previous frames */\n", | |
" if (fig.imageObj.src) {\n", | |
" (window.URL || window.webkitURL).revokeObjectURL(\n", | |
" fig.imageObj.src\n", | |
" );\n", | |
" }\n", | |
"\n", | |
" fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n", | |
" evt.data\n", | |
" );\n", | |
" fig.updated_canvas_event();\n", | |
" fig.waiting = false;\n", | |
" return;\n", | |
" } else if (\n", | |
" typeof evt.data === 'string' &&\n", | |
" evt.data.slice(0, 21) === 'data:image/png;base64'\n", | |
" ) {\n", | |
" fig.imageObj.src = evt.data;\n", | |
" fig.updated_canvas_event();\n", | |
" fig.waiting = false;\n", | |
" return;\n", | |
" }\n", | |
"\n", | |
" var msg = JSON.parse(evt.data);\n", | |
" var msg_type = msg['type'];\n", | |
"\n", | |
" // Call the \"handle_{type}\" callback, which takes\n", | |
" // the figure and JSON message as its only arguments.\n", | |
" try {\n", | |
" var callback = fig['handle_' + msg_type];\n", | |
" } catch (e) {\n", | |
" console.log(\n", | |
" \"No handler for the '\" + msg_type + \"' message type: \",\n", | |
" msg\n", | |
" );\n", | |
" return;\n", | |
" }\n", | |
"\n", | |
" if (callback) {\n", | |
" try {\n", | |
" // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n", | |
" callback(fig, msg);\n", | |
" } catch (e) {\n", | |
" console.log(\n", | |
" \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n", | |
" e,\n", | |
" e.stack,\n", | |
" msg\n", | |
" );\n", | |
" }\n", | |
" }\n", | |
" };\n", | |
"};\n", | |
"\n", | |
"// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n", | |
"mpl.findpos = function (e) {\n", | |
" //this section is from http://www.quirksmode.org/js/events_properties.html\n", | |
" var targ;\n", | |
" if (!e) {\n", | |
" e = window.event;\n", | |
" }\n", | |
" if (e.target) {\n", | |
" targ = e.target;\n", | |
" } else if (e.srcElement) {\n", | |
" targ = e.srcElement;\n", | |
" }\n", | |
" if (targ.nodeType === 3) {\n", | |
" // defeat Safari bug\n", | |
" targ = targ.parentNode;\n", | |
" }\n", | |
"\n", | |
" // pageX,Y are the mouse positions relative to the document\n", | |
" var boundingRect = targ.getBoundingClientRect();\n", | |
" var x = e.pageX - (boundingRect.left + document.body.scrollLeft);\n", | |
" var y = e.pageY - (boundingRect.top + document.body.scrollTop);\n", | |
"\n", | |
" return { x: x, y: y };\n", | |
"};\n", | |
"\n", | |
"/*\n", | |
" * return a copy of an object with only non-object keys\n", | |
" * we need this to avoid circular references\n", | |
" * http://stackoverflow.com/a/24161582/3208463\n", | |
" */\n", | |
"function simpleKeys(original) {\n", | |
" return Object.keys(original).reduce(function (obj, key) {\n", | |
" if (typeof original[key] !== 'object') {\n", | |
" obj[key] = original[key];\n", | |
" }\n", | |
" return obj;\n", | |
" }, {});\n", | |
"}\n", | |
"\n", | |
"mpl.figure.prototype.mouse_event = function (event, name) {\n", | |
" var canvas_pos = mpl.findpos(event);\n", | |
"\n", | |
" if (name === 'button_press') {\n", | |
" this.canvas.focus();\n", | |
" this.canvas_div.focus();\n", | |
" }\n", | |
"\n", | |
" var x = canvas_pos.x * this.ratio;\n", | |
" var y = canvas_pos.y * this.ratio;\n", | |
"\n", | |
" this.send_message(name, {\n", | |
" x: x,\n", | |
" y: y,\n", | |
" button: event.button,\n", | |
" step: event.step,\n", | |
" guiEvent: simpleKeys(event),\n", | |
" });\n", | |
"\n", | |
" /* This prevents the web browser from automatically changing to\n", | |
" * the text insertion cursor when the button is pressed. We want\n", | |
" * to control all of the cursor setting manually through the\n", | |
" * 'cursor' event from matplotlib */\n", | |
" event.preventDefault();\n", | |
" return false;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._key_event_extra = function (_event, _name) {\n", | |
" // Handle any extra behaviour associated with a key event\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.key_event = function (event, name) {\n", | |
" // Prevent repeat events\n", | |
" if (name === 'key_press') {\n", | |
" if (event.which === this._key) {\n", | |
" return;\n", | |
" } else {\n", | |
" this._key = event.which;\n", | |
" }\n", | |
" }\n", | |
" if (name === 'key_release') {\n", | |
" this._key = null;\n", | |
" }\n", | |
"\n", | |
" var value = '';\n", | |
" if (event.ctrlKey && event.which !== 17) {\n", | |
" value += 'ctrl+';\n", | |
" }\n", | |
" if (event.altKey && event.which !== 18) {\n", | |
" value += 'alt+';\n", | |
" }\n", | |
" if (event.shiftKey && event.which !== 16) {\n", | |
" value += 'shift+';\n", | |
" }\n", | |
"\n", | |
" value += 'k';\n", | |
" value += event.which.toString();\n", | |
"\n", | |
" this._key_event_extra(event, name);\n", | |
"\n", | |
" this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n", | |
" return false;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.toolbar_button_onclick = function (name) {\n", | |
" if (name === 'download') {\n", | |
" this.handle_save(this, null);\n", | |
" } else {\n", | |
" this.send_message('toolbar_button', { name: name });\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n", | |
" this.message.textContent = tooltip;\n", | |
"};\n", | |
"\n", | |
"///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n", | |
"// prettier-ignore\n", | |
"var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\n", | |
"mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", | |
"\n", | |
"mpl.extensions = [\"eps\", \"jpeg\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n", | |
"\n", | |
"mpl.default_extension = \"png\";/* global mpl */\n", | |
"\n", | |
"var comm_websocket_adapter = function (comm) {\n", | |
" // Create a \"websocket\"-like object which calls the given IPython comm\n", | |
" // object with the appropriate methods. Currently this is a non binary\n", | |
" // socket, so there is still some room for performance tuning.\n", | |
" var ws = {};\n", | |
"\n", | |
" ws.close = function () {\n", | |
" comm.close();\n", | |
" };\n", | |
" ws.send = function (m) {\n", | |
" //console.log('sending', m);\n", | |
" comm.send(m);\n", | |
" };\n", | |
" // Register the callback with on_msg.\n", | |
" comm.on_msg(function (msg) {\n", | |
" //console.log('receiving', msg['content']['data'], msg);\n", | |
" // Pass the mpl event to the overridden (by mpl) onmessage function.\n", | |
" ws.onmessage(msg['content']['data']);\n", | |
" });\n", | |
" return ws;\n", | |
"};\n", | |
"\n", | |
"mpl.mpl_figure_comm = function (comm, msg) {\n", | |
" // This is the function which gets called when the mpl process\n", | |
" // starts-up an IPython Comm through the \"matplotlib\" channel.\n", | |
"\n", | |
" var id = msg.content.data.id;\n", | |
" // Get hold of the div created by the display call when the Comm\n", | |
" // socket was opened in Python.\n", | |
" var element = document.getElementById(id);\n", | |
" var ws_proxy = comm_websocket_adapter(comm);\n", | |
"\n", | |
" function ondownload(figure, _format) {\n", | |
" window.open(figure.canvas.toDataURL());\n", | |
" }\n", | |
"\n", | |
" var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n", | |
"\n", | |
" // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n", | |
" // web socket which is closed, not our websocket->open comm proxy.\n", | |
" ws_proxy.onopen();\n", | |
"\n", | |
" fig.parent_element = element;\n", | |
" fig.cell_info = mpl.find_output_cell(\"<div id='\" + id + \"'></div>\");\n", | |
" if (!fig.cell_info) {\n", | |
" console.error('Failed to find cell for figure', id, fig);\n", | |
" return;\n", | |
" }\n", | |
" fig.cell_info[0].output_area.element.on(\n", | |
" 'cleared',\n", | |
" { fig: fig },\n", | |
" fig._remove_fig_handler\n", | |
" );\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_close = function (fig, msg) {\n", | |
" var width = fig.canvas.width / fig.ratio;\n", | |
" fig.cell_info[0].output_area.element.off(\n", | |
" 'cleared',\n", | |
" fig._remove_fig_handler\n", | |
" );\n", | |
" fig.resizeObserverInstance.unobserve(fig.canvas_div);\n", | |
"\n", | |
" // Update the output cell to use the data from the current canvas.\n", | |
" fig.push_to_output();\n", | |
" var dataURL = fig.canvas.toDataURL();\n", | |
" // Re-enable the keyboard manager in IPython - without this line, in FF,\n", | |
" // the notebook keyboard shortcuts fail.\n", | |
" IPython.keyboard_manager.enable();\n", | |
" fig.parent_element.innerHTML =\n", | |
" '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n", | |
" fig.close_ws(fig, msg);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.close_ws = function (fig, msg) {\n", | |
" fig.send_message('closing', msg);\n", | |
" // fig.ws.close()\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.push_to_output = function (_remove_interactive) {\n", | |
" // Turn the data on the canvas into data in the output cell.\n", | |
" var width = this.canvas.width / this.ratio;\n", | |
" var dataURL = this.canvas.toDataURL();\n", | |
" this.cell_info[1]['text/html'] =\n", | |
" '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.updated_canvas_event = function () {\n", | |
" // Tell IPython that the notebook contents must change.\n", | |
" IPython.notebook.set_dirty(true);\n", | |
" this.send_message('ack', {});\n", | |
" var fig = this;\n", | |
" // Wait a second, then push the new image to the DOM so\n", | |
" // that it is saved nicely (might be nice to debounce this).\n", | |
" setTimeout(function () {\n", | |
" fig.push_to_output();\n", | |
" }, 1000);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_toolbar = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var toolbar = document.createElement('div');\n", | |
" toolbar.classList = 'btn-toolbar';\n", | |
" this.root.appendChild(toolbar);\n", | |
"\n", | |
" function on_click_closure(name) {\n", | |
" return function (_event) {\n", | |
" return fig.toolbar_button_onclick(name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" function on_mouseover_closure(tooltip) {\n", | |
" return function (event) {\n", | |
" if (!event.currentTarget.disabled) {\n", | |
" return fig.toolbar_button_onmouseover(tooltip);\n", | |
" }\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" fig.buttons = {};\n", | |
" var buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'btn-group';\n", | |
" var button;\n", | |
" for (var toolbar_ind in mpl.toolbar_items) {\n", | |
" var name = mpl.toolbar_items[toolbar_ind][0];\n", | |
" var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", | |
" var image = mpl.toolbar_items[toolbar_ind][2];\n", | |
" var method_name = mpl.toolbar_items[toolbar_ind][3];\n", | |
"\n", | |
" if (!name) {\n", | |
" /* Instead of a spacer, we start a new button group. */\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
" buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'btn-group';\n", | |
" continue;\n", | |
" }\n", | |
"\n", | |
" button = fig.buttons[name] = document.createElement('button');\n", | |
" button.classList = 'btn btn-default';\n", | |
" button.href = '#';\n", | |
" button.title = name;\n", | |
" button.innerHTML = '<i class=\"fa ' + image + ' fa-lg\"></i>';\n", | |
" button.addEventListener('click', on_click_closure(method_name));\n", | |
" button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", | |
" buttonGroup.appendChild(button);\n", | |
" }\n", | |
"\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
"\n", | |
" // Add the status bar.\n", | |
" var status_bar = document.createElement('span');\n", | |
" status_bar.classList = 'mpl-message pull-right';\n", | |
" toolbar.appendChild(status_bar);\n", | |
" this.message = status_bar;\n", | |
"\n", | |
" // Add the close button to the window.\n", | |
" var buttongrp = document.createElement('div');\n", | |
" buttongrp.classList = 'btn-group inline pull-right';\n", | |
" button = document.createElement('button');\n", | |
" button.classList = 'btn btn-mini btn-primary';\n", | |
" button.href = '#';\n", | |
" button.title = 'Stop Interaction';\n", | |
" button.innerHTML = '<i class=\"fa fa-power-off icon-remove icon-large\"></i>';\n", | |
" button.addEventListener('click', function (_evt) {\n", | |
" fig.handle_close(fig, {});\n", | |
" });\n", | |
" button.addEventListener(\n", | |
" 'mouseover',\n", | |
" on_mouseover_closure('Stop Interaction')\n", | |
" );\n", | |
" buttongrp.appendChild(button);\n", | |
" var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n", | |
" titlebar.insertBefore(buttongrp, titlebar.firstChild);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._remove_fig_handler = function (event) {\n", | |
" var fig = event.data.fig;\n", | |
" if (event.target !== this) {\n", | |
" // Ignore bubbled events from children.\n", | |
" return;\n", | |
" }\n", | |
" fig.close_ws(fig, {});\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._root_extra_style = function (el) {\n", | |
" el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._canvas_extra_style = function (el) {\n", | |
" // this is important to make the div 'focusable\n", | |
" el.setAttribute('tabindex', 0);\n", | |
" // reach out to IPython and tell the keyboard manager to turn it's self\n", | |
" // off when our div gets focus\n", | |
"\n", | |
" // location in version 3\n", | |
" if (IPython.notebook.keyboard_manager) {\n", | |
" IPython.notebook.keyboard_manager.register_events(el);\n", | |
" } else {\n", | |
" // location in version 2\n", | |
" IPython.keyboard_manager.register_events(el);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._key_event_extra = function (event, _name) {\n", | |
" var manager = IPython.notebook.keyboard_manager;\n", | |
" if (!manager) {\n", | |
" manager = IPython.keyboard_manager;\n", | |
" }\n", | |
"\n", | |
" // Check for shift+enter\n", | |
" if (event.shiftKey && event.which === 13) {\n", | |
" this.canvas_div.blur();\n", | |
" // select the cell after this one\n", | |
" var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", | |
" IPython.notebook.select(index + 1);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_save = function (fig, _msg) {\n", | |
" fig.ondownload(fig, null);\n", | |
"};\n", | |
"\n", | |
"mpl.find_output_cell = function (html_output) {\n", | |
" // Return the cell and output element which can be found *uniquely* in the notebook.\n", | |
" // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", | |
" // IPython event is triggered only after the cells have been serialised, which for\n", | |
" // our purposes (turning an active figure into a static one), is too late.\n", | |
" var cells = IPython.notebook.get_cells();\n", | |
" var ncells = cells.length;\n", | |
" for (var i = 0; i < ncells; i++) {\n", | |
" var cell = cells[i];\n", | |
" if (cell.cell_type === 'code') {\n", | |
" for (var j = 0; j < cell.output_area.outputs.length; j++) {\n", | |
" var data = cell.output_area.outputs[j];\n", | |
" if (data.data) {\n", | |
" // IPython >= 3 moved mimebundle to data attribute of output\n", | |
" data = data.data;\n", | |
" }\n", | |
" if (data['text/html'] === html_output) {\n", | |
" return [cell, data, j];\n", | |
" }\n", | |
" }\n", | |
" }\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"// Register the function which deals with the matplotlib target/channel.\n", | |
"// The kernel may be null if the page has been refreshed.\n", | |
"if (IPython.notebook.kernel !== null) {\n", | |
" IPython.notebook.kernel.comm_manager.register_target(\n", | |
" 'matplotlib',\n", | |
" mpl.mpl_figure_comm\n", | |
" );\n", | |
"}\n" | |
], | |
"text/plain": [ | |
"<IPython.core.display.Javascript object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
}, | |
{ | |
"data": { | |
"text/html": [ | |
"<img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAgAElEQVR4nO3de3DU9b3/8Q8h95gsBSEXUKglgDVE5VBKL5By1VMi7enYIsOtdrjWoLRO6+2nkalAsGpPPSPi8aAzbSVoG2p7FJGASpUEbM/SshAvLSrGoPaUkIhHEhvy+v3BZOVLFkn45B0u+3zOPP7o5stm95uUvNzNLk5EREREFFe5030DiIiIiKh7YwASERERxVkMQCIiIqI4iwFIREREFGcxAImIiIjiLAYgERERUZzFACQiIiKKsxiARERERHEWA5CIiIgozmIAEhEREcVZDEAiIiKiOIsBSERERBRnMQCJiIiI4iwGIBEREVGcxQAkIiIiirMYgERERERxFgOQiIiIKM5iABIRERHFWQxAIiIiojiLAUhEREQUZzEAiYiIiOIsBiARERFRnMUAJCIiIoqzGIBEREREcRYDkIiIiCjOYgASERERxVkMQCIiIqI4iwFIREREFGcxAImIiIjiLAYgERERUZzFACQiIiKKsxiARERERHEWA5CIiIgozmIAEhEREcVZDEAiIiKiOIsBSERERBRnMQApbistLZVzTv/7v/8b8+OXXHKJioqKTum658yZo4EDBwYuO3DggKZNm6a+ffvKOadvfOMbp3Tdp7uBAwdqzpw50f/9/PPPyzmn559/vlPX88ADD+jRRx/t1J+J9bnmzJmjjIyMTl3Pydq2bZtKS0t18ODBdh8rKio65e8L38LhsMaOHausrCw55/Szn/3stNyOzvTee+/ppptuUkFBgTIyMpSSkqLBgwfr+uuv1+uvv97p63vsscdi3u8333xTzjn99Kc/7YqbTXTOxwCkuM1yAP7tb39TOBwOXLZkyRIlJyfrV7/6laqrq/Xaa6+d0nWf7o4fgI2NjaqurlZjY2OnrudUzm+sz2UxAH/605/KOac333yz3cf27NmjPXv2dOnn62iXXXaZ8vPztWHDBlVXV+vdd989Lbejo+3YsUN9+/bV+eefrzvvvFPPPvusnn/+ea1evVpf/epX1atXr05f55QpU9r9x5XEACTqbAxAitssB2CsJk6cqIsvvrjLrq+1tVUfffRRl11fRzt+AJ5qnTm/H3/8sf75z3/G/Fh3D8DTWWJiohYtWnTS4z766CO1trZ2wy06cY2NjcrJydEFF1yg2tramMf8+te/7vT1MgCJuiYGIMVtnR2AbU8/rl27Vrfeeqtyc3OVmZmpCRMm6NVXXw382WOfAm77wXS8tqcxDxw4oEWLFikvL09JSUn67Gc/q1tvvVVNTU2B63TO6brrrtODDz6oYcOGKSkpSQ8++KAeffRROee0ZcsWzZ07V71791ZmZqZmzZqlDz/8UO+++66+/e1vKxQKKScnRzfeeKM+/vjjk56fjz/+WD/60Y+UnZ2ttLQ0feUrX9GOHTs69BTw3r17NW3aNOXm5io5OVn9+vXT+PHjtXPnTklHR+Tx56PtfLVd3y9+8Qv98Ic/VF5ennr06KFXXnnlU58C3r17t8aPH6/09HSdf/75uu666/R///d/0ePavg6xnnZ2zqm0tFTSJ98XJ/p6xXoKuLNfw1/84hcaNmyY0tLSVFhYqP/+7//+1K9F29f4eMd+7Nlnn9W1116r888/X845HT58WEeOHNHKlSs1dOhQJScnq2/fvpo1a1a7QVZUVKRLLrlEVVVV+tKXvqTU1FQNHDhQjzzyiCTpqaee0uWXX660tDQVFBTomWee+dTbK0n33HOPnHMqLy8/6bG/+MUv5JxTVVVVu48tXbpUiYmJqqurU1FR0QnPw7ED8N5779WgQYOUkZGh0aNHq7q6ut31/u53v9Po0aOVlpam8847TxMnTmz3+du+F3bv3q1rrrlGWVlZ6tevn6699lo1NDSc9H4RnckxACluO9UBOGjQIM2YMUNPP/20ysvLdeGFFyo/P18tLS3RY48dgE1NTaqurtbll1+uiy66SNXV1dGnMQ8fPqzCwkJlZGTonnvu0aZNm3T77bcrMTFRX//61wO3xzmn/v37q7CwUGvXrtVzzz2n3bt3RwfAZz/7Wd14443atGmTVq5cqZ49e2r69OkaMWKE7rrrLlVWVuqmm26Sc0733nvvSc/PnDlz1KNHD/3oRz/Spk2bdN9996l///7Kyso66QAcOnSoBg8erF/+8pfaunWrKioqdOONN0aPCYfDuuiii3T55ZdHz0fbU+Zt19e/f39dffXV+v3vf6+nnnpKBw4cOOEATE5O1oUXXqhly5Zp06ZNuvPOO5WYmKji4uLocR0dgLW1tVq8eLGcc1q/fn3g6yW1H4Cd/RoOGjRIo0aN0hNPPKENGzboa1/7mhITE7V3794Tfi3+/ve/q7q6Ws45XX311dHbJH0yAPv376/58+frmWee0W9+8xu1tLRo/vz5cs6ppKREGzdu1OrVq9W3b19dcMEFge/7oqIi9enTR0OHDtWaNWv07LPPqri4WM45LV26VMOHD1d5ebk2bNig0aNHKyUlRXV1dSe8vZI0efJk9ezZUx9++OGnHidJzc3NysnJ0YwZMwKX//Of/1ReXp6+/e1vSzr69PtXvvIV5eTkRM9B23lo+/oOGjRIV155pZ588kk9+eSTGj58uD7zmc8EBttjjz0m55wmT56sJ598Uo8//rj+5V/+RcnJyXrxxRejx7X9HTF06FDdcccdqqys1H333aeUlBRde+21J71fRGdyDECK2051AB7/Q/2JJ56Qcy7wKEOsF4G0PcpybKtXr5ZzTk888UTg8pUrV8o5p02bNkUvc84pFAqpvr4+cGzbAFi8eHHg8m9+85tyzum+++4LXH7ZZZdpxIgRMe9zW6+88oqcc/rBD34QuLztB+enDcB//OMfcs7p3//93z/1c5zoKeC26xs7duwJP3b8AHTO6ec//3ng2GXLlsk5p5deeklSxweg9OlPAR8/ADv7NczOztYHH3wQvey9995TQkKCVqxY0e5zxbqd1113XeCytq//7NmzA5e3fQ2///3vBy7fsWOHnHO69dZbA/fJOac//elP0csOHDignj17Ki0tLTD2/vznP8s5p/vvv/9Tb+uwYcOUk5Nz0vvUVmlpqZKTk/X+++9HL3v88cflnNPWrVujl53sKeDhw4cH/mPs5ZdfDjwSeeTIEeXl5Wn48OE6cuRI9LhDhw6pX79++vKXvxy4Tc453X333YHP9f3vf1+pqamn/Wl2Ip8YgBS3neoAXL16deC4V199Vc45rVu3LnpZRwfgd77zHWVkZLT7QfL+++/LOaebbropeplzTv/2b//W7nYe+xTgsd1yyy1yzrV7scn06dPVp0+fmPe5rVWrVrUbBNLRR2QSExM/dQC2trbqc5/7nPr37697771X4XA48IO2rZMNwOMHXazPJX0yAP/xj38Ejm0bBD/5yU8C/7urB2Bnv4bXXHNNu+vMycnRwoUL210e63aeaAD+7ne/C1ze9jV8+eWX213PxRdfrC9+8YuB+5Sbm9vuuNzcXH3pS18KXNbc3CznnG688cZPva2dHYDvvfeekpOTddddd0UvGzNmjIYPHx447mQD8Oabbw5c3tTUJOecysrKJEk1NTUxR50kLVq0SAkJCdFfHWj7O+L4X/FoG/3vvfdeh+8f0ZkWA5Ditp/85Cef+pf40KFDNXHixOj/bhsfx//ieqxh0dEBOGHCBH3uc5+L+fkTExM1d+7c6P+O9WiO9MkA+OMf/xi4/EQDtyMvmmg7N++88067j2VnZ5/0KeC33npL3/ve95SdnS3nnHr37q3FixcHHvk62QA8/hG1E32uOXPmKDExsd2xhw8flnNOS5YskWQ3ADv7NTx+wEkdf2HNpw3A44de29cw1gswJkyYoMGDBwfu0/Hfm223a8qUKR26HcfXmaeA25o1a5YuuOACtbS06C9/+Yucc3rooYcCx5zKi0CO/fq++OKLcs7pl7/8Zbvjjv++P9H/h9rO+Zn2IiGizsQApLjtP//zP+Wc0//8z/+0+1hra6uysrICv5NkMQC/853v6Lzzzjvho0fHPppxoh+6FgPQ5xHA43vttdf0k5/8RD179tSCBQuil59sAMZ6hajPI4DvvvtuzEdw256y9nkE0Pdr2BUD8Piv/8keARw9enTgPnX1ALz33nsDT712pD/96U9yzqmiokLz5s1Tr1692g1I3wF4Ko8AMgDpXIwBSHHb3/72N/Xo0UM//vGP231sw4YN7UadxQB86KGHoi82OLa2AVJZWRm9rDsHYNsPyVP5HcATddlll+kLX/hC9H+PGDFCo0aNanfcqQ7AE/0OYNsv9be2tio1NbXdo6hr1qxpNwDvv/9+OedUU1PT7jYcPwC74mtoMQDbfjXh+uuvD1ze9jtxt912W+A+dfUAbGhoiL4NTKxHkiWpoqKi3WVf/vKXNWrUKKWnp0cfvT22b33rW+rXr1+7yzs6AI8cOaL+/fvrsssuC4z2Dz/8UP369dNXvvKV6GUMQDqXYwBSXLd48WL16NFD8+fP15NPPqlnn31Wd911l8477zyNHDlSzc3N0WMtBmDbK0gzMzN13333qbKyUqWlpUpKSor5CtLuGoCSNHPmzOhAbnsVcF5e3klfBfyXv/xFY8aM0f33369nnnlGW7Zs0W233aaEhITACw/mzJmjlJQUrVu3Ti+//LJ27doVuL7ODMATvQr4X//1XwN/fu7cuUpNTdW9996rzZs3a/ny5SooKGg3ANs+z4IFC1RVVaU//vGP0aevT/QqYJ+vocUAlKT58+erR48eWrJkiZ599lk99NBD6tevny644ILAI6YWA1D65I2g+/btq6VLl2rTpk164YUX9PDDD6uoqCjmG0G3vfCjR48eMf+lkLbv61WrVmnHjh3R+93RASh98h8yX//61/W73/1OTzzxhL7whS+c8FXADEA6F2MAUlzX2tqqBx98UCNHjlR6erqSk5OVn5+vm266SYcOHQocazEApaOvtly4cKFyc3OVmJiogQMH6pZbbjnhe8gdn9UAbG5u1o033qh+/fopNTU1+n5qJ3sfwPfff1/f/e53NWzYMGVkZOi8885TYWGhfvaznwVenfnWW29p8uTJyszMlHPt3wewMwMwIyNDu3bt0te+9jWlpaWpd+/eWrRoUbunDxsbGzV37lxlZ2crIyNDV111ld566612A0E6+iKavLw8JSQkBD7nid4H0OdraDUA294HcMiQIUpKStL555+vmTNnnvB9AGPdLp8BKH3yT8FdcsklSk9Pj/5TcAsWLFAkEml3fHNzs1JSUnTllVfGvL76+npdffXV6tWrl3r06CHn2r8PYKzbe/zX98knn9QXv/hFpaamKiMjQxMmTNC2bdsCxzAA6VyOAUhERGdMv//97+Wc09NPP326bwrROR0DkIiITnt79uzRhg0blJ+f3+7384io62MAEhHRaa+oqEiJiYkaNWqUXnnlldN9c4jO+RiARERERHEWA5CIiIgozmIAEhEREcVZDEAiIiKiOIsBSERERBRnMQA9OnLkiGpra9XQ0KDGxkYAAHAWaGhoUG1trY4cOXK6p8RpiwHoUW1trZxzAADgLHT8v4oTTzEAPWpoaIh+A53u/5oBAAAd0/YATkNDw+meEqctBqBHjY2Ncs6psbHxdN8UIiIi6mD8/GYAesU3EBER0dkXP78ZgF7xDURERHT2xc9vBqBXfAMRERGdffHzmwHoFd9AREREZ1/8/GYAesU3EBER0dkXP78ZgF7xDURERHT2xc9vBqBXfAMRERGdffHzmwHoFd9AREREZ1/8/GYAesU3EBER0dkXP78ZgF7xDURERHT2xc9vBqBXfAMRERGdffHzmwHoFd9AREREZ1/8/GYAesU3EBER0dkXP78ZgF7xDURERHT2xc9vBqBXfAO1b+BNT511iOj0drr/DogX9En8/GYAemX9DXS6/7IA4t3Z2Ok+Z0BXsooByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigHIAPSKAQgAgB2rGIAMQK8YgAAA2LGKAcgA9IoBCACAHasYgAxArxiAAADYsYoByAD0igEIAIAdqxiADECvGIAAANixigFoOACXL18u55xuuOGG6GWtra0qLS1Vbm6uUlNTVVRUpN27dwf+XFNTk0pKStSnTx+lp6frqquuUm1tbeCY+vp6zZw5U1lZWcrKytLMmTN18ODBwDH79u1TcXGx0tPT1adPHy1evFjNzc2BY3bt2qWxY8cqNTVVeXl5Wrp0qVpbWzt8HxmAAADYsYoBaDQAX375ZQ0aNEiFhYWBAVhWVqbMzExVVFQoEolo2rRpys3N1QcffBA9ZuHCherfv78qKysVDoc1btw4XXrppWppaYkec+WVV6qgoEBVVVWqqqpSQUGBiouLox9vaWlRQUGBxo0bp3A4rMrKSuXl5amkpCR6TGNjo7Kzs3XNNdcoEomooqJCmZmZuueeezp8PxmAAADYsYoBaDAADx06pPz8fFVWVqqoqCg6AFtbW5WTk6OysrLosU1NTQqFQlq9erUkqaGhQUlJSVq3bl30mLq6OiUkJGjjxo2SpJqaGjnntH379ugx1dXVcs7p1VdflSRt2LBBCQkJqqurix5TXl6ulJSU6Bd71apVCoVCampqih6zYsUK5eXldfhRQAYgAAB2rGIAGgzA2bNna8mSJZIUGIB79+6Vc07hcDhw/NSpUzV79mxJ0pYtW+ScU319feCYwsJC3XHHHZKkNWvWKBQKtfu8oVBIjzzyiCTp9ttvV2FhYeDj9fX1cs7pueeekyTNmjVLU6dODRwTDoflnNMb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iXrggQf07rvvRh37FBq1r7Pn+fh4CrhjdfY8Hzp0SAMGDNDVV1+tPXv2aOvWrcrPz9fcuXNP1104K+rseX700UeVmJioVatWae/evXrppZc0cuRIjRo16nTdhbOiQ4cOaefOndq5c6ecc7rvvvu0c+fO6Nvt8LPQNgbgaeqBBx7QwIEDlZycrBEjRmjr1q3Rj82ZM0dFRUWB41944QVdfvnlSk5O1qBBg3jzy07UmXNdVFQk51w7c+bM6f4bfpbV2e/pY2MAdrzOnudXXnlFEydOVFpamgYMGKAf/vCHPPrXgTp7nu+//359/vOfV1pamnJzczVjxgy988473Xyrz66ef/75T/37lp+FtjEAiYiIiOIsBiARERFRnMUAJCIiIoqzGIBEREREcRYDkIiIiCjOYgASERERxVkMQCIiIqI4iwFIREREFGcxAImIiIjiLAYgERERUZzFACQiIiKKsxiARERERHEWA5CIiIgozmIAEhEREcVZDEAiIiKiOIsBSERERBRnMQCJiIiI4iwGIBEREVGcxQAkIiIiirMYgERERERxFgOQiIiIKM5iABIRERHFWQxAIiIiojiLAUhEREQUZzEAiYiIiOIsBiARERFRnMUAJCIiIoqzGIBEREREcRYDkIiIiCjOYgASERERxVkMQCIiIqI4iwFIREREFGf9f1dWVWO1CytoAAAAAElFTkSuQmCC\" width=\"640\">" | |
], | |
"text/plain": [ | |
"<IPython.core.display.HTML object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"print(\"Performances of MT64 implemented in Cython\")\n", | |
"%timeit cython_uniform_mt(shape)\n", | |
"fig, ax = subplots()\n", | |
"ax.hist(cython_uniform_mt(shape).ravel())\n", | |
"ax.set_title(\"Uniform distribution from Cython\")\n", | |
"pass" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"Eureka ! This cython implementation is as fast as the numpy one, and much faster than the C or C++ implementations.\n", | |
"\n", | |
"Well I don't like the global variable and minor things in this code, but with just 50 lines of code, my problem is finding a solution ! Thanks Anandh Swaminathan for sharing this code.\n", | |
"\n", | |
"Here is the re-written version with a class to allow to have multiple generators with different seeds (Well, not that robust, but better than sharing the same seed and creating a bottleneck for the random number generation)." | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 13, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"%%cython\n", | |
"# cython: boundscheck=False\n", | |
"# cython: cdivision=True\n", | |
"# cython: wraparound=False\n", | |
"\n", | |
"import cython\n", | |
"import time\n", | |
"import numpy\n", | |
"from libc.stdlib cimport RAND_MAX\n", | |
"from libc.stdint cimport uint32_t, uint64_t\n", | |
"from libc.math cimport log, sqrt, cos, M_PI\n", | |
"\n", | |
"#Few constants for 64-bit Mersenne Twisters\n", | |
"cdef:\n", | |
" uint32_t NN=312\n", | |
" uint32_t MM=156\n", | |
" uint64_t MATRIX_A=0xB5026F5AA96619E9ULL\n", | |
" uint64_t UM=0xFFFFFFFF80000000ULL # Most significant 33 bits\n", | |
" uint64_t LM=0x7FFFFFFFULL #Least significant 31 bits\n", | |
" double EPS64 = numpy.finfo(numpy.float64).eps\n", | |
" double TWO_PI = 2.0 * M_PI \n", | |
" double NRM53 = 1.0/((1<<53)-1) # normalization factor for uniform\n", | |
"\n", | |
"cdef class MT:\n", | |
" \"\"\"\n", | |
" This class implements 64-bit Mersenne Twisters\n", | |
" \n", | |
" http://www.math.sci.hiroshima-u.ac.jp/m-mat/MT/VERSIONS/C-LANG/mt19937-64.c\n", | |
" \n", | |
" Inspired from:\n", | |
" https://github.com/ananswam/cython_random\n", | |
" with minor clean-ups\n", | |
" \n", | |
" Licence: MIT\n", | |
" \"\"\"\n", | |
" cdef:\n", | |
" uint64_t mt[312]\n", | |
" uint32_t mti\n", | |
" uint64_t mag01[2]\n", | |
" bint has_spare\n", | |
" double spare\n", | |
" \n", | |
" def __init__(self, seed):\n", | |
" self.mti = NN + 1\n", | |
" self._seed(<uint64_t> seed)\n", | |
" \n", | |
" cdef inline void _seed(self, uint64_t seed) nogil:\n", | |
" self.mt[0] = seed\n", | |
" for self.mti in range(1, NN):\n", | |
" self.mt[self.mti] = (6364136223846793005ULL * (self.mt[self.mti-1] ^ (self.mt[self.mti-1] >> 62)) + self.mti)\n", | |
" self.mag01[0] = 0ULL\n", | |
" self.mag01[1] = MATRIX_A\n", | |
" self.mti = NN\n", | |
" self.has_spare = False\n", | |
" \n", | |
" cdef inline uint64_t genrand64(self) nogil:\n", | |
" cdef: \n", | |
" uint32_t i\n", | |
" uint64_t x\n", | |
" if self.mti >= NN:\n", | |
" for i in range(NN - MM):\n", | |
" x = (self.mt[i]&UM) | (self.mt[i+1]&LM)\n", | |
" self.mt[i] = self.mt[i+MM] ^ (x>>1) ^ self.mag01[int(x&1ULL)]\n", | |
"\n", | |
" for i in range(NN-MM, NN-1):\n", | |
" x = (self.mt[i]&UM)|(self.mt[i+1]&LM)\n", | |
" self.mt[i] = self.mt[i+(MM-NN)] ^ (x>>1) ^ self.mag01[int(x&1ULL)]\n", | |
"\n", | |
" x = (self.mt[NN-1]&UM)|(self.mt[0]&LM)\n", | |
" self.mt[NN-1] = self.mt[MM-1] ^ (x>>1) ^ self.mag01[int(x&1ULL)]\n", | |
" self.mti = 0\n", | |
"\n", | |
" x = self.mt[self.mti]\n", | |
" self.mti += 1\n", | |
" x ^= (x >> 29) & 0x5555555555555555ULL\n", | |
" x ^= (x << 17) & 0x71D67FFFEDA60000ULL\n", | |
" x ^= (x << 37) & 0xFFF7EEE000000000ULL\n", | |
" x ^= (x >> 43);\n", | |
" return x\n", | |
" \n", | |
" def rand(self):\n", | |
" return self.genrand64()%(RAND_MAX+1)\n", | |
" \n", | |
" cdef inline double _uniform(self) nogil:\n", | |
" return (self.genrand64() >> 11) * NRM53\n", | |
" \n", | |
" def uniform(self):\n", | |
" \"Return a random value between [0:1[\"\n", | |
" return self._uniform()\n", | |
" \n", | |
" cdef inline double _normal_bm(self, double mu, double sigma) nogil:\n", | |
" cdef:\n", | |
" double u1=0.0, u2=0.0\n", | |
" \n", | |
" while (u1 == 0.0 ):\n", | |
" u1 = self._uniform()\n", | |
" u2 = self._uniform()\n", | |
"\n", | |
" return sigma * sqrt(-2.0 * log(u1)) * cos(TWO_PI * u2) + mu;\n", | |
"\n", | |
" def normal_bm(self, mu, sigma): \n", | |
" \"\"\"\n", | |
" Calculate the gaussian distribution using the Box–Muller algorithm\n", | |
"\n", | |
" Credits:\n", | |
" https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform\n", | |
"\n", | |
" :param mu: the center of the distribution\n", | |
" :param sigma: the width of the distribution\n", | |
" :return: random value\n", | |
" \"\"\" \n", | |
" return self._normal(mu, sigma)\n", | |
" \n", | |
" cdef inline double _normal_m(self, double mu, double sigma) nogil:\n", | |
" cdef: \n", | |
" double u1, u2, s=0.0\n", | |
" if self.has_spare:\n", | |
" self.has_spare = False\n", | |
" return mu + self.spare * sigma \n", | |
" else:\n", | |
" while (s>=1 or s<=0.0):\n", | |
" u1 = 2.0 * self._uniform() - 1.0\n", | |
" u2 = 2.0 * self._uniform() - 1.0\n", | |
" s = u1 * u1 + u2 * u2;\n", | |
" s = sqrt(-2.0*log(s)/s)\n", | |
" self.spare = u2 * s\n", | |
" self.has_spare = True\n", | |
" return mu + sigma * u1 * s;\n", | |
" \n", | |
" def normal_m(self, mu, sigma):\n", | |
" \"\"\"Implement Marsaglia polar method\n", | |
" https://en.wikipedia.org/wiki/Marsaglia_polar_method\n", | |
" \"\"\"\n", | |
" return self._normal_m(mu, sigma)\n", | |
"\n", | |
"\n", | |
"def cython_uniform_mtc(shape):\n", | |
" cdef: \n", | |
" uint64_t size = numpy.prod(shape), idx\n", | |
" double[::1] ary = numpy.empty(size)\n", | |
" MT mt = MT(time.time_ns())\n", | |
" with nogil:\n", | |
" for idx in range(size):\n", | |
" ary[idx] = mt._uniform()\n", | |
" return numpy.asarray(ary).reshape(shape) \n", | |
"\n", | |
"def cython_normal_bm_mtc(mu, sigma):\n", | |
" shape = mu.shape\n", | |
" assert mu.shape == sigma.shape\n", | |
" cdef: \n", | |
" uint64_t size = numpy.prod(shape), idx\n", | |
" double[::1] ary = numpy.empty(size)\n", | |
" double[::1] cmu = numpy.ascontiguousarray(mu, dtype=numpy.float64).ravel()\n", | |
" double[::1] csigma = numpy.ascontiguousarray(sigma, dtype=numpy.float64).ravel()\n", | |
" MT mt = MT(time.time_ns())\n", | |
" \n", | |
" with nogil:\n", | |
" for idx in range(size):\n", | |
" ary[idx] = mt._normal_bm(cmu[idx], csigma[idx])\n", | |
" return numpy.asarray(ary).reshape(shape) \n", | |
"\n", | |
"def cython_normal_m_mtc(mu, sigma):\n", | |
" shape = mu.shape\n", | |
" assert mu.shape == sigma.shape\n", | |
" cdef: \n", | |
" uint64_t size = numpy.prod(shape), idx\n", | |
" double[::1] ary = numpy.empty(size)\n", | |
" double[::1] cmu = numpy.ascontiguousarray(mu, dtype=numpy.float64).ravel()\n", | |
" double[::1] csigma = numpy.ascontiguousarray(sigma, dtype=numpy.float64).ravel()\n", | |
" MT mt = MT(time.time_ns())\n", | |
" \n", | |
" with nogil:\n", | |
" for idx in range(size):\n", | |
" ary[idx] = mt._normal_m(cmu[idx], csigma[idx])\n", | |
" return numpy.asarray(ary).reshape(shape) " | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 14, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"Performances of the cdef class for Mersenne-twisters implemented in Cython\n", | |
"20.9 ms ± 261 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" | |
] | |
}, | |
{ | |
"data": { | |
"application/javascript": [ | |
"/* Put everything inside the global mpl namespace */\n", | |
"/* global mpl */\n", | |
"window.mpl = {};\n", | |
"\n", | |
"mpl.get_websocket_type = function () {\n", | |
" if (typeof WebSocket !== 'undefined') {\n", | |
" return WebSocket;\n", | |
" } else if (typeof MozWebSocket !== 'undefined') {\n", | |
" return MozWebSocket;\n", | |
" } else {\n", | |
" alert(\n", | |
" 'Your browser does not have WebSocket support. ' +\n", | |
" 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", | |
" 'Firefox 4 and 5 are also supported but you ' +\n", | |
" 'have to enable WebSockets in about:config.'\n", | |
" );\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n", | |
" this.id = figure_id;\n", | |
"\n", | |
" this.ws = websocket;\n", | |
"\n", | |
" this.supports_binary = this.ws.binaryType !== undefined;\n", | |
"\n", | |
" if (!this.supports_binary) {\n", | |
" var warnings = document.getElementById('mpl-warnings');\n", | |
" if (warnings) {\n", | |
" warnings.style.display = 'block';\n", | |
" warnings.textContent =\n", | |
" 'This browser does not support binary websocket messages. ' +\n", | |
" 'Performance may be slow.';\n", | |
" }\n", | |
" }\n", | |
"\n", | |
" this.imageObj = new Image();\n", | |
"\n", | |
" this.context = undefined;\n", | |
" this.message = undefined;\n", | |
" this.canvas = undefined;\n", | |
" this.rubberband_canvas = undefined;\n", | |
" this.rubberband_context = undefined;\n", | |
" this.format_dropdown = undefined;\n", | |
"\n", | |
" this.image_mode = 'full';\n", | |
"\n", | |
" this.root = document.createElement('div');\n", | |
" this.root.setAttribute('style', 'display: inline-block');\n", | |
" this._root_extra_style(this.root);\n", | |
"\n", | |
" parent_element.appendChild(this.root);\n", | |
"\n", | |
" this._init_header(this);\n", | |
" this._init_canvas(this);\n", | |
" this._init_toolbar(this);\n", | |
"\n", | |
" var fig = this;\n", | |
"\n", | |
" this.waiting = false;\n", | |
"\n", | |
" this.ws.onopen = function () {\n", | |
" fig.send_message('supports_binary', { value: fig.supports_binary });\n", | |
" fig.send_message('send_image_mode', {});\n", | |
" if (fig.ratio !== 1) {\n", | |
" fig.send_message('set_dpi_ratio', { dpi_ratio: fig.ratio });\n", | |
" }\n", | |
" fig.send_message('refresh', {});\n", | |
" };\n", | |
"\n", | |
" this.imageObj.onload = function () {\n", | |
" if (fig.image_mode === 'full') {\n", | |
" // Full images could contain transparency (where diff images\n", | |
" // almost always do), so we need to clear the canvas so that\n", | |
" // there is no ghosting.\n", | |
" fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", | |
" }\n", | |
" fig.context.drawImage(fig.imageObj, 0, 0);\n", | |
" };\n", | |
"\n", | |
" this.imageObj.onunload = function () {\n", | |
" fig.ws.close();\n", | |
" };\n", | |
"\n", | |
" this.ws.onmessage = this._make_on_message_function(this);\n", | |
"\n", | |
" this.ondownload = ondownload;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_header = function () {\n", | |
" var titlebar = document.createElement('div');\n", | |
" titlebar.classList =\n", | |
" 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n", | |
" var titletext = document.createElement('div');\n", | |
" titletext.classList = 'ui-dialog-title';\n", | |
" titletext.setAttribute(\n", | |
" 'style',\n", | |
" 'width: 100%; text-align: center; padding: 3px;'\n", | |
" );\n", | |
" titlebar.appendChild(titletext);\n", | |
" this.root.appendChild(titlebar);\n", | |
" this.header = titletext;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n", | |
"\n", | |
"mpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n", | |
"\n", | |
"mpl.figure.prototype._init_canvas = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var canvas_div = (this.canvas_div = document.createElement('div'));\n", | |
" canvas_div.setAttribute(\n", | |
" 'style',\n", | |
" 'border: 1px solid #ddd;' +\n", | |
" 'box-sizing: content-box;' +\n", | |
" 'clear: both;' +\n", | |
" 'min-height: 1px;' +\n", | |
" 'min-width: 1px;' +\n", | |
" 'outline: 0;' +\n", | |
" 'overflow: hidden;' +\n", | |
" 'position: relative;' +\n", | |
" 'resize: both;'\n", | |
" );\n", | |
"\n", | |
" function on_keyboard_event_closure(name) {\n", | |
" return function (event) {\n", | |
" return fig.key_event(event, name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" canvas_div.addEventListener(\n", | |
" 'keydown',\n", | |
" on_keyboard_event_closure('key_press')\n", | |
" );\n", | |
" canvas_div.addEventListener(\n", | |
" 'keyup',\n", | |
" on_keyboard_event_closure('key_release')\n", | |
" );\n", | |
"\n", | |
" this._canvas_extra_style(canvas_div);\n", | |
" this.root.appendChild(canvas_div);\n", | |
"\n", | |
" var canvas = (this.canvas = document.createElement('canvas'));\n", | |
" canvas.classList.add('mpl-canvas');\n", | |
" canvas.setAttribute('style', 'box-sizing: content-box;');\n", | |
"\n", | |
" this.context = canvas.getContext('2d');\n", | |
"\n", | |
" var backingStore =\n", | |
" this.context.backingStorePixelRatio ||\n", | |
" this.context.webkitBackingStorePixelRatio ||\n", | |
" this.context.mozBackingStorePixelRatio ||\n", | |
" this.context.msBackingStorePixelRatio ||\n", | |
" this.context.oBackingStorePixelRatio ||\n", | |
" this.context.backingStorePixelRatio ||\n", | |
" 1;\n", | |
"\n", | |
" this.ratio = (window.devicePixelRatio || 1) / backingStore;\n", | |
"\n", | |
" var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n", | |
" 'canvas'\n", | |
" ));\n", | |
" rubberband_canvas.setAttribute(\n", | |
" 'style',\n", | |
" 'box-sizing: content-box; position: absolute; left: 0; top: 0; z-index: 1;'\n", | |
" );\n", | |
"\n", | |
" // Apply a ponyfill if ResizeObserver is not implemented by browser.\n", | |
" if (this.ResizeObserver === undefined) {\n", | |
" if (window.ResizeObserver !== undefined) {\n", | |
" this.ResizeObserver = window.ResizeObserver;\n", | |
" } else {\n", | |
" var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n", | |
" this.ResizeObserver = obs.ResizeObserver;\n", | |
" }\n", | |
" }\n", | |
"\n", | |
" this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n", | |
" var nentries = entries.length;\n", | |
" for (var i = 0; i < nentries; i++) {\n", | |
" var entry = entries[i];\n", | |
" var width, height;\n", | |
" if (entry.contentBoxSize) {\n", | |
" if (entry.contentBoxSize instanceof Array) {\n", | |
" // Chrome 84 implements new version of spec.\n", | |
" width = entry.contentBoxSize[0].inlineSize;\n", | |
" height = entry.contentBoxSize[0].blockSize;\n", | |
" } else {\n", | |
" // Firefox implements old version of spec.\n", | |
" width = entry.contentBoxSize.inlineSize;\n", | |
" height = entry.contentBoxSize.blockSize;\n", | |
" }\n", | |
" } else {\n", | |
" // Chrome <84 implements even older version of spec.\n", | |
" width = entry.contentRect.width;\n", | |
" height = entry.contentRect.height;\n", | |
" }\n", | |
"\n", | |
" // Keep the size of the canvas and rubber band canvas in sync with\n", | |
" // the canvas container.\n", | |
" if (entry.devicePixelContentBoxSize) {\n", | |
" // Chrome 84 implements new version of spec.\n", | |
" canvas.setAttribute(\n", | |
" 'width',\n", | |
" entry.devicePixelContentBoxSize[0].inlineSize\n", | |
" );\n", | |
" canvas.setAttribute(\n", | |
" 'height',\n", | |
" entry.devicePixelContentBoxSize[0].blockSize\n", | |
" );\n", | |
" } else {\n", | |
" canvas.setAttribute('width', width * fig.ratio);\n", | |
" canvas.setAttribute('height', height * fig.ratio);\n", | |
" }\n", | |
" canvas.setAttribute(\n", | |
" 'style',\n", | |
" 'width: ' + width + 'px; height: ' + height + 'px;'\n", | |
" );\n", | |
"\n", | |
" rubberband_canvas.setAttribute('width', width);\n", | |
" rubberband_canvas.setAttribute('height', height);\n", | |
"\n", | |
" // And update the size in Python. We ignore the initial 0/0 size\n", | |
" // that occurs as the element is placed into the DOM, which should\n", | |
" // otherwise not happen due to the minimum size styling.\n", | |
" if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n", | |
" fig.request_resize(width, height);\n", | |
" }\n", | |
" }\n", | |
" });\n", | |
" this.resizeObserverInstance.observe(canvas_div);\n", | |
"\n", | |
" function on_mouse_event_closure(name) {\n", | |
" return function (event) {\n", | |
" return fig.mouse_event(event, name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mousedown',\n", | |
" on_mouse_event_closure('button_press')\n", | |
" );\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseup',\n", | |
" on_mouse_event_closure('button_release')\n", | |
" );\n", | |
" // Throttle sequential mouse events to 1 every 20ms.\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mousemove',\n", | |
" on_mouse_event_closure('motion_notify')\n", | |
" );\n", | |
"\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseenter',\n", | |
" on_mouse_event_closure('figure_enter')\n", | |
" );\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseleave',\n", | |
" on_mouse_event_closure('figure_leave')\n", | |
" );\n", | |
"\n", | |
" canvas_div.addEventListener('wheel', function (event) {\n", | |
" if (event.deltaY < 0) {\n", | |
" event.step = 1;\n", | |
" } else {\n", | |
" event.step = -1;\n", | |
" }\n", | |
" on_mouse_event_closure('scroll')(event);\n", | |
" });\n", | |
"\n", | |
" canvas_div.appendChild(canvas);\n", | |
" canvas_div.appendChild(rubberband_canvas);\n", | |
"\n", | |
" this.rubberband_context = rubberband_canvas.getContext('2d');\n", | |
" this.rubberband_context.strokeStyle = '#000000';\n", | |
"\n", | |
" this._resize_canvas = function (width, height, forward) {\n", | |
" if (forward) {\n", | |
" canvas_div.style.width = width + 'px';\n", | |
" canvas_div.style.height = height + 'px';\n", | |
" }\n", | |
" };\n", | |
"\n", | |
" // Disable right mouse context menu.\n", | |
" this.rubberband_canvas.addEventListener('contextmenu', function (_e) {\n", | |
" event.preventDefault();\n", | |
" return false;\n", | |
" });\n", | |
"\n", | |
" function set_focus() {\n", | |
" canvas.focus();\n", | |
" canvas_div.focus();\n", | |
" }\n", | |
"\n", | |
" window.setTimeout(set_focus, 100);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_toolbar = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var toolbar = document.createElement('div');\n", | |
" toolbar.classList = 'mpl-toolbar';\n", | |
" this.root.appendChild(toolbar);\n", | |
"\n", | |
" function on_click_closure(name) {\n", | |
" return function (_event) {\n", | |
" return fig.toolbar_button_onclick(name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" function on_mouseover_closure(tooltip) {\n", | |
" return function (event) {\n", | |
" if (!event.currentTarget.disabled) {\n", | |
" return fig.toolbar_button_onmouseover(tooltip);\n", | |
" }\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" fig.buttons = {};\n", | |
" var buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'mpl-button-group';\n", | |
" for (var toolbar_ind in mpl.toolbar_items) {\n", | |
" var name = mpl.toolbar_items[toolbar_ind][0];\n", | |
" var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", | |
" var image = mpl.toolbar_items[toolbar_ind][2];\n", | |
" var method_name = mpl.toolbar_items[toolbar_ind][3];\n", | |
"\n", | |
" if (!name) {\n", | |
" /* Instead of a spacer, we start a new button group. */\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
" buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'mpl-button-group';\n", | |
" continue;\n", | |
" }\n", | |
"\n", | |
" var button = (fig.buttons[name] = document.createElement('button'));\n", | |
" button.classList = 'mpl-widget';\n", | |
" button.setAttribute('role', 'button');\n", | |
" button.setAttribute('aria-disabled', 'false');\n", | |
" button.addEventListener('click', on_click_closure(method_name));\n", | |
" button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", | |
"\n", | |
" var icon_img = document.createElement('img');\n", | |
" icon_img.src = '_images/' + image + '.png';\n", | |
" icon_img.srcset = '_images/' + image + '_large.png 2x';\n", | |
" icon_img.alt = tooltip;\n", | |
" button.appendChild(icon_img);\n", | |
"\n", | |
" buttonGroup.appendChild(button);\n", | |
" }\n", | |
"\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
"\n", | |
" var fmt_picker = document.createElement('select');\n", | |
" fmt_picker.classList = 'mpl-widget';\n", | |
" toolbar.appendChild(fmt_picker);\n", | |
" this.format_dropdown = fmt_picker;\n", | |
"\n", | |
" for (var ind in mpl.extensions) {\n", | |
" var fmt = mpl.extensions[ind];\n", | |
" var option = document.createElement('option');\n", | |
" option.selected = fmt === mpl.default_extension;\n", | |
" option.innerHTML = fmt;\n", | |
" fmt_picker.appendChild(option);\n", | |
" }\n", | |
"\n", | |
" var status_bar = document.createElement('span');\n", | |
" status_bar.classList = 'mpl-message';\n", | |
" toolbar.appendChild(status_bar);\n", | |
" this.message = status_bar;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n", | |
" // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n", | |
" // which will in turn request a refresh of the image.\n", | |
" this.send_message('resize', { width: x_pixels, height: y_pixels });\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.send_message = function (type, properties) {\n", | |
" properties['type'] = type;\n", | |
" properties['figure_id'] = this.id;\n", | |
" this.ws.send(JSON.stringify(properties));\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.send_draw_message = function () {\n", | |
" if (!this.waiting) {\n", | |
" this.waiting = true;\n", | |
" this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_save = function (fig, _msg) {\n", | |
" var format_dropdown = fig.format_dropdown;\n", | |
" var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n", | |
" fig.ondownload(fig, format);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_resize = function (fig, msg) {\n", | |
" var size = msg['size'];\n", | |
" if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n", | |
" fig._resize_canvas(size[0], size[1], msg['forward']);\n", | |
" fig.send_message('refresh', {});\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_rubberband = function (fig, msg) {\n", | |
" var x0 = msg['x0'] / fig.ratio;\n", | |
" var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n", | |
" var x1 = msg['x1'] / fig.ratio;\n", | |
" var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n", | |
" x0 = Math.floor(x0) + 0.5;\n", | |
" y0 = Math.floor(y0) + 0.5;\n", | |
" x1 = Math.floor(x1) + 0.5;\n", | |
" y1 = Math.floor(y1) + 0.5;\n", | |
" var min_x = Math.min(x0, x1);\n", | |
" var min_y = Math.min(y0, y1);\n", | |
" var width = Math.abs(x1 - x0);\n", | |
" var height = Math.abs(y1 - y0);\n", | |
"\n", | |
" fig.rubberband_context.clearRect(\n", | |
" 0,\n", | |
" 0,\n", | |
" fig.canvas.width / fig.ratio,\n", | |
" fig.canvas.height / fig.ratio\n", | |
" );\n", | |
"\n", | |
" fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_figure_label = function (fig, msg) {\n", | |
" // Updates the figure title.\n", | |
" fig.header.textContent = msg['label'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_cursor = function (fig, msg) {\n", | |
" var cursor = msg['cursor'];\n", | |
" switch (cursor) {\n", | |
" case 0:\n", | |
" cursor = 'pointer';\n", | |
" break;\n", | |
" case 1:\n", | |
" cursor = 'default';\n", | |
" break;\n", | |
" case 2:\n", | |
" cursor = 'crosshair';\n", | |
" break;\n", | |
" case 3:\n", | |
" cursor = 'move';\n", | |
" break;\n", | |
" }\n", | |
" fig.rubberband_canvas.style.cursor = cursor;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_message = function (fig, msg) {\n", | |
" fig.message.textContent = msg['message'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_draw = function (fig, _msg) {\n", | |
" // Request the server to send over a new figure.\n", | |
" fig.send_draw_message();\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_image_mode = function (fig, msg) {\n", | |
" fig.image_mode = msg['mode'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n", | |
" for (var key in msg) {\n", | |
" if (!(key in fig.buttons)) {\n", | |
" continue;\n", | |
" }\n", | |
" fig.buttons[key].disabled = !msg[key];\n", | |
" fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n", | |
" if (msg['mode'] === 'PAN') {\n", | |
" fig.buttons['Pan'].classList.add('active');\n", | |
" fig.buttons['Zoom'].classList.remove('active');\n", | |
" } else if (msg['mode'] === 'ZOOM') {\n", | |
" fig.buttons['Pan'].classList.remove('active');\n", | |
" fig.buttons['Zoom'].classList.add('active');\n", | |
" } else {\n", | |
" fig.buttons['Pan'].classList.remove('active');\n", | |
" fig.buttons['Zoom'].classList.remove('active');\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.updated_canvas_event = function () {\n", | |
" // Called whenever the canvas gets updated.\n", | |
" this.send_message('ack', {});\n", | |
"};\n", | |
"\n", | |
"// A function to construct a web socket function for onmessage handling.\n", | |
"// Called in the figure constructor.\n", | |
"mpl.figure.prototype._make_on_message_function = function (fig) {\n", | |
" return function socket_on_message(evt) {\n", | |
" if (evt.data instanceof Blob) {\n", | |
" /* FIXME: We get \"Resource interpreted as Image but\n", | |
" * transferred with MIME type text/plain:\" errors on\n", | |
" * Chrome. But how to set the MIME type? It doesn't seem\n", | |
" * to be part of the websocket stream */\n", | |
" evt.data.type = 'image/png';\n", | |
"\n", | |
" /* Free the memory for the previous frames */\n", | |
" if (fig.imageObj.src) {\n", | |
" (window.URL || window.webkitURL).revokeObjectURL(\n", | |
" fig.imageObj.src\n", | |
" );\n", | |
" }\n", | |
"\n", | |
" fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n", | |
" evt.data\n", | |
" );\n", | |
" fig.updated_canvas_event();\n", | |
" fig.waiting = false;\n", | |
" return;\n", | |
" } else if (\n", | |
" typeof evt.data === 'string' &&\n", | |
" evt.data.slice(0, 21) === 'data:image/png;base64'\n", | |
" ) {\n", | |
" fig.imageObj.src = evt.data;\n", | |
" fig.updated_canvas_event();\n", | |
" fig.waiting = false;\n", | |
" return;\n", | |
" }\n", | |
"\n", | |
" var msg = JSON.parse(evt.data);\n", | |
" var msg_type = msg['type'];\n", | |
"\n", | |
" // Call the \"handle_{type}\" callback, which takes\n", | |
" // the figure and JSON message as its only arguments.\n", | |
" try {\n", | |
" var callback = fig['handle_' + msg_type];\n", | |
" } catch (e) {\n", | |
" console.log(\n", | |
" \"No handler for the '\" + msg_type + \"' message type: \",\n", | |
" msg\n", | |
" );\n", | |
" return;\n", | |
" }\n", | |
"\n", | |
" if (callback) {\n", | |
" try {\n", | |
" // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n", | |
" callback(fig, msg);\n", | |
" } catch (e) {\n", | |
" console.log(\n", | |
" \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n", | |
" e,\n", | |
" e.stack,\n", | |
" msg\n", | |
" );\n", | |
" }\n", | |
" }\n", | |
" };\n", | |
"};\n", | |
"\n", | |
"// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n", | |
"mpl.findpos = function (e) {\n", | |
" //this section is from http://www.quirksmode.org/js/events_properties.html\n", | |
" var targ;\n", | |
" if (!e) {\n", | |
" e = window.event;\n", | |
" }\n", | |
" if (e.target) {\n", | |
" targ = e.target;\n", | |
" } else if (e.srcElement) {\n", | |
" targ = e.srcElement;\n", | |
" }\n", | |
" if (targ.nodeType === 3) {\n", | |
" // defeat Safari bug\n", | |
" targ = targ.parentNode;\n", | |
" }\n", | |
"\n", | |
" // pageX,Y are the mouse positions relative to the document\n", | |
" var boundingRect = targ.getBoundingClientRect();\n", | |
" var x = e.pageX - (boundingRect.left + document.body.scrollLeft);\n", | |
" var y = e.pageY - (boundingRect.top + document.body.scrollTop);\n", | |
"\n", | |
" return { x: x, y: y };\n", | |
"};\n", | |
"\n", | |
"/*\n", | |
" * return a copy of an object with only non-object keys\n", | |
" * we need this to avoid circular references\n", | |
" * http://stackoverflow.com/a/24161582/3208463\n", | |
" */\n", | |
"function simpleKeys(original) {\n", | |
" return Object.keys(original).reduce(function (obj, key) {\n", | |
" if (typeof original[key] !== 'object') {\n", | |
" obj[key] = original[key];\n", | |
" }\n", | |
" return obj;\n", | |
" }, {});\n", | |
"}\n", | |
"\n", | |
"mpl.figure.prototype.mouse_event = function (event, name) {\n", | |
" var canvas_pos = mpl.findpos(event);\n", | |
"\n", | |
" if (name === 'button_press') {\n", | |
" this.canvas.focus();\n", | |
" this.canvas_div.focus();\n", | |
" }\n", | |
"\n", | |
" var x = canvas_pos.x * this.ratio;\n", | |
" var y = canvas_pos.y * this.ratio;\n", | |
"\n", | |
" this.send_message(name, {\n", | |
" x: x,\n", | |
" y: y,\n", | |
" button: event.button,\n", | |
" step: event.step,\n", | |
" guiEvent: simpleKeys(event),\n", | |
" });\n", | |
"\n", | |
" /* This prevents the web browser from automatically changing to\n", | |
" * the text insertion cursor when the button is pressed. We want\n", | |
" * to control all of the cursor setting manually through the\n", | |
" * 'cursor' event from matplotlib */\n", | |
" event.preventDefault();\n", | |
" return false;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._key_event_extra = function (_event, _name) {\n", | |
" // Handle any extra behaviour associated with a key event\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.key_event = function (event, name) {\n", | |
" // Prevent repeat events\n", | |
" if (name === 'key_press') {\n", | |
" if (event.which === this._key) {\n", | |
" return;\n", | |
" } else {\n", | |
" this._key = event.which;\n", | |
" }\n", | |
" }\n", | |
" if (name === 'key_release') {\n", | |
" this._key = null;\n", | |
" }\n", | |
"\n", | |
" var value = '';\n", | |
" if (event.ctrlKey && event.which !== 17) {\n", | |
" value += 'ctrl+';\n", | |
" }\n", | |
" if (event.altKey && event.which !== 18) {\n", | |
" value += 'alt+';\n", | |
" }\n", | |
" if (event.shiftKey && event.which !== 16) {\n", | |
" value += 'shift+';\n", | |
" }\n", | |
"\n", | |
" value += 'k';\n", | |
" value += event.which.toString();\n", | |
"\n", | |
" this._key_event_extra(event, name);\n", | |
"\n", | |
" this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n", | |
" return false;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.toolbar_button_onclick = function (name) {\n", | |
" if (name === 'download') {\n", | |
" this.handle_save(this, null);\n", | |
" } else {\n", | |
" this.send_message('toolbar_button', { name: name });\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n", | |
" this.message.textContent = tooltip;\n", | |
"};\n", | |
"\n", | |
"///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n", | |
"// prettier-ignore\n", | |
"var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\n", | |
"mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", | |
"\n", | |
"mpl.extensions = [\"eps\", \"jpeg\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n", | |
"\n", | |
"mpl.default_extension = \"png\";/* global mpl */\n", | |
"\n", | |
"var comm_websocket_adapter = function (comm) {\n", | |
" // Create a \"websocket\"-like object which calls the given IPython comm\n", | |
" // object with the appropriate methods. Currently this is a non binary\n", | |
" // socket, so there is still some room for performance tuning.\n", | |
" var ws = {};\n", | |
"\n", | |
" ws.close = function () {\n", | |
" comm.close();\n", | |
" };\n", | |
" ws.send = function (m) {\n", | |
" //console.log('sending', m);\n", | |
" comm.send(m);\n", | |
" };\n", | |
" // Register the callback with on_msg.\n", | |
" comm.on_msg(function (msg) {\n", | |
" //console.log('receiving', msg['content']['data'], msg);\n", | |
" // Pass the mpl event to the overridden (by mpl) onmessage function.\n", | |
" ws.onmessage(msg['content']['data']);\n", | |
" });\n", | |
" return ws;\n", | |
"};\n", | |
"\n", | |
"mpl.mpl_figure_comm = function (comm, msg) {\n", | |
" // This is the function which gets called when the mpl process\n", | |
" // starts-up an IPython Comm through the \"matplotlib\" channel.\n", | |
"\n", | |
" var id = msg.content.data.id;\n", | |
" // Get hold of the div created by the display call when the Comm\n", | |
" // socket was opened in Python.\n", | |
" var element = document.getElementById(id);\n", | |
" var ws_proxy = comm_websocket_adapter(comm);\n", | |
"\n", | |
" function ondownload(figure, _format) {\n", | |
" window.open(figure.canvas.toDataURL());\n", | |
" }\n", | |
"\n", | |
" var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n", | |
"\n", | |
" // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n", | |
" // web socket which is closed, not our websocket->open comm proxy.\n", | |
" ws_proxy.onopen();\n", | |
"\n", | |
" fig.parent_element = element;\n", | |
" fig.cell_info = mpl.find_output_cell(\"<div id='\" + id + \"'></div>\");\n", | |
" if (!fig.cell_info) {\n", | |
" console.error('Failed to find cell for figure', id, fig);\n", | |
" return;\n", | |
" }\n", | |
" fig.cell_info[0].output_area.element.on(\n", | |
" 'cleared',\n", | |
" { fig: fig },\n", | |
" fig._remove_fig_handler\n", | |
" );\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_close = function (fig, msg) {\n", | |
" var width = fig.canvas.width / fig.ratio;\n", | |
" fig.cell_info[0].output_area.element.off(\n", | |
" 'cleared',\n", | |
" fig._remove_fig_handler\n", | |
" );\n", | |
" fig.resizeObserverInstance.unobserve(fig.canvas_div);\n", | |
"\n", | |
" // Update the output cell to use the data from the current canvas.\n", | |
" fig.push_to_output();\n", | |
" var dataURL = fig.canvas.toDataURL();\n", | |
" // Re-enable the keyboard manager in IPython - without this line, in FF,\n", | |
" // the notebook keyboard shortcuts fail.\n", | |
" IPython.keyboard_manager.enable();\n", | |
" fig.parent_element.innerHTML =\n", | |
" '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n", | |
" fig.close_ws(fig, msg);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.close_ws = function (fig, msg) {\n", | |
" fig.send_message('closing', msg);\n", | |
" // fig.ws.close()\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.push_to_output = function (_remove_interactive) {\n", | |
" // Turn the data on the canvas into data in the output cell.\n", | |
" var width = this.canvas.width / this.ratio;\n", | |
" var dataURL = this.canvas.toDataURL();\n", | |
" this.cell_info[1]['text/html'] =\n", | |
" '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.updated_canvas_event = function () {\n", | |
" // Tell IPython that the notebook contents must change.\n", | |
" IPython.notebook.set_dirty(true);\n", | |
" this.send_message('ack', {});\n", | |
" var fig = this;\n", | |
" // Wait a second, then push the new image to the DOM so\n", | |
" // that it is saved nicely (might be nice to debounce this).\n", | |
" setTimeout(function () {\n", | |
" fig.push_to_output();\n", | |
" }, 1000);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_toolbar = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var toolbar = document.createElement('div');\n", | |
" toolbar.classList = 'btn-toolbar';\n", | |
" this.root.appendChild(toolbar);\n", | |
"\n", | |
" function on_click_closure(name) {\n", | |
" return function (_event) {\n", | |
" return fig.toolbar_button_onclick(name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" function on_mouseover_closure(tooltip) {\n", | |
" return function (event) {\n", | |
" if (!event.currentTarget.disabled) {\n", | |
" return fig.toolbar_button_onmouseover(tooltip);\n", | |
" }\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" fig.buttons = {};\n", | |
" var buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'btn-group';\n", | |
" var button;\n", | |
" for (var toolbar_ind in mpl.toolbar_items) {\n", | |
" var name = mpl.toolbar_items[toolbar_ind][0];\n", | |
" var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", | |
" var image = mpl.toolbar_items[toolbar_ind][2];\n", | |
" var method_name = mpl.toolbar_items[toolbar_ind][3];\n", | |
"\n", | |
" if (!name) {\n", | |
" /* Instead of a spacer, we start a new button group. */\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
" buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'btn-group';\n", | |
" continue;\n", | |
" }\n", | |
"\n", | |
" button = fig.buttons[name] = document.createElement('button');\n", | |
" button.classList = 'btn btn-default';\n", | |
" button.href = '#';\n", | |
" button.title = name;\n", | |
" button.innerHTML = '<i class=\"fa ' + image + ' fa-lg\"></i>';\n", | |
" button.addEventListener('click', on_click_closure(method_name));\n", | |
" button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", | |
" buttonGroup.appendChild(button);\n", | |
" }\n", | |
"\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
"\n", | |
" // Add the status bar.\n", | |
" var status_bar = document.createElement('span');\n", | |
" status_bar.classList = 'mpl-message pull-right';\n", | |
" toolbar.appendChild(status_bar);\n", | |
" this.message = status_bar;\n", | |
"\n", | |
" // Add the close button to the window.\n", | |
" var buttongrp = document.createElement('div');\n", | |
" buttongrp.classList = 'btn-group inline pull-right';\n", | |
" button = document.createElement('button');\n", | |
" button.classList = 'btn btn-mini btn-primary';\n", | |
" button.href = '#';\n", | |
" button.title = 'Stop Interaction';\n", | |
" button.innerHTML = '<i class=\"fa fa-power-off icon-remove icon-large\"></i>';\n", | |
" button.addEventListener('click', function (_evt) {\n", | |
" fig.handle_close(fig, {});\n", | |
" });\n", | |
" button.addEventListener(\n", | |
" 'mouseover',\n", | |
" on_mouseover_closure('Stop Interaction')\n", | |
" );\n", | |
" buttongrp.appendChild(button);\n", | |
" var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n", | |
" titlebar.insertBefore(buttongrp, titlebar.firstChild);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._remove_fig_handler = function (event) {\n", | |
" var fig = event.data.fig;\n", | |
" if (event.target !== this) {\n", | |
" // Ignore bubbled events from children.\n", | |
" return;\n", | |
" }\n", | |
" fig.close_ws(fig, {});\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._root_extra_style = function (el) {\n", | |
" el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._canvas_extra_style = function (el) {\n", | |
" // this is important to make the div 'focusable\n", | |
" el.setAttribute('tabindex', 0);\n", | |
" // reach out to IPython and tell the keyboard manager to turn it's self\n", | |
" // off when our div gets focus\n", | |
"\n", | |
" // location in version 3\n", | |
" if (IPython.notebook.keyboard_manager) {\n", | |
" IPython.notebook.keyboard_manager.register_events(el);\n", | |
" } else {\n", | |
" // location in version 2\n", | |
" IPython.keyboard_manager.register_events(el);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._key_event_extra = function (event, _name) {\n", | |
" var manager = IPython.notebook.keyboard_manager;\n", | |
" if (!manager) {\n", | |
" manager = IPython.keyboard_manager;\n", | |
" }\n", | |
"\n", | |
" // Check for shift+enter\n", | |
" if (event.shiftKey && event.which === 13) {\n", | |
" this.canvas_div.blur();\n", | |
" // select the cell after this one\n", | |
" var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", | |
" IPython.notebook.select(index + 1);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_save = function (fig, _msg) {\n", | |
" fig.ondownload(fig, null);\n", | |
"};\n", | |
"\n", | |
"mpl.find_output_cell = function (html_output) {\n", | |
" // Return the cell and output element which can be found *uniquely* in the notebook.\n", | |
" // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", | |
" // IPython event is triggered only after the cells have been serialised, which for\n", | |
" // our purposes (turning an active figure into a static one), is too late.\n", | |
" var cells = IPython.notebook.get_cells();\n", | |
" var ncells = cells.length;\n", | |
" for (var i = 0; i < ncells; i++) {\n", | |
" var cell = cells[i];\n", | |
" if (cell.cell_type === 'code') {\n", | |
" for (var j = 0; j < cell.output_area.outputs.length; j++) {\n", | |
" var data = cell.output_area.outputs[j];\n", | |
" if (data.data) {\n", | |
" // IPython >= 3 moved mimebundle to data attribute of output\n", | |
" data = data.data;\n", | |
" }\n", | |
" if (data['text/html'] === html_output) {\n", | |
" return [cell, data, j];\n", | |
" }\n", | |
" }\n", | |
" }\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"// Register the function which deals with the matplotlib target/channel.\n", | |
"// The kernel may be null if the page has been refreshed.\n", | |
"if (IPython.notebook.kernel !== null) {\n", | |
" IPython.notebook.kernel.comm_manager.register_target(\n", | |
" 'matplotlib',\n", | |
" mpl.mpl_figure_comm\n", | |
" );\n", | |
"}\n" | |
], | |
"text/plain": [ | |
"<IPython.core.display.Javascript object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
}, | |
{ | |
"data": { | |
"text/html": [ | |
"<img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAgAElEQVR4nO3deXTU9b3/8Q8J2QhJKFsWUJASwBqiUorYFlII2y2RLscKHLbqYdWgVk/rdjVSBUKrtvVeEa8Fu0HANhS9FZGAikoC2jtYArhUUIzB5ZaQiFcShbx+f/DLyJABEj55B2Gez3Mefzj5EibfjMyLmczgREREREQRlTvTV4CIiIiIWjcGIBEREVGExQAkIiIiirAYgEREREQRFgOQiIiIKMJiABIRERFFWAxAIiIiogiLAUhEREQUYTEAiYiIiCIsBiARERFRhMUAJCIiIoqwGIBEREREERYDkIiIiCjCYgASERERRVgMQCIiIqIIiwFIREREFGExAImIiIgiLAYgERERUYTFACQiIiKKsBiARERERBEWA5CIiIgowmIAEhEREUVYDEAiIiKiCIsBSERERBRhMQCJiIiIIiwGIBEREVGExQAkIiIiirAYgEREREQRFgOQiIiIKMJiABIRERFFWAxAIiIiogiLAUhEREQUYTEAiYiIiCIsBiARERFRhMUAJCIiIoqwGIBEREREERYDkFqsgoICOef0v//7v2E/ftFFFyknJ+e0Pve0adPUo0ePkMv279+v8ePHq0uXLnLO6Xvf+95pfe4zXY8ePTRt2rTgfz/33HNyzum5555r1ud56KGH9NhjjzXr14T7vaZNm6bExMRmfZ5TtXnzZhUUFOjAgQONPpaTk3PatwvfAoGAhg4dquTkZDnn9Ktf/eqMXI/m9MEHH+iWW25RVlaWEhMTFRcXp969e+v666/Xm2++2ezPt3z58rBf99tvvy3nnH75y1+2xNU+oznnVFBQEPzvnTt3qqCgQG+//XajY3NycnTRRReZXp8zeZsnaogBSC2W5QB86623FAgEQi678cYbFRsbqz/96U8qKyvTG2+8cVqf+0x3/ACsqalRWVmZampqmvV5Tuf8hvu9LAbgL3/5Sznnwt7h7ty5Uzt37mzR36+pXXLJJcrMzNTatWtVVlam999//4xcj6a2detWdenSRZ07d9bdd9+tZ555Rs8995yWLFmib3/72+rQoUOzP+fYsWMb/eVKOrcGYFlZmSoqKoL//ec///mEf8liAFKkxACkFstyAIZrxIgRuvDCC1vs89XX1+vTTz9tsc/X1I4fgKdbc87vZ599ps8//zzsx1p7AJ7J2rZtqzlz5pzyuE8//VT19fWtcI1OXE1NjdLS0nTeeeeFjJlj+/Of/9zszxsJA/D4GIBEDEBqwZo7ABueflyxYoVuv/12paenKykpSbm5uXr99ddDfu2xTwE33DEdr+EP8/3792vOnDnKyMhQTEyMLrjgAt1+++2qra0N+ZzOOV133XV6+OGH1a9fP8XExOjhhx/WY489JuecNm7cqOnTp6tjx45KSkrSlClT9Mknn+j999/Xj370I6WkpCgtLU0333yzPvvss1Oen88++0w//elPlZqaqoSEBH3rW9/S1q1bm/QU8O7duzV+/Hilp6crNjZWXbt21fDhw7Vt2zZJR0fk8eej4Xw1fL4//OEPuummm5SRkaE2bdrotddeO+lTwDt27NDw4cPVrl07de7cWdddd53+7//+L3hcw/ch3NPOxz7l1nC7ONH3K9ydYXO/h3/4wx/Ur18/JSQkKDs7W//93/990u9Fw/f4eMd+7JlnntHVV1+tzp07yzmnQ4cO6ciRI1q0aJH69u2r2NhYdenSRVOmTGk0yBpGRGlpqS6//HLFx8erR48eWrZsmSTpb3/7my699FIlJCQoKytLTz/99EmvryTdd999cs6pqKjolMf+4Q9/kHNOpaWljT42b948tW3bVpWVlcrJyTnheTh2AN5///3q2bOnEhMTNXjwYJWVlTX6vE888YQGDx6shIQEtW/fXiNGjGj0+zfcFnbs2KEJEyYoOTlZXbt21dVXX63q6uqTfk3/+Z//qTZt2ujDDz9sdE6uvfba4GVHjhxRhw4ddNNNNwUvO/b2eKLvfcPtuOF79/LLL+vb3/62EhISdMEFF2jhwoU6cuTIyU/8///9H3zwQV188cWKj49XSkqKLrvsMj3xxBPBY8Ld5u+++24NGjRIX/nKV5SUlKRLL71Uv/3tbxv9xWPjxo3KyclRx44dFR8fr/POO08//OEPQ/7fXLx4sbKzs5WYmKj27durb9++uu2220553SmyYgBSi3W6A7Bnz56aNGmSnnrqKRUVFen8889XZmamDh8+HDz22AFYW1ursrIyXXrpperVq5fKysqCT2MeOnQo+Afffffdp/Xr1+vOO+9U27Zt9d3vfjfk+jjn1K1bN2VnZ2vFihV69tlntWPHjuAdxAUXXKCbb75Z69ev16JFixQdHa2JEydq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Gx13/O3+RP8PNZzzU71I6I9//KOcc3rzzTd1xx13BB95v+qqqzRhwgR9+umniouL089//vMTXl/p9F4EEu7PoOObPn26oqOjT/lI5vG3+a1btyo6Olq5ublatWqVNm/erFdeeUV33HFHo/PywgsvKC8vT4mJiXLOqVevXo2eHVi2bJkuv/xyRUdHq02bNho0aFDII+dEEgOQWrD/+q//knNO//M//9PoY/X19UpOTg75mSSLAXjVVVepffv2J3z06NhHM050p2sxAH0eATy+N954Q/fcc4+io6M1a9as4OWnGoDhXiHq8wjg+++/H/YR3IanrH0eAfT9HrbEADz++3+qRwAHDx4c8jW19AC8//77Q556bUp///vf5ZxTcXGxZsyYoQ4dOjQakL4D8HQeATzdAbhv3z4557R48WINGjQo+GjwI488oi5dumjdunVyrvGLX1prAJ7uI4A/+clPFB8fr0OHDoUcF24ANnT48GFt2bJFkyZNOuHt4pNPPtHatWv1jW98Q7GxsXrnnXdOer0osmIAUov11ltvqU2bNvrZz37W6GNr165tNOosBuAjjzwi546+2ODYGgZISUlJ8LLWHIANd5Kn8zOAJ+qSSy7RN77xjeB/DxgwQIMGDWp03OkOwBP9DGDDD/XX19crPj6+0aOoS5cubXSH++CDD8o5p127djW6DsffGbbE99BiADb8aMLxP0zf8DNxx/7cl8UArK6uDr4NTLhHkiWpuLi40WXf/OY3NWjQILVr1y746O2x/fCHP1TXrl0bXd7UAXjkyBF169ZNl1xyScho/+STT9S1a1d961vfCl7mOwCloz+Ll5ubq+joaG3YsEGStGfPHjl39EUoycnJjd7i6Pjb45NPPinnQl8o0pDPAGz4GcA777zzpMcdf5u/6aab1L59+5B3E/j00091/vnnn/K8VFdXyzmnn/70pyc8Zs2aNXLO6amnnjrp9aLIigFILdrcuXPVpk0bzZw5U2vWrNEzzzyje++9V+3bt9fAgQNVV1cXPNZiADa8gjQpKUkPPPCASkpKVFBQoJiYmLCvIG2tAShJkydPDg7khlcBZ2RknPJVwP/4xz80ZMgQPfjgg3r66ae1ceNG3XHHHYqKigp54cG0adMUFxenlStX6uWXX9b27dtDPl9zBuCJXgX8b//2byG/fvr06YqPj9f999+vDRs2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/vnnNWDAAMXFxemCCy7Qww8/3ErX9OyvOee6uLhYI0aMUOfOnZWUlKTBgwdr3bp1rXhtz96ae5tu6KWXXlJ0dLQuvvhi42t4btTc81xbW6vbb79d559/vmJjY9WrVy8tXbq0la7t2Vtzz/Of/vQnZWdnKyEhQWlpafrxj3+sf/3rX610bc/ONm3apLy8PKWnp8s5p7/+9a+n/DXcF7ZcDMAzUMP7Dz766KPatWuXbrjhBiUmJmrv3r1hj9+zZ4/atWunG264Qbt27dKjjz6qmJgY/eUvf2nla3721dxzfcMNN2jRokV6+eWX9eabb+q2225TTEyMAoFAK1/zs6vmnueGqqur1atXL40aNYoB2IRO5zyPGzdOl112mUpKSvT2229r69atjf4NdAqtuef5xRdfVFRUlH7zm99oz549evHFF3XRRRfp+9//fitf87OrtWvX6o477lBxcXGTBiD3hS0bA/AMNGjQIM2ePTvksn79+p3wXyD52c9+pn79+oVcNmvWLA0ePNjsOp4rNfdch+trX/ua5s2b19JX7ZzqdM/z+PHj9e///u8qKChgADah5p7np59+WikpKdq/f39rXL1zpuae51/+8pfq1atXyGUPPvigunfvbnYdz7WaMgC5L2zZGICt3On8G8RDhgzR9ddfH3LZ6tWr1bZtW3322Wdm1/VsryX+vecjR47ovPPO03/8x39YXMVzotM9z8uWLdPAgQP1+eefMwCb0Omc5zlz5ig3N1e33HKLMjIylJmZqZtvvlmffvppa1zls7LTOc+bN29WbGysnnrqKdXX1+uDDz7Q0KFDNWvWrNa4yudETRmA3Be2bAzAVq6yslLOuUZPwcyfP199+vQJ+2syMzM1f/78kMs2b94s55z27dtndl3P9k7nXB/fL37xC3Xs2FEffvihxVU8Jzqd8/zmm2+qa9eueuONNySJAdiETuc8jx49WnFxcRo7dqy2bt2qp556Sj169NDVV1/dGlf5rOx0/9z485//rPbt26tt27ZyzmncuHGMkmbUlAHIfWHLxgBs5Rr+cCktLQ25/N5771Xfvn3D/prMzEwtWLAg5LKXXnpJzjm9//77Ztf1bO90zvWxrVixQu3atVNJSYnVVTwnau55Pnz4sAYOHBjyw9sMwFN3OrfnkSNHKj4+XtXV1cHLiouL1aZNGx4FPEGnc5537typ9PR0/eIXv9A//vEPrVu3Tv3799c111zTGlf5nKipA5D7wpaLAdjK8RRw6+XzFPDKlSuVkJCgv/3tb5ZX8Zyouef5wIEDcs4pOjo6qE2bNsHLNm7c2FpX/azqdG7PU6dO1Ve/+tWQy3bt2iXnnN58802z63o2dzrnefLkybryyitDLnvxxRd5ZKoZ8RRw68cAPAMNGjRIc+bMCbnswgsvPOmLQC688MKQy2bPns0Pvjah5p5r6egjf/Hx8U16SwI6WnPO85EjR1ReXh5izpw56tu3r8rLy/XJJ5+01tU+62ru7fmRRx5RQkKCDh48GLxszZo1ioqK4hHAk9Tc8/zDH/5QV111VchlpaWlcs6psrLS7HqeSzX1RSDcF7ZcDMAzUMNbDCxdulS7du3SjTfeqMTERL3zzjuSpFtvvVVTpkwJHt/w0vef/OQn2rVrl5YuXcpL35tYc8/1ihUr1LZtWz300EN6//33g459Co0a19zzfHw8Bdy0mnueDx48qO7du+vKK6/Uzp07tWnTJmVmZmr69Oln6ks4K2rueX7sscfUtm1bLV68WLt379ZLL72kgQMHatCgQWfqSzgrOnjwoLZt26Zt27bJOacHHnhA27ZtC77dDveFtjEAz1APPfSQevToodjYWA0YMECbNm0KfmzatGnKyckJOf7555/XpZdeqtjYWPXs2ZM3v2xGzTnXOTk5cs41Mm3atNa/4mdZzb1NHxsDsOk19zy/9tprGjFihBISEtS9e3fddNNNPPrXhJp7nh988EF97WtfU0JCgtLT0zVp0iS99957rXytz66ee+65k/55y32hbQxAIiIiogiLAUhEREQUYTEAiYiIiCIsBiARERFRhMUAJCIiIoqwGIBEREREERYDkIiIiCjCYgASERERRVgMQCIiIqIIiwFIREREFGExAImIiIgiLAYgERERUYTFACQiIiKKsBiARERERBEWA5CIiIgowmIAEhEREUVYDEAiIiKiCIsBSERERBRhMQCJiIiIIiwGIBEREVGExQAkIiIiirAYgEREREQRFgOQiIiIKMJiABIRERFFWAxAIiIiogiLAUhEREQUYTEAiYiIiCIsBiARERFRhMUAJCIiIoqwGIBEREREERYDkIiIiCjCYgASERERRVj/DwrOREqEF4KqAAAAAElFTkSuQmCC\" width=\"640\">" | |
], | |
"text/plain": [ | |
"<IPython.core.display.HTML object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"print(\"Performances of the cdef class for Mersenne-twisters implemented in Cython\")\n", | |
"%timeit cython_uniform_mtc(shape)\n", | |
"fig, ax = subplots()\n", | |
"ax.hist(cython_uniform_mtc(shape).ravel())\n", | |
"ax.set_title(\"Uniform distribution from Cython with class\")\n", | |
"pass" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"This version with a class is even slightly faster. Let's double check the results are corrects. But before, let's summarise the performances obtained:\n", | |
"\n", | |
"## Summary of the performances obtained \n", | |
"\n", | |
"### Against Python" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 15, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"Performances of the raw random number generator\n", | |
"Random module from Python\n", | |
"58.1 ns ± 0.18 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)\n", | |
"MT written in Cython\n", | |
"45.6 ns ± 1.46 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)\n" | |
] | |
} | |
], | |
"source": [ | |
"print(\"Performances of the raw random number generator\")\n", | |
"\n", | |
"mt = MT(0)\n", | |
"print(\"Random module from Python\")\n", | |
"%timeit random.random()\n", | |
"print(\"MT written in Cython\")\n", | |
"%timeit mt.uniform()" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 16, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"Comparison of the performances of the various implementations\n", | |
"Numpy from Python\n", | |
"36.9 ms ± 130 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", | |
"\n", | |
"Cython with `rand` from C\n", | |
"59.1 ms ± 525 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", | |
"\n", | |
"Cython with C++ random number generator (32bits)\n", | |
"80.8 ms ± 717 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", | |
"\n", | |
"Cython with C++ random number generator (64bits)\n", | |
"67.5 ms ± 289 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", | |
"\n", | |
"Cython with Numpy's C-API random number generator (Permuted congruential generator)\n", | |
"33.3 ms ± 334 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", | |
"\n", | |
"Cython with MT implemented in Cython (original)\n", | |
"21.5 ms ± 225 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", | |
"\n", | |
"Cython with MT implemented in Cython (modified)\n", | |
"21.5 ms ± 824 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" | |
] | |
} | |
], | |
"source": [ | |
"# Comparison of the performances of the various implementations\n", | |
"print(\"Comparison of the performances of the various implementations\")\n", | |
"print(\"Numpy from Python\")\n", | |
"%timeit numpy.random.random(shape)\n", | |
"print()\n", | |
"print(\"Cython with `rand` from C\")\n", | |
"%timeit cython_uniform_rand(shape)\n", | |
"print()\n", | |
"print(\"Cython with C++ random number generator (32bits)\")\n", | |
"%timeit cython_uniform_cpp(shape)\n", | |
"print()\n", | |
"print(\"Cython with C++ random number generator (64bits)\")\n", | |
"%timeit cython_uniform64_cpp(shape)\n", | |
"print()\n", | |
"print(\"Cython with Numpy's C-API random number generator (Permuted congruential generator)\")\n", | |
"%timeit cython_uniform_cnp(shape)\n", | |
"print()\n", | |
"print(\"Cython with MT implemented in Cython (original)\")\n", | |
"%timeit cython_uniform_mt(shape)\n", | |
"print()\n", | |
"print(\"Cython with MT implemented in Cython (modified)\")\n", | |
"%timeit cython_uniform_mtc(shape)" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 17, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"Comparison performances for normal distributions:\n", | |
"Distribution from Numpy\n", | |
"171 ms ± 740 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", | |
"\n", | |
"Distribution from C++ (64 bits)\n", | |
"241 ms ± 779 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n", | |
"\n", | |
"Box-Muller tranformation\n", | |
"167 ms ± 250 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", | |
"\n", | |
"Marsaglia transformation\n", | |
"83.1 ms ± 212 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n", | |
"\n" | |
] | |
} | |
], | |
"source": [ | |
"print(\"Comparison performances for normal distributions:\")\n", | |
"z = numpy.zeros(shape)\n", | |
"s = numpy.ones(shape)\n", | |
"print(\"Distribution from Numpy\")\n", | |
"%timeit numpy.random.normal(z, s)\n", | |
"print()\n", | |
"print(\"Distribution from C++ (64 bits)\")\n", | |
"%timeit cython_normal64_cpp(z, s)\n", | |
"print()\n", | |
"print(\"Box-Muller tranformation\")\n", | |
"%timeit cython_normal_bm_mtc(z, s)\n", | |
"print()\n", | |
"print(\"Marsaglia transformation\")\n", | |
"%timeit cython_normal_m_mtc(z, s)\n", | |
"print()" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 20, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"data": { | |
"application/javascript": [ | |
"/* Put everything inside the global mpl namespace */\n", | |
"/* global mpl */\n", | |
"window.mpl = {};\n", | |
"\n", | |
"mpl.get_websocket_type = function () {\n", | |
" if (typeof WebSocket !== 'undefined') {\n", | |
" return WebSocket;\n", | |
" } else if (typeof MozWebSocket !== 'undefined') {\n", | |
" return MozWebSocket;\n", | |
" } else {\n", | |
" alert(\n", | |
" 'Your browser does not have WebSocket support. ' +\n", | |
" 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", | |
" 'Firefox 4 and 5 are also supported but you ' +\n", | |
" 'have to enable WebSockets in about:config.'\n", | |
" );\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n", | |
" this.id = figure_id;\n", | |
"\n", | |
" this.ws = websocket;\n", | |
"\n", | |
" this.supports_binary = this.ws.binaryType !== undefined;\n", | |
"\n", | |
" if (!this.supports_binary) {\n", | |
" var warnings = document.getElementById('mpl-warnings');\n", | |
" if (warnings) {\n", | |
" warnings.style.display = 'block';\n", | |
" warnings.textContent =\n", | |
" 'This browser does not support binary websocket messages. ' +\n", | |
" 'Performance may be slow.';\n", | |
" }\n", | |
" }\n", | |
"\n", | |
" this.imageObj = new Image();\n", | |
"\n", | |
" this.context = undefined;\n", | |
" this.message = undefined;\n", | |
" this.canvas = undefined;\n", | |
" this.rubberband_canvas = undefined;\n", | |
" this.rubberband_context = undefined;\n", | |
" this.format_dropdown = undefined;\n", | |
"\n", | |
" this.image_mode = 'full';\n", | |
"\n", | |
" this.root = document.createElement('div');\n", | |
" this.root.setAttribute('style', 'display: inline-block');\n", | |
" this._root_extra_style(this.root);\n", | |
"\n", | |
" parent_element.appendChild(this.root);\n", | |
"\n", | |
" this._init_header(this);\n", | |
" this._init_canvas(this);\n", | |
" this._init_toolbar(this);\n", | |
"\n", | |
" var fig = this;\n", | |
"\n", | |
" this.waiting = false;\n", | |
"\n", | |
" this.ws.onopen = function () {\n", | |
" fig.send_message('supports_binary', { value: fig.supports_binary });\n", | |
" fig.send_message('send_image_mode', {});\n", | |
" if (fig.ratio !== 1) {\n", | |
" fig.send_message('set_dpi_ratio', { dpi_ratio: fig.ratio });\n", | |
" }\n", | |
" fig.send_message('refresh', {});\n", | |
" };\n", | |
"\n", | |
" this.imageObj.onload = function () {\n", | |
" if (fig.image_mode === 'full') {\n", | |
" // Full images could contain transparency (where diff images\n", | |
" // almost always do), so we need to clear the canvas so that\n", | |
" // there is no ghosting.\n", | |
" fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", | |
" }\n", | |
" fig.context.drawImage(fig.imageObj, 0, 0);\n", | |
" };\n", | |
"\n", | |
" this.imageObj.onunload = function () {\n", | |
" fig.ws.close();\n", | |
" };\n", | |
"\n", | |
" this.ws.onmessage = this._make_on_message_function(this);\n", | |
"\n", | |
" this.ondownload = ondownload;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_header = function () {\n", | |
" var titlebar = document.createElement('div');\n", | |
" titlebar.classList =\n", | |
" 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n", | |
" var titletext = document.createElement('div');\n", | |
" titletext.classList = 'ui-dialog-title';\n", | |
" titletext.setAttribute(\n", | |
" 'style',\n", | |
" 'width: 100%; text-align: center; padding: 3px;'\n", | |
" );\n", | |
" titlebar.appendChild(titletext);\n", | |
" this.root.appendChild(titlebar);\n", | |
" this.header = titletext;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n", | |
"\n", | |
"mpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n", | |
"\n", | |
"mpl.figure.prototype._init_canvas = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var canvas_div = (this.canvas_div = document.createElement('div'));\n", | |
" canvas_div.setAttribute(\n", | |
" 'style',\n", | |
" 'border: 1px solid #ddd;' +\n", | |
" 'box-sizing: content-box;' +\n", | |
" 'clear: both;' +\n", | |
" 'min-height: 1px;' +\n", | |
" 'min-width: 1px;' +\n", | |
" 'outline: 0;' +\n", | |
" 'overflow: hidden;' +\n", | |
" 'position: relative;' +\n", | |
" 'resize: both;'\n", | |
" );\n", | |
"\n", | |
" function on_keyboard_event_closure(name) {\n", | |
" return function (event) {\n", | |
" return fig.key_event(event, name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" canvas_div.addEventListener(\n", | |
" 'keydown',\n", | |
" on_keyboard_event_closure('key_press')\n", | |
" );\n", | |
" canvas_div.addEventListener(\n", | |
" 'keyup',\n", | |
" on_keyboard_event_closure('key_release')\n", | |
" );\n", | |
"\n", | |
" this._canvas_extra_style(canvas_div);\n", | |
" this.root.appendChild(canvas_div);\n", | |
"\n", | |
" var canvas = (this.canvas = document.createElement('canvas'));\n", | |
" canvas.classList.add('mpl-canvas');\n", | |
" canvas.setAttribute('style', 'box-sizing: content-box;');\n", | |
"\n", | |
" this.context = canvas.getContext('2d');\n", | |
"\n", | |
" var backingStore =\n", | |
" this.context.backingStorePixelRatio ||\n", | |
" this.context.webkitBackingStorePixelRatio ||\n", | |
" this.context.mozBackingStorePixelRatio ||\n", | |
" this.context.msBackingStorePixelRatio ||\n", | |
" this.context.oBackingStorePixelRatio ||\n", | |
" this.context.backingStorePixelRatio ||\n", | |
" 1;\n", | |
"\n", | |
" this.ratio = (window.devicePixelRatio || 1) / backingStore;\n", | |
"\n", | |
" var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n", | |
" 'canvas'\n", | |
" ));\n", | |
" rubberband_canvas.setAttribute(\n", | |
" 'style',\n", | |
" 'box-sizing: content-box; position: absolute; left: 0; top: 0; z-index: 1;'\n", | |
" );\n", | |
"\n", | |
" // Apply a ponyfill if ResizeObserver is not implemented by browser.\n", | |
" if (this.ResizeObserver === undefined) {\n", | |
" if (window.ResizeObserver !== undefined) {\n", | |
" this.ResizeObserver = window.ResizeObserver;\n", | |
" } else {\n", | |
" var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n", | |
" this.ResizeObserver = obs.ResizeObserver;\n", | |
" }\n", | |
" }\n", | |
"\n", | |
" this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n", | |
" var nentries = entries.length;\n", | |
" for (var i = 0; i < nentries; i++) {\n", | |
" var entry = entries[i];\n", | |
" var width, height;\n", | |
" if (entry.contentBoxSize) {\n", | |
" if (entry.contentBoxSize instanceof Array) {\n", | |
" // Chrome 84 implements new version of spec.\n", | |
" width = entry.contentBoxSize[0].inlineSize;\n", | |
" height = entry.contentBoxSize[0].blockSize;\n", | |
" } else {\n", | |
" // Firefox implements old version of spec.\n", | |
" width = entry.contentBoxSize.inlineSize;\n", | |
" height = entry.contentBoxSize.blockSize;\n", | |
" }\n", | |
" } else {\n", | |
" // Chrome <84 implements even older version of spec.\n", | |
" width = entry.contentRect.width;\n", | |
" height = entry.contentRect.height;\n", | |
" }\n", | |
"\n", | |
" // Keep the size of the canvas and rubber band canvas in sync with\n", | |
" // the canvas container.\n", | |
" if (entry.devicePixelContentBoxSize) {\n", | |
" // Chrome 84 implements new version of spec.\n", | |
" canvas.setAttribute(\n", | |
" 'width',\n", | |
" entry.devicePixelContentBoxSize[0].inlineSize\n", | |
" );\n", | |
" canvas.setAttribute(\n", | |
" 'height',\n", | |
" entry.devicePixelContentBoxSize[0].blockSize\n", | |
" );\n", | |
" } else {\n", | |
" canvas.setAttribute('width', width * fig.ratio);\n", | |
" canvas.setAttribute('height', height * fig.ratio);\n", | |
" }\n", | |
" canvas.setAttribute(\n", | |
" 'style',\n", | |
" 'width: ' + width + 'px; height: ' + height + 'px;'\n", | |
" );\n", | |
"\n", | |
" rubberband_canvas.setAttribute('width', width);\n", | |
" rubberband_canvas.setAttribute('height', height);\n", | |
"\n", | |
" // And update the size in Python. We ignore the initial 0/0 size\n", | |
" // that occurs as the element is placed into the DOM, which should\n", | |
" // otherwise not happen due to the minimum size styling.\n", | |
" if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n", | |
" fig.request_resize(width, height);\n", | |
" }\n", | |
" }\n", | |
" });\n", | |
" this.resizeObserverInstance.observe(canvas_div);\n", | |
"\n", | |
" function on_mouse_event_closure(name) {\n", | |
" return function (event) {\n", | |
" return fig.mouse_event(event, name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mousedown',\n", | |
" on_mouse_event_closure('button_press')\n", | |
" );\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseup',\n", | |
" on_mouse_event_closure('button_release')\n", | |
" );\n", | |
" // Throttle sequential mouse events to 1 every 20ms.\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mousemove',\n", | |
" on_mouse_event_closure('motion_notify')\n", | |
" );\n", | |
"\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseenter',\n", | |
" on_mouse_event_closure('figure_enter')\n", | |
" );\n", | |
" rubberband_canvas.addEventListener(\n", | |
" 'mouseleave',\n", | |
" on_mouse_event_closure('figure_leave')\n", | |
" );\n", | |
"\n", | |
" canvas_div.addEventListener('wheel', function (event) {\n", | |
" if (event.deltaY < 0) {\n", | |
" event.step = 1;\n", | |
" } else {\n", | |
" event.step = -1;\n", | |
" }\n", | |
" on_mouse_event_closure('scroll')(event);\n", | |
" });\n", | |
"\n", | |
" canvas_div.appendChild(canvas);\n", | |
" canvas_div.appendChild(rubberband_canvas);\n", | |
"\n", | |
" this.rubberband_context = rubberband_canvas.getContext('2d');\n", | |
" this.rubberband_context.strokeStyle = '#000000';\n", | |
"\n", | |
" this._resize_canvas = function (width, height, forward) {\n", | |
" if (forward) {\n", | |
" canvas_div.style.width = width + 'px';\n", | |
" canvas_div.style.height = height + 'px';\n", | |
" }\n", | |
" };\n", | |
"\n", | |
" // Disable right mouse context menu.\n", | |
" this.rubberband_canvas.addEventListener('contextmenu', function (_e) {\n", | |
" event.preventDefault();\n", | |
" return false;\n", | |
" });\n", | |
"\n", | |
" function set_focus() {\n", | |
" canvas.focus();\n", | |
" canvas_div.focus();\n", | |
" }\n", | |
"\n", | |
" window.setTimeout(set_focus, 100);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_toolbar = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var toolbar = document.createElement('div');\n", | |
" toolbar.classList = 'mpl-toolbar';\n", | |
" this.root.appendChild(toolbar);\n", | |
"\n", | |
" function on_click_closure(name) {\n", | |
" return function (_event) {\n", | |
" return fig.toolbar_button_onclick(name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" function on_mouseover_closure(tooltip) {\n", | |
" return function (event) {\n", | |
" if (!event.currentTarget.disabled) {\n", | |
" return fig.toolbar_button_onmouseover(tooltip);\n", | |
" }\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" fig.buttons = {};\n", | |
" var buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'mpl-button-group';\n", | |
" for (var toolbar_ind in mpl.toolbar_items) {\n", | |
" var name = mpl.toolbar_items[toolbar_ind][0];\n", | |
" var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", | |
" var image = mpl.toolbar_items[toolbar_ind][2];\n", | |
" var method_name = mpl.toolbar_items[toolbar_ind][3];\n", | |
"\n", | |
" if (!name) {\n", | |
" /* Instead of a spacer, we start a new button group. */\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
" buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'mpl-button-group';\n", | |
" continue;\n", | |
" }\n", | |
"\n", | |
" var button = (fig.buttons[name] = document.createElement('button'));\n", | |
" button.classList = 'mpl-widget';\n", | |
" button.setAttribute('role', 'button');\n", | |
" button.setAttribute('aria-disabled', 'false');\n", | |
" button.addEventListener('click', on_click_closure(method_name));\n", | |
" button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", | |
"\n", | |
" var icon_img = document.createElement('img');\n", | |
" icon_img.src = '_images/' + image + '.png';\n", | |
" icon_img.srcset = '_images/' + image + '_large.png 2x';\n", | |
" icon_img.alt = tooltip;\n", | |
" button.appendChild(icon_img);\n", | |
"\n", | |
" buttonGroup.appendChild(button);\n", | |
" }\n", | |
"\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
"\n", | |
" var fmt_picker = document.createElement('select');\n", | |
" fmt_picker.classList = 'mpl-widget';\n", | |
" toolbar.appendChild(fmt_picker);\n", | |
" this.format_dropdown = fmt_picker;\n", | |
"\n", | |
" for (var ind in mpl.extensions) {\n", | |
" var fmt = mpl.extensions[ind];\n", | |
" var option = document.createElement('option');\n", | |
" option.selected = fmt === mpl.default_extension;\n", | |
" option.innerHTML = fmt;\n", | |
" fmt_picker.appendChild(option);\n", | |
" }\n", | |
"\n", | |
" var status_bar = document.createElement('span');\n", | |
" status_bar.classList = 'mpl-message';\n", | |
" toolbar.appendChild(status_bar);\n", | |
" this.message = status_bar;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n", | |
" // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n", | |
" // which will in turn request a refresh of the image.\n", | |
" this.send_message('resize', { width: x_pixels, height: y_pixels });\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.send_message = function (type, properties) {\n", | |
" properties['type'] = type;\n", | |
" properties['figure_id'] = this.id;\n", | |
" this.ws.send(JSON.stringify(properties));\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.send_draw_message = function () {\n", | |
" if (!this.waiting) {\n", | |
" this.waiting = true;\n", | |
" this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_save = function (fig, _msg) {\n", | |
" var format_dropdown = fig.format_dropdown;\n", | |
" var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n", | |
" fig.ondownload(fig, format);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_resize = function (fig, msg) {\n", | |
" var size = msg['size'];\n", | |
" if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n", | |
" fig._resize_canvas(size[0], size[1], msg['forward']);\n", | |
" fig.send_message('refresh', {});\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_rubberband = function (fig, msg) {\n", | |
" var x0 = msg['x0'] / fig.ratio;\n", | |
" var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n", | |
" var x1 = msg['x1'] / fig.ratio;\n", | |
" var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n", | |
" x0 = Math.floor(x0) + 0.5;\n", | |
" y0 = Math.floor(y0) + 0.5;\n", | |
" x1 = Math.floor(x1) + 0.5;\n", | |
" y1 = Math.floor(y1) + 0.5;\n", | |
" var min_x = Math.min(x0, x1);\n", | |
" var min_y = Math.min(y0, y1);\n", | |
" var width = Math.abs(x1 - x0);\n", | |
" var height = Math.abs(y1 - y0);\n", | |
"\n", | |
" fig.rubberband_context.clearRect(\n", | |
" 0,\n", | |
" 0,\n", | |
" fig.canvas.width / fig.ratio,\n", | |
" fig.canvas.height / fig.ratio\n", | |
" );\n", | |
"\n", | |
" fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_figure_label = function (fig, msg) {\n", | |
" // Updates the figure title.\n", | |
" fig.header.textContent = msg['label'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_cursor = function (fig, msg) {\n", | |
" var cursor = msg['cursor'];\n", | |
" switch (cursor) {\n", | |
" case 0:\n", | |
" cursor = 'pointer';\n", | |
" break;\n", | |
" case 1:\n", | |
" cursor = 'default';\n", | |
" break;\n", | |
" case 2:\n", | |
" cursor = 'crosshair';\n", | |
" break;\n", | |
" case 3:\n", | |
" cursor = 'move';\n", | |
" break;\n", | |
" }\n", | |
" fig.rubberband_canvas.style.cursor = cursor;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_message = function (fig, msg) {\n", | |
" fig.message.textContent = msg['message'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_draw = function (fig, _msg) {\n", | |
" // Request the server to send over a new figure.\n", | |
" fig.send_draw_message();\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_image_mode = function (fig, msg) {\n", | |
" fig.image_mode = msg['mode'];\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n", | |
" for (var key in msg) {\n", | |
" if (!(key in fig.buttons)) {\n", | |
" continue;\n", | |
" }\n", | |
" fig.buttons[key].disabled = !msg[key];\n", | |
" fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n", | |
" if (msg['mode'] === 'PAN') {\n", | |
" fig.buttons['Pan'].classList.add('active');\n", | |
" fig.buttons['Zoom'].classList.remove('active');\n", | |
" } else if (msg['mode'] === 'ZOOM') {\n", | |
" fig.buttons['Pan'].classList.remove('active');\n", | |
" fig.buttons['Zoom'].classList.add('active');\n", | |
" } else {\n", | |
" fig.buttons['Pan'].classList.remove('active');\n", | |
" fig.buttons['Zoom'].classList.remove('active');\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.updated_canvas_event = function () {\n", | |
" // Called whenever the canvas gets updated.\n", | |
" this.send_message('ack', {});\n", | |
"};\n", | |
"\n", | |
"// A function to construct a web socket function for onmessage handling.\n", | |
"// Called in the figure constructor.\n", | |
"mpl.figure.prototype._make_on_message_function = function (fig) {\n", | |
" return function socket_on_message(evt) {\n", | |
" if (evt.data instanceof Blob) {\n", | |
" /* FIXME: We get \"Resource interpreted as Image but\n", | |
" * transferred with MIME type text/plain:\" errors on\n", | |
" * Chrome. But how to set the MIME type? It doesn't seem\n", | |
" * to be part of the websocket stream */\n", | |
" evt.data.type = 'image/png';\n", | |
"\n", | |
" /* Free the memory for the previous frames */\n", | |
" if (fig.imageObj.src) {\n", | |
" (window.URL || window.webkitURL).revokeObjectURL(\n", | |
" fig.imageObj.src\n", | |
" );\n", | |
" }\n", | |
"\n", | |
" fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n", | |
" evt.data\n", | |
" );\n", | |
" fig.updated_canvas_event();\n", | |
" fig.waiting = false;\n", | |
" return;\n", | |
" } else if (\n", | |
" typeof evt.data === 'string' &&\n", | |
" evt.data.slice(0, 21) === 'data:image/png;base64'\n", | |
" ) {\n", | |
" fig.imageObj.src = evt.data;\n", | |
" fig.updated_canvas_event();\n", | |
" fig.waiting = false;\n", | |
" return;\n", | |
" }\n", | |
"\n", | |
" var msg = JSON.parse(evt.data);\n", | |
" var msg_type = msg['type'];\n", | |
"\n", | |
" // Call the \"handle_{type}\" callback, which takes\n", | |
" // the figure and JSON message as its only arguments.\n", | |
" try {\n", | |
" var callback = fig['handle_' + msg_type];\n", | |
" } catch (e) {\n", | |
" console.log(\n", | |
" \"No handler for the '\" + msg_type + \"' message type: \",\n", | |
" msg\n", | |
" );\n", | |
" return;\n", | |
" }\n", | |
"\n", | |
" if (callback) {\n", | |
" try {\n", | |
" // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n", | |
" callback(fig, msg);\n", | |
" } catch (e) {\n", | |
" console.log(\n", | |
" \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n", | |
" e,\n", | |
" e.stack,\n", | |
" msg\n", | |
" );\n", | |
" }\n", | |
" }\n", | |
" };\n", | |
"};\n", | |
"\n", | |
"// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n", | |
"mpl.findpos = function (e) {\n", | |
" //this section is from http://www.quirksmode.org/js/events_properties.html\n", | |
" var targ;\n", | |
" if (!e) {\n", | |
" e = window.event;\n", | |
" }\n", | |
" if (e.target) {\n", | |
" targ = e.target;\n", | |
" } else if (e.srcElement) {\n", | |
" targ = e.srcElement;\n", | |
" }\n", | |
" if (targ.nodeType === 3) {\n", | |
" // defeat Safari bug\n", | |
" targ = targ.parentNode;\n", | |
" }\n", | |
"\n", | |
" // pageX,Y are the mouse positions relative to the document\n", | |
" var boundingRect = targ.getBoundingClientRect();\n", | |
" var x = e.pageX - (boundingRect.left + document.body.scrollLeft);\n", | |
" var y = e.pageY - (boundingRect.top + document.body.scrollTop);\n", | |
"\n", | |
" return { x: x, y: y };\n", | |
"};\n", | |
"\n", | |
"/*\n", | |
" * return a copy of an object with only non-object keys\n", | |
" * we need this to avoid circular references\n", | |
" * http://stackoverflow.com/a/24161582/3208463\n", | |
" */\n", | |
"function simpleKeys(original) {\n", | |
" return Object.keys(original).reduce(function (obj, key) {\n", | |
" if (typeof original[key] !== 'object') {\n", | |
" obj[key] = original[key];\n", | |
" }\n", | |
" return obj;\n", | |
" }, {});\n", | |
"}\n", | |
"\n", | |
"mpl.figure.prototype.mouse_event = function (event, name) {\n", | |
" var canvas_pos = mpl.findpos(event);\n", | |
"\n", | |
" if (name === 'button_press') {\n", | |
" this.canvas.focus();\n", | |
" this.canvas_div.focus();\n", | |
" }\n", | |
"\n", | |
" var x = canvas_pos.x * this.ratio;\n", | |
" var y = canvas_pos.y * this.ratio;\n", | |
"\n", | |
" this.send_message(name, {\n", | |
" x: x,\n", | |
" y: y,\n", | |
" button: event.button,\n", | |
" step: event.step,\n", | |
" guiEvent: simpleKeys(event),\n", | |
" });\n", | |
"\n", | |
" /* This prevents the web browser from automatically changing to\n", | |
" * the text insertion cursor when the button is pressed. We want\n", | |
" * to control all of the cursor setting manually through the\n", | |
" * 'cursor' event from matplotlib */\n", | |
" event.preventDefault();\n", | |
" return false;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._key_event_extra = function (_event, _name) {\n", | |
" // Handle any extra behaviour associated with a key event\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.key_event = function (event, name) {\n", | |
" // Prevent repeat events\n", | |
" if (name === 'key_press') {\n", | |
" if (event.which === this._key) {\n", | |
" return;\n", | |
" } else {\n", | |
" this._key = event.which;\n", | |
" }\n", | |
" }\n", | |
" if (name === 'key_release') {\n", | |
" this._key = null;\n", | |
" }\n", | |
"\n", | |
" var value = '';\n", | |
" if (event.ctrlKey && event.which !== 17) {\n", | |
" value += 'ctrl+';\n", | |
" }\n", | |
" if (event.altKey && event.which !== 18) {\n", | |
" value += 'alt+';\n", | |
" }\n", | |
" if (event.shiftKey && event.which !== 16) {\n", | |
" value += 'shift+';\n", | |
" }\n", | |
"\n", | |
" value += 'k';\n", | |
" value += event.which.toString();\n", | |
"\n", | |
" this._key_event_extra(event, name);\n", | |
"\n", | |
" this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n", | |
" return false;\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.toolbar_button_onclick = function (name) {\n", | |
" if (name === 'download') {\n", | |
" this.handle_save(this, null);\n", | |
" } else {\n", | |
" this.send_message('toolbar_button', { name: name });\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n", | |
" this.message.textContent = tooltip;\n", | |
"};\n", | |
"\n", | |
"///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n", | |
"// prettier-ignore\n", | |
"var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\n", | |
"mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", | |
"\n", | |
"mpl.extensions = [\"eps\", \"jpeg\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n", | |
"\n", | |
"mpl.default_extension = \"png\";/* global mpl */\n", | |
"\n", | |
"var comm_websocket_adapter = function (comm) {\n", | |
" // Create a \"websocket\"-like object which calls the given IPython comm\n", | |
" // object with the appropriate methods. Currently this is a non binary\n", | |
" // socket, so there is still some room for performance tuning.\n", | |
" var ws = {};\n", | |
"\n", | |
" ws.close = function () {\n", | |
" comm.close();\n", | |
" };\n", | |
" ws.send = function (m) {\n", | |
" //console.log('sending', m);\n", | |
" comm.send(m);\n", | |
" };\n", | |
" // Register the callback with on_msg.\n", | |
" comm.on_msg(function (msg) {\n", | |
" //console.log('receiving', msg['content']['data'], msg);\n", | |
" // Pass the mpl event to the overridden (by mpl) onmessage function.\n", | |
" ws.onmessage(msg['content']['data']);\n", | |
" });\n", | |
" return ws;\n", | |
"};\n", | |
"\n", | |
"mpl.mpl_figure_comm = function (comm, msg) {\n", | |
" // This is the function which gets called when the mpl process\n", | |
" // starts-up an IPython Comm through the \"matplotlib\" channel.\n", | |
"\n", | |
" var id = msg.content.data.id;\n", | |
" // Get hold of the div created by the display call when the Comm\n", | |
" // socket was opened in Python.\n", | |
" var element = document.getElementById(id);\n", | |
" var ws_proxy = comm_websocket_adapter(comm);\n", | |
"\n", | |
" function ondownload(figure, _format) {\n", | |
" window.open(figure.canvas.toDataURL());\n", | |
" }\n", | |
"\n", | |
" var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n", | |
"\n", | |
" // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n", | |
" // web socket which is closed, not our websocket->open comm proxy.\n", | |
" ws_proxy.onopen();\n", | |
"\n", | |
" fig.parent_element = element;\n", | |
" fig.cell_info = mpl.find_output_cell(\"<div id='\" + id + \"'></div>\");\n", | |
" if (!fig.cell_info) {\n", | |
" console.error('Failed to find cell for figure', id, fig);\n", | |
" return;\n", | |
" }\n", | |
" fig.cell_info[0].output_area.element.on(\n", | |
" 'cleared',\n", | |
" { fig: fig },\n", | |
" fig._remove_fig_handler\n", | |
" );\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_close = function (fig, msg) {\n", | |
" var width = fig.canvas.width / fig.ratio;\n", | |
" fig.cell_info[0].output_area.element.off(\n", | |
" 'cleared',\n", | |
" fig._remove_fig_handler\n", | |
" );\n", | |
" fig.resizeObserverInstance.unobserve(fig.canvas_div);\n", | |
"\n", | |
" // Update the output cell to use the data from the current canvas.\n", | |
" fig.push_to_output();\n", | |
" var dataURL = fig.canvas.toDataURL();\n", | |
" // Re-enable the keyboard manager in IPython - without this line, in FF,\n", | |
" // the notebook keyboard shortcuts fail.\n", | |
" IPython.keyboard_manager.enable();\n", | |
" fig.parent_element.innerHTML =\n", | |
" '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n", | |
" fig.close_ws(fig, msg);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.close_ws = function (fig, msg) {\n", | |
" fig.send_message('closing', msg);\n", | |
" // fig.ws.close()\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.push_to_output = function (_remove_interactive) {\n", | |
" // Turn the data on the canvas into data in the output cell.\n", | |
" var width = this.canvas.width / this.ratio;\n", | |
" var dataURL = this.canvas.toDataURL();\n", | |
" this.cell_info[1]['text/html'] =\n", | |
" '<img src=\"' + dataURL + '\" width=\"' + width + '\">';\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.updated_canvas_event = function () {\n", | |
" // Tell IPython that the notebook contents must change.\n", | |
" IPython.notebook.set_dirty(true);\n", | |
" this.send_message('ack', {});\n", | |
" var fig = this;\n", | |
" // Wait a second, then push the new image to the DOM so\n", | |
" // that it is saved nicely (might be nice to debounce this).\n", | |
" setTimeout(function () {\n", | |
" fig.push_to_output();\n", | |
" }, 1000);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._init_toolbar = function () {\n", | |
" var fig = this;\n", | |
"\n", | |
" var toolbar = document.createElement('div');\n", | |
" toolbar.classList = 'btn-toolbar';\n", | |
" this.root.appendChild(toolbar);\n", | |
"\n", | |
" function on_click_closure(name) {\n", | |
" return function (_event) {\n", | |
" return fig.toolbar_button_onclick(name);\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" function on_mouseover_closure(tooltip) {\n", | |
" return function (event) {\n", | |
" if (!event.currentTarget.disabled) {\n", | |
" return fig.toolbar_button_onmouseover(tooltip);\n", | |
" }\n", | |
" };\n", | |
" }\n", | |
"\n", | |
" fig.buttons = {};\n", | |
" var buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'btn-group';\n", | |
" var button;\n", | |
" for (var toolbar_ind in mpl.toolbar_items) {\n", | |
" var name = mpl.toolbar_items[toolbar_ind][0];\n", | |
" var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", | |
" var image = mpl.toolbar_items[toolbar_ind][2];\n", | |
" var method_name = mpl.toolbar_items[toolbar_ind][3];\n", | |
"\n", | |
" if (!name) {\n", | |
" /* Instead of a spacer, we start a new button group. */\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
" buttonGroup = document.createElement('div');\n", | |
" buttonGroup.classList = 'btn-group';\n", | |
" continue;\n", | |
" }\n", | |
"\n", | |
" button = fig.buttons[name] = document.createElement('button');\n", | |
" button.classList = 'btn btn-default';\n", | |
" button.href = '#';\n", | |
" button.title = name;\n", | |
" button.innerHTML = '<i class=\"fa ' + image + ' fa-lg\"></i>';\n", | |
" button.addEventListener('click', on_click_closure(method_name));\n", | |
" button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", | |
" buttonGroup.appendChild(button);\n", | |
" }\n", | |
"\n", | |
" if (buttonGroup.hasChildNodes()) {\n", | |
" toolbar.appendChild(buttonGroup);\n", | |
" }\n", | |
"\n", | |
" // Add the status bar.\n", | |
" var status_bar = document.createElement('span');\n", | |
" status_bar.classList = 'mpl-message pull-right';\n", | |
" toolbar.appendChild(status_bar);\n", | |
" this.message = status_bar;\n", | |
"\n", | |
" // Add the close button to the window.\n", | |
" var buttongrp = document.createElement('div');\n", | |
" buttongrp.classList = 'btn-group inline pull-right';\n", | |
" button = document.createElement('button');\n", | |
" button.classList = 'btn btn-mini btn-primary';\n", | |
" button.href = '#';\n", | |
" button.title = 'Stop Interaction';\n", | |
" button.innerHTML = '<i class=\"fa fa-power-off icon-remove icon-large\"></i>';\n", | |
" button.addEventListener('click', function (_evt) {\n", | |
" fig.handle_close(fig, {});\n", | |
" });\n", | |
" button.addEventListener(\n", | |
" 'mouseover',\n", | |
" on_mouseover_closure('Stop Interaction')\n", | |
" );\n", | |
" buttongrp.appendChild(button);\n", | |
" var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n", | |
" titlebar.insertBefore(buttongrp, titlebar.firstChild);\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._remove_fig_handler = function (event) {\n", | |
" var fig = event.data.fig;\n", | |
" if (event.target !== this) {\n", | |
" // Ignore bubbled events from children.\n", | |
" return;\n", | |
" }\n", | |
" fig.close_ws(fig, {});\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._root_extra_style = function (el) {\n", | |
" el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._canvas_extra_style = function (el) {\n", | |
" // this is important to make the div 'focusable\n", | |
" el.setAttribute('tabindex', 0);\n", | |
" // reach out to IPython and tell the keyboard manager to turn it's self\n", | |
" // off when our div gets focus\n", | |
"\n", | |
" // location in version 3\n", | |
" if (IPython.notebook.keyboard_manager) {\n", | |
" IPython.notebook.keyboard_manager.register_events(el);\n", | |
" } else {\n", | |
" // location in version 2\n", | |
" IPython.keyboard_manager.register_events(el);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype._key_event_extra = function (event, _name) {\n", | |
" var manager = IPython.notebook.keyboard_manager;\n", | |
" if (!manager) {\n", | |
" manager = IPython.keyboard_manager;\n", | |
" }\n", | |
"\n", | |
" // Check for shift+enter\n", | |
" if (event.shiftKey && event.which === 13) {\n", | |
" this.canvas_div.blur();\n", | |
" // select the cell after this one\n", | |
" var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", | |
" IPython.notebook.select(index + 1);\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"mpl.figure.prototype.handle_save = function (fig, _msg) {\n", | |
" fig.ondownload(fig, null);\n", | |
"};\n", | |
"\n", | |
"mpl.find_output_cell = function (html_output) {\n", | |
" // Return the cell and output element which can be found *uniquely* in the notebook.\n", | |
" // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", | |
" // IPython event is triggered only after the cells have been serialised, which for\n", | |
" // our purposes (turning an active figure into a static one), is too late.\n", | |
" var cells = IPython.notebook.get_cells();\n", | |
" var ncells = cells.length;\n", | |
" for (var i = 0; i < ncells; i++) {\n", | |
" var cell = cells[i];\n", | |
" if (cell.cell_type === 'code') {\n", | |
" for (var j = 0; j < cell.output_area.outputs.length; j++) {\n", | |
" var data = cell.output_area.outputs[j];\n", | |
" if (data.data) {\n", | |
" // IPython >= 3 moved mimebundle to data attribute of output\n", | |
" data = data.data;\n", | |
" }\n", | |
" if (data['text/html'] === html_output) {\n", | |
" return [cell, data, j];\n", | |
" }\n", | |
" }\n", | |
" }\n", | |
" }\n", | |
"};\n", | |
"\n", | |
"// Register the function which deals with the matplotlib target/channel.\n", | |
"// The kernel may be null if the page has been refreshed.\n", | |
"if (IPython.notebook.kernel !== null) {\n", | |
" IPython.notebook.kernel.comm_manager.register_target(\n", | |
" 'matplotlib',\n", | |
" mpl.mpl_figure_comm\n", | |
" );\n", | |
"}\n" | |
], | |
"text/plain": [ | |
"<IPython.core.display.Javascript object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
}, | |
{ | |
"data": { | |
"text/html": [ | |
"<img src=\"data:image/png;base64,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2DA8+uijOHLkiOxyHFZ2drbN34UJCQlo1aoV+vXrx/spy+H1118vcQvCyJEjS5wVdCYMgPfAiBEj0LBhQ2zcuBGJiYkYOHAgHnroIWRkZMguzWHxEvDduXHZt1OnTjh9+jTOnDmjbXR7N5aBCQ0NhcViwciRI/Hggw/i5MmTsktzGO+++y5cXFywbds2m767du2a7NIcHi8Bl9+ff/6J//f//h+CgoJw9OhRLFmyBLVq1cKPP/4ouzRpGADvgfz8fIwePRoPPfQQateuDW9vbz4FrBMD4N0JDw+/7T2C9M9mz56NJk2aoEaNGmjRogWXLymn2/VdeHi47NIcHgPg3Vm7di2EEKhZsyYUReFTwLILICIiIqLKxQBIRERE5GQYAImIiIicDAMgERERkZNhACQiIiJyMgyARERERE6GAZCIiIjIyTAAEhERETkZBkAiIiIiJ8MASERERORkGACJiIiInAwDIBEREZGTYQAkIiIicjIMgEREREROhgGQiIiIyMkwABIRERE5GQZAIiIiIifDAEhERETkZBgAiYiIiJwMAyARERGRk2EAJCIiInIyDIBEREREToYBkIiIiMjJMAASERERORkGQCIiIiInwwBIRERE5GQYAImIiIicDAMgERERkZNhACQiIiJyMgyARERERE6GAZCIiIjIyTAAEhERETmZ/w8G9bvK75yhAgAAAABJRU5ErkJggg==\" width=\"640\">" | |
], | |
"text/plain": [ | |
"<IPython.core.display.HTML object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"fig, ax = subplots()\n", | |
"ref = numpy.histogram(numpy.random.normal(z, s).ravel(), 1000)\n", | |
"cpp = numpy.histogram(cython_normal64_cpp(z, s).ravel(), 1000)\n", | |
"obt = numpy.histogram(cython_normal_bm_mtc(z, s).ravel(), 1000)\n", | |
"obt2 = numpy.histogram(cython_normal_m_mtc(z, s).ravel(), 1000)\n", | |
"ax.plot((ref[1][1:]+ref[1][:-1])/2, ref[0], label=\"numpy\")\n", | |
"ax.plot((obt[1][1:]+obt[1][:-1])/2, obt[0], label=\"cython Box-Muller\")\n", | |
"ax.plot((obt2[1][1:]+obt2[1][:-1])/2, obt2[0], label=\"cython Marsaglia\")\n", | |
"ax.plot((cpp[1][1:]+cpp[1][:-1])/2, cpp[0], label=\"C++\")\n", | |
"ax.set_title(\"Equivalence of Normal distributions\")\n", | |
"ax.legend()\n", | |
"pass" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 19, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"Total execution time: 113.619\n" | |
] | |
} | |
], | |
"source": [ | |
"print(f\"Total execution time: {time.perf_counter()-start_time:.3f}\")" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"There was still a lot of time wasted in converting the uniform distribution to the normal one. This was related to the numerous transcendental functions like `sin`, `cos`, `sqrt` and `log` called in the conversion. The Marsaglia algorithm allows to generate 2 values for 2 polls, and does not use `sin` nor `cos` functions, which actually makes it twice faster than the Box-Muller algorithm. And since we generate billions of random values, this matters!\n", | |
"\n", | |
"## Outlook\n", | |
"I created a pool of thread in Python (from multiprocessing) and called the processing of each image in a separated thread. Since the complete generation of the image is in a `nogil` section, this runs efficiently on all cores of the computer. I got a 10x speed-up with multiprocessing versus the single threaded version on a 8-core computer thanks to hyperthreading. The perfromances are not that fancy but the bottleneck is now somewhere else.\n", | |
"\n", | |
"\n", | |
"## Conclusions\n", | |
"\n", | |
"There are several conclusion one can draw from this:\n", | |
"* Python is not slow !\n", | |
"* Numpy is even fast !\n", | |
"* Re-writting performance critical in C is not a silver bullet\n", | |
"* Re-writting performance critical in C++ does not help either in some cases\n", | |
"* Algorithms matters more than the programming language used !\n", | |
"* Benchmark, profile, measure ... is more deterministic than listening to friends" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [] | |
} | |
], | |
"metadata": { | |
"kernelspec": { | |
"display_name": "Python 3", | |
"language": "python", | |
"name": "python3" | |
}, | |
"language_info": { | |
"codemirror_mode": { | |
"name": "ipython", | |
"version": 3 | |
}, | |
"file_extension": ".py", | |
"mimetype": "text/x-python", | |
"name": "python", | |
"nbconvert_exporter": "python", | |
"pygments_lexer": "ipython3", | |
"version": "3.9.2" | |
} | |
}, | |
"nbformat": 4, | |
"nbformat_minor": 2 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
MKL Random generators, that leverage the Vector Statistics Random Number Generators of modern CPUs, or whatever mathematical library is used to implement Numpy is fast. You are comparing Numpy's implementation, dedicated to array data structures (contiguous in memory) that are perfect candidate for vector (SIMD) implementation, to standard libraries implementation that make no assumption whatsoever. It's not about languages, it's about using the right tools for the use case. Here it looks like Numpy is the right tool -or give you an easy interface to it.
Yes, it's trolling Friday.