Created
February 2, 2013 14:06
-
-
Save moricard/4697485 to your computer and use it in GitHub Desktop.
Visual Hacker News with text inside a force-graph node in d3.js
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
var vishna = (function() { | |
var urls = { //API urls | |
news : "http://hndroidapi.appspot.com/news/format/json/page/?appid=vishna&callback=?", | |
ask : "http://hndroidapi.appspot.com/ask/format/json/page/?appid=vishna&callback=?", | |
newest: "http://hndroidapi.appspot.com/newest/format/json/page/?appid=vishna&callback=?", | |
best : "http://hndroidapi.appspot.com/best/format/json/page/?appid=vishna&callback=?" | |
}, | |
thread = /^(item[?]id[=][0-9]+)/, //regexp for HN thread posts | |
w = Math.max( $(window).width() * 0.85, 960 ), //width | |
h = Math.max( $(window).height() * 0.85, 600 ), //height | |
m = 20, //margin | |
center = { //gravity center | |
x : ( w - m ) / 2, | |
y : ( h - m ) / 2 | |
}, | |
posts, //content | |
next, //next page | |
o, //opacity scale | |
r, //radius scale | |
z, //color scale | |
g, //gravity scale | |
t = { //time factors | |
minutes : 1, | |
hour : 60, | |
hours : 60, | |
day : 1440, | |
days : 1440 | |
}, | |
gravity = -0.01,//gravity constants | |
damper = 0.2, | |
friction = 0.9, | |
force = d3 //gravity engine | |
.layout | |
.force() | |
.size([ w - m, | |
h - m ]), | |
svg = d3 //container | |
.select("body article") | |
.append("svg") | |
.attr("height", h + "px") | |
.attr("width", w + "px"), | |
circles, //data representation | |
tooltip = CustomTooltip( "posts_tooltip", 240 ); | |
function init( category ) { | |
if ( urls[ category ] ) { | |
load( urls[ category ], function() { | |
launch(); | |
legend(); | |
}); | |
} | |
} | |
function update( category ) { | |
if ( urls[ category ] ) { | |
load( urls[ category ], function() { | |
circles | |
.transition() | |
.duration( 750 ) | |
.attr("r", function(d) { return r(d) + 100; }) | |
.delay( 250 ) | |
.style("opacity", function(d) { return 0; }) | |
.remove(); | |
launch(); | |
}); | |
} | |
} | |
function load( url, callback ){ | |
$.getJSON(url, function( data ) { | |
posts = data.items; | |
next = posts.pop(); | |
posts.map( function(d) { | |
var comments = parseInt( d.comments ), | |
score = parseInt( d.score ), | |
time = d.time.split(" "); | |
d.comments = comments ? comments : 0; | |
d.score = score ? score : 0; | |
d.time = time[0] * t[ time[1] ]; // number * factor | |
if ( thread.test(d.url) ) { | |
d.url = "http://news.ycombinator.com/" + d.url; | |
} | |
return d; | |
}); | |
// Defining the scales | |
r = d3.scale.linear() | |
.domain([ d3.min(posts, function(d) { return d.score; }), | |
d3.max(posts, function(d) { return d.score; }) ]) | |
.range([ 10, 130 ]) | |
.clamp(true); | |
z = d3.scale.linear() | |
.domain([ d3.min(posts, function(d) { return d.comments; }), | |
d3.max(posts, function(d) { return d.comments; }) ]) | |
.range([ '#ff7f0e', '#ff7f0e' ]); | |
o = d3.scale.linear() | |
.domain([ d3.min(posts, function(d) { return d.time; }), | |
d3.max(posts, function(d) { return d.time; }) ]) | |
.range([ 1, 0.2 ]); | |
g = function(d) { return -r(d) * r(d) / 2.5; }; | |
callback(); | |
}); | |
} | |
function launch() { | |
force | |
.nodes( posts ); | |
circles = svg | |
.append("g") | |
.attr("id", "circles") | |
.selectAll("a") | |
.data(force.nodes()); | |
// Init all circles at random places on the canvas | |
force.nodes().forEach( function(d, i) { | |
d.x = Math.random() * w; | |
d.y = Math.random() * h; | |
}); | |
var node = circles | |
.enter() | |
.append("g") | |
.append("a") | |
.attr("xlink:href", function(d) { return d.url; }) | |
.append("circle") | |
.attr("r", 0) | |
.attr("cx", function(d) { return d.x; }) | |
.attr("cy", function(d) { return d.y; }) | |
.attr("fill", function(d) { return z( d.comments ); }) | |
.attr("stroke-width", 2) | |
.attr("stroke", function(d) { return d3.rgb(z( d.comments )).darker(); }) | |
.attr("id", function(d) { return "post_#" + d.item_id; }) | |
.attr("title", function(d) { return d.title; }) | |
.style("opacity", function(d) { return o( d.time ); }) | |
.on("mouseover", function(d, i) { force.resume(); highlight( d, i, this ); }) | |
.on("mouseout", function(d, i) { downlight( d, i, this ); }); | |
// Bind data to <g>...</g> containers for the text. | |
var text = svg.append("svg:g").selectAll("g") | |
.data(force.nodes()) | |
.enter().append("svg:g"); | |
// Add actual text inside bound elements. | |
text.append("svg:text") | |
.attr("x", 8) | |
.attr("y", ".31em") | |
.text(function(d) { return d.title; }); | |
d3.selectAll("circle") | |
.transition() | |
.delay(function(d, i) { return i * 10; }) | |
.duration( 1000 ) | |
.attr("r", function(d) { return r( d.score ); }); | |
loadGravity( moveCenter ); | |
//Loads gravity | |
function loadGravity( generator ) { | |
force | |
.gravity(gravity) | |
.charge( function(d) { return g( d.score ); }) | |
.friction(friction) | |
.on("tick", function(e) { | |
generator(e.alpha); | |
node | |
.attr("cx", function(d) { return d.x; }) | |
.attr("cy", function(d) { return d.y; }); | |
//We also want the gravity engine to update text position. | |
text | |
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }); | |
}).start(); | |
} | |
// Generates a gravitational point in the middle | |
function moveCenter( alpha ) { | |
force.nodes().forEach(function(d) { | |
d.x = d.x + (center.x - d.x) * (damper + 0.02) * alpha; | |
d.y = d.y + (center.y - d.y) * (damper + 0.02) * alpha; | |
}); | |
} | |
} | |
function legend() { | |
var linearGradient = svg.append("defs") | |
.append("linearGradient") | |
.attr("id", "legendGradient") | |
.attr("x1", "0%") | |
.attr("y1", "0%") | |
.attr("x2", "0%") | |
.attr("y2", "100%") | |
.attr("spreadMethod", "pad"); | |
linearGradient | |
.append("stop") | |
.attr("offset", "0%") | |
.attr("stop-color", "#ff7f0c") | |
.attr("stop-opacity", "0.1"); | |
linearGradient | |
.append("stop") | |
.attr("offset", "100%") | |
.attr("stop-color", "#ff7f0c") | |
.attr("stop-opacity", "1"); | |
var legend = svg.append("g") | |
.attr("id", "legend"); | |
legend | |
.append("rect") | |
.attr("x", "20") | |
.attr("y", "20") | |
.attr("width", "20") | |
.attr("height", "200") | |
.attr("style", "fill:url(#legendGradient);"); | |
legend | |
.append("text") | |
.attr("x", 45) | |
.attr("y", 30) | |
.text("Oldest"); | |
legend | |
.append("text") | |
.attr("x", 45) | |
.attr("y", 220) | |
.text("Newest"); | |
} | |
function highlight( data, i, element ) { | |
d3.select( element ).attr( "stroke", "black" ); | |
var description = data.description.split("|"), | |
content = '<span class=\"title\"><a href=\"' + data.url + '\">' + data.title + '</a></span><br/>' + | |
description[0] + "<br/>" + | |
'<a href=\"http://news.ycombinator.com/item?id='+ data.item_id +'\">' + description[1] + '</a>'; | |
tooltip.showTooltip(content, d3.event); | |
} | |
function downlight( data, i, element ) { | |
d3.select(element).attr("stroke", function(d) { return d3.rgb( z( d.comments )).darker(); }); | |
} | |
//Register category selectors | |
$("a.category").on("click", function(e) { update( $(this).attr("value") ); }); | |
return { | |
categories : ["news", "best", "ask", "newest"], | |
init : init, | |
update : update | |
}; | |
})(); | |
vishna.init( window.location.href.split("#")[1] ? window.location.href.split("#")[1] : "news"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment