!date
Sat Jan 11 14:34:18 PST 2014
mpld3 creater Jake Vanderplas recently developed a plugin mechanism to permit adding custom interactivity to mpld3 plots. I had a little time to give it a try, and put together something I've always wanted: Cartesian fisheye distortion for scatter plots.
%pylab
import matplotlib.pyplot as plt, mpld3, mpld3.plugins
mpld3.enable_notebook(d3_url="/files/d3.v3.js")
Using matplotlib backend: module://IPython.kernel.zmq.pylab.backend_inline Populating the interactive namespace from numpy and matplotlib
fig = plt.figure()
xx = round_(randn(100), 2)
yy = round_(randn(100), 2)
scatter = plot(xx, yy, 's', color='k', mec='grey', mew=3)
fig.plugins = [mpld3.plugins.PointLabelTooltip(scatter[0]), FishEye()]
from mpld3.plugins import PluginBase
import jinja2
import json
class FishEye(PluginBase):
"""A interactive fish eye distortion plugin"""
HTML = jinja2.Template("""
<script src="files/fisheye.js"></script>
""")
FIG_JS = jinja2.Template("""
var a = fig.axes[0],
xFisheye = d3.fisheye.scale(function() {return a.x;}).focus(360),
yFisheye = d3.fisheye.scale(function() {return a.y;}).focus(90);
fig.canvas.on("mousemove", function() {
var mouse = d3.mouse(this);
xFisheye.focus(mouse[0]);
yFisheye.focus(mouse[1]);
a.zoomed(true);
});
a.xdom = xFisheye;
a.xmap = xFisheye;
a.x = xFisheye;
a.ydom = yFisheye;
a.ymap = yFisheye;
a.y = yFisheye;
""")
def __init__(self):
self.id = self.generate_unique_id()
def _fig_js_args(self):
return dict(id=self.id)
In order to make the scales update properly, I had to tweak the AXIS_CLASS javascript as well. Maybe there is a better way to do this, though.
# monkey patch AXIS_CLASS to take scale from axes even if it has been changed
AXIS_CLASS = """
function Axis(axes, position, nticks, tickvalues, tickformat){
this.axes = axes;
this.position = position;
this.nticks = nticks;
this.tickvalues = tickvalues;
this.tickformat = tickformat;
if (position == "bottom"){
this.transform = "translate(0," + this.axes.height + ")";
this.scale = function() { return this.axes.xdom; }; // changed here, and 3 analogous spots below
this.class = "x axis";
}else if (position == "top"){
this.transform = "translate(0,0)"
this.scale = function() { return this.axes.xdom; }; // changed
this.class = "x axis";
}else if (position == "left"){
this.transform = "translate(0,0)";
this.scale = function() { return this.axes.ydom; }; // changed
this.class = "y axis";
}else{
this.transform = "translate(" + this.axes.width + ",0)";
this.scale = function() { return this.axes.ydom; }; // changed
this.class = "y axis";
}
}
Axis.prototype.draw = function(){
this.axis = d3.svg.axis()
.scale(this.scale()) // changed here, now scale is a function
.orient(this.position)
.ticks(this.nticks)
.tickValues(this.tickvalues)
.tickFormat(this.tickformat);
this.elem = this.axes.baseaxes.append('g')
.attr("transform", this.transform)
.attr("class", this.class)
.call(this.axis);
};
Axis.prototype.zoomed = function(){
gFig = this.axis;
this.axis.scale(this.scale());
this.elem.call(this.axis);
};
"""
mpld3._js.ALL_FUNCTIONS[2] = AXIS_CLASS
And to get it to work on nbviewer, I had to edit a few .js urls by hand. I tried to put that in the github gist for my own reference in the future.