Skip to content

Instantly share code, notes, and snippets.

@madams1
Last active January 28, 2019 15:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save madams1/5c1ad1e56355c62e241fa009834a4b07 to your computer and use it in GitHub Desktop.
Save madams1/5c1ad1e56355c62e241fa009834a4b07 to your computer and use it in GitHub Desktop.
TAL_dialogue_with_filters
license: mit
height: 700
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=EB+Garamond:600|Roboto: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)
})
// 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)
})
});
.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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment