In [25]:
# imports widget side
from __future__ import print_function # For py 2.7 compat

from IPython.html import widgets # Widget definitions
from IPython.display import display # Used to display widgets in the notebook
from IPython.utils.traitlets import Unicode # Used to declare attributes of our widget
from IPython.html.widgets import interact, interactive, fixed

In [26]:
# imports render side
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl

import mpld3
from mpld3 import plugins, utils

In [27]:
# imports solve side
from scipy.optimize import fsolve
import math

#http://stackoverflow.com/questions/8739227/how-to-solve-a-pair-of-nonlinear-equations-using-python

#def expchelon(a, b, x):
#    return a * (1 - exp(-b * x))

#def fun(p1, p2):
#    x1, y1 = p1
#    x2, y2 = p2
#    def equations(p):
#        a, b = p
#        return (y1 - expchelon(a, b, x1), y2 - expchelon(a, b, x2))
#    return equations

#equations = fun((1,1), (2,4))
#a, b =  fsolve(equations, (1, 1))

#print((a, b), expchelon(a, b, 1), expchelon(a, b, 2))

In [28]:
# widget sync'd python side
class GraphWidget(widgets.DOMWidget):
    _view_name = Unicode('GraphView', sync=True)
    description = 'coord'    
    value = Unicode(sync=True)

In [29]:
%%javascript
//widget javascript side
require(["widgets/js/widget"], function(WidgetManager){
    
    // is based on the DatePickerView
    var GraphView = IPython.DOMWidgetView.extend({
        render: function() {
            //@ attr id : this is the id we reach to in the dragended function in the DragPlugin
            this.$text = $('<input />')
                .attr('type', 'text')
                .attr('id', 'feedback_widget')                
                .appendTo(this.$el);
        },
        
        update: function() {
            this.$text.val(this.model.get('value'));
            return GraphView.__super__.update.apply(this);
        },
        
        events: {"change": "handle_change"},
        
        handle_change: function(event) {
            this.model.set('value', this.$text.val());
            this.touch();
        },
    });
    
    WidgetManager.register_widget_view('GraphView', GraphView);
});



In [30]:
# visu plugin
class DragPlugin(plugins.PluginBase):
    JAVASCRIPT = r"""
mpld3.register_plugin("drag", DragPlugin);
DragPlugin.prototype = Object.create(mpld3.Plugin.prototype);
DragPlugin.prototype.constructor = DragPlugin;
DragPlugin.prototype.requiredProps = ["id"];
DragPlugin.prototype.defaultProps = {}
function DragPlugin(fig, props){
    mpld3.Plugin.call(this, fig, props);
    mpld3.insert_css("#" + fig.figid + " path.dragging",
                     {"fill-opacity": "1.0 !important",
                      "stroke-opacity": "1.0 !important"});
};$

DragPlugin.prototype.draw = function(){
    var obj = mpld3.get_element(this.props.id);

    var drag = d3.behavior.drag()
        .origin(function(d) { return {x:obj.ax.x(d[0]),
                                      y:obj.ax.y(d[1])}; })
        .on("dragstart", dragstarted)
        .on("drag", dragged)
        .on("dragend", dragended);

    obj.elements()
       .data(obj.offsets)
       .style("cursor", "default")
       .call(drag);

    function dragstarted(d) {
      d3.event.sourceEvent.stopPropagation();
      d3.select(this).classed("dragging", true);
    }

    function dragged(d, i) {
      d[0] = obj.ax.x.invert(d3.event.x);
      d[1] = obj.ax.y.invert(d3.event.y);
      d3.select(this)
        .attr("transform", "translate(" + [d3.event.x,d3.event.y] + ")");
    }

    function dragended(d,i) {
      d3.select(this).classed("dragging", false);
      //console.log(this);
      // feed back the new position to python, calling the feedback widget
      $('#feedback_widget').val("" + i + "," + d[0] + "," + d[1]).trigger("change");
    }
}"""

    def __init__(self, points):
        if isinstance(points, mpl.lines.Line2D):
            suffix = "pts"
        else:
            suffix = None

        self.dict_ = {"type": "drag",
                      "id": utils.get_id(points, suffix)}

In [31]:
# fit and draw
def expchelon(a, b, x):
    return a * (1 - exp(-b * x))

def make_equations(p1, p2):
    x1, y1 = p1
    x2, y2 = p2
    def equations(p):
        a, b = p
        return (y1 - expchelon(a, b, x1), y2 - expchelon(a, b, x2))
    return equations        
        
class Fit(object):
    def __init__(self, p):
        self.p = p
    
    def recalc_param(self):
        x1 = self.p[0][0]
        y1 = self.p[0][1]
        x2 = self.p[1][0]
        y2 = self.p[1][1]
        #a = (y2 - y1) / (x2 - x1)
        #b = y2 - a * x2
        
        _equations = make_equations(*self.p)
        a, b =  fsolve(_equations, (1, 1))
        return a, b
        
    def redraw(self, coord):
        #asymptote = 1
        #speed = 2
        
        if coord != "":
            i, x, y = coord.split(",")
            i = int(i)
            self.p[i][0] = float(x)
            self.p[i][1] = float(y)
            #print(i,x,y)
        asymptote, speed = self.recalc_param()
        x = np.linspace(0,10,50) # 50 x points from 0 to 10
        y = asymptote * (1.0 - np.exp(- speed * x))
        #y = a * x + b
    
        #print(coord)
        fig, ax = plt.subplots()
        points = ax.plot([xy[0] for xy in self.p],
                         [xy[1] for xy in self.p],
                         'or', alpha=0.5,
                     markersize=10, markeredgewidth=1)
        ax.plot(x,y,'r-')
        ax.set_title("Click and Drag, we match on asym : %2.2f speed : %2.2f"%(asymptote, speed), fontsize=18)

        plugins.connect(fig, DragPlugin(points[0]))

        fig_h = mpld3.display()
        display(fig_h)

In [32]:
# click and drag not active here, we just show how we fit
Fit([[1, 1.5], [6, 4]]).redraw("")



In [38]:
my_fit = Fit([[2, 6], [6, 7]])
def f(coord):
    return my_fit.redraw(coord)
    
interact(f, coord=GraphWidget()) # you can hide the hugly details somewhere in code with what's in next cell



In [39]:
%%javascript
$('#feedback_widget').hide();