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) }) });