Skip to content

Instantly share code, notes, and snippets.

@blink1073
Last active February 4, 2016 11:22
Show Gist options
  • Save blink1073/184601e653a0e4aace9f to your computer and use it in GitHub Desktop.
Save blink1073/184601e653a0e4aace9f to your computer and use it in GitHub Desktop.
QGrid IPython Widget
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"#qgrid - SlickGrid in IPython notebook\n",
"---"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Qgrid is an IPython extension which uses a javascript library called SlickGrid to render pandas DataFrames within an IPython notebook. It's being developed for use in [Quantopian's hosted research environment]( https://www.quantopian.com/research?utm_source=github&utm_medium=web&utm_campaign=qgrid-nbviewer), and this notebook demonstrates the current state of the project."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Overview\n",
"* [SlickGrid](https://github.com/mleibman/SlickGrid) is an an advanced javascript grid which allows users to scroll, sort, \n",
"and filter hundreds of thousands of rows with extreme responsiveness. \n",
"* [Pandas](https://github.com/pydata/pandas) is a powerful data analysis / manipulation library for Python, and DataFrames are the primary way of storing and manipulating two-dimensional data in pandas.\n",
"\n",
"[Qgrid](https://github.com/quantopian/qgrid) renders pandas DataFrames as SlickGrids, which enables users to explore the entire contents of a DataFrame using intuitive sorting and filtering controls. It's designed to be used within IPython notebook, and it's also fully functional when rendered in [nbviewer](http://nbviewer.ipython\n",
".org/github/quantopian/qgrid/blob/master/qgrid_demo.ipynb).\n",
"\n",
"This Demo adds an IPython Widget for a QGrid, which allows us to interact with the dataframe within the notebook."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Installation using 'pip install'\n",
"##### Qgrid is a python module so you can install it using pip:\n",
"\n",
"```pip install git+https://github.com/quantopian/qgrid```\n",
"\n",
"##### Import it into your namespace like you would for any other python module:"
]
},
{
"cell_type": "code",
"execution_count": 110,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"import qgrid"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Demo - Interacting with a DataFrame returned by Yahoo Finance\n",
"##### First, lets create a sample DataFrame using pandas 'get_data_yahoo' function:"
]
},
{
"cell_type": "code",
"execution_count": 111,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"randn = np.random.randn\n",
"\n",
"pd.set_option('display.max_rows', 8)\n",
"\n",
"from pandas.io.data import get_data_yahoo\n",
"spy = get_data_yahoo(\n",
" symbols='SPY',\n",
" start=pd.Timestamp('2011-01-01'),\n",
" end=pd.Timestamp('2014-01-01'),\n",
" adjust_price=True,\n",
")\n",
"\n",
"# use an integer index so we can add rows\n",
"spy = spy.reset_index()"
]
},
{
"cell_type": "code",
"execution_count": 136,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"application/javascript": [
"require([\"widgets/js/widget\", \"widgets/js/manager\"], function(widget, manager){\n",
"\n",
" /**\n",
" * Date input handler.\n",
" *\n",
" * Adapted from https://github.com/mleibman/SlickGrid/blob/master/slick.editors.js\n",
" * MIT License, Copyright (c) 2010 Michael Leibman\n",
" */\n",
" function DateEditor(args) {\n",
" var $input;\n",
" var defaultValue;\n",
" var scope = this;\n",
" var calendarOpen = false;\n",
" this.init = function () {\n",
" $input = $(\"<INPUT type=text class='editor-text' />\");\n",
" $input.appendTo(args.container);\n",
" $input.focus().select();\n",
" $input.datepicker({\n",
" showOn: \"button\",\n",
" buttonImageOnly: true,\n",
" beforeShow: function () {\n",
" calendarOpen = true\n",
" },\n",
" onClose: function () {\n",
" calendarOpen = false\n",
" }\n",
" });\n",
"\n",
" $input.width($input.width() - 18);\n",
" };\n",
"\n",
" this.destroy = function () {\n",
" $.datepicker.dpDiv.stop(true, true);\n",
" $input.datepicker(\"hide\");\n",
" $input.datepicker(\"destroy\");\n",
" $input.remove();\n",
" };\n",
"\n",
" this.show = function () {\n",
" if (calendarOpen) {\n",
" $.datepicker.dpDiv.stop(true, true).show();\n",
" }\n",
" };\n",
"\n",
" this.hide = function () {\n",
" if (calendarOpen) {\n",
" $.datepicker.dpDiv.stop(true, true).hide();\n",
" }\n",
" };\n",
"\n",
" this.position = function (position) {\n",
" if (!calendarOpen) {\n",
" return;\n",
" }\n",
" $.datepicker.dpDiv\n",
" .css(\"top\", position.top + 30)\n",
" .css(\"left\", position.left);\n",
" };\n",
"\n",
" this.focus = function () {\n",
" $input.focus();\n",
" };\n",
"\n",
" this.loadValue = function (item) {\n",
" defaultValue = item[args.column.field];\n",
" $input.val(defaultValue);\n",
" $input[0].defaultValue = defaultValue;\n",
" $input.select();\n",
" };\n",
"\n",
" this.serializeValue = function () {\n",
" return $input.val();\n",
" };\n",
"\n",
" this.applyValue = function (item, state) {\n",
" item[args.column.field] = state;\n",
" };\n",
"\n",
" this.isValueChanged = function () {\n",
" return (!($input.val() == \"\" && defaultValue == null)) && ($input.val() != defaultValue);\n",
" };\n",
"\n",
" this.validate = function () {\n",
" return {\n",
" valid: true,\n",
" msg: null\n",
" };\n",
" };\n",
"\n",
" this.init();\n",
" }\n",
"\n",
" /**\n",
" * Text input handler.\n",
" *\n",
" * From https://github.com/mleibman/SlickGrid/blob/master/slick.editors.js\n",
" * MIT License, Copyright (c) 2010 Michael Leibman\n",
" */\n",
" function TextEditor(args) {\n",
" var $input;\n",
" var defaultValue;\n",
" var scope = this;\n",
"\n",
" this.init = function () {\n",
" $input = $(\"<INPUT type=text class='editor-text' />\")\n",
" .appendTo(args.container)\n",
" .bind(\"keydown.nav\", function (e) {\n",
" if (e.keyCode === $.ui.keyCode.LEFT || e.keyCode === $.ui.keyCode.RIGHT) {\n",
" e.stopImmediatePropagation();\n",
" }\n",
" })\n",
" .focus()\n",
" .select();\n",
" };\n",
"\n",
" this.destroy = function () {\n",
" $input.remove();\n",
" };\n",
"\n",
" this.focus = function () {\n",
" $input.focus();\n",
" };\n",
"\n",
" this.getValue = function () {\n",
" return $input.val();\n",
" };\n",
"\n",
" this.setValue = function (val) {\n",
" $input.val(val);\n",
" };\n",
"\n",
" this.loadValue = function (item) {\n",
" defaultValue = item[args.column.field] || \"\";\n",
" $input.val(defaultValue);\n",
" $input[0].defaultValue = defaultValue;\n",
" $input.select();\n",
" };\n",
"\n",
" this.serializeValue = function () {\n",
" return $input.val();\n",
" };\n",
"\n",
" this.applyValue = function (item, state) {\n",
" item[args.column.field] = state;\n",
" };\n",
"\n",
" this.isValueChanged = function () {\n",
" return (!($input.val() == \"\" && defaultValue == null)) && ($input.val() != defaultValue);\n",
" };\n",
"\n",
" this.validate = function () {\n",
" if (args.column.validator) {\n",
" var validationResults = args.column.validator($input.val());\n",
" if (!validationResults.valid) {\n",
" return validationResults;\n",
" }\n",
" }\n",
"\n",
" return {\n",
" valid: true,\n",
" msg: null\n",
" };\n",
" };\n",
"\n",
" this.init();\n",
" }\n",
" \n",
" /**\n",
" * Validator for numeric cells.\n",
" */\n",
" function validateNumber(value) {\n",
" if (isNaN(value)) {\n",
" return {\n",
" valid: false,\n",
" msg: \"Please enter a valid integer\"\n",
" };\n",
" }\n",
" return {\n",
" valid: true,\n",
" msg: null\n",
" };\n",
" }\n",
"\n",
" var grid;\n",
"\n",
" var QGridView = widget.DOMWidgetView.extend({\n",
"\n",
" render: function() {\n",
" var that = this;\n",
" var cdn_base_url = \"https://cdn.rawgit.com/quantopian/qgrid/6988858671b6f80c7d1987e07ebff3dc810d20ce/qgrid/qgridjs/\";\n",
" \n",
" // Load the custom css\n",
" if ($(\"#dg-css\").length == 0){\n",
" $(\"head\").append([\n",
" \"<link href='\" + cdn_base_url + \"/lib/slick.grid.css' rel='stylesheet'>\",\n",
" \"<link href='\" + cdn_base_url + \"/lib/slick-default-theme.css' rel='stylesheet'>\",\n",
" \"<link href='http://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.4/css/jquery-ui.min.css' rel='stylesheet'>\",\n",
" \"<link id='dg-css' href='\" + cdn_base_url + \"/qgrid.css' rel='stylesheet'>\"\n",
" ]);\n",
" }\n",
" \n",
" // set the custom script load paths\n",
" var path_dictionary = {\n",
" jquery_drag: cdn_base_url + \"/lib/jquery.event.drag-2.2\",\n",
" slick_core: cdn_base_url + \"/lib/slick.core.2.2\",\n",
" slick_data_view: cdn_base_url + \"/lib/slick.dataview.2.2\",\n",
" slick_grid: cdn_base_url + \"/lib/slick.grid.2.2\",\n",
" data_grid: cdn_base_url + \"/qgrid\",\n",
" date_filter: cdn_base_url + \"/qgrid.datefilter\",\n",
" slider_filter: cdn_base_url + \"/qgrid.sliderfilter\",\n",
" filter_base: cdn_base_url + \"/qgrid.filterbase\",\n",
" handlebars: \"https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/2.0.0/handlebars.min\"\n",
" };\n",
"\n",
" var existing_config = require.s.contexts._.config;\n",
" if (!existing_config.paths['underscore']){\n",
" path_dictionary['underscore'] = \"https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min\";\n",
" }\n",
"\n",
" if (!existing_config.paths['moment']){\n",
" path_dictionary['moment'] = \"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.8.3/moment.min\";\n",
" }\n",
"\n",
" if (!existing_config.paths['jqueryui']){\n",
" path_dictionary['jqueryui'] = \"https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.1/jquery-ui.min\";\n",
" }\n",
"\n",
" require.config({\n",
" paths: path_dictionary\n",
" });\n",
" \n",
" if (typeof jQuery === 'function') {\n",
" define('jquery', function() { return jQuery; });\n",
" }\n",
" \n",
" /**\n",
" * Load the required scripts and create the grid.\n",
" */\n",
" require([\n",
" 'jquery',\n",
" 'jquery_drag',\n",
" 'slick_core',\n",
" 'slick_data_view'\n",
" ],\n",
" function() {\n",
" require(['slick_grid'], function() {\n",
" require([\"data_grid\"], function(dgrid) {\n",
" that.setupQGrid(dgrid);\n",
" });\n",
" });\n",
" });\n",
" },\n",
"\n",
" /**\n",
" * Set up our QGrid and event handlers.\n",
" */\n",
" setupQGrid: function(dgrid) {\n",
" var that = this;\n",
" // set up the divs and styles\n",
" this.$el.addClass('q-grid-container');\n",
" var table = this.$el.append('div');\n",
" table.addClass('q-grid');\n",
" var width = this.el.clientWidth.toString();\n",
" this.el.setAttribute(\"style\", \"max-width:\" + width + \"px;\");\n",
"\n",
" // create the table\n",
" var df = JSON.parse(this.model.get('_df_json'));\n",
" var column_types = JSON.parse(this.model.get('_column_types_json'));\n",
" \n",
" grid = new dgrid.QGrid(table[0], df, column_types);\n",
" grid.initialize_slick_grid();\n",
"\n",
" // set up editing\n",
" var editable = this.model.get('editable');\n",
" if (!editable) {\n",
" return;\n",
" }\n",
"\n",
" var sgrid = grid.slick_grid;\n",
" sgrid.setOptions({'editable': true, \n",
" 'autoEdit': false});\n",
" var columns = sgrid.getColumns();\n",
" for (var i = 1; i < columns.length; i++) {\n",
" if (columns[i].type === 'date') {\n",
" columns[i].editor = DateEditor;\n",
" } else {\n",
" columns[i].editor = TextEditor;\n",
" }\n",
" if (columns[i].type === 'number') {\n",
" columns[i].validator = validateNumber;\n",
" } \n",
" }\n",
" sgrid.setColumns(columns);\n",
"\n",
" // set up callbacks\n",
" sgrid.onCellChange.subscribe(function(e, args) {\n",
" var column = columns[args.cell].name;\n",
" var msg = {'row': args.row, 'column': column,\n",
" 'value': args.item[column], 'type': 'cell_change'};\n",
" that.sendMsg(msg);\n",
" });\n",
"\n",
" // subscribe to incoming messages from the QGridWidget\n",
" this.model.on('change:py_msg', this.handleMsg, this);\n",
" },\n",
" \n",
" /**\n",
" * Handle messages from the QGridWidget.\n",
" */\n",
" handleMsg: function() {\n",
" var sgrid = grid.slick_grid;\n",
" var msg = this.model.get('py_msg');\n",
" msg = JSON.parse(msg);\n",
"\n",
" if (msg.type === 'remove_row') {\n",
" var row = sgrid.getActiveCell().row;\n",
" var data = sgrid.getData().getItem(row); \n",
" grid.data_view.deleteItem(data.id);\n",
" msg = {'type': 'remove_row', 'row': row, 'id': data.id};\n",
" this.sendMsg(msg);\n",
"\n",
" } else if (msg.type === 'add_row') {\n",
" var dd = sgrid.getData();\n",
" dd.addItem(msg);\n",
" dd.refresh();\n",
" }\n",
" },\n",
"\n",
" /**\n",
" * Send a message to the QGridWidget.\n",
" */\n",
" sendMsg: function(msg) {\n",
" // add a unique id and send the message\n",
" var s = [];\n",
" var hexDigits = \"0123456789ABCDEF\";\n",
" for (var i = 0; i < 32; i++) {\n",
" s[i] = hexDigits.charAt(Math.floor(Math.random() * 0x10));\n",
" }\n",
" msg.uid = s.join(\"\");\n",
" this.model.set('js_msg', JSON.stringify(msg));\n",
" this.touch();\n",
" }\n",
" });\n",
"\n",
" manager.WidgetManager.register_widget_view('QGridView', QGridView);\n",
"});"
],
"text/plain": [
"<IPython.core.display.Javascript object>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"%%javascript\n",
"require([\"widgets/js/widget\", \"widgets/js/manager\"], function(widget, manager){\n",
"\n",
" /**\n",
" * Date input handler.\n",
" *\n",
" * Adapted from https://github.com/mleibman/SlickGrid/blob/master/slick.editors.js\n",
" * MIT License, Copyright (c) 2010 Michael Leibman\n",
" */\n",
" function DateEditor(args) {\n",
" var $input;\n",
" var defaultValue;\n",
" var scope = this;\n",
" var calendarOpen = false;\n",
" this.init = function () {\n",
" $input = $(\"<INPUT type=text class='editor-text' />\");\n",
" $input.appendTo(args.container);\n",
" $input.focus().select();\n",
" $input.datepicker({\n",
" showOn: \"button\",\n",
" buttonImageOnly: true,\n",
" beforeShow: function () {\n",
" calendarOpen = true\n",
" },\n",
" onClose: function () {\n",
" calendarOpen = false\n",
" }\n",
" });\n",
"\n",
" $input.width($input.width() - 18);\n",
" };\n",
"\n",
" this.destroy = function () {\n",
" $.datepicker.dpDiv.stop(true, true);\n",
" $input.datepicker(\"hide\");\n",
" $input.datepicker(\"destroy\");\n",
" $input.remove();\n",
" };\n",
"\n",
" this.show = function () {\n",
" if (calendarOpen) {\n",
" $.datepicker.dpDiv.stop(true, true).show();\n",
" }\n",
" };\n",
"\n",
" this.hide = function () {\n",
" if (calendarOpen) {\n",
" $.datepicker.dpDiv.stop(true, true).hide();\n",
" }\n",
" };\n",
"\n",
" this.position = function (position) {\n",
" if (!calendarOpen) {\n",
" return;\n",
" }\n",
" $.datepicker.dpDiv\n",
" .css(\"top\", position.top + 30)\n",
" .css(\"left\", position.left);\n",
" };\n",
"\n",
" this.focus = function () {\n",
" $input.focus();\n",
" };\n",
"\n",
" this.loadValue = function (item) {\n",
" defaultValue = item[args.column.field];\n",
" $input.val(defaultValue);\n",
" $input[0].defaultValue = defaultValue;\n",
" $input.select();\n",
" };\n",
"\n",
" this.serializeValue = function () {\n",
" return $input.val();\n",
" };\n",
"\n",
" this.applyValue = function (item, state) {\n",
" item[args.column.field] = state;\n",
" };\n",
"\n",
" this.isValueChanged = function () {\n",
" return (!($input.val() == \"\" && defaultValue == null)) && ($input.val() != defaultValue);\n",
" };\n",
"\n",
" this.validate = function () {\n",
" return {\n",
" valid: true,\n",
" msg: null\n",
" };\n",
" };\n",
"\n",
" this.init();\n",
" }\n",
"\n",
" /**\n",
" * Text input handler.\n",
" *\n",
" * From https://github.com/mleibman/SlickGrid/blob/master/slick.editors.js\n",
" * MIT License, Copyright (c) 2010 Michael Leibman\n",
" */\n",
" function TextEditor(args) {\n",
" var $input;\n",
" var defaultValue;\n",
" var scope = this;\n",
"\n",
" this.init = function () {\n",
" $input = $(\"<INPUT type=text class='editor-text' />\")\n",
" .appendTo(args.container)\n",
" .bind(\"keydown.nav\", function (e) {\n",
" if (e.keyCode === $.ui.keyCode.LEFT || e.keyCode === $.ui.keyCode.RIGHT) {\n",
" e.stopImmediatePropagation();\n",
" }\n",
" })\n",
" .focus()\n",
" .select();\n",
" };\n",
"\n",
" this.destroy = function () {\n",
" $input.remove();\n",
" };\n",
"\n",
" this.focus = function () {\n",
" $input.focus();\n",
" };\n",
"\n",
" this.getValue = function () {\n",
" return $input.val();\n",
" };\n",
"\n",
" this.setValue = function (val) {\n",
" $input.val(val);\n",
" };\n",
"\n",
" this.loadValue = function (item) {\n",
" defaultValue = item[args.column.field] || \"\";\n",
" $input.val(defaultValue);\n",
" $input[0].defaultValue = defaultValue;\n",
" $input.select();\n",
" };\n",
"\n",
" this.serializeValue = function () {\n",
" return $input.val();\n",
" };\n",
"\n",
" this.applyValue = function (item, state) {\n",
" item[args.column.field] = state;\n",
" };\n",
"\n",
" this.isValueChanged = function () {\n",
" return (!($input.val() == \"\" && defaultValue == null)) && ($input.val() != defaultValue);\n",
" };\n",
"\n",
" this.validate = function () {\n",
" if (args.column.validator) {\n",
" var validationResults = args.column.validator($input.val());\n",
" if (!validationResults.valid) {\n",
" return validationResults;\n",
" }\n",
" }\n",
"\n",
" return {\n",
" valid: true,\n",
" msg: null\n",
" };\n",
" };\n",
"\n",
" this.init();\n",
" }\n",
" \n",
" /**\n",
" * Validator for numeric cells.\n",
" */\n",
" function validateNumber(value) {\n",
" if (isNaN(value)) {\n",
" return {\n",
" valid: false,\n",
" msg: \"Please enter a valid integer\"\n",
" };\n",
" }\n",
" return {\n",
" valid: true,\n",
" msg: null\n",
" };\n",
" }\n",
"\n",
" var grid;\n",
"\n",
" var QGridView = widget.DOMWidgetView.extend({\n",
"\n",
" render: function() {\n",
" var that = this;\n",
" var cdn_base_url = \"https://cdn.rawgit.com/quantopian/qgrid/6988858671b6f80c7d1987e07ebff3dc810d20ce/qgrid/qgridjs/\";\n",
" \n",
" // Load the custom css\n",
" if ($(\"#dg-css\").length == 0){\n",
" $(\"head\").append([\n",
" \"<link href='\" + cdn_base_url + \"/lib/slick.grid.css' rel='stylesheet'>\",\n",
" \"<link href='\" + cdn_base_url + \"/lib/slick-default-theme.css' rel='stylesheet'>\",\n",
" \"<link href='http://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.4/css/jquery-ui.min.css' rel='stylesheet'>\",\n",
" \"<link id='dg-css' href='\" + cdn_base_url + \"/qgrid.css' rel='stylesheet'>\"\n",
" ]);\n",
" }\n",
" \n",
" // set the custom script load paths\n",
" var path_dictionary = {\n",
" jquery_drag: cdn_base_url + \"/lib/jquery.event.drag-2.2\",\n",
" slick_core: cdn_base_url + \"/lib/slick.core.2.2\",\n",
" slick_data_view: cdn_base_url + \"/lib/slick.dataview.2.2\",\n",
" slick_grid: cdn_base_url + \"/lib/slick.grid.2.2\",\n",
" data_grid: cdn_base_url + \"/qgrid\",\n",
" date_filter: cdn_base_url + \"/qgrid.datefilter\",\n",
" slider_filter: cdn_base_url + \"/qgrid.sliderfilter\",\n",
" filter_base: cdn_base_url + \"/qgrid.filterbase\",\n",
" handlebars: \"https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/2.0.0/handlebars.min\"\n",
" };\n",
"\n",
" var existing_config = require.s.contexts._.config;\n",
" if (!existing_config.paths['underscore']){\n",
" path_dictionary['underscore'] = \"https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min\";\n",
" }\n",
"\n",
" if (!existing_config.paths['moment']){\n",
" path_dictionary['moment'] = \"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.8.3/moment.min\";\n",
" }\n",
"\n",
" if (!existing_config.paths['jqueryui']){\n",
" path_dictionary['jqueryui'] = \"https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.1/jquery-ui.min\";\n",
" }\n",
"\n",
" require.config({\n",
" paths: path_dictionary\n",
" });\n",
" \n",
" if (typeof jQuery === 'function') {\n",
" define('jquery', function() { return jQuery; });\n",
" }\n",
" \n",
" /**\n",
" * Load the required scripts and create the grid.\n",
" */\n",
" require([\n",
" 'jquery',\n",
" 'jquery_drag',\n",
" 'slick_core',\n",
" 'slick_data_view'\n",
" ],\n",
" function() {\n",
" require(['slick_grid'], function() {\n",
" require([\"data_grid\"], function(dgrid) {\n",
" that.setupQGrid(dgrid);\n",
" });\n",
" });\n",
" });\n",
" },\n",
"\n",
" /**\n",
" * Set up our QGrid and event handlers.\n",
" */\n",
" setupQGrid: function(dgrid) {\n",
" var that = this;\n",
" // set up the divs and styles\n",
" this.$el.addClass('q-grid-container');\n",
" var table = this.$el.append('div');\n",
" table.addClass('q-grid');\n",
" var width = this.el.clientWidth.toString();\n",
" this.el.setAttribute(\"style\", \"max-width:\" + width + \"px;\");\n",
"\n",
" // create the table\n",
" var df = JSON.parse(this.model.get('_df_json'));\n",
" var column_types = JSON.parse(this.model.get('_column_types_json'));\n",
" \n",
" grid = new dgrid.QGrid(table[0], df, column_types);\n",
" grid.initialize_slick_grid();\n",
"\n",
" // set up editing\n",
" var editable = this.model.get('editable');\n",
" if (!editable) {\n",
" return;\n",
" }\n",
"\n",
" var sgrid = grid.slick_grid;\n",
" sgrid.setOptions({'editable': true, \n",
" 'autoEdit': false});\n",
" var columns = sgrid.getColumns();\n",
" for (var i = 1; i < columns.length; i++) {\n",
" if (columns[i].type === 'date') {\n",
" columns[i].editor = DateEditor;\n",
" } else {\n",
" columns[i].editor = TextEditor;\n",
" }\n",
" if (columns[i].type === 'number') {\n",
" columns[i].validator = validateNumber;\n",
" } \n",
" }\n",
" sgrid.setColumns(columns);\n",
"\n",
" // set up callbacks\n",
" sgrid.onCellChange.subscribe(function(e, args) {\n",
" var column = columns[args.cell].name;\n",
" var msg = {'row': args.row, 'column': column,\n",
" 'value': args.item[column], 'type': 'cell_change'};\n",
" that.sendMsg(msg);\n",
" });\n",
"\n",
" // subscribe to incoming messages from the QGridWidget\n",
" this.model.on('change:py_msg', this.handleMsg, this);\n",
" },\n",
" \n",
" /**\n",
" * Handle messages from the QGridWidget.\n",
" */\n",
" handleMsg: function() {\n",
" var sgrid = grid.slick_grid;\n",
" var msg = this.model.get('py_msg');\n",
" msg = JSON.parse(msg);\n",
"\n",
" if (msg.type === 'remove_row') {\n",
" var row = sgrid.getActiveCell().row;\n",
" var data = sgrid.getData().getItem(row); \n",
" grid.data_view.deleteItem(data.id);\n",
" msg = {'type': 'remove_row', 'row': row, 'id': data.id};\n",
" this.sendMsg(msg);\n",
"\n",
" } else if (msg.type === 'add_row') {\n",
" var dd = sgrid.getData();\n",
" dd.addItem(msg);\n",
" dd.refresh();\n",
" }\n",
" },\n",
"\n",
" /**\n",
" * Send a message to the QGridWidget.\n",
" */\n",
" sendMsg: function(msg) {\n",
" // add a unique id and send the message\n",
" var s = [];\n",
" var hexDigits = \"0123456789ABCDEF\";\n",
" for (var i = 0; i < 32; i++) {\n",
" s[i] = hexDigits.charAt(Math.floor(Math.random() * 0x10));\n",
" }\n",
" msg.uid = s.join(\"\");\n",
" this.model.set('js_msg', JSON.stringify(msg));\n",
" this.touch();\n",
" }\n",
" });\n",
"\n",
" manager.WidgetManager.register_widget_view('QGridView', QGridView);\n",
"});\n"
]
},
{
"cell_type": "code",
"execution_count": 137,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"import json\n",
"from IPython.html import widgets\n",
"from IPython.display import display, Javascript\n",
"from IPython.utils.traitlets import Unicode, Integer, Instance, Bool, List\n",
"from pandas import DataFrame\n",
"import uuid\n",
"\n",
"class QGridWidget(widgets.DOMWidget):\n",
" _view_name = Unicode('QGridView', sync=True)\n",
" _df_json = Unicode('', sync=True)\n",
" _column_types_json = Unicode('', sync=True)\n",
" _loop_guard = Bool(False)\n",
" _index_name = Unicode('')\n",
" js_msg = Unicode('', sync=True)\n",
" py_msg = Unicode('', sync=True)\n",
" \n",
" df = Instance(DataFrame)\n",
" editable = Bool(True, sync=True)\n",
"\n",
" def _df_changed(self):\n",
" \"\"\"Build the Data Table for the DataFrame.\"\"\"\n",
" if self._loop_guard:\n",
" return\n",
" display(Javascript(JAVASCRIPT))\n",
" df = self.df.copy()\n",
" \n",
" if not df.index.name:\n",
" df.index.name = 'Index'\n",
"\n",
" if type(df.index) == pd.core.index.MultiIndex:\n",
" df.reset_index(inplace=True)\n",
" else:\n",
" df.insert(0, df.index.name, df.index)\n",
" \n",
" self._index_name = df.index.name\n",
"\n",
" tc = dict(np.typecodes)\n",
" for key in np.typecodes.keys():\n",
" if \"All\" in key:\n",
" del tc[key]\n",
"\n",
" column_types = []\n",
" for col_name, dtype in df.dtypes.iteritems():\n",
" column_type = {'field': col_name}\n",
" for type_name, type_codes in tc.items():\n",
" if dtype.kind in type_codes:\n",
" column_type['type'] = type_name\n",
" break\n",
" column_types.append(column_type)\n",
" self._column_types_json = json.dumps(column_types)\n",
"\n",
" precision = pd.get_option('display.precision') - 1\n",
" \n",
" self._df_json = df.to_json(\n",
" orient='records',\n",
" date_format='iso',\n",
" double_precision=precision,\n",
" )\n",
" \n",
" def _js_msg_changed(self):\n",
" \"\"\"Handle incoming messages from the QGridView\"\"\"\n",
" data = json.loads(self.js_msg)\n",
"\n",
" if data['type'] == 'remove_row':\n",
" self._loop_guard = True\n",
" if data['row'] == 0:\n",
" self.df = self.df[1:]\n",
" self.df = pd.concat((self.df[:data['row']], self.df[data['row'] + 1:]))\n",
" self._loop_guard = False\n",
"\n",
" elif data['type'] == 'cell_change':\n",
" try:\n",
" self.df.set_value(self.df.index[data['row']], data['column'],\n",
" data['value'])\n",
" except ValueError:\n",
" pass\n",
" \n",
" def add_row(self, value=None):\n",
" \"\"\"Append a row at the end of the dataframe.\"\"\"\n",
" df = self.df\n",
" if not df.index.is_integer():\n",
" msg = 'alert(\"Cannot add a row a table with a non-integer index\")'\n",
" display(Javascript(msg))\n",
" return\n",
" last = df.loc[df.index[-1], :]\n",
" last.name += 1\n",
" self._loop_guard = True\n",
" self.df = self.df.append(last)\n",
" self._loop_guard = False\n",
" precision = pd.get_option('display.precision') - 1\n",
" row_data = last.to_json(date_format='iso',\n",
" double_precision=precision)\n",
" msg = json.loads(row_data)\n",
" msg[self._index_name] = str(last.name)\n",
" msg['id'] = str(last.name)\n",
" msg['type'] = 'add_row'\n",
" self._send_msg(msg)\n",
" \n",
" def _send_msg(self, msg):\n",
" \"\"\"Send a message to the QGridView\"\"\"\n",
" msg['uid'] = uuid.uuid4().hex\n",
" self.py_msg = json.dumps(msg)\n",
" \n",
" def remove_row(self, value):\n",
" \"\"\"Remove the current row from the table\"\"\"\n",
" self._send_msg({'type': 'remove_row'})"
]
},
{
"cell_type": "code",
"execution_count": 138,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/Users/ssilvester/anaconda/lib/python3.4/site-packages/pandas/core/internals.py:956: FutureWarning: comparison to `None` will result in an elementwise object comparison in the future.\n",
" return self._try_coerce_result(func(values, other))\n"
]
},
{
"data": {
"application/javascript": [
"\n",
"\n",
"require([\"widgets/js/widget\", \"widgets/js/manager\"], function(widget, manager){\n",
"\n",
" var SlickGridView = widget.DOMWidgetView.extend({\n",
" \n",
" render: function(){\n",
" var that = this;\n",
" \n",
" function DateEditor(args) {\n",
" var $input;\n",
" var defaultValue;\n",
" var scope = this;\n",
" var calendarOpen = false;\n",
"\n",
" this.init = function () {\n",
" $input = $(\"<INPUT type=text class='editor-text' />\");\n",
" $input.appendTo(args.container);\n",
" $input.focus().select();\n",
" $input.datepicker({\n",
" showOn: \"button\",\n",
" buttonImageOnly: true,\n",
" beforeShow: function () {\n",
" calendarOpen = true\n",
" },\n",
" onClose: function () {\n",
" calendarOpen = false\n",
" }\n",
" });\n",
" $input.width($input.width() - 18);\n",
" };\n",
"\n",
" this.destroy = function () {\n",
" $.datepicker.dpDiv.stop(true, true);\n",
" $input.datepicker(\"hide\");\n",
" $input.datepicker(\"destroy\");\n",
" $input.remove();\n",
" };\n",
"\n",
" this.show = function () {\n",
" if (calendarOpen) {\n",
" $.datepicker.dpDiv.stop(true, true).show();\n",
" }\n",
" };\n",
"\n",
" this.hide = function () {\n",
" if (calendarOpen) {\n",
" $.datepicker.dpDiv.stop(true, true).hide();\n",
" }\n",
" };\n",
"\n",
" this.position = function (position) {\n",
" if (!calendarOpen) {\n",
" return;\n",
" }\n",
" $.datepicker.dpDiv\n",
" .css(\"top\", position.top + 30)\n",
" .css(\"left\", position.left);\n",
" };\n",
"\n",
" this.focus = function () {\n",
" $input.focus();\n",
" };\n",
"\n",
" this.loadValue = function (item) {\n",
" defaultValue = item[args.column.field];\n",
" $input.val(defaultValue);\n",
" $input[0].defaultValue = defaultValue;\n",
" $input.select();\n",
" };\n",
"\n",
" this.serializeValue = function () {\n",
" return $input.val();\n",
" };\n",
"\n",
" this.applyValue = function (item, state) {\n",
" item[args.column.field] = state;\n",
" };\n",
"\n",
" this.isValueChanged = function () {\n",
" return (!($input.val() == \"\" && defaultValue == null)) && ($input.val() != defaultValue);\n",
" };\n",
"\n",
" this.validate = function () {\n",
" return {\n",
" valid: true,\n",
" msg: null\n",
" };\n",
" };\n",
"\n",
" this.init();\n",
" }\n",
" \n",
" function TextEditor(args) {\n",
" var $input;\n",
" var defaultValue;\n",
" var scope = this;\n",
"\n",
" this.init = function () {\n",
" $input = $(\"<INPUT type=text class='editor-text' />\")\n",
" .appendTo(args.container)\n",
" .bind(\"keydown.nav\", function (e) {\n",
" if (e.keyCode === $.ui.keyCode.LEFT || e.keyCode === $.ui.keyCode.RIGHT) {\n",
" e.stopImmediatePropagation();\n",
" }\n",
" })\n",
" .focus()\n",
" .select();\n",
" };\n",
"\n",
" this.destroy = function () {\n",
" $input.remove();\n",
" };\n",
"\n",
" this.focus = function () {\n",
" $input.focus();\n",
" };\n",
"\n",
" this.getValue = function () {\n",
" return $input.val();\n",
" };\n",
"\n",
" this.setValue = function (val) {\n",
" $input.val(val);\n",
" };\n",
"\n",
" this.loadValue = function (item) {\n",
" defaultValue = item[args.column.field] || \"\";\n",
" $input.val(defaultValue);\n",
" $input[0].defaultValue = defaultValue;\n",
" $input.select();\n",
" };\n",
"\n",
" this.serializeValue = function () {\n",
" return $input.val();\n",
" };\n",
"\n",
" this.applyValue = function (item, state) {\n",
" item[args.column.field] = state;\n",
" };\n",
"\n",
" this.isValueChanged = function () {\n",
" return (!($input.val() == \"\" && defaultValue == null)) && ($input.val() != defaultValue);\n",
" };\n",
"\n",
" this.validate = function () {\n",
" if (args.column.validator) {\n",
" var validationResults = args.column.validator($input.val());\n",
" if (!validationResults.valid) {\n",
" return validationResults;\n",
" }\n",
" }\n",
"\n",
" return {\n",
" valid: true,\n",
" msg: null\n",
" };\n",
" };\n",
"\n",
" this.init();\n",
" }\n",
" \n",
" var cdn_base_url = \"https://cdn.rawgit.com/quantopian/qgrid/6988858671b6f80c7d1987e07ebff3dc810d20ce/qgrid/qgridjs/\";\n",
" \n",
" if ($(\"#dg-css\").length == 0){\n",
" $(\"head\").append([\n",
" \"<link href='\" + cdn_base_url + \"/lib/slick.grid.css' rel='stylesheet'>\",\n",
" \"<link href='\" + cdn_base_url + \"/lib/slick-default-theme.css' rel='stylesheet'>\",\n",
" \"<link href='http://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.4/css/jquery-ui.min.css' rel='stylesheet'>\",\n",
" \"<link id='dg-css' href='\" + cdn_base_url + \"/qgrid.css' rel='stylesheet'>\"\n",
" ]);\n",
" }\n",
" \n",
" var path_dictionary = {\n",
" jquery_drag: cdn_base_url + \"/lib/jquery.event.drag-2.2\",\n",
" slick_core: cdn_base_url + \"/lib/slick.core.2.2\",\n",
" slick_data_view: cdn_base_url + \"/lib/slick.dataview.2.2\",\n",
" slick_grid: cdn_base_url + \"/lib/slick.grid.2.2\",\n",
" data_grid: cdn_base_url + \"/qgrid\",\n",
" date_filter: cdn_base_url + \"/qgrid.datefilter\",\n",
" slider_filter: cdn_base_url + \"/qgrid.sliderfilter\",\n",
" filter_base: cdn_base_url + \"/qgrid.filterbase\",\n",
" handlebars: \"https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/2.0.0/handlebars.min\"\n",
" };\n",
"\n",
" var existing_config = require.s.contexts._.config;\n",
" if (!existing_config.paths['underscore']){\n",
" path_dictionary['underscore'] = \"https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min\";\n",
" }\n",
"\n",
" if (!existing_config.paths['moment']){\n",
" path_dictionary['moment'] = \"https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.8.3/moment.min\";\n",
" }\n",
"\n",
" if (!existing_config.paths['jqueryui']){\n",
" path_dictionary['jqueryui'] = \"https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.1/jquery-ui.min\";\n",
" }\n",
"\n",
" require.config({\n",
" paths: path_dictionary\n",
" });\n",
" \n",
" if (typeof jQuery === 'function') {\n",
" define('jquery', function() { return jQuery; });\n",
" }\n",
" \n",
" function validateNumber(value) {\n",
" if (isNaN(value)) {\n",
" return {\n",
" valid: false,\n",
" msg: \"Please enter a valid integer\"\n",
" };\n",
" }\n",
" return {\n",
" valid: true,\n",
" msg: null\n",
" };\n",
" };\n",
" \n",
" function setupDGrid(dgrid) {\n",
" // set up the divs and styles\n",
" that.$el.addClass('q-grid-container');\n",
" var table = that.$el.append('div');\n",
" table.addClass('q-grid');\n",
" var width = that.el.clientWidth.toString();\n",
" that.el.setAttribute(\"style\", \"max-width:\" + width + \"px;\");\n",
"\n",
" // create the table\n",
" var df = JSON.parse(that.model.get('_df_json'));\n",
" var column_types = JSON.parse(that.model.get('_column_types_json'));\n",
" var grid = new dgrid.QGrid(table[0], df, column_types);\n",
" grid.initialize_slick_grid();\n",
"\n",
" // set up editing\n",
" var editable = that.model.get('editable');\n",
" if (!editable) {\n",
" return;\n",
" }\n",
"\n",
" var sgrid = grid.slick_grid;\n",
" sgrid.setOptions({'editable': true, \n",
" 'autoEdit': false});\n",
" var columns = sgrid.getColumns();\n",
" for (var i = 0; i < columns.length; i++) {\n",
" if (columns[i].type === 'date') {\n",
" columns[i].editor = DateEditor;\n",
" } else {\n",
" columns[i].editor = TextEditor;\n",
" }\n",
" if (columns[i].type === 'number') {\n",
" columns[i].validator = validateNumber;\n",
" } \n",
" }\n",
" sgrid.setColumns(columns);\n",
"\n",
" // set up callbacks\n",
" sgrid.onCellChange.subscribe(function(e, args) {\n",
" var column = columns[args.cell].name;\n",
" var msg = {'row': args.row, 'column': column,\n",
" 'value': args.item[column], 'type': 'cell_change'};\n",
" sendMsg(msg);\n",
" });\n",
" \n",
" function sendMsg(msg) {\n",
" // add a unique id and send the message\n",
" var s = [];\n",
" var hexDigits = \"0123456789ABCDEF\";\n",
" for (var i = 0; i < 32; i++) {\n",
" s[i] = hexDigits.charAt(Math.floor(Math.random() * 0x10));\n",
" }\n",
" msg.uid = s.join(\"\");\n",
" that.model.set('js_msg', JSON.stringify(msg));\n",
" that.touch();\n",
" }\n",
" \n",
" function handleMsg() {\n",
" var msg = that.model.get('py_msg');\n",
" msg = JSON.parse(msg);\n",
" \n",
" if (msg.type === 'remove_row') {\n",
" var row = sgrid.getActiveCell().row;\n",
" var data = sgrid.getData().getItem(row); \n",
" grid.data_view.deleteItem(data.id);\n",
" msg = {'type': 'remove_row', 'row': row, 'id': data.id};\n",
" sendMsg(msg);\n",
"\n",
" } else if (msg.type === 'add_row') {\n",
" var dd = sgrid.getData();\n",
" dd.addItem(msg);\n",
" dd.refresh();\n",
" }\n",
" }\n",
" that.model.on('change:py_msg', handleMsg, that);\n",
" };\n",
" \n",
" require([\n",
" 'jquery',\n",
" 'jquery_drag',\n",
" 'slick_core',\n",
" 'slick_data_view'\n",
" ],\n",
" function() {\n",
" require(['slick_grid'], function() {\n",
" require([\"data_grid\"], function(dgrid) {\n",
" setupDGrid(dgrid);\n",
" });\n",
" });\n",
" });\n",
" }\n",
" });\n",
" manager.WidgetManager.register_widget_view('SlickGridView', SlickGridView);\n",
"});"
],
"text/plain": [
"<IPython.core.display.Javascript object>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"from IPython.html.widgets import Button, HBox\n",
"from IPython.display import display\n",
"\n",
"# create a visualization for the dataframe\n",
"grid = QGridWidget(df=spy)\n",
"\n",
"add_row = Button(description=\"Add Row\")\n",
"add_row.on_click(grid.add_row)\n",
"\n",
"rem_row = Button(description=\"Remove Row\")\n",
"rem_row.on_click(grid.remove_row)\n",
"\n",
"display(HBox((add_row, rem_row)), grid)"
]
},
{
"cell_type": "code",
"execution_count": 122,
"metadata": {
"collapsed": false,
"scrolled": false
},
"outputs": [
{
"data": {
"text/plain": [
" Date Open High Low Close Volume \\\n",
"0 2011-01-03 115.648405 116.460708 114.726575 115.958727 138725200 \n",
"1 2011-01-04 116.214283 100.000000 115.173803 115.894838 137409700 \n",
"3 2011-01-06 116.542855 116.670633 115.922218 116.269042 122519000 \n",
"4 2011-01-07 116.424201 116.615867 115.137295 116.040867 156034600 \n",
"5 2011-01-10 115.529756 116.059125 115.182925 115.894838 122401700 \n",
"\n",
" Adj_Ratio \n",
"0 0.912701 \n",
"1 0.912701 \n",
"3 0.912701 \n",
"4 0.912701 \n",
"5 0.912701 "
]
},
"execution_count": 122,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"grid.df.head()"
]
},
{
"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.4.3"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment