Public
Edited
Feb 23, 2024
3 forks
22 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
data
Insert cell
curData = data.filter(d => d.State == state & d.Scenario == scenario & d.StatePctRecorded == statePctRecorded)
Insert cell
curStateData = ({
'DemVotes': d3.sum(curData, d=> +d.DemVotes),
'RepVotes': d3.sum(curData, d=> +d.RepVotes),
'TotalVoters': d3.sum(curData, d=> (+d.DemVotes) + (+d.RepVotes) + (+d.RemainingVotes))
})
Insert cell
curStateShape = stateShapes.features.filter(d => d.id == states.get(state).FIPS)
Insert cell
curCountyShapes = countyShapes.features.filter(d => d.id.slice(0,2) == states.get(state).FIPS)
Insert cell
curCountyShapes
Insert cell
shapesWithDataCounty = curCountyShapes // Counties for current state selected
.map(s => {
let curCountyResults = curData.filter(d=> d.FIPS == s.id)[0]
let curColor = curCountyResults && +curCountyResults.DemVotes == +curCountyResults.RepVotes ? '#ccc' :
curCountyResults && +curCountyResults.DemVotes > +curCountyResults.RepVotes ? '#2e74c0' :
'#cb454a';
let colRamp = d3.scaleLinear().domain([0, 1]).range(["#2e74c0", "#cb454a"])
let curColorRamped = colRamp(+curCountyResults.RepVotes / (+curCountyResults.DemVotes + +curCountyResults.RepVotes))
return ({
...s,
...curCountyResults,
color: curColor,
colorRamped: curColorRamped,
centroid: turf.centroid(s),
TotalVoters: (+curCountyResults.DemVotes) + (+curCountyResults.RepVotes) + (+curCountyResults.RemainingVotes)
})
})
Insert cell
colRamp = d3.scaleLinear().domain([0, 1]).range(["#2e74c0", "#cb454a"])
Insert cell
colRamp(0.75)
Insert cell
curStateCities = stateCitiesGeo.get(state)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
packedDotRadius = ( 0.037 * width * mapScale )* (1.5 / 30 ) // radius for inner dot. Should probably change it to function of max donut size
Insert cell
maxPackedDots = 208 // number of dots to put in the biggest County.
Insert cell
packedDotPop = // population associated with 1 dot it biggest County.
d3.max(shapesWithDataCounty, d=> d.TotalVoters) / maxPackedDots
Insert cell
pack = (data,outerRadius) => d3.pack()
.size([outerRadius*2, outerRadius*2])
.padding(1)
.radius(d=> d3.min([packedDotRadius, outerRadius]))
(d3.hierarchy(data));
Insert cell
circlePackData =
circles.map( d=> {
let packedDotCount = d3.max([1, Math.round(d.TotalVoters / packedDotPop)]) // d3.max to ensure dots > 0
let countyPack = pack( {children:d3.range(packedDotCount)}, d.r)
return {
r: countyPack.children.length > 1 ? countyPack.r : d.r,
x: d.x,
y: d.y,
children: countyPack.children.map(c => ({x: c.x, y: c.y, r: c.r, col: circlePackDotColor(d.DemVotes, d.RepVotes, d.TotalVoters)}))
} ;
})
Insert cell
circlePackDotColor = (demVoters, repVoters, totalVoters, colorScale = ({dem: "#2e74c0", rep: "#cb454a", remaining: '#BBB' })) => {
let rand = Math.random()
if (rand < +demVoters / +totalVoters ) {
return colorScale.dem
} else if (rand < (+demVoters + +repVoters)/ +totalVoters) {
return colorScale.rep
} else {
return colorScale.remaining
}
}
Insert cell
Insert cell
curProps = countyPartyProps.get(state).get(scenario).get(statePctRecorded)
Insert cell
dotPop = // population associated with 1 dot.
d3.sum(shapesWithDataCounty, d=> d.TotalVoters) / curDots.length
Insert cell
dotColor = function(propArray, randNum, colorScale = ({dem: "#2e74c0", rep: "#cb454a", remaining: '#fbf418' })) {
if(+randNum < +propArray[0]){
return colorScale.dem
} else if(+randNum < +propArray[1]) {
return colorScale.rep
} else {
return colorScale.remaining
}
}
Insert cell
curDots = stateDots.get(state)
.map( d => {
return {coordsProjected: projection(d.coords),
color: dotColor(curProps.get(d.FIPS), d.rand)
}
})
Insert cell
proportionToHex = x => (Math.round(x * 255)).toString(16);
Insert cell
proportionToHex(0.75)
Insert cell
Insert cell
Insert cell
curStateCentroidCoords = turf.centroid(curStateShape[0]).geometry.coordinates
Insert cell
projection =
d3.geoAlbers()
.rotate([(state == 'Arizona' ? 111.5 : -curStateCentroidCoords[0]), 0])
.center([0, curStateCentroidCoords[1]])
.fitSize([width*mapScale, height*mapScale], curStateShape[0])
Insert cell
Insert cell
mapScale = (sidebarPlotPixels > 0.12 * width ? 1 : 0.75)
Insert cell
height = width * 7/9
Insert cell
bubbleRadiusScale = d3.scaleSqrt()
.domain([0, d3.max(shapesWithDataCounty.map(d=> d.TotalVoters))])
.range([0, 0.035 * width * mapScale])
Insert cell
donutRadiusScale = d3.scaleSqrt()
.domain([0, d3.max(shapesWithDataCounty.map(d=> d.TotalVoters))])
.range([0, 0.037 * width * mapScale])
Insert cell
addStateOutlineToMap = (g) => {
g.append( "path" )
.attr( "d", d3.geoPath().projection(projection)(curStateShape[0] ))
.attr( "stroke", "#888")
.attr( "fill", "none")
.attr( "stroke-width", 1.8)
.attr( "opacity", 0.8 )
}
Insert cell
curStateShape[0]
Insert cell
d3.geoPath().projection(projection)(curStateShape[0] )
Insert cell
curStateShape[0]
Insert cell
addCountiesToMap = (g, mapSpecifier, opacity = 0.8) =>
g.selectAll("path")
.data(shapesWithDataCounty)
.join( "path" )
.attr( "fill",d => (mapSpecifier == "choropleth" ? d.color : "#FFFFFF00"))
//.attr( "fill",d => (mapSpecifier == "choropleth" ? d.color : "white"))
.attr( "opacity", opacity )
.attr( "stroke", "#999")
.attr( "d", d3.geoPath().projection(projection) )
.on('click', function(event, d) {console.log(d)} )
.on('mouseenter', function(event,d) {
d3.select('#county-results-' + mapSpecifier).style('visibility','visible')
.html(makeResultsTable(d.properties.name + ' County', d, mapSpecifier))
})
.on('mouseleave', function(event,d){
d3.select('#county-results-' + mapSpecifier).style('visibility','hidden')
})

