{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "> 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.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# 3.6. Creating a custom Javascript widget in the notebook: a spreadsheet editor for Pandas" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "1. Go [here](https://github.com/warpech/jquery-handsontable/tree/master/dist).\n", "2. Download `jquery.handsontable.full.css` and `jquery.handsontable.full.js`, and put these two files in `~\\.ipython\\profile_default\\static\\custom\\`.\n", "3. In this folder, add the following line in `custom.js`:\n", "`require(['/static/custom/jquery.handsontable.full.js']);`\n", "4. In this folder, add the following line in `custom.css`:\n", "`@import \"/static/custom/jquery.handsontable.full.css\"`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, refresh the notebook!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "1. Let's import a few functions and classes." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "from IPython.html import widgets\n", "from IPython.display import display\n", "from IPython.utils.traitlets import Unicode" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "class HandsonTableWidget(widgets.DOMWidget):\n", " _view_name = Unicode('HandsonTableView', sync=True)\n", " value = Unicode(sync=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "3. Now we write the Javascript code for the widget. The three important functions that are responsible for the synchronization are:\n", "\n", " * `render` for the widget initialization\n", " * `update` for Python to Javascript update\n", " * `handle_table_change` for Javascript to Python update" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "%%javascript\n", "var table_id = 0;\n", "require([\"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", "});" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "from io import StringIO # Python 2: from StringIO import StringIO\n", "import numpy as np\n", "import pandas as pd" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "class HandsonDataFrame(object):\n", " def __init__(self, df):\n", " self._df = df\n", " self._widget = HandsonTableWidget()\n", " self._widget.on_trait_change(self._on_data_changed, \n", " 'value')\n", " self._widget.on_displayed(self._on_displayed)\n", " \n", " def _on_displayed(self, e):\n", " # DataFrame ==> Widget (upon initialization only)\n", " json = self._df.to_json(orient='values')\n", " self._widget.value = json\n", " \n", " def _on_data_changed(self, e, val):\n", " # Widget ==> DataFrame (called every time the user\n", " # changes a value in the graphical widget)\n", " buf = StringIO(val)\n", " self._df = pd.read_json(buf, orient='values')\n", " \n", " def to_dataframe(self):\n", " return self._df\n", " \n", " def show(self):\n", " display(self._widget)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "5. Now, let's test all that! We first create a random `DataFrame`." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "data = np.random.randint(size=(3, 5), low=100, high=900)\n", "df = pd.DataFrame(data)\n", "df" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "6. We wrap it in a `HandsonDataFrame` and show it." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "ht = HandsonDataFrame(df)\n", "ht.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "7. We can now *change* the values interactively, and they will be changed in Python accordingly." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "ht.to_dataframe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> You'll find all the explanations, figures, references, and much more in the book (to be released later this summer).\n", "\n", "> [IPython Cookbook](http://ipython-books.github.io/), by [Cyrille Rossant](http://cyrille.rossant.net), Packt Publishing, 2014 (500 pages)." ] } ], "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.2" } }, "nbformat": 4, "nbformat_minor": 0 }