Public
Edited
Sep 5, 2023
Insert cell
Insert cell
cfb_data = FileAttachment("cfb_data.json").json()
Insert cell
viewof season_val = Inputs.select(cfb_data.map(d => d.Season.toString()), {label: "Season", sort: true, unique: true})
Insert cell
viewof neighborhood_value = Inputs.select([1,2,3], {'label': 'Neighborhood Value'})
Insert cell
viewof week_num = Inputs.select((cfb_data.filter(d => d.Season == season_val)).map(d => d.Week), {label: "Week", sort: true, unique:true})
Insert cell
function getAvailableTeams() {

var seasonData = cfb_data.filter(
d => (d.Season == season_val & d.Week <= week_num)
)
// Get all the home teams and away teams
var home_teams = Array.from(new Set(seasonData.map(d => d['Home Team'] )))
var away_teams = Array.from(new Set(seasonData.map(d => d['Away Team'] )))

// All teams is the union between the home teams and away teams
var all_teams = home_teams
away_teams.forEach(team => {
if (all_teams.includes(team) == false) {all_teams.push(team)}
})

// Sort the teams so they can be in alphabetical order
return all_teams.sort()

return all_teams
}
Insert cell
getAvailableTeams()
Insert cell
viewof team_val = Inputs.select(getAvailableTeams())
Insert cell
function getGraphData(team, season, week) {
// Get the data for the specified team
var team_data = cfb_data.filter(
d=> (d.Season == season) & (d.Week <= week) &
(d['Home Team'] == team || d['Away Team'] == team)
)

// Loop through each game and give it some more information
team_data.forEach(d => {
d.key = d["Winning Team"] + "-" + d["Losing Team"] + "-" + d["Week"]
, d.source = d["Losing Team"]
, d.target = d["Winning Team"]
})

return team_data
}
Insert cell
getGraphData("Michigan State", 2021, 8)
Insert cell
neighborhood_value
Insert cell
function getNetworkData() {
// This will get all of the source and target nodes for a given network of teams who have played against each other

var graphData = getGraphData(team_val, season_val, week_num)

var graphedTeams = [team_val]

// Get the two types of teams for each data record
var teamTypes = ['Home Team','Away Team']

for (let i = 1; i <= neighborhood_value; i++) {
// Go through each home and away team for all the games in the graphData
graphData.forEach( d => {

// Go through both Home Team and Away Team
teamTypes.forEach(tt => {

// If the team in question is not already included in the graphedTeams array, gather its info
if (graphedTeams.includes(d[tt]) == false) {
// Graph team information
let g = getGraphData(d[tt], season_val, week_num)
// Add data points to all the graphed information
graphData = graphData.concat(g)
// Add team to graphedTeams array to make sure there is no double counting
graphedTeams.push(d[tt])
}
})

})
}

// Next step is to go through all of the games and make sure both home and away teams are in the graphedTeams array
// This will prevent a bunch of one off nodes that are meaningless

var filteredGraphData = []
var filteredGameKeys = []

graphData.forEach(d => {
// If both teams are in the network, push game to filtered data
if (graphedTeams.includes(d['Home Team']) & graphedTeams.includes(d['Away Team'])) {

// Only want 1 instance of each game to prevent duplication
if (filteredGameKeys.includes(d.key) == false) {
filteredGraphData.push(d)
filteredGameKeys.push(d.key)
}
}
})

// Want to create all of the necessary information to be passed back for the force graph

// Node Information
var unique_nodes = []
var nodes = []
var edges = []

filteredGraphData.forEach(d => {
teamTypes.forEach( tt => {
if (unique_nodes.includes(d[tt]) == false) {
unique_nodes.push(d[tt])
nodes.push({id: d[tt]})
}
})
edges.push({source: d.source, target: d.target })
})
return {nodes: nodes, links: edges}

}
Insert cell
getNetworkData()

