Building CampaignHawk: Toggling Data Layers (Part 21)

Now that we have two distinct data layers — clustered and precinct — we need to add the ability to toggle between them when the radio button is changed. This will become increasingly important as we continue to add data layers.

The first thing we need to do is change the DataLayerPopoutContent component to make sure the list items, labels, and inputs have the correct properties. We will keep the first radio button as “no data” because by default, no data is showing. The general format of a list item looks like the code block below.

<li>
<input onChange={this.handleDataLayerChange}
type='radio'
name='data-layer-group'
id='no-data'
defaultChecked="checked" />
<label htmlFor='no-data'>No Data</label>
</li>

Now we need to change a few things to make our second and third radio buttons represent our new data layers. We will eventually pass the id of the list item into the function that controls the data layer, so make sure the id of the input and the htmlFor of the label match. We’ll deal with the handleDataLayerChange later.

Let’s change the first list item to all-voters-layer and the second list item to precinct-layer.

Then we should give some thought to how layer toggling works. Mapbox gives us quite a few useful functions; in this case, we are going to use the hasLayer, removeLayer, and addLayer functions. Our final toggle function is going to look something like the code block below.

if (map.hasLayer(layer)) {
map.removeLayer(layer);
} else {
map.addLayer(layer);
}

We’re also going to have to change our functions around. While we were building the data layers, we wanted to display them immediately, but now we want to create them once at the top of the function so we can reference them later on. So rather than calling addLayer in our allDataLayer function, we want to return the clusterGroup layer so we can reference it. This might sound confusing, but it will make a lot more sense later.

We should also rename a few of our functions so the name more accurately represents the purpose of the function. For example, our allDataLayer should really be called createAllVotersLayer, because what the function actually does is create the layer.

let createAllVotersLayer = () => {
let clusterGroup = new L.MarkerClusterGroup();
let dataLayer = L.mapbox.featureLayer()
.setGeoJSON(this.props.data)
return clusterGroup.addLayer(dataLayer)
}

We should also change the name of our precinctDataLayer function to createPrecinctLayer and modify the code so it returns the data layer:

let createPrecinctLayer = function() {
...
return L.mapbox.featureLayer(scaledPrecinctConcaveHulls);
}

Then, at the end of our if conditional in toggleDataLayers, we need to create all of our layers:

if (!this.props.loading) {
let createAllVotersLayer = () => {
...
}
let createPrecinctLayer = function() {
...
}
let allVotersLayer = createAllVotersLayer()
let precinctLayer = createPrecinctLayer()
} else { ...

So now that we have functions to create each of our layers and we’re calling those functions to create our layers, it’s time to set up toggling.

You may notice that our toggleDataLayer function takes in a layerName and is called by our handleDataLayerChange function in our popout, which passes in the id of the target:

handleDataLayerChange(e) {
this.props.toggleDataLayer(e.target.id)
},

This means that within our toggleDataLayer function, we know the id of the radio button that was selected, so we can use a few conditionals to determine whether or not a particular layer should be shown.

if (layerName === "all-voters-layer") {
map.addLayer(allVotersLayer)
}
if (layerName === "precinct-layer") {
map.addLayer(precinctLayer)
}

Unfortunately, it’s not quite so easy. For one, we’re rebuilding every layer each time we toggle a radio button which will cause some performance issues. But the bigger problem is that when we rebuild the layer, we’re creating a new layer with a different layer id, so we can’t access the old layer to be able to remove it.

First thing we need to do is move our layer-creation functions outside of our component, and in the case of createAllVotersLayer, change our props to VoterDataGeoJSON.find().fetch(). That would take too much code to demonstrate, but it looks like this.

Then we need to make our layers global by moving them inside our Tracker.autorun and inside the Mapbox.ready function:

Tracker.autorun(function () {
let handle = Meteor.subscribe('geojson');
if (Mapbox.loaded() && handle.ready()) {
...
allVotersLayer = createAllVotersLayer()
precinctLayer = createPrecinctLayer()

}
});

The last thing we need to do is make sure we remove the old layers before we add new ones. We can do that by going back into our toggleDataLayer function and adding the code block below, which lists the data layers (which can be easily added to later when we have more layers) and checks to see if any of them are already rendered. If so, it is removed.

let allLayers = [
allVotersLayer,
precinctLayer
]
_.each(allLayers, function(layer) {
if (map.hasLayer(layer)) {
map.removeLayer(layer)
}
})

This is not a particularly elegant solution, but it gets the job done. I’ll leave the refactoring for future me.

Next Steps

We’re going to add one more data layer that displays and filters voters based on their likelihood of voting.


One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.