Public
Edited
May 5
Insert cell
Insert cell
Insert cell
Insert cell
// First we load the data from the CSV
data = FileAttachment("data.csv").csv();
Insert cell
// We then clean/process the data into the desired attributes/types
// (this also filters out the blank columns that were present in the CSV)
cleanData = data.map(d => ({
Rank: +d["Rank"], // number
Name: d["Name"], // string
Platform: d["Platform"], // string
Metascore: +d["Metascore"], // number
// We want to maintain dates/date objects for our stream graph later
Date: new Date(d["Date"])
}));
Insert cell
// Used ChatGPT to learn both JavaScript and D3 functions
// to aggregate a new attribute: the average Metascore of a given platform
platformMetascore = Array.from(
d3.group(cleanData, d => d.Platform),
([platform, games]) => ({
Platform: platform,
// d3.mean returns the average value of the Metascores
AverageMeta: d3.mean(games, d => d.Metascore)
// d3.ascending sorts the values from least to greatest
})).sort((a, b) => d3.ascending(a.AverageMeta, b.AverageMeta));
Insert cell
Insert cell
// Inspired by code from Workshop 2 and the linked notebook in Canvas
// https://observablehq.com/@k-dasu/simple-visualization-example
// Used ChatGPT also to learn D3 syntax for axes/labels/text/translations
barChart = {
const width = 600;
const height = 400;
const marginTop = 20;
const marginRight = 20;
const marginBottom = 80;
const marginLeft = 30;

const x = d3.scaleBand()
.domain(platformMetascore.map(d => d.Platform))
.range([marginLeft, width - marginRight])
.padding(0.3);

const y = d3.scaleLinear()
.domain([90, 100])
.range([height - marginBottom, marginTop]);

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");

svg.append("g")
.selectAll("rect")
.data(platformMetascore)
.join("rect")
.attr("x", d => x(d.Platform))
.attr("y", d => y(d.AverageMeta))
.attr("width", x.bandwidth())
.attr("height", d => y(90) - y(d.AverageMeta))
.attr("fill", "steelblue");

svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x))
.selectAll("text")
.attr("transform", "rotate(-45)")
.style("text-anchor", "end");

svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(d3.axisLeft(y))