Insert cell
addCityLabels = g => {
g.style('pointer-events', 'none')
g.selectAll("circle")
.data(curStateCities.features)
.join("circle")
.attr("fill", "#333")
.attr("r", 2)
.attr("transform", d => {
let p = projection(d.geometry.coordinates)
return `translate(${p})`
})
g.selectAll("text")
.data(curStateCities.features)
.join("text")
.attr("fill", "#222222")
.text(d=> d.id)
.style('font-weight', 'bold')
.style('font-size', '19px')
.attr("x", 4)
.attr("y", 10)
.attr("transform", d => {
let p = projection(d.geometry.coordinates)
return `translate(${p})`
})
}
Insert cell
addBubblesToMap = (g, remainingOpacity = false, colorRamp = false) => {
let opacityMultiplier = (d) => remainingOpacity == false ? 1 : (1.5 * (+d.DemVotes + +d.RepVotes)/+d.TotalVoters)
g.style('pointer-events', 'none')
g.selectAll( "circle" )
.data(shapesWithDataCounty)
.join( "circle" )
.attr( "fill", d=> colorRamp ? d.colorRamped : d.color )
.attr( "fill-opacity", d=> 0.4 * opacityMultiplier(d) )
.attr( "stroke", d=> colorRamp ? d.colorRamped : d.color)
.attr( "stroke-opacity", d=> 0.4 + ( 0.4* opacityMultiplier(d)))
.attr('r',d=> bubbleRadiusScale(d.TotalVoters))
.attr("transform", d => {
let p = projection(d.centroid.geometry.coordinates)
return `translate(${p})`
})
}
Insert cell
addDonutsToMap = g => {
g.style('pointer-events', 'none')
g.selectAll("g")
.data( pieData )
.join('g')
.attr("class", 'circles')
.attr("id", d => ('circles' + d.id))
.attr('opacity', 0.75)
.selectAll('path')
.data((d,i)=> pie( pieData[i].data ))
.join("path")
.attr("fill", d => colors[d.data.name])
.attr("d", d3.arc()
.innerRadius( d=> 0.4 * d.data.r)
.outerRadius(d=> d.data.r))
.attr("transform", d=> "translate(" + d.data.x + "," + d.data.y + ")")
}
Insert cell
Insert cell
addSidebar = (container, mapSpecifier) => {
// add sidebar with counts for current state and placeholder for counties (to be triggered on mouseenter of counties).
const sidebar = container.append('div').style('width','23%').style('vertical-align','top').style('display','inline-block')
sidebar.append('div').html(makeResultsTable(state, curStateData, mapSpecifier))
sidebar.append('div').style('padding-top','40px').attr('id', 'county-results-' + mapSpecifier).style('visibility','hidden')
}
Insert cell
sidebarPlotPixels = 100
Insert cell
sideBarColOpacityHex = 'B3'
Insert cell
makeResultsTable = function(place, dd, mapSpecifier) {
let [demVotes, repVotes, totalVoters] = [+dd.DemVotes, +dd.RepVotes, +dd.TotalVoters]
let demoninator = demVotes + repVotes;
if(mapSpecifier != 'choropleth' & mapSpecifier != 'bubbles') {demoninator = totalVoters}
let outputHtml = `
<div id='sidebar'>
<h3>${place} Results</h3>
<table>
<thead>
<tr>
<th width="1%"></th>
<th width="30%" style="text-align: left;">Candidate</th>
<th style="vertical-align: middle;text-align: right;">Votes</th>
<th width= "40%"></th>
</tr>
</thead>
<tbody>
<tr style="max-width:20%">
<td><svg width="20" height="20" viewBox="0 0 20 20"><circle cx="50%" cy="50%" r="4" fill="${colors.rep + sideBarColOpacityHex}"></circle></svg></td>

<td>Republican</td>
<td style="text-align: right;">${d3.format(",")(repVotes)}</td>
<td><div class="bar-background" >
<span class="bar-span rep" style="width: ${100 * repVotes / demoninator}%"></span>
</div></td>
</tr>
<tr>
<td><svg width="20" height="20" viewBox="0 0 20 20"><circle cx="50%" cy="50%" r="4" fill="${colors.dem + sideBarColOpacityHex}"></circle></svg></td>
<td>Democrat</td>
<td style=";text-align: right;">${d3.format(",")(demVotes)}</td>
<td><div class="bar-background" >
<span class="bar-span dem" style="width: ${100 * demVotes / demoninator}%"></span>
</div></td>
</tr>`
if (mapSpecifier != 'choropleth' & mapSpecifier != 'bubbles' ) {
outputHtml += `<tr>
<td><svg width="20" height="20" viewBox="0 0 20 20"><circle cx="50%" cy="50%" r="4" fill=${mapSpecifier == "dots" ? "#fbf418": "#444"}></circle></svg></td>
<td>Est. Left to Count</td>
<td style="text-align: right;">~${d3.format(",.2r")(totalVoters - (demVotes + repVotes))}</td>
<td><div class="bar-background" >
<span class="bar-span" style="background:${mapSpecifier == "dots" ? "#fbf418": "#444"};width: ${100 * (totalVoters - (demVotes + repVotes)) / demoninator}%"></span>
</div></td>
</tr>
</tbody>
</table>`
} else {
outputHtml +=
` </tbody>
</table>
<div style="font-size: 12px;">Est. ${d3.format('.0%')((demVotes+ repVotes)/totalVoters)} votes in</div>`
}
outputHtml += `</div>
<style>
#sidebar td {
vertical-align: middle;
}
#sidebar .bar-background {
display: flex;
position: relative;
width: 100%;
height: 0.6rem;
}
#sidebar .bar-background::after {
content: "";
position: absolute;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
background-color: #ccc;
}
#sidebar .bar-span {
z-index:1;
height: 0.6rem;
position:relative;
}
#sidebar .dem {
background:${colors.dem + sideBarColOpacityHex};
}
#sidebar .rep {
background:${colors.rep + sideBarColOpacityHex};
}

</style>`
return outputHtml
}
Insert cell
Insert cell
annote1Url = FileAttachment("BubbleAnnot1@1.png").url()
Insert cell
annote2Url = FileAttachment("BubbleAnnot2.png").url()
Insert cell
annote3Url = FileAttachment("BubbleAnnot3.png").url()
Insert cell
annote4Url = FileAttachment("BubbleAnnot4@2.png").url()
Insert cell
annoteDonut = FileAttachment("annoteDonut2.png").url()
Insert cell
annotePackedDot = FileAttachment("annotePackedDot.png").url()
Insert cell
Insert cell
d3 = require('d3@6')
Insert cell
topojson = require("topojson-client@3")
Insert cell
turf = require('https://bundle.run/turf@3.0.14')
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more