Mapping in React and D3

A lot of folks today are talking about React, the Javascript framework that lets you build performant apps and UIs quickly and efficiently. I use React almost everyday at my job and in my own open source projects, often pairing it with Redux for state management.

But as a cartographer, my ultimate assessment of any framework comes down to its ability to help me make beautiful, engaging maps and visualizations. I've worked quite a bit with the ArcGIS Javascript API in React to make slippy maps, but I haven't fallen in love with it. D3, on the other hand (which ranks among my favorite libraries out there), is a lot more fun to play with in React. It's also a good bit more difficult to work with because of how differently it interacts with the DOM. While React uses a virtual DOM model to parse UI updates and render the proper HTML as a function of props and state, D3 just operates directly on the DOM, adding and removing elements as you please. This makes D3 quite a bit more opinionated than React. In this piece, I'll go over the two main strategies I've found for working with, and mapping with, D3 and React.

Before we get started, please take a read of one of my favorite pieces on this subject, which will be particularly useful for those of you who are new to React. Done? No really, go read it. It'll help you understand what is to follow. Sweet, on to the good stuff!

When mapping with D3 and React, I like to say that you can either write React code the D3 way or write D3 code the React way. By this I mean to say that you need to let one of these frameworks drive how you handle the DOM in any given situation. Of course, just as nothing in life is an either / or (more often, it's an &), this doesn't preclude you from using both techniques in a single app – on the contrary, I think these two methods are suited to solving different problems. Let's look at two examples, starting with writing React code the D3 way. Our task will be rendering a simple legend for a choropleth map. Let's take a look.

// import React
import * as React from 'react';
// import d3 - run npm install --save d3 to add
// d3 to your node_modules
import * as d3 from 'd3';

class MapLegend extends React.Component {
  
  constructor() {
    super();
    // props, bound methods of your React component
    // etc. can go in here
  }
  
  componentDidMount() {
    // time to make a legend here
    // start by storing all color values in an array
    let colorLegend = ["rgb(247,251,255)", "rgb(222,235,247)", "rgb(198,219,239)", "rgb(158,202,225)", "rgb(107,174,214)", "rgb(66,146,198)", "rgb(33,113,181)", "rgb(8,81,156)"];

    // get the svg that just mounted - this is componentDidMount()
    // so this function gets fired just after render()
    let svgLegend = d3.select('#legend');

    // now just inject standard D3 code
    svgLegend.append("g")
        .attr("transform", "translate(500, 525)")
        .selectAll("rect")
        .data(colorLegend)
        .enter()
        .append("rect")
        .attr("fill", function(d, i){ return colorLegend[i]; })
        .attr("x", function(d, i){ return (i*30); })
        .attr("y", 30)
        .attr("width", 30)
        .attr("height", 20);

    // add a title
    svgLegend.append("text")
        .attr("transform", "translate(500, 525)")
        .attr("font-size", "12px")
        .attr("font-family", "HelveticaNeue-Bold, Helvetica, sans-serif")
        .attr("y", 20)
        .text("Shootings Per Million");

    // add numbers as labels
    let labelsLegend = ["0-1","1-3","3-5","5-7","7-10","10-12","12-15",">15"];
    
    svgLegend.append("g")
        .attr("transform", "translate(500, 525)")
        .selectAll("text")
        .data(labelsLegend)
        .enter()
        .append("text")
        .attr("font-size", "10px")
        .attr("font-family", "HelveticaNeue-Light, Helvetica, sans-serif")
        .attr("x", function(d, i){ return (i*30); })
        .attr("y", 60)
        .text(function(d){ return d; })
  }
  
  render() {
    return <svg id='legend'></svg>;
  }
}

export default MapLegend;

So let's discuss what's going on here. Essentially, we are just using React to render a blank SVG container for us, rather than leaving that to D3. This is useful because it turns this SVG into a component, which – using ES6 classes – has lifecycle methods we can hijack to do anything we want. In this case, using componentDidMount() is the proper choice because it guarantees that the empty SVG is in the DOM before we execute any D3 code to act on it. The contents of the componentDidMount() method should look familiar to anyone who has worked with D3. Here we simply obtain a reference to our SVG in the DOM, append a <g> tag to house the <rect>'s that will make up our legend, add the proper color and text label to each rectangle, and add a title to the legend. Pretty simple, but pretty powerful. I call this writing React code the D3 way because we're really letting D3 drive the way this component renders. React is essentially just providing us an SVG shell into which D3 can put anything we want. Of course, since this legend is now a component, we can generalize it – say by turning the colorLegend or labelsLegend arrays into props that can render anything we hand off to the component.

The other way of doing things is what I like to call writing D3 code the React way. Taking this approach, React takes the reins of explicitly rendering things, to the point where <path> or <circle> elements become components. Let's take a look at how to render a simple map of the United States using this approach.

// import react
import * as React from 'react';
// be sure to import d3 - run npm install --save d3 first!
import * as d3 from 'd3';
// also import topojson so we work with topologically clean geographic files
// this will improve performance and reduce load time
// npm install --save topojson
import * as topojson from 'topojson'
// also import lodash
import * as _ from 'lodash';
// and import a custom component called State
// we'll get to this a bit later
import State from './State';

class Map extends React.Component {

  constructor() {
    super();
    this.state = { us: [] };
    this.generateStatePath = this.generateStatePath.bind(this);
  }
  
  componentDidMount() {
    // send off a request for the topojson data
    d3.json("https://d3js.org/us-10m.v1.json", (error, us) => {
      if (error) throw error;
      
      // store the data in state
      this.setState({
        us
      });
      
      // I prefer to use Redux for state management - if you're taking
      // that approach this would be a good place to dispatch an action, i.e.
      this.props.dispatch(actions.sendAPIDataToReducer({ us }));
    });
  }
  
  // let's define a method for rendering paths for each state
  generateStatePath(geoPath, data) {
    const generate = () => {
      let states = _.map(data, (feature, i) => {
        
        // generate the SVG path from the geometry
        let path = geoPath(feature);
        return <State path={path} key={i} />;
      }):
      return states;
    }
    
    let statePaths = generate();
    return statePaths;
  }
  
  render() {
    
    // create a geographic path renderer
    let geoPath = d3.geoPath();
    let data = this.state.us // or, the reference to the data in the reducer, whichever you are using
    
    // call the generateStatePaths method
    let statePaths = this.generateStatePaths(geoPath, data);
    
    return (
        <svg id='map-container'>
            <g id='states-container'>
                {statePaths}
            </g>
        </svg>
    ); 
  }
}

Alright, so let's dig into this piece of code. Here we are actually generating the JSX for our states inside of a separate method on our component called generateStatePath. You'll notice generateStatePath takes two arguments - geoPath and data. geoPath is just D3's geographic path generator. It what allows us to work dynamically with things like projected coordinate systems in D3 (which is pretty darn sweet for those of us who've spent many a day in hell reprojecting static maps in ArcGIS). The second argument is our data - in this case, our data is a 10m resolution topojson file of every state in the United States, which we fetch using the d3.json method and store in the component's state. Notice I make a comment about using Redux to store the topojson data rather than the component's state. The case for this is that your topojson is available to any component in your application, which becomes very useful for doing analysis or making different maps with the same root data.

The generateStatePath function then simply maps over every feature in the dataset, returning a State component for each. The result is an array of State components, which get rendered inside our <g></g> tag. Let's check out what this State component looks like.

import * as React from 'react';

const State = (props) => {
  
  return <path className='states' d={props.path} fill="#6C7680"
  stroke="#FFFFFF" strokeWidth={0.25} />;
};

export default State;

Our State component is really just an SVG path that gets rendered using the path prop handed down by our parent Map component. That path gets generated in the call to d3.geoPath(feature), which takes the input state feature as a GeoJSON object and outputs an SVG path that can be rendered on our page. You'll notice here we're applying the Stateless Functional Component paradigm to our component. Rather than using an ES6 class, we define our component as a function, which simply returns some JSX based on the props we provide it as an argument.

Here I suggest we're righting D3 code the React way because we're applying React's component hierarchy philosophy to rendering our D3 elements. What I appreciate about this approach is the ability to control the rendering of HTML elements at a very precise level – down to the point where we can write custom functions to handle all sorts of UI scenarios. These elements are now also subject to React's diffing algorithm, meaning that updates to data stored in Redux or in the component's state will trigger updates to these elements on the UI. With something like react-transition-group, you can begin to implement all of the beautiful magic behind D3 transitions using React!

If you're curious to see some of this code in action, check out this Github repo. This is an app I've been developing to help make sense of crowdsourced police shootings data gathered by the Guardian. An older, pure D3 / JS version of the app can be seen on my portfolio or here. I wanted to test myself to see if I could develop the same (or better) experience in React, using other cool technologies like Sass and redux-sagas. In my next post, I'll talk a bit about techniques for responsive typography in map focused layouts using Sass maps. More to come as well on async data loading using redux-sagas and axios. Thanks as always for reading.

Project #9: Building 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 HTML’s <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>
                <select id="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>
                    <option value="popdensity" data="choropleth-density" data-type="cartocss">Population Density</option>
                    <option value="medincome" data="choropleth-bubble" data-type="cartocss">Median Income</option>
                    <option value="povrate" data="choropleth-povrate" data-type="cartocss">Poverty Rate</option>
                    <option value="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>
The result of the above HTML – two clean, user-friendly dropdown menus.

The result of the above HTML – two clean, user-friendly dropdown menus.

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()]();
});
Notice how we changed the dropdown to Brooklyn and we are now zoomed into downtown Brooklyn. jQuery is a powerful framework for handling DOM-Javascript interaction.

Notice how we changed the dropdown to Brooklyn and we are now zoomed into downtown Brooklyn. jQuery is a powerful framework for handling DOM-Javascript interaction.

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 $30,000 – $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 container
var 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 dataset
var 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 dataset
var 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 svg
var 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 scatterplot
svg.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);
Look at that beautifully positioned data! Applying 50% transparency to the points allows overlapping data to be highlighted.

Look at that beautifully positioned data! Applying 50% transparency to the points allows overlapping data to be highlighted.

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 svg
svg.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 svg
svg.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 axes and a title helps to make the data readable and understandable. In this case, we see that there is a strong positive correlation between population density and crash density. But what about other variables.

Adding axes and a title helps to make the data readable and understandable. In this case, we see that there is a strong positive correlation between population density and crash density. But what about other variables.

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.

Notice that we have the Poverty Rate layer selected in the dropdown, the Poverty Rate layer is showing on the map, and the scatterplot is showing the relationship between Poverty Rate and Crash Density. Ah, the beauty of Integrated data views.

Notice that we have the Poverty Rate layer selected in the dropdown, the Poverty Rate layer is showing on the map, and the scatterplot is showing the relationship between Poverty Rate and Crash Density. Ah, the beauty of Integrated data views.

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 dataset
var dataset = data.features; // new data here

// update the circles
svg.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!