Public
Edited
Apr 30
1 star
Insert cell
Insert cell
/*
Motivations; We want to display company marketshare/success/dominance across the earlier era, versus the more modern era. This is a quick, easy to understand, non-cluttered visualization that shows which companies consistently hits and what proportion of the total hits were theirs.

Insights learned from this visualization of the dataset;

We want to see developer/company success/market share from our dataset of top performing games, across the early era, and the more modern era. We can see that PlayStation was FIRMLY dominant in the earlier 13 years of our data from 1996 - 2009, with being close to double any other company's output. Sega's flop in the conesole wars is immediately apparent. Nintendo remains stable, but not dominant, across the decades. It's also safe to say that Microsoft's Xbox line has gained some ground, making it Sony's biggest competitor. When interacting with the tooltip, it's interesting to see that much of Nintendo's success has been in handheld consoles specifically.

Strengths of this visualization; It's straigtforward and immediately portrays its story. The viewer can immediately understand That we're comparing two "eras" of video games. It's also amazing how accurate this data is to the history of these "console wars"; PlayStation's dominance, Sega's falloff, and the relative equality of the more modern era. The tooltip function allows the user to get instant information about console specifications, so that our aggregate for the visual does not remove important information about /which/ consoles spefically were top hits. However, because of how straightforward the Pie Chart is, interactivity by the viewer is not required to portray the data's story.

Weaknesses; Even with our use of aggregate, the colors are jarring. I *have* read the readings on color scheme choice, but here, I felt that it was important to use colors that represented the companies, with Red, Blue, and Green representing Sony, Nintendo, and Microsoft's Xbox quite well, I feel. That being said, it's still quite an ugly color scheme, but given that the data was so clearly divergent into different buckets, I decided it was appropriate, even if there are less jarring color schemes.
*/

