Public
Edited
Apr 29
Insert cell
Insert cell
d3 = require("d3@6")
Insert cell
// load data
raw_data = d3.csvParse(await FileAttachment("data.csv").text(), d3.autoType)
Insert cell
Insert cell
games = raw_data.map(({Name, Platform, Metascore, Date}) => (
{Name, Platform, Metascore, Date}
));
Insert cell
Insert cell
// creating a list of platforms and the number of times they appear in the top 100 games
platform_counts = games.reduce((acc, game) => {
if (!(game.Platform in acc)) {
acc[game.Platform] = 0;
}
acc[game.Platform] += 1;
return acc;
}, {});
Insert cell
// changing the format so it can be joined easier
platform_counts_arr = Object.entries(platform_counts)
.map(([platform, count]) => ({platform, count}))
.sort((a, b) => b.count - a.count);
Insert cell
/**
* Generates a pie chart SVG node with radial labels and connecting lines.
* Uses platform count data stored in `platform_counts_arr`.
*
* @returns {SVGSVGElement} The generated SVG node containing the pie chart.
*/
{
// chart sizing and spacing definitions. Height and Width define container size
const width = 800;
const height = 650;
const padding = { top: 350, right: 100, bottom: 0, left: 100 };
const contentWidth = width - padding.left - padding.right;
const contentHeight = height - padding.top - padding.bottom;

// Pie arc dimensions
const outerRadius = Math.min(contentWidth, contentHeight) / 2;
const innerRadius = outerRadius * 0.33;

// Additional spacing for label lines and label positions
const linePadding = outerRadius * 0.1;
const lineRadius = outerRadius + linePadding;
const labelPadding = 8; // creating a little spacing between the line and label
const labelRadius = lineRadius + labelPadding;

// Color scale using a rainbow interpolation across the number of platforms
const color = d3.scaleSequential()
.domain([0, platform_counts_arr.length])
.interpolator(d3.interpolateRainbow);

// Create SVG container
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);

svg.append("text")
.attr("x", width / 2)
.attr("y", height - 100)
.attr("text-anchor", "middle")
.style("font-family", "sans-serif")
.style("font-size", "24px")
.style("font-weight", "bold")
.text("Proportion of Platforms in Top 100 Games");

// Append a group element centered in the SVG
const g = svg.append("g")
.attr("transform", `translate(${width / 2}, ${height / 2})`);

// turns platform count data into arc angles (pie generator)
const pie = d3.pie().value(d => d.count);

// turns data in drawable arc d attributes for the path element (arc generator)
const dataArc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);

// arc generator for labels, giving labels spacing between pie and data
const labelArc = d3.arc()
.innerRadius(labelRadius)
.outerRadius(labelRadius);

// arc generator for line, giving line spacing between pie and data
const lineArc = d3.arc()
.innerRadius(lineRadius)
.outerRadius(lineRadius);

/**
* Determines if the label text should be rotated based on angle span.
* @param {Object} d - Pie slice data
* @returns {boolean}
*/
const isTextRotated = (d) => {
return (d.endAngle - d.startAngle) * 180 / Math.PI > 12;
}

/**
* Determines if the label is on the right side of the pie.
* @param {Object} d - Pie slice data
* @returns {boolean}
*/
const isRightSide = (d) => {
return (d.startAngle + d.endAngle) / 2 < Math.PI;
}

// Draw pie slices
g.selectAll('path')
.data(pie(platform_counts_arr))
.join('path')
.attr('d', dataArc)
.attr('fill', (d, i) => color(i))
.attr('stroke', 'white')
.style('stroke-width', '1px');

// Draw connector lines from label to arc edge
g.selectAll('polyline')
.data(pie(platform_counts_arr))
.join('polyline')
.attr('points', d => {
const lineStart = lineArc.centroid(d); // Start near label
const lineEnd = dataArc.centroid(d); // End near slice edge
return [lineStart, lineEnd];
})
.style('fill', 'none')
.style('stroke', 'black')
.style('stroke-width', 1);

// Add text labels around the pie
g.selectAll('text')
.data(pie(platform_counts_arr))
.join('text')
.attr('transform', d => {
const pos = labelArc.centroid(d);
const midAngleDegrees = (d.startAngle + d.endAngle) / 2 * 180 / Math.PI;
const rotation = isTextRotated(d) ? 0 : midAngleDegrees + 90;
return `translate(${pos[0]}, ${pos[1]}) rotate(${rotation})`;
})
.attr('text-anchor', d => isRightSide(d) ? 'start' : 'end')
.attr('alignment-baseline', 'middle')
.text(d => `${d.data.platform} (${d.data.count}%)`)
.style('font-family', 'arial')
.style('font-size', '12px')
.style('fill', 'black');

return svg.node();
}

Insert cell
Insert cell
Insert cell
// generate an array with each year that top games were made and their count per year
gamesPerYear = {
const parseTime = d3.timeParse("%d-%b-%y");
const years = games.map((game) => parseTime(game.Date).getFullYear())
const gamePerYearMap = years.reduce((acc, year) => {
if (!(year in acc)) {
acc[year] = 0;
}
acc[year] += 1;
return acc;
}, {});
return Object.entries(gamePerYearMap).map(([year, count]) => ({year, count}));
}
Insert cell
/**
* Creates a SVG that has a line graph displaying the number of top games released per year
*
* @returns {SVGSVGElement} The generated SVG node containing the line chart.
*/
{
// define the dimensions of the chart
const width = 800;
const height = 400;
const padding = { top: 50, right: 50, bottom: 50, left: 50 };
const contentWidth = width - padding.left - padding.right;
const contentHeight = height - padding.top - padding.bottom;

const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);

