The big leagues

This is the 7th and last post in my Visualization with React series. Previous post: Creating a React visualization web app

We can finally take the little wheels off and create a real React app.
That’s what we’ll make:
App screenshot
You can try the app for yourself here. The code is available on github.

This time, instead of loading data from a paltry CSV file, we’ll go live and hit the openweathermap API and get live weather data.

The app may be more complex than anything we’ve done before, but it’s really more of the same, so I’ll be probably quicker.

First, look at the secrets.json file. As you can guess, it can’t work as is – if you want to try this at home, you must get your own (free) API key from openweathermap.

Next, let’s take a look at our constants.js file.
At this stage, you might ask: why not put the API key in that constants file? Well, it’s good practice to never store your API keys in your code. So, I put it in a separate file.
Constants.js is pretty simple. It has: a url prefix (that of the openweathermap api). If they change it, I can just edit the constants file. The CITY_ID holds the identifier of San Francisco. You can replace it by whatever you want (it’s not too difficult to find the city_id for any given city at the OWM web site).
Finally, I have a json object called KEYS where I store what I’m interested in getting from the API.

Let’s move to the App.js file, which is the top-level react file.

import React, { Component } from 'react';
import './App.css';
import '../node_modules/react-vis/main.css';
import {json} from 'd3-request';
import * as CONSTANTS from './constants';
import secrets from './secrets.json';
const {API} = secrets;

Note that I can import my json file like a code file.

We start, unsurprisingly, by creating an App component, and by a constructor method.

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      highlighted: null
    };
    this.highlightX = this.highlightX.bind(this);
    this.processResults = this.processResults.bind(this);
  }

We initialize the state, and bind two methods. Nothing we haven’t seen.

  componentWillMount() {
    json(`${CONSTANTS.QUERY_PREFIX}?id=${CONSTANTS.CITY_ID}&appid=${API}&units=imperial`,
      this.processResults);
  }

Things get interesting with the componentWillMount method.
We use our d3-request json function to read the url in the first line, which we compose by joining the URL prefix, the city id, the api key, and I’ve added ‘units=imperial’ so that temperatures are in Farenheit. Feel free to change this to units=metric or remove it (in which case temperatures will be in Kelvin, why not).
The second argument of the json function is what is done with the request – this is our first private method, processResults, which is what comes next.

 processResults(error, queryResults) {
    if (error) {
      this.setState({error});
    }
    const data = CONSTANTS.KEYS.map(key => ({
      key,
      values: queryResults.list.map((d, i) => ({
        i,
        x: d.dt * 1000,
        y: d[key.key1] ? d[key.key1][key.key2] || 0 : 0 
      }))
    })).reduce((prev, curr) => {
      return {...prev, [curr.key.name]: curr.values}
    }, {
      'city-name': (
        queryResults &&
        queryResults.city &&
        queryResults.city.name
      ) || 'Unkown'
    });
    this.setState({data});
  }

If the data can’t load, error will have a value, and we’ll pass it to the state.
Else, we’re going to process the result of the query (queryResults) according to the structure that we want (CONSTANTS.KEYS).
Here, I’m using a sequence of map and reduce.
Map turns an array into another array of the same shape. Reduce turns an array into something else, such as an object as here.
queryResults is an object which has a list property. queryResults.list is an array of nested objects. This is why each entry of CONSTANTS.KEYS specifies two keys.
To simplify, one of these objects could look like this:

{
  "dt": 1471413600,
  "main": {
    "temp": 57.22,
    "temp_min": 53.32,
    "temp_max": 57.22,
    "pressure": 1017.57,
    "sea_level": 1025.63,
    "grnd_level": 1017.57,
    "humidity": 100,
    "temp_kf": 2.17
  },
  "weather": [
    {
      "id": 500,
      "main": "Rain",
      "description": "light rain",
      "icon": "10n"
    }
  ],
  "clouds": {
    "all": 24
  },
  "wind": {
    "speed": 3.42,
    "deg": 234.501
  },
  "rain": {
    "3h": 0.02
  },
  "sys": {
    "pod": "n"
  },
  "dt_txt": "2016-08-17 06:00:00"
}

So, if I’m interested in the temperature, I have to get it from main, then temp. (two keys)
For each entry of CONSTANTS.KEYS, I’m mapping queryResults to create a time series of objects with three properties: in x, a date in timestamp format (number of milliseconds since 1970). in y, the value I’m interested in. and in i, the index of that object in the time series.

When I’m done mapping CONSTANTS.KEYS, I have an array of such time series, or more exactly: an array of objects with a name property (which comes from CONSTANTS.KEYS) and a values property (the array of objects described above).

Finally, I’m reducing it to an object using reduce.
The reduce method works like this:

myArray.reduce(function(prev, curr) {
  // operation involving the previous result (prev) and the current element of the array (curr)
  return resultOfThatOperation;}, // which becomes prev in the next loop
  initialValueOfReduce) // which will go into prev the first time 

