{ "metadata": { "name": "" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Data Visualization With D3\n", "\n", "This lab is adapted from the fantastic [D3 Tutorial](http://alignedleft.com/tutorials/d3/) by Scott Murray. We\u2019ll work through a quick version of his tutorial, and then build a simple interactive visualization dashboard for a sample dataset. If you're interested in a more thorough introduction to D3, take a look at his book [Interactive Data Visualization for the Web](http://chimera.labs.oreilly.com/books/1230000000345/index.html), which is available for free online. Additionally, if you're thinking of working with D3 for your project, have a look at [this fantastic gallery](https://github.com/mbostock/d3/wiki/Gallery) for some really good starting points!\n", "\n", "We're going to use this lab to learn and practice D3 fundamentals, to make sure the concepts are solidified to make fancier visualizations.\n", "\n", "Don't forget to [fill out the response form](http://goo.gl/awwkDP)!\n", "\n", "## Basics\n", "\n", "* **HTML** - By now, hopefully you\u2019ve heard about HTML. HTML, invented by lazy physicists, is the language of the web, and we\u2019ll use it for laying out and displaying documents - in this case, our visualizations.\n", "* **CSS** - In modern webpages, HTML is used for layout of elements, while CSS is used to *style* the visual presentation of HTML, doing things like setting fonts and line widths, etc. We'll make heavy use of CSS to style our visualizations.\n", "* **DOM** - A DOM is an object model used to represent HTML documents as well as documents in other markup formats. A DOM can be thought of as a tree of nodes, where each node represents an element in the document, with the tree rooted at as the document. A visualization of the DOM for a simple table looks like this: \n", " \n", " \n", "\n", "* **Javascript** - A client side scripting language embedded in most web browsers. Has really nice integration with the current page's DOM and can manipulate it.\n", "* **SVG** - Scalable Vector Graphics format for describing vector graphics in XML. Very low level - allows for description of basic shapes. A green rectangle with a black border in SVG looks like this:\n", "\n", " \n", " \n", " \n", " \n", " Which, rendered by your web browser, looks like this:\n", " \n", " \n", " \n", " \n", "\n", " Since it\u2019s XML - we can load it as a DOM! And we can manipulate it with Javascript!\n", "\n", "* **D3** - A javascript library to generate visualizations. Works on things other than SVG, but SVG enables us to do things we can\u2019t with raster images - including *interactivity*. \n", "\n", "The key idea behind D3 is that we can programmatically manipulate SVG elements in a webpage to create great visualizations, and with the help of a little javascript, we can make them *interactive*.\n", "\n", "## Setup\n", "\n", "Since an iPython notebook is just a webpage, we can actually modify elements inside it while we're running. Let's take a look at what we mean - create a new html element in the middle of this webpage by running the following snippet." ] }, { "cell_type": "code", "collapsed": false, "input": [ "%%html\n", "
" ], "language": "python", "metadata": {}, "outputs": [ { "html": [ "
" ], "metadata": {}, "output_type": "display_data", "text": [ "" ] } ], "prompt_number": 2 }, { "cell_type": "markdown", "metadata": {}, "source": [ "What did we just do? We created an empty element (a div) with the ID 'd3-lab' in the middle of this very web page.\n", "\n", "Hmm... so what can we do with it? Well, let's execute some JavaScript inline as well." ] }, { "cell_type": "code", "collapsed": false, "input": [ "%%javascript\n", "\n", "require.config({paths: {d3: \"/files/d3.v3.min\"}});\n", "\n", "require([\"d3\"], function(d3) {\n", " d3.select(\"#d3-lab\").append(\"b\").text(\"My insanely cool visualization.\");\n", "});" ], "language": "python", "metadata": {}, "outputs": [ { "javascript": [ "\n", "require.config({paths: {d3: \"/files/d3.v3.min\"}});\n", "\n", "require([\"d3\"], function(d3) {\n", " d3.select(\"#d3-lab\").append(\"b\").text(\"My insanely cool visualization.\");\n", "});" ], "metadata": {}, "output_type": "display_data", "text": [ "" ] } ], "prompt_number": 3 }, { "cell_type": "markdown", "metadata": {}, "source": [ "You should see some text right beneath your html block above. OK! This is a building block for creating visualizations.\n", "\n", "## Using D3\n", "\n", "Ignoring the boilerplate - let's decompose the line that really matters - `d3.select(#d3-lab).text(\"My insanely cool visualization.\");`. What is this thing doing?\n", "\n", "First - `d3` is an object which contains a number of methods. One of them is `select` which allows us to select DOM elements.\n", "\n", "One of the key things in D3 is that almost all methods return a *selection*. Thus, we can chain together any methods that are avaiable to a selection on top of each other. \n", "\n", "In this example, once we've selected the ID, we care about (`d3-lab`), we then `append` a `b` element (`b` is *bold* in HTML). The `append` method returns another selection - namely, the element it just created. We then chain on the `text` method onto the `append`, which inserts text inside its selection.\n", "\n", "We now have the building blocks we need to build *real* visualizations with D3." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Binding to Data and Creating Visual Elements\n", "\n", "One of the central concepts in D3 is *binding data to visual elements*. This comes from the concept that a visualization is a transformation of data elements to some visual representation of that data. This might be transforming a bunch of numbers into a bar chart, [or using a statistical function to extract important words from Shakespeare's text and projecting them onto blades of a fancy chandelier above a bar in a museum](http://www.artnews.com/2012/10/16/ben-rubin-shakespeare-machine/).\n", "\n", "First, let's clear up some terminology:\n", "\n", "* **Data Elements** - Think of these as set of data points. They can be individual numbers, records in a data frame or some other kind of data.\n", "* **DOM Nodes** - These are a set of items in the DOM of the canvas that we're manipulating. For example, a bar chart might contain several `rect`'s - one for each bar. \n", "\n", "*Binding* Data Elements to DOM Nodes is the process of associating Data Elements with a selection DOM Nodes in a one-to-one mapping. You can think of this like a *join* in a database system.\n", "\n", "Let's look at a concrete example" ] }, { "cell_type": "code", "collapsed": false, "input": [ "%%html\n", "
" ], "language": "python", "metadata": {}, "outputs": [ { "html": [ "
" ], "metadata": {}, "output_type": "display_data", "text": [ "" ] } ], "prompt_number": 4 }, { "cell_type": "code", "collapsed": false, "input": [ "%%javascript\n", "\n", "require.config({paths: {d3: \"/files/d3.v3.min\"}});\n", "\n", "require([\"d3\"], function(d3) {\n", " //Setup local variables.\n", " var w = 500;\n", " var h = 100;\n", " var barPadding = 1;\n", " \n", " //Set up our dataset.\n", " var dataset = [15,12,21,42,12,10,1];\n", " \n", " //Create an svg element that's w x h in size.\n", " var svg = d3.select(\"#d3-bind\")\n", " .append(\"svg\")\n", " .attr(\"width\", w)\n", " .attr(\"height\", h);\n", " \n", " //Bind our data to SVG and create a rectangle for each one\n", " //This is where the magic happens!\n", " var bars = svg.selectAll(\"rect\")\n", " .data(dataset)\n", " .enter()\n", " .append(\"rect\");\n", " \n", " //For each bar, set its attributes as a function of its position\n", " //in the dataset and \n", " bars.attr(\"x\", function(d, i) { return i * (w / dataset.length); })\n", " .attr(\"y\", function(d) { return h - (d * 4); })\n", " .attr(\"width\", w / dataset.length - barPadding)\n", " .attr(\"height\", function(d) { return d * 4; })\n", " .attr(\"fill\", \"teal\");\n", "});" ], "language": "python", "metadata": {}, "outputs": [ { "javascript": [ "\n", "require.config({paths: {d3: \"/files/d3.v3.min\"}});\n", "\n", "require([\"d3\"], function(d3) {\n", " //Setup local variables.\n", " var w = 500;\n", " var h = 100;\n", " var barPadding = 1;\n", " \n", " //Set up our dataset.\n", " var dataset = [15,12,21,42,12,10,1];\n", " \n", " //Create an svg element that's w x h in size.\n", " var svg = d3.select(\"#d3-bind\")\n", " .append(\"svg\")\n", " .attr(\"width\", w)\n", " .attr(\"height\", h);\n", " \n", " //Bind our data to SVG and create a rectangle for each one\n", " //This is where the magic happens!\n", " var bars = svg.selectAll(\"rect\")\n", " .data(dataset)\n", " .enter()\n", " .append(\"rect\");\n", " \n", " //For each bar, set its attributes as a function of its position\n", " //in the dataset and \n", " bars.attr(\"x\", function(d, i) { return i * (w / dataset.length); })\n", " .attr(\"y\", function(d) { return h - (d * 4); })\n", " .attr(\"width\", w / dataset.length - barPadding)\n", " .attr(\"height\", function(d) { return d * 4; })\n", " .attr(\"fill\", \"teal\");\n", "});" ], "metadata": {}, "output_type": "display_data", "text": [ "" ] } ], "prompt_number": 5 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Alright, this function is a bit more substantial. What did we do here?\n", "\n", "First, we set up some local variables - width and height of the SVG element that we're going to manipulate. Then - we create our dataset. Here, it's just a list of numbers.\n", "\n", "Next, we *bind* our SVG to our dataset - more specifically, we *bind* all *rect* elements inside our SVG to each element in our dataset. But - we don't actually *have* any `rect` elements in our SVG - so won't that selection be empty? This is where this myseterious function `enter` comes in. The selection acts as a placeholder for any items that don't exist yet. This picture from the D3 paper you read for class summarizes what this function does pretty well.\n", "\n", "\n", "\n", "That is, we can think of the *binding* process as a database *full outer join*, and *enter* is a function that runs on the items in the result where the data is *not* bound to an element yet, *update* is a function which runs on the results where the data *is* bound to an element, and *exit* is a function which runs on elements that aren't bound to data. Another way to think of this is with a Constructor/Update/Destructor pattern from Object Oriented Programming. The first time a data item comes in, we run `enter` on it, and when it is deleted we run `exit`.\n", "\n", "Finally, we set some visual attributes of each *rect* item in our dataset. These attributes: x and y position of each bar, height and width of each bar, and fill color - are defined either as values (as in the case of width and fill), functions of each data item (as in the case of height and y), or functions of the data item and its *index* in the dataset (as in the case of x). D3 figures out how to set the attribute based on the type of the second argument you pass to the `attr` method." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### DIY\n", "1. Execute the code above again - you should see another copy of the bar chart. Why?\n", "2. Now try changing the color to something other than teal - generate a new chart.\n", "3. Now make the bars 1/10th as wide with the same width between them (*Hint: you'll need to modify the `width` and `x` attribute definitions*)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Scales and Axes\n", "\n", "At this point, you've seen how to manipulate D3 selections, bind data to SVG elements, and update visual attributes based on our data. So far the result is a simple bar chart, but we're reasoning about everything in pixel space, and everything seems pretty low level.\n", "\n", "Fortunately, D3 provides a bunch of tools to help us plot data on different scales and ,with different axes, or with a different layout. We'll have a look at each of these now." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Scales\n", "\n", "\"Scales are functions that map from an input domain to an output range.\" - Mike Bostock\n", "\n", "You can think about scales as the things that let you load up data and manipulate it one unit - inches, feet, tons - and translate it into some desired output unit (often pixels on the screen of our web browser).\n", "\n", "D3 provides a set of funcitonality to create these functions. Let's take a look at a simple linear scale. D3 contains other kinds of scale - logarithmic, power, quantile, etc.\n", "\n", "A scale can be constructed with code that looks like the following:\n", "\n", " var scale = d3.scale.linear().domain([x1,x2]).range([y1,y2]);\n", " \n", "After executing this code, the value of `scale` is a **function** which translates things from the range (x1,x2) to the range (y1,y2) with a linear transformation.\n", "\n", "In the code below, we've created a few example scales - designed to help with sizing of the bars, as well as positioning them - these scales take care of adding in some padding on either side of the chart.\n", "\n", "#### DIY\n", "1. In the last exercise - you compressed the x-scale manually. Now do it simply by manipulating the height and width scales below. You should be able to make this change in just a few characters in the definition of xScale and widthScale.\n", "2. Now try making the y-scale a log scale. This change will require a few more changes than the last one, but should be contained in definitions of the heightScale and yScale variables. (*Hint: you'll also need to change the left end of the domain.*)\n" ] }, { "cell_type": "code", "collapsed": false, "input": [ "%%html\n", "
" ], "language": "python", "metadata": {}, "outputs": [ { "html": [ "
" ], "metadata": {}, "output_type": "display_data", "text": [ "" ] } ], "prompt_number": 336 }, { "cell_type": "code", "collapsed": false, "input": [ "%%javascript\n", "\n", "require.config({paths: {d3: \"/files/d3.v3.min\"}});\n", "\n", "require([\"d3\"], function(d3) {\n", " //Setup local variables.\n", " var w = 500;\n", " var h = 400;\n", " var barPadding = 20;\n", " var padding = 50;\n", " \n", " //Set up our dataset.\n", " var dataset = [15,12,21,42,12,10,5,85];\n", " \n", " //Define your scales here\n", " var xScale = d3.scale.linear()\n", " .domain([0, dataset.length])\n", " .range([padding,(w-padding)]); \n", " \n", " var widthScale = d3.scale.linear()\n", " .domain([0, dataset.length])\n", " .range([0, (w-2*padding)]); \n", " \n", " var yScale = d3.scale.linear()\n", " .domain([0, d3.max(dataset)])\n", " .range([h-padding,padding]);\n", "\n", " var heightScale = d3.scale.linear()\n", " .domain([0, d3.max(dataset)])\n", " .range([0,h-2*padding]);\n", " \n", " //Create an svg element that's w x h in size.\n", " var svg = d3.select(\"#d3-scale\")\n", " .append(\"svg\")\n", " .attr(\"width\", w)\n", " .attr(\"height\", h);\n", " \n", " //Bind our data to SVG and create a rectangle for each one\n", " //This is where the magic happens!\n", " var bars = svg.selectAll(\"rect\")\n", " .data(dataset)\n", " .enter()\n", " .append(\"rect\");\n", " \n", " //For each bar, set its attributes as a function of its position\n", " //in the dataset and its value.\n", " bars.attr(\"x\", function(d, i) { return xScale(i); })\n", " .attr(\"y\", function(d) { return yScale(d); })\n", " .attr(\"width\", widthScale(0.8))\n", " .attr(\"height\", function(d) { return heightScale(d); })\n", " .attr(\"fill\", \"teal\");\n", "});" ], "language": "python", "metadata": {}, "outputs": [ { "javascript": [ "\n", "require.config({paths: {d3: \"/files/d3.v3.min\"}});\n", "\n", "require([\"d3\"], function(d3) {\n", " //Setup local variables.\n", " var w = 500;\n", " var h = 400;\n", " var barPadding = 20;\n", " var padding = 50;\n", " \n", " //Set up our dataset.\n", " var dataset = [15,12,21,42,12,10,5,85];\n", " \n", " //Define your scale here\n", " var xScale = d3.scale.linear()\n", " .domain([0, dataset.length])\n", " .range([padding,(w-padding)]); \n", " \n", " var yScale = d3.scale.linear()\n", " .domain([0, d3.max(dataset)])\n", " .range([h-padding,padding]);\n", " \n", " var heightScale = d3.scale.linear()\n", " .domain([0, d3.max(dataset)])\n", " .range([0,h-2*padding]);\n", " \n", " var widthScale = d3.scale.linear()\n", " .domain([0, dataset.length])\n", " .range([0, (w-2*padding)]);\n", " \n", " //Create an svg element that's w x h in size.\n", " var svg = d3.select(\"#d3-scale\")\n", " .append(\"svg\")\n", " .attr(\"width\", w)\n", " .attr(\"height\", h);\n", " \n", " //Bind our data to SVG and create a rectangle for each one\n", " //This is where the magic happens!\n", " var bars = svg.selectAll(\"rect\")\n", " .data(dataset)\n", " .enter()\n", " .append(\"rect\");\n", " \n", " //For each bar, set its attributes as a function of its position\n", " //in the dataset and \n", " bars.attr(\"x\", function(d, i) { return xScale(i); })\n", " .attr(\"y\", function(d) { return yScale(d); })\n", " .attr(\"width\", widthScale(0.8))\n", " .attr(\"height\", function(d) { return heightScale(d); })\n", " .attr(\"fill\", \"teal\");\n", " \n", " var yAxis = d3.svg.axis()\n", " .scale(yScale)\n", " .orient(\"left\")\n", " .ticks(5)\n", "\n", " svg.append(\"g\")\n", " .attr(\"class\", \"axis\")\n", " .attr(\"transform\", \"translate(\" + xScale(0) + \",0)\")\n", " .call(yAxis)\n", " \n", " var xAxis = d3.svg.axis()\n", " .scale(xScale)\n", " .orient(\"bottom\")\n", " .ticks(dataset.length)\n", "\n", " svg.append(\"g\")\n", " .attr(\"class\", \"axis\")\n", " .attr(\"transform\", \"translate(0, \" + yScale(0) + \")\")\n", " .call(xAxis)\n", "});" ], "metadata": {}, "output_type": "display_data", "text": [ "" ] } ], "prompt_number": 266 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Linear scales may seem trivial - but constructing these translations automatically can be *super* powerful when you're working on more complicated visualizations." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Axes\n", "\n", "Axes are functions which generate visual elements used to generate the visual elements of an axis - the x axis, y-axis, gridlines, etc. you see in many visualizations. \n", "\n", "In order to create an axis, we should have a `scale` for that axis. Luckily, we've already defined a few.\n", "\n", "Creating a new axis looks something like this:\n", "\n", " var yAxis = d3.svg.axis()\n", " .scale(yScale)\n", " .orient(\"left\")\n", " .ticks(5)\n", "\n", " svg.append(\"g\")\n", " .attr(\"class\", \"axis\")\n", " .attr(\"transform\", \"translate(\" + xScale(0) + \",0)\")\n", " .call(yAxis)\n", " \n", "In that code, first we create a new `axis`, then we assign it to a scale (in this case, the `yScale`). Next, we assign a number of ticks that we want the axis to display (by default, evenly spaced). Finally we *append* the axis to the svg element we've already selected, and translate it to the right place. The *call* piece of the last bit of code reflects the fact that `yAxis` is a *function* which generates the axis object.\n", "\n", "### DIY\n", "1. Add an axis to your visualization above by adding this code to your javascript.\n", "2. With the same basic code - add an x axis. It can be aligned to the top or the bottom of the chart - but make sure the number of ticks makes sense, and that the xAxis gets translated to the right place.\n", "\n", "We could style this axis by adding some CSS to the page that would control the line width, what the tick marks look like, etc." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Updates, Transitions, and Motion\n", "\n", "So far, we've looked at **static** visualizations. But the web is an incredibly dynamic medium, and we've already seen that we can change it. D3 provides some nice tools for describing what to do when data changes, and ways to control movement in our visualizations - let's make our charts **move!**\n", "\n", "### Updates\n", "In this section, we'll work with our original bar chart again, but with an additional element inserted to act like a button." ] }, { "cell_type": "code", "collapsed": false, "input": [ "%%html\n", "
" ], "language": "python", "metadata": {}, "outputs": [ { "html": [ "
" ], "metadata": {}, "output_type": "display_data", "text": [ "" ] } ], "prompt_number": 335 }, { "cell_type": "code", "collapsed": false, "input": [ "%%javascript\n", "\n", "require.config({paths: {d3: \"/files/d3.v3.min\"}});\n", "\n", "require([\"d3\"], function(d3) {\n", " //Setup local variables.\n", " var w = 500;\n", " var h = 400;\n", " var barPadding = 20;\n", " var padding = 50;\n", " \n", " //Set up our dataset.\n", " var dataset = [ 15, 12, 21, 42, 12, 10, 5, 85 ];\n", " \n", " //Define your scales here\n", " var xScale = d3.scale.linear()\n", " .domain([0, dataset.length])\n", " .range([padding,(w-padding)]); \n", " \n", " var widthScale = d3.scale.linear()\n", " .domain([0, dataset.length])\n", " .range([0, (w-2*padding)]); \n", " \n", " var yScale = d3.scale.linear()\n", " .domain([0, d3.max(dataset)])\n", " .range([h-padding,padding]);\n", "\n", " var heightScale = d3.scale.linear()\n", " .domain([0, d3.max(dataset)])\n", " .range([0,h-2*padding]);\n", " \n", " //Clear out old content, then create an svg element that's w x h in size.\n", " d3.select(\"#d3-update\").selectAll(\"*\").remove(); \n", " \n", " var svg = d3.select(\"#d3-update\")\n", " .append(\"svg\")\n", " .attr(\"width\", w)\n", " .attr(\"height\", h);\n", " \n", " //Bind our data to SVG and create a rectangle for each one\n", " //This is where the magic happens!\n", " var bars = svg.selectAll(\"rect\")\n", " .data(dataset)\n", " .enter()\n", " .append(\"rect\");\n", " \n", " //For each bar, set its attributes as a function of its position\n", " //in the dataset and \n", " bars.attr(\"x\", function(d, i) { return xScale(i); })\n", " .attr(\"y\", function(d) { return yScale(d); })\n", " .attr(\"width\", widthScale(0.8))\n", " .attr(\"height\", function(d) { return heightScale(d); })\n", " .attr(\"fill\", \"teal\");\n", " \n", " //Add a text element to our div\n", " d3.select(\"#d3-update\").append(\"p\").text(\"Click here to update our data.\")\n", " \n", " //On click, update with new data.\n", " d3.select(\"#d3-update\").select(\"p\")\n", " .on(\"click\", function() {\n", "\n", " //New values for dataset\n", " dataset = [ 11, 12, 62, 20, 18, 17, 16, 18 ];\n", "\n", " //Update all rects\n", " svg.selectAll(\"rect\")\n", " .data(dataset)\n", " .attr(\"y\", function(d) {\n", " return yScale(d);\n", " })\n", " .attr(\"height\", function(d) {\n", " return heightScale(d);\n", " });\n", "\n", " });\n", "});" ], "language": "python", "metadata": {}, "outputs": [ { "javascript": [ "\n", "require.config({paths: {d3: \"/files/d3.v3.min\"}});\n", "\n", "require([\"d3\"], function(d3) {\n", " //Setup local variables.\n", " var w = 500;\n", " var h = 400;\n", " var barPadding = 20;\n", " var padding = 50;\n", " \n", " //Set up our dataset.\n", " var dataset = [ 15, 12, 21, 42, 12, 10, 5, 85 ];\n", " \n", " //Define your scales here\n", " var xScale = d3.scale.linear()\n", " .domain([0, dataset.length])\n", " .range([padding,(w-padding)]); \n", " \n", " var widthScale = d3.scale.linear()\n", " .domain([0, dataset.length])\n", " .range([0, (w-2*padding)]); \n", " \n", " var yScale = d3.scale.linear()\n", " .domain([0, d3.max(dataset)])\n", " .range([h-padding,padding]);\n", "\n", " var heightScale = d3.scale.linear()\n", " .domain([0, d3.max(dataset)])\n", " .range([0,h-2*padding]);\n", " \n", " //Clear out old content, then create an svg element that's w x h in size.\n", " d3.select(\"#d3-update\").selectAll(\"*\").remove(); \n", " \n", " var svg = d3.select(\"#d3-update\")\n", " .append(\"svg\")\n", " .attr(\"width\", w)\n", " .attr(\"height\", h);\n", " \n", " //Bind our data to SVG and create a rectangle for each one\n", " //This is where the magic happens!\n", " var bars = svg.selectAll(\"rect\")\n", " .data(dataset)\n", " .enter()\n", " .append(\"rect\");\n", " \n", " //For each bar, set its attributes as a function of its position\n", " //in the dataset and \n", " bars.attr(\"x\", function(d, i) { return xScale(i); })\n", " .attr(\"y\", function(d) { return yScale(d); })\n", " .attr(\"width\", widthScale(0.8))\n", " .attr(\"height\", function(d) { return heightScale(d); })\n", " .attr(\"fill\", \"teal\");\n", " \n", " //Add a text element to our div\n", " d3.select(\"#d3-update\").append(\"p\").text(\"Click here to update our data.\")\n", " \n", " //On click, update with new data.\n", " d3.select(\"#d3-update\").select(\"p\")\n", " .on(\"click\", function() {\n", "\n", " //New values for dataset\n", " dataset = [ 11, 12, 62, 20, 18, 17, 16, 18 ];\n", "\n", " //Update all rects\n", " svg.selectAll(\"rect\")\n", " .data(dataset)\n", " .attr(\"y\", function(d) {\n", " return yScale(d);\n", " })\n", " .attr(\"height\", function(d) {\n", " return heightScale(d);\n", " });\n", "\n", " });\n", "});" ], "metadata": {}, "output_type": "display_data", "text": [ "" ] } ], "prompt_number": 334 }, { "cell_type": "markdown", "metadata": {}, "source": [ "What did we do here? We simply added a new paragraph element to the end of our div, then we **bound** a javascript listener to that paragraph.\n", "\n", "When we *bind* a listener to the element, it means that we specify a function to be executed when the listener is triggered - in this case when the text in the paragraph element is clicked.\n", "\n", "In this function, we define a new dataset, and then *bind* the data to all `rect` objects in our svg. That is - we overwrite the old data values with the new ones, and then update the data elements accordingly." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Transitions\n", "\n", "Ok, so we can make the data change, but that change was kind of abrupt.\n", "\n", "D3 has some **black magic** built in that lets us animate the data. \n", "\n", "Try adding the following two lines after \".data(dataset)\" in the \"update all the rects\" box.\n", "\n", " .transition()\n", " .duration(2000)\n", " \n", "Did you see that? Two extremely simple lines of code and D3 **animated** our chart for us. \n", "\n", "What's more - you can change the functions it uses to animate. Try adding:\n", " \n", " .ease(\"bounce\")\n", " \n", "Below `duration(2000)`\n", "\n", "D3 Provides several easing functions that can animate not just shape changes, but also color changes, too.\n", "\n", "### DIY\n", "\n", "1. Try a different argument to `ease`: available options include: \"linear\", \"circle\", \"elastic\". Can you describe how these are different from \"bounce\".\n", "2. D3 Also lets us introduce delays. These can be done on a per element basis. Add `.delay(function (d,i) { return i*200; })` beneath `transition` above. What happens?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##Interactivity\n", "\n", "Now that we know how to make static visualizations, and we have an understanding about how we can handle updates and changes to our charts - let's put that knowledge together to make *interactive* visualizations.\n", "\n", "Let's make a final version of our chart in which we'll add some interactivity. The effect we're creating is the following:\n", "\n", "1. When a bar is moused over, we're going to make it \"red\".\n", "2. When a bar is moused over, we're going to display a tooltip with the current data value next to the bar.\n", "3. When the mouse moves off a bar, the tool tip should be hidden.\n", "\n", "We've actually already seen a little bit of interactivity - when we clicked the paragraph text the chart updated, and we're going to use a very similar trick to handle the mouse interaction." ] }, { "cell_type": "code", "collapsed": false, "input": [ "%%html\n", "
" ], "language": "python", "metadata": {}, "outputs": [ { "html": [ "
" ], "metadata": {}, "output_type": "display_data", "text": [ "" ] } ], "prompt_number": 302 }, { "cell_type": "code", "collapsed": false, "input": [ "%%javascript\n", "\n", "require.config({paths: {d3: \"/files/d3.v3.min\"}});\n", "\n", "require([\"d3\"], function(d3) {\n", " //Setup local variables.\n", " var w = 500;\n", " var h = 400;\n", " var barPadding = 20;\n", " var padding = 50;\n", " \n", " //Set up our dataset.\n", " var dataset = [ 15, 12, 21, 42, 12, 10, 5, 85 ];\n", " \n", " //Define your scales here\n", " var xScale = d3.scale.linear()\n", " .domain([0, dataset.length])\n", " .range([padding,(w-padding)]); \n", " \n", " var widthScale = d3.scale.linear()\n", " .domain([0, dataset.length])\n", " .range([0, (w-2*padding)]); \n", " \n", " var yScale = d3.scale.linear()\n", " .domain([0, d3.max(dataset)])\n", " .range([h-padding,padding]);\n", "\n", " var heightScale = d3.scale.linear()\n", " .domain([0, d3.max(dataset)])\n", " .range([0,h-2*padding]);\n", " \n", " //Clear out old content, then create an svg element that's w x h in size.\n", " d3.select(\"#d3-interactive\").selectAll(\"*\").remove(); \n", " \n", " var svg = d3.select(\"#d3-interactive\")\n", " .append(\"svg\")\n", " .attr(\"width\", w)\n", " .attr(\"height\", h);\n", " \n", " //Bind our data to SVG and create a rectangle for each one\n", " //This is where the magic happens!\n", " var bars = svg.selectAll(\"rect\")\n", " .data(dataset)\n", " .enter()\n", " .append(\"rect\");\n", " \n", " //For each bar, set its attributes as a function of its position\n", " //in the dataset and \n", " bars.attr(\"x\", function(d, i) { return xScale(i); })\n", " .attr(\"y\", function(d) { return yScale(d); })\n", " .attr(\"width\", widthScale(0.8))\n", " .attr(\"height\", function(d) { return heightScale(d); })\n", " .attr(\"fill\", \"teal\");\n", " \n", " //Create a tooltip.\n", " var tip = d3.select(\"#d3-interactive\")\n", " .append(\"div\")\n", " .attr(\"id\", \"tooltip\");\n", " \n", " tip.append(\"p\")\n", " .attr(\"id\", \"value\")\n", " .style(\"text-align\", \"center\");\n", " \n", " tip.style(\"width\", \"30px\")\n", " .style(\"background-color\", \"white\")\n", " .style(\"position\", \"absolute\")\n", " .style(\"border\", \"2px solid\");\n", " \n", " \n", " \n", " //Your code for the DIY goes here.\n", "});" ], "language": "python", "metadata": {}, "outputs": [ { "javascript": [ "\n", "require.config({paths: {d3: \"/files/d3.v3.min\"}});\n", "\n", "require([\"d3\"], function(d3) {\n", " //Setup local variables.\n", " var w = 500;\n", " var h = 400;\n", " var barPadding = 20;\n", " var padding = 50;\n", " \n", " //Set up our dataset.\n", " var dataset = [ 15, 12, 21, 42, 12, 10, 5, 85 ];\n", " \n", " //Define your scales here\n", " var xScale = d3.scale.linear()\n", " .domain([0, dataset.length])\n", " .range([padding,(w-padding)]); \n", " \n", " var widthScale = d3.scale.linear()\n", " .domain([0, dataset.length])\n", " .range([0, (w-2*padding)]); \n", " \n", " var yScale = d3.scale.linear()\n", " .domain([0, d3.max(dataset)])\n", " .range([h-padding,padding]);\n", "\n", " var heightScale = d3.scale.linear()\n", " .domain([0, d3.max(dataset)])\n", " .range([0,h-2*padding]);\n", " \n", " //Clear out old content, then create an svg element that's w x h in size.\n", " d3.select(\"#d3-interactive\").selectAll(\"*\").remove(); \n", " \n", " var svg = d3.select(\"#d3-interactive\")\n", " .append(\"svg\")\n", " .attr(\"width\", w)\n", " .attr(\"height\", h);\n", " \n", " //Bind our data to SVG and create a rectangle for each one\n", " //This is where the magic happens!\n", " var bars = svg.selectAll(\"rect\")\n", " .data(dataset)\n", " .enter()\n", " .append(\"rect\");\n", " \n", " //For each bar, set its attributes as a function of its position\n", " //in the dataset and \n", " bars.attr(\"x\", function(d, i) { return xScale(i); })\n", " .attr(\"y\", function(d) { return yScale(d); })\n", " .attr(\"width\", widthScale(0.8))\n", " .attr(\"height\", function(d) { return heightScale(d); })\n", " .attr(\"fill\", \"teal\");\n", " \n", " //Create a tooltip.\n", " var tip = d3.select(\"#d3-interactive\")\n", " .append(\"div\")\n", " .attr(\"id\", \"tooltip\");\n", " \n", " tip.append(\"p\")\n", " .attr(\"id\", \"value\")\n", " .style(\"text-align\", \"center\");\n", " \n", " tip.style(\"width\", \"30px\")\n", " .style(\"background-color\", \"white\")\n", " .style(\"position\", \"absolute\")\n", " .style(\"border\", \"2px solid\");\n", " \n", " \n", " \n", " //On mouseover, update with new data.\n", " d3.select(\"#d3-interactive\").selectAll(\"rect\")\n", " .on(\"mouseover\", function(d,i) {\n", " tip\n", " .style(\"left\", (padding+xScale(i))+\"px\")\n", " .style(\"top\", yScale(d/2)+\"px\")\n", " .style(\"display\", null)\n", " .select(\"#value\")\n", " .text(d);\n", "\n", " d3.select(this).attr(\"fill\", \"red\");\n", " })\n", " .on(\"mouseout\", function(d,i) {\n", " d3.select(\"#tooltip\").style(\"display\", \"none\");\n", " d3.select(this).attr(\"fill\", \"teal\");\n", " });\n", "});" ], "metadata": {}, "output_type": "display_data", "text": [ "" ] } ], "prompt_number": 330 }, { "cell_type": "markdown", "metadata": {}, "source": [ "The code should look pretty familiar - with one catch - we've added a visual element called \"tooltip\" that is hidden.\n", "\n", "\n", "## Making The Right Selection\n", "\n", "We can bind events to a set of elements by making a selection, which we've already seen before.\n", "\n", "### DIY\n", "\n", "1. Beneath the code above, select all of the `rect` objects in the SVG.\n", "\n", "## Handling the mouse ins\n", "\n", "The code for handling mouse ins is not too difficult. It should look something like this:\n", "\n", " .on(\"mouseover\", function(d,i) {\n", " tip\n", " .style(\"left\", (padding+xScale(i))+\"px\")\n", " .style(\"top\", yScale(d/2)+\"px\")\n", " .style(\"display\", null)\n", " .select(\"#value\")\n", " .text(d);\n", "\n", " d3.select(this).attr(\"fill\", \"red\");\n", " })\n", "\n", "\n", "Basically, we set the visibility of the tooltip to true by updating its \"display\" attribute to \"true\", update its horizontal and vertical position, and set the color of the current bar to \"red\".\n", "\n", "### DIY\n", "\n", "1. Chain your selection with this code to handle mouse-ins on your selection above.\n", "\n", "## Handling the mouse outs\n", "\n", "With the mouse outs, we'll simply hide the tooltip and then make the bar teal again. The event for a mouseout is called \"mouseout\".\n", "\n", "### DIY\n", "\n", "1. Update the code you just wrote to handle the mouseouts. To hide the tooltip, set it's \"display\" style to \"none\". Don't forget your semicolon!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# What else can D3 do for me?\n", "\n", "So, we've seen some of the features that D3 provides out of the box, but there's much more - here are a few other things D3 can help you do.\n", "\n", "## Layouts\n", "Contrary to what the name implies, a layout does not actually handle how visual elements are layed out on the screen. Instead, they can be thought of as helper functions that take your input data and transform it to a new data that is easier for certain visualizations to work with.\n", "\n", "Example layouts include functions to translate your data (bar height) into information for a pie chart (width of wedge in degrees and offset), a layout for stacked bar charts, a layout for graph data (nodes and vertices), and *many* cartographic layouts for drawing maps.\n", "\n", "## Utilities\n", "D3 also includes tools to load up CSV and JSON files from a URL. In this case, your *data* becomes a record per row of your file, rather than a list of numbers. Handling this is slightly more complicated, but it's very similar to what we did above.\n", "\n", "## More Complicated Shapes\n", "To handle shapes beyond basic circles and lines, SVG supports the concept of `path`s. A path is like a digital line. You start your pen on a canvas, move to some other point, and continue until you pick your pen up. Optionally you might choose to fill in the area created by your curve.\n", "\n", "A triangle in SVG looks like this:\n", "\n", " \n", " \n", " \n", " Example triangle01- simple example of a 'path'\n", " A path that draws a triangle\n", " \n", " \n", " \n", "\n", "As you can imagine, drawing complicated curves like this would be tricky, so d3 provides a `line` method to help you draw lines more easily.\n", "\n", "# Moving Up The Stack\n", "\n", "Ok - so D3 is pretty cool, but manipulating shapes and positioning them on the canvas *just so* feels pretty low-level. You might be asking yourself \"Aren't there libraries like Matplotlib where I can say, 'here's my data, give me a bar chart.'?\" The answer is, YES!\n", "\n", "Some options:\n", "\n", "1. [Matplotlib](http://matplotlib.org/) - There is a backend for Matplotlib, [mpld3](http://mpld3.github.io/), which converts Matplotlib `Figure` objects to D3 visualizations. It has iPython notebook integration built in.\n", "2. [Vega](http://trifacta.github.io/vega/) by Trifacta - is a declarative layer for designing visualizations built on top of D3. Follow's Wilkinson's **Grammar of Graphics**. There is an early python wrapper called [Vincent](https://github.com/wrobstory/vincent) that lets you generate visualizations from python. The Grammar of Graphics is what allows R to built sophisticated plots in readable, concise, one-liners with the `ggplot2` package. `ggplot2`'s invtentor, Hadley Wickham, now actively contributes to Vega.\n", "\n", "So - try making those your first stop when designing an interactive visualization - it may be that you can create what you want in just a few lines of code, rather than spending hours fiddling with Javascript and SVG." ] } ], "metadata": {} } ] }