chart = {
data.forEach(d => {
d.score = +d.Metascore;
d.year = parseYear(d.Date);
});
const byPlatform = d3.group(data, d => d.platform);
console.log(
"Groups (platform: count):",
Array.from(byPlatform, ([platform, items]) => `${platform}: ${items.length}`)
);
const scoreBin = d => {
const f = Math.floor(d.score / 5) * 5;
return `${f}-${f + 5}`;
};
const bubbleData = [];
for (const [platform, items] of byPlatform) {
const byBinYear = d3.rollup(
items,
v => v.length,
scoreBin,
d => d.year
);
// Turn that nested Map into individual objects
for (const [scoreRange, yearMap] of byBinYear) {
for (const [year, count] of yearMap) {
bubbleData.push({ platform, scoreRange, year, count });
}
}
}
// Set up chart dimensions and margins
const width = 800;
const height = 500;
const margin = { top: 20, right: 50, bottom: 40, left: 100 };
// Build D3 scales
// X scale shows the year the game was published
// data domain = years
// d3.scaleLinear for the X axis (year)
const x = d3.scaleLinear()
.domain(d3.extent(bubbleData, d => d.year)).nice()
.range([margin.left, width - margin.right]);
// Y scale shows the platform of the games
// d3.scaleBand for the Y axis (platform)
const platforms = Array.from(new Set(bubbleData.map(d => d.platform)));
const y = d3.scaleBand()
.domain(platforms)
.range([margin.top, height - margin.bottom])
.padding(0.2);
// R-scale is the size of the circles
// data domain = counts
// d3.scaleSqrt for bubble radius (count)
const rScale = d3.scaleSqrt()
.domain([0, d3.max(bubbleData, d => d.count)])
.range([0, 20]);
// Indicate the color of the circles. The deep color indicates a higher volume of games in the group.
// d3.scaleSequential + d3.interpolateReds for bubble color (score bin lower bound)
const lows = bubbleData.map(d => +d.scoreRange.split("-")[0]);
const colorScale = d3.scaleSequential(d3.interpolateReds)
.domain(d3.extent(lows));
// Create the image
// d3.create for creating the image
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("width", "100%");
// Create X and Y axis in the image
// d3.axisBottom generates a bottom-oriented axis using the x scale, and .tickFormat(d3.format("d")) formats tick values as integers
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).tickFormat(d3.format("d")));
// Draw Y axis on the left
// d3.axisLeft generates a left-oriented axis using the Y scale
svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y));
// Labels
svg.append("text")
.attr("x", margin.left + (width - margin.left - margin.right) / 2)
.attr("y", height - margin.bottom + 40)
.attr("text-anchor", "middle")
.attr("font-size", 12)
.text("Year of Release");
svg.append("text")
.attr("transform", `rotate(-90)`)
.attr("x", - (margin.top + (height - margin.top - margin.bottom) / 2))
.attr("y", margin.left - 90)
.attr("text-anchor", "middle")
.attr("font-size", 12)
.text("Platform");
// Draw circles
svg.selectAll("circle")
.data(bubbleData)
.join("circle")
.attr("cx", d => x(d.year))
.attr("cy", d => y(d.platform) + y.bandwidth()/2)
.attr("r", d => rScale(d.count))
.attr("fill", d => colorScale(+d.scoreRange.split("-")[0]))
.attr("fill-opacity", 0.7)
.append("title")
.text(d => `${d.platform}, ${d.year}\n${d.scoreRange}: ${d.count} games`);
// Legend of color gradient bar for score bins (lightest to darkest)
const defs = svg.append("defs");
// Define a horizontal linear gradient from min to max score bin
const gradient = defs.append("linearGradient")
.attr("id", "score-gradient")
.attr("x1", "0%").attr("y1", "0%")
.attr("x2", "100%").attr("y2", "0%");
// Lightest color at 0%
gradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", colorScale(d3.min(lows)));
// Darkest color at 100%
gradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", colorScale(d3.max(lows)));
// Legend dimensions and position
const legendWidth = 200;
const legendHeight = 10;
const legendX = width - margin.right - legendWidth;
const legendY = margin.top;
// Draw the gradient rectangle
svg.append("rect")
.attr("x", legendX)
.attr("y", legendY)
.attr("width", legendWidth)
.attr("height", legendHeight)
.style("fill", "url(#score-gradient)");
// Labels for the ends of the gradient
svg.append("text")
.attr("x", legendX)
.attr("y", legendY - 5)
.attr("text-anchor", "start")
.text("Lowest bin");
svg.append("text")
.attr("x", legendX + legendWidth)
.attr("y", legendY - 5)
.attr("text-anchor", "end")
.text("Highest bin");
return svg.node();
}