Public
Edited
May 1
Insert cell
Insert cell
data.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
Insert cell
Insert cell
Insert cell
d3 = require("d3@6")
Insert cell
Insert cell
top_100_games = d3.csvParse(await FileAttachment("data.csv").text(), d3.autoType)
Insert cell
Insert cell
Insert cell
Insert cell
japanPlatforms = ["Nintendo64", "PlayStation", "PlayStation 3", "Dreamcast",
"Wii", "Switch", "PlayStation 4", "GameCube", "Wii U",
"PlayStation 5", "PlayStation 2", "Game Boy Advance", "3DS"]
Insert cell
Insert cell
usPlatforms = ["Xbox 360", "Xbox One", "PC", "Xbox Series X"]
Insert cell
Insert cell
platformGroups = d3.group(data, d => d.Platform)
Insert cell
Insert cell
japanCount = Array.from(platformGroups.entries()).reduce((count, [platform, games]) => {
return japanPlatforms.includes(platform) ? count + games.length : count;
}, 0)

Insert cell
Insert cell
usCount = Array.from(platformGroups.entries()).reduce((count, [platform, games]) => {
return usPlatforms.includes(platform) ? count + games.length : count;
}, 0)
Insert cell
Insert cell
countryData = [
{ country: "Japan", count: japanCount },
{ country: "United States", count: usCount }
]
Insert cell
Insert cell
width_map = 960
Insert cell
Insert cell
height_map = 500
Insert cell
Insert cell
svg_map = d3.create("svg")
.attr("width", width_map)
.attr("height", height_map)
Insert cell
Insert cell
projection = d3.geoNaturalEarth1()
.scale(width_map / 6)
.translate([width_map / 2, height_map / 2]);
Insert cell
Insert cell
path = d3.geoPath()
.projection(projection);
Insert cell
Insert cell
colorScale = d3.scaleLinear()
.domain([0, 100])
.range(["white", "green"]);
Insert cell
Insert cell
geojson = d3.json("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson")
Insert cell
Insert cell
svg_map.selectAll("path")
.data(geojson.features)
.enter()
.append("path")
.attr("d", path)
.attr("fill", d => {
if (d.properties.name === "Japan") {
const japanData = countryData.find(item => item.country === "Japan");
return colorScale(japanData ? japanData.count : 0);
} else if (d.properties.name === "United States of America") {
const usData = countryData.find(item => item.country === "United States");
return colorScale(usData ? usData.count : 0);
}
return "#f0f0f0";
})
.attr("stroke", "#999")
.attr("stroke-width", 0.5);

Insert cell
Insert cell
japanCoords = projection([139.6917, 35.6895])
Insert cell
Insert cell
usCoords = projection([-97.0, 38.0])
Insert cell
Insert cell
svg_map.append("circle")
.attr("cx", japanCoords[0])
.attr("cy", japanCoords[1])
.attr("r", 10)
.attr("fill", d => {
const japanData = countryData.find(item => item.country === "Japan");
return colorScale(japanData ? japanData.count : 0);
})
.attr("stroke", "#000")
.attr("stroke-width", 1);

