Public
Edited
Apr 27
1 star
Insert cell
Insert cell
//Concept 1. The standard bar chart. I am admittedly COMPLETELY new to Javascript, HTML, css, etc. I'm taking 162 and 163 this term, and self-teaching as I go. This is a simple visualization; we're going to see how the platforms rank among their top hits! Which is the best platform at the higher end? I'm going to be following the example horizontal bar chart, both to create a visualization, and to teach myself javascript as I go.
Insert cell
//STEP ONE, attach the data csv in the sidebar for our observbale.

data = {
const raw_data = d3.csvParse(await FileAttachment("data.csv").text());
//d3.csvParse() turns our text into an array of objects with one element per row
// File attachment().text() reads the entire file as a plain text string?

// STEP TWO, next we're going to use the map function.

// The map() method of Array instances creates a new array populated with the results of calling a provided function on every element in the calling array.
// .map() goes through each row and transforms it into a simpler object

// Example: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
// const array1 = [1, 4, 9, 16];
// // Pass a function to map
// const map1 = array1.map((x) => x * 2);
//
// console.log(map1);
// OUTPUT -> Array [2, 8, 18, 32]

const cleaned_data = raw_data.map(d => {
return {
Platform: d.Platform, // PS4/XBox/Dreamcast
Critic_Score: +d.Metascore // <-- Read Metascore column now. "+" forces a quick conversion from string to number.
};
});

// STEP THREE, remove rows that have invalid scores (originally thought of using the gamer score column but those had missing columns. I decided metascore was best, and this is a safety valve)

// We filter out any entries that don’t have valid numeric data
const filtered_data = cleaned_data.filter(d => !isNaN(d.Critic_Score));

// STEP 4: Return the cleaned and filtered dataset
return filtered_data;

// On output of this, pretty immediately we see a problem; the metascores are all astronomically high because this is a TOP game list, so it's, by nature, all relatively similar scores. We're going to have to do something about that in the future, but for now, let's follow the guide to get an understanding.
}
Insert cell
// It's time to start grouping the data.

averageCriticScoreByPlatform= {
// STEP ONE, group the data...

// d3.group() creates a Map, where each key is a platform name (PS4, XBox, Dreamcast, etc)
// and each value is an array of games released on that platform.
const grouped = d3.group(data, d => d.Platform);

// Now let's calculate the average

const result = Array.from(grouped, ([platform, values]) => {
return {
platform: platform, // Name of the platform
average: d3.mean(values, d => d.Critic_Score) // Average critic score for this platform

// Convert the grouped Map into an array of objects, one per platform
// Example object: { platform: "PS4", average: 90 }, if I'm understanding this right
};
});

// STEP TWO, sort the results. Let's do alphabetical
// (Later we'll try to learn how to apply the animation for rearrange by clusters of companies?)

result.sort((a, b) => d3.ascending(a.platform, b.platform));

// FOR MY OWN MEMORY!!! Observable assigns this to the variable `averageCriticScoreByPlatform`
return result;

// NICE! It worked! The objects look good so far, but we have yet to get to a visualization...
}

Insert cell
// Next is just formatting. The example does this individually, probably for ease of editing later
margin = ({ top: 40, right: 40, bottom: 40, left: 100 })
Insert cell
height = 500
Insert cell
width = 800
Insert cell
// Now let's create the visualization prototype from the example!
chart = {
// Use the preprocessed data from the last cell
const chartdata = averageCriticScoreByPlatform;

// STEP ONE: Create an SVG element to draw on
const svg = d3.create("svg") // The SVG element
.attr("viewBox", [0, 0, width, height]); // Set the visible area (origin = 0,0, h, w)
// 0,0 the origin

// STEP TWO: Create a horizontal (X) axis (horizontal bar graph) for scores
const x = d3.scaleLinear()
.domain([0, d3.max(chartdata, d => d.average)]) // Input: from 0 to the maximum average score
.range([margin.left, width - margin.right]); // Output: from left margin to right margin on the screen

// STEP THREE: Create a vertical (Y) axis (the keys)
const y = d3.scaleBand()
.domain(chartdata.map(d => d.platform)) // Domain = input: list of platform names
.rangeRound([margin.top, height - margin.bottom]) // Range = output: from top margin to bottom margin
.padding(0.1); // Just some tiny padding so they don't literally touch
// This might be incorrect but we can just comment out here

// STEP FOUR: Draw a bar for each platform
svg.selectAll("rect") // Target all <rect> elements (even if none exist yet)
.data(chartdata) // Join data to rectangles
.join("rect") // For each piece of data, create a new bar if needed
.attr("x", x(0)) // Start each bar at x = 0 (left side)
.attr("y", d => y(d.platform)) // Set vertical position based on the platform name
.attr("height", y.bandwidth()) // Make each bar as tall as the band allows
.attr("width", d => x(d.average) - x(0)) // Bar length depends on critic score
.attr("fill", "steelblue"); // Fill color of the bars

// NOW LET'S DRAW THE AXES!

// STEP FIVE: Function to draw X axis
const xAxis = g => g
.attr("transform", `translate(0,${height - margin.bottom})`) // Move to bottom of SVG
.call(d3.axisBottom(x)); // Create an x axis

// STEP SIX: Function to draw Y axis
const yAxis = g => g
.attr("transform", `translate(${margin.left},0)`) // Move axis to the left margin
.call(d3.axisLeft(y)); // Create an y axis (using scale both times)

//NOW TO ATTACH THE AXES TO THE VISUALIZATION!

// STEP SEVEN: Append the X axis to the SVG and add a label
svg.append("g") // Add a <g> group element to hold the X axis
.call(xAxis) // Call the xAxis function we just defined
.call(g => g.select(".tick:last-of-type text") // Select the last tick text
.clone() // Duplicate it
.attr("text-anchor", "middle") // Centers text
.attr("x", -(width - margin.left - margin.right) / 2) // Move to center of the chart
.attr("y", margin.bottom - 10) // Styling to move upward?
.attr("font-weight", "bold") // Make text bold
.text("Average Critic Score")); // Set label text for axis

// STEP EIGHT: Append the Y axis to the SVG and add a label
// Keeping in comments to explain even though they are identical, for my own review
svg.append("g") // Add a <g> group element to hold the Y axis
.call(yAxis) // Call the yAxis function we defined
.call(g => g.select(".tick:last-of-type text") // Select the last tick text
.clone()
.attr("transform", "rotate(-90)") // Rotate the text 90 degrees
.attr("text-anchor", "middle") // Center it vertically
.attr("x", -(15 - margin.top - margin.bottom) / 2) // Adjust
.attr("y", -80) // Move left
.attr("font-weight", "bold") // Make it bold
.text("Platform")); // Set label text

// STEP NINE: Return the SVG node for visualization display
return svg.node();
}

