A "Purple" Map of the 2024 US Presidential Election
(This is Part 1, a single state.)
I went looking for a "purple map" of the 2024 presidential election
— one of those maps that colors areas from red to blue depending
on how they voted.
And I couldn't find one! Well, I found lots of JPEGs and PDFs and such, but I couldn't find a single map that was interactive and let me zoom in and actually see the county-level data I was interested in.
Getting Data
It's not for lack of data. I'm happy to report that this year,
searching for 2024 presidential election county data
got several useful hits. I settled on the
MIT Election Lab data,
which has a GitHub
repository with a bunch of state-level files (that, weirdly, are all
zipped, so you have to unzip each one individually).
Then I needed GIS data for voting precincts. For New Mexico, I get data like that from UNM RGIS, though on the day I was working on this project, RGIS was down so I used a precinct map I'd downloaded earlier for the LWVNM Voter Guides.
What about national precinct data? I thought that would be easy; I
thought somewhere like the US Census had that. Maybe they do, but
their search makes it impossible to find; any search for keywords like
Voting Precincts comes up with things like the American
Community Survey data, not line data for geographic areas.
I had similar problems at data.gov, but after a bit of clicking around
I eventually found my way to
2020
Census Voting District (VTD) for United States, and converted the
shapefile to geojson for easier access:
ogr2ogr -f GeoJSON -t_srs crs:84 US-VTD.json cb_2020_us_vtd_500k.shp
Merging Voting Data with Precinct Data
At first, for testing, I used the NM precinct data file since it was so much smaller. (In fact, in the beginning I started by using only Los Alamos county, since it's tiny and made testing much faster, and in any case, the actual question I'd been trying to answer that sparked this investigation concerned a comparison between districts in White Rock.) It looks like this:
"features": [
{
"type": "Feature",
"properties": {
"ID": 1,
"GEOCODE": "35053000011",
"STATE": "35",
"VTD": "000011",
"VTD_NUM": 11,
"POP": 728,
"COUNTY_NAM": "Socorro",
"COUNTY_FIP": "053"
},
"geometry": {
"type": "Polygon",
"coordinates": ...
To the "properties" list, I wanted to add some additional fields, pulled from the individual_states/nm24.zip.
"R-vs-D": 0.6632016632016632,
"INDEPENDENT": 6,
"REPUBLICAN": 319,
"LIBERTARIAN": 0,
"DEMOCRAT": 162,
"SOCIALISM AND LIBERATION": 0,
"GREEN": 0
"R-vs-D" is calculated from the numbers in the CSV: it's R / (R + D), in other words, a number from 0 to 1 where 1 means all Republican votes (plus minor parties) and no Democrats, and 0 means all Dems and no Reps. It's the number that will be used to decide how red or blue each precinct should be colored.
Generating this is straightforward; if you want to see the details, you can read the code, purplemap.py on github>.
Making a Map
The easiest way I've found to create an interactive web map is with Python Folium. It writes all the JavaScript for you, so you can concentrate on preparing the data.
It's very simple to make an initial map:
def make_map(jsonfilename):
m = folium.Map(location=(34.4, -106.1), zoom_start=8)
Folium can read its data from a GeoJSON file, and it can make popups that show data when you click on a region. But what to show in the popup? I decided that I wanted to see the votes for all the parties, major and minor; the R-vs-D fraction that I'd generated; plus county name, precinct number, and total votes. (Unfortunately, the US VTD data doesn't have county name or population; I'll have to get county name from some other source and mix it in, and if I want population per voting precinct, that sounds like another data hunt and I'm not sure I care that much.)
So that code looks like this, using the fields from the NM VTD data:
# Should get this from reading the geojson, but
# what a drag to have to read such a huge file as a separate step
# just to get the list of parties
parties = ['REPUBLICAN', 'DEMOCRAT', 'LIBERTARIAN', 'GREEN',
'INDEPENDENT', 'SOCIALISM AND LIBERATION']
folium.GeoJson(jsonfilename, name="Voting",
style_function=purplestyle,
highlight_function=highlight_feature,
# popups on click:
popup=folium.GeoJsonPopup(
fields=['COUNTY_NAM', 'VTD_NUM', 'POP', 'R-vs-D']
+ parties,
aliases=['County:', 'Precinct:', 'Population', 'R-vs-D']
+ parties,
),
Note the style_function and highlight_function. The style function is what generates the purple color for the layer. It looks like this:
def purplestyle(feature):
"""Style each precinct according to its R-vs-D votes"""
if 'R-vs-D' in feature['properties']:
rvd = feature['properties']['R-vs-D']
fill_color = '#%02x00%02x' % (int(0xff * rvd), int(0xff * (1. - rvd)))
else: # No votes for either Republicans or Democrats
fill_color = '#ffffff'
return {
'fillColor': fill_color,
'color': 'white',
'weight': 1,
}
If there are R and D votes, then calculate a color where the red component shows Republican votes, the blue component shows Democrat votes and the green component is zero. If there are no votes for either party (precincts where no one lives, like wilderness land), color the precinct white. Either way, give it a white border.
The highlight function is called when you mouse over a precinct. It's much simpler: just turn the border black and make it thicker.
def highlight_feature(feature):
"""Called when mousing over a feature"""
return {
'color': 'black',
'weight': 3
}
Finally, the OpenStreetMap basemap that Folium automatically adds is helpful for figuring out where you are, but it makes it hard to compare colors in different precincts because they're blended with the colors from the basemap. So add an optional empty basemap that lets you turn off the OpenStreetMap layer:
folium.TileLayer("", name="No basemap", attr="blank", show=False).add_to(m)
folium.LayerControl().add_to(m)
The map is ready! Save it to an HTML file, and print out the name of the file so you can open it in your browser.
htmlfilename = "map.html"
m.save(htmlfilename)
print("Saved to", htmlfilename)
I started to extend this to the full US dataset,
but it got tangled because of the chaotic precinct names
in the US dataset, and I decided I needed to postpone that project
until at least after the NM Legislative session (I'm fairly busy right
now with
NM BillTracker updates).
So there may be a Part II.
[ 12:41 Jan 27, 2026 More programming | permalink to this entry | ]