Insert cell
Insert cell
svg_map.append("circle")
.attr("cx", usCoords[0])
.attr("cy", usCoords[1])
.attr("r", 10)
.attr("fill", d => {
const usData = countryData.find(item => item.country === "United States");
return colorScale(usData ? usData.count : 0);
})
.attr("stroke", "#000")
.attr("stroke-width", 1);
Insert cell
Insert cell
svg_map.append("text")
.attr("x", japanCoords[0])
.attr("y", japanCoords[1] - 15)
.text("Japan")
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.attr("font-weight", "bold");
Insert cell
Insert cell
svg_map.append("text")
.attr("x", usCoords[0])
.attr("y", usCoords[1] - 15)
.text("USA")
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.attr("font-weight", "bold");
Insert cell
Insert cell
svg_map.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.attr("font-size", "24px")
.attr("font-weight", "bold")
.text("Top 100 Metacritic Games by Country of Origin");
Insert cell
Insert cell
legendWidth = 300
Insert cell
Insert cell
legendHeight = 50;
Insert cell
Insert cell
legendX = width_map - legendWidth - 20;
Insert cell
Insert cell
legendY = height_map - legendHeight - 20;
Insert cell
Insert cell
legend = svg_map.append("g").attr("transform", `translate(${legendX}, ${legendY})`)
Insert cell
Insert cell
defs = svg_map.append("defs")
Insert cell
Insert cell
gradient = defs.append("linearGradient")
.attr("id", "legend-gradient")
.attr("x1", "0%")
.attr("x2", "100%")
.attr("y1", "0%")
.attr("y2", "0%")
Insert cell
Insert cell
gradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", "white")
Insert cell
Insert cell
gradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", "green")
Insert cell
Insert cell
legend.append("rect")
.attr("width", legendWidth)
.attr("height", 20)
.style("fill", "url(#legend-gradient)")
Insert cell
Insert cell
legend.append("text")
.attr("x", 0)
.attr("y", -5)
.text("Games by Platform Origin")
.attr("font-weight", "bold")
Insert cell
Insert cell
legend.append("text")
.attr("x", 0)
.attr("y", 35)
.text("1")
.append("text")
.attr("x", legendWidth)
.attr("y", 35)
.attr("text-anchor", "end")
.text("100")
Insert cell
Insert cell
legend.append("text")
.attr("x", 0)
.attr("y", 55)
.text(`Japan: ${countryData.find(item => item.country === "Japan")?.count || 0} games`)
Insert cell
Insert cell
legend.append("text")
.attr("x", legendWidth)
.attr("y", 55)
.attr("text-anchor", "end")
.text(`USA: ${countryData.find(item => item.country === "United States")?.count || 0} games`)
Insert cell
Insert cell
platformInfo = svg_map.append("g")
.attr("transform", `translate(20, ${height_map - 80})`)
Insert cell
Insert cell
platformInfo.append("text")
.attr("y", 0)
.text("Japanese Platforms: Nintendo64, PlayStation 1-5, Dreamcast, Wii, Switch, etc")
.attr("font-size", "12px")
Insert cell
Insert cell
platformInfo.append("text")
.attr("y", 20)
.text("American Platforms: Xbox 360, Xbox One, PC, Xbox Series X")
.attr("font-size", "12px")
Insert cell
svg_map.node()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
metascore_data = top_100_games.map(d => {
const year = d.Date.slice(-2);
return {
...d,
year: year,
Metascore: +d.Metascore
};
})
Insert cell
Insert cell
yearGroups = d3.group(metascore_data, d => d.year)
Insert cell
Insert cell
yearMetascores = Array.from(yearGroups, ([year, games]) => ({
year: year,
meanScore: d3.mean(games, d => d.Metascore),
count: games.length,
fullYear: year >= '50' ? 1900 + parseInt(year) : 2000 + parseInt(year)
}))
Insert cell
Insert cell
yearMetascores.sort((a, b) => a.fullYear - b.fullYear)
Insert cell
Insert cell
maxMeanYear = d3.greatest(yearMetascores, d => d.meanScore)
Insert cell
yearMetascores
Insert cell
Insert cell
width_hist = 720
Insert cell
Insert cell
height_hist = 320
Insert cell
Insert cell
svg_hist = d3.create("svg")
.attr("width", width_hist + 50 + 30)
.attr("height", height_hist + 40 + 40)
Insert cell
Insert cell
g_hist = svg_hist.append("g")
.attr("transform", `translate(${50},${40})`)
Insert cell
Insert cell
x = d3.scaleBand()
.domain(yearMetascores.sort((a, b) => a.fullYear - b.fullYear).map(d => `'${d.year}`)) // Sort *before* mapping for the domain
.range([0, width_hist])
.padding(0.2)
Insert cell
Insert cell
y = d3.scaleLinear()
.domain([d3.min(yearMetascores, d => d.meanScore) - 1, d3.max(yearMetascores, d => d.meanScore) + 1])
.nice()
.range([height_hist, 0])
Insert cell
Insert cell
svg_hist.selectAll(".bar")
.data(yearMetascores)
.join("rect")
.attr("class", "bar")
.attr("x", d => x(`'${d.year}`))
.attr("y", d => y(d.meanScore))
.attr("width", x.bandwidth())
.attr("height", d => height_hist - y(d.meanScore))
.attr("fill", "#aa46b4")
.attr("stroke", d => d.meanScore === maxMeanYear.meanScore ? "#000000" : "none")
.attr("stroke-width", d => d.meanScore === maxMeanYear.meanScore ? 3 : 0)
Insert cell
Insert cell
svg_hist.append("g")
.attr("transform", `translate(0,${height_hist})`)
.call(d3.axisBottom(x))
.selectAll("text")
.style("text-anchor", "middle")
Insert cell
Insert cell
svg_hist.append("g")
.call(d3.axisLeft(y).tickFormat(d => d.toFixed(1))) // 1 decimal place
Insert cell
Insert cell
svg_hist.append("text")
.attr("x", (width_hist + 50 + 30) / 2)
.attr("y", 40 / 2)
.attr("text-anchor", "middle")
.attr("font-size", "16px")
.attr("font-weight", "bold")
.text("Best Year in Gaming")
Insert cell
Insert cell
svg_hist.append("text")
.attr("x", (width_hist + 50 + 30) / 2)
.attr("y", height_hist + 40 + 40)
.attr("text-anchor", "middle")
.text("Year of Release");
Insert cell
Insert cell
svg_hist.append("text")
.attr("transform", "rotate(-90)")
.attr("y", + 15)
.attr("x", -height_hist / 2)
.attr("text-anchor", "middle")
.text("Mean Metascore")
Insert cell
svg_hist.node()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
platformCounts = d3.rollup(top_100_games, v => v.length, d => d.Platform);
Insert cell
Insert cell
totalGames = top_100_games.length
Insert cell
Insert cell
platformPercentages = Array.from(platformCounts, ([platform, count]) => ({
name: platform,
value: (count / totalGames) * 100
}));
Insert cell
Insert cell
// Star Plot Parameters
radius = 250
Insert cell
Insert cell
center = [radius, radius]
Insert cell
Insert cell
numPlatforms = platformPercentages.length
Insert cell
Insert cell
angleStep = (2 * Math.PI) / numPlatforms
Insert cell
Insert cell
platformPercentages.sort((a, b) => d3.ascending(a.name, b.name))
Insert cell
Insert cell
angles = platformPercentages.map((d, i) => i * angleStep)
Insert cell
Insert cell
radiusScale = d3.scaleLinear()
.domain([0, d3.max(platformPercentages, d => d.value)])
.range([0, radius])
Insert cell
Insert cell
line = d3.lineRadial()
.angle((d, i) => angles[i])
.radius(d => radiusScale(d.value));
Insert cell
Insert cell
// create svg container
svg_star = d3.create("svg")
.attr("width", 3 * radius)
.attr("height", 3 * radius)
Insert cell
Insert cell
// make transformation
g_star = svg_star.append("g")
.attr("transform", `translate(${center[0]}, ${center[1]})`)
Insert cell
Insert cell
gridLevels = [25, 50, 75, 100]
Insert cell
Insert cell
gridCircles = g_star.selectAll(".grid-circle")
.data(gridLevels)
.join("circle")
.attr("class", "grid-circle")
.attr("r", d => radiusScale(d))
.attr("fill", "none")
.attr("stroke", "lightgray")
.attr("stroke-width", 1)
.style("stroke-dasharray", "2 2"); // Optional: dashed lines
Insert cell
Insert cell
radialLines = g_star.selectAll(".radial-line")
.data(platformPercentages)
.join("line")
.attr("class", "radial-line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", (d, i) => radius * Math.cos(angles[i] - Math.PI / 2))
.attr("y2", (d, i) => radius * Math.sin(angles[i] - Math.PI / 2))
.attr("stroke", "lightgray")
.attr("stroke-width", 1);
Insert cell
Insert cell
axisLabels = g_star.selectAll(".axis-label")
.data(platformPercentages)
.join("text")
.attr("class", "axis-label")
.attr("x", (d, i) => (radius + 10) * Math.cos(angles[i] - Math.PI / 2))
.attr("y", (d, i) => (radius + 10) * Math.sin(angles[i] - Math.PI / 2))
.attr("dy", "0.35em")
.style("text-anchor", (d, i) => {
const angleDeg = (angles[i] * 180) / Math.PI;
return (angleDeg > 90 && angleDeg < 270) ? "end" : "start";
})
.style("font-size", "10px")
.text("100%"); // Label all axes with 100%
Insert cell
Insert cell
g_star.selectAll(".star-arm")
.data([platformPercentages]) // Bind an array containing the entire dataset
.join("path")
.attr("class", "star-arm")
.attr("d", d => line(d)) // Pass the entire array to the line function
.attr("fill", "lightblue")
.attr("opacity", 0.5)
.attr("stroke", "steelblue")
.attr("stroke-width", 2);
Insert cell
Insert cell
g_star.selectAll(".star-point")
.data(platformPercentages)
.join("circle")
.attr("class", "star-point")
.attr("cx", (d, i) => radiusScale(d.value) * Math.cos(angles[i] - Math.PI / 2)) // Adjust angle for correct orientation
.attr("cy", (d, i) => radiusScale(d.value) * Math.sin(angles[i] - Math.PI / 2)) // Adjust angle for correct orientation
.attr("r", 2)
.attr("fill", "steelblue")
Insert cell
Insert cell
g_star.selectAll(".star-label")
.data(platformPercentages)
.join("text")
.attr("class", "star-label")
.attr("x", (d, i) => (radiusScale(d.value) + 10) * Math.cos(angles[i] - Math.PI / 2)) // Position labels slightly outside
.attr("y", (d, i) => (radiusScale(d.value) + 10) * Math.sin(angles[i] - Math.PI / 2))
.attr("dy", "0.35em") // Adjust vertical alignment
.style("text-anchor", (d, i) => { // Adjust text anchor based on angle
const angleDeg = (angles[i] * 180) / Math.PI;
return (angleDeg > 90 && angleDeg < 270) ? "end" : "start";
})
.style("font-size", "10px")
.text(d => d.name)
Insert cell
svg_star.node()
Insert cell
Insert cell
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