Public
Edited
Oct 24, 2022
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const margins = { l: 30, r: 100, b: 20, t: 10 };
const drawWidth = width;
const drawHeight = 500;
const svg = d3
.create("svg")
.attr("width", drawWidth)
.attr("height", drawHeight)
.attr("viewBox", `0 0 ${drawWidth} ${drawHeight}`);

const g = svg.append("g");
if (zoomable) {
svg.call(
d3
.zoom()
.extent([
[0, 0],
[drawWidth, drawHeight]
])
.scaleExtent([1, 4])
.on("zoom", zoomed)
);
}
function zoomed({ transform }) {
timeline.selectAll("g").attr("transform", transform);
}
const timeline = g.append("g").attr("id", "timeline");
const legend = g.append("g");

legend
.append("text")
.attr("id", "legend")
.attr("x", margins["l"])
.attr("y", 40)
.attr("font-size", 45)
.text("");

legend
.append("text")
.attr("class", "legend-adv")
.attr("x", margins["l"])
.attr("y", 70)
.attr("font-size", 20)
.attr("fill", "#333")
.text("");

legend
.append("text")
.attr("class", "legend-detail")
.attr("x", margins["l"])
.attr("y", 100)
.attr("font-size", 20)
.attr("fill", "#333")
.text("");

const scaleCol = d3.scaleBand().domain([0, 1]).range([0, 100]);
const scaleRow = d3
.scaleBand()
.domain(d3.range(10))
.range([margins["t"], drawHeight - margins["b"]]);

const clubs = g.append("g").attr("id", "clubs");
clubs
.selectAll("rect")
.data(sorted_teams)
.join("rect")
.attr("x", (d, i) => drawWidth - margins["r"] + scaleCol(i % 2) + 20)
.attr("y", (d, i) => margins["t"] + scaleRow(parseInt(i / 2)))
.attr("width", scaleCol.bandwidth() / 2)
.attr("height", scaleRow.bandwidth() / 2)
.attr("fill", (d) => sc(d))
.attr("opacity", 1)
.attr("stroke", "#333")
.on("mouseover", function (event, d) {
d3.selectAll("circle").attr("opacity", "0.2");
d3.selectAll(`.${d.split(" ").join("")}`)
.transition()
.duration(400)
.attr("opacity", 1)
.attr("r", emphasisR);
d3.select("#legend").attr("fill", sc(d)).text(d);
d3.select(".legend-adv").text("");
d3.select(".legend-detail").text("");
})
.on("mouseleave", function (event, d) {
d3.selectAll("circle").attr("opacity", "1");
d3.selectAll(`.${d.split(" ").join("")}`)
.transition()
.duration(400)
.attr("opacity", 1)
.attr("r", defaultR);
});

clubs
.selectAll("text")
.data(sorted_teams)
.join("text")
.attr("x", (d, i) => drawWidth - margins["r"] + scaleCol(i % 2) + 20)
.attr("y", (d, i) => margins["t"] + scaleRow(parseInt(i / 2)))
.text((d) => d[0]);

const st = d3
.scaleBand()
.domain(d3.range(38))
.range([margins["l"], drawWidth - margins["r"]]);

const taxis = d3.axisBottom(st).tickFormat((d) => d + 1);
g.append("g")
.attr("transform", `translate(0, ${drawHeight - margins["b"]})`)
.call(taxis);

const sy = d3
.scaleLinear()
.domain([0, 95])
.range([drawHeight - margins["b"], margins["t"]])
.nice();

const yaxis = d3.axisRight(sy).ticks(10);
g.append("g").attr("transform", `translate(0, ${-margins["b"]})`).call(yaxis);

const defaultR = 4;
const emphasisR = 8;

