This D3 block was inspired by a video I saw on Reddit showing an animated chart of StackOverflow tag popularity. I thought it woudl be fun to recreate it with D3 and D3FC.
I obtained the data from the StackOverflow Data Explorer, using the following query:
select
'##TAG##' as tag, yr, mo, COUNT(*) as total
from (
select
MONTH(p.CreationDate) AS mo,
YEAR(p.CreationDate) AS yr
from Posts p
join PostTags pt on (p.id = pt.PostId)
join Tags t on (pt.TagId = t.id)
where t.TagName = '##TAG##'
) src
group by yr, mo
order by yr, mo
The chart is rendered using various D3FC components, making good use of the built-in transition support. The code has various comments if you're interested in how it was constructed.
Built with blockbuilder.org
xxxxxxxxxx
<head>
<!-- include polyfills for custom event and Symbol (for IE) -->
<script src="https://unpkg.com/babel-polyfill@6.26.0/dist/polyfill.js"></script>
<script src="https://unpkg.com/custom-event-polyfill@0.3.0/custom-event-polyfill.js"></script>
<!-- use babel so that we can use arrow functions and other goodness in this block! -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script src="https://unpkg.com/d3@5.5.0"></script>
<script src="https://unpkg.com/d3fc@14.0.41"></script>
<meta charset="utf-8" />
<style>
body {
font: 14px sans-serif;
background-color: #363b3f;
color: darkgrey;
}
.x-axis text {
fill: darkgrey;
}
.x-axis .domain {
display: none;
}
.x-axis .tick {
font-size: 12px;
}
#chart {
height: 480px;
}
.y-axis svg {
display: none;
}
.language-label {
text-anchor: end;
font-size: 20px;
fill: white;
alignment-baseline: central
}
.language-percent {
fill: darkgrey;
alignment-baseline: central
}
.container {
display: relative;
}
h2 {
font-size: 30px;
margin-bottom: 5px;
}
#date {
color: darkgrey;
font-size: 50px;
position: absolute;
bottom: 10px;
right: 10px;
}
</style>
</head>
<h2>Most popular programming languages on StackOverflow</h2>
<div class="container">
<div id="chart"></div>
<div id="date"></div>
</div>
<script type="text/babel">
const months = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ];
d3.csv("data.csv").then(data => {
// convert string properties to numbers
data.forEach(d => {
d.yr = Number(d.yr);
d.mo = Number(d.mo);
d.total = Number(d.total);
});
// find all unique tags
const tags = d3.set(data.map(d => d.tag)).values();
const colorScale = d3.scaleOrdinal(d3.schemeCategory10)
.domain(tags);
const percentFormat = d3.format(".1%");
// a D3FC bar series component https://d3fc.io/api/series-api.html
const barSeries = fc
.autoBandwidth(fc.seriesSvgBar())
.crossValue(d => d.tag)
.align("left")
.orient("horizontal")
.key(d => d.tag)
.mainValue(d => d.percent)
.decorate(selection => {
// this section uses the decorate pattern (https://d3fc.io/introduction/decorate-pattern.html)
// to modify the data-join used by the bar series, allowing various customisations
// colour each bar
selection.enter().style("fill", d => colorScale(d.tag));
// add language and percent indicators
selection
.enter()
.append("text")
.classed("language-label", true)
.attr("transform", "translate(-5, 0)")
.text(d => d.tag);
selection
.enter()
.append("text")
.classed("language-percent", true)
.attr("transform", "translate(5, 0)")
selection.select(".language-percent")
.text(d => percentFormat(d.percent));
});
// use D3FC extent (https://d3fc.io/api/extent-api.html) to compute
// a suitable y axis range
const yDomain = fc
.extentLinear()
.accessors([d => d.percent])
.include([0])
.pad([0.0, 0.05]);
// the D3FC chart component (https://d3fc.io/api/chart-api.html)
const chart = fc
.chartCartesian(d3.scaleLinear(), d3.scaleBand())
.xOrient("top")
.xTickFormat(percentFormat)
.svgPlotArea(barSeries);
const renderChart = (year, month) => {
// filter the tag data for year / month and sort
const currentTags = data
.filter(d => d.yr === year && d.mo === month)
.sort((a, b) => a.total - b.total);
// compute the percentages
const totalTagCount = d3.sum(currentTags, d => d.total);
currentTags.forEach(d => (d.percent = d.total / totalTagCount));
// update the chart domain
chart.yDomain(currentTags.map(d => d.tag)).xDomain(yDomain(currentTags));
// render the chart
d3.select("#chart")
.datum(currentTags)
.transition()
.ease(d3.easeLinear)
.duration(700)
.call(chart);
// update the date indicator
d3.select("#date")
.text(`${months[month - 1]} ${year}`);
};
let currentYear = 2009;
let currentMonth = 1;
// update the year / month on an interval timer
const interval = setInterval(() => {
renderChart(currentYear, currentMonth);
currentMonth ++;
if (currentMonth > 12) {
currentMonth = 1;
currentYear++;
}
if (currentMonth === 9 && currentYear === 2019) {
clearInterval(interval);
}
}, 800);
});
</script>
https://unpkg.com/babel-polyfill@6.26.0/dist/polyfill.js
https://unpkg.com/custom-event-polyfill@0.3.0/custom-event-polyfill.js
https://unpkg.com/babel-standalone@6/babel.min.js
https://unpkg.com/d3@5.5.0
https://unpkg.com/d3fc@14.0.41