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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more