What mine does is turn that array into an object. The keys of that object are the name property of each element, and the values are what’s behind the values property of that object (our time series).
And that final object has an extra property: city-name, the name of the city for which weather is being queried, if it exists.

When this object is created, we send it to the state.

  highlightX(highlighted) {
    this.setState({highlighted});
  }

highlightX is our other private method. What it does is send whatever it’s passed to the state. But since we create it here, it will pass that to the state of App, the top level component. If that state is changed, all the children (ie everything) may be re-rendered.

Finally, we have our render method.
We’ll skip on the styling – codewise, we’ll see that this method calls two components, LargeChart and SmallChart, with similar properties:

<LargeChart
  highlighted={highlighted}
  highlightX={this.highlightX}
  series={data.Temperature}
  title='Temperature'
/>

// ...

<SmallChart
  highlighted={highlighted}
  highlightX={this.highlightX}
  series={data.Pressure}
  title='Pressure'
/>

highlighted comes from the state of App.
highlightX is the callback method. We’ve seen this before – we give children components the ability to change the state of their parent, and we also pass them the result of that change.
Else, we pass to each component a part of our data object, and a different title.

Let’s move on to the large chart component:

import React, { Component } from 'react';
import moment from 'moment';
import {
  LineSeries,
  makeWidthFlexible,
  MarkSeries,
  VerticalGridLines,
  XAxis,
  XYPlot,
  YAxis
} from 'react-vis';

const HOUR_IN_MS = 60 * 60 * 1000;
const DAY_IN_MS = 24 * HOUR_IN_MS;

const FlexibleXYPlot = makeWidthFlexible(XYPlot);

so far, we’re importing a bunch of stuff.
the two constants that we create are the numbers of milliseconds in an hour, and in a day, which will be useful in a bit.
FlexibleXYPlot is a special react-vis component that can resize (in width). By using this as opposed to XYPlot with a fixed width, we can make our app layout responsive (try resizing the window!) without having to think too hard it.

