|
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) |
|
|
|
}) |
|
|
|
// tile 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) { |
|
console.log(d) |
|
}) |
|
|
|
// draw and customize 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() |
|
|
|
const yTicks = d3.select(".y-axis") |
|
.selectAll("line") |
|
.nodes() |
|
.reverse() |
|
|
|
d3.selectAll(yTicks) |
|
.attr("x1", (d, i) => -(i + 1) * width/5 - 50) |
|
|
|
// 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) |
|
}) |
|
|
|
}); |