Public
Edited
May 1
Insert cell
Insert cell
// Load the raw CSV data
rawData = FileAttachment("data.csv").csv()
Insert cell
// Process and clean the data
games = {
const processed = await rawData.map(game => ({
name: game.Name,
platform: game.Platform,
metascore: +game.Metascore,
releaseDate: new Date(game.Date),
year: new Date(game.Date).getFullYear(),
description: game.Title,
publisher: extractPublisher(game.Title),
genre: extractGenre(game.Title)
})).filter(d => !isNaN(d.metascore) && !isNaN(d.year));
// Helper function to extract publishers
function extractPublisher(desc) {
const publishers = [
"Nintendo", "Rockstar", "Sony", "Microsoft", "Valve",
"Capcom", "Bethesda", "Square Enix", "EA", "Activision",
"Ubisoft", "Sega", "Konami", "2K Games", "BioWare"
];
return publishers.find(p => desc.includes(p)) || "Other";
}
// Helper function to extract genres
function extractGenre(desc) {
const lcDesc = desc.toLowerCase();
if (lcDesc.includes("role") || lcDesc.includes("rpg")) return "RPG";
if (lcDesc.includes("shoot") || lcDesc.includes("fps")) return "Shooter";
if (lcDesc.includes("adventure")) return "Adventure";
if (lcDesc.includes("sport") || lcDesc.includes("nfl") || lcDesc.includes("nba")) return "Sports";
if (lcDesc.includes("race") || lcDesc.includes("drive") || lcDesc.includes("turismo")) return "Racing";
if (lcDesc.includes("fight") || lcDesc.includes("versus")) return "Fighting";
if (lcDesc.includes("platform")) return "Platformer";
if (lcDesc.includes("puzzle")) return "Puzzle";
if (lcDesc.includes("horror") || lcDesc.includes("survival")) return "Horror";
return "Other";
}
return processed;
}
Insert cell
viewof platformChart = {
// Process data
const platformData = d3.rollup(
games,
v => v.length,
d => d.platform
);
let sortedData = Array.from(platformData, ([platform, count]) => ({platform, count}));
// Create dropdown menu
const sortDropdown = html`
<div style="margin-bottom: 10px;">
<select class="sort-dropdown">
<option value="desc">Count (Low to High)</option>
<option value="asc">Count (High to Low)</option>
<option value="alpha-desc">Alphabetical (A-Z)</option>
<option value="alpha">Alphabetical (Z-A)</option>
</select>
</div>
`;
// Set up SVG container with viewBox for responsiveness
const container = html`<div>${sortDropdown}<div id="chart-container"></div></div>`;
const svg = d3.select(container).select("#chart-container")
.append("svg")
.attr("viewBox", "0 0 800 500")
.attr("width", "100%")
.style("max-width", "800px")
.style("height", "auto");
// Set up scales and axes
const margin = {top: 40, right: 40, bottom: 60, left: 120};
const width = 800 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
const chart = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
let x = d3.scaleLinear()
.range([0, width]);
let y = d3.scaleBand()
.range([height, 0])
.padding(0.2);
// Add axes with improved styling
const xAxis = chart.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0,${height})`)
.style("font-size", "12px");

const yAxis = chart.append("g")
.attr("class", "y axis")
.style("font-size", "12px");
// Add title with better styling
svg.append("text")
.attr("x", width / 2 + margin.left)
.attr("y", 25)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("font-weight", "600")
.text("Top Rated Games Releases by Platform");
// Create enhanced tooltip
const tooltip = d3.select("body").append("div")
.attr("class", "bar-tooltip")
.style("position", "absolute")
.style("background", "rgba(255, 255, 255, 0.95)")
.style("padding", "10px 15px")
.style("border-radius", "6px")
.style("pointer-events", "none")
.style("opacity", 0)
.style("font-family", "sans-serif")
.style("font-size", "13px")
.style("box-shadow", "0 3px 10px rgba(0,0,0,0.15)")
.style("border", "1px solid rgba(0,0,0,0.1)")
.style("backdrop-filter", "blur(2px)")
.style("transition", "all 0.2s ease");
// Initial sort
let currentSort = "desc";
updateChart(currentSort);
// Add event listener for dropdown
sortDropdown.querySelector(".sort-dropdown").addEventListener("change", (event) => {
currentSort = event.target.value;
updateChart(currentSort);
});
function updateChart(sortMethod) {
// Sort data based on selection
if (sortMethod === "desc") {
sortedData.sort((a, b) => b.count - a.count);
} else if (sortMethod === "asc") {
sortedData.sort((a, b) => a.count - b.count);
} else if (sortMethod === "alpha") {
sortedData.sort((a, b) => {
const platformA = a.platform.toLowerCase();
const platformB = b.platform.toLowerCase();
return platformA.localeCompare(platformB);
});
} else if (sortMethod === "alpha-desc") {
sortedData.sort((a, b) => {
const platformA = a.platform.toLowerCase();
const platformB = b.platform.toLowerCase();
return platformB.localeCompare(platformA);
});
}
// Update scales
x.domain([0, d3.max(sortedData, d => d.count) * 1.05]);
y.domain(sortedData.map(d => d.platform));
// Update axes with transitions
xAxis.transition().duration(750).call(d3.axisBottom(x));
yAxis.transition().duration(750).call(d3.axisLeft(y));
// Join new data with old elements
const bars = chart.selectAll(".bar")
.data(sortedData, d => d.platform);
// Exit old elements
bars.exit()
.transition()
.duration(500)
.attr("y", height)
.attr("height", 0)
.style("opacity", 0)
.remove();
// Enter new elements
const barsEnter = bars.enter()
.append("g")
.attr("class", "bar")
.attr("transform", d => `translate(0,${y(d.platform)})`)
.style("opacity", 0)
.on("mouseover", function(event, d) {
d3.select(this).raise();
tooltip
.style("opacity", 1)
.html(`
<div style="font-weight:600;margin-bottom:4px">${d.platform}</div>
<div>Total games: ${d.count}</div>
`)
.style("left", `${event.pageX + 15}px`)
.style("top", `${event.pageY - 28}px`);
})
.on("mouseout", function() {
tooltip.style("opacity", 0);
});
// Add bars with steelblue color and rounded corners
barsEnter.append("rect")
.attr("width", d => x(d.count))
.attr("height", y.bandwidth())
.attr("fill", "steelblue")
.attr("rx", 4)
.attr("ry", 4)
.attr("stroke", "white")
.attr("stroke-width", 1);
// Add value labels
barsEnter.append("text")
.attr("x", d => x(d.count) + 5)
.attr("y", y.bandwidth() / 2)
.attr("dy", "0.35em")
.text(d => d.count)
.style("fill", "white")
.style("font-size", "12px")
.style("font-weight", "bold");
// Update existing elements
bars.merge(barsEnter)
.transition()
.duration(750)
.attr("transform", d => `translate(0,${y(d.platform)})`)
.style("opacity", 1);
barsEnter.select("rect")
.transition()
.duration(750)
.attr("width", d => x(d.count));
barsEnter.select("text")
.transition()
.duration(750)
.attr("x", d => x(d.count) + 5);
}
// Add CSS styles
svg.append("style").text(`
.bar-tooltip {
transition: all 0.2s ease;
}
.sort-dropdown {
padding: 6px;
border-radius: 4px;
border: 1px solid #ddd;
font-size: 14px;
}
.bar rect {
transition: width 0.75s ease;
}
`);
return container;
}
Insert cell
{
// Dimensions and margins
const margin = {top: 40, right: 80, bottom: 60, left: 60};
const width = 800;
const height = 600;
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;

// Get unique platforms and sort them
const platforms = [...new Set(games.map(d => d.platform))].sort();
const allPlatforms = "All Platforms";
// Create SVG with responsive viewBox
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");

// Create controls container
const controls = svg.append("g")
.attr("transform", `translate(${margin.left}, 20)`);

// Add clean platform filter dropdown
controls.append("foreignObject")
.attr("width", 120)
.attr("height", 30)
.append("xhtml:select")
.attr("id", "platform-filter")
.style("width", "100%")
.style("padding", "4px")
.style("border-radius", "4px")
.style("border", "1px solid #ccc")
.style("font-size", "12px")
.style("background", "#f8f8f8")
.on("change", function() {
updateChart(this.value);
})
.selectAll("option")
.data([allPlatforms, ...platforms])
.enter()
.append("option")
.attr("value", d => d)
.text(d => d);

// Create chart group
const chart = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top + 20})`);

