Skip to content

Instantly share code, notes, and snippets.

@tacaswell
Last active June 9, 2016 14:11
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tacaswell/b00bada647cb1eab68b13ef2a7d765bb to your computer and use it in GitHub Desktop.
Save tacaswell/b00bada647cb1eab68b13ef2a7d765bb to your computer and use it in GitHub Desktop.
2016-05-26_pydata_awj
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# AWJ: Awkward Winter Jacket\n",
"## There are feathers coming out all over the place\n",
"\n",
"https://github.com/tacaswell/awj\n",
"\n",
"### Thomas A Caswell\n",
"BNL / matplotlib\n",
"\n",
"link to this notebook: https://gist.github.com/tacaswell/b00bada647cb1eab68b13ef2a7d765bb"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Feather: A Fast On-Disk Format for Data Frames for R and Python, powered by Apache Arrow\n",
"\n",
"\n",
"Reliable google search : \"python feather wes hadley\"\n",
"\n",
"blog post : https://blog.cloudera.com/blog/2016/03/feather-a-fast-on-disk-format-for-data-frames-for-r-and-python-powered-by-apache-arrow/\n",
"\n",
"source : https://github.com/wesm/feather\n",
"\n",
"pypi : https://pypi.python.org/pypi/feather-format\n",
"\n",
"``pip install feather-format``"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"import feather\n",
"import pandas as pd\n",
"import numpy as np\n",
"arr = np.random.randn(10000000)\n",
"# 10% nulls\n",
"arr[::10] = np.nan\n",
"df = pd.DataFrame({'column_{0}'.format(i): arr for i in range(10)})\n",
"feather.write_dataframe(df, 'test.feather')"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"775M\ttest.feather\r\n"
]
}
],
"source": [
"! du -h test.feather"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"CPU times: user 363 ms, sys: 897 ms, total: 1.26 s\n",
"Wall time: 1.56 s\n"
]
}
],
"source": [
"%time df = feather.read_dataframe('test.feather')"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"503.24675324675326"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"775 / 1.54"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## AWJ\n",
"\n",
"Obvious application is caching possibly expensive to produce dataframes"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"from awj import AWJ"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"%matplotlib notebook"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"from humanize.filesize import naturalsize as ns\n",
"def fmt_sz(x):\n",
" return 'cur: {} / max: {}'.format(ns(x.cache_size), ns(x.max_size))"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"!rm /tmp/awj_demo/*.feather"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"demo = AWJ('/tmp/awj_demo', max_size=.5)"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"th = np.linspace(0, 2*np.pi, 10024)"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"demo['sin'] = pd.DataFrame({'sin': np.sin(th), 'th': th})\n",
"demo['cos'] = pd.DataFrame({'cos': np.cos(th), 'th': th})"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"['sin', 'cos']\n",
"cur: 321.3 kB / max: 524.3 kB\n"
]
}
],
"source": [
"print(list(demo))\n",
"print(fmt_sz(demo))"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"['sin', 'cos']\n"
]
}
],
"source": [
"demo2 = AWJ('/tmp/awj_demo', max_size=.5)\n",
"print(list(demo2))\n",
"del demo2"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"demo['tan'] = pd.DataFrame({'cos': np.cos(th), 'th': th})\n",
"demo['tanh'] = pd.DataFrame({'tanh': np.tanh(th), 'th': th})"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"collapsed": false,
"scrolled": true
},
"outputs": [
{
"data": {
"text/plain": [
"['tanh', 'tan', 'cos']"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"list(demo)"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"(10024, 2)"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"demo['cos'].shape"
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"demo['sin'] = pd.DataFrame({'sin': np.sin(th), 'th': th})"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"['sin', 'tanh', 'cos']"
]
},
"execution_count": 21,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"list(demo)"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"['sin', 'tanh', 'cos']"
]
},
"execution_count": 22,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"list(demo.keys())"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {
"collapsed": false,
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"cur: 321.3 kB / max: 524.3 kB\n",
"cur: 160.6 kB / max: 524.3 kB\n"
]
}
],
"source": [
"print(fmt_sz(demo))\n",
"del demo['sin']\n",
"print(fmt_sz(demo))"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"160K\t/tmp/awj_demo/cos.feather\r\n"
]
}
],
"source": [
"! du -hs /tmp/awj_demo/*"
]
},
{
"cell_type": "code",
"execution_count": 52,
"metadata": {
"collapsed": true
},
"outputs": [
{
"data": {
"application/javascript": [
"/* Put everything inside the global mpl namespace */\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('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 = $('<div/>');\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",
" 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",
" '<div class=\"ui-dialog-titlebar ui-widget-header ui-corner-all ' +\n",
" 'ui-helper-clearfix\"/>');\n",
" var titletext = $(\n",
" '<div class=\"ui-dialog-title\" style=\"width: 100%; ' +\n",
" 'text-align: center; padding: 3px;\"/>');\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 = $('<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 = $('<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 rubberband = $('<canvas/>');\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);\n",
" canvas.attr('height', height);\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 = $('<div/>')\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 = $('<button/>');\n",
" button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n",
" 'ui-button-icon-only');\n",
" button.attr('role', 'button');\n",
" button.attr('aria-disabled', 'false');\n",
" button.click(method_name, toolbar_event);\n",
" button.mouseover(tooltip, toolbar_mouse_event);\n",
"\n",
" var icon_img = $('<span/>');\n",
" icon_img.addClass('ui-button-icon-primary ui-icon');\n",
" icon_img.addClass(image);\n",
" icon_img.addClass('ui-corner-all');\n",
"\n",
" var tooltip_span = $('<span/>');\n",
" tooltip_span.addClass('ui-button-text');\n",
" tooltip_span.html(tooltip);\n",
"\n",
" button.append(icon_img);\n",
" button.append(tooltip_span);\n",
"\n",
" nav_element.append(button);\n",
" }\n",
"\n",
" var fmt_picker_span = $('<span/>');\n",
"\n",
" var fmt_picker = $('<select/>');\n",
" fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n",
" fmt_picker_span.append(fmt_picker);\n",
" nav_element.append(fmt_picker_span);\n",
" this.format_dropdown = fmt_picker[0];\n",
"\n",
" for (var ind in mpl.extensions) {\n",
" var fmt = mpl.extensions[ind];\n",
" var option = $(\n",
" '<option/>', {selected: fmt === mpl.default_extension}).html(fmt);\n",
" fmt_picker.append(option)\n",
" }\n",
"\n",
" // Add hover states to the ui-buttons\n",
" $( \".ui-button\" ).hover(\n",
" function() { $(this).addClass(\"ui-state-hover\");},\n",
" function() { $(this).removeClass(\"ui-state-hover\");}\n",
" );\n",
"\n",
" var status_bar = $('<span class=\"mpl-message\"/>');\n",
" nav_element.append(status_bar);\n",
" this.message = status_bar[0];\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",
"\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",
"\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]);\n",
" fig.send_message(\"refresh\", {});\n",
" };\n",
"}\n",
"\n",
"mpl.figure.prototype.handle_rubberband = function(fig, msg) {\n",
" var x0 = msg['x0'];\n",
" var y0 = fig.canvas.height - msg['y0'];\n",
" var x1 = msg['x1'];\n",
" var y1 = fig.canvas.height - msg['y1'];\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, 0, fig.canvas.width, fig.canvas.height);\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",
" {\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.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",
" fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n",
" evt.data);\n",
" fig.updated_canvas_event();\n",
" fig.waiting = false;\n",
" return;\n",
" }\n",
" else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == \"data:image/png;base64\") {\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(\"No handler for the '\" + msg_type + \"' message type: \", msg);\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(\"Exception inside the 'handler_\" + msg_type + \"' callback:\", e, e.stack, msg);\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",
" if (e.target)\n",
" targ = e.target;\n",
" else if (e.srcElement)\n",
" targ = e.srcElement;\n",
" if (targ.nodeType == 3) // defeat Safari bug\n",
" targ = targ.parentNode;\n",
"\n",
" // jQuery normalizes the pageX and pageY\n",
" // pageX,Y are the mouse positions relative to the document\n",
" // offset() returns the position of the element relative to the document\n",
" var x = e.pageX - $(targ).offset().left;\n",
" var y = e.pageY - $(targ).offset().top;\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",
" 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",
" {\n",
" this.canvas.focus();\n",
" this.canvas_div.focus();\n",
" }\n",
"\n",
" var x = canvas_pos.x;\n",
" var y = canvas_pos.y;\n",
"\n",
" this.send_message(name, {x: x, y: y, button: event.button,\n",
" step: event.step,\n",
" guiEvent: simpleKeys(event)});\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",
"\n",
" // Prevent repeat events\n",
" if (name == 'key_press')\n",
" {\n",
" if (event.which === this._key)\n",
" return;\n",
" else\n",
" this._key = event.which;\n",
" }\n",
" if (name == 'key_release')\n",
" this._key = null;\n",
"\n",
" var value = '';\n",
" if (event.ctrlKey && event.which != 17)\n",
" value += \"ctrl+\";\n",
" if (event.altKey && event.which != 18)\n",
" value += \"alt+\";\n",
" if (event.shiftKey && event.which != 16)\n",
" value += \"shift+\";\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,\n",
" 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",
"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\", \"Pan axes with left mouse, zoom with right\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\", \"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\";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 overriden (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 = $(\"#\" + id);\n",
" var ws_proxy = comm_websocket_adapter(comm)\n",
"\n",
" function ondownload(figure, format) {\n",
" window.open(figure.imageObj.src);\n",
" }\n",
"\n",
" var fig = new mpl.figure(id, ws_proxy,\n",
" ondownload,\n",
" element.get(0));\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.get(0);\n",
" fig.cell_info = mpl.find_output_cell(\"<div id='\" + id + \"'></div>\");\n",
" if (!fig.cell_info) {\n",
" console.error(\"Failed to find cell for figure\", id, fig);\n",
" return;\n",
" }\n",
"\n",
" var output_index = fig.cell_info[2]\n",
" var cell = fig.cell_info[0];\n",
"\n",
"};\n",
"\n",
"mpl.figure.prototype.handle_close = function(fig, msg) {\n",
" fig.root.unbind('remove')\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).html('<img src=\"' + dataURL + '\">');\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 dataURL = this.canvas.toDataURL();\n",
" this.cell_info[1]['text/html'] = '<img src=\"' + dataURL + '\">';\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 () { fig.push_to_output() }, 1000);\n",
"}\n",
"\n",
"mpl.figure.prototype._init_toolbar = function() {\n",
" var fig = this;\n",
"\n",
" var nav_element = $('<div/>')\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) { continue; };\n",
"\n",
" var button = $('<button class=\"btn btn-default\" href=\"#\" title=\"' + name + '\"><i class=\"fa ' + image + ' fa-lg\"></i></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 = $('<span class=\"mpl-message\" style=\"text-align:right; float: right;\"/>');\n",
" nav_element.append(status_bar);\n",
" this.message = status_bar[0];\n",
"\n",
" // Add the close button to the window.\n",
" var buttongrp = $('<div class=\"btn-group inline pull-right\"></div>');\n",
" var button = $('<button class=\"btn btn-mini btn-primary\" href=\"#\" title=\"Stop Interaction\"><i class=\"fa fa-power-off icon-remove icon-large\"></i></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",
" event.shiftKey = false;\n",
" // Send a \"J\" for go to next cell\n",
" event.which = 74;\n",
" event.keyCode = 74;\n",
" manager.command_mode();\n",
" manager.handle_keydown(event);\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<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('matplotlib', mpl.mpl_figure_comm);\n",
"}\n"
],
"text/plain": [
"<IPython.core.display.Javascript object>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAgAElEQVR4XuydB3hVxdaGP0ihJfTee5XeIUBCUBARREARLCBFAVFAiuAt6r0C0qWIIIoNqSIKSCeBhE7ovYTee2ip8K85EP/IDZCTOWXvs795nnnS9po9887ayZfZM2ulAQsJkAAJkAAJkAAJkIClCKSx1Gg5WBIgARIgARIgARIgAVAA0glIgARIgARIgARIwGIEKAAtNuEcLgmQAAmQAAmQAAlQANIHSIAESIAESIAESMBiBCgALTbhHC4JkAAJkAAJkAAJUADSB0iABEiABEiABEjAYgQoAC024RwuCZAACZAACZAACVAA0gdIgARIgARIgARIwGIEKAAtNuEcLgmQAAmQAAmQAAlQANIHSIAESIAESIAESMBiBCgALTbhHC4JkAAJkAAJkAAJUADSB0iABEiABEiABEjAYgQoAC024RwuCZAACZAACZAACVAA0gdIgARIgARIgARIwGIEKAAtNuEcLgmQAAmQAAmQAAlQANIHSIAESIAESIAESMBiBCgALTbhHC4JkAAJkAAJkAAJUADSB0iABEiABEiABEjAYgQoAC024RwuCZAACZAACZAACVAA0gdIgARIgARIgARIwGIEKAAtNuEcLgmQAAmQAAmQAAlQANIHSIAESIAESIAESMBiBCgALTbhHC4JkAAJkAAJkAAJUADSB0iABEiABEiABEjAYgQoAC024RwuCZAACZAACZAACVAA0gdIgARIgARIgARIwGIEKAAtNuEcLgmQAAmQAAmQAAlQANIHSIAESIAESIAESMBiBCgALTbhHC4JkAAJkAAJkAAJUADSB0iABEiABEiABEjAYgQoAC024RwuCZAACZAACZAACVAA0gdIgARIgARIgARIwGIEKAAtNuEcLgmQAAmQAAmQAAlQANIHSIAESIAESIAESMBiBCgALTbhHC4JkAAJkAAJkAAJUADSB0iABEiABEiABEjAYgQoAC024RwuCZAACZAACZAACVAA0gdIgARIgARIgARIwGIEKAAtNuEcLgmQAAmQAAmQAAlQANIHSIAESIAESIAESMBiBCgALTbhHC4JkAAJkAAJkAAJUAAa1wfySdfekTpF6jnjdtMtPSObJ2MnH/LReTDpP4+nRzZ8tnSeLUPZUgAaajr+1plq8lWE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\">"
],
"text/plain": [
"<IPython.core.display.HTML object>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/plain": [
"<matplotlib.legend.Legend at 0x7fbb017a4e80>"
]
},
"execution_count": 52,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"fig, ax = plt.subplots()\n",
"ax.plot('th', 'cos', data=demo['cos'])\n",
"ax.legend()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## matplotlib 1.5.2rc1\n",
"\n",
"``conda install -c conda-forge/channel/rc matplotlib``"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Group leader at BNL\n",
"\n",
"Open job posting for my direct supervisor. If you have 7 yrs experiance, a PhD, and want to do scientific software come talk to me!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Source"
]
},
{
"cell_type": "code",
"execution_count": 57,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"import os\n",
"import os.path\n",
"import time\n",
"import heapq\n",
"\n",
"from collections.abc import MutableMapping\n",
"from random import shuffle\n",
"from glob import glob\n",
"\n",
"import feather\n",
"\n",
"\n",
"class AWJ(MutableMapping):\n",
" '''LRU cache for DataFrames backed by on-disk feather files\n",
"\n",
"\n",
" Parameters\n",
" ----------\n",
" cache_path : str or Path\n",
" The location of the cache files\n",
"\n",
" max_size : float, optional\n",
" The maximum size in MB of the cache directory.\n",
" '''\n",
" def __init__(self, cache_path, *, max_size=None):\n",
"\n",
" self._cache_path = cache_path\n",
" self.max_size = max_size\n",
" # convert to bytes\n",
" if self.max_size is not None:\n",
" self.max_size *= 1048576\n",
"\n",
" # TODO 2k compat\n",
" os.makedirs(cache_path, exist_ok=True)\n",
" self._fn_cache = dict()\n",
" self._sz_cache = dict()\n",
" # TODO replace this with a double linked list like boltons LRU\n",
" self._heap_map = dict()\n",
" self._heap = []\n",
"\n",
" # put files in to heap in random order\n",
" files = glob(os.path.join(self._cache_path, '*feather'))\n",
" shuffle(files)\n",
" for fn in files:\n",
" key = self._key_from_filename(fn)\n",
" self._fn_cache[key] = fn\n",
" stat = os.stat(fn)\n",
" self._sz_cache[key] = stat.st_size\n",
" heap_entry = [time.time(), key]\n",
" heapq.heappush(self._heap, heap_entry)\n",
" self._heap_map[key] = heap_entry\n",
"\n",
" # prune up front just in case\n",
" self.__prune_files()\n",
"\n",
" def __prune_files(self):\n",
" if self.max_size is None or not self.max_size > 0:\n",
" return\n",
"\n",
" # TODO deal with pathological case of single file larger than max_size\n",
" # as written this will result is all files being removed\n",
" cur_size = self.cache_size\n",
" while cur_size > self.max_size:\n",
" _, key = heapq.heappop(self._heap)\n",
" if key in self:\n",
" cur_size -= self._sz_cache[key]\n",
" del self[key]\n",
"\n",
" def _filename_from_key(self, key):\n",
" return os.path.join(self._cache_path, key + '.feather')\n",
"\n",
" def _key_from_filename(self, fn):\n",
" fn, ext = os.path.splitext(os.path.basename(fn))\n",
" return fn\n",
"\n",
" def __setitem__(self, key, df):\n",
" fn = self._filename_from_key(key)\n",
" feather.write_dataframe(df, fn)\n",
" self._fn_cache[key] = fn\n",
" self._sz_cache[key] = os.stat(fn).st_size\n",
" if key in self._heap_map:\n",
" self._heap_map[key][0] = time.time()\n",
" # ensure the heap invariant\n",
" heapq.heapify(self._heap)\n",
" else:\n",
" heap_entry = [time.time(), key]\n",
" self._heap_map[key] = heap_entry\n",
" heapq.heappush(self._heap, heap_entry)\n",
"\n",
" self.__prune_files()\n",
"\n",
" def __getitem__(self, key):\n",
" fn = self._fn_cache[key]\n",
" ret = feather.read_dataframe(fn)\n",
" self._heap_map[key][0] = time.time()\n",
" # ensure the heap invariant\n",
" heapq.heapify(self._heap)\n",
" return ret\n",
"\n",
" def __delitem__(self, key):\n",
" fn = self._fn_cache.pop(key)\n",
" self._sz_cache.pop(key)\n",
" self._heap_map.pop(key)\n",
" os.unlink(fn)\n",
"\n",
" def __contains__(self, key):\n",
" return key in self._fn_cache\n",
"\n",
" def __iter__(self):\n",
" return iter(self._fn_cache)\n",
"\n",
" @property\n",
" def cache_path(self):\n",
" return self._cache_path\n",
"\n",
" @property\n",
" def cache_size(self):\n",
" return sum(v for v in self._sz_cache.values())\n",
"\n",
" def __len__(self):\n",
" return len(self._fn_cache)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.5.1"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment