This is one of the 100 recipes of the IPython Cookbook, the definitive guide to high-performance scientific computing and data science in Python.

3.6. Creating a custom Javascript widget in the notebook: a spreadsheet editor for Pandas

You need IPython 2.0+ for this recipe. Besides, you need the Handsontable Javascript library. Below are the instructions to load this Javascript library in the IPython notebook.

  1. Go here.
  2. Download jquery.handsontable.full.css and jquery.handsontable.full.js, and put these two files in ~\.ipython\profile_default\static\custom\.
  3. In this folder, add the following line in custom.js: require(['/static/custom/jquery.handsontable.full.js']);
  4. In this folder, add the following line in custom.css: @import "/static/custom/jquery.handsontable.full.css"

Now, refresh the notebook!

  1. Let's import a few functions and classes.
In [ ]:
from IPython.html import widgets
from IPython.display import display
from IPython.utils.traitlets import Unicode
  1. We create a new widget. The value trait will contain the JSON representation of the entire table. This trait will be synchronized between Python and Javascript thanks to IPython 2.0's widget machinery.
In [ ]:
class HandsonTableWidget(widgets.DOMWidget):
    _view_name = Unicode('HandsonTableView', sync=True)
    value = Unicode(sync=True)
  1. Now we write the Javascript code for the widget. The three important functions that are responsible for the synchronization are:

    • render for the widget initialization
    • update for Python to Javascript update
    • handle_table_change for Javascript to Python update
In [ ]:
%%javascript
var table_id = 0;
require(["widgets/js/widget"], function(WidgetManager){    
    // Define the HandsonTableView
    var HandsonTableView = IPython.DOMWidgetView.extend({
        
        render: function(){
            // Initialization: creation of the HTML elements
            // for our widget.
            
            // Add a <div> in the widget area.
            this.$table = $('<div />')
                .attr('id', 'table_' + (table_id++))
                .appendTo(this.$el);
            // Create the Handsontable table.
            this.$table.handsontable({
            });
            
        },
        
        update: function() {
            // Python --> Javascript update.
            
            // Get the model's JSON string, and parse it.
            var data = $.parseJSON(this.model.get('value'));
            // Give it to the Handsontable widget.
            this.$table.handsontable({data: data});
            
            // Don't touch this...
            return HandsonTableView.__super__.update.apply(this);
        },
        
        // Tell Backbone to listen to the change event 
        // of input controls.
        events: {"change": "handle_table_change"},
        
        handle_table_change: function(event) {
            // Javascript --> Python update.
            
            // Get the table instance.
            var ht = this.$table.handsontable('getInstance');
            // Get the data, and serialize it in JSON.
            var json = JSON.stringify(ht.getData());
            // Update the model with the JSON string.
            this.model.set('value', json);
            
            // Don't touch this...
            this.touch();
        },
    });
    
    // Register the HandsonTableView with the widget manager.
    WidgetManager.register_widget_view(
        'HandsonTableView', HandsonTableView);
});
  1. Now, we have a synchronized table widget that we can already use. But we'd like to integrate it with Pandas. To do this, we create a light wrapper around a 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. We'll need to re-display the widget if we change the DataFrame in Python.
In [ ]:
from io import StringIO  # Python 2: from StringIO import StringIO
import numpy as np
import pandas as pd
In [ ]:
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(val)
        self._df = pd.read_json(buf, orient='values')
        
    def to_dataframe(self):
        return self._df
        
    def show(self):
        display(self._widget)
  1. Now, let's test all that! We first create a random DataFrame.
In [ ]:
data = np.random.randint(size=(3, 5), low=100, high=900)
df = pd.DataFrame(data)
df
  1. We wrap it in a HandsonDataFrame and show it.
In [ ]:
ht = HandsonDataFrame(df)
ht.show()
  1. We can now change the values interactively, and they will be changed in Python accordingly.
In [ ]:
ht.to_dataframe()

You'll find all the explanations, figures, references, and much more in the book (to be released later this summer).

IPython Cookbook, by Cyrille Rossant, Packt Publishing, 2014 (500 pages).