visualizationOne = {
// STEP ONE: Prep chart sizing and coloring

const radius = Math.min(width, height) / 3.5; // Set pie radius based on available space

const colorScale = d3.scaleOrdinal() // Create a color scale for company names
.domain(["Sony", "Nintendo", "Microsoft", "Sega", "PC"]) // Define which company maps to which color
.range(["green", "red", "blue", "orange", "grey"]); // Assign colors

// STEP TWO: Create the base SVG we'll draw on

const svg = d3.create("svg") // Create the SVG element
.attr("viewBox", [0, 0, width, height]) // Set the viewable dimensions
.style("font", "12px sans-serif"); // Set global font style for all text

// STEP THREE: Define pie layout logic and arc shape

const pie = d3.pie() // D3 function that computes pie layout
.value(d => d.count) // Each slice size is based on company game count
.sort(null); // Keep original order (don’t sort slices)

const arc = d3.arc() // Generator to build each arc shape
.outerRadius(radius) // Full size of slice (no donut)
.innerRadius(0); // 0 for full pie (not donut)

// STEP FOUR: Tooltip logic (shown on hover)

const tooltip = d3.select("body").append("div") // Create floating tooltip div
.attr("class", "tooltip") // Assign CSS class
.style("position", "absolute") // Float it relative to page
.style("pointer-events", "none") // Disable interactions
.style("padding", "8px") // Style: padding
.style("background", "white") // Background color
.style("border", "1px solid #ccc") // Light border
.style("border-radius", "4px") // Rounded corners
.style("box-shadow", "0 2px 6px rgba(0,0,0,0.1)") // Subtle drop shadow
.style("opacity", 0); // Start invisible

// STEP FIVE: Function to draw one pie chart
// Accepts the data, a horizontal position, and a label (i.e. time range)

function drawPie(data, cx, label) {
const g = svg.append("g") // Create new group for this pie
.attr("transform", `translate(${cx},${height / 2})`); // Center pie at (cx, middle of SVG)

const pieData = pie(data); // Run pie layout on data

const total = d3.sum(data, d => d.count); // Total games, used to calculate percentages

// STEP SIX: Draw each pie slice

g.selectAll("path") // For each slice (path element)
.data(pieData) // Bind pie-layout data
.enter()
.append("path") // Create SVG path for slice
.attr("d", arc) // Generate arc shape
.attr("fill", d => colorScale(d.data.company)) // Color based on company

// STEP SEVEN: Hover functionality for the mouseover!

.on("mouseover", (event, d) => { // Create and define the tooltip event
tooltip
.html(`
<strong>${d.data.company}</strong><br>
${d.data.tooltip.replace(/\n/g, "<br>")}`) // Set tooltip content: bold company name and multiline tooltip info

.style("left", (event.pageX + 10) + "px") // Position tooltip horizontally beside mouse
.style("top", (event.pageY + 10) + "px") // vertically beside
.style("opacity", 1); // Pop into view!
})

.on("mouseout", () => { // ... Back to transparent!
tooltip.style("opacity", 0);
});


// STEP EIGHT: Percent labels on the slices!

g.selectAll("text.label") // Select text elements with class "label"
.data(pieData) // Bind pie chart data (angle-based)
.enter() // enter() = For each data item, create a new text element
.append("text")
.attr("class", "label") // Add CSS class for any future styling

.attr("transform", d => `translate(${arc.centroid(d)})`)
// Use arc.centroid(d) to get [x, y] at the center of the slice, and move the label there

.attr("text-anchor", "middle") // Center-align the text horizontally
.attr("dy", "0.35em") // Vertical offset so label sits visually centered

.style("fill", "white") // White text for contrast on colored slices
.style("font-weight", "bold") // Make labels visually strong

.text(d => `${((d.data.count / total) * 100).toFixed(1)}%`); // Computers the percent, to a single decimal place, for slices

// STEP NINE: Pie labels!

g.append("text") // Append a new <text> element to the pie group
.attr("y", radius + 20) // Position below the pie chart (20px below radius)
.attr("text-anchor", "middle") // Centered horizontally with respect to pie
.attr("font-weight", "bold") // Bold for visibility
.text(label); // Set the label (We got this passed )

}

// STEP TEN: Draw two pie charts for the two eras
// This shifts the positioning, artistic choices
drawPie(processedData.byPeriod.before2010, width * 0.22, "1996–2009")
drawPie(processedData.byPeriod.after2009, width * 0.58, "2010–2023");


// STEP ELEVEN: Build the legend

const legend = svg.append("g")
.attr("transform", `translate(${width - margin.right}, ${margin.top})`);
// Create a group <g> element for the legend, positioned in top-right corner

const companies = ["Sony", "Nintendo", "Microsoft", "Sega", "PC"];
// Manually define the set of companies expected in the pie charts

companies.forEach((company, i) => {
const legendRow = legend.append("g")
.attr("transform", `translate(0, ${i * 20})`);
// Create a horizontal legend row for each company, stacking vertically (20px apart)

legendRow.append("rect")
.attr("width", 12)
.attr("height", 12)
.attr("fill", colorScale(company)); // Legend box, for the companies

legendRow.append("text")
.attr("x", -8) // Alighment for the square
.attr("y", 10)
.attr("text-anchor", "end") // Right-align the text
.text(company); // Display the company name
});

return svg.node();
}
Insert cell
data = d3.csvParse(await FileAttachment("data.csv").text(), d3.autoType)
Insert cell
d3 = require("d3@6")
Insert cell
processedData = {
// Map each console to its parent company
// We're going to aggregate here. Class terms! Simply too many consoles for a visualization to provide a user with any accurate data. Without consolidating, we'd have to use shades of colors to distinguish between the consoles, which would be very ugly and messy. This way, we can simply show company dominance over the decades. The tooltip function we will build will display the subinformation, but that is just a plus; a viewer doesn't have to interact with the graphic to get the overall picture!
const platformToCompany = {
"Nintendo 64": "Nintendo",
"Game Boy Advance": "Nintendo",
"3DS": "Nintendo",
"Wii": "Nintendo",
"Wii U": "Nintendo",
"Switch": "Nintendo",
"GameCube": "Nintendo",
"PlayStation": "Sony",
"PlayStation 2": "Sony",
"PlayStation 3": "Sony",
"PlayStation 4": "Sony",
"PlayStation 5": "Sony",
"Xbox": "Microsoft",
"Xbox 360": "Microsoft",
"Xbox One": "Microsoft",
"Xbox Series X": "Microsoft",
"Dreamcast": "Sega",
"PC": "PC"
};

// STEP ONE: Parse each game's release date and assign a year and company

// d3 parser; reads dates like from our data csv into JavaScript Date objects
const parseDate = d3.timeParse("%d-%b-%y");

data.forEach(d => {
if (d.Date) {
const parsed = parseDate(d.Date); // Convert string date into Date object (or null if fails)
d.Year = parsed ? parsed.getFullYear() : null; // Extract 4-digit year if valid, otherwise store null
} else {
d.Year = null; // I'm honestly still not sure if there's missing dates,
// but simply so the project scales with different data sets in the same format,
// allowing us to mutate the data and this still be usable.
}

d.Company = platformToCompany[d.Platform]; // Match platform to its developer
});

// STEP TWO: Group each game into 1996 - 2009 (pre2010), 2010 - 2023 (post2009)

// Our maps for company counts for each pie
const before2010 = new Map();
const after2009 = new Map();

for (const d of data) { // Loop through filtered entries, funnel into the correct map
const targetMap = d.Year <= 2009 ? before2010 : after2009;

// Pull existing company data if present, otherwise initialize
const entry = targetMap.get(d.Company) || {
count: 0, // total number of games for this company
platformCounts: new Map() // tracks how many games came from each individual platform
};
entry.count += 1; // Increment the correct company's bucket
// We also need to increment the tooltip keeping track of specific console info
entry.platformCounts.set(
d.Platform,
(entry.platformCounts.get(d.Platform) || 0) + 1 // Inrement the console count, or initialize to one
);
targetMap.set(d.Company, entry); // update the map with modified entry
}


// STEP FOUR: Convert map data into array format. Create tooltip

// Takes a company map and returns an array with {company, count, tooltip}
// Each item will include: company name, total game count, and a tooltip string
function transform(map) {
return Array.from(
map,
([company, { count, platformCounts }]) => { // Creates the tooltip for each slice
const tooltip = Array.from( // Example: "PlayStation: 6 (return) PlayStation 2: 3"
platformCounts, // iterate over each [platform, count] pair
([platform, count]) => `${platform}: ${count}` // convert each pair to a string line
).join("\n"); // join all lines with newline for multiline tooltip
return {
company, // company name (e.g., "Sony")
count, // total number of games across all platforms
tooltip // preformatted breakdown for on-hover display
};
}
);
}

// Data is processed for our next step!
return {
byPeriod: {
before2010: transform(before2010),
after2009: transform(after2009)
}
};
}
Insert cell
margin = ({ top: 40, right: 200, bottom: 40, left: 40 })
Insert cell
height = 500
Insert cell
width = 1000
Insert cell
dualPieChart = {
// STEP ONE: Prep chart sizing and coloring

const radius = Math.min(width, height) / 3.5; // Set pie radius based on available space

const colorScale = d3.scaleOrdinal() // Create a color scale for company names
.domain(["Sony", "Nintendo", "Microsoft", "Sega", "PC"]) // Define which company maps to which color
.range(["green", "red", "blue", "orange", "grey"]); // Assign colors

// STEP TWO: Create the base SVG we'll draw on

const svg = d3.create("svg") // Create the SVG element
.attr("viewBox", [0, 0, width, height]) // Set the viewable dimensions
.style("font", "12px sans-serif"); // Set global font style for all text

// STEP THREE: Define pie layout logic and arc shape

const pie = d3.pie() // D3 function that computes pie layout
.value(d => d.count) // Each slice size is based on company game count
.sort(null); // Keep original order (don’t sort slices)

const arc = d3.arc() // Generator to build each arc shape
.outerRadius(radius) // Full size of slice (no donut)
.innerRadius(0); // 0 for full pie (not donut)

// STEP FOUR: Tooltip logic (shown on hover)

const tooltip = d3.select("body").append("div") // Create floating tooltip div
.attr("class", "tooltip") // Assign CSS class
.style("position", "absolute") // Float it relative to page
.style("pointer-events", "none") // Disable interactions
.style("padding", "8px") // Style: padding
.style("background", "white") // Background color
.style("border", "1px solid #ccc") // Light border
.style("border-radius", "4px") // Rounded corners
.style("box-shadow", "0 2px 6px rgba(0,0,0,0.1)") // Subtle drop shadow
.style("opacity", 0); // Start invisible

// STEP FIVE: Function to draw one pie chart
// Accepts the data, a horizontal position, and a label (i.e. time range)

function drawPie(data, cx, label) {
const g = svg.append("g") // Create new group for this pie
.attr("transform", `translate(${cx},${height / 2})`); // Center pie at (cx, middle of SVG)

const pieData = pie(data); // Run pie layout on data

const total = d3.sum(data, d => d.count); // Total games, used to calculate percentages

// STEP SIX: Draw each pie slice

g.selectAll("path") // For each slice (path element)
.data(pieData) // Bind pie-layout data
.enter()
.append("path") // Create SVG path for slice
.attr("d", arc) // Generate arc shape
.attr("fill", d => colorScale(d.data.company)) // Color based on company

// STEP SEVEN: Hover functionality for the mouseover!

.on("mouseover", (event, d) => { // Create and define the tooltip event
tooltip
.html(`
<strong>${d.data.company}</strong><br>
${d.data.tooltip.replace(/\n/g, "<br>")}`) // Set tooltip content: bold company name and multiline tooltip info

.style("left", (event.pageX + 10) + "px") // Position tooltip horizontally beside mouse
.style("top", (event.pageY + 10) + "px") // vertically beside
.style("opacity", 1); // Pop into view!
})

.on("mouseout", () => { // ... Back to transparent!
tooltip.style("opacity", 0);
});


// STEP EIGHT: Percent labels on the slices!

g.selectAll("text.label") // Select text elements with class "label"
.data(pieData) // Bind pie chart data (angle-based)
.enter() // enter() = For each data item, create a new text element
.append("text")
.attr("class", "label") // Add CSS class for any future styling

.attr("transform", d => `translate(${arc.centroid(d)})`)
// Use arc.centroid(d) to get [x, y] at the center of the slice, and move the label there

.attr("text-anchor", "middle") // Center-align the text horizontally
.attr("dy", "0.35em") // Vertical offset so label sits visually centered

.style("fill", "white") // White text for contrast on colored slices
.style("font-weight", "bold") // Make labels visually strong

.text(d => `${((d.data.count / total) * 100).toFixed(1)}%`); // Computers the percent, to a single decimal place, for slices

// STEP NINE: Pie labels!

g.append("text") // Append a new <text> element to the pie group
.attr("y", radius + 20) // Position below the pie chart (20px below radius)
.attr("text-anchor", "middle") // Centered horizontally with respect to pie
.attr("font-weight", "bold") // Bold for visibility
.text(label); // Set the label (We got this passed )

}

// STEP TEN: Draw two pie charts for the two eras
// This shifts the positioning, artistic choices
drawPie(processedData.byPeriod.before2010, width * 0.22, "1996–2009")
drawPie(processedData.byPeriod.after2009, width * 0.58, "2010–2023");


// STEP ELEVEN: Build the legend

const legend = svg.append("g")
.attr("transform", `translate(${width - margin.right}, ${margin.top})`);
// Create a group <g> element for the legend, positioned in top-right corner

const companies = ["Sony", "Nintendo", "Microsoft", "Sega", "PC"];
// Manually define the set of companies expected in the pie charts

companies.forEach((company, i) => {
const legendRow = legend.append("g")
.attr("transform", `translate(0, ${i * 20})`);
// Create a horizontal legend row for each company, stacking vertically (20px apart)

legendRow.append("rect")
.attr("width", 12)
.attr("height", 12)
.attr("fill", colorScale(company)); // Legend box, for the companies

legendRow.append("text")
.attr("x", -8) // Alighment for the square
.attr("y", 10)
.attr("text-anchor", "end") // Right-align the text
.text(company); // Display the company name
});

return svg.node();
}
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