#!/usr/bin/env python # coding: utf-8 # > This is one of the 100 recipes of the [IPython Cookbook](http://ipython-books.github.io/), 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](http://handsontable.com) Javascript library. Below are the instructions to load this Javascript library in the IPython notebook. # # 1. Go [here](https://github.com/warpech/jquery-handsontable/tree/master/dist). # 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 # 2. 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) # 3. 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[ ]: get_ipython().run_cell_magic('javascript', '', 'var table_id = 0;\nrequire(["widgets/js/widget"], function(WidgetManager){ \n // Define the HandsonTableView\n var HandsonTableView = IPython.DOMWidgetView.extend({\n \n render: function(){\n // Initialization: creation of the HTML elements\n // for our widget.\n \n // Add a
in the widget area.\n this.$table = $(\'
\')\n .attr(\'id\', \'table_\' + (table_id++))\n .appendTo(this.$el);\n // Create the Handsontable table.\n this.$table.handsontable({\n });\n \n },\n \n update: function() {\n // Python --> Javascript update.\n \n // Get the model\'s JSON string, and parse it.\n var data = $.parseJSON(this.model.get(\'value\'));\n // Give it to the Handsontable widget.\n this.$table.handsontable({data: data});\n \n // Don\'t touch this...\n return HandsonTableView.__super__.update.apply(this);\n },\n \n // Tell Backbone to listen to the change event \n // of input controls.\n events: {"change": "handle_table_change"},\n \n handle_table_change: function(event) {\n // Javascript --> Python update.\n \n // Get the table instance.\n var ht = this.$table.handsontable(\'getInstance\');\n // Get the data, and serialize it in JSON.\n var json = JSON.stringify(ht.getData());\n // Update the model with the JSON string.\n this.model.set(\'value\', json);\n \n // Don\'t touch this...\n this.touch();\n },\n });\n \n // Register the HandsonTableView with the widget manager.\n WidgetManager.register_widget_view(\n \'HandsonTableView\', HandsonTableView);\n});\n') # 4. 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) # 5. 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 # 6. We wrap it in a `HandsonDataFrame` and show it. # In[ ]: ht = HandsonDataFrame(df) ht.show() # 7. 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](http://ipython-books.github.io/), by [Cyrille Rossant](http://cyrille.rossant.net), Packt Publishing, 2014 (500 pages).