// Create scales with focused y-axis (90-100)
const x = d3.scaleLinear()
.domain(d3.extent(games, d => d.year))
.range([0, innerWidth])
.nice();

const y = d3.scaleLinear()
.domain([90, 100])
.range([innerHeight, 0]);

// Vibrant color scale that makes points stand out
const color = d3.scaleOrdinal()
.domain(platforms)
.range([
"#FF6B6B", "#4ECDC4", "#45B7D1", "#FFA07A", "#98D8C8",
"#F06292", "#7986CB", "#9575CD", "#64B5F6", "#4DB6AC",
"#81C784", "#FFD54F", "#FF8A65", "#A1887F", "#90A4AE"
]);

// Add axes with better styling
chart.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(x).tickFormat(d3.format("d")))
.style("font-size", "10px")
.call(g => g.append("text")
.attr("x", innerWidth)
.attr("y", 30)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.text("Year"));

chart.append("g")
.call(d3.axisLeft(y).ticks(5))
.style("font-size", "10px")
.call(g => g.append("text")
.attr("transform", "rotate(-90)")
.attr("y", -40)
.attr("dy", "0.71em")
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.text("Metascore (90-100)"));

// Add grid lines with better styling
chart.append("g")
.attr("class", "grid")
.call(d3.axisLeft(y)
.tickSize(-innerWidth)
.tickFormat(""))
.selectAll(".tick line")
.attr("stroke", "#f0f0f0")
.attr("stroke-width", 1);

