Building CampaignHawk: Voter Filter Data Layer (Part 22)
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 is the lead developer and co-founder of Sightline Maps, the most intuitive platform for 3D printing topographical maps, as well as LearnPhoenix.io, an advanced tutorial site for building scaleable production apps with Phoenix and React.
Cumulative Time
Additional
- Building CampaignHawk: An open-source election canvassing app with Meteor and React (Part 1)
- File Structure and Packages (Part 2)
- Styling the Sidenav (Part 3)
- Styling Tooltips (Part 4)
- Connecting Tooltips with React (Part 5)
- Making Modals (Part 6)
- Populating Modals (Part 7)
- Connecting Modals with React (Part 8)
- Mapbox and Data (Part 9)
- Thinking about Data (Part 10)
- Mapbox and GeoJSON (Part 11)
- Mapbox.js and Meteor Data (Part 12)
- Popouts and Radio Buttons (Part 13)
- Radio Button Styling (Part 14)
- Triggering Functions with Radio Buttons (Part 15)
- Spacial Analysis Overview (Part 16)
- Clustering with Leaflet Markercluster (Part 17)
- Making a Precinct Data Layer (Part 18)
- Scaling Colors and Values with D3 (Part 19)
- Styling the Precinct Data Layer (Part 20)
- Toggling Data Layers (Part 21)
- Voter Filter Data Layer (Part 22)