{ "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": {} } ] }