An attempt to procedurally draw the amazing data visualization of African-American demographics in 1890 by W. E. B. Du Bois based on census data. Rural/Small Town/Suburban/Urban is just a threshold for pop density of less than 10, 10 to 100, 100 to 1000 and 1000 plus people per square mile from census land area of counties.
This version uses the second-most populous area (the top line) as the baseline for the scale to draw the spiral, emphasizing with the spiral the vast difference between where the majority of that group lives compared to where else that group resides.
Celebrate Black History
<!DOCTYPE html>
<head>
<title>W E B Du Bois Data Visualization Remade</title>
</head>
<meta charset="utf-8">
<style>
svg {
height: 300px;
width: 300px;
display: inline-block;
}
body {
background: #e6d7c8;
}
.rural {
fill: #d9384a;
stroke: #a7212f;
background: #d9384a;
border: 1px solid #a7212f;
}
.urban {
fill: #3e6454;
stroke: #4f5e51;
background: #3e6454;
border: 1px solid #4f5e51;
}
.suburban {
fill: #5a74b0;
stroke: #47527a;
background: #5a74b0;
border: 1px solid #47527a;
}
.smalltown {
fill: #f4b32a;
stroke: #c9a15a;
background: #f4b32a;
border: 1px solid #c9a15a;
}
.final {
stroke: none;
}
.legend {
height: 200px;
width: 200px;
padding: 40px;
display: inline-block;
vertical-align: top;
}
.legend > div {
line-height: 40px;
}
.legend-item {
display: inline-block;
width: 20px;
height: 20px;
margin-right: 15px;
}
path {
stroke-linejoin: round;
stroke: none;
}
#viz > div {
display: inline-block;
width: 400px;
height: 50px;
}
</style>
<body>
<div id="viz">
<div class="legend">
<div><div class="legend-item urban"></div>Urban</div>
<div><div class="legend-item suburban"></div>Suburban</div>
<div><div class="legend-item smalltown"></div>Small Town</div>
<div><div class="legend-item rural"></div>Rural</div>
</div>
<div><a href="//www.loc.gov/pictures/resource/ppmsca.33873/?co=anedub" target="_blank">Based on a data visualization of African-American demographics in 1890 by W. E. B. Du Bois</a>
</div>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js" type="text/JavaScript"></script>
<script src="states.js" type="text/JavaScript"></script>
<script src="fipsToState.js" type="text/JavaScript"></script>
<script>
["white", "hispanic", "black", "asian", "nativeamerican", "hawaiipacificisland", "multiracial"].forEach(race => {
const div = d3.select("body").append("div")
div.append("h1")
.html(race)
.style("text-align", "center")
Object.keys(fipsToState).forEach(fip => {
const svg = div
.append("svg")
.attr("class", "f" + fip)
.attr("width", 300)
.attr("height", 300)
drawState(svg, fip)
})
function drawState(svg, selectedStateID) {
const selectedState = states.filter(d => d.id === selectedStateID)[0]
if (!selectedState) {
return
}
const ruralPopulation = selectedState.rural ? selectedState.rural[race] : 0
const urbanPopulation = selectedState.urban ? selectedState.urban[race] : 0
const suburbanPopulation = selectedState.suburban ? selectedState.suburban[race] : 0
const smallTownPopulation = selectedState.smalltown ? selectedState.smalltown[race] : 0
const piScale = d3.scaleLinear().domain([0,20]).range([0,Math.PI * 2])
let newData = []
const duboisData = [
{ type: "rural", value: ruralPopulation },
{ type: "smalltown", value: smallTownPopulation },
{ type: "suburban", value: suburbanPopulation },
{ type: "urban", value: urbanPopulation }
]
.sort((a,b) => a.value - b.value)
const max = duboisData[2].value
const popScale = d3.scaleLinear().domain([0, max]).range([0,75])
const ruralData = d3.range(popScale(ruralPopulation))
const urbanData = d3.range(popScale(urbanPopulation))
const suburbanData = d3.range(popScale(suburbanPopulation))
const smallTownData = d3.range(popScale(smallTownPopulation))
svg
.append("g")
.attr("transform", `translate(${100 - popScale(duboisData[2].value)},50)`)
.attr("class", "dubois")
svg
.append("text")
.text(fipsToState[selectedStateID])
.style("text-anchor", "middle")
.attr("x", 100)
.attr("y", 30)
const topArea = d3.area()
.x(d => d)
.y0(0)
.y1(10)
svg.select("g.dubois")
.append("path")
.attr("class", duboisData[2].type)
.attr("transform", "translate(0,0)")
.attr("d", topArea(d3.range(popScale(duboisData[2].value))))
const secondArea = d3.area()
.x0(d => popScale(duboisData[0].value) - d)
.x1(d => popScale(duboisData[0].value) - d + 10)
.y0(d => d)
.y1(d => d)
const secondOffset = popScale(duboisData[2].value) - popScale(duboisData[0].value) - 10
svg.select("g.dubois")
.append("path")
.attr("class", duboisData[0].type)
.attr("transform", `translate(${secondOffset},10)`)
.attr("d", secondArea(d3.range(popScale(duboisData[0].value))))
const thirdOffset = secondOffset
const thirdOffsetY = 10 + popScale(duboisData[0].value)
const thirdArea = d3.area()
.x0(d => d)
.x1(d => d + 10)
.y0(d => d)
.y1(d => d)
svg.select("g.dubois")
.append("path")
.attr("class", duboisData[1].type)
.attr("transform", `translate(${thirdOffset},${thirdOffsetY})`)
.attr("d", thirdArea(d3.range(popScale(duboisData[1].value))))
let fourthOffsetY = thirdOffsetY + popScale(duboisData[1].value)
const connectorAmount = Math.min(50, popScale(duboisData[3].value))
let fourthOffset = thirdOffset + popScale(duboisData[1].value) - connectorAmount
const radialConnectorArea = d3.area()
.x0(d => connectorAmount - d)
.x1(d => connectorAmount - d + 10)
.y0(d => d)
.y1(d => d)
svg.select("g.dubois")
.append("path")
.attr("class", "final " + duboisData[3].type)
.attr("transform", `translate(${fourthOffset},${fourthOffsetY})`)
.attr("d", radialConnectorArea(d3.range(connectorAmount)))
let rawSpiralData = Math.max(popScale(duboisData[3].value) - 50, 0) + 6
const spiralThicknessScaleA = d3.scaleLinear().domain([rawSpiralData,1]).range([50,1])
const spiralThicknessScaleB = d3.scaleLinear().domain([rawSpiralData,1]).range([1,7])
const spiralData = rawSpiralData
const spiralOffsetScale = d3.scaleLinear().domain([50,30]).range([9,0])
const fifthOffsetY = fourthOffsetY + 25 + connectorAmount + 3
const fifthOffset = fourthOffset + connectorAmount - spiralOffsetScale(50)
let spiralThickness = (d,spiralData,rawSpiralData) => {
if (!spiralData) {
if (rawSpiralData > 50) {
return 50 - spiralThicknessScaleA(d) - spiralThicknessScaleB(d)
}
return 50 - d
}
if (rawSpiralData > 50) {
return 50 - spiralThicknessScaleA(d) + spiralThicknessScaleB(d) - 7
}
return d >= spiralData - 6 ? 50 - d : 50 - d - 8
}
const radialArea = d3.radialArea()
.angle(d => -piScale(d%20) - 0.57)
.innerRadius(d => spiralThickness(d,spiralData,rawSpiralData))
.outerRadius(d => spiralThickness(d,undefined,rawSpiralData))
.curve(d3.curveBasis)
svg.select("g.dubois")
.append("path")
.attr("class", "final " + duboisData[3].type)
.attr("transform", `translate(${fifthOffset},${fifthOffsetY})`)
.attr("d", radialArea(d3.range(spiralData)))
}
})
</script>
const fipsToState = {
"2": "ALASKA",
"28": "MISSISSIPPI",
"1": "ALABAMA",
"30": "MONTANA",
"5": "ARKANSAS",
"37": "NORTH CAROLINA",
"60": "AMERICAN SAMOA",
"38": "NORTH DAKOTA",
"4": "ARIZONA",
"31": "NEBRASKA",
"6": "CALIFORNIA",
"33": "NEW HAMPSHIRE",
"8": "COLORADO",
"34": "NEW JERSEY",
"9": "CONNECTICUT",
"35": "NEW MEXICO",
"11": "DISTRICT OF COLUMBIA",
"32": "NEVADA",
"10": "DELAWARE",
"36": "NEW YORK",
"12": "FLORIDA",
"39": "OHIO",
"13": "GEORGIA",
"40": "OKLAHOMA",
"66": "GUAM",
"41": "OREGON",
"15": "HAWAII",
"42": "PENNSYLVANIA",
"19": "IOWA",
"72": "PUERTO RICO",
"16": "IDAHO",
"44": "RHODE ISLAND",
"17": "ILLINOIS",
"45": "SOUTH CAROLINA",
"18": "INDIANA",
"46": "SOUTH DAKOTA",
"20": "KANSAS",
"47": "TENNESSEE",
"21": "KENTUCKY",
"48": "TEXAS",
"22": "LOUISIANA",
"49": "UTAH",
"25": "MASSACHUSETTS",
"51": "VIRGINIA",
"24": "MARYLAND",
"78": "VIRGIN ISLANDS",
"23": "MAINE",
"50": "VERMONT",
"26": "MICHIGAN",
"53": "WASHINGTON",
"27": "MINNESOTA",
"55": "WISCONSIN",
"29": "MISSOURI",
"54": "WEST VIRGINIA",
"56": "WYOMING"
}