Created
August 17, 2012 01:34
-
-
Save vlandham/3375142 to your computer and use it in GitHub Desktop.
Example of using different grouping mechanism
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
root = exports ? this | |
# Help with the placement of nodes | |
RadialPlacement = () -> | |
# stores the key -> location values | |
values = d3.map() | |
# how much to separate each location by | |
increment = 20 | |
# how large to make the layout | |
radius = 200 | |
# where the center of the layout should be | |
center = {"x":0, "y":0} | |
# what angle to start at | |
start = -120 | |
current = start | |
# Given an center point, angle, and radius length, | |
# return a radial position for that angle | |
radialLocation = (center, angle, radius) -> | |
x = (center.x + radius * Math.cos(angle * Math.PI / 180)) | |
y = (center.y + radius * Math.sin(angle * Math.PI / 180)) | |
{"x":x,"y":y} | |
# Main entry point for RadialPlacement | |
# Returns location for a particular key, | |
# creating a new location if necessary. | |
placement = (key) -> | |
value = values.get(key) | |
if !values.has(key) | |
value = place(key) | |
value | |
# Gets a new location for input key | |
place = (key) -> | |
value = radialLocation(center, current, radius) | |
values.set(key,value) | |
current += increment | |
value | |
# Given a set of keys, perform some | |
# magic to create a two ringed radial layout. | |
# Expects radius, increment, and center to be set. | |
# If there are a small number of keys, just make | |
# one circle. | |
setKeys = (keys) -> | |
# start with an empty values | |
values = d3.map() | |
# number of keys to go in first circle | |
firstCircleCount = 360 / increment | |
# if we don't have enough keys, modify increment | |
# so that they all fit in one circle | |
if keys.length < firstCircleCount | |
increment = 360 / keys.length | |
# set locations for inner circle | |
firstCircleKeys = keys.slice(0,firstCircleCount) | |
firstCircleKeys.forEach (k) -> place(k) | |
# set locations for outer circle | |
secondCircleKeys = keys.slice(firstCircleCount) | |
# setup outer circle | |
radius = radius + radius / 1.8 | |
increment = 360 / secondCircleKeys.length | |
secondCircleKeys.forEach (k) -> place(k) | |
placement.keys = (_) -> | |
if !arguments.length | |
return d3.keys(values) | |
setKeys(_) | |
placement | |
placement.center = (_) -> | |
if !arguments.length | |
return center | |
center = _ | |
placement | |
placement.radius = (_) -> | |
if !arguments.length | |
return radius | |
radius = _ | |
placement | |
placement.start = (_) -> | |
if !arguments.length | |
return start | |
start = _ | |
current = start | |
placement | |
placement.increment = (_) -> | |
if !arguments.length | |
return increment | |
increment = _ | |
placement | |
return placement | |
Network = () -> | |
# variables we want to access | |
# in multiple places of Network | |
width = 960 | |
height = 800 | |
# allData will store the unfiltered data | |
allData = [] | |
curLinksData = [] | |
curNodesData = [] | |
linkedByIndex = {} | |
# these will hold the svg groups for | |
# accessing the nodes and links display | |
nodesG = null | |
linksG = null | |
# these will point to the circles and lines | |
# of the nodes and links | |
node = null | |
link = null | |
# variables to refect the current settings | |
# of the visualization | |
layout = "force" | |
filter = "all" | |
sort = "songs" | |
# groupCenters will store our radial layout for | |
# the group by artist layout. | |
groupCenters = null | |
# our force directed layout | |
force = d3.layout.force() | |
# color function used to color nodes | |
nodeColors = d3.scale.category20() | |
# tooltip used to display details | |
tooltip = Tooltip("vis-tooltip", 230) | |
# charge used in artist layout | |
charge = (node) -> -Math.pow(node.radius, 2.0) / 2 | |
# Starting point for network visualization | |
# Initializes visualization and starts force layout | |
network = (selection, data) -> | |
# format our data | |
allData = setupData(data) | |
# create our svg and groups | |
vis = d3.select(selection).append("svg") | |
.attr("width", width) | |
.attr("height", height) | |
linksG = vis.append("g").attr("id", "links") | |
nodesG = vis.append("g").attr("id", "nodes") | |
# setup the size of the force environment | |
force.size([width, height]) | |
setLayout("force") | |
setFilter("all") | |
# perform rendering and start force layout | |
update() | |
# The update() function performs the bulk of the | |
# work to setup our visualization based on the | |
# current layout/sort/filter. | |
# | |
# update() is called everytime a parameter changes | |
# and the network needs to be reset. | |
update = () -> | |
# filter data to show based on current filter settings. | |
curNodesData = filterNodes(allData.nodes) | |
curLinksData = filterLinks(allData.links, curNodesData) | |
# sort nodes based on current sort and update centers for | |
# radial layout | |
if layout == "radial" | |
artists = sortedArtists(curNodesData, curLinksData) | |
updateCenters(artists) | |
# reset nodes in force layout | |
force.nodes(curNodesData) | |
# enter / exit for nodes | |
updateNodes() | |
# always show links in force layout | |
if layout == "force" | |
force.links(curLinksData) | |
updateLinks() | |
else | |
# reset links so they do not interfere with | |
# other layouts. updateLinks() will be called when | |
# force is done animating. | |
force.links([]) | |
# if present, remove them from svg | |
if link | |
link.data([]).exit().remove() | |
link = null | |
# start me up! | |
force.start() | |
# Public function to switch between layouts | |
network.toggleLayout = (newLayout) -> | |
force.stop() | |
setLayout(newLayout) | |
update() | |
# Public function to switch between filter options | |
network.toggleFilter = (newFilter) -> | |
force.stop() | |
setFilter(newFilter) | |
update() | |
# Public function to switch between sort options | |
network.toggleSort = (newSort) -> | |
force.stop() | |
setSort(newSort) | |
update() | |
# Public function to update highlighted nodes | |
# from search | |
network.updateSearch = (searchTerm) -> | |
searchRegEx = new RegExp(searchTerm.toLowerCase()) | |
node.each (d) -> | |
element = d3.select(this) | |
match = d.name.toLowerCase().search(searchRegEx) | |
if searchTerm.length > 0 and match >= 0 | |
element.style("fill", "#F38630") | |
.style("stroke-width", 2.0) | |
.style("stroke", "#555") | |
d.searched = true | |
else | |
d.searched = false | |
element.style("fill", (d) -> nodeColors(d.artist)) | |
.style("stroke-width", 1.0) | |
network.updateData = (newData) -> | |
allData = setupData(newData) | |
link.remove() | |
node.remove() | |
update() | |
# called once to clean up raw data and switch links to | |
# point to node instances | |
# Returns modified data | |
setupData = (data) -> | |
# initialize circle radius scale | |
countExtent = d3.extent(data.nodes, (d) -> d.playcount) | |
circleRadius = d3.scale.sqrt().range([3, 12]).domain(countExtent) | |
data.nodes.forEach (n) -> | |
# set initial x/y to values within the width/height | |
# of the visualization | |
n.x = randomnumber=Math.floor(Math.random()*width) | |
n.y = randomnumber=Math.floor(Math.random()*height) | |
# add radius to the node so we can use it later | |
n.radius = circleRadius(n.playcount) | |
# id's -> node objects | |
nodesMap = mapNodes(data.nodes) | |
# switch links to point to node objects instead of id's | |
data.links.forEach (l) -> | |
l.source = nodesMap.get(l.source) | |
l.target = nodesMap.get(l.target) | |
# linkedByIndex is used for link sorting | |
linkedByIndex["#{l.source.id},#{l.target.id}"] = 1 | |
data | |
# Helper function to map node id's to node objects. | |
# Returns d3.map of ids -> nodes | |
mapNodes = (nodes) -> | |
nodesMap = d3.map() | |
nodes.forEach (n) -> | |
nodesMap.set(n.id, n) | |
nodesMap | |
# Helper function that returns an associative array | |
# with counts of unique attr in nodes | |
# attr is value stored in node, like 'artist' | |
nodeCounts = (nodes, attr) -> | |
counts = {} | |
nodes.forEach (d) -> | |
counts[d[attr]] ?= 0 | |
counts[d[attr]] += 1 | |
counts | |
# Given two nodes a and b, returns true if | |
# there is a link between them. | |
# Uses linkedByIndex initialized in setupData | |
neighboring = (a, b) -> | |
linkedByIndex[a.id + "," + b.id] or | |
linkedByIndex[b.id + "," + a.id] | |
# Removes nodes from input array | |
# based on current filter setting. | |
# Returns array of nodes | |
filterNodes = (allNodes) -> | |
filteredNodes = allNodes | |
if filter == "popular" or filter == "obscure" | |
playcounts = allNodes.map((d) -> d.playcount).sort(d3.ascending) | |
cutoff = d3.quantile(playcounts, 0.5) | |
filteredNodes = allNodes.filter (n) -> | |
if filter == "popular" | |
n.playcount > cutoff | |
else if filter == "obscure" | |
n.playcount <= cutoff | |
filteredNodes | |
# Returns array of artists sorted based on | |
# current sorting method. | |
sortedArtists = (nodes,links) -> | |
artists = [] | |
if sort == "links" | |
counts = {} | |
links.forEach (l) -> | |
counts[l.source.artist] ?= 0 | |
counts[l.source.artist] += 1 | |
counts[l.target.artist] ?= 0 | |
counts[l.target.artist] += 1 | |
# add any missing artists that dont have any links | |
nodes.forEach (n) -> | |
counts[n.artist] ?= 0 | |
# sort based on counts | |
artists = d3.entries(counts).sort (a,b) -> | |
b.value - a.value | |
# get just names | |
artists = artists.map (v) -> v.key | |
else | |
# sort artists by song count | |
counts = nodeCounts(nodes, "artist") | |
artists = d3.entries(counts).sort (a,b) -> | |
b.value - a.value | |
artists = artists.map (v) -> v.key | |
artists | |
updateCenters = (artists) -> | |
if layout == "radial" | |
groupCenters = RadialPlacement().center({"x":width/2, "y":height / 2 - 100}) | |
.radius(300).increment(18).keys(artists) | |
# Removes links from allLinks whose | |
# source or target is not present in curNodes | |
# Returns array of links | |
filterLinks = (allLinks, curNodes) -> | |
curNodes = mapNodes(curNodes) | |
allLinks.filter (l) -> | |
curNodes.get(l.source.id) and curNodes.get(l.target.id) | |
# enter/exit display for nodes | |
updateNodes = () -> | |
node = nodesG.selectAll("circle.node") | |
.data(curNodesData, (d) -> d.id) | |
node.enter().append("circle") | |
.attr("class", "node") | |
.attr("cx", (d) -> d.x) | |
.attr("cy", (d) -> d.y) | |
.attr("r", (d) -> d.radius) | |
.style("fill", (d) -> nodeColors(d.artist)) | |
.style("stroke", (d) -> strokeFor(d)) | |
.style("stroke-width", 1.0) | |
node.on("mouseover", showDetails) | |
.on("mouseout", hideDetails) | |
node.exit().remove() | |
# enter/exit display for links | |
updateLinks = () -> | |
link = linksG.selectAll("line.link") | |
.data(curLinksData, (d) -> "#{d.source.id}_#{d.target.id}") | |
link.enter().append("line") | |
.attr("class", "link") | |
.attr("stroke", "#ddd") | |
.attr("stroke-opacity", 0.8) | |
.attr("x1", (d) -> d.source.x) | |
.attr("y1", (d) -> d.source.y) | |
.attr("x2", (d) -> d.target.x) | |
.attr("y2", (d) -> d.target.y) | |
link.exit().remove() | |
# switches force to new layout parameters | |
setLayout = (newLayout) -> | |
layout = newLayout | |
if layout == "force" | |
force.on("tick", forceTick) | |
.charge(-200) | |
.linkDistance(50) | |
else if layout == "radial" | |
force.on("tick", radialTick) | |
.charge(charge) | |
# switches filter option to new filter | |
setFilter = (newFilter) -> | |
filter = newFilter | |
# switches sort option to new sort | |
setSort = (newSort) -> | |
sort = newSort | |
# tick function for force directed layout | |
forceTick = (e) -> | |
node | |
.attr("cx", (d) -> d.x) | |
.attr("cy", (d) -> d.y) | |
link | |
.attr("x1", (d) -> d.source.x) | |
.attr("y1", (d) -> d.source.y) | |
.attr("x2", (d) -> d.target.x) | |
.attr("y2", (d) -> d.target.y) | |
# tick function for radial layout | |
radialTick = (e) -> | |
node.each(moveToRadialLayout(e.alpha)) | |
node | |
.attr("cx", (d) -> d.x) | |
.attr("cy", (d) -> d.y) | |
if e.alpha < 0.03 | |
force.stop() | |
updateLinks() | |
# Adjusts x/y for each node to | |
# push them towards appropriate location. | |
# Uses alpha to dampen effect over time. | |
moveToRadialLayout = (alpha) -> | |
k = alpha * 0.1 | |
(d) -> | |
centerNode = groupCenters(d.artist) | |
d.x += (centerNode.x - d.x) * k | |
d.y += (centerNode.y - d.y) * k | |
# Helper function that returns stroke color for | |
# particular node. | |
strokeFor = (d) -> | |
d3.rgb(nodeColors(d.artist)).darker().toString() | |
# Mouseover tooltip function | |
showDetails = (d,i) -> | |
content = '<p class="main">' + d.name + '</span></p>' | |
content += '<hr class="tooltip-hr">' | |
content += '<p class="main">' + d.artist + '</span></p>' | |
tooltip.showTooltip(content,d3.event) | |
# higlight connected links | |
if link | |
link.attr("stroke", (l) -> | |
if l.source == d or l.target == d then "#555" else "#ddd" | |
) | |
.attr("stroke-opacity", (l) -> | |
if l.source == d or l.target == d then 1.0 else 0.5 | |
) | |
# link.each (l) -> | |
# if l.source == d or l.target == d | |
# d3.select(this).attr("stroke", "#555") | |
# highlight neighboring nodes | |
# watch out - don't mess with node if search is currently matching | |
node.style("stroke", (n) -> | |
if (n.searched or neighboring(d, n)) then "#555" else strokeFor(n)) | |
.style("stroke-width", (n) -> | |
if (n.searched or neighboring(d, n)) then 2.0 else 1.0) | |
# highlight the node being moused over | |
d3.select(this).style("stroke","black") | |
.style("stroke-width", 2.0) | |
# Mouseout function | |
hideDetails = (d,i) -> | |
tooltip.hideTooltip() | |
# watch out - don't mess with node if search is currently matching | |
node.style("stroke", (n) -> if !n.searched then strokeFor(n) else "#555") | |
.style("stroke-width", (n) -> if !n.searched then 1.0 else 2.0) | |
if link | |
link.attr("stroke", "#ddd") | |
.attr("stroke-opacity", 0.8) | |
# Final act of Network() function is to return the inner 'network()' function. | |
return network | |
# Activate selector button | |
activate = (group, link) -> | |
d3.selectAll("##{group} a").classed("active", false) | |
d3.select("##{group} ##{link}").classed("active", true) | |
$ -> | |
myNetwork = Network() | |
d3.selectAll("#layouts a").on "click", (d) -> | |
newLayout = d3.select(this).attr("id") | |
activate("layouts", newLayout) | |
myNetwork.toggleLayout(newLayout) | |
d3.selectAll("#filters a").on "click", (d) -> | |
newFilter = d3.select(this).attr("id") | |
activate("filters", newFilter) | |
myNetwork.toggleFilter(newFilter) | |
d3.selectAll("#sorts a").on "click", (d) -> | |
newSort = d3.select(this).attr("id") | |
activate("sorts", newSort) | |
myNetwork.toggleSort(newSort) | |
$("#song_select").on "change", (e) -> | |
songFile = $(this).val() | |
d3.json "data/#{songFile}", (json) -> | |
myNetwork.updateData(json) | |
$("#search").keyup () -> | |
searchTerm = $(this).val() | |
myNetwork.updateSearch(searchTerm) | |
d3.json "data/call_me_al.json", (json) -> | |
myNetwork("#vis", json) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment