Skip to content

Instantly share code, notes, and snippets.

@dpopowich
Last active May 25, 2016 21:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dpopowich/cba9f20520f5164deec9f1d5dcba1a14 to your computer and use it in GitHub Desktop.
Save dpopowich/cba9f20520f5164deec9f1d5dcba1a14 to your computer and use it in GitHub Desktop.
Interactive SVG Charts With D3 - Part 1

This single page app is a tutorial demonstrating user interaction with a SVG chart 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";
/* Weekly closings of the Dow-Jones industrial average, July 1971 - August 1974
*
* array of tuples: (timestamp (milliseconds since epoch), value (dollars))
* From July, 1971 (W27) to August, 1974 (W31). It is sorted on the x-axis!
*
* source: https://datamarket.com/data/list/?q=provider:tsdl
*/
var DOWJONES_WEEKLY = [[47951999000.0, 890.19], [48556799000.0, 901.8], [49161599000.0, 888.51], [49766399000.0, 887.78], [50371199000.0, 858.43], [50975999000.0, 850.61], [51580799000.0, 856.02], [52185599000.0, 880.91], [52790399000.0, 908.15], [53395199000.0, 912.75], [53999999000.0, 911], [54604799000.0, 908.22], [55209599000.0, 889.31], [55814399000.0, 893.98], [56419199000.0, 893.91], [57023999000.0, 874.85], [57628799000.0, 852.37], [58233599000.0, 839], [58838399000.0, 840.39], [59443199000.0, 812.94], [60047999000.0, 810.67], [60652799000.0, 816.55], [61257599000.0, 859.59], [61862399000.0, 856.75], [62467199000.0, 873.8], [63071999000.0, 881.17], [63676799000.0, 890.2], [64281599000.0, 910.37], [64886399000.0, 906.68], [65491199000.0, 907.44], [66095999000.0, 906.38], [66700799000.0, 906.68], [67305599000.0, 917.59], [67910399000.0, 917.52], [68515199000.0, 922.79], [69119999000.0, 942.43], [69724799000.0, 939.87], [70329599000.0, 942.88], [70934399000.0, 942.28], [71539199000.0, 940.7], [72143999000.0, 962.6], [72748799000.0, 967.72], [73353599000.0, 963.8], [73958399000.0, 954.17], [74563199000.0, 941.23], [75167999000.0, 941.83], [75772799000.0, 961.54], [76377599000.0, 971.25], [76982399000.0, 961.39], [77587199000.0, 934.45], [78191999000.0, 945.06], [78796799000.0, 944.69], [79401599000.0, 929.03], [80006399000.0, 938.06], [80611199000.0, 922.26], [81215999000.0, 920.45], [81820799000.0, 926.7], [82425599000.0, 951.76], [83030399000.0, 964.18], [83635199000.0, 965.83], [84239999000.0, 959.36], [84844799000.0, 970.05], [85449599000.0, 961.24], [86054399000.0, 947.23], [86659199000.0, 943.03], [87263999000.0, 953.27], [87868799000.0, 945.36], [88473599000.0, 930.46], [89078399000.0, 942.81], [89683199000.0, 946.42], [90287999000.0, 984.12], [90892799000.0, 995.26], [91497599000.0, 1005.57], [92102399000.0, 1025.21], [92707199000.0, 1023.43], [93311999000.0, 1033.19], [93916799000.0, 1027.24], [94521599000.0, 1004.21], [95126399000.0, 1020.02], [95731199000.0, 1047.49], [96335999000.0, 1039.36], [96940799000.0, 1026.19], [97545599000.0, 1003.54], [98150399000.0, 980.81], [98755199000.0, 979.46], [99359999000.0, 979.23], [99964799000.0, 959.89], [100569599000.0, 961.32], [101174399000.0, 972.23], [101779199000.0, 963.05], [102383999000.0, 922.71], [102988799000.0, 951.01], [103593599000.0, 931.07], [104198399000.0, 959.36], [104803199000.0, 963.2], [105407999000.0, 922.19], [106012799000.0, 953.87], [106617599000.0, 927.89], [107222399000.0, 895.17], [107827199000.0, 930.84], [108431999000.0, 893.96], [109036799000.0, 920], [109641599000.0, 888.55], [110246399000.0, 879.82], [110851199000.0, 891.71], [111455999000.0, 870.11], [112060799000.0, 885.99], [112665599000.0, 910.9], [113270399000.0, 936.71], [113875199000.0, 908.87], [114479999000.0, 852.38], [115084799000.0, 871.84], [115689599000.0, 863.49], [116294399000.0, 887.57], [116899199000.0, 898.63], [117503999000.0, 886.36], [118108799000.0, 927.9], [118713599000.0, 947.1], [119318399000.0, 971.25], [119923199000.0, 978.63], [120527999000.0, 963.73], [121132799000.0, 987.06], [121737599000.0, 935.28], [122342399000.0, 908.42], [122947199000.0, 891.33], [123551999000.0, 854], [124156799000.0, 822.25], [124761599000.0, 838.05], [125366399000.0, 815.65], [125971199000.0, 818.73], [127180799000.0, 848.02], [127785599000.0, 880.23], [128390399000.0, 841.48], [128995199000.0, 855.47], [129599999000.0, 859.39], [130204799000.0, 843.94], [130809599000.0, 820.4], [131414399000.0, 820.32], [132019199000.0, 855.99], [132623999000.0, 851.92], [133228799000.0, 878.05], [133833599000.0, 887.83], [134438399000.0, 878.13], [135043199000.0, 846.68], [135647999000.0, 847.54], [136252799000.0, 844.81], [136857599000.0, 859.9], [137462399000.0, 834.64], [138067199000.0, 845.9], [138671999000.0, 850.44], [139276799000.0, 818.84], [139881599000.0, 816.65], [140486399000.0, 802.17], [141091199000.0, 853.72], [141695999000.0, 843.09], [142300799000.0, 815.39], [142905599000.0, 802.41], [143510399000.0, 791.77], [144115199000.0, 787.23], [144719999000.0, 787.94], [145324799000.0, 784.57], [145929599000.0, 752.58]];
function Chart(svg, width, height, data) {
var self = this,
dateFormatter = d3.time.format("%Y-%m-%d");
this.svg = svg;
this.PADDING = 50;
this.WIDTH = width;
this.HEIGHT = height;
this.data = data;
// the formatting function for the x-axis. See
// https://github.com/mbostock/d3/wiki/Time-Formatting#format_multi
this.xaxisFormatter = d3.time.format.utc.multi([
[".%L", function(d) { return d.getUTCMilliseconds(); }],
[":%S", function(d) { return d.getUTCSeconds(); }],
["%I:%M", function(d) { return d.getUTCMinutes(); }],
["%I %p", function(d) { return d.getUTCHours(); }],
//["%a %d", function(d) { return d.getUTCDay() && d.getUTCDate() !== 1; }],
["%m/%d", function(d) { return d.getUTCDate() !== 1; }],
["%b", function(d) { return d.getUTCMonth(); }],
["%Y", function() { return true; }]
]);
// min/max time (X-axis). The data is in sorted order, so we just
// grab the X value from the first and last elements
this.min_time = new Date(data[0][0]);
this.max_time = new Date(data[data.length-1][0]);
// calculate the min/max values
this.min_value = Infinity;
this.max_value = -Infinity;
data.map(function(xy) {
var y = xy[1];
if (y < self.min_value) {
self.min_value = y;
}
if (y > self.max_value) {
self.max_value = y;
}
});
// The domains for each axis
this.xdomain = [this.min_time, this.max_time];
this.ydomain = [this.min_value, this.max_value];
// x-scale, a UTC time scale
this.xscale = d3.time.scale.utc()
.domain(this.xdomain)
.range([0, this.WIDTH-this.PADDING]);
// y-scale
this.yscale = d3.scale.linear()
.domain(this.ydomain)
.range([this.HEIGHT-this.PADDING, 0]);
this.tip = d3.tip()
.attr('class', 'dot-tip')
.offset(function(d) {
var x = self.xscale(d[0]),
y = self.yscale(d[1]),
left = (x < 100) ? 100 : 0,
top = (y < 100) ? 20 : -10;
return [top, left];
})
.direction(function(d) {
return (self.yscale(d[1]) < 100) ? 's' : 'n';
})
.html(function(d) {
var strDate = "<strong>Date:</strong> " + dateFormatter(new Date(d[0])) + "</br></br>",
strValue = "<strong>Value:</strong> " + d[1] + "</br></br>";
return strDate + strValue;
});
}
Chart.prototype = {
_Zoom: function(g, chart) {
/* _Zoom(g, chart)
*
* create the d3 zoom behaviour and event callbacks
*
* g - the element to call the zoom function on
* chart - the Chart instance
*
*/
// a flag to turn off zooming
if (chart.zoomOff) {
return;
}
// create zoom for mouse events on main chart (i.e, mousewheel
// events will zoom in/out) and register event handlers
var zoom = d3.behavior.zoom()
// set the xscale for zooming in the X-coord
.x(chart.xscale)
// on zoom event, call the zoomed function
.on('zoom', zoomed);
// apply it to the passed in element
zoom(g);
function zoomed() {
/* manage zoom event */
var domain = chart.xscale.domain(), // the current domain
delta = domain[1] - domain[0]; // time delta in domain
// if before beginning of data, adjust to beginning with same
// time delta
if (domain[0] < chart.min_time) {
domain[0] = chart.min_time;
domain[1] = Math.min(+chart.min_time + delta, chart.max_time);
}
// if after end of data, adjust to end with same
// time delta
if (domain[1] > chart.max_time) {
domain[0] = Math.max(chart.min_time, +chart.max_time - delta);
domain[1] = chart.max_time;
}
// update the chart xscale domain
chart.xscale.domain(domain);
// when updating xscale, must also update zoom
zoom.x(chart.xscale);
// with new domain set, re-render
chart.Render();
}
},
_Clear: function() {
/* _Clear() */
// select all elements **inside** the SVG and remove them
this.svg.selectAll('*').remove();
},
_DrawLine: function() {
/* DrawLine() */
// reference to `this` used in closures
var self = this;
// create line function for use with PATH d attribute
var line = d3.svg.line()
.x(function(d) {
return self.xscale(d[0]);
})
.y(function(d) {
return self.yscale(d[1]);
});
// create a group to hold the path
var g = this.svg.append('g')
.data([this.data])
.call(this._Zoom, this);
// create a rectangle to collect the mouse events
g.append('rect')
.attr('width', this.WIDTH)
.attr('height', this.HEIGHT)
.style('fill', 'none')
.style('pointer-events', 'all');
// create the PATH element, using the line function above for
// the d attribute. Note the clip-path.
g.append('path')
.attr({fill: 'none',
stroke: 'blue',
d: line,
'clip-path': 'url(#clip)'});
// the clip-path for the PATH, so when we zoom and the line
// extends into the "margins" those portions of the line are
// clipped.
g.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", this.WIDTH-this.PADDING)
.attr("height", this.HEIGHT-this.PADDING);
return g;
},
_DrawXAxis: function() {
/* _DrawXAxis() */
// append G to hold x-axis
this.svg.append('g')
// give it a class attribute
.classed('axis', true)
// transform it, so it is placed within the padding at the bottom of the chart
.attr('transform', 'translate(0, ' + (this.HEIGHT - this.PADDING) + ')')
// use d3 axis function to generate the x-axis
.call(d3.svg.axis()
// uses the same scaling function as the PATH
.scale(this.xscale)
// see above for details about the formatter
.tickFormat(this.xaxisFormatter)
// orient for the bottom
.orient('bottom'));
},
_DrawYAxis: function() {
/* _DrawYAxis() */
// append G to hold y-axis
this.svg.append('g')
// give it a class attribute
.classed('axis', true)
// transform it, so it is placed within the padding at the right of the chart
.attr('transform', 'translate(' + (this.WIDTH - this.PADDING) + ')')
// use d3 axis function to generate the y-axis
.call(d3.svg.axis()
// uses the same scaling function as the PATH
.scale(this.yscale)
// orient for the right-hand side
.orient('right'));
},
_Tip: function(g) {
/* add tool-tips */
var self = this,
update = g.selectAll('circle').data(g.data()[0])
// append a circle for each point
update.enter()
.append('circle')
.attr('r', 5)
.attr('cx', function(d) {
return self.xscale(d[0]);
})
.attr('cy', function(d) {
return self.yscale(d[1]);
})
.attr('fill', 'blue')
.attr('opacity', 0)
.on('mouseenter', function(d) {
d3.select(this).attr('opacity', .5);
self.tip.show(d);
})
.on('mouseout', function(d) {
d3.select(this).attr('opacity', 0);
self.tip.hide(d);
});
g.call(this.tip);
},
Render: function() {
/* Render() */
// clear the svg canvas
this._Clear();
// draw X-axis
this._DrawXAxis();
// draw Y-axis
this._DrawYAxis();
// draw the line
this._DrawLine();
},
RenderP2: function() {
/* Render() */
// clear the svg canvas
this._Clear();
// draw X-axis
this._DrawXAxis();
// draw Y-axis
this._DrawYAxis();
// draw the line
this._Tip(this._DrawLine());
}
};
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.
*/
var __chart = new Chart(d3.select('svg'), WIDTH, HEIGHT, DOWJONES_WEEKLY);
return {
intro: function() {
/* Introduction */
// assign a D3 selection to the single SVG element on the
// page to a variable named svg.
var svg = d3.select('svg');
// create a new chart. WIDTH and HEIGHT are constants
// defined within scope. DOWJONES_WEEKLY is an array of
// two-element arrays, [timestamp, value], e.g:
//
// [[47951999000.0, 890.19], [48556799000.0, 901.8], ...]
var chart = new Chart(svg, WIDTH, HEIGHT, DOWJONES_WEEKLY);
// render the chart
chart.Render();
},
_render: __chart.Render,
_clear: __chart._Clear,
clear: function() {
var svg = d3.select('svg');
var chart = new Chart(svg, WIDTH, HEIGHT, DOWJONES_WEEKLY);
chart._Clear();
},
_drawline: __chart._DrawLine,
drawline: function() {
var svg = d3.select('svg');
var chart = new Chart(svg, WIDTH, HEIGHT, DOWJONES_WEEKLY);
chart.zoomOff = true;
chart._Clear();
chart._DrawLine();
},
_drawxaxis: __chart._DrawXAxis,
drawxaxis: function() {
var svg = d3.select('svg');
var chart = new Chart(svg, WIDTH, HEIGHT, DOWJONES_WEEKLY);
chart._Clear();
chart._DrawXAxis();
},
_drawyaxis: __chart._DrawYAxis,
drawyaxis: function() {
var svg = d3.select('svg');
var chart = new Chart(svg, WIDTH, HEIGHT, DOWJONES_WEEKLY);
chart._Clear();
chart._DrawYAxis();
},
_zoom: __chart._Zoom,
part2: function() {
var svg = d3.select('svg');
var chart = new Chart(svg, WIDTH, HEIGHT, DOWJONES_WEEKLY);
chart.Render = chart.RenderP2;
chart.Render();
}
}; // end of return {...};
}
week closing
1971-W27 890.19
1971-W28 901.80
1971-W29 888.51
1971-W30 887.78
1971-W31 858.43
1971-W32 850.61
1971-W33 856.02
1971-W34 880.91
1971-W35 908.15
1971-W36 912.75
1971-W37 911.00
1971-W38 908.22
1971-W39 889.31
1971-W40 893.98
1971-W41 893.91
1971-W42 874.85
1971-W43 852.37
1971-W44 839.00
1971-W45 840.39
1971-W46 812.94
1971-W47 810.67
1971-W48 816.55
1971-W49 859.59
1971-W50 856.75
1971-W51 873.80
1971-W52 881.17
1972-W01 890.20
1972-W02 910.37
1972-W03 906.68
1972-W04 907.44
1972-W05 906.38
1972-W06 906.68
1972-W07 917.59
1972-W08 917.52
1972-W09 922.79
1972-W10 942.43
1972-W11 939.87
1972-W12 942.88
1972-W13 942.28
1972-W14 940.70
1972-W15 962.60
1972-W16 967.72
1972-W17 963.80
1972-W18 954.17
1972-W19 941.23
1972-W20 941.83
1972-W21 961.54
1972-W22 971.25
1972-W23 961.39
1972-W24 934.45
1972-W25 945.06
1972-W26 944.69
1972-W27 929.03
1972-W28 938.06
1972-W29 922.26
1972-W30 920.45
1972-W31 926.70
1972-W32 951.76
1972-W33 964.18
1972-W34 965.83
1972-W35 959.36
1972-W36 970.05
1972-W37 961.24
1972-W38 947.23
1972-W39 943.03
1972-W40 953.27
1972-W41 945.36
1972-W42 930.46
1972-W43 942.81
1972-W44 946.42
1972-W45 984.12
1972-W46 995.26
1972-W47 1005.57
1972-W48 1025.21
1972-W49 1023.43
1972-W50 1033.19
1972-W51 1027.24
1972-W52 1004.21
1973-W01 1020.02
1973-W02 1047.49
1973-W03 1039.36
1973-W04 1026.19
1973-W05 1003.54
1973-W06 980.81
1973-W07 979.46
1973-W08 979.23
1973-W09 959.89
1973-W10 961.32
1973-W11 972.23
1973-W12 963.05
1973-W13 922.71
1973-W14 951.01
1973-W15 931.07
1973-W16 959.36
1973-W17 963.20
1973-W18 922.19
1973-W19 953.87
1973-W20 927.89
1973-W21 895.17
1973-W22 930.84
1973-W23 893.96
1973-W24 920.00
1973-W25 888.55
1973-W26 879.82
1973-W27 891.71
1973-W28 870.11
1973-W29 885.99
1973-W30 910.90
1973-W31 936.71
1973-W32 908.87
1973-W33 852.38
1973-W34 871.84
1973-W35 863.49
1973-W36 887.57
1973-W37 898.63
1973-W38 886.36
1973-W39 927.90
1973-W40 947.10
1973-W41 971.25
1973-W42 978.63
1973-W43 963.73
1973-W44 987.06
1973-W45 935.28
1973-W46 908.42
1973-W47 891.33
1973-W48 854.00
1973-W49 822.25
1973-W50 838.05
1973-W51 815.65
1973-W52 818.73
1974-W01 848.02
1974-W02 880.23
1974-W03 841.48
1974-W04 855.47
1974-W05 859.39
1974-W06 843.94
1974-W07 820.40
1974-W08 820.32
1974-W09 855.99
1974-W10 851.92
1974-W11 878.05
1974-W12 887.83
1974-W13 878.13
1974-W14 846.68
1974-W15 847.54
1974-W16 844.81
1974-W17 859.90
1974-W18 834.64
1974-W19 845.90
1974-W20 850.44
1974-W21 818.84
1974-W22 816.65
1974-W23 802.17
1974-W24 853.72
1974-W25 843.09
1974-W26 815.39
1974-W27 802.41
1974-W28 791.77
1974-W29 787.23
1974-W30 787.94
1974-W31 784.57
1974-W32 752.58
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8">
<title>Interactive SVG Charts With D3 – Part 1</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="https://artandlogic.com/2016/05/interactive-svg-charts-d3-part-1/">Interactive SVG Charts With D3 – Part 1</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="5,12,15">
<p>
This tutorial will take you through several slides
demonstrating how we build the interactive chart to the right,
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). The chart on the right demonstrates zooming which
<strong>requires a scroll-wheel mouse</strong>.
</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 three other articles
about D3 you may find interesting:
</p>
<ul>
<li> <a href="http://artandlogic.com/2015/06/d3-data-driven-documents/">Meet
D3: Data Driven Documents</a> - an overview of what D3
is and is not.
</li>
<li> <a href="http://artandlogic.com/2015/06/d3-binding-data/">D3:
Binding Data</a> - explores the heart and soul of D3: data
binding.
</li>
<li><a href="http://artandlogic.com/2015/09/d3-getting-interactive-with-your-svg-elements/">
D3: Getting Interactive With Your SVG Elements</a> -
explores user interaction capabilities with SVG.
</li>
</ul>
<p>
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>. 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.
</li>
</ol>
<p>
In this slide, we see the complete chart. It draws one line,
time series data of Dow Jones industrial averages, July 1971 -
August 1974. Placing your mouse pointer over the chart, then
scrolling the mouse wheel, you'll see the chart zoom-in or
zoom-out, centering the zoom action around your mouse pointer.
</p>
<p>
In <strong>line 5</strong> in the javascript on the right, we
use d3 to select the svg element from the page, then
in <strong>line 12</strong>, instantiate a new Chart, passing
in the svg selection, it's width and height, along with the
data, which is an array of two-element arrays: [timestamp,
value]. In <strong>line 15</strong>, we call the rendering
method on the chart.
</p>
<p>
The rest of this tutorial explores the Render() method.
</p>
</script>
<script type="text/slide"
data-title="The Render() method"
data-function="intro"
data-display-function="_render"
data-highlight-lines="4,7,10,13">
<p>
In the javascript on the right you will see we break down the
rendering of the chart into several steps:
</p>
<ul>
<li><strong>Line 4:</strong> clear the canvas of all elements.
This is important because as we interact with the chart, e.g,
zooming in, we need to redraw the canvas. If we don't clear
it first, ever-more elements will be created and appended to
the SVG element with each redraw. Clearing first gives an
empty canvas.
</li>
<li><strong>Line 7:</strong> this method draws the X-axis along the bottom of
the chart.
</li>
<li><strong>Line 10:</strong> this method draws the Y-axis
along the right side of the chart.
</li>
<li><strong>Line 13:</strong> this method does the heavy lifting, drawing our
line and setting up the zoom behavior.
</li>
</ul>
<p>
Next, we'll look at each of these methods, one at a time.
</p>
</script>
<script type="text/slide"
data-title="The _Clear() method"
data-function="clear"
data-display-function="_clear"
data-highlight-lines="">
<p>
The <code>_Clear()</code> function selects all children of the
svg element (<code>svg.selectAll('*')</code>) and removes them
from their parent element (the SVG), thus clearing the svg of
all its children.
</p>
</script>
<script type="text/slide"
data-title="The _DrawXAxis() method"
data-function="drawxaxis"
data-display-function="_drawxaxis"
data-highlight-lines="4,6,8,10,12,14,16">
<p>
The <code>_DrawXAxis()</code> function draws the X-axis at the
bottom of the chart using
d3's <a href="https://github.com/d3/d3/wiki/SVG-Axes#axis">axis
component</a>.
</p>
<p>
We begin by creating a G element (<strong>line 4</strong>) so
we have a container for all the elements that make up the
whole axis. (Use your browser's developer tools to inspect
the axis: you will see it is made up of several inner G
elements for each tick, which in turn contain the tick lines
and text of each tick, as well as a PATH element that draws
the main horizontal line.) This allows us to assign
attributes to the whole axis, such as a class for styling
(<strong>line 6</strong>) and a transformation to move the
axis to the bottom of the chart (<strong>line 8</strong>).
</p>
<p>
In <strong>line 10</strong> we use
d3's <a href="https://github.com/d3/d3/wiki/Selections#call">selection.call()</a>
function which takes a function as its first argument and
calls the function with the selection as its argument and
returns the selection as its value. This is a powerful tool
to allow chaining of calls and is shorthand for:
</p>
<pre>
<code>
var g = svg.append('g').attr(...).attr(...);
var axis = d3.svg.axis();
axis(g);
</code>
</pre>
<p>d3.svg.axis() returns a function that should be called on a
selection that contains a SVG or G element. It is designed to
work with one of d3's scale functions. In <strong>line
12</strong> we set the scale for the axis, specifying
the <a href="https://github.com/d3/d3/wiki/Time-Scales#utc">d3
scale function </a>defined in the chart's constructor:
</p>
<pre>
<code>
// x-scale, a UTC time scale
this.xscale = d3.time.scale.utc()
.domain([this.min_time, this.max_time])
.range([0, this.WIDTH - this.PADDING]);
</code>
</pre>
<p>
The <a href="https://github.com/d3/d3/wiki/Time-Scales#domain">domain</a>
is set to the computed min and max time of the input domain
and
the <a href="https://github.com/d3/d3/wiki/Time-Scales#range">range</a>
is set to the scale's output range, the configured WIDTH of
the chart, minus some padding (and the y-axis will sit in this
padding).
<p>
<p>In <strong>line 14</strong> we set the formatting of the
ticks (not discussed here, but you can look at the source of the
constructor for commentary. Finally, in <strong>line
16</strong> we set the orientation of the axis
to <em>bottom</em>, instructing the axis function in what
direction it should draw its ticks and where to place text.
</p>
</script>
<script type="text/slide"
data-title="The _DrawYAxis() method"
data-function="drawyaxis"
data-display-function="_drawyaxis"
data-highlight-lines="12,14">
<p>
The <code>_DrawYAxis()</code> function draws the Y-axis at the
right of the chart using
d3's <a href="https://github.com/d3/d3/wiki/SVG-Axes#axis">axis
component</a>.
</p>
<p>
This is nearly identical to _DrawXAxis(), discussed in the
previous slide, so please see that slide for details. The
only major difference is the setting of the scale
in <strong>line 12</strong>, using the y-scale function
defined in the constructor:
</p>
<pre style="clear:right">
<code>
// y-scale
this.yscale = d3.scale.linear()
.domain([this.min_value, this.max_value])
.range([this.HEIGHT-this.PADDING, 0]);
</code>
</pre>
<p>
Note that this is
a <a href="https://github.com/d3/d3/wiki/Quantitative-Scales#linear">linear
scale</a> and sets its <em>domain</em> to the input's min and
max values and its <em>range</em> to the output's range, the
configured HEIGHT minus padding (where the x-axis sits).
Another difference, in <strong>line 14</strong>, we set the
orientation of the axis to <em>right</em>, instructing the
axis function in what direction it should draw its ticks and
where to place text.
</p>
<p>
</p>
</script>
<script type="text/slide"
data-title="The _DrawLine() method"
data-function="drawline"
data-display-function="_drawline"
data-highlight-lines="7,8,11,16-18,21-25,29-33,38-42">
<p>
And here it is, drawing the line. In <strong>line 7</strong>
we create
a <a href="https://github.com/d3/d3/wiki/SVG-Shapes#line">d3.svg.line</a>,
a function that produces output suitable for
the <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d">d
attribute of a path element</a>, a mini-language that
describes the line. Using your browser's developer tools,
inspect the PATH element in the chart and take a gander at the
value of the <strong>d</strong> element. Care to build that
manually? I didn't think so. In <strong>lines 8 and 11</strong> we
set the line's x and y scales, which we discussed in the
previous two slides.
</p>
<p>
In <strong>lines 16-18</strong> we create a G element to hold
the PATH. We bind the data to the element (data binding was
discussed in
a <a href="http://artandlogic.com/2015/06/d3-binding-data/">previous
blog post</a>) and apply the zoom behavior (discussed in the
next slide). NOTE: for purposes of this slide, the zoom
behavior does not actually function.
</p>
<p>
In <strong>lines 21-25</strong> we create an invisible
rectangle (setting fill to <em>none</em>). What's the point
of an invisible rectangle? We need an element to collect our
mouse events. The G element is just a container and does not
capture events. By placing an invisible rectangle as an
element to hover over and
setting <strong>pointer-events</strong> to <em>all</em> we can
collect the events which will bubble up to the containing G
element which has the zoom behavior applied to it.
</p>
<p>
We place a PATH element in the containing G element
in <strong>lines 29-33</strong>. We set fill to <em>none</em>
(if you're wondering why, delete the fill attribute with your
browser's developer tools. In <strong>line 32</strong> we use
the <code>line</code> variable we created in line 7 as the
value for the <strong>d</strong> attribute.
</p>
<p>
In <strong>line 33</strong> we set the <strong>clip-path</strong>
attribute of the PATH to the value of the id of the clipPath
element defined in <strong>lines 38-42</strong>.
From <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Element/clipPath">MDN</a>:
<q>
The clipping path restricts the region to which paint can be
applied. Conceptually, any parts of the drawing that lie
outside of the region bounded by the currently active clipping
path are not drawn.
</q>
</p>
</script>
<script type="text/slide"
data-title="Zooming"
data-function="intro"
data-display-function="_zoom"
data-highlight-lines="18-22,25,28,31,48,50,52">
<p>
The chart on the right is once again our fully functioning
demo. The function to the right, <code>_Zoom()</code>, is the zoom behavior we saw
applied to the G element containing our PATH in the previous slide:
</p>
<pre>
<code>
// create a group to hold the path
var g = this.svg.append('g')
.data([this.data])
.call(this._Zoom, this);
</code>
</pre>
<p>
We can see the function takes a G element as its first
argument, and a Chart instance as its second argument, which
coincides with the use of <code>call()</code> above.
</p>
<p>
In <strong>lines 18-22</strong> we create
the <a href="https://github.com/d3/d3/wiki/Zoom-Behavior#zoom">zoom
behavior</a>, a function which should be called with a
selection (a G element) as its argument (<strong>line
25</strong>). When creating the behavior we must specify the
scale we wish to zoom, in this case, the x-axis (<strong>line
20</strong>) and set the event handler for
the <a href="https://github.com/d3/d3/wiki/Zoom-Behavior#on">zoom
event</a> (<strong>line 22</strong>).
</p>
<p>
The handler, <code>zoomed</code> (<strong>line 28</strong>),
is called when the view changes. It begins by fetching the
x-scale's new domain (<strong>line 31</strong>). Most of the
function, lines 32-45, is making sure we do not scale beyond
the limits of our data.
<p>
<p>
Having changed the chart's x-scale's domain (<strong>line
48</strong>), we must change the zoom's also (<strong>line
50</strong>). Then we render the chart again (<strong>line
52</strong>).
</script>
<script type="text/slide"
data-title="Coming Next: Part 2"
data-function="part2"
data-display-function="<none>">
<p>
This ends Part 1 of the tutorial. In Part 2 we will add
tool-tips for each data point on the line. Try it now: hover
your mouse over the line, when it is over a data point, the
point will appear and a tool-tip will display the detail of
the datum. <em>Stay tuned!</em>
</p>
<p>
Until then, please feel free to leave comments about the
tutorial
on <a href="https://artandlogic.com/2016/05/interactive-svg-charts-d3-part-1/">Art&amp;Logic's
blog post</a>.
</p>
</script>
<!-- /SLIDES -->
<!-- ------------------------------------------------------------ -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.js"
charset="utf-8">
</script>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3-tip/0.6.3/d3-tip.min.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;
}
.axis {
font-size: .8em;
}
.axis line {
stroke: #333;
}
.axis path {
shape-rendering: crispEdges;
fill: none;
stroke: #333;
}
.dot-tip {
line-height: 1;
font-weight: bold;
padding: 12px;
background: rgba(204, 204, 204, 0.8);
color: blue;
border-radius: 12px;
}
/*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'),
displayfunc = el.getAttribute('data-display-function');
if (!el.hasAttribute('data-title')) {
console.error('Slide #' + (i+1) + ' does not have required attribute: data-title');
}
if (!funcname) {
console.error('Slide #' + (i+1) + ' does not have required attribute: data-function');
}
if (funcname && (functions[funcname] === undefined)) {
console.error('Slide #' + (i+1) + ' specified function is not defined: ' + funcname);
}
if (displayfunc && displayfunc !== '<none>' && (functions[displayfunc] === undefined)) {
console.error('Slide #' + (i+1) + ' specified display-function is not defined: ' + displayfunc);
}
});
}
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, display_func;
// 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')];
// the display-function
display_func = tmpl.attr('data-display-function');
if (display_func) {
if (display_func !== '<none>') {
display_func = functions[display_func];
}
} else {
// by defualt we display the slide function
display_func = slide_func;
}
// *** 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 ***
if (display_func === '<none>') {
// no display function specified
el_src.style('display', 'none');
} else {
// make sure the src is displayed
el_src.style('display', null);
// 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(display_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