for (const t of teams) {
timeline
.append("g")
.selectAll("circle")
.data(getSorted(t))
.join(
(enter) => {
enter
.append("circle")
.attr("class", t.split(" ").join(""))
.attr("cx", (d, i) => st(i) + st.bandwidth() / 2)
.attr("cy", (d, i) => {
return sy(getLatestPoints(d.team, i)) - margins["b"];
})
.attr("ith", (d, i) => i)
.attr("stroke", "#444")
.attr("fill", (d) => sc(d.team))
.attr("r", defaultR)
.on("mouseover", function (event, d) {
const ith = d3.select(this).attr("ith");
d3.select("#timeline").selectAll("circle").attr("opacity", 0.2);
d3.selectAll(`.${d.team.split(" ").join("")}`).attr("opacity", 1);
d3.select("#legend").call(drawLegend, d);
d3.select(".legend-adv").call(drawLegendAdv, d, ith);
d3.select(".legend-detail").call(drawLegendDetail, d, ith);

d3.select(this).transition().duration(500).attr("r", emphasisR);
})
.on("mouseleave", function (event, d) {
d3.select(this).transition().duration(500).attr("r", defaultR);
});
},
(update) => update,
(exit) => exit.remove()
);
}
return svg.node();
}
Insert cell
Insert cell
function getSorted(team) {
return d3.sort(
point_history.filter((d) => d.team === team),
(d) => d.date
);
}
Insert cell
function drawLegend(sel, d) {
sel.attr("fill", sc(d.team)).attr("stroke", "#444").text(`${d.team}`);
}
Insert cell
function drawLegendAdv(sel, d, ith) {
const detail = data.filter((det) => det.MatchNumber == d.matchNumber)[0];

const vs =
detail["HomeTeam"] === d.team ? detail["AwayTeam"] : detail["HomeTeam"];

d3.selectAll(`.${vs.split(" ").join("")}`).attr("opacity", 1);

const oppSumPoint = getLatestPoints(vs, ith);
const oppPoint = d.point === 3 ? 0 : d.point === 1 ? 1 : 3;
const ownSumPoint = getLatestPoints(d.team, ith);
sel.html(
`**** (${ownSumPoint - d.point}→${ownSumPoint}) vs ${vs} (${
oppSumPoint - oppPoint
}→${oppSumPoint})`
);
}
Insert cell
function drawLegendDetail(sel, d) {
const detail = data.filter(
(det) =>
det.RoundNumber == d.roundNumber &&
(det.HomeTeam == d.team || det.AwayTeam == d.team)
)[0];
const date = timeFormat(timeParse(detail.DateUtc.slice(0, -1)));
sel.html(`${date} (@ ${detail.Location}) [Match ${detail.RoundNumber}]`);
}
Insert cell
function getLatestPoints(team, matchIndex) {
const sorted = d3.sort(
point_history.filter((d) => d.team == team),
(d) => d.date
);
return d3.cumsum(
sorted.filter((d, i) => i <= matchIndex),
(d) => d.point
)[matchIndex];
}
Insert cell
Insert cell
timeFormat = d3.timeFormat("%Y-%m-%d")
Insert cell
timeParse = d3.utcParse("%Y-%m-%d %H:%M:%S")
Insert cell
Insert cell
epl2021 = FileAttachment("epl-2021.json").json()
Insert cell
data = epl2021.map((d) => {
const home_score = d.HomeTeamScore;
const away_score = d.AwayTeamScore;
const home_point =
home_score > away_score ? 3 : home_score == away_score ? 1 : 0;
const away_point =
away_score > home_score ? 3 : home_score == away_score ? 1 : 0;
return Object.assign(d, { home_point: home_point, away_point: away_point });
})
Insert cell
sorted_teams = d3
.sort(Array.from(teams), (d) => getLatestPoints(d, 37))
.reverse()
Insert cell
Insert cell
Insert cell
sc = d3.scaleOrdinal().domain(teams).range(colors)
Insert cell
point_history = d3.merge(
data.map((d) => {
return [
{
date: timeParse(d.DateUtc.slice(0, -1)),
matchNumber: d.MatchNumber,
roundNumber: d.RoundNumber,
team: d.HomeTeam,
point: d.home_point
},
{
date: timeParse(d.DateUtc.slice(0, -1)),
matchNumber: d.MatchNumber,
roundNumber: d.RoundNumber,
team: d.AwayTeam,
point: d.away_point
}
];
})
)
Insert cell
d3.sort(
data.filter((d) => {
const t = "Burnley";
return d.HomeTeam === t || d.AwayTeam === t;
}),
(d) => d.MatchNumber
)
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