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) + ')' }