Adding HTML elements to figures

This notebook contains examples how to add HTML elements to figures and create interaction between Javascript and Python code.

Note: this notebook makes interactive calculation when slider position is changed, so you need to download this notebook to see any changes in plot.

In [1]:
%matplotlib inline
import matplotlib.pylab as plt
import mpld3
mpld3.enable_notebook()

Simple example: slider plugin

We add a simple slider HTML element <input type="range"> to our figure. When slider position is changed, we call kernel.execute() and pass updated value to Python function runCalculation(). In this simple example we just update the frequency $\omega$ of $\sin(\omega x)$.

In [2]:
class SliderView(mpld3.plugins.PluginBase):
    """ Add slider and JavaScript / Python interaction. """

    JAVASCRIPT = """
    mpld3.register_plugin("sliderview", SliderViewPlugin);
    SliderViewPlugin.prototype = Object.create(mpld3.Plugin.prototype);
    SliderViewPlugin.prototype.constructor = SliderViewPlugin;
    SliderViewPlugin.prototype.requiredProps = ["idline", "callback_func"];
    SliderViewPlugin.prototype.defaultProps = {}

    function SliderViewPlugin(fig, props){
        mpld3.Plugin.call(this, fig, props);
    };

    SliderViewPlugin.prototype.draw = function(){
      var line = mpld3.get_element(this.props.idline);
      var callback_func = this.props.callback_func;

      var div = d3.select("#" + this.fig.figid);

      // Create slider
      div.append("input").attr("type", "range").attr("min", 0).attr("max", 10).attr("step", 0.1).attr("value", 1)
          .on("change", function() {
              var command = callback_func + "(" + this.value + ")";
              console.log("running "+command);
              var callbacks = { 'iopub' : {'output' : handle_output}};
              var kernel = IPython.notebook.kernel;
              kernel.execute(command, callbacks, {silent:false});
          });

      function handle_output(out){
        //console.log(out);
        var res = null;
        // if output is a print statement
        if (out.msg_type == "stream"){
          res = out.content.data;
        }
        // if output is a python object
        else if(out.msg_type === "pyout"){
          res = out.content.data["text/plain"];
        }
        // if output is a python error
        else if(out.msg_type == "pyerr"){
          res = out.content.ename + ": " + out.content.evalue;
          alert(res);
        }
        // if output is something we haven't thought of
        else{
          res = "[out type not implemented]";  
        }

        // Update line data
        line.data = JSON.parse(res);
        line.elements()
          .attr("d", line.datafunc(line.data))
          .style("stroke", "black");

       }

    };
    """

    def __init__(self, line, callback_func):
        self.dict_ = {"type": "sliderview",
                      "idline": mpld3.utils.get_id(line),
                      "callback_func": callback_func}
In [3]:
import numpy as np

def updateSlider(val1):
    t = np.linspace(0, 10, 500)
    y = np.sin(val1*t)
    return map(list, list(zip(list(t), list(y))))

fig, ax = plt.subplots(figsize=(8, 4))

t = np.linspace(0, 10, 500)
y = np.sin(t)
ax.set_xlabel('Time')
ax.set_ylabel('Amplitude')

# create the line object
line, = ax.plot(t, y, '-k', lw=3, alpha=0.5)
ax.set_ylim(-1.2, 1.2)
ax.set_title("Slider demo")

mpld3.plugins.connect(fig, SliderView(line, callback_func="updateSlider"))

Note: this notebook makes interactive calculation when slider position is changed, so you need to download this notebook to see any changes in plot.

Complex example: beam deflection

When creating more interaction between Javascript and Python, things get easily quite complicated. Therefore one should consider using e.g. Backbone or similar to get more structured code in Javascript side. IPython notebook seems to be using Backbone internally already.

In the next example we add more inputs and use Backbone to handle syncronizing Python and Javascript. In this example we calculate the deflection line $v(x)$ for simple supported Euler Bernoulli beam and update visualization when user changes force location $x \in [0, 1]$. The formula for deflection $v(x)$ is

\begin{equation} v(x) = \frac{FL^2}{6EI}\left[ \frac{ab}{L^2}(L+b)\frac{x}{L} - b\left(\frac{x}{L}\right)^3 + \frac{1}{L^2}^3 \right], \end{equation}

where \begin{equation}

