Public
Edited
Mar 20, 2023
Fork of HW3 - D3
Insert cell
Insert cell
Insert cell
Insert cell
base_bg_data = FileAttachment("games_detailed_info_trimmed.csv").csv({typed: true})

Insert cell
Insert cell
Insert cell
viewof bg_data_basic = aq.from(base_bg_data).view(5)
Insert cell
Insert cell
{
// Most aspects will be shameless copy-pastes from the D3 intro notebooks from class.
// But there will at least be an effort to play with values
// to make more appropriate for this notebook's content.
// Setting these variables so everything can act more template-like
const totalWidth = width;
const totalHeight = 400;

// Set up some margins. These will be experimentally derived values, ha.
const margin = ({top: 20, bottom: 45, left: 120, right: 20});

// Determine the values for space taken by the chart body
// NEW: Add some buffer so tooltips don't get cut off
const visWidth = totalWidth - margin.left - margin.right - 200;
const visHeight = totalHeight - margin.top - margin.bottom;
// set up outer svg container
const svg = d3.create('svg')
.attr('width', totalWidth).attr('height', totalHeight);

// set up scales
const y = d3.scaleLinear()
.domain([0, d3.max(base_bg_data, d => d.average)]).nice()
.range([visHeight, 0]);
const x = d3.scaleLinear()
.domain([0, d3.max(base_bg_data, d => d.averageweight)]).nice()
.range([0, visWidth]);
// set up svg group for chart
const g = svg.append("g")
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// set up axes
const xAxis = d3.axisBottom(x)
const yAxis = d3.axisLeft(y);

// set up a radius for circle sizes
const radius = 2;

// set up y-axis and label
g.append("g")
.call(yAxis)
.append("text")
.attr("x", -40)
.attr("y", visHeight/2)
.attr("fill", "black")
.attr("dominant-baseline", "middle")
.text("Average Rating");

// set up x-axis and label
g.append('g')
.attr('transform', `translate(0, ${visHeight})`)
.call(xAxis)
.append('text')
.attr('fill', 'black')
.attr('x', visWidth / 2)
.attr('y', 40)
.text("Average Complexity");

// draw mark for each element in data
g.selectAll('circle')
.data(base_bg_data)
.join('circle')
.attr('cx', d => x(d.averageweight))
.attr('cy', d => y(d.average))
.attr('width', d => x(d.count_games))
.attr('r', radius)
.attr('fill-opacity', 0.2)
.attr('fill', 'rebeccapurple')
// add the tooltip triggers
.on('mouseenter', mouseEnter)
.on('mouseleave', mouseLeave);

// throw a title on there
g.append('text')
.attr('transform', `translate(${visWidth/2}, 0)`)
.style('text-anchor', 'middle')
.text('Complex Boardgames Tend to Have Better Reviews');

// NEW blatantly copy tooltips from https://observablehq.com/@nyuvis/interaction?collection=@nyuvis/guides-and-examples
// create tooltip
const tooltip = g.append('g')
.attr('visibility', 'hidden');
const tooltipHeight = 16;
// add a rectangle to the tooltip to serve as a background
const tooltipRect = tooltip.append('rect')
.attr('fill', 'white')
.attr('rx', 2)
.attr('height', tooltipHeight)
.style("stroke", "black")
.style("stroke-width", 1);
// add a text element to the tooltip to contain the label
const tooltipText = tooltip.append('text')
.attr('fill', 'rebeccapurple')
.attr('font-family', 'sans-serif')
.attr('font-size', 12)
.attr('y', 3) // offset it from the edge of the rectangle
.attr('x', 3) // offset it from the edge of the rectangle
.attr('dominant-baseline', 'hanging')
// handle hovering over a circle
function mouseEnter(event, d) {
// make the circle larger
d3.select(this)
.attr('r', radius * 2);
// update the label's text and get its width - wow this is handy
tooltipText.text(d.primary);
const labelWidth = tooltipText.node().getComputedTextLength();
// set the width of the tooltip's background rectangle
// to match the width of the label, plus some extra space
tooltipRect.attr('width', labelWidth + 6);
// move the tooltip to the position of the circle (offset by a bit)
// and make the tooltip visible
const xPos = x(d.averageweight) + radius * 3;
const yPos = y(d.average) - tooltipHeight / 2;

tooltip.attr('transform', `translate(${xPos},${yPos})`)
.attr('visibility', 'visible');
}
// handle leaving a circle
function mouseLeave(event, d) {
// reset the size of the circle
d3.select(this)
.attr('r', radius)
// make the tooltip invisible
tooltip
.attr('visibility', 'hidden');
}
return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
d_rollup = bg_data_basic
.groupby('minplayers')
.rollup({
count_games: d => op.count()
})
.orderby('minplayers')
.objects()
Insert cell
{
// Most aspects will be shameless copy-pastes from the D3 intro notebooks from class.
// But there will at least be an effort to play with values
// to make more appropriate for this notebook's content.
// Setting these variables so everything can act more template-like
const totalWidth = width;
const totalHeight = 800;

// Set up some margins. These will be experimentally derived values, ha.
const margin = ({top: 20, bottom: 45, left: 120, right: 20});

// Determine the values for space taken by the chart body
const visWidth = totalWidth - margin.left - margin.right;
const visHeight = totalHeight - margin.top - margin.bottom;
// set up outer svg container
const svg = d3.create('svg')
.attr('width', totalWidth).attr('height', totalHeight);

// set up scales
const y = d3.scaleBand()
.domain(d_rollup.map(d => d.minplayers))
.range([0, visHeight])
.padding(0.2);
const x = d3.scaleLinear()
.domain([0, d3.max(d_rollup, d => d.count_games)]).nice()
.range([0, visWidth]);
// set up svg group for chart
const g = svg.append("g")
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// set up axes
const xAxis = d3.axisBottom(x)//.tickFormat(format);
const yAxis = d3.axisLeft(y);

// set up y-axis and label (no label)
g.append("g")
.call(yAxis)
.append("text")
.attr("x", -40)
.attr("y", visHeight/2)
.attr("fill", "black")
.attr("dominant-baseline", "middle")
.text("Minimum Players");

// set up x-axis and label
g.append('g')
.attr('transform', `translate(0, ${visHeight})`)
.call(xAxis)
.append('text')
.attr('fill', 'black')
.attr('x', visWidth / 2)
.attr('y', 40)
.text("Number of Games");

// draw mark for each element in data
g.selectAll('rect')
.data(d_rollup)
.join('rect')
.attr('x', 0)
.attr('y', d => y(d.minplayers))
.attr('width', d => x(d.count_games))
.attr('height', y.bandwidth())
.attr('fill', 'rebeccapurple')
// add the tooltip triggers
.on('mouseover', mouseEnter)
.on("mousemove", mouseMove)
.on('mouseleave', mouseLeave);

// throw a title on there
g.append('text')
.attr('transform', `translate(${visWidth/2}, 0)`)
.style('text-anchor', 'middle')
.text('Boardgames Overwhelmingly Support As Few As 2 Players');

// NEW blatantly copy tooltips from https://observablehq.com/@nyuvis/interaction?collection=@nyuvis/guides-and-examples
// create tooltip
const tooltip = g.append('g')
.attr('visibility', 'hidden');
const tooltipHeight = 30;
// add a rectangle to the tooltip to serve as a background
const tooltipRect = tooltip.append('rect')
.attr('fill', 'white')
.attr('rx', 5)
.attr('y', -0.5*tooltipHeight)
.attr('x', 10)
.attr('height', tooltipHeight)
.style("stroke", "black")
.style("stroke-width", 2);
// add a text element to the tooltip to contain the label
const tooltipText = tooltip.append('text')
.attr('fill', 'rebeccapurple')
.attr('font-family', 'sans-serif')
.attr('font-size', 24)
.attr('y', -0.5*tooltipHeight + 4) // offset it from the edge of the rectangle
.attr('x', 13) // offset it from the edge of the rectangle
.attr('dominant-baseline', 'hanging')
// handle entering a shape
function mouseEnter(event, d) {
// update the label's text and get its width - wow this is handy
tooltipText.text(d.count_games);
const labelWidth = tooltipText.node().getComputedTextLength();
// set the width of the tooltip's background rectangle
// to match the width of the label, plus some extra space
tooltipRect.attr('width', labelWidth + 6);
// move the tooltip to the mouse position
// and make the tooltip visible
var coords = d3.pointer(event);
const xPos = coords[0];
const yPos = coords[1];
tooltip.attr('transform', `translate(${xPos},${yPos})`)
.attr('visibility', 'visible');
}

function mouseMove(event, d) {
// keep moving the tooltip with the mouse position
var coords = d3.pointer(event);
const xPos = coords[0];
const yPos = coords[1];
tooltip.attr('transform', `translate(${xPos},${yPos})`)
}
// handle leaving the shape
function mouseLeave(event, d) {
// make the tooltip invisible
tooltip
.attr('visibility', 'hidden');
}
return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
viewof mechanics_by_yr = bg_data_basic
.derive({Mechanic: d => op.split(d.boardgamemechanic, ', ')})
.unroll('Mechanic', {drop: 'boardgamemechanic'})
.filter(d => d.yearpublished >= 2000)
.groupby('yearpublished', 'Mechanic')
.rollup({
count_per_mechanic: d => op.count()
})
.orderby('yearpublished')
.view(3)
Insert cell
Insert cell
viewof mechanics_by_yr_filtered = mechanics_by_yr
.filter(d => d.Mechanic == 'Cooperative Game' || d.Mechanic == 'Solo / Solitaire Game' || d.Mechanic == 'Traitor Game')
.view(3)
Insert cell
Insert cell
linedata = mechanics_by_yr_filtered
.groupby('Mechanic')
.orderby('yearpublished')
.rollup({values: d => op.array_agg(op.row_object('yearpublished', 'count_per_mechanic'))})
.objects()
Insert cell
linedata[0]
Insert cell
Insert cell
domains = mechanics_by_yr_filtered.rollup({
x: d => [op.min(d.yearpublished), op.max(d.yearpublished)], // [min, max] domain array
y: d => [op.min(d.count_per_mechanic), op.max(d.count_per_mechanic)], // [min, max] domain array
groups: d => op.array_agg_distinct(d.Mechanic), // array of unique values
years: d => op.array_agg_distinct(d.yearpublished)
})
.objects()[0]
Insert cell
Insert cell
color = d3.scaleOrdinal()
.domain(domains.groups)
.range(["royalblue", "lightsteelblue", "tomato"]);
Insert cell
Insert cell
Insert cell
years = domains.years.sort();
Insert cell
Insert cell
Insert cell
Insert cell
{
// Most aspects will be shameless copy-pastes from the D3 intro notebooks from class.
// But there will at least be an effort to play with values
// to make more appropriate for this notebook's content.
const indexOf1 = linedata.map(e => e.Mechanic).indexOf(line1);
const indexOf2 = linedata.map(e => e.Mechanic).indexOf(line2);
// Setting these variables so everything can act more template-like
const totalWidth = width;
const totalHeight = 400;

// Set up some margins. These will be experimentally derived values, ha.
const margin = ({top: 30, bottom: 45, left: 162, right: 20});

// Determine the values for space taken by the chart body
const visWidth = totalWidth - margin.left - margin.right;
const visHeight = totalHeight - margin.top - margin.bottom;

// set up outer svg container
const svg = d3.create('svg')
.attr('width', totalWidth).attr('height', totalHeight)
// add the tooltip triggers to the entire container this time because hitting lines exactly is frustrating
.on('mouseover', mouseEnter)
.on("mousemove", mouseMove)
.on('mouseleave', mouseLeave);

// set up scales
const x = d3.scaleLinear()
.domain(domains.x)
.range([0, visWidth]);
const y = d3.scaleLinear()
.domain(domains.y).nice()
.range([visHeight, 0]);
// set up svg group for chart
const g = svg.append("g")
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// set up axes
const xAxis = d3.axisBottom(x).tickFormat(d3.format("d"))
const yAxis = d3.axisLeft(y);


// set up y-axis and label (no label)
g.append("g")
.call(yAxis)
.append("text")
.attr("x", -40)
.attr("y", visHeight/2)
.attr("fill", "black")
.attr("dominant-baseline", "middle")
.text("Number of Games");


// set up x-axis and label
g.append('g')
.attr('transform', `translate(0, ${visHeight})`)
.call(xAxis)
.append('text')
.attr('fill', 'black')
.attr('x', visWidth / 2)
.attr('y', 40)
.text("Game's Publishing Year");

// set up path generator
const line = d3.line()
.x(d => x(d.yearpublished))
.y(d => y(d.count_per_mechanic));

// draw mark for each element in data
g.append('path')
.attr('stroke-width', 3)
.attr('fill', 'none')
.attr('d', line(linedata[indexOf1].values))
.attr('stroke', color(line1));

g.append('path')
.attr('stroke-width', 3)
.attr('fill', 'none')
.attr('d', line(linedata[indexOf2].values))
.attr('stroke', color(line2));
// throw a title on there
svg.append('text')
.attr('transform', `translate(${totalWidth/2}, 15)`)
.style('text-anchor', 'middle')
.text('Use of Alternative Group Mechanics Has Grown');

// create tooltip
const tooltip = svg.append('g')
.attr('visibility', 'hidden');
const tooltipHeight = 16;
// add a rectangle to the tooltip to serve as a background
const tooltipRect = tooltip.append('rect')
.attr('fill', 'white')
.attr('rx', 2)
.attr('y', -0.5*tooltipHeight)
.attr('x', 10)
.attr('height', tooltipHeight)
.style("stroke", "black")
.style("stroke-width", 1);
// add a text element to the tooltip to contain the label
const tooltipText = tooltip.append('text')
.attr('fill', 'royalblue')
.attr('font-family', 'sans-serif')
.attr('font-size', 12)
.attr('y', -0.5*tooltipHeight + 4) // offset it from the edge of the rectangle
.attr('x', 13) // offset it from the edge of the rectangle
.attr('dominant-baseline', 'hanging')
// handle entering a shape
function mouseEnter(event, d) {
// move the tooltip to the mouse position
// and make the tooltip visible
var coords = d3.pointer(event);
const xPos = coords[0];
const yPos = coords[1];
tooltip.attr('transform', `translate(${xPos},${yPos})`)
.attr('visibility', 'visible');
}
function mouseMove(event, d) {
// Recall we have attached the tooltip and events to the svg, so calcs must account for margins
console.log('Inverted pointer location: ' + x.invert(d3.pointer(event)[0]-margin.left));
var loc = x.invert(d3.pointer(event)[0]-margin.left);

// Bisect to get the closest year index...
const i = d3.bisectCenter(years, x.invert(d3.pointer(event)[0]-margin.left));
console.log('Mouse move returns year ' + (domains.x[0]+i));

console.log(x(loc));

/////////////////////////////////////////////////////////////////////
// TODO: Reference the y-values of the active lines at that x index.
// Calculate tooltip position using the found values and x and y scales.
// Transform tooltip to location.
// Update text to values.
// Placeholder below until done.

tooltipText.text(domains.x[0]+i);
const labelWidth = tooltipText.node().getComputedTextLength();
tooltipRect.attr('width', labelWidth + 6);
var coords = d3.pointer(event);
const xPos = coords[0];
const yPos = coords[1];
tooltip.attr('transform', `translate(${xPos},${yPos})`)
.attr('visibility', 'visible');
}
// handle leaving the shape
function mouseLeave(event, d) {
// make the tooltip invisible
tooltip
.attr('visibility', 'hidden');
}
return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
d3 = require("d3@7")
Insert cell
import {Swatches} from "@d3/color-legend"
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