Skip to content

Instantly share code, notes, and snippets.

@dpopowich
Last active October 5, 2015 13:55
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dpopowich/11681070e5d5f925fd80 to your computer and use it in GitHub Desktop.
Save dpopowich/11681070e5d5f925fd80 to your computer and use it in GitHub Desktop.
D3: Getting Interactive With Your SVG Elements

This single page app is a tutorial demonstrating user interaction with SVG using D3. It is part of a blog post written for Art & Logic, Inc..

Of particular note, if viewing this on bl.ocks.org, it will not format well in the default iframe view...it is designed to be viewed raw, in a modern browser.

/*jslint browser: true, indent: 3, white: true */
"use strict";
function demo_functions(d3, WIDTH, HEIGHT, el_widgets,
disable_run_again, enable_run_again) {
/* Each slide declares a function with a data-function
* attribute. The string value must match a property in
* this object. When user navigates to a slide, the
* associated function is executed.
*/
return {
intro: function() {
/* Introduction */
// each function will start with assigning a D3 selection to
// the single SVG element on the page to a variable named
// svg.
var svg = d3.select('svg');
// create a red circle to the left of the canvas
svg.append('circle')
.attr({cx: 75, cy: 200, r: 50, fill: 'red'});
},
animate: function() {
/* Animate Elements */
var svg = d3.select('svg'), // svg selection
x = 75, y = HEIGHT/2, // x, y coord
r = 50, // radius
padding = r+25, // some padding from the edge
circle; // our red circle
// disable the run again button
disable_run_again();
// create the circle
circle = svg.append('circle')
.attr({cx: x, cy: y, r: r, fill: 'red'});
function shift_right() {
/* function to shift circle 10px to the right */
x += 10; // shift x coor. to the right
// assign the new center-x coor.
circle.attr('cx', x);
// if we haven't reached as far as we're going to go,
// reschedule another shift to the right
if (x < WIDTH-padding) {
setTimeout(shift_right, 25);
} else {
// we reached the end...enable the run again button.
enable_run_again();
}
}
// schedule the first call to shift the circle 1 sec from now
setTimeout(shift_right, 1000);
},
formula: function() {
/* Animate by Formula */
var svg = d3.select('svg'), // svg selection
R = 150, // radius of outer circle
CX = 375, // center-X
CY = 200, // center-Y
radians = 0; // starting point of red circle
// disable the run again button
disable_run_again();
// create the outer circle - the one the red circle
// circumnavigates
svg.append('circle')
.attr({cx: CX, cy: CY, r: R, fill: 'none', stroke: '#ccc'});
function draw() {
/* draw/move the red circle along the circumference */
// the update selection of the red circle. Note how we
// use a class to distinguish it from the other circle.
var update = svg.selectAll('circle.on-cir');
// bind data (our single radian value
update.data([radians])
.enter() // enter selection for when it
// doesn't exist...
.append('circle') // ...add the red circle
.classed('on-cir', true) // class it so it will be found
// next iteration
.attr({cx: CX+R, cy: CY, r: 50, opacity:0.7, fill: 'red'});
// set the x,y coord using trig to locate the red circle
// so many radians around the outer circle
update
.attr({
cx: function(rad) {
return CX + R*Math.cos(rad);
},
cy: function(rad) {
return CY - R*Math.sin(rad);
}
});
// if we haven't gone 2pi around, bump the radians and
// reschedule
if (radians < 2*Math.PI) {
radians += 0.1;
setTimeout(draw, 50);
} else {
// we reached the end...enable the run again button.
enable_run_again();
}
}
// schedule the first call
draw();
},
formula2: function() {
/* The Joy of Data Binding */
var svg = d3.select('svg'), // svg selection
R = 150, // radius of outer circle
CX = 375, // center-X
CY = 200, // center-Y
// in this version, radians is an array
radians = [0, Math.PI/2, Math.PI, 1.5*Math.PI],
// and we having a length-matching array for colors
colors = ['red', 'blue', 'green', 'yellow'];
// disable the run again button
disable_run_again();
// create the outer circle - the one the red circle
// circumnavigates
svg.append('circle')
.attr({cx: CX, cy: CY, r: R, fill: 'none', stroke: '#ccc'});
function draw() {
/* draw/move the red circle along the circumference */
// the update selection of the red circle. Note how we
// use a class to distinguish it from the other circle.
var update = svg.selectAll('circle.on-cir');
// bind data (our single radian value
update.data(radians)
.enter() // enter selection for when it
// doesn't exist...
.append('circle') // ...add the red circle
.classed('on-cir', true) // class it so it will be found
// next iteration
.attr({cx: CX+R, cy: CY, r: 50, opacity:0.7,
fill: function(d, i) {
/*jslint unparam: true */
return colors[i];
}});
// set the x,y coord using trig to locate the red circle
// so many radians around the outer circle
update
.attr({
cx: function(rad) {
return CX + R*Math.cos(rad);
},
cy: function(rad) {
return CY - R*Math.sin(rad);
}
});
// if we haven't gone 2pi around, bump the radians and
// reschedule
if (radians[0] < 2*Math.PI) {
radians = radians.map(function(r) {
return r + 0.1;
});
setTimeout(draw, 50);
} else {
// we reached the end...enable the run again button.
enable_run_again();
}
}
// schedule the first call
draw();
},
dnd: function() {
/* Drag & Drop - The Hard Way */
var svg = d3.select('svg'), // svg selection
circle, // red circle
R = 50, // its radius
dragging = false; // state of dragging
// create the red circle
circle = svg.append('circle')
// giving it this class will change the cursor when
// hovering over it
.classed('moveable', true)
.attr({cx: 475, cy: 200, r: R, fill: 'red'});
// a blue rect - an obstacle
svg.append('rect')
.attr({x: 475, y: 150, width: 100, height: 100,
fill: 'blue', opacity: 0.8});
// set up broswer event handlers to manage drag & drop
circle
.on('mousedown', function() {
// on mousedown, set the dragging flag to true
dragging = true;
})
.on('mouseup', function() {
// on mouseup or mouseleave, dragging becomes false
dragging = false;
})
.on('mouseleave', function() {
// on mouseup or mouseleave, dragging becomes false
dragging = false;
})
.on('mousemove', function() {
// on mousemove, if dragging is true, move the circle
var mouse, x, y;
// dragging?
if (!dragging) {
return;
}
// we're dragging, get the mouse coordinates
mouse = d3.mouse(this);
x = mouse[0];
y = mouse[1];
// move the circle to the new x,y
circle.attr({cx: x, cy: y});
});
},
dnd2: function() {
/* Drag & Drop - The Hard Way (cont'd) */
var svg = d3.select('svg'), // svg selection
circle, // red circle
R = 50, // its radius
dragging = false; // state of dragging
// create the red circle
circle = svg.append('circle')
// giving it this class will change the cursor when
// hovering over it
.classed('moveable', true)
.attr({cx: 475, cy: 200, r: R, fill: 'red'});
// a blue rect - an obstacle
svg.append('rect')
.attr({x: 475, y: 150, width: 100, height: 100,
fill: 'blue', opacity: 0.8});
// set up broswer event handlers to manage drag & drop
circle
.on('mousedown', function() {
// on mousedown, set the dragging flag to true
dragging = true;
// pop to circle to the top by moving it to the last
// child
this.parentNode.appendChild(this);
})
.on('mouseup', function() {
// on mouseup or mouseleave, dragging becomes false
dragging = false;
})
.on('mouseleave', function() {
// on mouseup or mouseleave, dragging becomes false
dragging = false;
})
.on('mousemove', function() {
// on mousemove, if dragging is true, move the circle
var mouse, x, y;
// dragging?
if (!dragging) {
return;
}
// we're dragging, get the mouse coordinates
mouse = d3.mouse(this);
x = mouse[0];
y = mouse[1];
// move the circle to the new x,y, but only if it's
// within bounds
if (x >= R && x <= WIDTH-R
&& y >= R && y <= HEIGHT-R) {
circle.attr({cx: x, cy: y});
}
});
},
d3drag: function() {
/* The Easy Way: Using d3.behavior.drag() */
var svg = d3.select('svg'), // svg selection
circle, // red circle
R = 50, // its radius
cx = 475, cy = 200, // starting coord
drag; // d3 drag object
// create d3 drag object
drag = d3.behavior.drag()
.on('dragstart', function() {
// pop to circle to the top by moving it to the last
// child
this.parentNode.appendChild(this);
})
.on('drag', function() {
var e, x, y;
// instead of getting the mouse coord we use the event
// generated by d3, particularly, the change in x,y
e = d3.event;
x = cx + e.dx;
y = cy + e.dy;
// move the circle to the new x,y, but only if it's
// within bounds
if (x >= R && x <= WIDTH-R
&& y >= R && y <= HEIGHT-R) {
// change cx, cy only after we know it's within
// bounds...this gives smoother movement around the
// boundaries
cx = x, cy=y;
circle.attr({cx: cx, cy: cy});
}
});
// create the red circle
circle = svg.append('circle')
// giving it this class will change the cursor when
// hovering over it
.classed('moveable', true)
.attr({cx: cx, cy: cy, r: R, fill: 'red'})
.call(drag);
// a blue rect - an obstacle
svg.append('rect')
.attr({x: 475, y: 150, width: 100, height: 100,
fill: 'blue', opacity: 0.8});
},
legend: function() {
/* Interacting With a Legend */
var svg = d3.select('svg'), // svg selection
circle, // red circle
R = 50, // radius
cx = 300, cy = 200, // starting coord
xonly = false, // moving *only* in x?
yonly = false, // moving *only* in y?
drag; // d3 drag object
// create d3 drag object
drag = d3.behavior.drag()
.on('drag', function() {
var e = d3.event, x, y;
// adjust x if we're not y only
x = yonly ? cx : cx + e.dx;
// adjust y if we're not x only
y = xonly ? cy : cy + e.dy;
// move the circle to the new x,y, but only if it's
// within bounds
if (x >= R && x <= WIDTH-R
&& y >= R && y <= HEIGHT-R) {
cx = x, cy = y;
circle.attr({cx: cx, cy: cy});
}
});
// create the circle
circle = svg.append('circle')
.classed('moveable', true)
.attr({cx: cx, cy: cy, r: R, fill: 'red',
stroke: '#333', 'stroke-width': '2px'})
.call(drag);
// create checkboxes
['X-only', 'Y-only'].forEach(function(id) {
var label; // the label to hold the checkbox
// create label - we'll use the label text as the input id
// attribute value
label = el_widgets.append('label')
.attr('for', id);
// append the checkbox
label.append('input')
.attr({type: 'checkbox', id: id})
.on('change', function() {
/* on change event - when the user clicks on or off
* a checkbox */
var target = d3.event.target, // which checkbox?
checked = target.checked; // is it on or off?
// depending on which was changed, we set that one
// accordingly and force the other off (because to
// have both on would prevent any movement)
if (target.id === 'X-only') {
xonly = checked;
if (yonly) {
yonly = false;
d3.select('#Y-only').property('checked', false);
}
} else {
yonly = checked;
if (xonly) {
xonly = false;
d3.select('#X-only').property('checked', false);
}
}
});
// append label text
label.append('span').html(id);
}); // end of forEach(...);
// create radio selection
['red', 'blue', 'green', 'transparent'].forEach(function(color) {
var label; // the label to hold the radio
// create label - we'll use the label text as the input id
// attribute value
label = el_widgets.append('label')
.attr('for', color);
// first color -- apply some spacing
if (color === 'red') {
label.style('margin-left', '2em');
}
// append the radio buttons - initialize with red checked
label.append('input')
.attr({type: 'radio', id: color, name: 'color', value: color,
checked: color==='red' ? '' : null})
.on('change', function() {
// on radio button change, change the color of the
// circle to the selected button's value
circle.attr('fill', d3.event.target.value);
});
// append label text
label.append('span').html(color);
});
}
}; // end of return {...};
}
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8">
<title>D3: Getting Interactive With Your SVG Elements</title>
<link rel="stylesheet" type="text/css" href="./slides.css" />
<link rel="stylesheet" type="text/css" href="./prism.css" />
</head>
<body>
<div class="title">
<a href="http://artandlogic.com/2015/09/d3-getting-interactive-with-your-svg-elements/">D3: Getting Interactive With Your SVG Elements</a>
</div>
<section id="slide">
<div class="heading"></div>
<div id="demo">
<div id="widgets"></div>
<button id="execute">Run Again</button>
</div>
<div class="content"></div>
</section><section id="src">
<div class="heading">Source</div>
</section>
<!-- TEMPLATES -->
<script id="links" type="text/template"><div class="links">
<a data-nav="prev" class="<%=prev_hidden%>" href="<%=prev%>"><%=prev_title%> &larr;</a>
<a data-nav="next" class="<%=next_hidden%>" href="<%=next%>">&rarr; <%=next_title%></a>
</div></script>
</script>
<!-- /TEMPLATES -->
<!-- SLIDES -->
<script type="text/slide"
data-title="Introduction"
data-function="intro"
data-highlight-lines="6">
<p>
This tutorial will take you through several slides leading up
to demonstrating user interaction with a SVG canvas
using <a href="http://d3js.org/">D3</a>.
</p>
<p>
This tutorial is a single page app requiring viewing in a
modern browser (recent chrome or firefox, for best viewing
pleasure).
</p>
<p>
It is assumed you have some basic familiarity with D3. If
you're new to D3, I'd recommend you first read some
introductory material. I have written two other articles
about D3 you may find interesting:
<a href="http://artandlogic.com/2015/06/d3-data-driven-documents/">Meet
D3: Data Driven Documents</a>
and <a href="http://artandlogic.com/2015/06/d3-binding-data/">D3:
Binding Data</a>. (Each of those articles has links to other
D3 resources.) In this tutorial, it is assumed the reader has
a solid understanding of the information in those articles, or
articles like them.
</p>
<p>
Each slide in the tutorial will have the same format as this
slide:
</p>
<ol>
<li> The left pane will have a narative description of the
current topic. At top and bottom there will be
navigation links taking you to the next or previous
slides.
</li>
<li> In the upper right of the left pane will be a SVG
element. Here you will be able to see (and interact!)
with elements inside the SVG. Just below the SVG is a
button labeled <strong>Run Again</strong>. Depending on
the state of the chart, this button may be disabled by
the demo. Clicking this button will redraw the SVG
element and execute the function you see in the right
pane all over again.
</li>
<li> In the right pane is the javascript function run for each
slide. When a slide is loaded the following steps are
taken:
<ol>
<li> The text of the left pane is assembled along with the
navigation links.
</li>
<li> The javascript source in the right pane is
highlighted and inserted.
</li>
<li> The SVG element is inserted in the left pane.
</li>
<li> The javascript function you see in the right pane is
executed.
</li>
</ol>
Clicking the <strong>Run Again</strong> button does steps
3 and 4 over again.
</li>
</ol>
<p>
In this slide, there is no animation or interaction with the
chart. A red circle is drawn at point 75,200 with radius 50.
You'll notice that in line #6 (highlighted) we assign a
variable to the D3 selection of the SVG element on the page
and then use that variable to append our red circle. Each
page has exactly one SVG element and each function will start
with a line like #6.
</p>
<p>
Jumping right to user interactions would be daunting. We'll
get there, but first we'll explore moving elements around
programatically. Once we see what needs to happen to move
elements around the canvas, we can look at browser event
management to do user interactions.
</p>
<p>
We'll start with something very simple. In the next slide, the
red circle will be moved horizontally to the right.
</p>
</script>
<script type="text/slide"
data-title="Animate Elements"
data-function="animate"
data-highlight-lines="22">
<p>
Did it go by too quickly? Click <strong>Run Again</strong> to
see the simple animation again.
</p>
<p>
It's very intuitive: to move the circle element horizontally,
change the X-coordinate of the circle. There are no cirlce
object methods to call! Simply manipulate the
SVG <code>&lt;circle&gt;</code> element's <code>cx</code>
attribute and the SVG engine built into the browser will do
the rest!
</p>
<p>
At line #16 we define a function to shift the circle to the
right and to reschedule itself if the circle hasn't reached
its destination. The function is scheduled
with <code>setTimeout()</code> at line #35 and, internally,
with each invocation at line #27. All the magic happens at
line #22 (highlighted).
</p>
<p>
Note how the <strong>Run Again</strong> button is disabled at
line #10 and not enabled again
until <code>shift_right()</code> is about to exit for the last
time (line #30).
</p>
<p>
Speaking of those disable/enable functions...where did they
come from? There are six variables available to these
functions (assigned in a closure you don't see), five of which
are used in this function:
</p>
<ul>
<li>
<strong>d3</strong> - The global variable from the d3
package.
</li>
<li>
<strong>WIDTH</strong> - The width of the SVG element.
</li>
<li>
<strong>HEIGHT</strong> - The height of the SVG element.
</li>
<li>
<strong>disable_run_again()</strong> - A function to disable
the <strong>Run Again</strong> button.
</li>
<li>
<strong>enable_run_again()</strong> - A function to enable
the <strong>Run Again</strong> button.
</li>
<li>
The sixth variable, <strong>el_widgets</strong>, is not used
until the last slide and will be discussed there.
</li>
</ul>
<p>
In the next slide, we'll get more sophisticated and move our
circle according to a formula: tracing the circumference of a
circle.
</p>
</script>
<script type="text/slide"
data-title="Animate by Formula"
data-function="formula">
<p>
This is conceptually identical to the previous tutorial: we're
moving the circle according to a formula. In the previous
slide, it was a straight line. In this slide, a more complex
formula, moving the red circle around the circumference of
another circle.
</p>
<p>
There is one big difference, however. In the this slide, we
are using <a href="http://artandlogic.com/2015/06/d3-binding-data/">D3
data binding</a> and update/enter selections to create and
update the circle attributes.
</p>
<p>
The critical piece of data is declared in line
#7, <code>radians</code>, which measures the number of radians
the red circle has traveled around the circumference. In the
function, <code>draw()</code>, we do a data bind to a
single-element array holding the current radian value (line
#25). The first time <code>draw()</code> is called the update
selection will be empty, so the enter selection will be
non-empty and the circle is appended. On subsequent calls, the
update selection will be non-empty, so all that happens is the
cx,cy coordinates get updated (lines #36-42) using
trigonometry to place the circle so many radians around the
circumference.
</p>
<p>
Why use data binding? We could have, like in the previous
slide, created the circle outside of the scheduled moving
function, then inside the scheduled function, just update the
location of the red circle based on the current radian value.
Two reasons: 1) its idiomatic, but more importantly, 2) when
we DO have more than one object we want to manage, the power
of using data binding is unleashed upon our charts.
</p>
<p>
To demonstrate this power, we will look at this function again
with only a few line changes...
</p>
</script>
<script type="text/slide"
data-title="The Joy of Data Binding"
data-function="formula2"
data-highlight-lines="7-10,28,35-38,54-57">
<p>
This slide is nearly identical to the previous slide. The
differences are highlighted in the source:
</p>
<ul>
<li><strong>7-10</strong> - In this slide, <code>radians</code>
is an array of starting locations for each of our circles.
In addition, we use a matching (in length) array of colors,
one for each of our circles.
</li>
<li><strong>28</strong> - A minor difference,
since <code>radians</code> is an array, we can use it
directly, instead of wrapping it in an array literal.
</li>
<li><strong>35-38</strong> - Instead of assigning the color as
a static value, we use a function to grab a color. Here we
use the index of the current datum to fetch the
corresponding color in <code>colors</code>.
</li>
<li><strong>54-57</strong> - We need to update all the radian
values, so we use <code>Array.prototype.map()</code>. As our
sentinel, we use the first element (which started at 0rad)
to see if it has gone all the way around the circumference.
</li>
</ul>
<p>
Consider this: we could easily rewrite the previous slide
using this function with only two changes, to lines #8 and
#10:
</p>
<pre><code>
// in this version, radians is an array
radians = [0],
// and we having a length-matching array for colors
colors = ['red'];
</code></pre>
<p>
Which begs the question: why not always use data binding, even
when you only have one data item? You never know when one will
become many!
</p>
<hr/>
<p>
So far, we have seen only computer generated animation. What
about human interaction? Now that we see moving items is a
simple matter of updating SVG element locaton attributes, we
can use browser events to help us manage element movement...
</p>
</script>
<script type="text/slide"
data-title="Drag & Drop - The Hard Way"
data-function="dnd">
<p>
In the chart, we have a red circle that is partially hidden by
a blue square (which has some opacity, so we can see the
circle underneath it). The blue square cannot be moved (it's
an obstacle), but the red circle can: hover the cursor over
the red circle and the cursor will change, indicating it can
be moved. Note how the cursor does not change when it's over
the blue square, even the portion of the square that has the
circle beneath it. Click-hold on the circle and drag the
mouse to the left, pulling the circle out from behind the blue
square.
</p>
<p>
The first half of the function looks familiar: set up some
variables and then create our circle and rectangle. The rest
of the function is something we haven't seen, yet: we set up a
series of event listeners on our circle element:
</p>
<ul>
<li><strong>mousedown</strong> - when the mouse is clicked on
the circle, the <code>dragging</code> flag is set to true.
</li>
<li><strong>mouseup, mouseleave</strong> - when the mouse is
released, or the mouse leaves the circle, the flag is set to
false.
</li>
<li><strong>mousemove</strong> - when the mouse is moved while
over the circle AND the flag is true (which means the user
must be holding the mouse click), the circle will move along
with the mouse. Since the circle moves with the mouse,
the <strong>mouseleave</strong> event will not be fired
until you hit the edge of the canvas or cross paths with
that pesky blue square.
</li>
</ul>
<p>
Note line #44. The function <code>d3.mouse(container)</code>
returns an array of the x,y coordinates of the mouse relative
to the container (typically the SVG element or a G element).
Here we use the circle as the parameter in which case the
container holding the circle will be used. Using these
coordinates will place the center of the circle under the
mouse, regardless of where you first click on the mouse.
</p>
<p>
It works. Yay! But it's clunky, particularly how the browser
behaves when you get to the edges of the canvas. And that
dang blue square! Let's take care of those issues...
</p>
</script>
<script type="text/slide"
data-title="Drag & Drop - The Hard Way (cont'd)"
data-function="dnd2"
data-highlight-lines="25-27,51-54">
<p>
This version is nearly identical to the previous slide, with
only two changes:
</p>
<ul>
<li><strong>25-27</strong> - With SVG, elements that appear
later in the DOM are <em>on top</em> of earlier elements, i.e,
if two elements overlap, the element that was <em>last</em> in
the DOM will appear on top and by default, receive all browser
events. This is why the blue square is over the red circle
and why mouse events on the circle, when obscured by the blue
square, do not reach the circle, because the blue square comes
later in the DOM. Line #27 fixes this. When the user clicks
on any part of the circle, we raise the circle to the top by
placing it <em>last</em> among its parent's child nodes (a
little known feature of <code>appendChild()</code> is when the
node to be appended is already a child of the target node, it
is moved to the end of its child list).
</li>
<li><strong>51-54</strong> - We check the values of x,y to
make sure they will not place the circle beyond the
boundaries and only update the location if the circle will
remain fully within the canvas.
</li>
</ul>
<p>
This is better, but still klunky, particular at the edges of
the canvas. As we move the mouse beyond the boundaries, the
x,y are out of range, so we don't do an update, but that makes
the circle come to a full-stop. It's difficult to glide the
circle along the canvas edges.
</p>
<p>
To fix this, we need more information. We can acquire the
information we need (in particular, it would be nice to have
the <em>delta</em> in x,y from its previous location), but
this is difficult information to retrieve and there are
(shock!) browser differences to deal with. Fortunately, D3
provides its own mechanism for managing drag & drop which
fixes all of these issues. Why didn't we start there? I'm a
firm believer that understanding the base technology can only
help understand higher level tools. In other words, while the
tools you'll see next will simplify a lot of our code, it's
not magic! It's just helping us maintain mouse (and for touch
screeens, touch) events...
</p>
</script>
<script type="text/slide"
data-title="The Easy Way: Using d3.behavior.drag()"
data-function="d3drag">
<p>
D3
defines <a href="https://github.com/mbostock/d3/wiki/Behaviors">two
behaviors</a>, drag and zoom. Behaviors encapsulate a
collection of low-level user-interactions (such as we saw in
the previous slides) into higher level abstractons. This slide
demonstrates the drag behavior.
</p>
<p>
In line #10 we construct
a <a href="https://github.com/mbostock/d3/wiki/Drag-Behavior">drag
behavior</a>. The returned value is actually a function that
we can use to call on a D3 selection, or use as a parameter to
<a href="https://github.com/mbostock/d3/wiki/Selections#call">selection.call()</a>,
which is what we do in line #43. Drag behaviours have three
high-level events we can bind to the drag object:
</p>
<ul>
<li><strong>dragstart</strong> - when a drag gesture starts
(e.g, mousedown, touch).
</li>
<li><strong>drag</strong> - when the drag gesture moves.
</li>
<li><strong>dragend</strong> - when the drag gesture finishes
(e.g, mouseup)
</li>
</ul>
<p>
All the machinery we used in the prior slides to manage
dragging are now encapsulated in D3's implementation of the
drag behavior, freeing us to focus on what we <em>want</em> to happen
on these events, rather than on the <em>how</em>.
</p>
<p>
Note: these events are applied to the behavior, <em>not</em>
the selection!
</p>
<p>
We use two of the drag events, <code>dragstart</code>
and <code>drag</code>. In <code>dragstart</code> we raise the
circle to the top and in <code>drag</code> we calculate the
new center point of the circle, confirm it's within our
boundary and, if so, set the new values on the circle.
</p>
<p>
Another benefit of D3's drag event is that it exposes deltas
of the x,y coordinates, relative to its position at the
beginning of the gesture. We use these in lines #22-23. The
benefits of using the deltas, as opposed to the raw x,y values,
as we did in the previous slides, is it permits an overall
smoother user-experience: the circle will now glide along the
edges and when a user first clicks on the circle, it doesn't
snap into place on the new center point.
</p>
</script>
<script type="text/slide"
data-title="Interacting With a Legend"
data-function="legend"
data-highlight-lines="16-19,54-69,93-96">
<p>
In our last slide, we'll look at how we can interact with our
chart using a legend.
<p>
<p>
The <a href="?s=1">second slide</a> listed a number of
variables that are available to the slide functions from a
closure, one of which we haven't seen until
now: <code>el_widgets</code>. This is a D3 selection of a DIV
just below the SVG element. The function inserts into the DIV
two checkboxes to limit movement, either in the x-direction or
the y-direction, and three radio buttons to change the color
of the circle. Clicking on these input elements will affect
change on the chart.
</p>
<p>
Changing the color is quite simple and intuitive: set up a
radio button group and when one is clicked, change
the <code>fill</code> attribute of the circle to the value of
the newly selected value. You can see this in lines #93-96
(highlighted).
</p>
<p>
Limiting movement is a little more involved. At the top of
the function we set up two boolean
variables, <code>xonly</code> and <code>yonly</code>, both
initially <code>false</code>, to track the current state
established by the checkboxes. In the <code>drag</code>
event, lines #16-19 (higlighted), we adjust the center point
values based on the truthiness of <code>xonly</code>
and <code>yonly</code>. The rest of the functionality is
enabled by managing the state in the <code>change</code>
event, lines #54-69 (highlighted).
</p>
<hr>
<h2>Fini!</h2>
<p>
Comments can be left on
the <a href="http://artandlogic.com/2015/09/d3-getting-interactive-with-your-svg-elements/">Art
&amp; Logic blog post</a> introducing this tutorial.
</p>
<p>
This will be a good starting point for the next planned
tutorial in this series of articles, where we will explore
managing time series data in a chart we can interact with,
both on the chart, and with a legend that affects behavior
change.
</p>
<p>
<a href="http://artandlogic.com/tag/d3/">Stay tuned</a>!
</p>
</script>
<!-- /SLIDES -->
<!-- ------------------------------------------------------------ -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.js"
charset="utf-8">
</script>
<script src="./prism.js" data-manual></script>
<script src="./demo_functions.js"></script>
<script src="./slides.js"></script>
<script>init_slides(d3, Prism, demo_functions);</script>
</body>
</html>
/* http://prismjs.com/download.html?themes=prism&languages=clike+javascript&plugins=line-highlight+line-numbers */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
code[class*="language-"]::selection, code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #a67f59;
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
pre[data-line] {
position: relative;
padding: 1em 0 1em 3em;
}
.line-highlight {
position: absolute;
left: 0;
right: 0;
padding: inherit 0;
margin-top: 1em; /* Same as .prism’s padding-top */
background: hsla(24, 20%, 50%,.08);
background: -moz-linear-gradient(left, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0));
background: -webkit-linear-gradient(left, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0));
background: -o-linear-gradient(left, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0));
background: linear-gradient(left, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0));
pointer-events: none;
line-height: inherit;
white-space: pre;
}
.line-highlight:before,
.line-highlight[data-end]:after {
content: attr(data-start);
position: absolute;
top: .4em;
left: .6em;
min-width: 1em;
padding: 0 .5em;
background-color: hsla(24, 20%, 50%,.4);
color: hsl(24, 20%, 95%);
font: bold 65%/1.5 sans-serif;
text-align: center;
vertical-align: .3em;
border-radius: 999px;
text-shadow: none;
box-shadow: 0 1px white;
}
.line-highlight[data-end]:after {
content: attr(data-end);
top: auto;
bottom: .4em;
}
pre.line-numbers {
position: relative;
padding-left: 3.8em;
counter-reset: linenumber;
}
pre.line-numbers > code {
position: relative;
}
.line-numbers .line-numbers-rows {
position: absolute;
pointer-events: none;
top: 0;
font-size: 100%;
left: -3.8em;
width: 3em; /* works for line-numbers below 1000 lines */
letter-spacing: -1px;
border-right: 1px solid #999;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.line-numbers-rows > span {
pointer-events: none;
display: block;
counter-increment: linenumber;
}
.line-numbers-rows > span:before {
content: counter(linenumber);
color: #999;
display: block;
padding-right: 0.8em;
text-align: right;
}
/* http://prismjs.com/download.html?themes=prism&languages=clike+javascript&plugins=line-highlight+line-numbers */
var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(){var e=/\blang(?:uage)?-(?!\*)(\w+)\b/i,t=_self.Prism={util:{encode:function(e){return e instanceof n?new n(e.type,t.util.encode(e.content),e.alias):"Array"===t.util.type(e)?e.map(t.util.encode):e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/\u00a0/g," ")},type:function(e){return Object.prototype.toString.call(e).match(/\[object (\w+)\]/)[1]},clone:function(e){var n=t.util.type(e);switch(n){case"Object":var a={};for(var r in e)e.hasOwnProperty(r)&&(a[r]=t.util.clone(e[r]));return a;case"Array":return e.map&&e.map(function(e){return t.util.clone(e)})}return e}},languages:{extend:function(e,n){var a=t.util.clone(t.languages[e]);for(var r in n)a[r]=n[r];return a},insertBefore:function(e,n,a,r){r=r||t.languages;var i=r[e];if(2==arguments.length){a=arguments[1];for(var l in a)a.hasOwnProperty(l)&&(i[l]=a[l]);return i}var o={};for(var s in i)if(i.hasOwnProperty(s)){if(s==n)for(var l in a)a.hasOwnProperty(l)&&(o[l]=a[l]);o[s]=i[s]}return t.languages.DFS(t.languages,function(t,n){n===r[e]&&t!=e&&(this[t]=o)}),r[e]=o},DFS:function(e,n,a){for(var r in e)e.hasOwnProperty(r)&&(n.call(e,r,e[r],a||r),"Object"===t.util.type(e[r])?t.languages.DFS(e[r],n):"Array"===t.util.type(e[r])&&t.languages.DFS(e[r],n,r))}},plugins:{},highlightAll:function(e,n){for(var a,r=document.querySelectorAll('code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'),i=0;a=r[i++];)t.highlightElement(a,e===!0,n)},highlightElement:function(a,r,i){for(var l,o,s=a;s&&!e.test(s.className);)s=s.parentNode;s&&(l=(s.className.match(e)||[,""])[1],o=t.languages[l]),a.className=a.className.replace(e,"").replace(/\s+/g," ")+" language-"+l,s=a.parentNode,/pre/i.test(s.nodeName)&&(s.className=s.className.replace(e,"").replace(/\s+/g," ")+" language-"+l);var u=a.textContent,g={element:a,language:l,grammar:o,code:u};if(!u||!o)return t.hooks.run("complete",g),void 0;if(t.hooks.run("before-highlight",g),r&&_self.Worker){var c=new Worker(t.filename);c.onmessage=function(e){g.highlightedCode=n.stringify(JSON.parse(e.data),l),t.hooks.run("before-insert",g),g.element.innerHTML=g.highlightedCode,i&&i.call(g.element),t.hooks.run("after-highlight",g),t.hooks.run("complete",g)},c.postMessage(JSON.stringify({language:g.language,code:g.code,immediateClose:!0}))}else g.highlightedCode=t.highlight(g.code,g.grammar,g.language),t.hooks.run("before-insert",g),g.element.innerHTML=g.highlightedCode,i&&i.call(a),t.hooks.run("after-highlight",g),t.hooks.run("complete",g)},highlight:function(e,a,r){var i=t.tokenize(e,a);return n.stringify(t.util.encode(i),r)},tokenize:function(e,n){var a=t.Token,r=[e],i=n.rest;if(i){for(var l in i)n[l]=i[l];delete n.rest}e:for(var l in n)if(n.hasOwnProperty(l)&&n[l]){var o=n[l];o="Array"===t.util.type(o)?o:[o];for(var s=0;s<o.length;++s){var u=o[s],g=u.inside,c=!!u.lookbehind,f=0,h=u.alias;u=u.pattern||u;for(var p=0;p<r.length;p++){var d=r[p];if(r.length>e.length)break e;if(!(d instanceof a)){u.lastIndex=0;var m=u.exec(d);if(m){c&&(f=m[1].length);var y=m.index-1+f,m=m[0].slice(f),v=m.length,k=y+v,b=d.slice(0,y+1),w=d.slice(k+1),N=[p,1];b&&N.push(b);var O=new a(l,g?t.tokenize(m,g):m,h);N.push(O),w&&N.push(w),Array.prototype.splice.apply(r,N)}}}}}return r},hooks:{all:{},add:function(e,n){var a=t.hooks.all;a[e]=a[e]||[],a[e].push(n)},run:function(e,n){var a=t.hooks.all[e];if(a&&a.length)for(var r,i=0;r=a[i++];)r(n)}}},n=t.Token=function(e,t,n){this.type=e,this.content=t,this.alias=n};if(n.stringify=function(e,a,r){if("string"==typeof e)return e;if("Array"===t.util.type(e))return e.map(function(t){return n.stringify(t,a,e)}).join("");var i={type:e.type,content:n.stringify(e.content,a,r),tag:"span",classes:["token",e.type],attributes:{},language:a,parent:r};if("comment"==i.type&&(i.attributes.spellcheck="true"),e.alias){var l="Array"===t.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(i.classes,l)}t.hooks.run("wrap",i);var o="";for(var s in i.attributes)o+=(o?" ":"")+s+'="'+(i.attributes[s]||"")+'"';return"<"+i.tag+' class="'+i.classes.join(" ")+'" '+o+">"+i.content+"</"+i.tag+">"},!_self.document)return _self.addEventListener?(_self.addEventListener("message",function(e){var n=JSON.parse(e.data),a=n.language,r=n.code,i=n.immediateClose;_self.postMessage(JSON.stringify(t.util.encode(t.tokenize(r,t.languages[a])))),i&&_self.close()},!1),_self.Prism):_self.Prism;var a=document.getElementsByTagName("script");return a=a[a.length-1],a&&(t.filename=a.src,document.addEventListener&&!a.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)),_self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism);
Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\w\W]*?\*\//,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:/(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(true|false)\b/,"function":/[a-z0-9_]+(?=\()/i,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/};
Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)\b/,number:/\b-?(0x[\dA-Fa-f]+|0b[01]+|0o[0-7]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|Infinity)\b/,"function":/[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*(?=\()/i}),Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\\\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0}}),Prism.languages.insertBefore("javascript","class-name",{"template-string":{pattern:/`(?:\\`|\\?[^`])*`/,inside:{interpolation:{pattern:/\$\{[^}]+\}/,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}}}),Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/<script[\w\W]*?>[\w\W]*?<\/script>/i,inside:{tag:{pattern:/<script[\w\W]*?>|<\/script>/i,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.javascript},alias:"language-javascript"}}),Prism.languages.js=Prism.languages.javascript;
!function(){function e(e,t){return Array.prototype.slice.call((t||document).querySelectorAll(e))}function t(e,t){return t=" "+t+" ",(" "+e.className+" ").replace(/[\n\t]/g," ").indexOf(t)>-1}function n(e,n,i){for(var o,l=n.replace(/\s+/g,"").split(","),a=+e.getAttribute("data-line-offset")||0,d=r()?parseInt:parseFloat,c=d(getComputedStyle(e).lineHeight),s=0;o=l[s++];){o=o.split("-");var u=+o[0],m=+o[1]||u,h=document.createElement("div");h.textContent=Array(m-u+2).join(" \n"),h.className=(i||"")+" line-highlight",t(e,"line-numbers")||(h.setAttribute("data-start",u),m>u&&h.setAttribute("data-end",m)),h.style.top=(u-a-1)*c+"px",t(e,"line-numbers")?e.appendChild(h):(e.querySelector("code")||e).appendChild(h)}}function i(){var t=location.hash.slice(1);e(".temporary.line-highlight").forEach(function(e){e.parentNode.removeChild(e)});var i=(t.match(/\.([\d,-]+)$/)||[,""])[1];if(i&&!document.getElementById(t)){var r=t.slice(0,t.lastIndexOf(".")),o=document.getElementById(r);o&&(o.hasAttribute("data-line")||o.setAttribute("data-line",""),n(o,i,"temporary "),document.querySelector(".temporary.line-highlight").scrollIntoView())}}if("undefined"!=typeof self&&self.Prism&&self.document&&document.querySelector){var r=function(){var e;return function(){if("undefined"==typeof e){var t=document.createElement("div");t.style.fontSize="13px",t.style.lineHeight="1.5",t.style.padding=0,t.style.border=0,t.innerHTML="&nbsp;<br />&nbsp;",document.body.appendChild(t),e=38===t.offsetHeight,document.body.removeChild(t)}return e}}(),o=0;Prism.hooks.add("complete",function(t){var r=t.element.parentNode,l=r&&r.getAttribute("data-line");r&&l&&/pre/i.test(r.nodeName)&&(clearTimeout(o),e(".line-highlight",r).forEach(function(e){e.parentNode.removeChild(e)}),n(r,l),o=setTimeout(i,1))}),addEventListener("hashchange",i)}}();
!function(){"undefined"!=typeof self&&self.Prism&&self.document&&Prism.hooks.add("complete",function(e){if(e.code){var t=e.element.parentNode,s=/\s*\bline-numbers\b\s*/;if(t&&/pre/i.test(t.nodeName)&&(s.test(t.className)||s.test(e.element.className))&&!e.element.querySelector(".line-numbers-rows")){s.test(e.element.className)&&(e.element.className=e.element.className.replace(s,"")),s.test(t.className)||(t.className+=" line-numbers");var n,a=e.code.match(/\n(?!$)/g),l=a?a.length+1:1,m=new Array(l+1);m=m.join("<span></span>"),n=document.createElement("span"),n.className="line-numbers-rows",n.innerHTML=m,t.hasAttribute("data-start")&&(t.style.counterReset="linenumber "+(parseInt(t.getAttribute("data-start"),10)-1)),e.element.appendChild(n)}}})}();
html,body {
height: 100%;
margin: 0;
padding: 0;
font-family: "Neue Helvetica", Helvetica, Arial, sans-serif;
font-size: 1em;
color: black;
background: white;
}
.title, .heading {
padding: 5px 0;
margin-bottom: 15px;
font-size: 30px;
font-weight: bold;
line-height: 1em;
text-align: center;
background: #81977F;
}
.heading {
position: fixed;
padding: 5px;
font-size: 24px;
text-align: left;
background: #ccc;
transform: translate(0, -40px);
}
.title a {
color: black;
text-decoration: none;
}
section {
display: inline-block;
margin-top: 40px;
border-right: 5px solid #81977F;
padding: 5px 15px;
height: calc(100% - 150px);
overflow: auto;
}
#demo {
float: right;
margin: 0 0 1em 1em;
text-align: center;
}
#slide {
width: calc(60% - 35px);
}
#slide code {
padding: 2px 5px;
background: #ddd;
}
#slide pre code {
background: inherit;
}
#src {
border-right: none;
width: calc(40% - 50px);
}
.hidden {
visibility: hidden;
}
.links {
margin: 10px 0;
clear: both;
}
.links a {
font-size: 1.2em;
}
.links a[data-nav=next] {
float: right;
}
svg {
display: block;
margin: 5px auto;
border: 1px solid black;
}
#execute {
margin-top: 5px;
}
.moveable {
cursor: move;
}
/*jslint browser: true, indent: 3, white: true, nomen: true */
function init_slides(d3, prism, function_generator) {
"use strict";
var slides = d3.selectAll('script[type="text/slide"]'),
el_slide = d3.select('#slide'),
el_demo = d3.select('#demo'),
el_src = d3.select('#src'),
el_execute = d3.select('#execute'),
el_widgets = d3.select('#widgets'),
LINKS_TMPL = d3.select('#links').html(),
WIDTH = 600, // svg width
HEIGHT = 400,// svg height
slide_func = null,
current = 0,
minslide = 0,
maxslide = slides.size()-1,
disable_run_again = function() {
/* disable the "Run Again" button */
el_execute.property('disabled', true);
},
enable_run_again = function() {
/* enable the "Run Again" button */
el_execute.property('disabled', false);
},
functions = function_generator(d3, WIDTH, HEIGHT, el_widgets,
disable_run_again, enable_run_again);
function verify() {
/* verify each slide has all required attributes and a defined
* function */
slides.each(function(d, i) {
/*jslint unparam: true */
var el = this,
funcname = el.getAttribute('data-function');
['data-title', 'data-function'].forEach(function(attr) {
if (!el.hasAttribute(attr)) {
console.error('Slide #' + (i+1) + ' does not have required attribute: ' + attr);
}
});
if (funcname && (functions[funcname] === undefined)) {
console.error('Slide #' + (i+1) + ' specified function is not defined: ' + funcname);
}
});
}
function execute() {
/* set up svg element and execute the function */
var widgetsNode;
// empty #widgets
widgetsNode = el_widgets.node();
while (widgetsNode.firstChild) {
widgetsNode.removeChild(widgetsNode.firstChild);
}
// remove/re-add <svg>
el_demo.select('svg').remove();
el_demo.insert('svg', '#widgets')
.attr({width: WIDTH, height: HEIGHT});
slide_func();
}
function go(slide) {
/* go to a slide (where: 0 <= slide < slides.size()) */
var tmpl, title, highlight, hidden, links, linksNode;
// confirm sanity
if (slide < minslide || slide > maxslide) {
console.error('Illegal slide: #' + slide);
current = 0;
}
// previous button
hidden = (slide === minslide);
links = LINKS_TMPL.replace(/<%=prev_hidden%>/g, hidden ? "hidden" : "");
if (!hidden) {
title = slides[0][slide-1].getAttribute('data-title');
links = links.replace(/<%=prev%>/g, slide-1);
links = links.replace(/<%=prev_title%>/g, title);
}
// next button
hidden = (slide === maxslide);
links = links.replace(/<%=next_hidden%>/g, hidden ? "hidden" : "");
if (!hidden) {
title = slides[0][slide+1].getAttribute('data-title');
links = links.replace(/<%=next%>/g, slide+1);
links = links.replace(/<%=next_title%>/g, title);
}
// the template
tmpl = d3.select(slides[0][slide]);
title = tmpl.attr('data-title');
highlight = tmpl.attr('data-highlight-lines');
// the slide function
slide_func = functions[tmpl.attr('data-function')];
// *** set the slide content ***
// remove any previous links
el_slide.selectAll('.links').remove();
// add title
el_slide.select('.heading').html(title);
// create node from links html
var linksNode = document.createElement('div');
linksNode.innerHTML = links;
linksNode = linksNode.firstChild;
// insert the links node before the SVG demo
el_slide.node().insertBefore(linksNode, el_demo.node());
// insert the content html
el_slide.select('.content').html(tmpl.html());
// append the links node, so it's at the bottom, too.
el_slide.node().appendChild(linksNode.cloneNode(true));
// *** set source and highlight it ***
// remove existing <pre>
el_src.select('pre').remove();
// append new, highlighted <pre><code>...</code></pre>
prism.highlightElement(
el_src.append('pre')
.classed('line-numbers', true)
.attr('data-line', highlight)
.append('code')
.classed('language-javascript', true)
.html(slide_func.toString()
.replace(/function\s*\(\)[^]*?\/\*/, 'function() { /*')
.replace(/^ {6}/gm, ''))
.node());
// execute function (which sets up SVG element)
execute();
}
function init() {
/* initialize events, etc. */
var s;
// clicking a nav link -- set up listener using delegation
d3.select('#slide')
.on('click', function() {
var e = d3.event, target = e.target;
// only interested in anchors with data-nav attribute
if (! (target.nodeName === 'A' && target.hasAttribute('data-nav'))) {
return;
}
// prevent following link
e.preventDefault();
// do the math to compute the next slide
if (target.getAttribute('data-nav') === 'prev') {
current -= 1;
} else {
current += 1;
}
// allow back/forward buttons to work
window.history.pushState({slide:current}, target.textContent, '?s=' + current);
// go to the new current slide
go(current);
});
window.addEventListener('popstate', function(e) {
var state = e.state;
// set the current slide from the state, defaulting to the
// first slide
current = (state && state.slide) || 0;
// go to the new current slide
go(current);
});
// location.search indicates slide??
// regex looking for ?s=SLIDE... or ...&s=SLIDE...
s = /[&?]s=([0-9]+)/.exec(location.search);
if (s !== null) {
// match found, the slide number will be the 2nd element of
// the returned array
current = + s[1];
}
// clicking Run Again button
el_execute.on('click', execute);
}
// verify each slide is valid
verify();
// initialize global event handlers
init();
// go to the current slide
go(current);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment