|
<!DOCTYPE html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<meta name="name" content="Concept Map" /> |
|
<meta name="description" content="An abstract mapping for parameters. Works best if first tag is 'unique' among the tracklist, and the second tag applies to multiple tracks"/> |
|
<meta name="mintags" content="2" /> |
|
<meta name="maxtags" content="2" /> |
|
<title>Jam Cellar 2014-05-06</title> |
|
|
|
<style> |
|
|
|
svg { |
|
font: 12px sans-serif; |
|
} |
|
|
|
text { |
|
pointer-events: none; |
|
} |
|
|
|
.inner_node rect { |
|
pointer-events: all; |
|
} |
|
|
|
.inner_node rect.highlight { |
|
stroke: #315B7E; |
|
stroke-width: 2px; |
|
} |
|
|
|
.outer_node circle { |
|
fill: #fff; |
|
stroke: steelblue; |
|
stroke-width: 1.5px; |
|
pointer-events: all; |
|
} |
|
|
|
.outer_node circle.highlight |
|
{ |
|
stroke: #315B7E; |
|
stroke-width: 2px; |
|
} |
|
|
|
.link { |
|
fill: none; |
|
} |
|
|
|
</style> |
|
</head> |
|
<body> |
|
<script src="https://d3js.org/d3.v3.min.js"></script> |
|
<script> |
|
// |
|
// Generated by the Exaile Playlist Analyzer plugin. |
|
// (C) 2014 Dustin Spicuzza <[email protected]> |
|
// |
|
// This work is licensed under the Creative Commons Attribution 4.0 |
|
// International License. To view a copy of this license, visit |
|
// http://creativecommons.org/licenses/by/4.0/. |
|
// |
|
// Inspired by http://www.findtheconversation.com/concept-map/ |
|
// Loosely based on http://bl.ocks.org/mbostock/4063550 |
|
// |
|
|
|
|
|
var data = [[120, ["like", "call response", "dramatic intro", "has breaks", "male vocalist", "silly", "swing"]], [150, ["brassy", "like", "calm energy", "female vocalist", "swing", "fun"]], [170, ["calm energy", "instrumental", "swing", "like", "happy"]], [140, ["has breaks", "male vocalist", "swing", "piano", "banjo", "chill"]], [160, ["calm energy", "instrumental", "swing", "like", "interesting"]], [140, ["brassy", "like", "energy", "dramatic intro", "male vocalist", "baseball", "swing"]], [170, ["instrumental", "interesting", "high energy", "like", "swing"]], [140, ["instrumental", "energy", "like", "swing"]], [200, ["instrumental", "brassy", "dramatic intro", "like", "swing"]], [160, ["male vocalist", "brassy", "swing", "like", "my favorites"]], [130, ["like", "interesting", "dramatic intro", "male vocalist", "silly", "swing", "gospel"]], [160, ["like", "long intro", "announcer", "energy", "swing", "female vocalist"]], [170, ["instrumental", "swing", "bass", "like"]], [150, ["like", "interesting", "has breaks", "instrumental", "chunky", "swing", "banjo", "trumpet"]], [170, ["like", "has breaks", "male vocalist", "silly", "swing", "banjo"]], [190, ["instrumental", "banjo", "swing"]], [130, ["instrumental", "brassy", "banjo", "like", "swing"]], [160, ["brassy", "like", "energy", "instrumental", "big band", "jam", "swing"]], [150, ["like", "male vocalist", "live", "swing", "piano", "banjo", "chill"]], [150, ["like", "trick ending", "instrumental", "chunky", "swing", "chill"]], [120, ["brassy", "like", "female vocalist", "swing", "chill", "energy buildup"]], [150, ["brassy", "like", "interesting", "instrumental", "swing", "piano"]], [190, ["brassy", "like", "long intro", "energy", "baseball", "swing", "female vocalist"]], [180, ["calm energy", "female vocalist", "live", "like", "swing"]], [200, ["banjo", "like", "long intro", "interesting", "energy", "my favorites", "male vocalist", "silly", "swing", "fun", "balboa"]], [150, ["brassy", "calm energy", "chunky", "instrumental", "old-timey", "live", "swing"]], [160, ["like", "call response", "interesting", "instrumental", "calm energy", "swing"]], [180, ["interesting", "swing", "fast", "male vocalist"]], [150, ["calm energy", "chunky", "swing", "female vocalist", "like"]], [180, ["like", "has breaks", "male vocalist", "chunky", "silly", "swing"]], [140, ["instrumental", "brassy", "dramatic intro", "swing", "chill"]], [150, ["male vocalist", "trumpet", "like", "swing"]], [150, ["instrumental", "energy", "like", "has breaks", "swing"]], [180, ["brassy", "like", "energy", "has breaks", "instrumental", "has calm", "swing"]], [150, ["female vocalist", "swing"]], [170, ["instrumental", "brassy", "energy", "swing"]], [170, ["calm energy", "instrumental", "energy", "like", "swing"]], [190, ["brassy", "like", "instrumental", "high energy", "swing", "trumpet"]], [160, ["male vocalist", "energy", "swing", "old-timey"]], [170, ["like", "oldies", "my favorites", "fast", "male vocalist", "high energy", "swing"]]]; |
|
|
|
// transform the data into a useful representation |
|
// 1 is inner, 2, is outer |
|
|
|
// need: inner, outer, links |
|
// |
|
// inner: |
|
// links: { inner: outer: } |
|
|
|
|
|
var outer = d3.map(); |
|
var inner = []; |
|
var links = []; |
|
|
|
var outerId = [0]; |
|
|
|
data.forEach(function(d){ |
|
|
|
if (d == null) |
|
return; |
|
|
|
i = { id: 'i' + inner.length, name: d[0], related_links: [] }; |
|
i.related_nodes = [i.id]; |
|
inner.push(i); |
|
|
|
if (!Array.isArray(d[1])) |
|
d[1] = [d[1]]; |
|
|
|
d[1].forEach(function(d1){ |
|
|
|
o = outer.get(d1); |
|
|
|
if (o == null) |
|
{ |
|
o = { name: d1, id: 'o' + outerId[0], related_links: [] }; |
|
o.related_nodes = [o.id]; |
|
outerId[0] = outerId[0] + 1; |
|
|
|
outer.set(d1, o); |
|
} |
|
|
|
// create the links |
|
l = { id: 'l-' + i.id + '-' + o.id, inner: i, outer: o } |
|
links.push(l); |
|
|
|
// and the relationships |
|
i.related_nodes.push(o.id); |
|
i.related_links.push(l.id); |
|
o.related_nodes.push(i.id); |
|
o.related_links.push(l.id); |
|
}); |
|
}); |
|
|
|
data = { |
|
inner: inner, |
|
outer: outer.values(), |
|
links: links |
|
} |
|
|
|
// sort the data -- TODO: have multiple sort options |
|
outer = data.outer; |
|
data.outer = Array(outer.length); |
|
|
|
|
|
var i1 = 0; |
|
var i2 = outer.length - 1; |
|
|
|
for (var i = 0; i < data.outer.length; ++i) |
|
{ |
|
if (i % 2 == 1) |
|
data.outer[i2--] = outer[i]; |
|
else |
|
data.outer[i1++] = outer[i]; |
|
} |
|
|
|
console.log(data.outer.reduce(function(a,b) { return a + b.related_links.length; }, 0) / data.outer.length); |
|
|
|
|
|
// from d3 colorbrewer: |
|
// This product includes color specifications and designs developed by Cynthia Brewer (http://colorbrewer.org/). |
|
var colors = ["#a50026","#d73027","#f46d43","#fdae61","#fee090","#ffffbf","#e0f3f8","#abd9e9","#74add1","#4575b4","#313695"] |
|
var color = d3.scale.linear() |
|
.domain([60, 220]) |
|
.range([colors.length-1, 0]) |
|
.clamp(true); |
|
|
|
var diameter = 960; |
|
var rect_width = 40; |
|
var rect_height = 14; |
|
|
|
var link_width = "1px"; |
|
|
|
var il = data.inner.length; |
|
var ol = data.outer.length; |
|
|
|
var inner_y = d3.scale.linear() |
|
.domain([0, il]) |
|
.range([-(il * rect_height)/2, (il * rect_height)/2]); |
|
|
|
mid = (data.outer.length/2.0) |
|
var outer_x = d3.scale.linear() |
|
.domain([0, mid, mid, data.outer.length]) |
|
.range([15, 170, 190 ,355]); |
|
|
|
var outer_y = d3.scale.linear() |
|
.domain([0, data.outer.length]) |
|
.range([0, diameter / 2 - 120]); |
|
|
|
|
|
// setup positioning |
|
data.outer = data.outer.map(function(d, i) { |
|
d.x = outer_x(i); |
|
d.y = diameter/3; |
|
return d; |
|
}); |
|
|
|
data.inner = data.inner.map(function(d, i) { |
|
d.x = -(rect_width / 2); |
|
d.y = inner_y(i); |
|
return d; |
|
}); |
|
|
|
|
|
function get_color(name) |
|
{ |
|
var c = Math.round(color(name)); |
|
if (isNaN(c)) |
|
return '#dddddd'; // fallback color |
|
|
|
return colors[c]; |
|
} |
|
|
|
// Can't just use d3.svg.diagonal because one edge is in normal space, the |
|
// other edge is in radial space. Since we can't just ask d3 to do projection |
|
// of a single point, do it ourselves the same way d3 would do it. |
|
|
|
|
|
function projectX(x) |
|
{ |
|
return ((x - 90) / 180 * Math.PI) - (Math.PI/2); |
|
} |
|
|
|
var diagonal = d3.svg.diagonal() |
|
.source(function(d) { return {"x": d.outer.y * Math.cos(projectX(d.outer.x)), |
|
"y": -d.outer.y * Math.sin(projectX(d.outer.x))}; }) |
|
.target(function(d) { return {"x": d.inner.y + rect_height/2, |
|
"y": d.outer.x > 180 ? d.inner.x : d.inner.x + rect_width}; }) |
|
.projection(function(d) { return [d.y, d.x]; }); |
|
|
|
|
|
var svg = d3.select("body").append("svg") |
|
.attr("width", diameter) |
|
.attr("height", diameter) |
|
.append("g") |
|
.attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")"); |
|
|
|
|
|
// links |
|
var link = svg.append('g').attr('class', 'links').selectAll(".link") |
|
.data(data.links) |
|
.enter().append('path') |
|
.attr('class', 'link') |
|
.attr('id', function(d) { return d.id }) |
|
.attr("d", diagonal) |
|
.attr('stroke', function(d) { return get_color(d.inner.name); }) |
|
.attr('stroke-width', link_width); |
|
|
|
// outer nodes |
|
|
|
var onode = svg.append('g').selectAll(".outer_node") |
|
.data(data.outer) |
|
.enter().append("g") |
|
.attr("class", "outer_node") |
|
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; }) |
|
.on("mouseover", mouseover) |
|
.on("mouseout", mouseout); |
|
|
|
onode.append("circle") |
|
.attr('id', function(d) { return d.id }) |
|
.attr("r", 4.5); |
|
|
|
onode.append("circle") |
|
.attr('r', 20) |
|
.attr('visibility', 'hidden'); |
|
|
|
onode.append("text") |
|
.attr('id', function(d) { return d.id + '-txt'; }) |
|
.attr("dy", ".31em") |
|
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; }) |
|
.attr("transform", function(d) { return d.x < 180 ? "translate(8)" : "rotate(180)translate(-8)"; }) |
|
.text(function(d) { return d.name; }); |
|
|
|
// inner nodes |
|
|
|
var inode = svg.append('g').selectAll(".inner_node") |
|
.data(data.inner) |
|
.enter().append("g") |
|
.attr("class", "inner_node") |
|
.attr("transform", function(d, i) { return "translate(" + d.x + "," + d.y + ")"}) |
|
.on("mouseover", mouseover) |
|
.on("mouseout", mouseout); |
|
|
|
inode.append('rect') |
|
.attr('width', rect_width) |
|
.attr('height', rect_height) |
|
.attr('id', function(d) { return d.id; }) |
|
.attr('fill', function(d) { return get_color(d.name); }); |
|
|
|
inode.append("text") |
|
.attr('id', function(d) { return d.id + '-txt'; }) |
|
.attr('text-anchor', 'middle') |
|
.attr("transform", "translate(" + rect_width/2 + ", " + rect_height * .75 + ")") |
|
.text(function(d) { return d.name; }); |
|
|
|
// need to specify x/y/etc |
|
|
|
d3.select(self.frameElement).style("height", diameter - 150 + "px"); |
|
|
|
function mouseover(d) |
|
{ |
|
// bring to front |
|
d3.selectAll('.links .link').sort(function(a, b){ return d.related_links.indexOf(a.id); }); |
|
|
|
for (var i = 0; i < d.related_nodes.length; i++) |
|
{ |
|
d3.select('#' + d.related_nodes[i]).classed('highlight', true); |
|
d3.select('#' + d.related_nodes[i] + '-txt').attr("font-weight", 'bold'); |
|
} |
|
|
|
for (var i = 0; i < d.related_links.length; i++) |
|
d3.select('#' + d.related_links[i]).attr('stroke-width', '5px'); |
|
} |
|
|
|
function mouseout(d) |
|
{ |
|
for (var i = 0; i < d.related_nodes.length; i++) |
|
{ |
|
d3.select('#' + d.related_nodes[i]).classed('highlight', false); |
|
d3.select('#' + d.related_nodes[i] + '-txt').attr("font-weight", 'normal'); |
|
} |
|
|
|
for (var i = 0; i < d.related_links.length; i++) |
|
d3.select('#' + d.related_links[i]).attr('stroke-width', link_width); |
|
} |
|
|
|
</script> |
|
|