Bulding Interactive D3 Dashboards with Carto Web Maps
This post comes from a piece I wrote as part of Azavea's Summer of Maps program. To read the original post, head to the Azavea website.
Dashboards are all the rage these days. The talented folks at Stamen and Tableau have shown just how powerful a well-designed dashboard can be for integrating statistical and geospatial stories on a single web page. I wanted to see if I could emulate their work in an interactive web application I’ve been developing for Transportation Alternatives that explores correlations between traffic crashes and poverty in New York City. But rather than using ready-made software like Tableau, I wanted to build and design the application through code using two powerful open-source tools – CARTO and D3. This post will go through the design process I used for building my dashboard, highlighting some of the key pieces of code needed to get CARTO and D3 talking to one another.
Building from scratch means writing a good bit of code, pulling mostly from the CARTO.js and D3.js libraries. This post does not cover these libraries in depth, but if you’re looking to learn more check out CARTO’s Map Academy or Scott Murray’s D3 tutorials. This post also does not cover basic HTML and CSS, which are needed to add elements and styles to your web page. Learn more about both of these languages from the World Wide Web Consortium (W3C).
Adding Dropdown Menus
To design my dashboard, I began by thinking carefully about user experience. Only essential information should be present to the viewer at any one time. Moreover, it should be easy to navigate. Too many options or poorly labeled graphics confuse even savvy users. For this reason, I placed all layer selectors and zoom selectors into two clean dropdown menus using <select>
and <option>
tags.
<div id="dashboard"><div id="dashboard-title-container"><div id="dashboard-title">EXPLORE DATA ON NYC <br />CITY COUNCIL DISTRICTS</div></div><div id="selector-container"><div id="selector-menu"><form><div style="display: inline-block;"><label for="layer-selector" style="padding-bottom: 5px;">Select a Visualization</label><selectid="layer-selector"class="selector"style="margin-right: 10px;"><option value="intensity" data="torque" data-type="cartocss">Heat Map of Crashes</option><option value="area" data="choropleth-area" data-type="cartocss">Crahes per Square Mile</option><optionvalue="popdensity"data="choropleth-density"data-type="cartocss">Population Density</option><optionvalue="medincome"data="choropleth-bubble"data-type="cartocss">Median Income</option><optionvalue="povrate"data="choropleth-povrate"data-type="cartocss">Poverty Rate</option><optionvalue="unemprate"data="choropleth-umemployment"data-type="cartocss">Unemployment Rate</option></select></div><div style="display: inline-block;"><label for="zoom-selector" style="padding-bottom: 5px;">Zoom to a Borough</label><select id="zoom-selector" class="selector"><option value="bronx">Bronx</option><option value="brooklyn">Brooklyn</option><option value="manhattan">Manhattan</option><option value="queens">Queens</option><option value="statenisland">Staten Island</option></select></div></form></div></div><div id="d3-elements"></div></div>
Dropdown menus are a powerful design choice because they are familiar to a lot of users and can store a lot of options that would take up too much space otherwise. Getting these buttons to do something just involves a bit of jQuery directing what should happen when each option is selected. For example, in the code below, when someone chooses a borough from the zoom selector, the map should pan to that borough.
var ZoomActions = {bronx: function () {var bronxXY = [[40.893719, -73.918419],[40.797624, -73.853531]];map.fitBounds(bronxXY);},brooklyn: function () {var brooklynXY = [[40.695664, -74.048538],[40.581554, -73.8580653]];map.fitBounds(brooklynXY);},manhattan: function () {var manhattanXY = [[40.804641, -73.968887],[40.709719, -73.978157]];map.fitBounds(manhattanXY);},queens: function () {var queensXY = [[40.808, -73.974],[40.691499, -73.752]];map.fitBounds(queensXY);},statenIsland: function () {var statenislandXY = [[40.6534, -74.2734],[40.498512, -74.128189]];map.fitBounds(statenislandXY);}};$('#zoom-selector').change(function () {ZoomActions[$(this).val()]();});
Adding Graphics with D3
The trickier part of the dashboard, of course, is actually adding the good stuff – graphics that respond interactively to user input. To get a sense of what’s possible with D3, check out this gallery. I decided to stick with two classics for my dashboard – the scatterplot and the bar chart.
The scatterplot is particularly powerful as a simple, intuitive graphic that shows relationships between two variables – say, the poverty rate of NYC city council districts against the density of traffic crashes. Making one with D3 involves four major steps: 1) loading data and setting up scales, 2) positioning your data, 3) making some axes, and 4) adding animation. This same basic workflow goes for bar charts as well.
Loading Data and Setting Up Scales
Loading data is fairly straightforward in D3 – just choose the function that matches your file type. I loaded data stored in a GeoJSON using the d3.json
function, but you could load a CSV, for example, using d3.csv
. Once your data is loaded, you can begin to set up scales. Scales can take a while to conceptualize and get comfortable with, but the basic premise is to take a domain of real world data (i.e. median incomes ranging from 120,000) and scale it across a range of pixel values (i.e. over 300px on your monitor). Scales do this math for you and allow your data to be handled dynamically even as it grows. Here’s all the code you need to set up a basic linear scale:
// set the width, height, and margins of your svg containervar margin = { top: 35, right: 35, bottom: 35, left: 35 },w = 300 - margin.left - margin.right,h = 225 - margin.top - margin.bottom;// create the x scale, ranging from 0 to the maximum value in our datasetvar xScale = d3.scale.linear().domain([0,d3.max(dataset, function (d) {return d.properties.pop_density;})]).range([0, w]);// create the y scale, ranging from 0 to the maximum value in our datasetvar yScale = d3.scale.linear().domain([0,d3.max(dataset, function (d) {return d.properties.crash_area;})]).range([h, 0]);
Positioning Your Data
With data and scales in hand, we now need a place to put it. This is where scalable vector graphics (SVG) come in. For now, think of SVG as the container that holds our data; or, for the more artistic types, as our canvas. The key thing to know right off the bat with SVG is that the (0,0) point, from which all attributes are referenced, is in the top left of the container – that’s your starting point. Positioning data, then, is as simple as using the scales you set up above to give your data the appropriate (x, y) coordinates based on their values. You can do this using these few lines of code:
// create the scatterplot svgvar svg = d3.select('#d3-elements') // finds the dashboard space set aside for d3.append('svg') // adds an svg.attr('width', w + margin.left + margin.right) // sets width of svg.attr('height', h + margin.top + margin.bottom) // sets height of svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');// make a scatterplotsvg.selectAll('circle').data(dataset) // gets the data.enter().append('circle').attr('cx', function (d) {return xScale(d.properties.pop_density); // gives our x coordinate}).attr('cy', function (d) {return yScale(d.properties.crash_area); // gives our y coordinate}).attr('r', function (d) {return 4; // sets the radius of each circle}).attr('fill', '#1DFF84').attr('opacity', 0.5);
Making Some Axes
So we have our data, it’s scaled and positioned, but we can’t actually understand the values they represent because we have no axes. Fortunately, D3 axes are a breeze. Essentially, we just call d3.svg.axis()
, feed it our x and y scales, set a few styling parameters, and tack it onto the svg like so:
var xAxis = d3.svg.axis().scale(xScale) // feed the xAxis the xScale.orient('bottom').ticks(5);var yAxis = d3.svg.axis().scale(yScale) // feed the yAxis the yScale.orient('right').ticks(5);// append the x axis to the svgsvg.append('g') // creates a group so we can store lines and tick marks together.attr('class', 'x axis').attr('transform', 'translate(0,' + h + ')').call(xAxis);// append the y axis to the svgsvg.append('g') // creates a group so we can store lines and tick marks together.attr('class', 'y axis').attr('transform', 'translate(0,0)').call(yAxis);
Adding Animation
At this point, we have a beautiful scatterplot, but the data is still static. The whole point of using D3 is to create graphics that change dynamically. That’s where transitions come in. Transitions are exciting, lively, and make your data come to life. We can also wire transitions in the graphics to changes in map layers, such that we get two views of the same data at once.
Fortunately, D3 makes transitions easy to implement. The general idea is to bind the new dataset (the values we’re transitioning to), update our scales to match this dataset, start a transition for a set duration of time, and move the circles to their new positions. Within the Javascript function that controls the dropdown menu, just add a few lines of code:
// update the x scale, no need to update the y scale if the values don’t change.var xScale = d3.scale.linear().domain([0,d3.max(dataset, function (d) {return d.properties.pop_density;})]).range([0, w]);// update your datasetvar dataset = data.features; // new data here// update the circlessvg.selectAll('circle') // select all the data points.data(dataset) // bind the new dataset.transition() // transition!.duration(1500) // for 1500 milliseconds, or 1.5 seconds.attr('cx', function (d) {return xScale(d.properties.pop_density); // get new x positions}).attr('cy', function (d) {return yScale(d.properties.crash_area); // get new y positions}).attr('r', function (d) {return 4;}).attr('fill', '#1DFF84').attr('opacity', 0.5);
Adding transitions gets a bit more complicated when the size of your dataset changes. Doing that requires diving more deeply into D3’s enter
and exit
functions and thinking more carefully about data joins. When in doubt, check out some of the examples by D3 creator Mike Bostock.
Bringing It All Together
By tying your D3 interactivity to your CARTO layer selector, your dashboard and your map can talk to one another. They help to tell the same story in different ways, pointing at nuances in the data that fit the needs of multiple users all in a simple, clean interface. To access the entire source code for this example, head over to this Summer of Maps Github repo. To play with the app yourself, head here. Happy dashboarding!