Building CampaignHawk: Voter Filter Data Layer (Part 22)

Sam Corcos
5 min readSep 29, 2015

--

We now have two data layers between which we can toggle. I’m thinking it would be good to have one more data layer with more dynamic data that can be changed from the user interface, such as a slider that lets the user filter out unlikely voters. When it’s done, your map should look like the example below.

This is going to start out looking like the createAllVotersLayer, so we’ll just copy that function and name it createFilteredVoterLayer:

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

The next thing we need to do is figure out the best way to let the user filter out unlikely voters. We should be able to do this in our sidenav popout.

For now, we’re going to leave the styling as the HTML default, but in order to do so, we need to make our input styling more specific. Change input to the following in styles.scss:

[type="text"], [type="email"], [type="phone"] {

Then within our DataLayerPopoutContent component, we need to add the slider. We’re going to create it as a separate element within our render function because we want its presence to be controlled by state (we only want it visible if the “Filter Non-Voters” radio button is selected).

We want the value of our slider to be bound to state, so we’re going to need an onChange function and a defaultValue that’s held in state. First let’s create the component, then we can create all the tangential functions:

let filterRange = (
<div className="filter-voter-range">
<span>0%</span>
<input type="range" onChange={this.handleVoterSliderChange} defaultValue={this.state.voterSliderValue}/>
<span>100%</span>
</div>
)

Within DataLayerPopoverContent, let’s define the initial state of the voter slider. We want it to start at zero because we want the filter to start out by not filtering anything.

getInitialState() {
return {
voterSliderValue: 0
}
},

Then we need a function to call onChange, which we will call handleVoterSliderChange, which we will leave empty for the time being:

handleVoterSliderChange(e) {
...
},

And in order to render the slider, we need to add it to and rename our last input/label in our list:

<input 
onChange={this.handleDataLayerChange}
type='radio'
name='data-layer-group'
id='filter-non-voters' />
<label
htmlFor='filter-non-voters'>Filter Non-Voters</label>
{filterRange}

If you look at it now, the positioning is not exactly right, so let’s add some styling to filter-voter-range to fix that:

ul {
list-style-type: none;
li {
.filter-voter-range {
white-space:nowrap;
}
}

}

At this point, we have a slider that looks like the image below.

Now we need to pass the value from the slider change up to a parent component. Since it’s MapChild that controls what is displayed on the map, we need the function that refreshes the map with the filtered data to live in the MapChild component.

This function is removing the previous layer, creating a new one based on the input from the slider, then adding that new layer to the map.

refreshVoterFilterLayer(value) {
if (map.hasLayer(filteredVoterLayer)) {
map.removeLayer(filteredVoterLayer)
}
createFilteredVoterLayer(value)
map.addLayer(filteredVoterLayer)
},

Then we need to pass refreshVoterFilterLayer all the way down DataLayerPopoutContent as props. Once that’s done, we can change our handleVoterSliderChange to call our refreshVoterFilterLayer function.

handleVoterSliderChange(e) {
this.props.refreshVoterFilterLayer(e.target.value)
},

So now when we change the slider, the value is passed up to the refreshVoterFilterLayer function. In order to make this work, we need to change the filterVoterDataLayer function to filter out values that are less than the input value. This is pretty easy using _.filter. Then change the GeoJSON input to the new filteredVoters array.

let votingPercentage = value / 100;
let filteredVoters = _.filter(VoterDataGeoJSON.find().fetch()[0]
.features,
function(feature) {
return feature.properties.history > votingPercentage
}
)
...
let dataLayer = L.mapbox.featureLayer().setGeoJSON(filteredVoters)

Then at the end of our function, we need to return the layer. But we want to add a conditional before we return the layer. First we want to test if the layer already exists. If so, we want to update it to the new layer. Otherwise, we just return the current layer.

if (typeof filteredVoterLayer !== "undefined") {
return filteredVoterLayer = clusterGroup.addLayer(dataLayer)
}
return clusterGroup.addLayer(dataLayer)

Then we need to call our createFilteredVoterLayer in our Tracker.autorun the same way we created our other layers, except this time we need to call it with a default value of zero:

createFilteredVoterLayer(value=0)

And that should do it!

Next Steps

The next thing we could do is make a transition for the slider so it only appears when the filter layer is chosen, but that’s not a major feature at this point. We could also work on adding some content to our modals so the campaign manager can fill out forms to add volunteers.

But I think this is a good point to start writing a few end-to-end tests with Cucumber. We’ve written enough code to where we might start breaking functionality if we refactor (which is becoming increasingly necessary).

--

--

Sam Corcos

Software developer, founder, author - CarDash - Learn Phoenix - SightlineMaps.com