export default class LargeChart extends Component {
  render() {
  	const {highlighted, highlightX, series} = this.props;
   	const minValue = Math.min(...series.map(d => d.y));
  	const maxValue = Math.max(...series.map(d => d.y));

  	const yDomain = [0.98 * minValue, 1.02 * maxValue];
  	const tickValues = series.map(d => d.x);

    const labelValues = makeLabelValues(series);

Eh, we could have made a pure functional component, since this one only has a render method and no state.
First, we come up with the bounds of the domain. We don’t have to. Note the use of the spread operator for a very concise way to write this.
We create yDomain with a little bit of margin – we start with 98% of the smallest value, and go to 102% of the maximum value.
If we don’t define a domain, then react-vis will create it based on the data – it will start with exactly the smallest value and end with exactly the highest.

tickValues will be all the different x values.
labelValues will be created by a separate function (in the end). We’ll write a label for every day of our time series at precisely midnight.

Now we’ll create the chart proper.

<FlexibleXYPlot
    	height={300}
    	margin={{top: 5, bottom: 25, left: 40, right: 0}}
    	onMouseLeave={() => this.props.highlightX(null)}
    	yDomain={yDomain}
    >
    	<VerticalGridLines 
    		values={labelValues}
    	/>
        <HorizontalGridLines />
    	<LineSeries 
    		data={series}
    		onNearestX={highlightX}
    		stroke='#11939a'
    		strokeWidth={2}
    	/>
    	{highlighted ? 
    		<LineSeries
    			data={[
    				{x: highlighted && highlighted.x, y: yDomain[0]},
    				{x: highlighted && highlighted.x, y: yDomain[1]}
    			]}
    			stroke='rgba(17,147,154,0.7)'
    			strokeWidth={2}
    		/> : null
    	}
       {highlighted ?  
       <MarkSeries
          data={[{
            x: highlighted && highlighted.x,
            y: highlighted && series[highlighted.i].y
          }]}
          color='rgba(17,147,154,0.7)'
        /> : null
        }
    	<XAxis 
    		tickSize={4}
    		tickValues={tickValues}
    		labelValues={labelValues}
    		labelFormat={(d) => moment(new Date(d)).format('MM/DD')}
    	/>
        <YAxis 
        	tickSize={4}
        />
    	
    </FlexibleXYPlot>

The first interesting thing is the onMouseLeave property of FlexibleXYPlot. If the mouse leaves the chart, we’ll use our highlightX callback function to pass “null” to the state of App.
In other words, when the mouse is not on a chart, the value of highlighted is null.

A bit later, we see that the first LineSeries has an onNearestX property. What it does is that when somebody mouseovers the chart, it sends the underlying datapoint to the state of App. Remember that these datapoints are objects with three property: x, i and y.

So, at a given moment, highlighted is either null, or of the form: {x: (a date in timestamps format), y: (a value), i: (the position of that datapoint in the time series, ie 10 if that’s the 10th point)}.

Let’s go on.
There’s an interesting construct between curly braces. Remember that in JSX, whatever is between curly braces will be evaluated. Here, we have a ternary operator (condition ? true : false). If highlighted exists, then we will create a LineSeries, else nothing.
What this means is that if there’s a value for highlighted, we are going to draw a vertical line that spans the whole chart, at the level of the mouseover.
We then have a similar construction for a MarkSeries, which, likewise, draws a circle at the same position as the highlighted data point.

Finally, we create the axes. The XAxis has a few interesting properties: tickValues, which we defined above – all the possible x values, labelValues and labelFormat. labelValues determine where the labels will be drawn. Finally, labelFormat determines what will be drawn, as a function of each value in labelValues.

export function makeLabelValues(series) {
  const firstDate = new Date(series[0].x);
  const firstDateHour = firstDate.getHours();
  const firstMidnight = series[0].x + (24 - firstDateHour) * HOUR_IN_MS;

  return [0, 1, 2, 3, 4].map(d => firstMidnight + d * DAY_IN_MS);
}

the last part of this module creates those label values.

Our time series has a number of elements (normally 39), every 3 hours. But the first element is probably not the very beginning of a day. We’d like our labels to be exactly aligned with the start of a day (midnight).
So we are going to figure out what time it is on the first element of the time series, compute when exactly is the next day, and then create an array of 5 values corresponding to the time we just computed, then the time 24 hours after (start of the next day), the time 24 hours after that, etc.
As a result, we’ll have a list of 5 ‘midnights’ exactly a day apart.

Finally, let’s look at SmallChart.

SmallChart is very similar to LargeChart. The styling is a bit different (these charts are, well, smaller). The Smallchart component also have an onNearestX hook that sends a datapoint to the state of App.
Unlike LargeChart, it doesn’t draw a vertical line; it just draws a dot on the curve corresponding to the highlighted time.

So, since App sends the same highlighted to all chart components, mousing over any chart makes that dot appear on all charts. How do we know where to draw it?
If I mouseover on a chart, onNearestX will send to its corresponding callback function the underlying datapoint, that has an x and a y property (and possibly others). If I wanted to draw a dot on that same chart, that would be easy – I already know the x and y coordinates where I would have to draw it. But how can I draw a dot on all the other charts?
This is why I added that i property to the timeseries to begin with. When I mouseover on a point on any chart, the object I send to App’s state has that i property. In other words, I can know that I’ve mouseovered the 10th point of the chart (or the 17th, or the 3rd, etc.). When the LargeChart and SmallChart components will draw the dot, they will draw it on top of the 10th (or 17th, or 3rd… well, the i-th) point of their own chart.

<MarkSeries
  data={[{
    x: highlighted && highlighted.x,
    y: highlighted && series[highlighted.i].y
  }]}
  color='rgba(17,147,154,0.7)'
  size='2'
/>

That’s what series[highlighted.i].y means.

And that’s it! A complete dashboard with linked charts. You can tell that temperature is lower at night, and probably lower when there are clouds, but then again it’s San Francisco so it’s probably always 60 more or less.

This project could be much more complex – we could have added a tooltip like in our previous app… added extra series… and arranged info in a lot of different ways. We could also let the user change the city through a text box. Well, you can make your own version!

(if you’re curious why I haven’t hosted this app on github, it’s because we can only access an HTTP version of the OWM API for free, and since github pages are hosted over HTTPS those requests will be blocked.)

Now, what

Congratulations for reading that far, you are awesome.
So you want to take your react game to the next level and do amazing visualizations?
You should continue exploring react-vis. I’ve only used a very basic case. We use it at Uber to create pretty elaborated charts and dashboards.
Our visualization team is also behind three amazing open source libraries which play well with React: React-Map-GL, to create maps with MapBox and React; Deck.gl, to create webGL layers over maps, and Luma.gl, a WebGL framework.

One of the best features of React is that it has so many existing modules. React-motion is, IMO, the best way to handle animation in React as of now, especially when SVG elements are concerned. Check out that great article by Nash Vail.

The React project page is an evident resource. React is alive, keeps adding features, and there is constantly more code out there using react and pushing the envelope.

The Airbnb style guide is a solid reference for writing good modern javascript. We don’t follow it at Uber; we have slightly different rules, which are not published. Anyway, the best part of such a system is its consistency, and Airbnb style guide is definitely consistent.

React Rocks! is a great collection of examples and a source of inspiration.

Once your application become complex enough, it’s difficult to handle states and callbacks everywhere. You can use a store to address this. Right now the state of the art solution is redux. The author of Redux, Dan Abramov, gives an amazing video tutorial which doubles as an excellent showcase for React and ES6.

 

One thought on “The big leagues

Leave a Reply