From df2d79189c3295f01dcb36d4e738fddccab8ece8 Mon Sep 17 00:00:00 2001 From: jaseg Date: Wed, 10 Mar 2021 22:36:58 +0100 Subject: [PATCH] Add initial accelerometer data analysis --- .../Accelerometer Data Analysis.ipynb | 3293 +++++++++++++++++ prototype/sensor-analysis/test_run.sqlite3 | Bin 0 -> 114688 bytes 2 files changed, 3293 insertions(+) create mode 100644 prototype/sensor-analysis/Accelerometer Data Analysis.ipynb create mode 100755 prototype/sensor-analysis/test_run.sqlite3 diff --git a/prototype/sensor-analysis/Accelerometer Data Analysis.ipynb b/prototype/sensor-analysis/Accelerometer Data Analysis.ipynb new file mode 100644 index 0000000..f25f4cb --- /dev/null +++ b/prototype/sensor-analysis/Accelerometer Data Analysis.ipynb @@ -0,0 +1,3293 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 182, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "import sqlite3\n", + "import math\n", + "import warnings\n", + "import struct\n", + "import itertools\n", + "import numpy as np\n", + "import scipy.fftpack\n", + "import scipy.signal\n", + "from matplotlib import pyplot as plt\n", + "import scipy.optimize\n", + "%matplotlib notebook" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "db = sqlite3.connect('test_run.sqlite3')" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Last run was ID #2 with 1759 packets total, 504 distinct over 409.4534661769867s\n" + ] + } + ], + "source": [ + "last_run, = db.execute('SELECT MAX(run_id) FROM packets').fetchone()\n", + "num_packets, = db.execute('SELECT COUNT(*) FROM packets WHERE run_id=?', (last_run,)).fetchone()\n", + "num_packets_distinct, = db.execute('SELECT COUNT(*) FROM (SELECT DISTINCT data FROM packets WHERE run_id=?)', (last_run,)).fetchone()\n", + "timespan_start, timespan_end = db.execute('SELECT MIN(timestamp_us)/1e6, MAX(timestamp_us)/1e6 FROM packets WHERE run_id=?', (last_run,)).fetchone()\n", + "timespan = timespan_end - timespan_start\n", + "print(f'Last run was ID #{last_run} with {num_packets} packets total, {num_packets_distinct} distinct over {timespan}s')" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "timestamps = db.execute('SELECT timestamp_us/1e6 FROM packets WHERE run_id=? ORDER BY timestamp_us', (last_run,)).fetchall()\n", + "timestamps = [ ts - timespan_start for ts, in timestamps ]\n", + "deltas = [ b-a for a, b in zip(timestamps[:-1], timestamps[1:]) ]" + ] + }, + { + "cell_type": "code", + "execution_count": 201, + "metadata": { + "scrolled": false + }, + "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(\"
\");\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", + " '';\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", + " '';\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 = '';\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 = '';\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": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 201, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fig, ax = plt.subplots()\n", + "ax.grid()\n", + "ax.plot(sorted(deltas)[:-2])" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Average speed of rotation: 7.94 Hz / 476 rpm\n" + ] + } + ], + "source": [ + "def fun(x, args):\n", + " deltas, = args # poor api\n", + " return np.sqrt(np.mean([ ((val + 0.5*x[0]) % x[0] - 0.5*x[0])**2 for val in deltas ]))\n", + "res = scipy.optimize.minimize(fun, 0.1, args=[sorted(deltas)[:-2]])\n", + "interval = np.abs(res.x[0])\n", + "print(f'Average speed of rotation: {1/interval:.2f} Hz / {60 / interval:.0f} rpm')" + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Packet length: 40\n" + ] + } + ], + "source": [ + "packet_lengths = db.execute('SELECT LENGTH(data) FROM packets WHERE run_id=? GROUP BY LENGTH(data)', (last_run,)).fetchall()\n", + "assert len(packet_lengths) == 1\n", + "packet_len, = packet_lengths[0]\n", + "print('Packet length:', packet_len)" + ] + }, + { + "cell_type": "code", + "execution_count": 97, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Very approximate lower bound on baudrate: 92476.55870407648 bd\n" + ] + } + ], + "source": [ + "approx_baudrate = 1.0 / (np.mean([ x for x in deltas if x < interval*0.1 ]) / (packet_len*10))\n", + "print(f'Very approximate lower bound on baudrate: {approx_baudrate} bd')" + ] + }, + { + "cell_type": "code", + "execution_count": 152, + "metadata": {}, + "outputs": [], + "source": [ + "def decode_packet(packet):\n", + " seq, *data, _crc = struct.unpack(':7: UserWarning: BUG: Duplicate sequence number 241 for 2 payloads!\n", + " warnings.warn(f'BUG: Duplicate sequence number {seq} for {len(set(le_packets))} payloads!')\n", + ":7: UserWarning: BUG: Duplicate sequence number 242 for 2 payloads!\n", + " warnings.warn(f'BUG: Duplicate sequence number {seq} for {len(set(le_packets))} payloads!')\n", + ":7: UserWarning: BUG: Duplicate sequence number 243 for 2 payloads!\n", + " warnings.warn(f'BUG: Duplicate sequence number {seq} for {len(set(le_packets))} payloads!')\n", + ":7: UserWarning: BUG: Duplicate sequence number 244 for 2 payloads!\n", + " warnings.warn(f'BUG: Duplicate sequence number {seq} for {len(set(le_packets))} payloads!')\n" + ] + } + ], + "source": [ + "# group packets by sequence number\n", + "by_seq = { k: list(g) for k, g in itertools.groupby(packets, key=lambda x: x[0]) }\n", + "for seq, le_packets in by_seq.items():\n", + " # make sure we only ever have one version of a packet with a particular sequence number (no CRC collisions)\n", + " if len(set(le_packets)) > 1:\n", + " # In test_run.sqlite3 run 2 this happens to coincide with the time I intentionally bumped the rotor... ?\n", + " warnings.warn(f'BUG: Duplicate sequence number {seq} for {len(set(le_packets))} payloads!')\n", + " print('BUG: Duplicate sequence number')\n", + " print('Sequence number:', seq)\n", + " for seq, data in set(le_packets):\n", + " print(' ', data)" + ] + }, + { + "cell_type": "code", + "execution_count": 155, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sequence number range: 1 ... 510\n" + ] + } + ], + "source": [ + "seqs = list(by_seq)\n", + "print(f'Sequence number range: {min(seqs)} ... {max(seqs)}')" + ] + }, + { + "cell_type": "code", + "execution_count": 166, + "metadata": {}, + "outputs": [], + "source": [ + "# FIXME this is only approximate, doesn't consider sequence numbers properly!!!\n", + "reassembled_values = np.array([ val for (_seq, values), *_rest in by_seq.values() for val in values[:8] ])" + ] + }, + { + "cell_type": "code", + "execution_count": 206, + "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(\"
\");\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", + " '';\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", + " '';\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 = '';\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 = '';\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": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Text(0, 0.5, '$a\\\\; [ms^{-1}]$')" + ] + }, + "execution_count": 206, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mems_lsb_per_g = 68 # LSBs per 1g for our accelerometer\n", + "\n", + "fig, ax = plt.subplots()\n", + "ax.plot(reassembled_values / mems_lsb_per_g )\n", + "ax.grid()\n", + "\n", + "g = 9.8066\n", + "g_to_ms = lambda x: x * g\n", + "ms_to_g = lambda x: x / g\n", + "\n", + "ax.set_ylabel(r'$a\\; [g]$')\n", + "secax_y = ax.secondary_yaxis(\n", + " 'right', functions=(g_to_ms, ms_to_g))\n", + "secax_y.set_ylabel(r'$a\\; [ms^{-1}]$')" + ] + }, + { + "cell_type": "code", + "execution_count": 210, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Largest peak at 3.1162324649298596 Hz / 186.97394789579158 rpm\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(\"
\");\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", + " '';\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", + " '';\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 = '';\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 = '';\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": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Text(0.5, 0, 'Frequency [Hz]')" + ] + }, + "execution_count": 210, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sampling_rate = 10 # sps, set in firmware\n", + "\n", + "N = 1000\n", + "T = 1/sampling_rate\n", + "x = np.linspace(0.0, N*T, N)\n", + "y = reassembled_values[500:2000] / mems_lsb_per_g # cut out beginning and that time we tapped the thing\n", + "yf = scipy.fftpack.fft(y)\n", + "xf = np.linspace(0.0, 1/(2*T), N//2)\n", + "mag = 2/N * np.abs(yf[:N//2])\n", + "\n", + "peaks, _ = scipy.signal.find_peaks(mag, height=.1, distance=1/T)\n", + "assert peaks\n", + "\n", + "peak_data = sorted([ (-mag[idx], xf[idx]) for idx in peaks ])\n", + "largest_peak_f = peak_data[0][1]\n", + "print(f'Largest peak at {largest_peak_f:.2} Hz / {largest_peak_f * 60:.0} rpm')\n", + "\n", + "fig, ax = plt.subplots()\n", + "ax.grid()\n", + "ax.axvline(xf[peaks], color='orange')\n", + "ax.plot(xf, mag)\n", + "ax.set_ylabel('Magnitude [g]')\n", + "ax.set_xlabel('Frequency [Hz]')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "ax.magnitude_spectrum(reassembled_values[500:2000]/mems_lsb_per_g)" + ] + }, + { + "cell_type": "code", + "execution_count": 217, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Centrifugal acceleration at 3.12 Hz: 21.09 m/s^2 / 2.15 g\n" + ] + } + ], + "source": [ + "r_mems = 55e-3 # radius of our sensor from the axis of rotation in m\n", + "f = largest_peak_f\n", + "omega = 2*np.pi*f # angular velocity\n", + "centrifugal_acceleration = omega**2 * r_mems # m/s^2\n", + "print(f'Centrifugal acceleration at {largest_peak_f:.2f} Hz: {centrifugal_acceleration:.2f} m/s^2 / {centrifugal_acceleration/g:.2f} g')" + ] + } + ], + "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": 4 +} diff --git a/prototype/sensor-analysis/test_run.sqlite3 b/prototype/sensor-analysis/test_run.sqlite3 new file mode 100755 index 0000000000000000000000000000000000000000..d177eabcbeb25404f4bcf90f35e629a696b43475 GIT binary patch literal 114688 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 literal 0 HcmV?d00001