Skip to content

Instantly share code, notes, and snippets.

@sathomas
Last active December 12, 2016 19:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save sathomas/a7b0062211af69981ff3 to your computer and use it in GitHub Desktop.
Save sathomas/a7b0062211af69981ff3 to your computer and use it in GitHub Desktop.
Jazz Connections

This graph shows the top 25 jazz albums of all time (at least according to one blogger.) Links between the albums represent musicians that played on both. Click on the nodes and the links for more information.

Note: iTunes links are not affiliate links.

This visualization is a real application of the D3.js force layout. It demonstrates some of the principles in a series on that layout. You can review that series beginning with the first example.

<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Jazz Connections</title>
<style>
body, h1, h2 {
color: #444;
font-family: 'Helvetica Neue', Helvetica, sans-serif;
font-weight: 300;
}
#graph {
float: left;
position: relative;
}
#notes {
float: left;
margin-left: 20px;
}
h1, h2 {
margin: 0;
}
h1 {
font-size: 1.4em;
margin-bottom: 0.2em;
}
h2 {
font-size: 1.1em;
margin-bottom: 1em;
}
.artwork img {
border: 1px solid #fff;
-webkit-box-shadow: 0 3px 5px rgba(0,0,0,.3);
-moz-box-shadow: rgba(0,0,0,.3) 0 3px 5px;
border-color: #a2a2a2 9;
}
ul {
list-style: none;
padding-left: 0;
}
li {
padding-top: 0.2em;
}
.node circle, circle.node {
cursor: pointer;
fill: #ccc;
stroke: #fff;
stroke-width: 1px;
}
.edge line, line.edge {
cursor: pointer;
stroke: #aaa;
stroke-width: 2px;
}
.labelNode text, text.labelNode {
cursor: pointer;
fill: #444;
font-size: 11px;
font-weight: normal;
}
ul.connection {
background-color: #f0f0f0;
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 5px 10px rgba(0,0,0,0.2);
cursor: pointer;
font-size: 11px;
font-weight: normal;
padding: 10px;
position: absolute;
}
ul.connection:before,
ul.connection:after {
border: 10px solid transparent;
content: '';
position: absolute;
}
ul.connection:before {
border-bottom-color: #f0f0f0;
top: -19px;
left: 20px;
z-index: 2;
}
ul.connection:after {
border-bottom-color: rgba(0, 0, 0, 0.2);
top: -21px;
left: 20px;
z-index: 1;
}
/*
ul.connection li {
background-color: #eee;
border-left: 1px #444 solid;
border-right: 1px #444 solid;
font-size: 11px;
font-weight: normal;
margin: 0 50% 0 -50%;
padding: 2px 4px;
}
ul.connection li:first-child {
border-top: 1px #444 solid;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
ul.connection li:last-child {
border-bottom: 1px #444 solid;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
*/
ul.connection.hidden {
display: none;
}
</style>
</head>
<body>
<div id='container'>
<div id='graph'></div>
<div id='notes'></div>
</div>
<script src='http://d3js.org/d3.v3.min.js'></script>
<script>
// Define the dimensions of the visualization. We're using
// a size that's convenient for displaying the graphic on
// http://jsDataV.is
var width = 436,
height = 476;
// Visual properties of the graph are next. We need to make
// those that are going to be animated accessible to the
// JavaScript.
var labelFill = '#444';
var adjLabelFill = '#aaa';
var edgeStroke = '#aaa';
var nodeFill = '#ccc';
var nodeRadius = 10;
var selectedNodeRadius = 30;
var linkDistance = Math.min(width,height)/4;
// Find the main graph container.
var graph = d3.select('#graph');
// Create the SVG container for the visualization and
// define its dimensions.
var svg = graph.append('svg')
.attr('width', width)
.attr('height', height);
// Select the container for the notes and dimension it.
var notes = d3.select('#notes')
.style({
'width': 620-width + 'px',
'height': height + 'px'
});
// Utility function to update the position properties
// of an arbtrary edge that's part of a D3 selection.
// The optional parameter is the array of nodes for
// the edges. If present, the source and target properties
// are assumed to be indices in this array rather than
// direct references.
var positionEdge = function(edge, nodes) {
edge.attr('x1', function(d) {
return nodes ? nodes[d.source].x : d.source.x;
}).attr('y1', function(d) {
return nodes ? nodes[d.source].y : d.source.y;
}).attr('x2', function(d) {
return nodes ? nodes[d.target].x : d.target.x;
}).attr('y2', function(d) {
return nodes ? nodes[d.target].y : d.target.y;
});
};
// Utility function to update the position properties
// of an arbitrary node that's part of a D3 selection.
var positionNode = function(node) {
node.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
};
// Utility function to position text associated with
// a label pseudo-node. The optional third parameter
// requests transition to the specified fill color.
var positionLabelText = function(text, pseudonode, fillColor) {
// What's the width of the text element?
var textWidth = text.getBBox().width;
// How far is the pseudo-node from the real one?
var diffX = pseudonode.x - pseudonode.node.x;
var diffY = pseudonode.y - pseudonode.node.y;
var dist = Math.sqrt(diffX * diffX + diffY * diffY);
// Shift in the x-direction a fraction of the text width
var shiftX = textWidth * (diffX - dist) / (dist * 2);
shiftX = Math.max(-textWidth, Math.min(0, shiftX));
var shiftY = pseudonode.node.selected ? selectedNodeRadius : nodeRadius;
shiftY = 0.5 * shiftY * diffY/Math.abs(diffY);
var select = d3.select(text);
if (fillColor) {
select = select.transition().style('fill', fillColor);
}
select.attr('transform', 'translate(' + shiftX + ',' + shiftY + ')');
};
// Define the data.
data = [
{
"artist": "Miles Davis",
"title": "Kind of Blue",
"itunes": "https://itunes.apple.com/us/album/kind-of-blue-legacy-edition/id300865074",
"cover": "http://a4.mzstatic.com/us/r30/Features/v4/7b/f1/43/7bf14346-8e9d-e243-16e1-81f7377699a7/dj.aacpwrsd.170x170-75.jpg",
"color": "#47738C",
"text": "#0A0606",
"musicians": [
"Cannonball Adderley",
"Paul Chambers",
"Jimmy Cobb",
"John Coltrane",
"Miles Davis",
"Bill Evans"
]
},{
"artist": "John Coltrane",
"title": "A Love Supreme",
"itunes": "https://itunes.apple.com/us/album/a-love-supreme-deluxe-edition/id3269858",
"cover": "http://a5.mzstatic.com/us/r30/Features/v4/b3/67/bf/b367bf8e-4380-e566-de28-2c2bc69ef8ed/V4HttpAssetRepositoryClient-ticket.kvpiloem.jpg-2481435390037859980.170x170-75.jpg",
"color": "#747C7B",
"text": "#343437",
"musicians": [
"John Coltrane",
"Jimmy Garrison",
"Elvin Jones",
"McCoy Tyner"
]
},{
"artist": "The Dave Brubeck Quartet",
"title": "Time Out",
"itunes": "https://itunes.apple.com/us/album/time-out-50th-anniversary/id316475425",
"cover": "http://a5.mzstatic.com/us/r30/Music/fc/10/e4/mzi.nxyspvyl.170x170-75.jpg",
"color": "#42578E",
"text": "#D57130",
"musicians": [
"Dave Brubeck",
"Paul Desmond",
"Joe Morello",
"Eugene Write"
]
},{
"artist": "Duke Ellington",
"title": "Ellington at Newport",
"itunes": "https://itunes.apple.com/us/album/ellington-at-newport-1956/id214466665",
"cover": "http://a2.mzstatic.com/us/r30/Features/e5/43/3b/dj.kzeqdddm.170x170-75.jpg",
"color": "#D06B2F",
"text": "#2F4051",
"musicians": [
"Harry Carney",
"John Willie Cook",
"Duke Ellington",
"Paul Gonsalves",
"Jimmy Grissom",
"Jimmy Hamilton",
"Johnny Hodges",
"Quentin Jackson",
"William Anderson",
"Ray Nance",
"Russell Procope",
"John Sanders",
"Clark Terry",
"James Woode",
"Britt Woodman",
"Sam Woodyar"
]
},{
"artist": "The Quintet",
"title": "Jazz at Massey Hall",
"itunes": "https://itunes.apple.com/us/album/quintet-jazz-at-massey-hall/id152035858",
"cover": "http://a5.mzstatic.com/us/r30/Features/e7/f0/51/dj.suakypmw.170x170-75.jpg",
"color": "#B03239",
"text": "#191A18",
"musicians": [
"Dizzy Gillespie",
"Charles Mingus",
"Charlie Parker",
"Bud Powell",
"Max Roach"
]
},{
"artist": "Louis Armstrong",
"title": "The Best of the Hot Five and Hot Seven Recordings",
"itunes": "https://itunes.apple.com/us/album/best-hot-5-hot-7-recordings/id201274363",
"cover": "http://a2.mzstatic.com/us/r30/Music/54/10/9d/mzi.tzqujtgu.170x170-75.jpg",
"color": "#B08865",
"text": "#483830",
"musicians": [
"Lil Hardin Armstrong",
"Louis Armstrong",
"Clarence Babcock",
"Pete Briggs",
"Mancy Carr",
"Baby Dodds",
"Johnny Dodds",
"Earl Hines",
"Kid Ory",
"Don Redman",
"Fred Robinson",
"Zutty Singleton",
"Johnny St. Cyr",
"Jimmy Strong",
"John Thomas",
"Dave Wilborn"
]
},{
"artist": "John Coltrane",
"title": "Blue Trane",
"itunes": "https://itunes.apple.com/us/album/blue-train-bonus-track-version/id724748588",
"cover": "http://a5.mzstatic.com/us/r30/Music4/v4/4f/fe/d8/4ffed84e-8865-22b9-7a8e-ac1eaa07a542/00724359692456.170x170-75.jpg",
"color": "#0C7F90",
"text": "#041E1F",
"musicians": [
"Paul Chambers",
"John Coltrane",
"Kenny Drew",
"Curtis Fuller",
"Philly Joe Jones",
"Lee Morgan"
]
},{
"artist": "Stan Getz and João Gilberto",
"title": "Getz/Gilberto",
"itunes": "https://itunes.apple.com/us/album/getz-gilberto/id485540980",
"cover": "http://a3.mzstatic.com/us/r30/Features/b2/47/50/dj.qizgqbsb.170x170-75.jpg",
"color": "#ED8C42",
"text": "#24231B",
"musicians": [
"Milton Banana",
"Stan Getz",
"Astrud Gilberto",
"João Gilberto",
"Antonio Carlos Jobim",
"Sebastião Neto"
]
},{
"artist": "Charles Mingus",
"title": "Mingus Ah Um",
"itunes": "https://itunes.apple.com/us/album/mingus-ah-um-legacy-edition/id316476217",
"cover": "http://a2.mzstatic.com/us/r30/Music/85/f3/ef/mzi.lugdrvxc.170x170-75.jpg",
"color": "#B07B89",
"text": "#28282C",
"musicians": [
"Willie Dennis",
"Booker Ervin",
"Shafi Hadi",
"John Handy",
"Jimmy Knepper",
"Charles Mingus",
"Horace Parlan",
"Dannie Richmond"
]
},{
"artist": "Erroll Garner",
"title": "Concert by the Sea",
"itunes": "https://itunes.apple.com/us/album/concert-by-the-sea/id192787263",
"cover": "http://a4.mzstatic.com/us/r30/Music/38/50/49/mzi.trcydnrm.170x170-75.jpg",
"color": "#6B548B",
"text": "#2A231F",
"musicians": [
"Denzil Best",
"Eddie Calhoun",
"Erroll Garner"
]
},{
"artist": "Miles Davis",
"title": "Bitches Brew",
"itunes": "https://itunes.apple.com/us/album/bitches-brew-legacy-edition/id387785709",
"cover": "http://a5.mzstatic.com/us/r30/Features/v4/2c/a0/0c/2ca00caf-3889-59a9-c927-86011f9e8aab/dj.ibkxnerg.170x170-75.jpg",
"color": "#649B9E",
"text": "#212B30",
"musicians": [
"Don Alias",
"Harvey Brooks",
"Billy Cobham",
"Chick Corea",
"Miles Davis",
"Jack DeJohnette",
"Dave Holland",
"Bennie Maupin",
"John McLaughlin",
"Airto Moreira",
"Juma Santos",
"Wayne Shorter",
"Lenny White",
"Larry Young",
"Joe Zawinul"
]
},{
"artist": "Sonny Rollings",
"title": "Saxophone Colossus",
"itunes": "https://itunes.apple.com/us/album/saxophone-colossus-reissue/id129952067",
"cover": "http://a5.mzstatic.com/us/r30/Features/9c/45/92/dj.lhsjunju.170x170-75.jpg",
"color": "#32ADF1",
"text": "#0D1D27",
"musicians": [
"Tommy Flanagan",
"Sonny Rollins",
"Max Roach",
"Doug Watkins"
]
},{
"artist": "Art Blakey and The Jazz Messengers",
"title": "Moanin’",
"itunes": "https://itunes.apple.com/us/album/moanin-remastered/id725816184",
"cover": "http://a3.mzstatic.com/us/r30/Music/v4/a4/9e/d9/a49ed936-5c20-2e6b-fbc4-246006bdcbce/00724349532458.170x170-75.jpg",
"color": "#C3A550",
"text": "#161A11",
"musicians": [
"Art Blakey",
"Lee Morgan",
"Benny Golson",
"Bobby Timmons",
"Jymie Merritt"
]
},{
"title": "Clifford Brown and Max Roach",
"itunes": "https://itunes.apple.com/us/album/clifford-brown-and-max-roach/id539526",
"cover": "http://a3.mzstatic.com/us/r30/Features/8f/65/0e/dj.wlajviuz.170x170-75.jpg",
"color": "#DB6C39",
"text": "#120C19",
"musicians": [
"Clifford Brown",
"Harold Land",
"George Morrow",
"Richie Powell",
"Max Roach"
]
},{
"artist": "Thelonious Monk Quartet with John Coltrane",
"title": "At Carnegie Hall",
"itunes": "https://itunes.apple.com/us/album/at-carnegie-hall/id715742134",
"cover": "http://a3.mzstatic.com/us/r30/Music/v4/16/97/61/16976189-228e-5fb8-16e3-f2aa8ae09590/00094633517356.170x170-75.jpg",
"color": "#A2444C",
"text": "#49637B",
"musicians": [
"Ahmed Abdul-Malik",
"John Coltrane",
"Thelonious Monk",
"Shadow Wilson"
]
},{
"artist": "Hank Mobley",
"title": "Soul Station",
"itunes":"https://itunes.apple.com/us/album/soul-station-remastered/id724903486",
"cover": "http://a3.mzstatic.com/us/r30/Music6/v4/ec/1b/a3/ec1ba39e-d279-8ead-f460-58206f973ebf/00724349534353.170x170-75.jpg",
"color": "#3D97C5",
"text": "#152D45",
"musicians": [
"Art Blakey",
"Paul Chambers",
"Wynton Kelly",
"Hank Mobley"
]
},{
"artist": "Cannonball Adderly",
"title": "Somethin’ Else",
"itunes": "https://itunes.apple.com/us/album/somethin-else/id721271258",
"cover": "http://a5.mzstatic.com/us/r30/Music4/v4/2d/aa/d7/2daad774-8979-0bad-01d0-fd40936cded1/05099972196250.170x170-75.jpg",
"color": "#3C838D",
"text": "#020C09",
"musicians": [
"Cannonball Adderley",
"Art Blakey",
"Miles Davis",
"Hank Jones",
"Sam Jones"
]
},{
"artist": "Wayne Shorter",
"title": "Speak No Evil",
"itunes": "https://itunes.apple.com/us/album/speak-no-evil/id721228463",
"cover": "http://a5.mzstatic.com/us/r30/Music4/v4/c6/bd/7f/c6bd7fd6-7693-7dd0-9740-cdb564455673/05099963651157.170x170-75.jpg",
"color": "#10507E",
"text": "#06111F",
"musicians": [
"Ron Carter",
"Herbie Hancock",
"Freddie Hubbard",
"Elvin Jones",
"Wayne Shorter"
]
},{
"artist": "Miles Davis",
"title": "Birth of the Cool",
"itunes": "https://itunes.apple.com/nz/album/birth-of-the-cool-remastered/id724552390",
"cover": "http://a1.mzstatic.com/us/r30/Music/v4/c5/47/19/c5471971-aed3-b477-1340-e246068e2388/00724353011758.170x170-75.jpg",
"color": "#B23035",
"text": "#1E1A1E",
"musicians": [
"Bill Barber",
"Nelson Boyd",
"Kenny Clarke",
"Junior Collins",
"Miles Davis",
"Kenny Hagood",
"Al Haig",
"J. J. Johnson",
"Lee Konitz",
"John Lewis",
"Al McKibbon",
"Gerry Mulligan",
"Max Roach",
"Gunther Schuller",
"Joe Shulman",
"Sandy Siegelstein",
"Kai Winding"
]
},{
"artist": "Herbie Hancock",
"title": "Maiden Voyage",
"itunes": "https://itunes.apple.com/us/album/maiden-voyage-remastered/id721271378",
"cover": "http://a4.mzstatic.com/us/r30/Music/v4/c6/d1/7e/c6d17eaf-9e4c-d4fc-6253-c0f21a202585/05099963651454.170x170-75.jpg",
"color": "#B4CA68",
"text": "#0F404D",
"musicians": [
"Ron Carter",
"George Coleman",
"Herbie Hancock",
"Freddie Hubbard",
"Tony Williams"
]
},{
"artist": "Vince Guaraldi Trio",
"title": "A Boy Named Charlie Brown",
"itunes": "https://itunes.apple.com/us/album/a-boy-named-charlie-brown/id139986031",
"cover": "http://a4.mzstatic.com/us/r30/Features/bc/4e/6c/dj.ajuwwbjm.170x170-75.jpg",
"color": "#761826",
"text": "#322431",
"musicians": [
"Colin Bailey",
"Monty Budwig",
"Vince Guaraldi"
]
},{
"artist": "Eric Dolphy",
"title": "Out to Lunch",
"itunes": "https://itunes.apple.com/us/album/out-to-lunch/id721218360",
"cover": "http://a2.mzstatic.com/us/r30/Music/v4/b2/84/55/b2845520-816f-4531-2ff9-75faa3f0473c/05099963652253.170x170-75.jpg",
"color": "#84C2DD",
"text": "#172F33",
"musicians": [
"Richard Davis",
"Eric Dolphy",
"Freddie Hubbard",
"Bobby Hutcherson",
"Tony Williams"
]
},{
"artist": "Oliver Nelson",
"title": "The Blues and the Abstract Truth",
"itunes": "https://itunes.apple.com/us/album/blues-abstract-truth/id72075",
"cover": "http://a4.mzstatic.com/us/r30/Features/b6/bc/02/dj.ijqtqnth.170x170-75.jpg",
"color": "#C56A46",
"text": "#303969",
"musicians": [
"George Barrow",
"Paul Chambers",
"Eric Dolphy",
"Bill Evans",
"Roy Haynes",
"Freddie Hubbard",
"Oliver Nelson"
]
},{
"artist": "Dexter Gordon",
"title": "Go",
"itunes": "https://itunes.apple.com/us/album/go/id721286755",
"cover": "http://a1.mzstatic.com/us/r30/Music4/v4/ad/42/18/ad4218a3-b877-2eca-c144-51908649b166/05099993409957.170x170-75.jpg",
"color": "#EF5437",
"text": "#31474D",
"musicians": [
"Sonny Clark",
"Dexter Gordon",
"Billy Higgins",
"Butch Warren"
]
},{
"title": "Sarah Vaughan with Clifford Brown",
"itunes": "https://itunes.apple.com/ie/album/sarah-vaughan-clifford-brown/id81905225",
"cover": "http://a5.mzstatic.com/us/r30/Music/y2004/m06/d08/h09/s06.oyswffkb.170x170-75.jpg",
"color": "#29AFDD",
"text": "#203230",
"musicians": [
"Joe Benjamin",
"Clifford Brown",
"Roy Haynes",
"Jimmy Jones",
"John Malachi",
"Herbie Mann",
"Paul Quinichette",
"Sarah Vaughan",
"Ernie Wilkins"
]
}
];
// Find the graph nodes from the data set. Each
// album is a separate node.
var nodes = data.map(function(entry, idx, list) {
// This iteration returns a new object for
// each node.
var node = {};
// We retain some of the album's properties.
node.title = entry.title;
node.subtitle = entry.artist;
node.image = entry.cover;
node.url = entry.itunes;
node.color = entry.color;
node.text = entry.text;
// We'll also copy the musicians, again using
// a more neutral property. At the risk of
// some confusion, we're going to use the term
// "link" to refer to an individual connection
// between nodes, and we'll use the more
// mathematically correct term "edge" to refer
// to a line drawn between nodes on the graph.
// (This may be confusing because D3 refers to
// the latter as "links."
node.links = entry.musicians.slice(0);
// As long as we're iterating through the nodes
// array, take the opportunity to create an
// initial position for the nodes. Somewhat
// arbitrarily, we start the nodes off in a
// circle in the center of the container.
var radius = 0.4 * Math.min(height,width);
var theta = idx*2*Math.PI / list.length;
node.x = (width/2) + radius*Math.sin(theta);
node.y = (height/2) + radius*Math.cos(theta);
// Return the newly created object so it can be
// added to the nodes array.
return node;
});
// Identify all the indivual links between nodes on
// the graph. As noted above, we're using the term
// "link" to refer to a single connection. As we'll
// see below, we'll call lines drawn on the graph
// (which may represent a combination of multiple
// links) "edges" in a nod to the more mathematically
// minded.
var links = [];
// Start by iterating through the albums.
data.forEach(function(srcNode, srcIdx, srcList) {
// For each album, iterate through the musicians.
srcNode.musicians.forEach(function(srcLink) {
// For each musican in the "src" album, iterate
// through the remaining albums in the list.
for (var tgtIdx = srcIdx + 1;
tgtIdx < srcList.length;
tgtIdx++) {
// Use a variable to refer to the "tgt"
// album for convenience.
var tgtNode = srcList[tgtIdx];
// Is there any musician in the "tgt"
// album that matches the musican we're
// currently considering from the "src"
// album?
if (tgtNode.musicians.some(function(tgtLink){
return tgtLink === srcLink;
})) {
// When we do find a match, add a new
// link to the links array.
links.push({
source: srcIdx,
target: tgtIdx,
link: srcLink
});
}
}
});
});
// Now create the edges for our graph. We do that by
// eliminating duplicates from the links array.
var edges = [];
// Iterate through the links array.
links.forEach(function(link) {
// Assume for now that the current link is
// unique.
var existingEdge = false;
// Look through the edges we've collected so
// far to see if the current link is already
// present.
for (var idx = 0; idx < edges.length; idx++) {
// A duplicate link has the same source
// and target values.
if ((link.source === edges[idx].source) &&
(link.target === edges[idx].target)) {
// When we find an existing link, remember
// it.
existingEdge = edges[idx];
// And stop looking.
break;
}
}
// If we found an existing edge, all we need
// to do is add the current link to it.
if (existingEdge) {
existingEdge.links.push(link.link);
} else {
// If there was no existing edge, we can
// create one now.
edges.push({
source: link.source,
target: link.target,
links: [link.link]
});
}
});
// Start the creation of the graph by adding the edges.
// We add these first so they'll appear "underneath"
// the nodes.
var edgeSelection = svg.selectAll('.edge')
.data(edges)
.enter()
.append('line')
.classed('edge', true)
.style('stroke', edgeStroke)
.call(positionEdge, nodes);
// Next up are the nodes.
var nodeSelection = svg.selectAll('.node')
.data(nodes)
.enter()
.append('g')
.classed('node', true)
.call(positionNode);
nodeSelection.append('circle')
.attr('r', nodeRadius)
.attr('data-node-index', function(d,i) { return i;})
.style('fill', nodeFill)
// Now that we have our main selections (edges and
// nodes), we can create some subsets of those
// selections that will be helpful. Those subsets
// will be tied to individual nodes, so we'll
// start by iterating through them. We do that
// in two separate passes.
nodeSelection.each(function(node){
// First let's identify all edges that are
// incident to the node. We collect those as
// a D3 selection so we can manipulate the
// set easily with D3 utilities.
node.incidentEdgeSelection = edgeSelection
.filter(function(edge) {
return nodes[edge.source] === node ||
nodes[edge.target] === node;
});
});
// Now make a second pass through the nodes.
nodeSelection.each(function(node){
// For this pass we want to find all adjacencies.
// An adjacent node shares an edge with the
// current node.
node.adjacentNodeSelection = nodeSelection
.filter(function(otherNode){
// Presume that the nodes are not adjacent.
var isAdjacent = false;
// We can't be adjacent to ourselves.
if (otherNode !== node) {
// Look the incident edges of both nodes to
// see if there are any in common.
node.incidentEdgeSelection.each(function(edge){
otherNode.incidentEdgeSelection.each(function(otherEdge){
if (edge === otherEdge) {
isAdjacent = true;
}
});
});
}
return isAdjacent;
});
});
// Next we create a array for the node labels.
// We're going to use a "hidden" force layout to
// position the labels so they don't overlap
// each other. ("Hidden" because the links won't
// be visible.)
var labels = [];
var labelLinks = [];
nodes.forEach(function(node, idx){
// For each node on the graph we create
// two pseudo-nodes for its label. Once
// pseudo-node will be anchored to the
// center of the real node, while the
// second will be linked to that node.
// Add the pseudo-nodes to their array.
labels.push({node: node});
labels.push({node: node});
// And create a link between them.
labelLinks.push({
source: idx * 2,
target: idx * 2 + 1
});
});
// Construct the selections for the label layout.
// There's no need to add any markup for the
// pseudo-links between the label nodes, but
// we do need a selection so we can run the
// force layout.
var labelLinkSelection = svg.selectAll('line.labelLink')
.data(labelLinks);
// The label pseud-nodes themselves are just
// `<g>` containers.
var labelSelection = svg.selectAll('g.labelNode')
.data(labels)
.enter()
.append('g')
.classed('labelNode',true);
// Now add the text itself. Of the paired
// pseudo-nodes, only odd ones get the text
// elements.
labelSelection.append('text')
.text(function(d, i) {
return i % 2 == 0 ? '' : d.node.title;
})
.attr('data-node-index', function(d, i){
return i % 2 == 0 ? 'none' : Math.floor(i/2);
});
// The last bit of markup are the lists of
// connections for each link.
var connectionSelection = graph.selectAll('ul.connection')
.data(edges)
.enter()
.append('ul')
.classed('connection hidden', true)
.attr('data-edge-index', function(d,i) {return i;});
connectionSelection.each(function(connection){
var selection = d3.select(this);
connection.links.forEach(function(link){
selection.append('li')
.text(link);
})
})
// Create the main force layout.
var force = d3.layout.force()
.size([width, height])
.nodes(nodes)
.links(edges)
.linkDistance(linkDistance)
.charge(-500);
// Create the force layout for the labels.
var labelForce = d3.layout.force()
.size([width, height])
.nodes(labels)
.links(labelLinks)
.gravity(0)
.linkDistance(0)
.linkStrength(0.8)
.charge(-100);
// Let users drag the nodes.
nodeSelection.call(force.drag);
// Function to handle clicks on node elements
var nodeClicked = function(node) {
// Ignore events based on dragging.
if (d3.event.defaultPrevented) return;
// Remember whether or not the clicked
// node is currently selected.
var selected = node.selected;
// Keep track of the desired text color.
var fillColor;
// In all cases we start by resetting
// all the nodes and edges to their
// de-selected state. We may override
// this transition for some nodes and
// edges later.
nodeSelection
.each(function(node) { node.selected = false; })
.selectAll('circle')
.transition()
.attr('r', nodeRadius)
.style('fill', nodeFill);
edgeSelection
.transition()
.style('stroke', edgeStroke);
labelSelection
.transition()
.style('opacity', 0);
// Now see if the node wasn't previously selected.
if (!selected) {
// This node wasn't selected before, so
// we want to select it now. That means
// changing the styles of some of the
// elements in the graph.
// First we transition the incident edges.
node.incidentEdgeSelection
.transition()
.style('stroke', node.color);
// Now we transition the adjacent nodes.
node.adjacentNodeSelection.selectAll('circle')
.transition()
.attr('r', nodeRadius)
.style('fill', node.color);
labelSelection
.filter(function(label) {
var adjacent = false;
node.adjacentNodeSelection.each(function(d){
if (label.node === d) {
adjacent = true;
}
})
return adjacent;
})
.transition()
.style('opacity', 1)
.selectAll('text')
.style('fill', adjLabelFill);
// And finally, transition the node itself.
d3.selectAll('circle[data-node-index="'+node.index+'"]')
.transition()
.attr('r', selectedNodeRadius)
.style('fill', node.color);
// Make sure the node's label is visible
labelSelection
.filter(function(label) {return label.node === node;})
.transition()
.style('opacity', 1);
// And note the desired color for bundling with
// the transition of the label position.
fillColor = node.text;
// Delete the current notes section to prepare
// for new information.
notes.selectAll('*').remove();
// Fill in the notes section with informationm
// from the node. Because we want to transition
// this to match the transitions on the graph,
// we first set it's opacity to 0.
notes.style({'opacity': 0});
// Now add the notes content.
notes.append('h1').text(node.title);
notes.append('h2').text(node.subtitle);
if (node.url && node.image) {
notes.append('div')
.classed('artwork',true)
.append('a')
.attr('href', node.url)
.append('img')
.attr('src', node.image);
}
var list = notes.append('ul');
node.links.forEach(function(link){
list.append('li')
.text(link);
})
// With the content in place, transition
// the opacity to make it visible.
notes.transition().style({'opacity': 1});
} else {
// Since we're de-selecting the current
// node, transition the notes section
// and then remove it.
notes.transition()
.style({'opacity': 0})
.each('end', function(){
notes.selectAll('*').remove();
});
// Transition all the labels to their
// default styles.
labelSelection
.transition()
.style('opacity', 1)
.selectAll('text')
.style('fill', labelFill);
// The fill color for the current node's
// label must also be bundled with its
// position transition.
fillColor = labelFill;
}
// Toggle the selection state for the node.
node.selected = !selected;
// Update the position of the label text.
var text = d3.select('text[data-node-index="'+node.index+'"]').node();
var label = null;
labelSelection.each(function(d){
if (d.node === node) { label = d; }
})
if (text && label) {
positionLabelText(text, label, fillColor);
}
};
// Function to handle click on edges.
var edgeClicked = function(edge, idx) {
// Remember the current selection state of the edge.
var selected = edge.selected;
// Transition all connections to hidden. If the
// current edge needs to be displayed, it's transition
// will be overridden shortly.
connectionSelection
.each(function(edge) { edge.selected = false; })
.transition()
.style('opacity', 0)
.each('end', function(){
d3.select(this).classed('hidden', true);
});
// If the current edge wasn't selected before, we
// want to transition it to the selected state now.
if (!selected) {
d3.select('ul.connection[data-edge-index="'+idx+'"]')
.classed('hidden', false)
.style('opacity', 0)
.transition()
.style('opacity', 1);
}
// Toggle the resulting selection state for the edge.
edge.selected = !selected;
};
// Handle clicks on the nodes.
nodeSelection.on('click', nodeClicked);
labelSelection.on('click', function(pseudonode) {
nodeClicked(pseudonode.node);
});
// Handle clicks on the edges.
edgeSelection.on('click', edgeClicked);
connectionSelection.on('click', edgeClicked);
// Animate the force layout.
force.on('tick', function() {
// Constrain all the nodes to remain in the
// graph container.
nodeSelection.each(function(node) {
node.x = Math.max(node.x, 2*selectedNodeRadius);
node.y = Math.max(node.y, 2*selectedNodeRadius);
node.x = Math.min(node.x, width-2*selectedNodeRadius);
node.y = Math.min(node.y, height-2*selectedNodeRadius);
});
// Kick the label layout to make sure it doesn't
// finish while the main layout is still running.
labelForce.start();
// Calculate the positions of the label nodes.
labelSelection.each(function(label, idx) {
// Label pseudo-nodes come in pairs. We
// treat odd and even nodes differently.
if(idx % 2) {
// Odd pseudo-nodes have the actual text.
// That text needs a real position. The
// pseudo-node itself we leave to the
// force layout to position.
positionLabelText(this.childNodes[0], label);
} else {
// Even pseudo-nodes (which have no text)
// are fixed to the center of the
// corresponding real node. This will
// override the position calculated by
// the force layout.
label.x = label.node.x;
label.y = label.node.y;
}
});
// Calculate the position for the connection lists.
connectionSelection.each(function(connection){
var x = (connection.source.x + connection.target.x)/2 - 27;
var y = (connection.source.y + connection.target.y)/2;
d3.select(this)
.style({
'top': y + 'px',
'left': x + 'px'
});
});
// Update the posistions of the nodes and edges.
nodeSelection.call(positionNode);
labelSelection.call(positionNode);
edgeSelection.call(positionEdge);
labelLinkSelection.call(positionEdge);
});
// Start the layout computations.
force.start();
labelForce.start();
</script>
</body>
</html>
http://jsdatav.is/img/thumbnails/jazz.png
@owendall
Copy link

Nice! I am a big Jazz fan, so this is especially cool to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment