#!/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).