Public
Edited
May 1
Insert cell
Insert cell
Insert cell
d3 = require("d3@6")
Insert cell
Insert cell
// Best Videogames of All Time dataset
data = d3.csvParse(await FileAttachment("Best Videogames of All Time.csv").text(), d3.autoType)
Insert cell
Insert cell
Insert cell
parseTime = d3.timeParse("%Y")
Insert cell
Insert cell
/**
* Parses the date data to select only the year, and then adds the correct year prefix.
* @constructor
* @param {string} date - The date provided by our data.
*/
function dateSplitter(date) {
const before = "19"; // year prefix, 1900s
const after = "20"; // year prefix, 2000s
const fullDate = date.split("-");
if (fullDate[2] > 89) {
return before.concat(fullDate[2]);
}
return after.concat(fullDate[2]); // note: this method would probably be a lot less effective after 2089. but we're not there yet!
}
Insert cell
Insert cell
/**
* Processes the dates given by our data and saves the correct year of dates and the corresponding ranks. Also includes the other data categories for additional usage.
* @constructor
*/
function dateProcessing() {
const datedData = []
data.forEach(d => {
const dateData = {
rank: d.Rank,
name: d.Name,
platform: d.Platform,
metascore: d.Metascore,
date: parseTime(dateSplitter(d.Date)),
}
datedData.push(dateData);
})
return datedData
}
Insert cell
Insert cell
/**
* Parses the platforms given by our data into groups, adding to the count of the platform group for each duplicate.
* @constructor
*/
function platformProcessing() {
const platforms = {};
data.forEach(d => {
if (d.Platform in platforms) {
platforms[d.Platform].count += 1;
platforms[d.Platform].cumulativeRank += d.Rank;
platforms[d.Platform].cumulativeScore += d.Metascore;
}
else {
const platform = {
platform_name: d.Platform,
count: 1,
cumulativeRank: d.Rank,
cumulativeScore: d.Metascore,
averageRank: 0,
averageScore: 0
}
platforms[d.Platform] = platform;
}
})

const platformData = []
Object.keys(platforms).forEach(d => {
platforms[d].averageRank = platforms[d].cumulativeRank / platforms[d].count;
platforms[d].averageScore = platforms[d].cumulativeScore / platforms[d].count;
platforms[d].averageScore = ((platforms[d].averageScore - 94) / 5) * 100;
console.log(platforms[d]);
platformData.push(platforms[d]);
})

platformData.sort(function(a, b) {
if (a.platform_name < b.platform_name) { return -1; }
if (a.platform_name > b.platform_name) {return 1; }
return 0;
})

console.log(platformData)
return platformData
}
Insert cell
Insert cell
margin = ({top: 40, right: 40, bottom: 40, left: 40})
Insert cell
height = 500
Insert cell
width = 800
Insert cell
Insert cell
Insert cell
chart1 = {
// process data
const chartdata = dateProcessing();
// create svg
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");

// x scale: time by year
const x = d3.scaleTime()
.domain([d3.min(chartdata, d => d.date), d3.max(chartdata, d => d.date)])
.range([margin.left, width - margin.right])

// y scale: rank of game
const y = d3.scaleLinear()
.domain([d3.min(chartdata, d => d.rank), d3.max(chartdata, d => d.rank)])
.range([height - margin.bottom, margin.top]);

// x axis
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(8));

// y axis
svg.append("g")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y).ticks(5));

// adding points for each rank
svg.append("g")
.selectAll("circle")
.data(chartdata)
.join("circle")
.attr("r", 3)
.attr("cx", d => x(d.date))
.attr("cy", d => y(d.rank))
.style("fill", "red");