// SUCCESS! Visualization of our gamer data, in the model of the example of HW1! But the immediate problem we recognized while setting up the data csv is that these scores are all incredibly close...
Insert cell
// I remember that not starting at 0 presented issues; if we start at another (higher) number, the visualization will overemphasize differences due to scale, but... I think here, this is appropriate. This is a list of exceptionally successful games. In order to visualize the difference, I think we want to start the scale at 90, since the lowest score appears to be around 95. I'm going to see the results when I set the visualization to start at 90 and see if I can add functionality that we discussed in coding lab on Friday (again, this is all one learning experiment for me!)
Insert cell
// OKAY! This (below) is my first visualization I think! I think it would be very interesting to add the animation for shifting to rearrange the bars by company, buuuut I did not figure out how to do that on Friday. So for now, this is my first visualization!

// Yes, scale of scores is emphasized. Obviously, Dreamcast is only 3.3 average metascore higher than 3DS by actual numerical value, and the scale of the bar over emphasizes this difference, but this really lets us see which consoles stood above and beyond. I'm surprised to see that the Dreamcast was by far the most consistently high rated, and many other were middle of the pack. I never actually owned a 3DS, but it's also, interestingly, lower than Game Boy Advance.

// Anyway! This is visualization one! I will add more commentation to this as I see fit (in terms of insight description)
// I might actually add a button to switch BETWEEN the two views, later, so we can show correct scaling, but also see the outliers more clearly (without warping the user perception of the data), so in some sense, perhaps both of these visualizations are my first submission...(?)

// TODO? Add a button to switch between views? (Scaling of the X axis)

// (This was written after output worked)
Insert cell
visualizationOne_HorizontalBar_Zoom_PlusHover = {

// NEW code sections will be outlined. I'm removing old learning comments for clarity. Code will remain the same wherever possible
const chartdata = averageCriticScoreByPlatform;

const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);

const x = d3.scaleLinear()
.domain([90, d3.max(chartdata, d => d.average)])
.range([margin.left, width - margin.right]);

const y = d3.scaleBand()
.domain(chartdata.map(d => d.platform))
.rangeRound([margin.top, height - margin.bottom])
.padding(0.1);

// NEW
// Create a <div> element that floats over the chart and acts as a tooltip box.
// The hover display we created in the codelab on Friday
// This is an HTML element, not part of the SVG — so it's styled with CSS and positioned absolutely.
const tooltip = d3.select("body")
.append("div")
.style("position", "absolute") // Float it relative to the page
.style("background", "white") // White background for readability
.style("padding", "4px 8px") // Space inside the box
.style("border", "1px solid gray") // Thin border
.style("border-radius", "4px") // Rounded corners
.style("font-weight", "bold") // Bold font
.style("pointer-events", "none") // Allow mouse to pass through it
.style("opacity", 0); // Start hidden
// END NEW

svg.selectAll("rect")
.data(chartdata)
.join("rect")
.attr("x", d => x(90))
.attr("y", d => y(d.platform))
.attr("height", y.bandwidth())
.attr("width", d => x(d.average) - x(90))
.attr("fill", "steelblue")
// NEW
.on("mouseover", function(event, d) {
d3.select(this)
.attr("fill", "orange"); // Highlight this bar, this is where color changes?

tooltip
.html(`Score: ${d.average.toFixed(1)}`) // Set HTML inside the box
.style("left", (event.pageX + 10) + "px") // Position it near the mouse (X)
.style("top", (event.pageY - 20) + "px") // Slightly above the mouse (Y)
.style("opacity", 1); // Show it
})
.on("mousemove", function(event, d) {
// Update tooltip position as mouse moves
tooltip
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 20) + "px");
})
.on("mouseout", function(event, d) {
d3.select(this)
.attr("fill", "steelblue"); // Reset bar color when mouseover leaves

tooltip
.style("opacity", 0); // Hide tooltip
});
// END NEW
const xAxis = g => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x));

const yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y));

svg.append("g")
.call(xAxis)
.call(g => g.select(".tick:last-of-type text")
.clone()
.attr("text-anchor", "middle")
.attr("x", -(width - margin.left - margin.right) / 2)
.attr("y", margin.bottom - 10)
.attr("font-weight", "bold")
.text("Average Critic Score"));

svg.append("g")
.call(yAxis)
.call(g => g.select(".tick:last-of-type text")
.clone()
.attr("transform", "rotate(-90)")
.attr("text-anchor", "middle")
.attr("x", -(15 - margin.top - margin.bottom) / 2)
.attr("y", -80)
.attr("font-weight", "bold")
.text("Platform"));

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