return svg.node();
}
Insert cell
Insert cell
Insert cell
// I wanted to aggregate another attribute: the number of games from a platform on the list
platformCount = Array.from(
// https://observablehq.com/@d3/d3-group
// Used the d3 function rollup to get each platform and their game counts
d3.rollup(cleanData, v => v.length, d => d.Platform),
([Platform, Count]) => ({ Platform, Count })
)
Insert cell
Insert cell
// Inspired by/Modified code from https://observablehq.com/@d3/pie-chart/2 (Citation)
// to create the pie chart for my dataset
pieChart = {
const width = 500;
const height = 500;

const color = d3.scaleOrdinal()
.domain(platformCount.slice().sort((a, b) => d3.descending(a.Count, b.Count)).map(d => d.Platform))
// The D3 quantize and interpolateSpectral functions allow us to create a color scale and interpolate
// the values to get a nice rainbow-like range
.range(d3.quantize(t => d3.interpolateSpectral(t * 0.8 + 0.1), platformCount.length).reverse())

// The remaining D3 functions are used to create the pie chart and each individual wedge or arc
const pie = d3.pie()
.sort(null)
.value(d => d.Count);

const arc = d3.arc()
.innerRadius(0)
.outerRadius(500 / 2 - 1);

const labelRadius = arc.outerRadius()() * 0.8;

const arcLabel = d3.arc()
.innerRadius(labelRadius)
.outerRadius(labelRadius);

const arcs = pie(platformCount.slice().sort((a, b) => d3.descending(a.Count, b.Count)));

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("style", "max-width: 100%; height: auto; font: 12px sans-serif;");

svg.append("g")
.attr("stroke", "white")
.selectAll()
.data(arcs)
.join("path")
.attr("fill", d => color(d.data.Platform))
.attr("d", arc)
.append("title")
.text(d => `${d.data.Platform}: ${d.data.Count}`);

svg.append("g")
.attr("text-anchor", "middle")
.selectAll()
.data(arcs.filter(d => d.data.Count >= 4))
.join("text")
.attr("transform", d => `translate(${arcLabel.centroid(d)})`)
.call(text => text.append("tspan")
.attr("y", "-0.4em")
.attr("font-weight", "bold")
.text(d => d.data.Platform))
.call(text => text.filter(d => (d.endAngle - d.startAngle) > 0.25).append("tspan")
.attr("x", 0)
.attr("y", "0.7em")
.attr("fill-opacity", 0.7)
.text(d => d.data.Count));

return svg.node();
}
Insert cell
Insert cell
// I wanted a list of all platforms
platforms = Array.from(new Set(cleanData.map(d => d.Platform))).sort()
Insert cell
// Used ChatGPT to help create a function that returns an array of every year
// and the number of games of each platform in that year
gamesOfYear = {
const gameCounts = {};
for (const d of cleanData) {
const year = d.Date.getFullYear(); // Date as year (string)
const platform = d.Platform; // Platform
if (!gameCounts[year]) {
gameCounts[year] = {};
}
if (!gameCounts[year][platform]) {
gameCounts[year][platform] = 0;
}
gameCounts[year][platform]++;
}

// We map the years from string to number, and sort them in ascending order
const years = Object.keys(gameCounts).map(Number).sort((a, b) => a - b);

// This returns the record object for one year, and maps it to all years
return years.map(year => {
const record = { Year: year };
for (const platform of platforms) {
// If a platform didn't have a release in a year, replace it with 0
record[platform] = gameCounts[year]?.[platform] || 0;
}
return record;
});
}
Insert cell
Insert cell
// Used ChatGPT to learn the D3 syntax and steps necessary to create a Stream Graph
// Also inspired by Workshop 2 code and the linked notebook in Canvas
// https://observablehq.com/@k-dasu/simple-visualization-example
streamGraph = {
const width = 800;
const height = 500;
const marginTop = 20;
const marginRight = 20;
const marginBottom = 30;
const marginLeft = 20;

const color = d3.scaleOrdinal()
.domain(platforms)
// This puts two color schemes together, so that we have enough colors for all the platforms
.range(d3.schemeSet3.concat(d3.schemeSet1));

const x = d3.scaleLinear()
// D3 extent just sets the domain of us, with the first year and the last years of the data
.domain(d3.extent(gamesOfYear, d => d.Year))
.range([marginLeft, width - marginRight]);

// This allows data to appear as areas stacked upon each other
const stack = d3.stack()
.keys(platforms)
// This is what gives that stream/wiggle shape
.offset(d3.stackOffsetWiggle);

const gamesStack = stack(gamesOfYear);

const y = d3.scaleLinear()
// The domain here is for the stacked layers of the graph
// where the smallest and largest y-values create the stacks
.domain([
d3.min(gamesStack, layer => d3.min(layer, d => d[0])),
d3.max(gamesStack, layer => d3.max(layer, d => d[1]))
])
.range([height - marginBottom, marginTop])

// This returns an array of evenly-spaced numbers
// In our case, the minimum and maximum of years in the list
// with step 1 between them (so we have every year)
const years = d3.range(
d3.min(gamesOfYear, d => d.Year),
d3.max(gamesOfYear, d => d.Year),
1
);

// This sets up how the area of a stack is generated
const area = d3.area()
.x(d => x(d.data.Year))
.y0(d => y(d[0]))
.y1(d => y(d[1]))
// This D3 function applies a certain interpolation
// giving us curves for our areas instead of straight edges
.curve(d3.curveCatmullRom);

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");

// This draws each layer/area for each platform using the color scale above
// It also makes the labels for each color if you hover your cursor over them
svg.selectAll("path")
.data(gamesStack)
.join("path")
.attr("fill", ({ key }) => color(key))
.attr("d", area)
.append("title")
.text(({ key }) => key);

svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x).ticks(years.length).tickFormat(d3.format("d")));

svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(d3.axisLeft(y).ticks(10));

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