{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# COS 463 Wireless Networks - Spring 2019\n", "# Lab 1: Building a Spectrum Analyzer using HackRF\n", "\n", "In this lab we will be exploring the discrete Fourier transform which is one of the most important concepts in radio signal processing. We will also be working with real radio hardware to record radio signals and to understand some of the quirks of radio hardware. Finally, we'll implement a simple spectrograph which is a tool that's commonly used to visualize signals in both frequency and time.\n", "\n", "Beyond introducing these three concepts, the goal of this lab is to develop an understanding of the lowest \"layer\" of wireless communication - the \"physical\" layer. By the end of this lab, you should be able to answer:\n", "\n", "- What is a radio \"signal\"? How is a radio signal represented in the digital domain?\n", "- What is the wireless \"spectrum\"?\n", "- What does it mean for a signal to have high bandwidth?" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "%matplotlib notebook\n", "\n", "import math\n", "\n", "from IPython.display import HTML\n", "from matplotlib import animation, pyplot, rc\n", "import numpy\n", "import SoapySDR\n", "from SoapySDR import SOAPY_SDR_RX, SOAPY_SDR_CF32" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Part 0: The Discrete Fourier Transform" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Consider the equation for the DFT of a signal $x$ of length $N$\n", "$$\\Large X[k] = \\sum_{n=0}^{N-1} x[n] e^{-i 2 \\pi \\frac{k n}{N}}$$\n", "In the above equation, $X$ is also length $N$ and the value at index $k$ is the frequency component of $x$ at normalized frequency $2\\pi \\frac{k}{N}$ radians per sample. Each element of $X$ is the result of multiplying the signal $x$ by a tone (a signal with only one frequency component) at the appropriate frequency, then summing the result. It's remarkable that the DFT is invertible; to go from $X$ to $x$ simply apply the same transformation except with a positive exponential instead of the negative. This is the \"Inverse Discrete Fourier Transform\" (IDFT).\n", "\n", "### Problem 0.1 Implementing the DFT (10 pts)\n", "- _Implement the DFT on your own using the above equation (let $k$ range from 0 to $N - 1$). Compare against numpy's version on a random signal. Use the below cell for your code and testing._" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "def dft(signal):\n", " \"\"\"\n", " The input is a complex signal, and the output is a complex signal of the same length\n", " \"\"\"\n", " # Implement this function\n", " return signal\n", "\n", "# random complex sequence\n", "signal = numpy.random.random(10) + 1j * numpy.random.random(10)\n", "# Assert that your code matches numpy's version of the DFT\n", "#numpy.testing.assert_almost_equal(dft(signal), numpy.fft.fft(signal))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The DFT measures the frequency components of a time-domain signal at frequencies equally spaced between $0$ and $2 \\pi$ radians per sample (note the units, which may be unfamiliar). If a signal is sampled with bandwidth $B$ Hz, then the DFT will return frequencies between $0$ and $B$ Hz, or equivalently, between $-B/2$ and $B/2$ (positive frequencies above $B/2$ are aliased to negative frequencies. You can read more about this phenomenon [here](https://en.wikipedia.org/wiki/Aliasing)).\n", "\n", "So the signal bandwidth (time resolution) determines the spectrum frequency range. It so happens that the \"dual\" relationship is also true: The signal duration determines the spectrum frequency resolution.\n", "\n", "### Problem 0.2 Understanding Frequency (10 pts)\n", "Consider the signal given below which consists of one frequency. If you take the DFT of this signal, the frequency will be between two bins." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "application/javascript": [ "/* Put everything inside the global mpl namespace */\n", "window.mpl = {};\n", "\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('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", "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 = $('
');\n", " this._root_extra_style(this.root)\n", " this.root.attr('style', 'display: inline-block');\n", "\n", " $(parent_element).append(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 (mpl.ratio != 1) {\n", " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.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", " this.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 = $(\n", " '
');\n", " var titletext = $(\n", " '
');\n", " titlebar.append(titletext)\n", " this.root.append(titlebar);\n", " this.header = titletext[0];\n", "}\n", "\n", "\n", "\n", "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", "\n", "}\n", "\n", "\n", "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", "\n", "}\n", "\n", "mpl.figure.prototype._init_canvas = function() {\n", " var fig = this;\n", "\n", " var canvas_div = $('
');\n", "\n", " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", "\n", " function canvas_keyboard_event(event) {\n", " return fig.key_event(event, event['data']);\n", " }\n", "\n", " canvas_div.keydown('key_press', canvas_keyboard_event);\n", " canvas_div.keyup('key_release', canvas_keyboard_event);\n", " this.canvas_div = canvas_div\n", " this._canvas_extra_style(canvas_div)\n", " this.root.append(canvas_div);\n", "\n", " var canvas = $('');\n", " canvas.addClass('mpl-canvas');\n", " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", "\n", " this.canvas = canvas[0];\n", " this.context = canvas[0].getContext(\"2d\");\n", "\n", " var backingStore = this.context.backingStorePixelRatio ||\n", "\tthis.context.webkitBackingStorePixelRatio ||\n", "\tthis.context.mozBackingStorePixelRatio ||\n", "\tthis.context.msBackingStorePixelRatio ||\n", "\tthis.context.oBackingStorePixelRatio ||\n", "\tthis.context.backingStorePixelRatio || 1;\n", "\n", " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", "\n", " var rubberband = $('');\n", " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", "\n", " var pass_mouse_events = true;\n", "\n", " canvas_div.resizable({\n", " start: function(event, ui) {\n", " pass_mouse_events = false;\n", " },\n", " resize: function(event, ui) {\n", " fig.request_resize(ui.size.width, ui.size.height);\n", " },\n", " stop: function(event, ui) {\n", " pass_mouse_events = true;\n", " fig.request_resize(ui.size.width, ui.size.height);\n", " },\n", " });\n", "\n", " function mouse_event_fn(event) {\n", " if (pass_mouse_events)\n", " return fig.mouse_event(event, event['data']);\n", " }\n", "\n", " rubberband.mousedown('button_press', mouse_event_fn);\n", " rubberband.mouseup('button_release', mouse_event_fn);\n", " // Throttle sequential mouse events to 1 every 20ms.\n", " rubberband.mousemove('motion_notify', mouse_event_fn);\n", "\n", " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", "\n", " canvas_div.on(\"wheel\", function (event) {\n", " event = event.originalEvent;\n", " event['data'] = 'scroll'\n", " if (event.deltaY < 0) {\n", " event.step = 1;\n", " } else {\n", " event.step = -1;\n", " }\n", " mouse_event_fn(event);\n", " });\n", "\n", " canvas_div.append(canvas);\n", " canvas_div.append(rubberband);\n", "\n", " this.rubberband = rubberband;\n", " this.rubberband_canvas = rubberband[0];\n", " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", " this.rubberband_context.strokeStyle = \"#000000\";\n", "\n", " this._resize_canvas = function(width, height) {\n", " // Keep the size of the canvas, canvas container, and rubber band\n", " // canvas in synch.\n", " canvas_div.css('width', width)\n", " canvas_div.css('height', height)\n", "\n", " canvas.attr('width', width * mpl.ratio);\n", " canvas.attr('height', height * mpl.ratio);\n", " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", "\n", " rubberband.attr('width', width);\n", " rubberband.attr('height', height);\n", " }\n", "\n", " // Set the figure to an initial 600x600px, this will subsequently be updated\n", " // upon first draw.\n", " this._resize_canvas(600, 600);\n", "\n", " // Disable right mouse context menu.\n", " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\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 nav_element = $('
')\n", " nav_element.attr('style', 'width: 100%');\n", " this.root.append(nav_element);\n", "\n", " // Define a callback function for later on.\n", " function toolbar_event(event) {\n", " return fig.toolbar_button_onclick(event['data']);\n", " }\n", " function toolbar_mouse_event(event) {\n", " return fig.toolbar_button_onmouseover(event['data']);\n", " }\n", "\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", " // put a spacer in here.\n", " continue;\n", " }\n", " var button = $('');\n", " button.click(method_name, toolbar_event);\n", " button.mouseover(tooltip, toolbar_mouse_event);\n", " nav_element.append(button);\n", " }\n", "\n", " // Add the status bar.\n", " var status_bar = $('');\n", " nav_element.append(status_bar);\n", " this.message = status_bar[0];\n", "\n", " // Add the close button to the window.\n", " var buttongrp = $('
');\n", " var button = $('');\n", " button.click(function (evt) { fig.handle_close(fig, {}); } );\n", " button.mouseover('Stop Interaction', toolbar_mouse_event);\n", " buttongrp.append(button);\n", " var titlebar = this.root.find($('.ui-dialog-titlebar'));\n", " titlebar.prepend(buttongrp);\n", "}\n", "\n", "mpl.figure.prototype._root_extra_style = function(el){\n", " var fig = this\n", " el.on(\"remove\", function(){\n", "\tfig.close_ws(fig, {});\n", " });\n", "}\n", "\n", "mpl.figure.prototype._canvas_extra_style = function(el){\n", " // this is important to make the div 'focusable\n", " el.attr('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", " }\n", " else {\n", " // location in version 2\n", " IPython.keyboard_manager.register_events(el);\n", " }\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", " // 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", "\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= 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('matplotlib', mpl.mpl_figure_comm);\n", "}\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "x = numpy.linspace(0, 2 * numpy.pi, 100)\n", "lpf_signal = numpy.sin(x)\n", "signal = lpf_signal + numpy.random.normal(size=len(lpf_signal), scale=0.5)\n", "\n", "pyplot.figure()\n", "pyplot.plot(signal, 'red', label='Signal')\n", "pyplot.plot(lpf_signal, 'blue', label='Signal after low-pass filtering')\n", "pyplot.legend()\n", "pyplot.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Consider the random signal below. Take the DFT of this signal, then zero-out the higher half of the frequencies. The high frequencies are considered those greater than $B/4$ and less than $-B/4$ (we want to remove large negative frequencies as well). Be careful to remove the right frequencies. Then use the inverse DFT to recover the filtered time-domain signal.\n", "\n", "- _Plot the original signal and the signal after the low-pass filter (you can plot just the real component of both signals)_.\n", "\n", "Note that in practice, this isn't a great low-pass filter. You can read more [here](https://dsp.stackexchange.com/questions/6220/why-is-it-a-bad-idea-to-filter-by-zeroing-out-fft-bins) if you're curious why this approach is suboptimal." ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": true }, "outputs": [], "source": [ "signal_len = 64\n", "signal = numpy.random.random(signal_len) + 1j * numpy.random.random(signal_len)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Part 1: Interfacing with the HackRF radio" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The [HackRF](https://github.com/mossmann/hackrf/wiki/HackRF-One) is a low-cost software-defined radio (SDR). We use the term \"software-defined\" to distinguish from the more typical hardware radios found in things like cell phones where radio parameters like center frequency, sample rate (bandwidth), gain, and analog filters are all set in hardware. For SDRs, these parameters can vary over a wide range and can be set in software. So SDRs are more flexible than typical radio hardware, but they are more expensive and draw more power.\n", "\n", "Nearly all modern digital radios (software-defined or not) will deal with complex digital samples. These samples are sometimes called \"in-phase and quadrature\" samples or simply I/Q samples (\"in-phase\" refers to the real part and \"quadrature\" refers to the imaginary part). The reason for using complex values has to do with hardware design which we won't go into in this course, but it is also mathematically convenient (consider the definition of the DFT uses complex values).\n", "\n", "### Problem 1.1 Tuning in on the airwaves (10 pts):\n", "- _Using the API below, tune your HackRF hardware to a center frequency of 1250MHz at a bandwidth of 5MHz and capture 1M samples. Plot the samples in the time domain and the frequency domain (i.e. after applying the DFT)._\n", "\n", "Plot the real and imaginary components of the time signal separately (on the same plot), and plot the magnitude of the spectrum (with a logarithmic scale).\n", "\n", "Use the `Radio` class provided below. Software radios usually do buffered I/O, meaning samples are buffered in the radio hardware and then the whole buffer is sent to the computer. This is instead of sending one sample at a time to the computer, which would be inefficient. So you'll need to grab samples in chunks of the buffer size and store them in memory. The buffer size for the HackRF is 131072 samples, and is set in hardware so it cannot be changed. You may grab samples from the HackRF in chunk sizes up to and including the buffer size." ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class Radio:\n", " def __init__(self, *args, **kwargs):\n", " self.sdr = SoapySDR.Device(*args, **kwargs)\n", "\n", " def set_sample_rate(self, sample_rate_hz):\n", " self.sdr.setSampleRate(SOAPY_SDR_RX, 0, sample_rate_hz)\n", "\n", " def set_center_frequency(self, freq_hz):\n", " self.sdr.setFrequency(SOAPY_SDR_RX, 0, freq_hz)\n", "\n", " def start_receive(self):\n", " self.rx_stream = self.sdr.setupStream(SOAPY_SDR_RX, SOAPY_SDR_CF32)\n", " self.sdr.activateStream(self.rx_stream)\n", "\n", " def stop_receive(self):\n", " self.sdr.deactivateStream(self.rx_stream)\n", " self.sdr.closeStream(self.rx_stream)\n", " self.rx_stream = None\n", "\n", " def grab_samples(self, rx_buff):\n", " if self.rx_stream is None:\n", " raise RuntimeError(\"Need to start receiving before grabbing samples\")\n", "\n", " if len(rx_buff) > self.get_buffer_size():\n", " raise RuntimeError(\"Number of samples cannot be more than the buffer size\")\n", "\n", " resp = self.sdr.readStream(self.rx_stream, [rx_buff], numElems=len(rx_buff))\n", " if resp.ret != len(rx_buff):\n", " raise RuntimeError('Receive failed: {}'.format(SoapySDR.errToStr(resp.ret)))\n", "\n", " def get_buffer_size(self):\n", " return 131072" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You should notice a signal centered at 1252 MHz with bandwidth 1 MHz. This is a real signal, i.e. it reflects the content of the wireless medium. But you should also notice in the spectrum a large magnitude at frequency 0. This is an artifact of the radio hardware (i.e. it's not a real signal in the wireless medium), and it's called the \"DC offset.\" DC offset is very difficult to remove from the hardware, and so most practical radios simply deal with it, usually by not sending any data close to the center frequency and removing the DC offset in software.\n", "\n", "One way to remove the DC offset from the recorded data is to just zero out the zero-frequency bin of the spectrum. But this requires taking an FFT of the data. How could you remove the DC offset from a recorded signal directly in the time domain? Hint: What does the zero frequency represent? Look at the definition of the DFT when $k = 0$.\n", "### Problem 1.2 Removing the DC offset (10 pts)\n", "- _Perform this operation to remove the DC offset from your received signal and plot the time signal and the spectrum just like in Problem 1.1._" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Part 2: Fine-tuning Your Reception\n", "\n", "Both transmitters and receivers can have different **gains**. A transmitter's gain determines how loud the signal is over-the-air, and a receiver's gain determines how much a signal is amplified before being digitally sampled. The receive gain is important to get right because it determines the **dynamic range** of the analog-to-digital converter (ADC). For example, an 8-bit ADC has values between 0 and 255. The dynamic range is the voltage difference between the value of 0 and the value of 255. If the receive gain is too low, then the signal may be lost in the noise, but if the receive gain is too high, then all of your signal will have value close to 255 which will cause distortion. This distortion is known as \"clipping,\" and is demonstrated below." ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "data": { "application/javascript": [ "/* Put everything inside the global mpl namespace */\n", "window.mpl = {};\n", "\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('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", "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 = $('
');\n", " this._root_extra_style(this.root)\n", " this.root.attr('style', 'display: inline-block');\n", "\n", " $(parent_element).append(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 (mpl.ratio != 1) {\n", " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.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", " this.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 = $(\n", " '
');\n", " var titletext = $(\n", " '
');\n", " titlebar.append(titletext)\n", " this.root.append(titlebar);\n", " this.header = titletext[0];\n", "}\n", "\n", "\n", "\n", "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", "\n", "}\n", "\n", "\n", "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", "\n", "}\n", "\n", "mpl.figure.prototype._init_canvas = function() {\n", " var fig = this;\n", "\n", " var canvas_div = $('
');\n", "\n", " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", "\n", " function canvas_keyboard_event(event) {\n", " return fig.key_event(event, event['data']);\n", " }\n", "\n", " canvas_div.keydown('key_press', canvas_keyboard_event);\n", " canvas_div.keyup('key_release', canvas_keyboard_event);\n", " this.canvas_div = canvas_div\n", " this._canvas_extra_style(canvas_div)\n", " this.root.append(canvas_div);\n", "\n", " var canvas = $('');\n", " canvas.addClass('mpl-canvas');\n", " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", "\n", " this.canvas = canvas[0];\n", " this.context = canvas[0].getContext(\"2d\");\n", "\n", " var backingStore = this.context.backingStorePixelRatio ||\n", "\tthis.context.webkitBackingStorePixelRatio ||\n", "\tthis.context.mozBackingStorePixelRatio ||\n", "\tthis.context.msBackingStorePixelRatio ||\n", "\tthis.context.oBackingStorePixelRatio ||\n", "\tthis.context.backingStorePixelRatio || 1;\n", "\n", " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", "\n", " var rubberband = $('');\n", " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", "\n", " var pass_mouse_events = true;\n", "\n", " canvas_div.resizable({\n", " start: function(event, ui) {\n", " pass_mouse_events = false;\n", " },\n", " resize: function(event, ui) {\n", " fig.request_resize(ui.size.width, ui.size.height);\n", " },\n", " stop: function(event, ui) {\n", " pass_mouse_events = true;\n", " fig.request_resize(ui.size.width, ui.size.height);\n", " },\n", " });\n", "\n", " function mouse_event_fn(event) {\n", " if (pass_mouse_events)\n", " return fig.mouse_event(event, event['data']);\n", " }\n", "\n", " rubberband.mousedown('button_press', mouse_event_fn);\n", " rubberband.mouseup('button_release', mouse_event_fn);\n", " // Throttle sequential mouse events to 1 every 20ms.\n", " rubberband.mousemove('motion_notify', mouse_event_fn);\n", "\n", " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", "\n", " canvas_div.on(\"wheel\", function (event) {\n", " event = event.originalEvent;\n", " event['data'] = 'scroll'\n", " if (event.deltaY < 0) {\n", " event.step = 1;\n", " } else {\n", " event.step = -1;\n", " }\n", " mouse_event_fn(event);\n", " });\n", "\n", " canvas_div.append(canvas);\n", " canvas_div.append(rubberband);\n", "\n", " this.rubberband = rubberband;\n", " this.rubberband_canvas = rubberband[0];\n", " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", " this.rubberband_context.strokeStyle = \"#000000\";\n", "\n", " this._resize_canvas = function(width, height) {\n", " // Keep the size of the canvas, canvas container, and rubber band\n", " // canvas in synch.\n", " canvas_div.css('width', width)\n", " canvas_div.css('height', height)\n", "\n", " canvas.attr('width', width * mpl.ratio);\n", " canvas.attr('height', height * mpl.ratio);\n", " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", "\n", " rubberband.attr('width', width);\n", " rubberband.attr('height', height);\n", " }\n", "\n", " // Set the figure to an initial 600x600px, this will subsequently be updated\n", " // upon first draw.\n", " this._resize_canvas(600, 600);\n", "\n", " // Disable right mouse context menu.\n", " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\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 nav_element = $('
')\n", " nav_element.attr('style', 'width: 100%');\n", " this.root.append(nav_element);\n", "\n", " // Define a callback function for later on.\n", " function toolbar_event(event) {\n", " return fig.toolbar_button_onclick(event['data']);\n", " }\n", " function toolbar_mouse_event(event) {\n", " return fig.toolbar_button_onmouseover(event['data']);\n", " }\n", "\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", " // put a spacer in here.\n", " continue;\n", " }\n", " var button = $('