// Add title with better styling
svg.append("text")
.attr("x", width / 2)
.attr("y", 20)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("font-weight", "bold")
.style("font-family", "sans-serif")
.text("Meta Score Distribution for Top Rated Games");

// Create enhanced tooltip
const tooltip = d3.select("body").append("div")
.attr("class", "scatter-tooltip")
.style("position", "absolute")
.style("background", "rgba(255, 255, 255, 0.96)")
.style("border-radius", "6px")
.style("padding", "12px")
.style("pointer-events", "none")
.style("opacity", 0)
.style("box-shadow", "0 3px 14px rgba(0,0,0,0.15)")
.style("border", "1px solid rgba(0,0,0,0.1)")
.style("font-family", "sans-serif")
.style("font-size", "12px")
.style("max-width", "300px")
.style("backdrop-filter", "blur(2px)")
.style("transition", "opacity 0.2s, transform 0.2s");

// Initial chart render with more visible points
let circles = chart.append("g")
.selectAll("circle")
.data(games)
.join("circle")
.attr("cx", d => x(d.year))
.attr("cy", d => y(d.metascore))
.attr("r", 5) // Slightly larger default size
.attr("fill", d => color(d.platform))
.attr("opacity", 0.9) // More opaque
.attr("stroke", "white")
.attr("stroke-width", 1)
.on("mouseover", function(event, d) {
d3.select(this)
.attr("r", 7) // More noticeable hover size
.attr("stroke-width", 2);

tooltip.html(`
<div class="tooltip-header" style="margin-bottom: 8px;">
<div style="font-size: 18px; font-weight: bold; color: ${color(d.platform)}; margin-bottom: 2px;">${d.metascore}</div>
<div style="font-size: 14px; font-weight: 600;">${d.name}</div>
</div>
<div class="tooltip-details" style="font-size: 12px; line-height: 1.5;">
<div><span style="font-weight: 500; color: #555;">Platform:</span> ${d.platform}</div>
<div><span style="font-weight: 500; color: #555;">Year:</span> ${d.year}</div>
<div><span style="font-weight: 500; color: #555;">Publisher:</span> ${d.publisher || 'N/A'}</div>
<div><span style="font-weight: 500; color: #555;">Genre:</span> ${d.genre || 'N/A'}</div>
</div>
`)
.style("left", `${event.pageX + 15}px`)
.style("top", `${event.pageY - 15}px`)
.style("opacity", 1)
.style("transform", "translateY(-5px)");
})
.on("mouseout", function() {
d3.select(this)
.attr("r", 5)
.attr("stroke-width", 1);
tooltip
.style("opacity", 0)
.style("transform", "translateY(0)");
})
.on("mousemove", (event) => {
tooltip
.style("left", `${event.pageX + 15}px`)
.style("top", `${event.pageY - 15}px`);
});