Insert cell
height = 400
Insert cell
width = 800;
Insert cell
svg = d3.select(DOM.svg(width, height))
.attr('width', width)
.attr('height', height);
Insert cell
simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(d => d.id))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
Insert cell
mychart = {

const data = getNetworkData()
console.log(data)
// Handling links
const links = svg.selectAll("line")
.data(data.links, d => `${d.source.id}-${d.target.id}`);

links.exit().remove();

const newLinks = links.enter().append("line")
.attr("stroke", "#000")
.attr("stroke-width", 1)
.attr('marker-end','url(#arrowhead)'); // adding an arrow to the end of the link

// Adding arrow definition
svg.append('defs').append('marker')
.attr('id','arrowhead')
.attr('viewBox','-0 -5 10 10')
.attr('refX',13)
.attr('refY',0)
.attr('orient','auto')
.attr('markerWidth',13)
.attr('markerHeight',13)
.attr('xoverflow','visible')
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#999')
.style('stroke','none');

// Handling nodes
const nodes = svg.selectAll("circle")
.data(data.nodes, d => d.id);

nodes.exit().remove();

const newNodes = nodes.enter().append("circle")
.attr("r", 5)
.attr("fill", "#69b3a2")
.call(d3.drag().on("start", d3.dragstarted).on("drag", d3.dragged).on("end",d3.dragended));

// Combine new nodes and existing nodes
nodes.merge(newNodes);

// Combine new links and existing links
links.merge(newLinks);

// Update and restart the simulation.
simulation.nodes(data.nodes);
simulation.force("link").links(data.links);
simulation.alpha(1).restart();

function ticked() {
links
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);

nodes
.attr("cx", d => d.x)
.attr("cy", d => d.y);
}

simulation.on("tick", ticked);

function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}

function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}

return svg.link()
}

Insert cell
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
Insert cell
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
Insert cell
async function fetchData() {
const data = await getNetworkData(); // Assume this function fetches your data
updateNetwork(data);
}
Insert cell
fetchData()
Insert cell
function drawChart() {

const data = getNetworkData()
const links = data.links.map(d => Object.create(d))
const nodes = data.nodes.map(d => Object.create(d))
const radius = 5;
var tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id))
.force("charge", d3.forceManyBody().strength(-30))
.force("center", d3.forceCenter(width/2, height/2));
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
const link = svg.append("g")
.attr("stroke", "#aaa")
.attr("stroke-opacity", 0.3)
.selectAll("line")
.data(links)
.join("line")
.attr("stroke-width", d => Math.sqrt(d.value) / 2);
const node = svg.append("g")
.attr("stroke", "#000")
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", radius)
//.attr("fill", color)
.call(d3.drag(simulation))
.on('mouseover.fade', fade(0.1))
.on('mouseout.fade', fade(1));
const textElems = svg.append('g')
.selectAll('text')
.data(nodes)
.join('text')
.text(d => d.id)
.attr('font-size',12)
.attr('font-size',12)
.call(d3.drag(simulation))
.on('mouseover.fade', fade(0.1))
.on('mouseout.fade', fade(1));
simulation.on("tick", () => {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("cx", function(d) { return d.x = Math.max((radius+1), Math.min(width - (radius+1), d.x)); })
.attr("cy", function(d) { return d.y = Math.max((radius+1), Math.min(height - (radius+1), d.y)); });
textElems
.attr("x", d => d.x + 10)
.attr("y", d => d.y)
.attr("visibility", "hidden");
});
function fade(opacity) {
return d => {
node.style('opacity', function (o) { return isConnected(d, o) ? 1 : opacity });
textElems.style('visibility', function (o) { return isConnected(d, o) ? "visible" : "hidden" });
link.style('stroke-opacity', o => (o.source === d || o.target === d ? 1 : opacity));
if(opacity === 1){
node.style('opacity', 1)
textElems.style('visibility', 'hidden')
link.style('stroke-opacity', 0.3)
}
};
}
const linkedByIndex = {};
links.forEach(d => {
linkedByIndex[`${d.source.index},${d.target.index}`] = 1;
});

function isConnected(a, b) {
return linkedByIndex[`${a.index},${b.index}`] || linkedByIndex[`${b.index},${a.index}`] || a.index === b.index;
}
//invalidation.then(() => simulation.stop());
return svg.node()
}
Insert cell
drawChart()
Insert cell
drag = simulation => {
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
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