act act - 1 month ago 10x
Javascript Question

D3: Using a closure to update a selection without re-binding data

I have lifted the following code from Mike Bostock's example of using D3 to create a line drawing tool.

This tool creates a drag behaviour that enables the user to draw a curve by dragging on the SVG canvas.

When the drag behaviour starts, the listener for this event captures the coordinates of the start of the behaviour and creates a new SVG path. This listener also attaches a new listener to the event that fires on the continuation of the drag behaviour; this listener (the "drag" listener) updates the SVG path. This occurs on the line of code beginning:

active.attr("d", line)

How does the "drag" listener update the SVG path without an additional line that re-binds the coordinate data (stored in the listener's closure as variable "d") to the SVG path?

By my understanding of how d3 works, this tool shouldn't work. Instead, one would have to change:
active.attr("d", line)
active.datum(d).attr("d", line)

Any help would be much appreciated!

var line = d3.line()

var svg ="svg")
.container(function() { return this; })
.subject(function() { var p = [d3.event.x, d3.event.y]; return [p, p]; })
.on("start", dragstarted));

function dragstarted() {
var d = d3.event.subject,
active = svg.append("path").datum(d),
x0 = d3.event.x,
y0 = d3.event.y;

d3.event.on("drag", function() {
var x1 = d3.event.x,
y1 = d3.event.y,
dx = x1 - x0,
dy = y1 - y0;

if (dx * dx + dy * dy > 100) d.push([x0 = x1, y0 = y1]);
else d[d.length - 1] = [x1, y1];
active.attr("d", line);

<!DOCTYPE html>
<meta charset="utf-8">

path {
fill: none;
stroke: #000;
stroke-width: 3px;
stroke-linejoin: round;
stroke-linecap: round;

<svg width="960" height="500">
<rect fill="#fff" width="100%" height="100%"></rect>
<script src="//"></script>

Update 1:
I've experimented with a basic closure structure (i.e. a user-initiated closure which returns a function similar to the "drag" event listener that the user can invoke through the console) but I cannot replicate the way the script works above. I'm completely flummoxed.


In this example there is no update to the data that would require a re-bind. In fact, there is only one single datum which gets modified during the drag gesture. When the drag actually starts, the subject as specified by

.subject(function() { var p = [d3.event.x, d3.event.y]; return [p, p]; })

is captured and stored in variable d:

var d = d3.event.subject,

As can be seen from the above two lines, this will start off as an array containing two coordinate pairs defining the start and end points of the path, which are initially identical.

The next line binds this single datum/subject to the newly created path for this drag gesture:

active = svg.append("path").datum(d),

It is important to keep in mind, that the variable d contains a reference to the actual data bound to the path. When modifying d you are in fact modifying the datum which was bound beforehand. This happens because you are dealing with a reference.

That said, the next steps are pretty straightforward as during the gesture, the array of coordinates is kept up to date by adjusting coodinate values or pushing new values to the array. Again, this is actually modifying the datum bound to the path.

When finally setting the line generator line on the path's d attribute

active.attr("d", line);

the generator will be called implicitly passing in the new value of the datum bound and, thus, generate the path following the pointer's movement during the gesture.