=\begin{cases} 0 & ,x<a\ x-a & ,x\geq a \end{cases} \end{equation} and $a$ is distance from left support, $a+b=L$.

More information about deflection: http://en.wikipedia.org/wiki/Deflection_%28engineering%29

First we define the template we use in our plugin:

In [4]:
from IPython import display
display.HTML("""
<script type="text/template" id="tools-template">
    <h3>Tools</h3>
    <p><strong>Force location</strong></p>
    <input id="slider1" type="range" min="0" max="1" step="0.01" value="0.50" style="display: inline-block;">
    <label id="slider1label" for="slider1" style="display: inline-block; width: 40px;">0.50</label>
    <p><strong>Boundary conditions</strong></p>
    <select id="boundary_conditions">
          <option value="simple-simple">Simple support-Simple support</option>
          <option value="clamp-simple">Clamp-Simple support</option>
          <option value="clamp-clamp">Clamp-Clamp</option>
    </select>
    <p><strong>Young's modulus (GPa)</strong></p>
    <input id="young" type="number" value="210"/>
    <p><strong>Other options</strong></p>
    <div>
        <label><span style="vertical-align: middle">Use FEM to calculate deflection line?</span>
        <input id="useFEM" type="checkbox" style="vertical-align: middle" /></label>
    </div>
</script>
""")
Out[4]:

Our plugin code comes next. Note that we have now Backbone model LineModel to handle Python-Javascript-interaction and Backbone views ToolsView and CanvasView to take care of visualization when data is changed.

Note that not all input elements are "connected" to the visualization, they are more like placeholders ready for your own coding experiments. They are not implemented on purpose to keep lines of code as low as possible. To get them work, modify this.notImplemented $\rightarrow$ this.modelChanged in initialize function and change var command = ... in modelChanged to pass more parameters to notebook server. Don't forget to change Python side function accordingly.

