{
"metadata": {
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"mimetype": "text/x-python",
"name": "python",
"pygments_lexer": "ipython3"
},
"name": "",
"signature": "sha256:e4dbef6fc65c1be6d0ab64906dc1ab7b1c910b8ce89388286c2992c2cb49f56a"
},
"nbformat": 3,
"nbformat_minor": 0,
"worksheets": [
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Blending IPython's widgets and mpld3's plugins\n",
"==============================================\n",
"\n",
"This notebook performs a function quite similar to the 'sliderPlugin' example.\n",
"Browser side visualisation is actionable and triggers recalculations in the ipython backend.\n",
"While the sliderPlugin connects to the kernel, we use IPython's facilities : interact does the lifting for us.\n",
"\n",
"Because you need an IPython instance running, you cannot use it directly on nbviewer for example. _You have to download this notebook and run it in IPython yourself._ \n",
"\n",
"I used IPython 3.0.0-dev as of 2014/11/03. The widget interface does not seems so stable for now so you may have to tinker to get this working. If you experience problems I think that the examples we built on would be good material to get the whole thing working again.\n",
"\n",
"Objective\n",
"---------\n",
"\n",
"We want to fit a curve in a cloud of points.\n",
"The points are drag/drop-able by the user of the notebook and upon dropping the point, the fit is recalculated.\n",
"\n",
"The model can be pretty much any $R \\to R$ function, with any number of parameters.\n",
"\n",
"In what follows you will see it :\n",
"- (partially) applied to an \"first order exponential response to an Heavyside function\" (for lack of better wording on my side);\n",
"- applied to an arc-tangente.\n",
"\n",
"\n",
"Architecture\n",
"------------\n",
"\n",
"Here is how things are organized :\n",
"0. code copyied from the ClickInfo/DragPoints examples on the mpld3 side will generate updates when the user drag and drop the circles;\n",
"1. these updates are the new coordinates of a given point of the cloud;\n",
"2. the update trigger the 'change' event on a text widget from IPython (code taken from the custom widget example);\n",
"3. IPython cogs and wheels transmit the update back to the IPython server side;\n",
"4. where we recalculate parameters, and redraw everything."
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# imports widget side\n",
"# see https://github.com/ipython/ipython/blob/2.x/examples/Interactive%20Widgets/Custom%20Widgets.ipynb\n",
"# and https://github.com/ipython/ipython/blob/master/examples/Interactive%20Widgets/Custom%20Widget%20-%20Hello%20World.ipynb\n",
"\n",
"from __future__ import print_function # For py 2.7 compat\n",
"\n",
"from IPython.html import widgets # Widget definitions\n",
"from IPython.display import display # Used to display widgets in the notebook\n",
"from IPython.utils.traitlets import Unicode # Used to declare attributes of our widget\n",
"from IPython.html.widgets import interact, interactive, fixed"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 1
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# imports render side\n",
"# see http://mpld3.github.io/examples/drag_points.html\n",
"\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"import matplotlib as mpl\n",
"\n",
"import mpld3\n",
"from mpld3 import plugins, utils"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 2
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# imports solve side\n",
"# see http://stackoverflow.com/questions/8739227/how-to-solve-a-pair-of-nonlinear-equations-using-python\n",
"\n",
"from scipy.optimize import fsolve\n",
"\n",
"#def expchelon(a, b, x):\n",
"# return a * (1 - np.exp(-b * x))\n",
"\n",
"#def fun(p1, p2):\n",
"# x1, y1 = p1\n",
"# x2, y2 = p2\n",
"# def equations(p):\n",
"# a, b = p\n",
"# return (y1 - expchelon(a, b, x1), y2 - expchelon(a, b, x2))\n",
"# return equations\n",
"\n",
"#equations = fun((1,1), (2,4))\n",
"#a, b = fsolve(equations, (1, 1))\n",
"\n",
"#print((a, b), expchelon(a, b, 1), expchelon(a, b, 2))"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 3
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# widget sync'd python side\n",
"class GraphWidget(widgets.DOMWidget):\n",
" _view_name = Unicode('GraphView', sync=True)\n",
" description = 'coord' \n",
" value = Unicode(sync=True)"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 4
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"%%javascript\n",
"//widget javascript side\n",
"require([\"widgets/js/widget\", \"widgets/js/manager\"], function(widget, manager){\n",
" // is based on the DatePickerView\n",
" var GraphView = widget.DOMWidgetView.extend({\n",
" render: function() {\n",
" //@ attr id : this is the id we reach to in the dragended function in the DragPlugin\n",
" this.$text = $('')\n",
" .attr('type', 'text')\n",
" .attr('id', 'feedback_widget') \n",
" .appendTo(this.$el);\n",
" },\n",
" \n",
" update: function() {\n",
" this.$text.val(this.model.get('value'));\n",
" return GraphView.__super__.update.apply(this);\n",
" },\n",
" \n",
" events: {\"change\": \"handle_change\"},\n",
" \n",
" handle_change: function(event) {\n",
" this.model.set('value', this.$text.val());\n",
" this.touch();\n",
" },\n",
" });\n",
" \n",
" manager.WidgetManager.register_widget_view('GraphView', GraphView);\n",
"});"
],
"language": "python",
"metadata": {},
"outputs": [
{
"javascript": [
"//widget javascript side\n",
"require([\"widgets/js/widget\", \"widgets/js/manager\"], function(widget, manager){\n",
" // is based on the DatePickerView\n",
" var GraphView = widget.DOMWidgetView.extend({\n",
" render: function() {\n",
" //@ attr id : this is the id we reach to in the dragended function in the DragPlugin\n",
" this.$text = $('')\n",
" .attr('type', 'text')\n",
" .attr('id', 'feedback_widget') \n",
" .appendTo(this.$el);\n",
" },\n",
" \n",
" update: function() {\n",
" this.$text.val(this.model.get('value'));\n",
" return GraphView.__super__.update.apply(this);\n",
" },\n",
" \n",
" events: {\"change\": \"handle_change\"},\n",
" \n",
" handle_change: function(event) {\n",
" this.model.set('value', this.$text.val());\n",
" this.touch();\n",
" },\n",
" });\n",
" \n",
" manager.WidgetManager.register_widget_view('GraphView', GraphView);\n",
"});"
],
"metadata": {},
"output_type": "display_data",
"text": [
""
]
}
],
"prompt_number": 5
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# visu plugin\n",
"# based on DragPlugin\n",
"class DragPlugin(plugins.PluginBase):\n",
" JAVASCRIPT = r\"\"\"\n",
"$('#feedback_widget').hide();\n",
"mpld3.register_plugin(\"drag\", DragPlugin);\n",
"DragPlugin.prototype = Object.create(mpld3.Plugin.prototype);\n",
"DragPlugin.prototype.constructor = DragPlugin;\n",
"DragPlugin.prototype.requiredProps = [\"id\"];\n",
"DragPlugin.prototype.defaultProps = {}\n",
"function DragPlugin(fig, props){\n",
" mpld3.Plugin.call(this, fig, props);\n",
" mpld3.insert_css(\"#\" + fig.figid + \" path.dragging\",\n",
" {\"fill-opacity\": \"1.0 !important\",\n",
" \"stroke-opacity\": \"1.0 !important\"});\n",
"};$\n",
"\n",
"DragPlugin.prototype.draw = function(){\n",
" var obj = mpld3.get_element(this.props.id);\n",
"\n",
" var drag = d3.behavior.drag()\n",
" .origin(function(d) { return {x:obj.ax.x(d[0]),\n",
" y:obj.ax.y(d[1])}; })\n",
" .on(\"dragstart\", dragstarted)\n",
" .on(\"drag\", dragged)\n",
" .on(\"dragend\", dragended);\n",
"\n",
" obj.elements()\n",
" .data(obj.offsets)\n",
" .style(\"cursor\", \"default\")\n",
" .call(drag);\n",
"\n",
" function dragstarted(d) {\n",
" d3.event.sourceEvent.stopPropagation();\n",
" d3.select(this).classed(\"dragging\", true);\n",
" }\n",
"\n",
" function dragged(d, i) {\n",
" d[0] = obj.ax.x.invert(d3.event.x);\n",
" d[1] = obj.ax.y.invert(d3.event.y);\n",
" d3.select(this)\n",
" .attr(\"transform\", \"translate(\" + [d3.event.x,d3.event.y] + \")\");\n",
" }\n",
"\n",
" function dragended(d,i) {\n",
" d3.select(this).classed(\"dragging\", false);\n",
" // feed back the new position to python, triggering 'change' on the widget\n",
" $('#feedback_widget').val(\"\" + i + \",\" + d[0] + \",\" + d[1]).trigger(\"change\");\n",
" }\n",
"}\"\"\"\n",
"\n",
" def __init__(self, points):\n",
" if isinstance(points, mpl.lines.Line2D):\n",
" suffix = \"pts\"\n",
" else:\n",
" suffix = None\n",
"\n",
" self.dict_ = {\"type\": \"drag\",\n",
" \"id\": utils.get_id(points, suffix)}"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 6
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# fit and draw\n",
"class Fit(object):\n",
" def __init__(self, simulate, double_seeding=False):\n",
" self.simulate = simulate\n",
" \n",
" # i will draw initial points at random\n",
" # the number of points will increase until we match arity with the function to be fit(ted?)\n",
" pseudo_fit = []\n",
" while len(pseudo_fit) < 100:\n",
" # just in case, I want to avoid inifite loops...\n",
" try:\n",
" simulate(0, pseudo_fit)\n",
" print(\"we have %d parameters\"%len(pseudo_fit))\n",
" break\n",
" except IndexError:\n",
" pseudo_fit.append(1)\n",
" \n",
" # we generate a random cloud \n",
" # the dots are distributed in (>0, >0) quadrant \n",
" self.p = np.random.standard_exponential((len(pseudo_fit), 2))\n",
" \n",
" # first guess ! all ones.\n",
" self.fit = np.array(pseudo_fit)\n",
" \n",
" def make_equations(self):\n",
" def equations(params):\n",
" return self.p[:,1] - self.simulate(self.p[:,0], params)\n",
" self.equations = equations\n",
" \n",
" def recalc_param(self):\n",
" self.make_equations()\n",
" self.fit = fsolve(self.equations, np.ones(self.fit.shape), xtol=0.01)\n",
" \n",
" def redraw(self, coord):\n",
" # we have an update !\n",
" \n",
" # record the new position for given point \n",
" if coord != \"\":\n",
" i, x, y = coord.split(\",\")\n",
" i = int(i)\n",
" self.p[i][0] = float(x)\n",
" self.p[i][1] = float(y)\n",
" \n",
" # recalculate best fit\n",
" self.recalc_param()\n",
" \n",
" # draw things\n",
" x = np.linspace(0, 10, 50) # 50 x points from 0 to 10\n",
" y = self.simulate(x, self.fit)\n",
" \n",
" fig, ax = plt.subplots()\n",
"\n",
" points = ax.plot(self.p[:,0], self.p[:,1],'or', alpha=0.5, markersize=10, markeredgewidth=1)\n",
" \n",
" ax.plot(x,y,'r-')\n",
" ax.set_title(\"Click and Drag\\n, we match on : %s\"%np.array_str(self.fit, precision=2), fontsize=12)\n",
"\n",
" plugins.connect(fig, DragPlugin(points[0]))\n",
"\n",
" fig_h = mpld3.display()\n",
" display(fig_h)"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 7
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"# click and drag not active here, we just show how we fit\n",
"\n",
"def exp_ech(x, params):\n",
" return params[0] * (1 - np.exp(-params[1] * x))\n",
"\n",
"# we ensure we will fit nicely by setting p[0] at [0,0]\n",
"# in effect adding one degree of liberty\n",
"Fit(exp_ech).redraw(\"0,0,0\")"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"we have 2 parameters\n"
]
},
{
"html": [
"\n",
"\n",
"\n",
"\n",
"\n",
""
],
"metadata": {},
"output_type": "display_data",
"text": [
""
]
}
],
"prompt_number": 8
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"def arctan(x, params):\n",
" return params[0] * np.arctan(params[1] * x + params[2])\n",
"\n",
"my_fit = Fit(arctan)\n",
"\n",
"# not sure why, but you can't do\n",
"# interact(my_fit.redraw, coord=GraphWidget())\n",
"# so we need :\n",
"def f(coord):\n",
" return my_fit.redraw(coord)\n",
" \n",
"interact(f, coord=GraphWidget())"
],
"language": "python",
"metadata": {},
"outputs": [
{
"html": [
"\n",
"\n",
"\n",
"\n",
"\n",
""
],
"metadata": {},
"output_type": "display_data",
"text": [
""
]
}
],
"prompt_number": 9
}
],
"metadata": {}
}
]
}