const g = svg.append("g")
.attr("transform", `translate(${padding.left},${padding.top})`);

// creating the scales
const x = d3.scaleLinear()
.domain(d3.extent(gamesPerYear, d => d.year))
.range([0, contentWidth]);

const y = d3.scaleLinear()
.domain([0, d3.max(gamesPerYear, d => d.count)])
.range([contentHeight, 0]);

// adding in the axes
g.append("g")
.attr("transform", `translate(0,${contentHeight})`)
.call(d3.axisBottom(x).tickFormat(d3.format("d")));

g.append("g")
.call(d3.axisLeft(y));

// returns a generator function that constructs the line
// piece by piece probably with M or L commands in the d attr
const line = d3.line()
.x(d => x(d.year))
.y(d => y(d.count));

g.append("path")
.datum(gamesPerYear)
.attr("fill", "none")
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("d", line);

// adding dots to make it clear where each data point is
g.selectAll("circle")
.data(gamesPerYear)
.join("circle")
.attr("cx", d => x(d.year))
.attr("cy", d => y(d.count))
.attr("r", 3)
.attr("fill", "blue");

// adding title to the top
svg.append("text")
.attr("x", width / 2)
.attr("y", padding.top / 2)
.attr("text-anchor", "middle")
.style("font-family", "sans-serif")
.style("font-size", "18px")
.style("font-weight", "bold")
.text("Number of Top 100 Games Released Per Year");

return svg.node();
}
Insert cell
Insert cell
Insert cell
// creating a matrix representation of a graph with weights to pass into a chord diagram
chordDiagramData = {
// Get the unique platforms
const platforms = Array.from(new Set(games.map(game => game.Platform)));
const n = platforms.length;
// Create an index map for lookup
const indexMap = Object.fromEntries(platforms.map((p, i) => [p, i]));
// Initialize a square matrix of zeros
const graph = Array(n).fill().map(() => Array(n).fill(0));
// Group games by Name
const gamesByName = d3.group(games, d => d.Name);
// Link all platforms that share a game
for (const [name, games] of gamesByName.entries()) {
const gamePlatforms = Array.from(new Set(games.map(game => game.Platform)));
for (let i = 0; i < gamePlatforms.length; i++) {
for (let j = i + 1; j < gamePlatforms.length; j++) {
const a = indexMap[gamePlatforms[i]];
const b = indexMap[gamePlatforms[j]];
graph[a][b] += 1;
graph[b][a] += 1;
}
}
}
return [platforms, graph];
}
Insert cell
/**
* Creates a SVG that has a chord diagram showing which platforms shared top games.
*
* @returns {SVGSVGElement} The generated SVG node containing the chord diagram.
*/
{
// destructure data from before
const [platforms, graph] = chordDiagramData;

// define the dimensions of the diagram
const width = 700;
const height = 600;
const padding = { top: 175, right: 50, bottom: 50, left: 50 };
const contentWidth = width - padding.left - padding.right;
const contentHeight = height - padding.top - padding.bottom;
const outerRadius = Math.min(contentWidth, contentHeight) * 0.5;
const innerRadius = outerRadius * 0.9;

// defining color scale to handle 18 categories
const color = d3.scaleOrdinal()
.domain(platforms)
.range(d3.schemeCategory10.concat(d3.schemeSet3));

// creating the svg to display
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

// creating the group for all arcs, ribbons, and text to go into
const container_group = svg.append("g")
.attr("transform", `translate(${width / 2},${height / 2})`);

// Generate the chords from the matrix
// declaring that subgroups (ribbons) will be sorted in descending order
const chordData = d3.chord()
.padAngle(0.06)
.sortSubgroups(d3.descending)(graph);

const outer_layer_group = container_group.append("g")
.selectAll("g")
.data(chordData.groups)
.join("g");

// Draw outer arcs (platform labels), append runs on each g tag
outer_layer_group.append("path")
.attr("fill", d => color(platforms[d.index]))
.attr("stroke", d => d3.rgb(color(platforms[d.index])).darker())
.attr("d", d3.arc().innerRadius(innerRadius).outerRadius(outerRadius));

// Draw the ribbons (chords)
container_group.append("g")
.attr("fill-opacity", 0.7)
.selectAll("path")
.data(chordData)
.join("path")
.attr("d", d3.ribbon().radius(innerRadius))
.attr("fill", d => color(platforms[d.source.index]))
.attr("stroke", d => d3.rgb(color(platforms[d.source.index])).darker());
// Add labels to the outer arcs
outer_layer_group.append("text")
.attr("dy", "4px")
.attr("transform", d => {
const midAngle = (d.startAngle + d.endAngle) / 2;
return `
rotate(${(midAngle * 180 / Math.PI - 90)})
translate(${outerRadius + 4})
${midAngle > Math.PI ? "rotate(180)" : ""}
`
})
.attr("text-anchor", d => {
const midAngle = (d.startAngle + d.endAngle) / 2;
return midAngle > Math.PI ? "end" : "start";
})
.text(d => platforms[d.index])
.style("font-family", "sans-serif")
.style("font-size", "12px");

// adding title to the top
svg.append("text")
.attr("x", width / 2)
.attr("y", height - padding.bottom / 2)
.attr("text-anchor", "middle")
.style("font-family", "sans-serif")
.style("font-size", "18px")
.style("font-weight", "bold")
.text("Platforms that shared top games");
return svg.node();
}
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more