Public
Edited
Mar 20, 2021
3 stars
Insert cell
Insert cell
Insert cell
chart = {
const svg = d3.create("svg")
.attr("viewBox", [-width / 2, -height / 2, width, height]);

const chords = chord(matrix);

const textId = DOM.uid("text");

svg.append("path")
.attr("id", textId.id)
.attr("fill", "none")
.attr("d", d3.arc()({outerRadius, startAngle: 0, endAngle: 2 * Math.PI}));
var grads = svg.append("defs")
.selectAll("linearGradient")
.data(chords)
.enter()
.append("linearGradient")
.attr("id", getGradID)
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", function(d, i){ return innerRadius * Math.cos((d.source.endAngle-d.source.startAngle) / 2 + d.source.startAngle - Math.PI/2); })
.attr("y1", function(d, i){ return innerRadius * Math.sin((d.source.endAngle-d.source.startAngle) / 2 + d.source.startAngle - Math.PI/2); })
.attr("x2", function(d,i){ return innerRadius * Math.cos((d.target.endAngle-d.target.startAngle) / 2 + d.target.startAngle - Math.PI/2); })
.attr("y2", function(d,i){ return innerRadius * Math.sin((d.target.endAngle-d.target.startAngle) / 2 + d.target.startAngle - Math.PI/2); })
if (focusedTeamIndex >= 0) {
grads.append("stop")
.attr("offset", "0%")
.attr("stop-color", function(d){ return nodeIsFocusedTeam(d) ? nonFocusedTeamColor(d) : '#00000010'})
}
else {
grads.append("stop")
.attr("offset", "0%")
.attr("stop-color", function(d){ return color(d.source.index)})

grads.append("stop")
.attr("offset", "100%")
.attr("stop-color", function(d){ return color(d.target.index)})
}

svg.append("g")
.attr("fill-opacity", 0.75)
.selectAll("g")
.data(chords)
.join("path")
.attr("d", ribbon)
.attr("fill", d => `url(#${getGradID(d)}`)
.style("mix-blend-mode", "hue")//"multiply")
.append("title")
.text(d => `Trades between ${teamCodes[d.source.index].code} and ${teamCodes[d.target.index].code}`)

svg.append("g")
.attr("font-family", "Arial Narrow, sans-serif")
.attr("font-weight", "900")
.attr("font-size", 15)
.selectAll("g")
.data(chords.groups)
.join("g")
.call(g => g.append("path")
.attr("d", arc)
.attr("fill", d => color(d.index))
.attr("stroke", "#fff"))
.call(g => g.append("text")
.attr("dy", -3)
.append("textPath")
.attr("xlink:href", textId.href)
.attr("startOffset", d => d.startAngle * outerRadius)
.text(d => teamCodes[d.index].code))
.style('opacity', 0.7)
.style('cursor', 'hand')
.on('click', (_, {index}) => setFocusedTeamIndex(index))
.on("mouseover", function(){d3.select(this).style("opacity", "1");})
.on("mouseout", function(){d3.select(this).style("opacity", ".7");})
.call(g => g.append("title")
.text(d => teamCodes[d.index].code));

return svg.node();
}
Insert cell
Insert cell
mutable hoveredTeamIndex = -1
Insert cell
mutable focusedTeamIndex = -1
Insert cell
setHoveredTeamIndex = (i) => mutable hoveredTeamIndex = i
Insert cell
setFocusedTeamIndex = function(index) {
mutable focusedTeamIndex = focusedTeamIndex === index ? -1 : index
}
Insert cell
nodeIsFocusedTeam = function(d) {
return d.source.index === focusedTeamIndex || d.target.index === focusedTeamIndex
}
Insert cell
nonFocusedTeamColor = function(d) {
return color(d.source.index !== focusedTeamIndex ? d.source.index : d.target.index)
}
Insert cell
function getGradID(d) { return "linkGrad-" + d.source.index + "-" + d.target.index; }
// The code can do more sophisticated gradient chords between teams, but this turned
// out to be somewhat confusing, so now they monochromatic.

Insert cell
Insert cell
teamCodes = [{"code":"ATL","color":"#E13A3E"},{"code":"BKN","color":"#000000"},{"code":"BOS","color":"#007239"},{"code":"CHA","color":"#00788C"},{"code":"CHI","color":"#C60033"},{"code":"CLE","color":"#B2004A"},{"code":"DAL","color":"#0064B1"},{"code":"DEN","color":"#0E2240"},{"code":"DET","color":"#1D428A"},{"code":"GSW","color":"#1D428A"},{"code":"HOU","color":"#CE1141"},{"code":"IND","color":"#002D62"},{"code":"LAC","color":"#C8102E"},{"code":"LAL","color":"#552583"},{"code":"MEM","color":"#5D76A9"},{"code":"MIA","color":"#98002E"},{"code":"MIL","color":"#00471B"},{"code":"MIN","color":"#0C2340"},{"code":"NOP","color":"#002B5C"},{"code":"NYK","color":"#006BB6"},{"code":"OKC","color":"#007AC1"},{"code":"ORL","color":"#006BB7"},{"code":"PHI","color":"#006BB6"},{"code":"PHX","color":"#1D1160"},{"code":"POR","color":"#DE2032"},{"code":"SAC","color":"#5B2B82"},{"code":"SAS","color":"#000000"},{"code":"UTA","color":"#002B5C"},{"code":"WAS","color":"#002B5C"}]

Insert cell
colorsByTeamCode = d3.reduce(teamCodes, (a, t) => {a[t.code] = t.color; return a}, {})
Insert cell
color = i => colorsByTeamCode[teamCodes[i].code]
Insert cell
Insert cell
Insert cell
tradeLimiterFn = trade => {
if (selectedSeasons.length === 0) return true
const tradeDate = new Date(trade.date)
return selectedSeasons.map(s => s.value).reduce((a, s) => (a || s.includes(tradeDate)), false)
}

Insert cell
trades = d3.filter(rawTrades, tradeLimiterFn)
Insert cell
rawDataDateRange = {
const dates = rawTrades.map(x => new Date(x.date))
const earliest = d3.min(dates)
const latest = d3.max(dates)
return new DateRange(earliest, latest)
}
Insert cell
seasons = { // all availabel seasons, based on trade data
return d3.range(1900, new Date().getFullYear())
.map(y => {
return {
name: `${y}-${y+1}`,
value: new DateRange(new Date(y, 8, 1), new Date(y+1, 7, 31))
}
})
.filter(o => o.value.intersects(rawDataDateRange))
}
Insert cell
Insert cell
matrix = {
const index = new Map(teamCodes.map((o, i) => [o.code, i]));
const matrix = Array.from(index, () => new Array(teamCodes.length).fill(0));
for (const trade of trades) {
for (const teamCombo of combos(trade.teamCodes)) {
const teams = [...teamCombo].sort()
matrix[index.get(teams[0])][index.get(teams[1])] += trade.playerCount
}
}
return matrix;
}
Insert cell
chord = d3.chordDirected()
.padAngle(12 / innerRadius)
.sortSubgroups(d3.descending)
//.sortChords(d3.descending)
Insert cell
arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
Insert cell
ribbon = d3.ribbon()
.radius(innerRadius - 0.5)
.padAngle(1 / innerRadius)
Insert cell
outerRadius = innerRadius + 16
Insert cell
innerRadius = Math.min(width, height) * 0.5 - 30
Insert cell
width = 840
Insert cell
height = width
Insert cell
Insert cell
Insert cell
Insert cell
import { combos } from '@ndp/combos'
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