Created
November 28, 2011 04:31
-
-
Save captproton/1399117 to your computer and use it in GitHub Desktop.
d3 for mere mortals
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html> | |
| <head> | |
| <title>D3 for Mere Mortals</title> | |
| <script type="text/javascript" src="http://www.recursion.org/d3.js"></script> | |
| <script type="text/javascript" src="http://www.recursion.org/d3.time.js"></script> | |
| <script type="text/javascript" src="http://www.recursion.org/jquery-1.6.2.min.js"></script> | |
| <script type="text/javascript" src="http://www.recursion.org/prettify.js"></script> | |
| <link href="http://www.recursion.org/prettify.css" type="text/css" rel="stylesheet"/> | |
| <style type="text/css"> | |
| body { | |
| width: 850px; | |
| margin-left: auto; | |
| margin-right: auto; | |
| font-family: Helvetica, sans-serif; | |
| font-size: 14px; | |
| } | |
| </style> | |
| <style type="text/css" id="day-length-css"> | |
| div#day-length text { | |
| fill: gray; | |
| font-family: Helvetica, sans-serif; | |
| font-size: 10px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <p>D3 shares similarities with its predecessor Protovis. However, instead of having its own graphical representation, D3 uses SVG (or HTML for simpler visualizations). This means that using D3 requires a fair amount of understanding of SVG. Fortunately, D3 provides a jQuery-like interface to the DOM, which means you will not have to endure the <a href="http://www.recursion.org/2006/1/20/xml-situps">XML situps</a>.</p> | |
| <p>SVG provides basic shape primitives like <code>line</code>, <code>rect</code>, and <code>circle</code> as well as <code>text</code> and <code>path</code> to build complicated lines and shapes. If you think about a bar chart, you can see how you could make one of lines and rectangles with text for labels. D3 provides an API to help you place your rectangles in the correct location on the canvas.</p> | |
| <p>To place a rectangle on the canvas, you need to understand that the axis starts in the top left corner and counts up towards the bottom of the screen. An SVG <code>rect</code> is positioned from its top-left corner and has a width and height.</p> | |
| <p>Here's a diagram of a rectangle positioned at (100, 100) with a height of 100 pixels and a width of 200 pixels.<p> | |
| <p>To produce this with D3, add a container <code>div</code> to the page, and the following JavaScript code.<p> | |
| <pre class="code prettyprint" data-id="rect-demo-js"></pre> | |
| <p>The result looks like this.</p> | |
| <div id="rect-demo"></div> | |
| <p>Not very impressive, eh? However, it does demonstrate how to use the D3 API to build an SVG document. The generated SVG code looks like this:</p> | |
| <pre class="prettyprint"> | |
| <svg width="400" height="300"> | |
| <rect x="100" y="100" height="100" width="200"></rect> | |
| </svg> | |
| </pre> | |
| <script type="text/javascript" id="rect-demo-js"> | |
| var rectDemo = d3.select("#rect-demo"). | |
| append("svg:svg"). | |
| attr("width", 400). | |
| attr("height", 300); | |
| rectDemo.append("svg:rect"). | |
| attr("x", 100). | |
| attr("y", 100). | |
| attr("height", 100). | |
| attr("width", 200); | |
| </script> | |
| <h2>Drawing a Bar Chart</h2> | |
| <p>A vertical bar chart (or column chart) is basically a series of rectangles with the sizes proportional to the data being represented. D3 provides <a href="https://github.com/mbostock/d3/wiki/Quantitative-Scales">scales</a> to translate between the input (your data) and the output (an x or y position on the canvas).</p> | |
| <h3>Understanding Scales</h3> | |
| <p>A linear scale is declared like this: | |
| <pre class="prettyprint"> | |
| var xScale = d3.scale.linear(). | |
| domain([0, 20]). // your data minimum and maximum | |
| range([0, 100]); // the pixels to map to, e.g., the width of the diagram. | |
| </pre> | |
| <p><code>d3.scale.linear()</code> returns a function which above is stored in the xScale variable. In use, the xScale function maps between data and pixel positions:</p> | |
| <pre class="prettyprint"> | |
| xScale(0) // => 0 | |
| xScale(10) // => 50 | |
| xScale(20) // => 100 | |
| </pre> | |
| <p>In D3, scales can be used two ways to set the position of an element. If the data is a simple array of numbers, the scale itself can be passed to the <a href="https://github.com/mbostock/d3/wiki/Selections#attr">attr</a> method:</p> | |
| <pre class="prettyprint"> | |
| attr("x", xScale) | |
| </pre> | |
| <p>If the data is more complicated, or if you need to perform additional calculations to adjust the position, you can pass a function to <code>attr</code>. It takes two arguments: <code>datum</code> and <code>index</code> (which are often written as "d" and "i"). Remember that JavaScript functions can accept a variable number of arguments, so if you only need <code>datum</code> your function does not need to take <code>index</code>.</p> | |
| <p>Inside this function, you can call the scale function, manipulate the result, and return it.</p> | |
| <pre class="prettyprint"> | |
| attr("x", function(datum, index) { | |
| return xScale(datum.foobar) + whatever; | |
| }) | |
| </pre> | |
| <p>Using a scale reduces the amount of math you need to do to lay out a visualization, but using them is not required.</p> | |
| <h3>Diagramming a Bar Chart</h3> | |
| <p>In the diagram below, a single <code>rect</code> from a bar chart is represented. The canvas is 300 pixels tall and the width is the number of data points multiplied by the bar width <em>bw</em>. There are two scale functions, <em>x</em> and <em>y</em>.</p> | |
| <p>The x position of the top left of the rectangle is easy: it is <code>x(i)</code> where <code>x</code> is the scale function and <code>i</code> is the index of the current datum. Since the SVG axis is located at the top of the graph, the height <em>h</em> is subtracted from the value of <em>y(datum)</em> to get the y position.</p> | |
| <p>The width is similarly easy, as it is defined to be <em>bw</em>. The height is <em>y(datum)</em>.<p> | |
| <img src="http://www.recursion.org/d3-for-mere-mortals/svg-bar-graph-diagram.png" alt="diagram of a column graph in SVG" width="570" height="460"> | |
| <h3>Drawing the Bars</h3> | |
| <p>To give a concrete example, here is a bar graph showing <a href="http://www.recursion.org/2011/1/22/2010-books">books read per year</a>.</p> | |
| <img src="http://www.recursion.org/d3-for-mere-mortals/books-per-year-smaller.png" width="257" height="228"> | |
| <p>To replicate this in D3, you will create a series of rectangles of the appropriate height along the x axis. D3's <a href="https://github.com/mbostock/d3/wiki/Quantitative-Scales#linear">linear scale</a> will provide functions used to map a data point on the x and y axis.</p> | |
| <p>The data for the chart is a list of JavaScript objects with properties for year and number of books read. Since the years are continuous, there is no need to track them with the data, but doing so makes the code simpler to understand. The technique of using an array of JavaScript objects will be more useful for more complicated data, as you will see below.</p> | |
| <p>To draw the <code>rect</code>s, first use the <a href="https://github.com/mbostock/d3/wiki/Selections#d3_selectAll">selectAll</a> method returns to create a selector, then use the <a href="https://github.com/mbostock/d3/wiki/Selections#data">data</a> method to binds the array of data objects to the selection. Calling <a href="https://github.com/mbostock/d3/wiki/Selections#enter">enter</a> on the selection will create any objects that don't have corresponding elements in the data array.</p> | |
| <p>Since there's no existing <code>rect</code>s returned by the selector, one will be created for each data element, so it may seem superfluous. However, the <code>selectAll</code>/<code>data</code>/<code>enter</code> sequence can be used to add or remove elements from an existing list. Take a look at the <a href="http://mbostock.github.com/d3/tutorial/circle.html">Three Little Circles</a> demo for more about <code>data</code> and <code>enter</code>.</p> | |
| <p>The rest of the diagram all comes down to positioning and formatting.</p> | |
| <pre class="code prettyprint" data-id="bar-demo-js"></pre> | |
| <p>The result looks like this:</p> | |
| <div id="bar-demo"></div> | |
| <script type="text/javascript" id="bar-demo-js"> | |
| var data = [{year: 2006, books: 54}, | |
| {year: 2007, books: 43}, | |
| {year: 2008, books: 41}, | |
| {year: 2009, books: 44}, | |
| {year: 2010, books: 35}]; | |
| var barWidth = 40; | |
| var width = (barWidth + 10) * data.length; | |
| var height = 200; | |
| var x = d3.scale.linear().domain([0, data.length]).range([0, width]); | |
| var y = d3.scale.linear().domain([0, d3.max(data, function(datum) { return datum.books; })]). | |
| rangeRound([0, height]); | |
| // add the canvas to the DOM | |
| var barDemo = d3.select("#bar-demo"). | |
| append("svg:svg"). | |
| attr("width", width). | |
| attr("height", height); | |
| barDemo.selectAll("rect"). | |
| data(data). | |
| enter(). | |
| append("svg:rect"). | |
| attr("x", function(datum, index) { return x(index); }). | |
| attr("y", function(datum) { return height - y(datum.books); }). | |
| attr("height", function(datum) { return y(datum.books); }). | |
| attr("width", barWidth). | |
| attr("fill", "#2d578b"); | |
| </script> | |
| <h3>Adding Text and Axes</h3> | |
| <p>To add numbers inside the bars, <a href="http://www.w3.org/TR/SVG/text.html">svg:text</a> elements must be positioned in the same place as the top of the bar and padding added to make it look right. The <code>svg:text</code> element has a lot of attributes for positioning and formatting. In this example, <a href="http://www.w3.org/TR/SVG/text.html#TextElementDXAttribute">dx</a>, <a href="http://www.w3.org/TR/SVG/text.html#TextElementDYAttribute">dy</a>, <a href="http://www.w3.org/TR/SVG/text.html#TextAnchorProperty">text-anchor</a> and <a href="http://www.w3.org/TR/SVG/styling.html#StyleAttribute">style</a> are used.</p> | |
| <p>Again, <code>selectAll</code> is used get a selection of elements, data is bound to them with <code>data</code> and then <code>enter</code> is used to add the elements to to the chart.</p> | |
| <pre class="code prettyprint" data-id="bar-demo-part2-js"></pre> | |
| <div id="bar-demo-part2"></div> | |
| <script type="text/javascript"> | |
| var barDemo = d3.select("#bar-demo-part2"). | |
| append("svg:svg"). | |
| attr("width", width). | |
| attr("height", height); | |
| barDemo.selectAll("rect"). | |
| data(data). | |
| enter(). | |
| append("svg:rect"). | |
| attr("x", function(datum, index) { return x(index); }). | |
| attr("y", function(datum) { return height - y(datum.books); }). | |
| attr("height", function(datum) { return y(datum.books); }). | |
| attr("width", barWidth). | |
| attr("fill", "#2d578b"); | |
| </script> | |
| <script type="text/javascript" id="bar-demo-part2-js"> | |
| barDemo.selectAll("text"). | |
| data(data). | |
| enter(). | |
| append("svg:text"). | |
| attr("x", function(datum, index) { return x(index) + barWidth; }). | |
| attr("y", function(datum) { return height - y(datum.books); }). | |
| attr("dx", -barWidth/2). | |
| attr("dy", "1.2em"). | |
| attr("text-anchor", "middle"). | |
| text(function(datum) { return datum.books;}). | |
| attr("fill", "white"); | |
| </script> | |
| <p>To add an x axis, the height of the chart must be increased to make room. A padding of 30 pixels will work nicely. By keeping the old <code>height</code> variable and increasing the height of the <code>svg:svg</code> canvas by <code>padding</code>, the rest of the code does not need to change. The axis could be positioned by using <code>height + padding</code> as the baseline, but using translate makes the x and y attributes simpler.</p> | |
| <pre class="code prettyprint" data-id="bar-demo-part3-js"></pre> | |
| <p>The font has also been changed to closer match the template. This can also be done with CSS, as you'll see in the next example.</p> | |
| <p>And the result:</p> | |
| <div id="bar-demo-part3"></div> | |
| <script type="text/javascript"> | |
| var padding = 30; | |
| var barDemo = d3.select("#bar-demo-part3"). | |
| append("svg:svg"). | |
| attr("width", width). | |
| attr("height", height + padding); | |
| barDemo.selectAll("rect"). | |
| data(data). | |
| enter(). | |
| append("svg:rect"). | |
| attr("x", function(datum, index) { return x(index); }). | |
| attr("y", function(datum) { return height - y(datum.books); }). | |
| attr("height", function(datum) { return y(datum.books); }). | |
| attr("width", barWidth). | |
| attr("fill", "#2d578b"); | |
| barDemo.selectAll("text"). | |
| data(data). | |
| enter().append("svg:text"). | |
| attr("x", function(datum, index) { return x(index) + barWidth; }). | |
| attr("y", function(datum) { return height - y(datum.books); }). | |
| attr("dx", -barWidth/2). | |
| attr("dy", "1.2em"). | |
| attr("text-anchor", "middle"). | |
| attr("style", "font-size: 12; font-family: Helvetica, sans-serif;"). | |
| text(function(datum) { return datum.books;}). | |
| attr("fill", "white"); | |
| </script> | |
| <script type="text/javascript" id="bar-demo-part3-js"> | |
| barDemo.selectAll("text.yAxis"). | |
| data(data). | |
| enter().append("svg:text"). | |
| attr("x", function(datum, index) { return x(index) + barWidth; }). | |
| attr("y", height). | |
| attr("dx", -barWidth/2). | |
| attr("text-anchor", "middle"). | |
| attr("style", "font-size: 12; font-family: Helvetica, sans-serif"). | |
| text(function(datum) { return datum.year;}). | |
| attr("transform", "translate(0, 18)"). | |
| attr("class", "yAxis"); | |
| </script> | |
| <h2>Drawing a Custom Visualization</h2> | |
| <p>So far, you would have been better off using <a href="http://code.google.com/apis/chart/">Google Chart Tools</a> to produce a simple chart like this. But this barely scratches the surface of what D3 is capable of. A more complicated example will show how you can make custom visualizations with D3 that are impossible with standard charting libraries. This is where D3 really shines.</p> | |
| <p>The goal is to make a chart of day lengths throughout the year, like this one <a href="http://radiochildcare.org/AstronomyWithChildren/Day_Length_Thru_the_Year.html">drawn by a child from M&M child care</a>.</p> | |
| <img src="http://www.recursion.org/d3-for-mere-mortals/mm-day-length-graph.jpg" width="700" height="533"> | |
| <p>This chart has several interesting aspects that are worth noting. It is centered on noon and the length of the day is shown with two lines over the central axis, which forms a shape which is colored in and the background is blue to represent night. There are four labeled axes, top and bottom and left and right. Building a visualization like it is not likely to come from a pre-packaged library.</p> | |
| <h3>Planning the Visualization</h3> | |
| <p>To plan the attack, a diagram of the desired visualization is helpful.</p> | |
| <img src="http://www.recursion.org/d3-for-mere-mortals/svg-sunrise-sunset-diagram.png" width="515" height="446"> | |
| <p>This brings several points to the foreground:</p> | |
| <ul> | |
| <li>The visualization will be inset with a margin of 40 pixels to print the axes. Using transform will make the math for this simple.</li> | |
| <li>Axes will go above and below and left and right of the graphic. Since the axes are all time-related, it makes sense to use <a href="https://github.com/mbostock/d3/wiki/Time-Scales">time scales</a>.</li> | |
| <li>Helpfully, the y axis counts up the same way as the SVG coordinate system so you do not need to reverse the axis.</li> | |
| <li>The sunrise section will be yellow and will be made up of what is left of a rect after drawing shapes with a <code>svg:path</code> for sunrise time and sunset time. <a href="https://github.com/mbostock/d3/wiki/SVG-Shapes#area">d3.svg.area</a> makes this easy.</li> | |
| <li>The points on the path need to be connected into a smooth curve. There are different kinds of <a href="https://github.com/mbostock/d3/wiki/SVG-Shapes#area_interpolate">interpolation</a> for this. | |
| </ul> | |
| <h3>New Concepts</h3> | |
| <p>For this example, some new concepts will be introduced:</p> | |
| <ul> | |
| <li><a href="https://github.com/mbostock/d3/wiki/Time-Scales">Time scales</a>, an extension of linear scales tailored for time values.</li> | |
| <li>The <a href="http://www.w3.org/TR/SVG/struct.html#Groups">svg:g</a> element which groups other elements like a <code>div</code> and can be transformed as a group.</li> | |
| <li>Drawing complex shapes with <a href="http://www.w3.org/TR/SVG/paths.html#PathElement">svg:path</a>, which is complicated, but made easy with the <a href="https://github.com/mbostock/d3/wiki/SVG-Shapes#area">d3.svg.area</a> function.</li> | |
| <li>Using <a href="https://github.com/mbostock/d3/wiki/Arrays#d3_range">d3.range</a> to create an array of numbers.</li> | |
| <li>Using <a href="https://github.com/mbostock/d3/wiki/Time-Scales#ticks">scale.ticks</a> to generate tick marks for the axes.</li> | |
| <li>Using SVG translation to simplify positioning.</li> | |
| <li>Styling text with CSS.</li> | |
| <li>Adjusting pixel positions to avoid anti-aliasing</li> | |
| </ul> | |
| <h3>Digression: Anti-aliasing</h3> | |
| <p>Anti-aliasing helps make text and curved lines look smoother, but can make straight lines appear fuzzy. If you are using a WebKit-based browser, the lines on the left should be fuzzy, while those on the right should be crisp.</p> | |
| <div id="antialias-demo"></div> | |
| <script type="text/javascript"> | |
| var antialias = d3.select("#antialias-demo"). | |
| append("svg:svg"). | |
| attr("width", 200). | |
| attr("height", 100); | |
| antialias.append("svg:line"). | |
| attr("x1", 10). | |
| attr("y1", 50). | |
| attr("x2", 90). | |
| attr("y2", 50). | |
| attr("stroke", "black"); | |
| antialias.append("svg:line"). | |
| attr("x1", 50). | |
| attr("y1", 10). | |
| attr("x2", 50). | |
| attr("y2", 90). | |
| attr("stroke", "black"); | |
| antialias.append("svg:line"). | |
| attr("x1", 100). | |
| attr("y1", 50.5). | |
| attr("x2", 180). | |
| attr("y2", 50.5). | |
| attr("stroke", "black"); | |
| antialias.append("svg:line"). | |
| attr("x1", 140.5). | |
| attr("y1", 10). | |
| attr("x2", 140.5). | |
| attr("y2", 90). | |
| attr("stroke", "black"); | |
| </script> | |
| <p>Anti-aliasing often happens when drawing straight lines with D3. If the line is 1 pixel tall and positioned exactly on a pixel, it will straddle the pixel and become fuzzy. To avoid it, add or subtract 0.5 to the pixel position. The example below uses this technique to prevent fuzziness.</p> | |
| <p>However, rendering differs depending on the SVG layout engine. In the example above, Firefox 5 renders fuzzy vertical lines when positioned exactly on a pixel, while the horizontal lines are not.</p> | |
| <p>Setting the SVG <code>shape-rendering</code> property to <code>crispEdges</code> disables anti-aliasing for some or all lines. However, this may have unwanted side-effects if the visualization has curved lines, making them look jagged.</p> | |
| <pre class="prettyprint"> | |
| svg { | |
| shape-rendering: crispEdges; | |
| } | |
| </pre> | |
| <p>A better solution may be to target the <code>crispEdges</code> property at certain (vertical or horizontal) lines with a CSS class.</p> | |
| <pre class="prettyprint"> | |
| line.crisp { | |
| shape-rendering: crispEdges; | |
| } | |
| </pre> | |
| <p>Using this solution without pixel munging seems to work better across different browsers, but you should test it yourself and see what works best for your audience.</p> | |
| <h3>Getting the Data</h3> | |
| <p>Having game-planned the graphic, the first step is to collect the data. You can find sunrise and sunset times for cities around the world at <a href="http://www.timeanddate.com/">timeanddate.com</a>. I could not find a source of sunrise and sunset times in a computer-readable format and didn't feel like writing a scraper, so I selected a few days throughout the year for the selected location (Minneapolis, Minnesota).</p> | |
| <p>The data format is like this:</p> | |
| <pre class="prettyprint"> | |
| { | |
| date: new Date(2011, 4, 15), // day of year | |
| sunrise: [7, 51], // sunrise time as array of hours and minutes | |
| sunset: [16, 42] // likewise, sunset time (24 hour clock) | |
| } | |
| </pre> | |
| <p>Sunrise and sunset times are stored as an array of hour and minute values. These are used to construct a <code>Date</code> to represent the time of day when plotting.</p> | |
| <h3>Building the Visualization</h3> | |
| <p>The JavaScript code for the visualization is below, with comments in line.</p> | |
| <pre class="code prettyprint" data-id="day-length-js"></pre> | |
| Using CSS helps remove duplication from the text formatting attributes: | |
| <pre class="code prettyprint" data-id="day-length-css"></pre> | |
| And the final result: | |
| <div id="day-length"></div> | |
| <script type="text/javascript" id="day-length-js"> | |
| var width = 700; | |
| var height = 525; | |
| var padding = 40; | |
| // the vertical axis is a time scale that runs from 00:00 - 23:59 | |
| // the horizontal axis is a time scale that runs from the 2011-01-01 to 2011-12-31 | |
| var y = d3.time.scale().domain([new Date(2011, 0, 1), new Date(2011, 0, 1, 23, 59)]).range([0, height]); | |
| var x = d3.time.scale().domain([new Date(2011, 0, 1), new Date(2011, 11, 31)]).range([0, width]); | |
| var monthNames = ["Jan", "Feb", "Mar", "April", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; | |
| // Sunrise and sun set times for dates in 2011. I have picked the 1st | |
| // and 15th day of every month, plus other important dates like equinoxes | |
| // and solstices and dates around the standard time/DST transition. | |
| var data = [ | |
| {date: new Date(2011, 0, 1), sunrise: [7, 51], sunset: [16, 42]}, | |
| {date: new Date(2011, 0, 15), sunrise: [7, 48], sunset: [16, 58]}, | |
| {date: new Date(2011, 1, 1), sunrise: [7, 33], sunset: [17, 21]}, | |
| {date: new Date(2011, 1, 15), sunrise: [7, 14], sunset: [17, 41]}, | |
| {date: new Date(2011, 2, 1), sunrise: [6, 51], sunset: [18, 0]}, | |
| {date: new Date(2011, 2, 12), sunrise: [6, 32], sunset: [18, 15]}, // dst - 1 day | |
| {date: new Date(2011, 2, 13), sunrise: [7, 30], sunset: [19, 16]}, // dst | |
| {date: new Date(2011, 2, 14), sunrise: [7, 28], sunset: [19, 18]}, // dst + 1 day | |
| {date: new Date(2011, 2, 14), sunrise: [7, 26], sunset: [19, 19]}, | |
| {date: new Date(2011, 2, 20), sunrise: [07, 17], sunset: [19, 25]}, // equinox | |
| {date: new Date(2011, 3, 1), sunrise: [6, 54], sunset: [19, 41]}, | |
| {date: new Date(2011, 3, 15), sunrise: [6, 29], sunset: [19, 58]}, | |
| {date: new Date(2011, 4, 1), sunrise: [6, 3], sunset: [20, 18]}, | |
| {date: new Date(2011, 4, 15), sunrise: [5, 44], sunset: [20, 35]}, | |
| {date: new Date(2011, 5, 1), sunrise: [5, 30], sunset: [20, 52]}, | |
| {date: new Date(2011, 5, 15), sunrise: [5, 26], sunset: [21, 1]}, | |
| {date: new Date(2011, 5, 21), sunrise: [5, 26], sunset: [21, 3]}, // solstice | |
| {date: new Date(2011, 6, 1), sunrise: [5, 30], sunset: [21, 3]}, | |
| {date: new Date(2011, 6, 15), sunrise: [5, 41], sunset: [20, 57]}, | |
| {date: new Date(2011, 7, 1), sunrise: [5, 58], sunset: [20, 40]}, | |
| {date: new Date(2011, 7, 15), sunrise: [6, 15], sunset: [20, 20]}, | |
| {date: new Date(2011, 8, 1), sunrise: [6, 35], sunset: [19, 51]}, | |
| {date: new Date(2011, 8, 15), sunrise: [6, 51], sunset: [19, 24]}, | |
| {date: new Date(2011, 8, 23), sunrise: [7, 1], sunset: [19, 9]}, // equinox | |
| {date: new Date(2011, 9, 1), sunrise: [7, 11], sunset: [18, 54]}, | |
| {date: new Date(2011, 9, 15), sunrise: [7, 28], sunset: [18, 29]}, | |
| {date: new Date(2011, 10, 1), sunrise: [7, 51], sunset: [18, 2]}, | |
| {date: new Date(2011, 10, 5), sunrise: [7, 57], sunset: [17, 56]}, // last day of dst | |
| {date: new Date(2011, 10, 6), sunrise: [6, 58], sunset: [16, 55]}, // standard time | |
| {date: new Date(2011, 10, 7), sunrise: [6, 59], sunset: [16, 54]}, // standard time + 1 | |
| {date: new Date(2011, 10, 15), sunrise: [7, 10], sunset: [16, 44]}, | |
| {date: new Date(2011, 11, 1), sunrise: [7, 31], sunset: [16, 33]}, | |
| {date: new Date(2011, 11, 15), sunrise: [7, 44], sunset: [16, 32]}, | |
| {date: new Date(2011, 11, 22), sunrise: [7, 49], sunset: [16, 35]}, // solstice | |
| {date: new Date(2011, 11, 31), sunrise: [7, 51], sunset: [16, 41]} | |
| ]; | |
| function yAxisLabel(d) { | |
| if (d == 12) { return "noon"; } | |
| if (d < 12) { return d; } | |
| return (d - 12); | |
| } | |
| // The labels along the x axis will be positioned on the 15th of the | |
| // month | |
| function midMonthDates() { | |
| return d3.range(0, 12).map(function(i) { return new Date(2011, i, 15) }); | |
| } | |
| var dayLength = d3.select("#day-length"). | |
| append("svg:svg"). | |
| attr("width", width + padding * 2). | |
| attr("height", height + padding * 2); | |
| // create a group to hold the axis-related elements | |
| var axisGroup = dayLength.append("svg:g"). | |
| attr("transform", "translate("+padding+","+padding+")"); | |
| // draw the x and y tick marks. Since they are behind the visualization, they | |
| // can be drawn all the way across it. Because the <g> has been | |
| // translated, they stick out the left side by going negative. | |
| axisGroup.selectAll(".yTicks"). | |
| data(d3.range(5, 22)). | |
| enter().append("svg:line"). | |
| attr("x1", -5). | |
| // Round and add 0.5 to fix anti-aliasing effects (see above) | |
| attr("y1", function(d) { return d3.round(y(new Date(2011, 0, 1, d))) + 0.5; }). | |
| attr("x2", width+5). | |
| attr("y2", function(d) { return d3.round(y(new Date(2011, 0, 1, d))) + 0.5; }). | |
| attr("stroke", "lightgray"). | |
| attr("class", "yTicks"); | |
| axisGroup.selectAll(".xTicks"). | |
| data(midMonthDates). | |
| enter().append("svg:line"). | |
| attr("x1", x). | |
| attr("y1", -5). | |
| attr("x2", x). | |
| attr("y2", height+5). | |
| attr("stroke", "lightgray"). | |
| attr("class", "yTicks"); | |
| // draw the text for the labels. Since it is the same on top and | |
| // bottom, there is probably a cleaner way to do this by copying the | |
| // result and translating it to the opposite side | |
| axisGroup.selectAll("text.xAxisTop"). | |
| data(midMonthDates). | |
| enter(). | |
| append("svg:text"). | |
| text(function(d, i) { return monthNames[i]; }). | |
| attr("x", x). | |
| attr("y", -8). | |
| attr("text-anchor", "middle"). | |
| attr("class", "axis xAxisTop"); | |
| axisGroup.selectAll("text.xAxisBottom"). | |
| data(midMonthDates). | |
| enter(). | |
| append("svg:text"). | |
| text(function(d, i) { return monthNames[i]; }). | |
| attr("x", x). | |
| attr("y", height+15). | |
| attr("text-anchor", "middle"). | |
| attr("class", "xAxisBottom"); | |
| axisGroup.selectAll("text.yAxisLeft"). | |
| data(d3.range(5, 22)). | |
| enter(). | |
| append("svg:text"). | |
| text(yAxisLabel). | |
| attr("x", -7). | |
| attr("y", function(d) { return y(new Date(2011, 0, 1, d)); }). | |
| attr("dy", "3"). | |
| attr("class", "yAxisLeft"). | |
| attr("text-anchor", "end"); | |
| axisGroup.selectAll("text.yAxisRight"). | |
| data(d3.range(5, 22)). | |
| enter(). | |
| append("svg:text"). | |
| text(yAxisLabel). | |
| attr("x", width+7). | |
| attr("y", function(d) { return y(new Date(2011, 0, 1, d)); }). | |
| attr("dy", "3"). | |
| attr("class", "yAxisRight"). | |
| attr("text-anchor", "start"); | |
| // create a group for the sunrise and sunset paths | |
| var lineGroup = dayLength.append("svg:g"). | |
| attr("transform", "translate("+ padding + ", " + padding + ")"); | |
| // draw the background. The part of this that remains uncovered will | |
| // represent the daylight hours. | |
| lineGroup.append("svg:rect"). | |
| attr("x", 0). | |
| attr("y", 0). | |
| attr("height", height). | |
| attr("width", width). | |
| attr("fill", "lightyellow"); | |
| // The meat of the visualization is surprisingly simple. sunriseLine | |
| // and sunsetLine are areas (closed svg:path elements) that use the date | |
| // for the x coordinate and sunrise and sunset (respectively) for the y | |
| // coordinate. The sunrise shape is anchored at the top of the chart, and | |
| // sunset area is anchored at the bottom of the chart. | |
| var sunriseLine = d3.svg.area(). | |
| x(function(d) { return x(d.date); }). | |
| y1(function(d) { return y(new Date(2011, 0, 1, d.sunrise[0], d.sunrise[1])); }). | |
| interpolate("linear"); | |
| lineGroup. | |
| append("svg:path"). | |
| attr("d", sunriseLine(data)). | |
| attr("fill", "steelblue"); | |
| var sunsetLine = d3.svg.area(). | |
| x(function(d) { return x(d.date); }). | |
| y0(height). | |
| y1(function(d) { return y(new Date(2011, 0, 1, d.sunset[0], d.sunset[1])); }). | |
| interpolate("linear"); | |
| lineGroup.append("svg:path"). | |
| attr("d", sunsetLine(data)). | |
| attr("fill", "steelblue"); | |
| // finally, draw a line representing 12:00 across the entire | |
| // visualization | |
| lineGroup.append("svg:line"). | |
| attr("x1", 0). | |
| attr("y1", d3.round(y(new Date(2011, 0, 1, 12))) + 0.5). | |
| attr("x2", width). | |
| attr("y2", d3.round(y(new Date(2011, 0, 1, 12))) + 0.5). | |
| attr("stroke", "lightgray"); | |
| </script> | |
| <h3>Next Steps</h3> | |
| <p>Now that the visualization is complete, think of what could be done to improve it. You could label the solstices and equinoxes with exact times, or plot different sunset and sunrise times for different locations against each other, or make a label that shows exact times for the day the user is are mousing over...</p> | |
| <p>The possibilities are endless.</p> | |
| <h2>Debugging D3</h2> | |
| <p>Lest you assume these examples were coded straight through, let me assure you this was not the case. There was a significant amount of trial-and-error coding needed to get them working.</p> | |
| <p>One of the advantages of D3 using SVG as its native graphical representation is that it is easier to debug the WebKit Inspector or Firebug.</p> | |
| <img src="http://www.recursion.org/d3-for-mere-mortals/svg-code-inspector.png" width="516" height="70" alt="SVG in the WebKit Inspector"> | |
| <p>This is incredibly useful. You can try it on this page to see how the SVG code for these examples is generated.</p> | |
| <p>Another technique that has been helpful for me is to use <code>console.log</code> in positioning functions. This lets you inspect the datum and index of an element as well as the calculated position for the x or y value. For example:</p> | |
| <pre class="prettyprint"> | |
| attr("x", function(d, i) { | |
| console.log(d); | |
| console.log(i); | |
| return x(d.whatever); | |
| }) | |
| </pre> | |
| <h2>Further Reading</h2> | |
| <p>Now you know about as much about D3 as I do. Obviously, this barely scratches the surface. For more tutorials and resources about D3, read the following articles:</p> | |
| <ul> | |
| <li>The <a href="http://mbostock.github.com/d3/">D3 homepage</a>, <a href="http://mbostock.github.com/d3/ex/">examples</a> (don't miss the examples <a href="https://github.com/mbostock/d3/tree/master/examples">only in the source code</a>), and <a href="https://github.com/mbostock/d3/wiki/API-Reference">API documentation</a>.</li> | |
| <li>The D3 tutorials <a href="http://mbostock.github.com/d3/tutorial/circle.html">Three Little Circles</a>, <a href="http://mbostock.github.com/d3/tutorial/bar-1.html">A Bar Chart, Part 1</a>, and <a href="http://mbostock.github.com/d3/tutorial/bar-2.html">A Bar Chart, Part 2</a></li> | |
| <li>Jan Willem Tulp's <a href="">D3 blog category</a> especially <a href="http://www.janwillemtulp.com/2011/03/20/tutorial-introduction-to-d3/">Tutorial: Introduction to D3</a> and <a href="http://www.janwillemtulp.com/2011/04/01/tutorial-line-chart-in-d3/">Tutorial: Line chart in D3</a>.</li> | |
| <li><a href="http://nelsonslog.wordpress.com/2011/04/11/d3-scales-and-interpolation/">D3 scales and interpolation</a> by Nelson Minar.</li> | |
| <li><a href="http://www.jeromecukier.net/blog/2011/08/11/d3-scales-and-color/">D3: Scales and Color</a> by Jerome Cukier (with nice diagrams explaining scales).</li> | |
| <li><a href="http://www.verisi.com/resources/d3-tutorial-basic-charts.htm">Creating Basic Charts using d3.js</a> by Ben Lorica (view source to see the D3 code).</li> | |
| <li><a href="http://christopheviau.com/d3_tutorial/">Try D3 Now</a> by Christophe Viau.</li> | |
| <li><a href="http://dealloc.me/2011/06/24/d3-is-not-a-graphing-library.html">D3.js is Not a Graphing Library, Let's Design a Line Graph</a> by Justin Palmer.</li> | |
| </ul> | |
| <p>You also need to read about SVG to know what's possible – because with D3, you can do anything SVG can do.</p> | |
| <ul> | |
| <li>The <a href="http://www.w3.org/TR/SVG/">SVG spec</a> is a good place to start.</li> | |
| <li><a href="http://www.learnsvg.com/book-learnsvg/">Learn SVG</a> by Jon Frost, Stefan Goesser and Michel Hirtzler provides a gentler introduction.</li> | |
| </ul> | |
| <p>That's it for now. I hope this tutorial was helpful for you.</p> | |
| <hr> | |
| Return to <a href="/">recursion</a>. <a href="http://luke.francl.org/">More about me</a>. | |
| <script type="text/javascript"> | |
| $(function() { | |
| $("pre.code").each(function() { $(this).html($("#" + $(this).data("id")).text().trim()); }); | |
| prettyPrint(); | |
| }) | |
| </script> | |
| <script type="text/javascript"> | |
| var _gaq = _gaq || []; | |
| _gaq.push(['_setAccount', 'UA-13262353-1']); | |
| _gaq.push(['_trackPageview']); | |
| (function() { | |
| var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; | |
| ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; | |
| var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment