Public
Edited
Mar 14, 2023
1 fork
2 stars
Insert cell
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', '#dd2');
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', 'rgba(204,204,204,0)');
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) {
//setup.game_cnt = setup.game_cnt + 1;
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 shade = "#ddd";
if (
game.data.WTeam &&
game.data.LTeam &&
game.data.WTeam.win & (game.data.WTeam.score > 1)
) {
let pred = get_predicted_probability(
game.data.WTeam.TeamID,
game.data.LTeam.TeamID
);
// setup.log_loss = setup.log_loss - Math.log(pred);
if (pred >= 0.5) {
shade = d3.interpolateGreens(1.5 * (pred - 0.5));
} else if (pred < 0.5) {
shade = d3.interpolateReds(1.5 * (0.5 - pred));
}
}

let div = d3
.create("div")
.attr("class", "game-container")
.style("background-color", shade)
.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");

div.append(() => team_container(top_team, game.data.round, "top_team"));
div.append(() => team_container(bot_team, game.data.round, "bot_team"));

if (
game.data.WTeam &&
game.data.WTeam.win &&
game.data.LTeam &&
game.data.WTeam.score > 1
) {
let pred = get_predicted_probability(
game.data.WTeam.TeamID,
game.data.LTeam.TeamID
);
let game_log_loss = -Math.log(pred);

tippy(div.node(), {
content: `Prediction was that ${game.data.WTeam.TeamName} would beat ${
game.data.LTeam.TeamName
} with probabiltiy ${format(pred)} for a Log Loss of ${format(
game_log_loss
)}.`
});
} else if (
game.data.WTeam &&
game.data.WTeam.win &&
game.data.LTeam &&
game.data.WTeam.score == 1
) {
tippy(div.node(), { content: "Game cancelled" });
} else if (
game.data.WTeam &&
game.data.WTeam.TeamID &&
game.data.LTeam &&
game.data.LTeam.TeamID &&
!game.data.WTeam.win
) {
let pred = get_predicted_probability(
game.data.WTeam.TeamID,
game.data.LTeam.TeamID
);
let game_log_loss = -Math.log(pred);

tippy(div.node(), {
content: `Prediction is that ${game.data.WTeam.TeamName} will beat ${
game.data.LTeam.TeamName
} with probabiltiy ${format(pred)}.`
});
}

// 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 = `2021,,${game.data.WTeam.TeamID},WS,${game.data.LTeam.TeamID},LS,,
${game.data.WTeam.TeamName}, ${game.data.LTeam.TeamName}`;
pbcopy(result_text);
}
});

// if (
// game.data.WTeam &&
// game.data.WTeam.TeamID == 1116 &&
// game.data.LTeam &&
// game.data.LTeam.TeamID == 1403
// ) {
// console.log(['problem is ', game.data]);
// }

return div.node();
}
Insert cell
format = d3.format('0.5f')
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')
.style('background', 'rgba(204, 204, 204, 0)')
.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;
});
// 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());
tippy(seed_span.node(), { content: title, allowHTML: true });
}
}
// 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
// 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
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function log_loss(game) {
let pred = get_predicted_probability(game.WTeamID, game.LTeamID);
return -Math.log(cut(pred));
}
Insert cell
function cut(x, eps = 1e-15) {
let x2 = x;
if (x < eps) {
x2 = eps;
} else if (x > 1 - eps) {
x2 = 1 - eps;
}
return x2;
}
Insert cell
function set_setup(csv_file) {
let p = csv_file.search('\n');
let header = csv_file.slice(0, p).toLowerCase();
csv_file = header + csv_file.slice(p);
let results = d3.csvParse(csv_file);
let year = parseInt(results[0].id.slice(0, 4));
let sex = results[0].id.slice(10, 11);
if (sex == '1') {
sex = 'Men';
} else {
sex = 'Women';
}
mutable setup = {
men_or_women: sex,
year: year,
results: results,
log_loss: 0
};
}
Insert cell
mutable setup = ({})
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
Insert cell
this_years_results = {
let results;
if (setup.men_or_women == "Men") {
results = mresults.filter((d) => d.Season == setup.year);
} else {
results = wresults.filter((d) => d.Season == setup.year);
}
results.map(function (o) {
o.playin_flag = check_playin(o);
});
return results;
}
Insert cell
this_years_seeds = {
if (setup.men_or_women == "Men") {
return mseeds.filter((d) => d.Season == setup.year);
} else {
return wseeds.filter((d) => d.Season == setup.year);
}
}
Insert cell
function check_playin(result) {
let wteam_id = result.WTeamID;
if (this_years_seeds.filter((o) => o.TeamID == wteam_id).length == 0) {
return false;
}
let wteam_seed = this_years_seeds.filter((o) => o.TeamID == wteam_id)[0].Seed;
if (wteam_seed.length == 4) {
let lteam_id = result.LTeamID;
if (this_years_seeds.filter((o) => o.TeamID == lteam_id).length == 0) {
return false;
}
let lteam_seed = this_years_seeds.filter((o) => o.TeamID == lteam_id)[0]
.Seed;
if (lteam_seed.length == 4) {
return true;
} else {
return false;
}
} else {
return false;
}
}
Insert cell
Insert cell
wseeds = d3.csvParse(await FileAttachment("WNCAATourneySeeds@2.csv").text())
Insert cell
wresults = d3.csvParse(
await FileAttachment("WNCAATourneyCompactResults@30.csv").text()
)
Insert cell
Insert cell
team_map = {
if (setup.men_or_women == "Men") {
return mteam_map;
} else {
return wteam_map;
}
}
Insert cell
wteam_map = {
let wteams = d3.csvParse(await FileAttachment("WTeams.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.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.csv").text())
Insert cell
mresults = {
if (radio_project == "False") {
return d3.csvParse(
await FileAttachment("MNCAATourneyCompactResults@70.csv").text()
);
} else {
return setup.results.map((o) => ({
Season: setup.year,
DayNum: "150",
WTeamID: o.pred > 0.5 ? o.id.slice(5, 9) : o.id.slice(10, 14),
LTeamID: o.pred > 0.5 ? o.id.slice(10, 14) : o.id.slice(5, 9),
WScore: "51",
LScore: "49",
WLoc: "?",
NumOT: "?"
}));
}
}
Insert cell
Insert cell
// d3 = require("d3@7")
Insert cell
import { select, radio } from "@jashkenas/inputs"
Insert cell
tippy = require("tippy.js@6")
Insert cell
style = html`<link rel="stylesheet" href="${await require.resolve(
`tippy.js/themes/light.css`
)}">`
Insert cell
Insert cell
div_height = 0.85 * screen.availHeight
// div_height = window.innerHeight
Insert cell
game_height = div_height / 20
Insert cell
div_width = 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: middle;
line-height: ${game_height / 2}px;
white-space: nowrap;
overflow: hidden;
font: 14px sans-serif;
vertical-align: middle
}
.championship {
cursor: default;
width: ${1.4 * team_width}px;
height: ${(1.4 * game_height) / 2}px;
vertical-align: middle;
line-height: ${game_height / 2}px;
white-space: nowrap;
overflow: hidden;
font: 18px sans-serif;
vertical-align: middle
}
.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

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