2

Toward learning how to create a new mpld3 plugin, I took an existing example, LinkedDataPlugin (http://mpld3.github.io/examples/heart_path.html), and modified it slightly by deleting references to lines object. That is, I created the following:

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 = ["idpts", "idpatch"];
    DragPlugin.prototype.defaultProps = {}
    function DragPlugin(fig, props){
        mpld3.Plugin.call(this, fig, props);
    };

    DragPlugin.prototype.draw = function(){
        var patchobj = mpld3.get_element(this.props.idpatch, this.fig);
        var ptsobj = mpld3.get_element(this.props.idpts, this.fig);

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

        patchobj.path.attr("d", patchobj.datafunc(ptsobj.offsets,
                                                  patchobj.pathcodes));
        patchobj.data = ptsobj.offsets;

        ptsobj.elements()
           .data(ptsobj.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] = ptsobj.ax.x.invert(d3.event.x);
          d[1] = ptsobj.ax.y.invert(d3.event.y);
          d3.select(this)
            .attr("transform", "translate(" + [d3.event.x,d3.event.y] + ")");
          patchobj.path.attr("d", patchobj.datafunc(ptsobj.offsets,
                                                    patchobj.pathcodes));
        }

        function dragended(d, i) {
          d3.select(this).classed("dragging", false);
        }
    }

    mpld3.register_plugin("drag", DragPlugin);
    """

    def __init__(self, points, patch):

        print "Points ID : ", utils.get_id(points)
        self.dict_ = {"type": "drag",
                      "idpts": utils.get_id(points),
                      "idpatch": utils.get_id(patch)}

However, when I try to link the plugin to a figure, as in

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

I get an error, 'module' is not callable, pointing to this line. What does this mean and why doesn't it work? Thanks.

I'm adding additional code to show that linking more than one Plugin might be problematic. But this may be entirely due to some silly mistake on my part, or there is a way around it. The following code based on LinkedViewPlugin generates three panels, in which the top and the bottom panel are supposed to be identical. Mouseover in the middle panel was expected to control the display in the top and bottom panels, but updates occur in the bottom panel only. It would be nice to be able to figure out how to reflect the changes in multiple panels. Thanks.

import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import mpld3
from mpld3 import plugins, utils


class LinkedView(plugins.PluginBase):
    """A simple plugin showing how multiple axes can be linked"""

    JAVASCRIPT = """
    mpld3.register_plugin("linkedview", LinkedViewPlugin);
    LinkedViewPlugin.prototype = Object.create(mpld3.Plugin.prototype);
    LinkedViewPlugin.prototype.constructor = LinkedViewPlugin;
    LinkedViewPlugin.prototype.requiredProps = ["idpts", "idline", "data"];
    LinkedViewPlugin.prototype.defaultProps = {}
    function LinkedViewPlugin(fig, props){
        mpld3.Plugin.call(this, fig, props);
    };

    LinkedViewPlugin.prototype.draw = function(){
      var pts = mpld3.get_element(this.props.idpts);
      var line = mpld3.get_element(this.props.idline);
      var data = this.props.data;

      function mouseover(d, i){
        line.data = data[i];
        line.elements().transition()
            .attr("d", line.datafunc(line.data))
            .style("stroke", this.style.fill);
      }
      pts.elements().on("mouseover", mouseover);
    };
    """

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

        self.dict_ = {"type": "linkedview",
                      "idpts": utils.get_id(points, suffix),
                      "idline": utils.get_id(line),
                      "data": linedata}

class LinkedView2(plugins.PluginBase):
    """A simple plugin showing how multiple axes can be linked"""

    JAVASCRIPT = """
    mpld3.register_plugin("linkedview", LinkedViewPlugin2);
    LinkedViewPlugin2.prototype = Object.create(mpld3.Plugin.prototype);
    LinkedViewPlugin2.prototype.constructor = LinkedViewPlugin2;
    LinkedViewPlugin2.prototype.requiredProps = ["idpts", "idline", "data"];
    LinkedViewPlugin2.prototype.defaultProps = {}
    function LinkedViewPlugin2(fig, props){
        mpld3.Plugin.call(this, fig, props);
    };

    LinkedViewPlugin2.prototype.draw = function(){
      var pts = mpld3.get_element(this.props.idpts);
      var line = mpld3.get_element(this.props.idline);
      var data = this.props.data;

      function mouseover(d, i){
        line.data = data[i];
        line.elements().transition()
            .attr("d", line.datafunc(line.data))
            .style("stroke", this.style.fill);
      }
      pts.elements().on("mouseover", mouseover);
    };
    """

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

        self.dict_ = {"type": "linkedview",
                      "idpts": utils.get_id(points, suffix),
                      "idline": utils.get_id(line),
                      "data": linedata}

fig, ax = plt.subplots(3)

# scatter periods and amplitudes
np.random.seed(0)
P = 0.2 + np.random.random(size=20)
A = np.random.random(size=20)
x = np.linspace(0, 10, 100)
data = np.array([[x, Ai * np.sin(x / Pi)]
                 for (Ai, Pi) in zip(A, P)])
points = ax[1].scatter(P, A, c=P + A,
                       s=200, alpha=0.5)
ax[1].set_xlabel('Period')
ax[1].set_ylabel('Amplitude')

# create the line object
lines = ax[0].plot(x, 0 * x, '-w', lw=3, alpha=0.5)
ax[0].set_ylim(-1, 1)
ax[0].set_title("Hover over points to see lines")
linedata = data.transpose(0, 2, 1).tolist()
plugins.connect(fig, LinkedView(points, lines[0], linedata))

# second set of lines exactly the same but in a different panel
lines2 = ax[2].plot(x, 0 * x, '-w', lw=3, alpha=0.5)
ax[2].set_ylim(-1, 1)
ax[2].set_title("Hover over points to see lines #2")
plugins.connect(fig, LinkedView2(points, lines2[0], linedata))

mpld3.show()

edited on 8/22/14

I am editing this code further to work out problems with creating a plugin to control behavior in two axes. Here's the code:

class LinkedDragPlugin(plugins.PluginBase):
    JAVASCRIPT = r"""
    mpld3.register_plugin("drag", LinkedDragPlugin);
    LinkedDragPlugin.prototype = Object.create(mpld3.Plugin.prototype);
    LinkedDragPlugin.prototype.constructor = LinkedDragPlugin;
    LinkedDragPlugin.prototype.requiredProps = ["idpts", "idline", "idpatch", 
                                                "idpts2", "idline2", "idpatch2"];
    LinkedDragPlugin.prototype.defaultProps = {}
    function LinkedDragPlugin(fig, props){
        mpld3.Plugin.call(this, fig, props);
    };

    LinkedDragPlugin.prototype.draw = function(){
        var ptsobj = mpld3.get_element(this.props.idpts, this.fig);
        var ptsobj2 = mpld3.get_element(this.props.idpts2, this.fig);
        console.log(ptsobj)
        console.log(ptsobj2)
        var lineobj = mpld3.get_element(this.props.idline, this.fig);
        var lineobj2 = mpld3.get_element(this.props.idline2, this.fig);
        console.log(lineobj)
        console.log(lineobj2)
        var patchobj = mpld3.get_element(this.props.idpatch, this.fig);
        var patchobj2 = mpld3.get_element(this.props.idpatch2, this.fig);
        console.log(patchobj)
        console.log(patchobj2)        

    mpld3.register_plugin("drag", LinkedDragPlugin);
    """

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

        self.dict_ = {"type": "drag",
                      "idpts": utils.get_id(points[0], suffix),
                      "idline": utils.get_id(line[0]),
                      "idpatch": utils.get_id(patch[0]),
                      "idpts2": utils.get_id(points[1], suffix),
                      "idline2": utils.get_id(line[1]),
                      "idpatch2": utils.get_id(patch[1])}
        print "ids :", self.dict_

bmap=brewer2mpl.get_map('Greys','Sequential',5)

fig, ax = plt.subplots(1, 2)
fig.set_size_inches(6, 4)
w = 500
h = 300
pt1 = ax[0].plot([w*0.1, w*0.9], [h*0.1, h*0.9], 'bo', ms=10, alpha=0.3)
line1 = ax[0].plot([w*0.1, w*0.9], [h*0.1, h*0.9], 'k', ms=10, lw=2, alpha=0.0)
v = [(0, h*0.1), (w, h*0.1), (0, h*0.9), (w, h*0.9),
     (w*0.1, 0), (w*0.1, h), (w*0.9, 0), (w*0.9, h)]
c = [1, 2, 1, 2, 1, 2, 1, 2]
p = path.Path(v, c)
patch = patches.PathPatch(p, fill=None, alpha=0.5)
patch1 = ax[0].add_patch(patch)
ax[0].set_xlim([0, 500])
ax[0].set_ylim([0, 300])

w = 400
h = 400
pt2 = ax[1].plot([w*0.1, w*0.9], [h*0.1, h*0.9], 'bo', ms=10, alpha=0.3)
#line2 = ax[1].plot([w*0.1, w*0.9], [h*0.1, h*0.9], 'bo', ms=10, alpha=0.3)
line2 = ax[1].plot([w*0.1, w*0.9], [h*0.1, h*0.9], 'k', ms=10, lw=2, alpha=0.0)
v = [(0, h*0.1), (w, h*0.1), (0, h*0.9), (w, h*0.9),
     (w*0.1, 0), (w*0.1, h), (w*0.9, 0), (w*0.9, h)]
c = [1, 2, 1, 2, 1, 2, 1, 2]
p = path.Path(v, c)
patch = patches.PathPatch(p, fill=None, alpha=0.5)
patch2 = ax[1].add_patch(patch)
ax[1].set_xlim([0, 400])
ax[1].set_ylim([0, 400])

plugins.connect(fig, LinkedDragPlugin([pt1[0], pt2[0]], [line1[0], line2[0]], [patch1, patch2]))
mpld3.show()

When I examine the ptsobj, ptsobj2, lineobj, lineobj2, patchobj, and patchobj2, I see that

ptsobj = Marker
ptsobj2 = Marker
lineobj = Line2D
lineobj2 = null
patchobj = Patch
patchobj2 = Patch

So there are issues to be worked out...

JasonMArcher
  • 14,195
  • 22
  • 56
  • 52
sjp14051
  • 99
  • 9

1 Answers1

1

My guess is that you named your file DragPlugin.py and used import DragPlugin at the top of your script. Try doing from DragPlugin import DragPlugin instead

jakevdp
  • 77,104
  • 11
  • 125
  • 160
  • Eventually I got it to work; I think it required restarting the Kernel (or something, I forget). However, I later noticed something else about the plugins. I was using variants of LinkedViewPlugin (LinkedView1 and LinkedView2) to connect three panels, so that mouseover in panel 1 updates behavior in the other two. I noticed that I can link 1 and 2 or 1 and 3 but not both. Is this correct and is there a way around it? Thanks. – sjp14051 Aug 19 '14 at 20:41
  • You can certainly link multiple panels: the linked view plugin takes a reference to the objects you'd like to update. You could use the same strategy to update an arbitrary number of objects with suitable modifications to the Python and Javascript. – jakevdp Aug 20 '14 at 17:32
  • I'm not sure what I am doing wrong, but this is not what I observe. Please look at the code (edited above) that I tested. This is based on LinkedViewPlugin in custom_plugin.py. I copied and modified the appropriate class and linked it as I should (I think). But the second plugin.connect overrides the first, and I see updates occurring in only one of the panels. I also tried plotting the circles in the middle panel twice, thinking that one set would control the upper panel and the other set would control the lower panel. It didn't work. Thanks. – sjp14051 Aug 20 '14 at 22:21
  • Ah - when you use the javascript ``on("mouseover", ...)``, it overwrites any previous mouseover behavior for those elements. So when you connect the second plugin, it will overwrite the behavior of the first. If you want a single mouseover to affect multiple plots, you could write a new (single) plugin which does this. – jakevdp Aug 20 '14 at 22:42
  • Yes, that makes sense. I'll give that a try next. Thanks for the pointer. – sjp14051 Aug 21 '14 at 00:35
  • Well, I tried this. It didn't quite go as expected. Here I created two axes and created vline/hline in each with points at the intersection. The idea was to control I doubled up on every js function to differentiate mousedrag in each panel. When I inspect the pts, line, patch objects, I find the following: pts = null for both, line = Line object and null, patch = both correct patches. I suspect that some of the variables may be clashing. Can you point to where this may be happening? Thanks. – sjp14051 Aug 23 '14 at 02:31
  • The latest edit had a mistake. Inside __init__, I should have used points[0] rather than points because I am now using an array. Ptsobj and ptsobj2 are now alive but lineobj2 is still null. – sjp14051 Aug 23 '14 at 02:51
  • I got it to work by replacing `line2` as described above. This must have something to do with how things are layered, with `patch2` sitting on top of `line2`. It's a bit confusing but I am glad to finally have it working. – sjp14051 Aug 23 '14 at 02:59