Updated¶
Based on this issue.
Changes:
custom.js
. There are better ways to do this: install_nbextension
, etc.%%html
<style>@import url("http://handsontable.com/dist/jquery.handsontable.full.css")</style>
This proof-of-concept brings an interactive Excel-like data grid editor in the IPython notebook, compatible with Pandas' DataFrame. It means that whenever you have a DataFrame in the IPython notebook, you can edit it within an integrated GUI in the notebook, and the corresponding Python object will be automatically updated in real-time.
This proof-of-concept uses the new widget machinery of IPython 2.0. You need the latest development version of IPython (or v2.0beta, or v2.0 when it's released within the next few weeks). You also need the Handsontable Javascript library. Other data grid editors could probably be used as well.
There are multiple steps to make this example work (assuming you have the latest IPython).
jquery.handsontable.full.css
and jquery.handsontable.full.js
, and put these two files in ~\.ipython\profile_default\static\custom\
.custom.js
:require(['/static/custom/jquery.handsontable.full.js']);
custom.css
:@import "/static/custom/jquery.handsontable.full.css"
from __future__ import print_function # For py 2.7 compat
from IPython.html import widgets # Widget definitions
from IPython.display import display # Used to display widgets in the notebook
from IPython.utils.traitlets import List, Dict # Used to declare attributes of our widget
value
trait will contain the JSON representation of the entire table. This trait will be synchronized between Python and Javascript by IPython 2.0's widget machinery.class HandsonTableWidget(widgets.DOMWidget):
_view_name = Unicode('HandsonTableView', sync=True)
value = Unicode(sync=True)
settings = Dict(sync=True)
Now we write the Javascript code for the widget. There is a tiny bit of boilerplate code, but have a look at the three important functions that are responsible for the synchronization:
render
for the widget initializationupdate
for Python --> JS updatehandle_table_change
for JS --> Python updateThis is a bit oversimplified, of course. You will find more information on this tutorial.
%%javascript
;(function(require){
"use strict";
var window = this;
require([
"base/js/namespace",
"widgets/js/manager",
"widgets/js/widget",
"underscore",
"jquery",
"http://handsontable.com/dist/jquery.handsontable.full.js"
], function(IPython, manager, widget, _, $){
// Just once, ensure that the scroll event is called on the window for HoT
var $win = $(window);
IPython.notebook.element.on("scroll", function(){ $win.scroll(); });
// Define the HandsonTableView
var HandsonTableView = widget.DOMWidgetView.extend({
render: function(){
// CREATION OF THE WIDGET IN THE NOTEBOOK.
// Add a <div> in the widget area.
this.$table = $('<div />').appendTo(this.$el);
var view = this;
// Create the Handsontable table.
this.$table.handsontable({
// when working in HoT, don't listen for command mode keys
afterSelection: function(){ IPython.keyboard_manager.disable(); },
afterDeselect: function(){ IPython.keyboard_manager.enable(); },
// the data changed. `this` is the HoT instance
afterChange: function(changes, source){
// don't update if we did the changing!
if(source === "loadData"){ return; }
view.handle_table_change(this.getData());
}
});
},
update: function() {
// PYTHON --> JS UPDATE.
// Get the model's value (JSON)
var json = this.model.get('value');
// Parse it into data (list of lists)
var data = JSON.parse(json);
// Give it to the Handsontable widget.
this.$table.handsontable({data: data});
// Don't touch this...
return HandsonTableView.__super__.update.apply(this);
},
handle_table_change: function(data) {
// JS --> PYTHON UPDATE.
// Update the model with the JSON string.
this.model.set('value', JSON.stringify(data));
// Don't touch this...
this.touch();
}
});
// Register the HandsonTableView with the widget manager.
manager.WidgetManager.register_widget_view('HandsonTableView', HandsonTableView);
});
}).call(this, require);
DataFrame
instance. We create two callback functions for synchronizing the Pandas object with the IPython widget. Changes in the GUI will automatically trigger a change in the DataFrame
, but the converse is not true. You'll need to re-display the widget if you change the DataFrame
in Python.import StringIO
import numpy as np
import pandas as pd
class HandsonDataFrame(object):
def __init__(self, df):
self._df = df
self._widget = HandsonTableWidget()
self._widget.on_trait_change(self._on_data_changed, 'value')
self._widget.on_displayed(self._on_displayed)
def _on_displayed(self, e):
# DataFrame ==> Widget (upon initialization only)
json = self._df.to_json(orient='values')
self._widget.value = json
def _on_data_changed(self, e, val):
# Widget ==> DataFrame (called every time the user
# changes a value in the graphical widget)
buf = StringIO.StringIO(val)
self._df = pd.read_json(buf, orient='values')
def to_dataframe(self):
return self._df
def show(self):
display(self._widget)
DataFrame
.data = np.random.randint(size=(3, 5), low=100, high=900)
df = pd.DataFrame(data)
df
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
0 | 175 | 339 | 184 | 125 | 620 |
1 | 106 | 828 | 493 | 673 | 329 |
2 | 310 | 479 | 784 | 109 | 392 |
HandsonDataFrame
and show it.ht = HandsonDataFrame(df)
ht.show()
We can now change the values interactively, and they will be changed in Python automatically.
ht.to_dataframe()
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
0 | 175 | 339 | 184 | 125 | 620 |
1 | 106 | 828 | 493 | 673 | 329 |
2 | 310 | 479 | 784 | 109 | 392 |
There are many ways this proof-of-concept could be improved.
DataFrame
at very change, but update the same DataFrame
instance in-place.DataFrame
in the notebook is the HandsonDataFrame
.