In [5]:
class MyUserInterface(mpld3.plugins.PluginBase):
    """ Here we use Backbone to create more structured Javascript. """

    JAVASCRIPT = """

var LineModel = Backbone.Model.extend({

    initialize: function(options) {
        this.options = options || {};
        this.on("change:sliderPosition", this.modelChanged);
        this.on("change:boundaryCondition", this.notImplemented);
        this.on("change:youngsModulus", this.notImplemented);
        this.on("change:useFEM", this.notImplemented);
    },

    /**
        This example should be quite easy to extend to use more inputs. You
        just have to pass more model.get('...') things to kernel execute command below.
    */
    notImplemented: function(model) {
        alert("This function is not implemented in the example on purpose.");
    },

    /**
        Model changed, execute notebook kernel and update model data.
    */
    modelChanged: function(model) {
        var command = this.options.callback_func + "(" + model.get('sliderPosition') + ")";
        console.log("IPython kernel execute "+command);
        var callbacks = {
            'iopub' : {
                'output' : function(out) {
                    //console.log(out);
                    var res = null;
                    // if output is a print statement
                    if (out.msg_type == "stream"){
                      res = out.content.data;
                    }
                    // if output is a python object
                    else if(out.msg_type === "pyout"){
                      res = out.content.data["text/plain"];
                    }
                    // if output is a python error
                    else if(out.msg_type == "pyerr"){
                      res = out.content.ename + ": " + out.content.evalue;
                      alert(res);
                    }
                    // if output is something we haven't thought of
                    else{
                      res = "[out type not implemented]";
                      alert(res);
                    }
                    model.set("line", JSON.parse(res));
                }
            }
        };
        IPython.notebook.kernel.execute(command, callbacks, {silent:false});
    }

});


var ToolsView = Backbone.View.extend({

    /**
        This view renders toolbar with slider and other html elements.
    */
    initialize: function(options) {
        this.options = options || {};
        _.bindAll(this, 'render');
    },

    render: function() {
        var template = _.template($("#tools-template").html(), {});
        $(this.el).append(template);
        return this;
    },

    /**
        Listen event changes.
    */
    events: {
        "change #slider1": "changeSlider1",
        "change #boundary_conditions": "changeBoundaryConditions",
        "change #young": "changeModulus",
        "change #useFEM": "changeUseFEM"
    },

    changeSlider1: function(ev) {
        var sliderPosition = $(ev.currentTarget).val();
        this.model.set('sliderPosition', sliderPosition);
        $(this.el).find("#slider1label").text(parseFloat(sliderPosition).toFixed(2));
    },

    changeBoundaryConditions: function(ev) {
        this.model.set('boundaryCondition', $(ev.currentTarget).val());
    },

    changeModulus: function(ev) {
        this.model.set('youngsModulus', $(ev.currentTarget).val());
    },

    changeUseFEM: function(ev) {
        var isChecked = $(ev.currentTarget).is(":checked");
        this.model.set('useFEM', isChecked);
    }

});

var CanvasView = Backbone.View.extend({

    initialize: function(options) {
        this.options = options || {};
        this.line = mpld3.get_element(this.options.props.idline);
        _.bindAll(this, 'render');
        this.model.bind('change:line', this.render);
    },

    /**
        Update line when model changes, f.e. new data is calculated
        inside notebook and updated to Backbone model.
    */
    render: function() {
        this.line.elements().transition()
            .attr("d", this.line.datafunc(this.model.get('line')))
            .style("stroke", "black");
    }

});

// PLUGIN START

mpld3.register_plugin("myuserinterface", MyUserInterfacePlugin);
MyUserInterfacePlugin.prototype = Object.create(mpld3.Plugin.prototype);
MyUserInterfacePlugin.prototype.constructor = MyUserInterfacePlugin;
MyUserInterfacePlugin.prototype.requiredProps = ["idline", "callback_func"];
MyUserInterfacePlugin.prototype.defaultProps = {}

function MyUserInterfacePlugin(fig, props){
    mpld3.Plugin.call(this, fig, props);
};

MyUserInterfacePlugin.prototype.draw = function() {

    // Some hacking to get proper layout.
    var div = $("#" + this.fig.figid).attr("style", "border: 1px solid;");
    var figdiv = div.find("div");
    figdiv.attr("style", "display: inline;");

    // Create LineModel
    var lineModel = new LineModel({
      callback_func: this.props.callback_func
    });

    // Create tools view
    var myel = $('<div style="float: left; margin: 10px 30px;" id="tools"></div>');
    div.append(myel);
    var toolsView = new ToolsView({
        el: myel,
        model: lineModel
    });
    toolsView.render();

    // Create canvas view which updates line visualization when the model is changed
    var canvasView = new CanvasView({
        el: figdiv,
        model: lineModel,
        props: this.props
    });

};
"""

    def __init__(self, line, callback_func):
        self.dict_ = {"type": "myuserinterface",
                      "idline": mpld3.utils.get_id(line),
                      "callback_func": callback_func}

Next we do the actual calculation of the deflection using Python and display results:

In [6]:
import numpy as np

L = 1.0
F = 3.0
E = 100.0
I = 0.1

def v_(x, a):
    b = L - a
    v = a*b/L**2*(L+b)*x/L - b*(x/L)**3
    if x-a > 0.0:
        v += 1.0/L**2*(x-a)**3
    v *= F*L**2/(6.0*E*I)
    return v

v = np.vectorize(v_)

def runCalculation(a):
    """ 
    """
    x = np.linspace(0, L, 500)
    y = -v(x, a)*1000.0
    return map(list, list(zip(list(x), list(y))))

fig, ax = plt.subplots(figsize=(8, 4))

t = np.linspace(0, 1, 200)
y = np.sin(t)
ax.set_xlabel('x [m]')
ax.set_ylabel('Deflection [mm]')
ax.set_title('Euler-Bernoulli beam deflection line')

# create the line object
initial_data = np.array(runCalculation(0.5))
line, = ax.plot(initial_data[:, 0], initial_data[:, 1], '-k', lw=3, alpha=0.5)
ax.plot([0.975, 1.025, 1.00, 0.975], [-1, -1, 0, -1], '-k', lw=1)
ax.plot([-0.025, 0.025, 0.000, -0.025], [-1, -1, 0, -1], '-k', lw=1)
ax.set_ylim(-10, 5)
ax.grid(lw=0.1, alpha=0.2)

mpld3.plugins.connect(fig, MyUserInterface(line, callback_func="runCalculation"))

Note: this notebook makes interactive calculation when slider position is changed, so you need to download this notebook to see any changes in plot.

Note 2: The line is updated only when force location is changed, but it should be trivial to extend this example use more tools.

In [6]: