Public
Edited
Mar 20
3 forks
Importers
19 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function make_brackets() {
let div = d3
.create('div')
.style('width', div_width + 'px')
.style('height', div_height + 'px')
.style('position', 'relative');

let svg = div
.append('svg')
.attr('width', div_width)
.attr('height', div_height)
.style('width', div_width + 'px')
.style('height', div_height + 'px')
.style('position', 'absolute');

// Draw connectors on the SVG background
svg
.selectAll('path.link')
.data(tourney_tree.links())
.join('path')
.attr('class', function(d) {
let label = 'link';
if (
d.target.data.WTeam &&
d.target.data.LTeam &&
d.source.data.WTeam &&
d.source.data.LTeam &&
(d.target.data.WTeam.TeamID == d.source.data.WTeam.TeamID ||
d.target.data.WTeam.TeamID == d.source.data.LTeam.TeamID)
) {
label = label + ' Team' + d.target.data.WTeam.TeamID;
}
return label;
})
.attr('stroke', 'black')
.attr('stroke-width', 1)
.attr('fill', 'none')
.attr('d', connector);

div
.selectAll('div.game')
.data(tourney_tree.descendants())
.enter()
.append(d => game_container(d));

// Highlight a team's path through the tournament
// on mouseenter.
div
.selectAll('.team')
.on('mouseenter', function() {
let class_label = d3
.select(this)
.attr('class')
.split(' ')[2];
div
.selectAll('.team.' + class_label)
.style('background-color', '#9999ee');
div.selectAll('.game.' + class_label).style('border', 'solid 3px black');
svg.selectAll('.link.' + class_label).attr('stroke-width', 3);
})
.on('mouseleave', function() {
let class_label = d3
.select(this)
.attr('class')
.split(' ')[2];
div.selectAll('.team.' + class_label).style('background-color', null);
div.selectAll('.game.' + class_label).style('border', 'solid 1px black');
svg.selectAll('.link.' + class_label).attr('stroke-width', 1);
});

return div.node();
}
Insert cell
Insert cell
function game_container(game) {
let top_team, bot_team;
if (game.data.WTeam) {
if (game.data.WTeam.pos == "top") {
top_team = game.data.WTeam;
} else if (game.data.WTeam.pos == "bot") {
bot_team = game.data.WTeam;
}
}
if (game.data.LTeam) {
if (game.data.LTeam.pos == "top") {
top_team = game.data.LTeam;
} else if (game.data.LTeam.pos == "bot") {
bot_team = game.data.LTeam;
}
}

let div = d3
.create("div")
.style("width", function () {
if (game.data.round == 6) {
return 1.4 * team_width + "px";
} else {
return team_width + "px";
}
})
.style("height", function () {
if (game.data.round == 6) {
return 1.4 * game_height + "px";
} else {
return game_height + "px";
}
})
.style("left", function () {
if (game.data.round == 6) {
return (game.x2 - 0.2 * team_width).toString() + "px";
} else {
return game.x2 + "px";
}
})
.style("top", function () {
if (game.data.round == 6) {
return (game.y2 - 0.2 * game_height).toString() + "px";
} else {
return game.y2 + "px";
}
})
.style("border", "solid 0.5px black")
.style("position", "absolute");

// Check for upsets to highlight
if (game.data.WTeam && game.data.LTeam && game.data.WTeam.win) {
if (game.data.WTeam.seed - game.data.LTeam.seed > 4) {
div.append(() => team_container(top_team, game.data.round, "upset"));
div.append(() => team_container(bot_team, game.data.round, "upset"));
} else if (game.data.round == 6) {
if (game.data.WTeam.pos == "top") {
div.append(() => team_container(top_team, game.data.round, "champion"));
div.append(() => team_container(bot_team, game.data.round, "bot_team"));
} else {
div.append(() => team_container(top_team, game.data.round, "top_team"));
div.append(() => team_container(bot_team, game.data.round, "champion"));
}
} else {
div.append(() => team_container(top_team, game.data.round, "top_team"));
div.append(() => team_container(bot_team, game.data.round, "bot_team"));
}
} else {
div.append(() => team_container(top_team, game.data.round, "top_team"));
div.append(() => team_container(bot_team, game.data.round, "bot_team"));
}

// cmd-click on an unplayed game to copy a result template to update the result file
div.on("click", function (evt) {
if (evt.metaKey) {
let result_text = `2024,,${game.data.WTeam.TeamID},WS,${game.data.LTeam.TeamID},LS,,
${game.data.WTeam.TeamName}, ${game.data.LTeam.TeamName}`;
pbcopy(result_text);
}
});

return div.node();
}
Insert cell
// Each game_container contains two team_containers
// They're displayed slightly differently depending on
// round and whether they're on the top or bottom (tb).
function team_container(team, round, tb) {
let div = d3
.create("div")
.attr("class", function (d) {
let label = "team " + tb + " ";
if (team && team.TeamID) {
label = label + "Team" + team.TeamID;
}
if (round == 6) {
label = label + " championship";
}
return label;
})
// .style("font", round < 6 ? "14px sans-serif" : "16px sans-serif");
// Round 1 games display the seed and might have a play-in to highlight.
if (round == 1) {
let seed_span = div.append("span").text(team.seed + " ");
if (team.playin) {
seed_span.style("color", "blue");
let title = "Play-in info <br />";
title = title + `${team.TeamName}: ${team.playin.score} <br /> `;
title =
title +
`${team.playin.playin_opponent_name}: ${team.playin.playin_opponent_score}`;
seed_span.attr("title", title);
tippy(seed_span.node());
}
}
// Add name and score
let team_display;
if (team && team.TeamNames && team.score == 0) {
team_display = team.TeamNames;
} else if (team) {
team_display = team.TeamName;
}
div.append("span").text(team_display);
if (team && team.score) {
div.append("span").attr("class", "score").text(team.score);
}

return div.node();
}
Insert cell
function connector(link) {
if (link.target.data.round >= 2) {
let x1 = link.source.x2;
let y1 = link.source.y2;
let x2 = link.target.x2;
let y2 = link.target.y2;
let xx1, yy1, xx2, yy2;

let path;

// We can tell whether we're linking up or down by
// comparing expected seeds.
let [seed1, seed2] = link.target.data.expected_seeds;
// Upper links
if (seed1 < seed2) {
// Left half of bracket
if (link.target.data.region == 'W' || link.target.data.region == 'Y') {
xx1 = x1 + team_width / 2;
yy1 = y1;
xx2 = x2 + team_width;
yy2 = y2 + game_height / 2;
path = [[xx1, yy1], [xx1, yy2], [xx2, yy2]];
return d3.line()(path);
}
// Right half of bracket
else if (
link.target.data.region == 'X' ||
link.target.data.region == 'Z'
) {
xx1 = x1 + team_width / 2;
yy1 = y1;
xx2 = x2;
yy2 = y2 + game_height / 2;
path = [[xx1, yy1], [xx1, yy2], [xx2, yy2]];
return d3.line()(path);
}
}
// Lower links
else {
// Left half of bracket
if (link.target.data.region == 'W' || link.target.data.region == 'Y') {
xx1 = x1 + team_width / 2;
yy1 = y1 + game_height;
xx2 = x2 + team_width;
yy2 = y2 + game_height / 2;
path = [[xx1, yy1], [xx1, yy2], [xx2, yy2]];
return d3.line()(path);
}
// Right half of bracket
else if (
link.target.data.region == 'X' ||
link.target.data.region == 'Z'
) {
xx1 = x1 + team_width / 2;
yy1 = y1 + game_height;
xx2 = x2;
yy2 = y2 + game_height / 2;
path = [[xx1, yy1], [xx1, yy2], [xx2, yy2]];
return d3.line()(path);
}
}
} else {
return null;
}
}
Insert cell
Insert cell
function horizontal_rescaler(d) {
let region = d.data.region;
let scaler;
let s = 3; // A small amount to shift round 2 games
// Upper right
if (region == 'X' || region == 'WX') {
if (d.data.round == 2) {
scaler = d3
.scaleLinear()
.domain([0, div_width])
.range([
0.5 * div_width - 1.5 * team_width - s,
div_width - team_width - s
]);
} else {
scaler = d3
.scaleLinear()
.domain([0, div_width])
.range([0.5 * div_width - 1.5 * team_width, div_width - team_width]);
}
}
// Upper left
else if (region == 'W') {
if (d.data.round == 2) {
scaler = d3
.scaleLinear()
.domain([0, div_width])
.range([0.5 * (div_width + team_width) + s, s]);
} else {
scaler = d3
.scaleLinear()
.domain([0, div_width])
.range([0.5 * (div_width + team_width), 0]);
}
}
// Lower left
else if (region == 'Y' || region == 'YZ') {
if (d.data.round == 2) {
scaler = d3
.scaleLinear()
.domain([0, div_width])
.range([0.5 * (div_width + team_width) + s, s]);
} else {
scaler = d3
.scaleLinear()
.domain([0, div_width])
.range([0.5 * (div_width + team_width), 0]);
}
}
// Lower right
else if (region == 'Z') {
if (d.data.round == 2) {
scaler = d3
.scaleLinear()
.domain([0, div_width])
.range([
0.5 * div_width - 1.5 * team_width - s,
div_width - team_width - s
]);
} else {
scaler = d3
.scaleLinear()
.domain([0, div_width])
.range([0.5 * div_width - 1.5 * team_width, div_width - team_width]);
}
} else if (region == 'CH')
scaler = d3
.scaleLinear()
.domain([0, div_width])
.range([0.5 * (div_width - team_width), 0]);

return scaler(d.y);
}
Insert cell
function vertical_rescaler(d) {
let region = d.data.region;

// Upper left
if (region == 'W') {
let scaler = d3
.scaleLinear()
.domain([-0.25 * div_height, 0.25 * div_height])
.range([div_height - game_height / 2, -game_height / 2]);
return scaler(d.x);
}
// Upper right
else if (region == 'X') {
let scaler = d3
.scaleLinear()
.domain([0, 0.5 * div_height])
.range([div_height - game_height / 2, -game_height / 2]);
return scaler(d.x);
}
// Lower left
else if (region == 'Y') {
let scaler = d3
.scaleLinear()
.domain([div_height * 0.5, div_height])
.range([div_height - game_height / 2, -game_height / 2]);
return scaler(d.x);
}
// Lower right
else if (region == 'Z') {
let scaler = d3
.scaleLinear()
.domain([div_height * 0.75, 1.25 * div_height])
.range([div_height - game_height / 2, -game_height / 2]);
return scaler(d.x);
} else if (region == 'WX') {
return 0.4 * div_height;
} else if (region == 'YZ') {
return 0.6 * div_height;
} else if (region == 'CH') {
return 0.5 * div_height;
}
}
Insert cell
Insert cell
tourney_tree.descendants()
Insert cell
// Apply d3.hierarchy and d3.tree
// We can define display_map below to order the seeds in
// round 1 however we want.
tourney_tree = {
let tree = d3.hierarchy(tourney);
tree.sort(function(x, y) {
return (
display_map.get(y.data.expected_seeds[0]) -
display_map.get(x.data.expected_seeds[0])
);
});
d3
.tree()
.separation((a, b) => (a.parent == b.parent ? 1 : 1.1) / a.depth)
.size([div_height, div_width])(tree);

tree.descendants().forEach(function(v) {
v.x2 = horizontal_rescaler(v);
v.y2 = vertical_rescaler(v);
});

return tree;
}
Insert cell
Insert cell
tourney = {
// Start with a basic nested JSON tree down
// to the regional finals.
let tourney = {
region: "CH",
round: 6,
expected_seeds: [1, 1],
children: [
{
region: "WX",
round: 5,
expected_seeds: [1, 1],
children: [
{
region: "W",
round: 4,
expected_seeds: [1, 2]
},
{
region: "X",
round: 4,
expected_seeds: [1, 2]
}
]
},
{
region: "YZ",
round: 5,
expected_seeds: [1, 1],
children: [
{
region: "Y",
round: 4,
expected_seeds: [1, 2]
},
{
region: "Z",
round: 4,
expected_seeds: [1, 2]
}
]
}
]
};

// Now we'll just use a basic tree-traversal technique
// to continue down to round 1
let stack = [
tourney.children[0].children[0],
tourney.children[0].children[1],
tourney.children[1].children[0],
tourney.children[1].children[1]
];
while (stack.length > 0) {
let node = stack.pop();
let high_seed = node.expected_seeds[0];
let new_low_seed = 2 ** (5 - node.round + 1) - high_seed + 1;
let child0 = {
region: node.region,
expected_seeds: [high_seed, new_low_seed],
round: node.round - 1
};
let low_seed = node.expected_seeds[1];
let lower_seed = 2 ** (5 - node.round + 1) - low_seed + 1;
let child1 = {
region: node.region,
expected_seeds: [lower_seed, low_seed],
round: node.round - 1
};
node.children = [child0, child1];

// If round > 2, then we'll just push both
// new empty nodes to the stack. We don't yet
// know whose playing in those games.
if (node.round > 2) {
stack.push(child0);
stack.push(child1);
}
// If round == 2, then the children will be in round 1
// so we can look up the game result.
else {
Object.assign(child0, round1_info(child0));
Object.assign(child1, round1_info(child1));
}
}
// At this point, the tree structure of the tournament is
// constructed but we need to re-traverse to get game results.
stack = [tourney];
while (stack.length > 0) {
let node = stack.pop();
if (node.visits == 1) {
node.visits = 2;
} else {
node.visits = 1;
}
let [child0, child1] = node.children;
let team0, team1;
if (child0.WTeam && child0.WTeam.win) {
team0 = Object.assign({}, child0.WTeam);
delete team0.win;
delete team0.score;
team0.pos = get_pos(team0.seed, node.round, team0.slot[0]);
node.WTeam = team0; // Arbitrary, for now
}
if (child1.WTeam && child1.WTeam.win) {
team1 = Object.assign({}, child1.WTeam);
delete team1.win;
delete team1.score;
team1.pos = get_pos(team1.seed, node.round, team1.slot[0]);
node.LTeam = team1; // Arbitrary, for now
}
// If we have both teams, then we attempt to assess the victor
if (team0 && team1) {
let scores = get_scores(team0.TeamID, team1.TeamID);
team0.score = scores[0];
team1.score = scores[1];
let wTeam, lTeam;
if (team0.score > team1.score) {
team0.win = true;
node.WTeam = team0;
node.LTeam = team1;
} else if (team0.score < team1.score) {
team1.win = true;
node.WTeam = team1;
node.LTeam = team0;
}
}
if (node.visits == 1) {
stack.push(node);
if (node.round > 2) {
stack.push(child0);
stack.push(child1);
}
}
}
return tourney;
}
Insert cell
// Here's how I'd like the seeds to be positioned in round 1
display_map = {
let display_order = [1, 16, 9, 8, 5, 12, 13, 4, 3, 14, 11, 6, 7, 10, 15, 2];
let display_map = new Map();
display_order.forEach((d, i) => display_map.set(d, i + 1));
return display_map;
}
Insert cell
// The info that we need for round 1
function round1_info(o) {
let team0 = get_team(o.region, o.expected_seeds[0]);
let team1 = get_team(o.region, o.expected_seeds[1]);
let scores = get_scores(team0.TeamID, team1.TeamID);
team0.score = scores[0];
team0.pos = get_pos(team0.seed, 1);
team1.score = scores[1];
team1.pos = get_pos(team1.seed, 1);

let wTeam, lTeam;
if (team0.score > team1.score) {
wTeam = team0;
wTeam.win = true;
lTeam = team1;
} else if (team0.score < team1.score) {
wTeam = team1;
wTeam.win = true;
lTeam = team0;
} else {
wTeam = team0;
wTeam.win = false;
lTeam = team1;
}
return { WTeam: wTeam, LTeam: lTeam };
}
Insert cell
// Get the place of a team in a game_container on either the top or bottom
// as a function of its seed and round.
function get_pos(seed, round, region) {
let s;
if (typeof seed == 'number') {
s = seed;
} else {
s = parseInt(seed.slice(1, 3));
}
if (round <= 4) {
let n;
if (s % 2 == 1) {
n = (s - 1) / 2;
} else {
n = 16 - s / 2;
}
let bits = n.toString(2).padStart(4, '0');
let bit = bits[round - 1];
if (bit == '0') {
return 'top';
} else {
return 'bot';
}
} else {
if ((region == 'W' || region == 'Y') && round == 5) {
return 'top';
} else if ((region == 'X' || region == 'Z') && round == 5) {
return 'bot';
} else if ((region == 'W' || region == 'X') && round == 6) {
return 'top';
} else if ((region == 'Z' || region == 'Y') && round == 6) {
return 'bot';
}
}
}
Insert cell
// Look up a team based on its region and seed
function get_team(region, expected_seed) {
let slot, teamID, teamName, teamNames;
let playin = false;
if (expected_seed < 10) {
slot = region + '0' + expected_seed.toString();
} else {
slot = region + expected_seed.toString();
}
let seed = this_years_seeds.map(d => d.Seed).indexOf(slot);
if (seed > -1) {
teamID = this_years_seeds[seed].TeamID;
teamName = team_map.get(teamID);
}
// The data expresses round 1 seeds mostly just as you'd expect.
// In the case of a play-in game, there's an 'a' or 'b' appended to
// the two seeds you'd expect to find. Thus, if we didn't find the
// seed above that means we've got a play-in game. We'll display
// just the winner but include the results of the playin via a tooltip.
else {
let seeda = this_years_seeds.map(d => d.Seed).indexOf(slot + 'a');
let seedb = this_years_seeds.map(d => d.Seed).indexOf(slot + 'b');
if (seeda > -1) {
let playin_opponentID, playin_opponent_name, playin_opponent_score, score;
let teamIDa = this_years_seeds[seeda].TeamID;
let teamNamea = team_map.get(teamIDa);
let teamIDb = this_years_seeds[seedb].TeamID;
let teamNameb = team_map.get(teamIDb);
let scores = get_scores(teamIDa, teamIDb);
if (scores[0] < scores[1]) {
teamName = teamNameb;
teamID = teamIDb;
score = scores[1];
playin_opponentID = teamIDa;
playin_opponent_name = teamNamea;
playin_opponent_score = scores[0];
} else if (scores[0] > scores[1]) {
teamName = teamNamea;
teamID = teamIDa;
score = scores[0];
playin_opponentID = teamIDb;
playin_opponent_name = teamNameb;
playin_opponent_score = scores[1];
} else {
teamName = teamNamea;
teamNames = teamNamea + '/' + teamNameb;
score = 0;
playin_opponent_name = teamNameb;
playin_opponent_score = 0;
playin_opponentID = teamIDb;
}
playin = {
score: score,
teamID: teamIDa,
playin_opponentID: playin_opponentID,
playin_opponent_name: playin_opponent_name,
playin_opponent_score: playin_opponent_score
};
}
}

return {
TeamName: teamName,
TeamNames: teamNames,
TeamID: teamID,
seed: expected_seed,
slot: slot,
playin: playin
};
}
Insert cell
// Look up the scores
function get_scores(teamID1, teamID2) {
let result = this_years_results.filter(function(o) {
return (
(o.WTeamID == teamID1 && o.LTeamID == teamID2) ||
(o.WTeamID == teamID2 && o.LTeamID == teamID1)
);
});
if (result.length == 1) {
result = result[0];
if (result.WTeamID == teamID1) {
return [result.WScore, result.LScore].map(d => parseInt(d));
} else if (result.LTeamID == teamID1) {
return [result.LScore, result.WScore].map(d => parseInt(d));
}
} else {
return [0, 0];
}
}
Insert cell
Insert cell
Insert cell
this_years_results = {
if (men_or_women == "Men") {
return mresults.filter(d => d.Season == year);
} else {
return wresults.filter(d => d.Season == year);
}
}
Insert cell
this_years_seeds = {
if (men_or_women == "Men") {
return mseeds.filter(d => d.Season == year);
} else {
return wseeds.filter(d => d.Season == year);
}
}
Insert cell
Insert cell
wseeds = d3.csvParse(await FileAttachment("WNCAATourneySeeds@5.csv").text())
Insert cell
Insert cell
team_map = {
if (men_or_women == "Men") {
return mteam_map;
} else {
return wteam_map;
}
}
Insert cell
wteam_map = {
let wteams = d3.csvParse(await FileAttachment("WTeams@2.csv").text());
let wteam_map = new Map();
wteams.forEach(function(team) {
wteam_map.set(team.TeamID, team.TeamName);
});
return wteam_map;
}
Insert cell
mteam_map = {
let mteams = d3.csvParse(await FileAttachment("MTeams@5.csv").text());
let mteam_map = new Map();
mteams.forEach(function(team) {
mteam_map.set(team.TeamID, team.TeamName);
});
return mteam_map;
}
Insert cell
mseeds = d3.csvParse(await FileAttachment("MNCAATourneySeeds@5.csv").text())
Insert cell
import { mresults, wresults } from "7eee9e12156dd090"
Insert cell
// From @mbostock/pbcopy
function pbcopy(text) {
const fake = document.body.appendChild(document.createElement("textarea"));
fake.style.position = "absolute";
fake.style.left = "-9999px";
fake.setAttribute("readonly", "");
fake.value = "" + text;
fake.select();
try {
return document.execCommand("copy");
} catch (err) {
return false;
} finally {
fake.parentNode.removeChild(fake);
}
}
Insert cell
Insert cell
// Current data imported from the notebook
import { ncaa_results_2023 } from "c7bca04a9bd56e41"
Insert cell
import { select, radio } from "@jashkenas/inputs"
Insert cell
tippy = require("https://unpkg.com/tippy.js@2.5.4/dist/tippy.all.min.js")
Insert cell
Insert cell
div_height = 0.85 * screen.availHeight
// div_height = 0.625 * div_width
Insert cell
game_height = div_height / 20
Insert cell
div_width = 0.99 * width
Insert cell
team_width = div_width / 9
Insert cell
styles = html`
<style>
text {
cursor: default
}
.team {
cursor: default;
width: ${team_width}px;
height: ${game_height / 2}px;
vertical-align: baseline;
white-space: nowrap;
overflow: hidden;
font: 14px sans-serif;
}
.team span {
line-height: ${game_height / 2}px;
}
.championship {
cursor: default;
width: ${1.4 * team_width}px;
height: ${(1.4 * game_height) / 2}px;
line-height: ${game_height / 2}px;
white-space: nowrap;
overflow: hidden;
font: 16px sans-serif;
}
.team span {
line-height: ${game_height / 2}px;
}
.score {
display: inline-block;
position: absolute;
right: 0px;
color: white;
background-color: #333333;
height: ${game_height / 2}px;
}
.championship > .score {
display: inline-block;
position: absolute;
right: 0px;
color: white;
background-color: #333333;
height: ${(1.4 * game_height) / 2}px;
}
.top_team {
background-color: #efefef
}
.bot_team {
background-color: #dedede
}
.upset {
background-color: #dd9999
}
.champion {
background-color: #ffd700
}

</style>`
Insert cell
function get_tourney_stats(men_or_women) {
let results, team_map;
if (men_or_women == "Men") {
results = mresults;
team_map = mteam_map;
} else {
results = wresults;
team_map = wteam_map;
}
let year_one = d3.min(results, (o) => o.Season);
let champions = d3
.rollups(
results,
function (this_years_results) {
let max_day = d3.max(this_years_results, (o) => o.DayNum);
let championship_game = this_years_results.find(
(o) => o.DayNum == max_day
);
let champion = get_winner(championship_game);
return team_map.get(get_winner(championship_game));
},
(o) => o.Season
)
.map((a) => a[1]);
champions = d3
.groups(champions, (s) => s)
.map((a) => ({ team: a[0], n: a[1].length }));
champions = d3
.sort(
d3.rollups(
champions,
(a) => a,
(o) => o.n
),
(a) => -a[0]
)
.map(([n, T]) => ({ n, teams: d3.sort(T.map((o) => o.team)) }));
champions.year_one = year_one;
return champions;
}
Insert cell
function get_winner(game) {
if (parseInt(game.WScore) > parseInt(game.LScore)) {
return game.WTeamID;
} else {
return game.LTeamID;
}
}
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