// Update chart with enhanced animations
function updateChart(selectedPlatform) {
const filteredData = selectedPlatform === allPlatforms
? games
: games.filter(d => d.platform === selectedPlatform);

// Update circles with smooth transitions
circles = chart.selectAll("circle")
.data(filteredData, d => d.name + d.platform + d.year);

// Exit animation - shrink and fade out
circles.exit()
.transition()
.duration(600)
.attr("r", 0)
.attr("opacity", 0)
.remove();

// Enter animation - grow and fade in
circles.enter()
.append("circle")
.attr("cx", d => x(d.year))
.attr("cy", d => y(d.metascore))
.attr("r", 0)
.attr("fill", d => color(d.platform))
.attr("opacity", 0)
.attr("stroke", "white")
.attr("stroke-width", 1)
.call(enter => {
enter.transition()
.duration(600)
.delay((d, i) => selectedPlatform === allPlatforms ? i * 3 : 0)
.attr("r", 5)
.attr("opacity", 0.9);
});

// Update existing points
circles
.transition()
.duration(600)
.attr("cx", d => x(d.year))
.attr("cy", d => y(d.metascore));

// Re-attach event handlers
chart.selectAll("circle")
.on("mouseover", function(event, d) {
d3.select(this)
.attr("r", 7)
.attr("stroke-width", 2);
tooltip.html(`
<div class="tooltip-header" style="margin-bottom: 8px;">
<div style="font-size: 18px; font-weight: bold; color: ${color(d.platform)}; margin-bottom: 2px;">${d.metascore}</div>
<div style="font-size: 14px; font-weight: 600;">${d.name}</div>
</div>
<div class="tooltip-details" style="font-size: 12px; line-height: 1.5;">
<div><span style="font-weight: 500; color: #555;">Platform:</span> ${d.platform}</div>
<div><span style="font-weight: 500; color: #555;">Year:</span> ${d.year}</div>
<div><span style="font-weight: 500; color: #555;">Publisher:</span> ${d.publisher || 'N/A'}</div>
<div><span style="font-weight: 500; color: #555;">Genre:</span> ${d.genre || 'N/A'}</div>
</div>
`)
.style("left", `${event.pageX + 15}px`)
.style("top", `${event.pageY - 15}px`)
.style("opacity", 1)
.style("transform", "translateY(-5px)");
})
.on("mouseout", function() {
d3.select(this)
.attr("r", 5)
.attr("stroke-width", 1);
tooltip
.style("opacity", 0)
.style("transform", "translateY(0)");
});
}

// Add CSS styles
svg.append("style").text(`
.scatter-tooltip {
transition: all 0.2s ease-out;
}
.tooltip-header {
border-bottom: 1px solid rgba(0,0,0,0.1);
padding-bottom: 6px;
margin-bottom: 6px;
}
#platform-filter {
cursor: pointer;
transition: border-color 0.2s;
}
#platform-filter:hover {
border-color: #999;
}
#platform-filter:focus {
outline: none;
border-color: #4a90e2;
box-shadow: 0 0 0 2px rgba(74,144,226,0.2);
}
`);

