Add user controls for data slicing and dicing
Now comes the fun part. All that extra effort we put into making our components aware of filtering, and it all comes down to this: User controls.
Here’s what we’re building:
It’s a set of filters for users to slice and dice our visualization. The shortened dataset gives you 2 years, 12 job titles, and 50 US states. You’ll get 5 years and many more job titles with the full dataset.
We’re using the architecture we discussed earlier to make it work. Clicking buttons updates a filter function and communicates it all the way up to the App
component. App
then uses it to update this.state.filteredSalaries
, which triggers a re-render and updates our dataviz.
We’re building in 4 steps, top to bottom:
- Update
App.js
with filtering and a<Controls>
render - Build a
Controls
component, which builds the filter based on inputs - Build a
ControlRow
component, which handles a row of buttons - Build a
Toggle
component, which is a button
We’ll go through the files linearly. That makes them easier for me to explain and easier for you to understand, but that also means there’s going to be a long period where all you’re seeing is an error like this:
If you want to see what’s up during this process, just remove an import or two and maybe a thing from render. For instance, it’s complaining about ControlRow
in this screenshot. Remove the ControlRow
import on top and delete <ControlRow ... />
from render. The error goes away, and you see what you’re doing.
Step 1: Update App.js
All right, you know the drill. Add imports, tweak some things, add to render. We have to import Controls
, set up filtering, update the map’s zoom
prop, and render a white rectangle and Controls
.
The white rectangle makes it so the zoomed-in map doesn’t cover up the histogram. I’ll explain when we get there.
// src/App.jsimport MedianLine from './components/MedianLine';// markua-start-insertimport Controls from './components/Controls';// markua-end-insertclass App extends Component {state = {// ...medianIncomes: [],// markua-start-insertsalariesFilter: () => true,// markua-end-insertfilteredBy: {// ...}}// ...// markua-start-insertupdateDataFilter(filter, filteredBy) {this.setState({salariesFilter: filter,filteredBy: filteredBy});}// markua-end-insertrender() {// ...}}
We import the Controls
component and add a default salariesFilter
function to this.state
. The updateDataFilter
method passes the filter function and filteredBy
dictionary from arguments to App state. We’ll use it as a callback in Controls
.
The rest of filtering setup happens in the render method.
// src/App.jsclass App extends Component {// ...render() {// ...// markua-start-deleteconst filteredSalaries = this.state.techSalaries// markua-end-delete// markua-start-insertconst filteredSalaries = this.state.techSalaries.filter(this.state.salariesFilter)// markua-end-insert// ...let zoom = null,medianHousehold = // ...// markua-start-insertif (this.state.filteredBy.USstate !== '*') {zoom = this.state.filteredBy.USstate;medianHousehold = d3.mean(this.state.medianIncomesByUSState[zoom],d => d.medianIncome);}// markua-end-insert// ...}}
We add a .filter
call to filteredSalaries
, which uses our salariesFilter
method to throw out anything that doesn’t fit. Then we set up zoom
if a US state was selected.
We built the CountyMap
component to focus on a given US state. Finding the centroid of a polygon, re-centering the map, and increasing the sizing factor. It creates a nice zoom effect.
And here’s the downside of this approach. SVG doesn’t know about element boundaries. It just renders stuff.
See, it goes under the histogram. Let’s fix that and add the Controls
render while we’re at it.
// src/App.jsclass App extends Component {// ...render() {// ...return (<div //...><svg //...><CountyMap //... />// markua-start-insert<rect x="500" y="0"width="600"height="500"style={{fill: 'white'}} />// markua-end-insert<Histogram //... /><MedianLine //.. /></svg>// markua-start-insert<Controls data={this.state.techSalaries}updateDataFilter={this.updateDataFilter.bind(this)} />// markua-end-insert</div>)}}