Built with blockbuilder.org
Recreated from https://pudding.cool/2017/09/this-american-life/
license: mit | |
height: 700 |
Built with blockbuilder.org
Recreated from https://pudding.cool/2017/09/this-american-life/
<!DOCTYPE html> | |
<head> | |
<meta charset="utf-8"> | |
<script src="https://d3js.org/d3.v5.min.js"></script> | |
<script src="https://unpkg.com/d3-svg-annotation"></script> | |
<link href="https://fonts.googleapis.com/css?family=EB+Garamond:600|Roboto:300,400,500,700" rel="stylesheet"> | |
<link href="tal_viz_styles.css" rel="stylesheet"> | |
</head> | |
<body> | |
<div class="chart"> | |
<div class="chart-title"> | |
Gender breakdown of episodes | |
</div> | |
<div class="chart-filters"> | |
<!-- <div class="chart-filter topic-selection"> | |
<label>Topic</label> | |
<br /> | |
<select type="dropdown"></select> | |
</div> --> | |
<div class="chart-filter year-selection"> | |
<label>Year</label> | |
<br /> | |
<select type="dropdown"></select> | |
</div> | |
<div class="chart-filter episode-selection"> | |
<label>Episode</label> | |
<br /> | |
<select type="dropdown"></select> | |
</div> | |
</div> | |
<div class="chart-tooltip"> | |
<div class="tt-info"> | |
<span class="tt-heading"></span> | |
<br /> | |
<span class="tt-description"></span> | |
<div class="gender-bars"> | |
<div class="gender-prop"> | |
<div class="gender-bar female-bar"></div> | |
<span class="gender-label female"></span> | |
</div> | |
<div class="gender-prop"> | |
<div class="gender-bar male-bar"></div> | |
<span class="gender-label male"></span> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script src="tal_viz.js"></script> | |
</body> |
d3.csv("https://raw.githubusercontent.com/polygraph-cool/this-american-life/master/data/act1.csv") | |
.then(function(data) { | |
// settings | |
const width = 750 | |
const height = 420 | |
const margins = {left: 5, right: 40, top: 40, bottom: 20} | |
let dataFiltered = false; | |
const maleColor = "#6767FF" | |
const femaleColor = "#FA676C" | |
const colorScale = d3.scaleLinear() | |
.domain([0, 100]) | |
.range([femaleColor, maleColor]) | |
// binning | |
const x = d3.scaleLinear() | |
.domain([0, 100]) | |
.range([0, width]) | |
// Generate a histogram using 30 uniformly-spaced bins. | |
const histogram = d3.histogram() | |
.value(d => +d.malePercent) | |
.domain(x.domain()) | |
.thresholds(d3.range(30).map(d => d * 100/30)) | |
const binnedData = histogram(data) | |
const maxInBin = d3.max(binnedData).length | |
function showTooltip(episode) { | |
// constants used in tooltip info | |
const episodeData = data.filter(d => d.episode === episode)[0] | |
const malePerc = d3.format(".01f")(episodeData.malePercent) | |
const femalePerc = d3.format(".01f")(100 - episodeData.malePercent) | |
// position tooltip relative to pointer | |
const coords = [d3.event.clientX, d3.event.clientY] | |
const ttBoundingRect = d3.select(".chart-tooltip") | |
.node() | |
.getBoundingClientRect() | |
d3.select(".chart-tooltip") | |
.style("left", `${coords[0] <= width / 2 ? coords[0] + 25 : coords[0] - 25 - 300}px`) | |
.style("top", `${coords[1] - ttBoundingRect.height / 2}px`) | |
// adjust the text | |
d3.select(".tt-heading") | |
.text(`#${episodeData.episode}: ${episodeData.title}`) | |
d3.select(".tt-description") | |
.text(`${episodeData.description}`) | |
// size and label the bars | |
d3.select(".female-bar") | |
.style("width", `${femalePerc}px`) | |
.style("background-color", femaleColor) | |
d3.select(".gender-label.female") | |
.style("color", femaleColor) | |
.text(`${femalePerc}% female dialogue`) | |
d3.select(".male-bar") | |
.style("width", `${episodeData.malePercent}px`) | |
.style("background-color", maleColor) | |
d3.select(".gender-label.male") | |
.style("color", maleColor) | |
.text(`${malePerc}% male dialogue`) | |
d3.select(".chart-tooltip") | |
.style("opacity", 0.9) | |
} | |
const svg = d3.select(".chart").append("svg") | |
.attr("width", width + margins.left + margins.right) | |
.attr("height", height + margins.top + margins.bottom) | |
binnedData.forEach(function(bin, ind) { | |
d3.select("svg") | |
.append("g") | |
.attr("class", `col-${ind}`) | |
.attr("transform", `translate(${margins.left}, ${margins.top})`) | |
d3.select(`.col-${ind}`) | |
.selectAll(".rect") | |
.data(bin) | |
.enter() | |
.append("rect") | |
.attr("class", "rect") | |
.attr("width", width / 30) | |
.attr("height", height / maxInBin) | |
.attr("x", width / 30 * ind) | |
.attr("y", (d, i) => height / maxInBin * i) | |
.attr("fill", d => colorScale(+d.malePercent)) | |
.attr("stroke", "#fff") | |
.attr("stroke-width", 1) | |
.style("opacity", 0.6) | |
}) | |
//// mouse events //// | |
d3.selectAll(".rect") | |
.on("mouseover", function(d, i) { | |
d3.select(this) | |
.style("opacity", 1) | |
.style("cursor", "pointer") | |
showTooltip(d.episode) | |
}) | |
.on("mouseout", function(d, i) { | |
const inFilter = d3.select(this).classed("filtered") | |
d3.select(this) | |
.style("opacity", dataFiltered ? (inFilter ? 1 : 0.3) : 0.6) | |
d3.select(".chart-tooltip") | |
.style("opacity", 0) | |
}) | |
.on("click", function(d, i) { | |
window.open(`https://hw2.thisamericanlife.org/radio-archives/episode/${d.episode}`, "_blank") | |
}) | |
//// axes //// | |
const xAxis = d3.axisBottom(x) | |
.tickValues([0, 50, 100]) | |
d3.select("svg") | |
.append("g") | |
.attr("class", "x-axis") | |
.attr("transform", `translate(${margins.left}, 28)`) | |
.call(xAxis); | |
const xAxisLabels = [ | |
{label: "100% FEMALE", color: femaleColor, anchor: "start"}, | |
{label: "50/50", color: colorScale(50), anchor: "middle"}, | |
{label: "100% MALE", color: maleColor, anchor: "end"} | |
] | |
d3.select(".x-axis") | |
.selectAll("text") | |
.data(xAxisLabels) | |
.attr("transform", "translate(0, -28)") | |
.attr("text-anchor", d => d.anchor) | |
.attr("fill", d => d.color) | |
.text(d => d.label) | |
const yScale = d3.scaleLinear() | |
.domain([0, 50]) | |
.range([0, height + height/maxInBin]) | |
const yAxis = d3.axisRight(yScale) | |
.tickValues([10, 20, 30, 40]) | |
d3.select("svg") | |
.append("g") | |
.attr("class", "y-axis") | |
.attr("transform", `translate(${margins.left + width}, ${margins.top})`) | |
.call(yAxis) | |
d3.select(".y-axis") | |
.select(".domain") | |
.remove() | |
d3.select(".y-axis") | |
.selectAll("line") | |
.data([615, 550, 340, 130]) | |
.attr("x1", d => -d) | |
//// filtering //// | |
const years = [ ...new Set(data.map(d => d.year))] // unique years | |
years.unshift("All") // for removing filter | |
const episodes = data.map(d => { | |
return {episode: d.episode, title: d.title} | |
}) | |
episodes.unshift({episode: "All"}) // for removing filter | |
const yearSelector = d3.select(".year-selection") | |
.select("select") | |
.selectAll("option") | |
.data(years) | |
.enter() | |
.append("option") | |
.text(d => d) | |
.property("value", d => d) | |
const episodeSelector = d3.select(".episode-selection") | |
.select("select") | |
.selectAll("option") | |
.data(episodes) | |
.enter() | |
.append("option") | |
.text(d => d.title ? `${d.episode}: ${d.title}` : d.episode) | |
.property("value", d => d.episode) | |
const dataTiles = d3.selectAll(".rect") | |
d3.select(".year-selection") | |
.select("select") | |
.on("change", function(d) { | |
// remove episode filter | |
episodeSelector | |
.filter(d => d.episode === "All") | |
.property("selected", true) | |
// make selections | |
const selectedYear = d3.select(this).property("value") | |
const selectedTiles = dataTiles.filter(d => d.year === selectedYear) | |
// set filter status if year selected | |
dataFiltered = selectedYear !== "All" | |
// highlight visually | |
dataTiles | |
.classed("filtered", false) | |
.transition("filter") | |
.duration(200) | |
.style("opacity", selectedYear !== "All" ? 0.3 : 0.6) | |
selectedTiles | |
.classed("filtered", true) | |
.transition("filter") | |
.duration(200) | |
.style("opacity", 1) | |
}) | |
d3.select(".episode-selection") | |
.select("select") | |
.on("change", function(d) { | |
// remove year filter | |
yearSelector | |
.filter(d => d === "All") | |
.property("selected", true) | |
// make selections | |
const selectedEpisode = d3.select(this).property("value") | |
const selectedTiles = dataTiles.filter(d => d.episode === selectedEpisode) | |
// set filter status if episode selected | |
dataFiltered = selectedEpisode !== "All" | |
// highlight visually | |
dataTiles | |
.classed("filtered", false) | |
.transition("filter") | |
.duration(200) | |
.style("opacity", selectedEpisode !== "All" ? 0.3 : 0.6) | |
selectedTiles | |
.classed("filtered", true) | |
.transition("filter") | |
.duration(200) | |
.style("opacity", 1) | |
}) | |
//// annotation //// | |
const type = d3.annotationLabel | |
const annotations = [{ | |
note: { | |
label: "There is only one episode with exclusively female voice versus 18 episodes with exclusively male voices", | |
title: "#9: Julia Sweeney", | |
orientation: "leftRight", | |
align: "bottom" | |
}, | |
//can use x, y directly instead of data | |
x: 17.5, | |
y: 53, | |
className: "annotation", | |
dy: 75, | |
dx: 0, | |
connector: { | |
end: "dot" | |
} | |
}] | |
const makeAnnotations = d3.annotation() | |
.type(type) | |
.textWrap(150) | |
.annotations(annotations) | |
d3.select("svg") | |
.append("g") | |
.attr("class", "annotation-group") | |
.lower() // makes sure the annotation stays underneath tile | |
// modify the annotation appearance | |
.style("font-family", "Roboto") | |
.style("font-size", 12) | |
.call(makeAnnotations) | |
d3.select(".annotation-note-label") | |
.style("font-weight", 300) | |
}) |
.chart { | |
margin-left: 20px; | |
} | |
.chart-title { | |
font-family: "EB Garamond", serif; | |
font-size: 28px; | |
margin-bottom: 12px; | |
} | |
/* tooltipping */ | |
.chart-tooltip { | |
font-family: "Roboto", sans-serif; | |
pointer-events: none; | |
width: 300px; | |
position: fixed; | |
border: 1px solid #efefef; | |
opacity: 0; | |
border-radius: 3px; | |
box-shadow: 0 8px 4px -7px #e4e4e4; | |
background-color: #fff; | |
} | |
.tt-info { | |
margin: 12px 12px 12px 12px; | |
} | |
.tt-heading { | |
font-size: 15px; | |
font-weight: 700; | |
} | |
.tt-description { | |
margin-top: 4px; | |
margin-bottom: 4px; | |
font-size: 12px; | |
font-weight: 400; | |
display: inline-block; | |
} | |
.gender-bars { | |
height: 40px; | |
} | |
.gender-prop { | |
float: left; | |
} | |
.gender-bar { | |
float: left; | |
height: 10px; | |
margin-top: 8px; | |
margin-right: 8px; | |
} | |
.gender-label { | |
float: left; | |
font-size: 11px; | |
font-weight: 500; | |
margin-top: 7px; | |
} | |
/* axes */ | |
.x-axis path, .x-axis line { | |
color: #ccc; | |
stroke: #ccc; | |
} | |
.x-axis text { | |
font-family: "Roboto", sans-serif; | |
font-size: 13px; | |
font-weight: 500; | |
} | |
.y-axis text { | |
font-family: "Roboto", sans-serif; | |
font-size: 12px; | |
font-weight: 400; | |
fill: #ccc; | |
} | |
.y-axis line { | |
color: #ccc; | |
stroke: #ccc; | |
} | |
/* filters */ | |
.chart-filters { | |
font-family: "Roboto", sans-serif; | |
font-size: 12px; | |
font-weight: 500; | |
color: #555; | |
margin-bottom: 20px; | |
} | |
.chart-filter { | |
display: inline-block; | |
margin-left: 25px; | |
} | |
.chart-filter select { | |
margin-top: 5px; | |
max-width: 175px; | |
} |