This is the 6th post in my Visualization with React series. Previous post: Beyond rendering
Playing with codepen is fun, but chances are that you have other ambitions for your visualization projects. In the real world, well, in my day job at least, React is used to create web apps. So, in that last part, we’re going to create our own web app, load some real data, and use some existing modules to visualize it like pros! In this article, we are going to build this:
You can try a demo of the app at https://jckr.github.io/example-react-app/.
All the code is at https://github.com/jckr/example-react-app.
What’s the difference between a web app and a web page you may ask? Well, a web page is an html document with some links to scripts or some inline javascript. A web app is a comprehensive system of code files working together, including a server. All of these files are transformed via a build system, which creates a compiled version that runs on the browser. Such transformations can include JSX support, or supporting ES6/ES7 syntax. Your work can be split in many, easy to read, easy to maintain source files, but your browser will just read one single file written in a version of Javascript it can understand.
That may sound like a lot of work to setup. Up to a few weeks ago, the easiest way to get started was to use a web scaffolding tool such as Yeoman. Scaffolding means that all the little parts that need to be installed or configured to get that going are taken care of, leaving you with a structure that you can use to build your web app upon.
Facebook recently released ‘create-react-app’, which is a simpler scaffolding tool, aimed at simple React apps.
You will need access to a command line environment, such as Terminal on MacOS or Cygwin on windows, and have nodejs and npm installed. (see https://nodejs.org/en/). You will need node version 4 and above. You may want to use nvm to easily change versions of node if needed.
Here’s an illustrated guide to what you need to do to get started:
First, install create-react-app with the command: npm install create-react-app -g.
From the parent directory where you want your app to be, use the command: create-react-app + the name of your app. This will also be the name of the directory this app will be in.
The above command will copy a bunch of files. When it’s done, go to the directory of your app and type npm start…
And lo and behold, the app will start and a browser window will appear with the results!
From now on, whenever you change one of the source files, the app will reload and reflect the changes.
Remember when we did scatterplots, I never really got into doing the menial work of making gridlines, axes etc. Exactly for this reason – it can be a lot of manual work.
But now that we are going to build a professional looking web app, we are going to go all the way.
One component, one file
The app we are building has 3 components: the App, which is the parent; Scatterplot, which is the chart proper and HintContent, for some fine control about what the tooltip looks like.
There an index.html and an index.js file, which are very simple:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>React App</title> </head> <body> <div id="root"></div> </body> </html>
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; import './index.css'; ReactDOM.render( <App />, document.getElementById('root') );
There’s really not much to say about the index.html. All it does it create a div with an id of root, where everything will go.
The index.js ends with the familiar command ReactDOM.render( … ), which will output our component into the aforementioned root div.
But it starts with a few import statements. What they do is that they link the source files together.
Index starts by importing functionalities from react: React and ReactDOM. This was done in our codepen environment by using the settings.
The next two lines link our index.js file with other files that we control: App and index.css. App contains our highest-level component, and index.css contains the styles.
I’ve done some changes in index.css for styles I couldn’t reach with react – styles of the body, for instance, or some styles of elements created by libraries over which I didn’t have direct control (more on that later). Else, I’m using inline styles, in the React tradition.
Let’s move to our App.js source file, which describes the App component.
Its last line is:
export default App;
And this is the line which corresponds to what we had seen earlier in index.js:
import App from './App';
With this pair of statements, inside of index.js, App will be equivalent to what it was inside of App.js when exported.
Using this construct, using import and export, files can be kept short, legible and focused on one specific problem.
But let’s take another look at the first two lines of App.js:
import React, {Component} from 'react'; import {csv} from 'd3-request';
What are those curly braces?
If a module (a javascript file which imports or exports) has a default export, then when importing it, you can just use
import WhatEverNameYouWant from 'module';
Where WhatEverNameYouWant is actually whatever name you want. Typically, it would start by an uppercase letter, but you do you.
But a module can export other things than the default export. In that case, you have to use curly braces. There’s many articles on the subject such as this one. I’m mostly bringing it up so that you see there’s a difference between using the curly braces or not.
In our source files, we are going to use export default. We are also going to define one component only per source file, which makes the whole import / export deal easier to follow.
create-react-app has no dependencies beyond react – which means that it doesn’t need other modules to be installed. But this project does! It needs d3-request and react-vis.
You can install them from the command line, by typing npm install d3-request –save, and npm install react-vis.
The App component – loading and passing data
Our app component will do two things: load the data, and when the data is loaded, pass it to the Scatterplot component so it can draw a chart.
I’ve hinted at the componentWillMount lifecycle method as a great place to load data, so let’s try that!
class App extends Component { constructor(props) { super(props); this.state = {}; }
I’m starting with the constructor method. The only reason why I do that is to initialize the state to an empty object. If I didn’t, the state would initially be undefined. And while it’s not a deal-breaker, I would have to add more tests so that the app doesn’t break before there’s anything actually useful in the state.
componentWillMount() { csv('./src/data/birthdeathrates.csv', (error, data) => { if (error) { this.setState({loadError: true}); } this.setState({ data: data.map(d => ({...d, x: Number(d.birth), y: Number(d.death)})) }); }) }
ComponentWillMount: there we go.
we are using the same data file as before. Here, I’ve hardcoded it in a string, but that address could totally come from a property passed to App.
Also, note I’m using csv from d3. That used to be d3.csv, but not anymore because I’m importing just csv from d3, or more precisely from its sublibrary ‘d3-request’. One of the big changes of d3 v4, also recently released, is that its code is available as smaller chunks. There are other ways to load a csv file, but d3’s csv method is super convenient and it’s also a great way to show how to cherrypick one useful part of a large library.
So, we are loading this file. What’s next? If loading the file raises an error, I am going to signal it via the state (setState({loadError: true});). Else, I am going to pass the content of the file to the state.
Not just the raw contents: I am going to slightly transform it. Inside the csv method, data is an array of objects corresponding to the columns of my file. There are three columns: country, birth and deaths. I have therefore arrays with three properties, and their values is what is being read by csv from the file, as strings.
The map statement turns that object into: a copy of all these properties (that’s what {…d does), plus x and y properties which simply convert the birth and deaths properties of each object to numbers (ie “36.4” to 36.4).
So, whether this file succeeds or fails to load, I’m going to change the state of the component.
What values can the state take?
When the component is first created, state is empty.
Then, componentWillMount attempts to load the file. State is still empty. During that very short time, render will fire (more on that soon).
Then, the file will either load or not. If it loads, state will now hold a data property, and since state changes, the component will re-render. If it doesn’t load, state will have a loadError property and the component will also re-render.
Which takes us to the rendering of the component. You’ll see that these 3 situations are taken care of.
render() { if (this.state.loadError) { return <div>couldn't load file</div>; } if (!this.state.data) { return <div />; } return <div style={{ background: '#fff', borderRadius: '3px', boxShadow: '0 1 2 0 rgba(0,0,0,0.1)', margin: 12, padding: 24, width: '350px' }}> <h1>Birth and death rates of selected countries</h1> <h2>per 1,000 inhabitants</h2> <Scatterplot data={this.state.data}/> </div>; }
if (this.state.loadError) – that’s the situation where the data didn’t load. That’s also why I did initiate state to an empty object, because if this.state was undefined, this syntax would cause an error. (this.state && this.state.error) would be ok, but I might as well just initialize the state.
if (!this.state.data) takes care of the situation where the data didn’t load yet. We also know that there hasn’t been an error yet, else the first condition would have been triggered. In a professional setting, that’s where you’d put a progress bar or a spinner. Loading a 70 line csv isn’t going to take long though, so that would be over the top, which is why there’s just an empty div.
Finally, if neither of these conditions are met, we are going to render a card with a Scatterplot element inside. We’re going to render a little more than just the Scatterplot element – we’re styling the div on which it will stand and adding some titling.
The Scatterplot component: introducing react-vis
React-vis is the charting library we use at Uber.
The main idea is that we can create charts by composing elements, just like a web page:
<XYPlot width={300} height={300}> <HorizontalGridLines /> <LineSeries data={[ {x: 1, y: 10}, {x: 2, y: 5}, {x: 3, y: 15} ]}/> <XAxis /> <YAxis /> </XYPlot>
… creates a very simple line chart with horizontal gridlines and axes. Don’t want the gridline? remove the
Do you need another line chart? You can add another LineSeries element. Or a VerticalBarSeries. Or a RadialChart (pie or donut charts). And so on and so forth.
React-vis handles all the nitty gritty of making charts, so we don’t have to.
Let’s dive into Scatterplot.
import React, {Component} from 'react'; import { Hint, HorizontalGridLines, MarkSeries, VerticalGridLines, XAxis, XYPlot, YAxis } from 'react-vis';
scatterplot.js starts by familiar import statements. We only import what we need from ‘react-vis’.
import HintContent from './hint-content.js';
Then, we import HintContent – hint-content.js uses a default export, so no need for curly braces. By the way, that .js extension is not mandatory in the file name.
export default class Scatterplot extends Component { constructor(props) { super(props); this.state = { value: null }; this._rememberValue = this._rememberValue.bind(this); this._forgetValue = this._forgetValue.bind(this); } _rememberValue(value) { this.setState({value}); } _forgetValue() { this.setState({ value: null }); }
We could have made Scatterplot a pure functional component… if we had passed callback functions for handling mouseover. Those functions could have changed the state of the App component, which would re-render its children – Scatterplot. Since no component outside of Scatterplot is interested in knowing where the mouse is, that component can have its own state.
We are also adding two private functions. They have to be bound to “this” – we are creating a class, and those functions have to be tied to each instance of that class. The other way to think about it is: you can add private functions to a React component, but if they use the state, properties or private variables, you will have to bind them to ‘this’ in the constructor.
render() { const {data} = this.props; const {value} = this.state; return <div> <XYPlot margin={{top:5, left: 60, right: 5, bottom: 30}} width={320} height={290}> <VerticalGridLines /> <HorizontalGridLines /> <XAxis/> <YAxis/> <MarkSeries data={data} onValueMouseOver={this._rememberValue} onValueMouseOut={this._forgetValue} opacity={0.7} /> {value ? <Hint value={value}> <HintContent value={value} /> </Hint> : null } </XYPlot> <div style={{ color: '#c6c6c6', fontSize: 11, lineHeight: '13px', textAlign: 'right', transform: 'rotate(-90deg) translate(120px, -160px)' }}>Death Rates</div> <div style={{ color: '#c6c6c6', fontSize: 11, lineHeight: '13px', textAlign: 'right', transform: 'translate(-5px,-14px)', width: '320px' }}>Birth Rates</div> </div>; }
What we are returning is a div element. The reason is that at the very end, we are writing the name of the axes on that div. But mostly, that div will hold a XYPlot component, which comes from react-vis. I’m passing: a margin property, a height and a width. Margin is optional and I’m using it for control. Height is mandatory, width as well, though react-vis has a responsive component that makes the charts adapt to the width of the page (not used here).
Then, I’m simply adding: horizontal and vertical gridlines, and horizontal and vertical axes. I’m using default settings for all of them (full disclosure – I’ve changed a few things via the index.css stylesheet). But the way labels and lines are organized is fine by me.
Then, we’re adding the MarkSeries component, which is all the circles.
<MarkSeries data={data} onValueMouseOver={this._rememberValue} onValueMouseOut={this._forgetValue} opacity={0.7} />
The data property comes from the properties passed to the Scatterplot component. It needs to have an x and y properties, which is why I transformed our csv file like so. It could also have a size or a color property, but we’re not going to use these in our example.
I’m using an opacity property to better show which marks overlap. I could also have made them smaller, but I’m sticking to the defaults.
Finally, we’re using the onValueMouseOver and onValueMouseOut properties to pass functions to handle what happens when the user is going to, well, mouse over one of the marks, or remove their mouse cursor from them. Those are our private functions from before:
_rememberValue(value) { this.setState({value}); } _forgetValue() { this.setState({ value: null }); }
When a user passes their mouse on a mark, the corresponding datapoint (value) will be passed to the state. And when the user removes their mouse, the value property is reset to null.
finally, right under the MarkSeries component, is where we call our Hint:
{value ? <Hint value={value}> <HintContent value={value} /> </Hint> : null }
If value (from the state) is worth something, then we create a Hint component. That one comes from react-vis, and handles positioning of the tooltip plus some default content. But I want to control exactly what I show inside my tooltip, so I’ve created a component to do just that.
Creating a specialized component like this is great, because it hides this complexity from the Scatterplot component. All that Scatterplot needs to know is that it’s passing properties to a HintContent component, which returns… something good.
Because of imports and exports, it’s generally a good idea to create such small specialized components.
For the win: the hint content component
import React from 'react'; export default function HintContent({value}) { const {birth, country, death} = value; return <div> <div style={{ borderBottom: '1px solid #717171', fontWeight: 'bold', marginBottom: 5, paddingBottom: 5, textTransform: 'uppercase' }}>{country}</div> {_hintRow({label: 'Birth Rates', value: birth})}, {_hintRow({label: 'Death Rates', value: death})} </div>; } function _hintRow({label, value}) { return <div style={{position: 'relative', height: '15px', width: '100%'}}> <div style={{position: 'absolute'}}>{label}</div> <div style={{position: 'absolute', right: 0}}>{value}</div> </div>; }
The HintComponent is a pure functional component and is our default export.
There’s another function in that module, and we’re not exporting it. In other words, it won’t be accessible by other parts of the app. The only place where it can be used is within this file. Traditionally, those start with an underscore.
The only point of the HintComponent is to offer fine control on the appearance of the tooltip (which also receives styles from index.css). But I wanted to control exactly how the various parts of the data point will appear inside.
So, it has 3 rows. The first one contains the name of the country (well, its 3-letter ISO code). I chose to make it bolder and uppercase. Also, it will have a border separating it from the rest of the card.
The next two rows are similar, which is why I created a function to render them as opposed to just retype it.
It’s a relative div, which takes all the space, with two absolute divs as children. The label one has no position information, so its attached to its top left corner, but the value one has a right attribute of 0, so its attached to its top right corner. So, for each row, the label is going to be left-aligned, and the value, right-aligned.
And that’s it!
For our grand finale, we are going to create a more complex application with several charts that interact with one another…
One of the links to your github repo is broken.
fixed, thanks!