return svg.node();
}
Insert cell
{
// Specify the chart's dimensions
const width = 928;
const height = 500;
const marginTop = 40;
const marginRight = 20;
const marginBottom = 60;
const marginLeft = 60;

// Process the data for stacking
const genreCounts = d3.rollup(
games,
v => v.length,
d => d.genre,
d => d.year
);

// Get unique genres and years
const genres = Array.from(genreCounts.keys());
const years = Array.from(new Set(games.map(d => d.year))).sort((a, b) => a - b);

// Prepare data in stack format
const stackData = genres.map(genre => ({
genre,
values: years.map(year => ({
year,
count: genreCounts.get(genre)?.get(year) || 0
}))
}));

// Filter out genres with very few games
const filteredData = stackData.filter(d =>
d3.sum(d.values, v => v.count) > 10
);

// Create the stack layout - CHANGED OFFSET TO NONE TO REMOVE NEGATIVES
const series = d3.stack()
.keys(filteredData.map(d => d.genre))
.value((year, genre) => {
const genreData = filteredData.find(d => d.genre === genre);
const yearData = genreData.values.find(v => v.year === year);
return yearData ? yearData.count : 0;
})
.offset(d3.stackOffsetNone) // Changed from wiggle to remove negatives
.order(d3.stackOrderNone)
(years);

// Prepare scales
const x = d3.scaleLinear()
.domain(d3.extent(years))
.range([marginLeft, width - marginRight]);

// MODIFIED Y-SCALE TO START AT 0
const y = d3.scaleLinear()
.domain([0, d3.max(series.flat(2))]) // Now starts at 0
.range([height - marginBottom, marginTop]);

const color = d3.scaleOrdinal()
.domain(series.map(d => d.key))
.range(d3.schemeTableau10);

// Construct the area shape
const area = d3.area()
.x((d, i) => x(years[i]))
.y0(d => y(d[0]))
.y1(d => y(d[1]))
.curve(d3.curveBasis);

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

// Add y-axis with grid lines - NOW ONLY POSITIVE
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(d3.axisLeft(y).ticks(height / 80))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("x2", width - marginLeft - marginRight)
.attr("stroke-opacity", 0.1))
.call(g => g.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text("↑ Game Count"));

// Add x-axis
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x).tickFormat(d3.format("d")).tickSizeOuter(0))
.call(g => g.append("text")
.attr("x", width - marginRight)
.attr("y", 30)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.text("Year"));

// Create a group for the streams
const streamGroup = svg.append("g");

// Add paths for each genre
const paths = streamGroup.selectAll()
.data(series)
.join("path")
.attr("fill", d => color(d.key))
.attr("d", area)
.attr("opacity", 0.7)
.attr("stroke", "white")
.attr("stroke-width", 0.5)
.on("mouseover", function(event, d) {
d3.select(this).attr("opacity", 1).raise();
const totalGames = d3.sum(d, segment => segment[1] - segment[0]);
const peakIndex = d3.maxIndex(d, segment => segment[1] - segment[0]);
const peakYear = years[peakIndex];
const peakCount = d[peakIndex][1] - d[peakIndex][0];
tooltip
.style("opacity", 1)
.html(`
<div class="tooltip-title">${d.key}</div>
<div>Total games: ${totalGames}</div>
<div>Peak year: ${peakYear} (${peakCount} games)</div>
`);
})
.on("mousemove", function(event) {
tooltip
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 20) + "px");
})
.on("mouseout", function() {
d3.select(this).attr("opacity", 0.7);
tooltip.style("opacity", 0);
});

// Add chart title
svg.append("text")
.attr("x", width / 2)
.attr("y", marginTop)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("font-weight", "bold")
.text("Game Releases by Genre Over Time");

// Create tooltip
const tooltip = d3.select("body").append("div")
.attr("class", "streamgraph-tooltip")
.style("position", "absolute")
.style("background", "white")
.style("padding", "8px 12px")
.style("border", "1px solid #ddd")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("opacity", 0)
.style("font-family", "sans-serif")
.style("font-size", "12px")
.style("box-shadow", "0 2px 4px rgba(0,0,0,0.1)");

// Add CSS styles
svg.append("style").text(`
.streamgraph-tooltip {
transition: opacity 0.2s;
}
.tooltip-title {
font-weight: bold;
margin-bottom: 4px;
color: #333;
border-bottom: 1px solid #eee;
}
`);

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