return svg.node();
}
Insert cell
Insert cell
Insert cell
chart2 = {
// process data
const chartdata = platformProcessing();
// create svg
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: 11px sans-serif;");

// color scale
const color = d3.scaleOrdinal()
.domain(chartdata.map(d => d.platform_name))
.range(d3.quantize(t => d3.interpolateSpectral(t + 0.1), chartdata.length).reverse());

// create pie layout + arc gen
const pie = d3.pie()
.sort(null)
.value(d => d.count)
.padAngle(0.008);

const arc = d3.arc()
.innerRadius((Math.min(width, height) / 2) * 0.25)
.outerRadius(Math.min(width, height) / 2);

// separate arc for labels
const labelRadius = arc.outerRadius()() * 0.75;

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

// building the arcs
const arcs = pie(chartdata);

// adding hover labels
svg.append("g")
.attr("stroke", "black")
.selectAll()
.data(arcs)
.join("path")
.attr("fill", d => color(d.data.platform_name))
.attr("d", arc)
.append("title")
.text(d => `${d.data.platform_name}: ${d.data.count}`)


// adding visible lables
svg.append("g")
.attr("text-anchor", "middle")
.selectAll()
.data(arcs)
.join("text")
.attr("transform", function(d, i) {
// rotating text so that it fits on each slice!
var rotation = d.endAngle < Math.PI ? (d.startAngle / 2 + d.endAngle / 2) * 180 / Math.PI : (d.startAngle / 2 + d.endAngle / 2 + Math.PI) * 180 / Math.PI;
return "translate(" + arcLabel.centroid(d) + ") rotate(-90) rotate(" + rotation + ")"
})
.call(text => text.append("tspan")
.attr("y", "0.4em")
.attr("x", "0em")
.attr("font-weight", "bold")
.text(d => `${d.data.platform_name}: ${d.data.count}`))

return svg.node();
}
Insert cell
Insert cell
Insert cell
chart3 = {
// process data
const chartdata = platformProcessing();
// create svg
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; font: 11px sans-serif;");

// building radial field...
const radialScale = d3.scaleLinear()
.domain([d3.min(data, d => d.Rank), d3.max(data, d => d.Rank)])
.range([height - margin.bottom * 7, margin.top]);

const ticks = [d3.max(data, d => d.Rank) * 4 / 5, d3.max(data, d => d.Rank) * 3 / 5, d3.max(data, d => d.Rank) * 2 / 5, d3.max(data, d=>d.Rank) * 1 / 5, d3.min(data, d => d.Rank)];

// adding radar circles
svg.append("g")
.selectAll("circle")
.data(ticks)
.join("circle")
.attr("cx", width/2)
.attr("cy", height/2 )
.attr("fill", "none")
.attr("stroke", "gray")
.attr("stroke-opacity", 0.4)
.attr("r", d => radialScale(d))

// adding text for circle ranges
svg.append("g")
.selectAll(".ticklabel")
.data(ticks)
.join("text")
.attr("class", "ticklabel")
.attr("x", width / 2 + 5)
.attr("y", d => height / 2 - radialScale(d))
.text(d => d.toString())
/**
* Takes angle and value and returns coordinates that match with the value at that angle!
* @constructor
* @param (int) angle - ngle
* @param (int) value - value of angle
*/
function angleToCoordinate(angle, value) {
let x = Math.cos(angle) * radialScale(value);
let y = Math.sin(angle) * radialScale(value);
return {"x": width / 2 + x, "y": height / 2 - y};
}
// adding axes for each platform...
const axesData = chartdata.map((f, i) => {
let angle = (Math.PI / 2) + (2 * Math.PI * i / chartdata.length);
return {
"name": f.platform_name,
"angle": angle,
"line_coord": angleToCoordinate(angle, 0),
"label_coord": angleToCoordinate(angle, -5)
}
})

// drawing lines to each platform
svg.append("g")
.selectAll("line")
.data(axesData)
.join("line")
.attr("x1", width / 2)
.attr("y1", height / 2)
.attr("x2", d => d.line_coord.x)
.attr("y2", d => d.line_coord.y)
.attr("stroke", "black")
.attr("stroke-opacity", 0.5)

// adding labels for each platform
svg.append("g")
.attr("text-anchor", "middle")
.selectAll(".axislabel")
.data(axesData)
.join("text")
.attr("x", d => d.label_coord.x)
.attr("y", d => d.label_coord.y)
.text(d => d.name);

// line and color...
const line = d3.line()
.x(d => d.x)
.y(d => d.y);
const colors = ["darkorange", "navy"];

/**
* Returns an array of points for the path to follow based on the angle and value of the point
* This currently has an issue where it basically loops through the whole chart, on every loop? I'm not sure how to fix it, but it'll have to do with the time we have. It still works at least!
* @constructor
* @param (array) data_point - the chart data
* @param (int) value - value associated with field from chart data
*/
function getRankPath(data_point, value){
let coordinates = [];
for (var i = 0; i < chartdata.length; i++){
let angle = (Math.PI / 2) + (2 * Math.PI * i / chartdata.length);
coordinates.push(angleToCoordinate(angle, chartdata[i][value]));
}
return coordinates;
}

svg.append("g")
.selectAll("path")
.data(chartdata)
.join("path")
.datum(d => getRankPath(d, "averageRank"))
.attr("d", line)
.attr("stroke-width", 1)
.attr("stroke", colors[0])
.attr("fill", colors[0])
.attr("stroke-opacity", 1)
.attr("fill-opacity", 0.01)
svg.append("g")
.selectAll("path")
.data(chartdata)
.join("path")
.datum(d => getRankPath(d, "averageScore"))
.attr("d", line)
.attr("stroke-width", 1)
.attr("stroke", colors[1])
.attr("fill", colors[1])
.attr("stroke-opacity", 1)
.attr("fill-opacity", 0.01)

// adding a legend for ease of reading! :3
svg.append("circle")
.attr("cx",50)
.attr("cy",50)
.attr("r", 6)
.style("fill", "darkorange")
svg.append("circle")
.attr("cx",50)
.attr("cy",80)
.attr("r", 6)
.style("fill", "navy")

svg.append("text")
.attr("x", 70)
.attr("y", 50)
.text("Average Rank")
.style("font-size", "11px")
.attr("alignment-baseline","middle")

svg.append("text")
.attr("x", 70)
.attr("y", 80)
.text("Average Metascore")
.style("font-size", "11px")
.attr("alignment-baseline","middle")
svg.append("text")
.attr("x", 70)
.attr("y", 90)
.text("(scaled to % of range 94-99)")
.style("font-size", "9px")
.attr("alignment-baseline","middle")
return svg.node();
}
Insert cell
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