The code here copies the mpld3 example for dragged points plugin, and adds a Python callback to the dragged circles. Five main things change:
redrawable
) to the points we want to update after the drag.It's hacky and not perfect, but possibly the best we can do at the time. Code that is different from the original is bookended with a // DIFFERENT
comment if multi-line, or else have // DIFFERENT
inline.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
import mpld3
# DIFFERENT
# This will be called from within the Javascript
def update_data(xprev, yprev, xnew, ynew):
"""Keep the overall mean the same."""
print "UPDATING..."
global x, y
L = len(x)
if L > 1:
for i in range(L):
if xprev == x[i] and yprev == y[i]:
dx = (xprev - xnew) / (L - 1)
dy = (yprev - ynew) / (L - 1)
x = [xnew if j == 1 else x[j] + dx for j in range(L)]
y = [ynew if j == 1 else y[j] + dy for j in range(L)]
new_data = [list(xy) for xy in zip(x, y)]
print "sending back new data:", new_data
return new_data
#break
# Otherwise no changes
print "sending back old data:", [list(z) for z in zip(x,y)]
return [list(z) for z in zip(x,y)]
# DIFFERENT
# Everything in this cell is new
javascript_response_handler = r"""
// Handle Python output:
update_targets = function(out, ax){
console.log("UPDATE_TARGETS");
var res = null;
var output = null;
if ((out.content.user_expressions === undefined) ||
(out.content.user_expressions.output === undefined)) {
return;
}
output = out.content.user_expressions.output;
if ((output.status != "ok") || (output.data === undefined)) {
return;
}
res = output.data["text/plain"];
console.log(res);
if (res == undefined)
return;
new_positions = JSON.parse(res);
var redrawables = d3.selectAll("[name=redrawable]");
console.log("redrawables: " + redrawables);
redrawables.data(new_positions)
.attr("transform", function(d) {
var x = ax.x(d[0]);
var y = ax.y(d[1]);
return "translate(" + [x, y] + ")";
});
};
"""
# Everything in this cell is new
javascript_callback_invoker = r"""
// Call the Python kernel from within the notebook:
request_update = function(x0, y0, x1, y1, ax){
console.log("REQUEST UPDATE");
var kernel = IPython.notebook.kernel;
custom_updater = function(python_response) {
update_targets(python_response, ax);
};
var callbacks = {shell: {reply: custom_updater}};
if (!kernel) return;
args = x0 + "," + y0 + "," + x1 + "," + y1;
var msg_id = kernel.execute(
"", callbacks, {user_expressions:{output: "update_data(" + args + ")"}});
};
"""
##
# This is the example from
# http://mpld3.github.io/examples/drag_points.html.
#
# Three things change:
# (1) There's extra Javascript code to communicate with the IPython kernel.
# (2) We add an extra unique class (`redrawable`) to the points we want
# to update after the drag.
# (3) We save the start position in dragstarted().
# (4) We call the extra javascript code in dragended();
#
class DragPlugin(mpld3.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")
.attr("name", "redrawable") // DIFFERENT
.call(drag);
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("dragging", true);
// DIFFERENT. (This is crude, sorry.)
d[2] = d[0];
d[3] = d[1];
// DIFFERENT
}
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) {
d3.select(this).classed("dragging", false);
// DIFFERENT. (Below)
// Pass the axis object (obj.ax) too so we can transform
// from screen to data coordinates and back.
request_update(d[2], d[3], d[0], d[1], obj.ax);
}
};
""" + javascript_response_handler + javascript_callback_invoker
def __init__(self, points):
if isinstance(points, mpl.lines.Line2D):
suffix = "pts"
else:
suffix = None
self.dict_ = {"type": "drag",
"id": mpld3.utils.get_id(points, suffix)}
fig, ax = plt.subplots()
np.random.seed(0)
x = np.random.normal(size=5)
y = np.random.normal(size=5)
points = ax.plot(x, y, 'or', alpha=0.5,
markersize=50, markeredgewidth=1)
ax.set_title("Click and Drag with Callback: Jupyter + IPython + mpld3", fontsize=18)
mpld3.plugins.connect(fig, DragPlugin(points[0]))
mpld3.display()
The code works for versions below IPython 2 and below, then commenters' posts work for IPython 2 notebooks and IPython 3 notebooks. This code works for Juptyer 4 notebooks.
Everywhere where something is different from the original is bookended with a
// DIFFERENT
comment if multi-line, or else have // DIFFERENT
inline.
from math import pi, sin
from IPython.core.display import HTML
# Add an input form similar to what we saw above
input_form = """
<div style="background-color:gainsboro; border:solid black; width:600px; padding:20px;">
Code: <input type="text" id="code_input" size="50" height="2" value="sin(pi / 2)"><br>
Result: <input type="text" id="result_output" size="50" value="1.0"><br>
<button onclick="exec_code()">Execute</button>
</div>
"""
# here the javascript has a function to execute the code
# within the input box, and a callback to handle the output.
javascript = """
<script type="text/Javascript">
function handle_output(out){ // DIFFERENT
var res = null;
// DIFFERENT
var output = null;
if ((out.content.user_expressions !== undefined) &&
(out.content.user_expressions.output !== undefined)) {
output = out.content.user_expressions.output;
}
if ((output.status == "ok") && (output.data !== undefined)) {
res = output.data["text/plain"];
}
// DIFFERENT
console.log(res);
document.getElementById("result_output").value = res;
};
function exec_code(){
var code_input = document.getElementById('code_input').value;
var kernel = Jupyter.notebook.kernel; // DIFFERENT (IPython deprecated)
var callbacks = {shell : {reply : new_handle_output}}; // DIFFERENT
document.getElementById("result_output").value = ""; // clear output box
// DIFFERENT
var msg_id = kernel.execute(
"", // DIFFERENT (For return value, put the function call in user_expressions.)
callbacks,
{user_expressions:{output: "wrapper(" + code_input + ")"}});
// DIFFERENT
console.log("button pressed");
}
</script>
"""
def wrapper(code):
print "in the wrapper"
result = code
return "it's ...{}...".format(result)
HTML(input_form + javascript)