chord-links.js
var width = 960,
height = 500,
angle = d3.scale.linear()
var events = d3.range(10).map(function(d, i){
return {
i: i,
name: randString(Math.floor(Math.random()*10) + 10),
people: [],
places: []
}
})
angle.domain([0, events.length - 1]).range([-12, 12])
events.forEach(function(d, i){
d.θ = angle(d.i)
d.rotateCenter = P(width/2 + 700, height/2)
d.rectWidth = 220
d.rectHeight = 30
d.translate = P(width/2 - 220 - 100, height/2)
})
var people = d3.range(15).map(function(d, i){
var rv = {
i: i,
name: randString(Math.floor(Math.random()*5) + 5)
}
rv.events = events.filter(function(){ return Math.random() < .3 })
rv.events.forEach(function(e){ e.people.push(rv) })
return rv
})
angle.domain([0, people.length - 1]).range([-60, -3])
people.forEach(function(d, i){
d.θ = angle(d.i)
d.rotateCenter = P(width/2, height/2)
d.rectWidth = 100
d.rectHeight = 12
d.translate = P(width/2 + 180, height/2)
})
var places = d3.range(15).map(function(d, i){
var rv = {
i: i,
name: randString(Math.floor(Math.random()*5) + 5)
}
rv.events = events.filter(function(){ return Math.random() < .3 })
rv.events.forEach(function(e){ e.places.push(rv) })
return rv
})
angle.domain([0, places.length - 1]).range([3, 60])
places.forEach(function(d, i){
d.θ = angle(d.i)
d.rotateCenter = P(width/2, height/2)
d.rectWidth = 100
d.rectHeight = 12
d.translate = P(width/2 + 180, height/2)
})
events.concat(people).concat(places).forEach(function(d){
d.corners = d3.range(4).map(function(i){
var x = d.translate.x + (i == 0 || i == 3 ? 0 : d.rectWidth)
var y = d.translate.y + (i < 2 ? 0 : d.rectHeight)
return rotatePoint({x: x, y: y}, d.rotateCenter, d.θ)
})
})
var svg = d3.select('body')
.append('svg')
.attr({width: width, height: height})
var eventGs = svg.dataAppend(events, 'g.place').call(rotateTransform)
eventGs.append('rect')
.attr({width: 220, height: 30})
.on('mouseover', makeLeftConnections)
eventGs.append('text')
.text(ƒ('name'))
.attr({dy: '1.33em', dx: '.33em'})
var peopleG = svg.dataAppend(people, 'g.people').call(rotateTransform)
peopleG.append('rect')
.attr({width: 100, height: 12})
.on('mouseover', makeRightConnections)
peopleG.append('text')
.text(ƒ('name'))
.attr({dy: '1em', dx: '.33em'})
var placesG = svg.dataAppend(places, 'g.places').call(rotateTransform)
placesG.append('rect')
.attr({width: 100, height: 12})
.on('mouseover', makeRightConnections)
placesG.append('text')
.text(ƒ('name'))
.attr({dy: '1em', dx: '.33em'})
function drawConnections(connections){
var color = randColor()
svg.append('g').dataAppend(connections, 'path.connection')
.style('fill', color)
.attr('d', function(d){
return [
'M', d.from.top,
'C', [d.from.top.x, d.from.top.y],
[d.from.top.x, d.from.top.y],
d.from.top,
'L', d.from.bot,
'C', [d.from.bot.x, d.from.bot.y],
[d.from.bot.x, d.from.bot.y],
d.from.bot,
].join(' ')
})
.transition().duration(1000)
.attr('d', function(d){
var fSin = Math.sin(Math.PI/2 + Math.PI/180*d.from.θ)
var fCos = Math.cos(Math.PI/2 + Math.PI/180*d.from.θ)
var tSin = Math.sin(Math.PI/2 + Math.PI/180*d.to.θ)
var tCos = Math.cos(Math.PI/2 + Math.PI/180*d.to.θ)
return [
'M', d.to.top,
'C', [d.to.top.x - tSin*100, d.to.top.y - tCos*100],
[d.from.top.x - fSin*100, d.from.top.y - fCos*100],
d.from.top,
'L', d.from.bot,
'C', [d.from.bot.x - fSin*100, d.from.bot.y - fCos*100],
[d.to.bot.x - tSin*100, d.to.bot.y - tCos*100],
d.to.bot,
].join(' ')
})
.each('end', function(d){
if (!d.exiting) return d.drawing = false
d3.select(this).transition().duration(1000)
.call(exitConnection)
})
}
function makeRightConnections(datum){
exitConnections()
var connections = datum.events.map(function(d){
return { from:
{ top: datum.corners[0],
bot: datum.corners[3],
θ: -datum.θ},
to:
{ top: d.corners[1],
bot: d.corners[2],
θ: -d.θ + 180},
drawing: true,
exiting: false
}
})
drawConnections(connections)
}
function makeLeftConnections(datum){
exitConnections()
var connections = datum.people.concat(datum.places).map(function(d){
return { from:
{ top: datum.corners[1],
bot: datum.corners[2],
θ: -datum.θ + 180},
to:
{ top: d.corners[0],
bot: d.corners[3],
θ: -d.θ},
drawing: true,
exiting: false
}
})
drawConnections(connections)
}
function rotateTransform(sel){
sel.attr('transform', function(d, i){
return 'rotate(' + d.θ + ' ' + d.rotateCenter +') ' +
'translate(' + d.translate + ')'
})
}
function exitConnections(){
svg.selectAll('.connection')
.each(function(d){ d.exiting = true })
.style('opacity', .7)
.filter(function(d){ return !d.drawing })
.each(function(d){ d.drawing = true })
.transition().duration(1000)
.call(exitConnection)
}
function exitConnection(sel){
sel.attr('d', function(d){
return [
'M', d.to.top,
'C', [d.to.top.x, d.to.top.y],
[d.to.top.x, d.to.top.y],
d.to.top,
'L', d.to.bot,
'C', [d.to.bot.x, d.to.bot.y],
[d.to.bot.x, d.to.bot.y],
d.to.bot,
].join(' ')
})
.remove()
}
function rotatePoint(p, c, θ){
var sin = Math.sin(θ*Math.PI/180)
var cos = Math.cos(θ*Math.PI/180)
var x0 = p.x - c.x
var y0 = p.y - c.y
var x1 = x0*cos - y0*sin
var y1 = x0*sin + y0*cos
return P(x1 + c.x, y1 + c.y)
}
function randString(len){
var alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ '
return d3.range(len).map(function(){
return alpha[Math.floor(Math.random()*alpha.length)]
}).join('')
}
function randColor(){
return 'rgb(' + [Math.random()*255, Math.random()*255, Math.random()*255].map(Math.round) + ')'
}