diff options
author | Peter Wu <peter@lekensteyn.nl> | 2014-05-21 15:13:16 +0200 |
---|---|---|
committer | Peter Wu <peter@lekensteyn.nl> | 2014-05-21 15:13:16 +0200 |
commit | 0334aa5e0051b59ce4050fd306b26119466e2991 (patch) | |
tree | 656de6f8ab686b3340abdd4dd86d196048d47ff3 /bubble.js | |
parent | 0d2a9c9fd6c16ced9c684354690cc62dfd3d58f0 (diff) | |
download | d3viz-0334aa5e0051b59ce4050fd306b26119466e2991.tar.gz |
Move scripts to subdir
Our scripts end up in js/, third-party libraries end up in lib/.
Diffstat (limited to 'bubble.js')
-rw-r--r-- | bubble.js | 407 |
1 files changed, 0 insertions, 407 deletions
diff --git a/bubble.js b/bubble.js deleted file mode 100644 index c37426e..0000000 --- a/bubble.js +++ /dev/null @@ -1,407 +0,0 @@ -/* jshint devel:true, browser:true */ -/* global d3, collisionTick, preprocess */ -'use strict'; - -/* settings */ -// whether to enable expensive features -var I_GOT_MONEY = 1; -// whether to ignore lonely users -var KILL_LONERS = true; -// if positive, then users with fewer than this number of tweets will be ignored -var USER_MIN_TWEET_COUNT = 2; -// if true, enable sticky nodes unless Ctrl is held. If false, stick only if -// ctrl is held (the inverse). -var STICKY_DEFAULT = true; - - -/* functions */ - -function getEdgesNodes(nodesCsv, edgesCsv, completeCallback) { - var results = {}; - var checker = function (what) { - return function (error, rows) { - if (error) { - console.log('Cannot complete requests!', error); - return; - } - results[what] = rows; - // all data available, call callback function - if (results.nodes && results.edges) { - completeCallback(results); - } - }; - }; - // fetch both files, asynchronously. - nodesCsv.get(checker('nodes')); - edgesCsv.get(checker('edges')); -} - -function initForce(width, height) { - console.log('Initing force with dimensions', width, height); - var force = d3.layout.force() - .charge(-10) // default -30 - //.linkDistance(20) // default 20 - .size([width, height]); - - // sticky positions after dragging if CTRL is held down - function sticky() { - return STICKY_DEFAULT ^ d3.event.sourceEvent.ctrlKey; - } - force.drag() - .on('dragstart', function (d) { - // if ctrl is pressed, sticky... - if (sticky()) { - d.fixed = true; - } - }) - .on('dragend', function (d) { - // release if not sticky, otherwise keep sticky - d.fixed = sticky(); - }); - - return force; -} - -function initZoom(svg) { - var zoomBehavior = d3.behavior.zoom() - .scaleExtent([0.1, 10]) // min/max zoom - .on('zoomstart', zoomStart) - .on('zoom', zoom); - var container = svg - .call(zoomBehavior) - .on('dblclick.zoom', null) - // the zoom transformations apply here. - .append('g'); - var zoomInfo = d3.select('#infobox .zoom-level'); - zoomInfo.text('100'); - - var oldTranslation, oldScale, panAllowed = false; - // pan is only allowed if the source is not an element - function zoomStart() { - var ev = d3.event.sourceEvent; - panAllowed = ev.target === svg.node(); - oldTranslation = zoomBehavior.translate(); - oldScale = zoomBehavior.scale(); - } - function zoom() { - // disallow pan only if not zooming - if (!panAllowed && d3.event.scale === oldScale) { - d3.event.translate = oldTranslation; - zoomBehavior.translate(oldTranslation); - } - container.attr('transform', - 'translate(' + d3.event.translate + ')' + - 'scale(' + d3.event.scale + ')'); - // save old translation for later restore if disabled. - oldTranslation = d3.event.translate; - oldScale = d3.event.scale; - zoomInfo.text(Math.floor(100 * oldScale)); - } - return container; -} - -// contents holds the actual nodes and edges and exists to allow pan/zoom -var contents; -// force layout configuration -var force; - -function initSvg() { - var svg = d3.select('#map').append('svg'); - contents = initZoom(svg); - - svg.append('defs') - // definition for an arrow head. Note: affected by stroke width of the path - .append('marker') - .attr('id', 'arrow') - .attr('markerWidth', 6) - .attr('markerHeight', 6) - .attr('orient', 'auto') - // make dimensions relative to this box instead of absolute pixels - // NOTE: capital 'B'!!! view *B* ox!!! Spent two hours on that... - // viewBox="x y width height" - .attr('viewBox', '-10 -5 10 10') - .attr('markerUnits', 'userSpaceOnUse') - .append('path') - .attr('class', 'arrow-head') - // M x,y - absolute moveTo - // L x,y - relative lineTo - // arrow head '>' as in: (edge) --- '>' o (node) - .attr('d', 'M-10,-5 L0,0 L-10,5'); - - // default the space of the force layout to the svg canvas dimension. If you - // want to allow pan / zoom with more data out of view, multiply this by - // some factor. - var dim = d3.select('#map').node().getBoundingClientRect(); - // begin simulation - force = initForce(dim.width, dim.height); -} - -function processData(data) { - var infoPane = d3.select('#infobox'); - - preprocess(data, { - kill_loners: KILL_LONERS, - minTweetCount: USER_MIN_TWEET_COUNT - }); - infoPane.select('.node-count').text(data.nodes.length); - infoPane.select('.edge-count').text(data.edges.length); - force.nodes(data.nodes) - .links(data.edges) - .start(); - - // element 'g' groups SVG elements, useful to apply a single transf. to all - /* edges */ - var link = contents.append('g').selectAll('path') - .data(force.links()) - .enter().append('path') - .attr('class', 'link') - .attr('marker-end', 'url(#arrow)') - .style('stroke-width', function (d) { - return Math.sqrt(d.value); - }); - link.append('title') - .text(function (d) { - return d.value; - }); - - /* nodes */ - var node = contents.append('g').selectAll('circle') - .data(force.nodes()) - .enter().append('circle') - .attr('class', function (d) { - return 'node ' + (d.isSpam ? 'spam' : 'ham'); - }) - .attr('r', function (d) { - return d.radius; - }) - .call(force.drag); - node.append('title') - .text(function (d) { - return d.name; - }); - - var infoBox = d3.select('#infobox'); - - // double-click locks selection of a user on hover - var selectedNode = null; - node.on('dblclick', function (d) { - if (selectedNode === d) { - // no update needed, unmark for dynamic update - selectedNode = null; - d3.select(this).classed('selected', false); - } else { - selectedNode = d; - updateInfobox(d, this); - } - console.log(selectedNode, d); - infoBox.classed('user-locked', selectedNode === d); - }) - .on('mouseover', function (d) { - console.log('Hovering over', d); - // only update on hover if no node is selected - if (selectedNode === null) { - updateInfobox(d, this); - } - // always update neighboring edges - contents.selectAll('.link') - .classed('neighbor', function (edge) { - return edge.source === d || edge.target === d; - }); - // ... and also update neighboring nodes - contents.selectAll('.node') - .classed('neighbor', function (node) { - return node.relatedTo.indexOf(d.index) >= 0 || - node.relatedFrom.indexOf(d.index) >= 0; - }); - }) - .on('mouseout', function (d) { - contents.selectAll('.neighbor') - .classed('neighbor', false); - }); - - - // info panel for each user node - var userInfo = infoPane.select('.user-info'); - // highlight selected node and update the user info in infobox - function updateInfobox(d, nodeElm) { - // unselect other nodes, mark self as selected. - contents.select('.node.selected').classed('selected', false); - d3.select(nodeElm).classed('selected', true); - - // display user block - userInfo.style('display', 'block'); - - userInfo.select('.name a').remove(); - userInfo.select('.name').append('a') - .attr('target', '_blank') - .attr('href', 'https://twitter.com/' + d.name) - .text(d.name); - userInfo.select('.userid').text(d.group); - userInfo.select('.tweet-count') - .text(d.tweetCount); - userInfo.select('.spam-status') - .text(d.isSpam ? 'SPAM' : 'ham'); - - var nodes = { - 'from': force.nodes().filter(function (edge) { - return edge.relatedTo.indexOf(d.index) >= 0; - }), - 'to': force.nodes().filter(function (edge) { - return edge.relatedFrom.indexOf(d.index) >= 0; - }) - }; - Object.keys(nodes).forEach(function (dir) { - var related = nodes[dir]; - related.sort(function sort_by_name(a, b) { - return a.name.localeCompare(b.name); - }); - userInfo.select('.relations-' + dir + '-count') - .text(related.length); - var relations = userInfo.select('.relations-' + dir) - .selectAll('li') - .data(related, function key_func_links(d) { - // unique keys to group by node (index) - return d.index; - }); - relations.enter().append('li') - .text(function (d) { - return dir + ' ' + d.name; - }); - relations.exit().remove(); - }); - } - - force.on('tick', function() { - // based on http://bl.ocks.org/mbostock/1153292 - link.attr('d', function force_tick(d) { - // https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#Arcs - // A rx ry x-axis-rotation large-arc-flag sweep-flag x y - // a rx ry x-axis-rotation large-arc-flag sweep-flag dx dy - // rx and ry are the offsets from the center between current - // position and (last-x, last-y) - // large-arc-flag 0 means draw the arc on the angle < 180 degree - var dx = d.target.x - d.source.x, - dy = d.target.y - d.source.y, - r = Math.sqrt(dx * dx + dy * dy), - // curve radius is based on two circles, with their radius being - // an offset from the center between the start and end point - cr = r; - // remove the radius such that the arrow head just hits the node - var ratio = (r - d.target.radius) / r; - dx *= ratio; - dy *= ratio; - return 'M' + d.source.x + ',' + d.source.y + ' ' + - 'a' + cr + ',' + cr + ' 0 0 0 ' + - dx + ',' + dy; - }); - - // warning: expensive! - if (I_GOT_MONEY) { - collisionTick(force.nodes()); - } - - node.attr('cx', function(d) { return d.x; }) - .attr('cy', function(d) { return d.y; }); - // PROFILE - ticks++; - }); -} - -// initialize information panel -(function () { - var infoPane = d3.select('#infobox'); - infoPane.select('.draggable') - .on('mousedown', function () { - infoPane.call(d3.behavior.drag().on('drag', dragger)); - }) - .on('mouseup', function () { - infoPane.on('mousedown.drag', null); - }); - - function dragger() { - var changes = { - 'left': d3.event.dx, - 'top': d3.event.dy - }; - // add the differences to the old positions - for (var name in changes) { - var newValue = parseInt(infoPane.style(name)); - newValue += changes[name]; - infoPane.style(name, newValue + 'px'); - } - } -}()); - -function run() { - /* fetch CSV files and render the result */ - getEdgesNodes( - // userid,name,tweetCount - d3.csv('users.csv') - .row(function (d) { - return { - name: d.name, - group: +d.userid, - tweetCount: d.tweetcount, - radius: Math.sqrt(d.tweetcount), - isSpam: +d.isspam, - // indices of nodes that link to this one - relatedTo: [], relatedFrom: [], - }; - }), - // source,target,value - d3.csv('links.csv') - .row(function (d) { - return { - source: +d.source, - target: +d.target, - value: +d.value - }; - }), - processData /* callback function when data is ready */ - ); -} - -// initialize SVG element and force -initSvg(); - -// Set PROFILE=1 to enable profiling when using the button. -var ticks = 0, PROFILE = /\bprofile\b/.test(location.search); -if (/\bno-auto\b/.test(location.search)) { - // advanced stuff here: profiling! - d3.select('body').append('button') - .style('position', 'absolute') - .style('z-index', 2) - .style('font-size', '20em') - .text('RUN') - .on('click', function () { - d3.select(this).remove(); - if (PROFILE) { - ticks = 0; - console.time('Run'); - console.timeline('Run'); - console.profile('Run'); - } - run(); - if (PROFILE) { - setTimeout(function () { - console.log('Ticks:', ticks); - console.timelineEnd('Run'); - console.profileEnd('Run'); - console.timeEnd('Run'); - }, 30000); - } - }); -/* notes -X 30 ticks, firefox -X 66 ticks, chromium -X disabled edge positioning, 84 ticks, chromium - -70 ticks in 31.3s, chromium (disabled title elements) [69,30.3] -73 ticks in 30.8s, chromium (title elements enabled) [68,30.4] -62 ticks in 30.2s, chromium (removed 2x 'g' elements) [60,31.8] - -55 ticks in 32.9s, chromium (with collision check) [54,30.4] - */ -} else { - run(); -} |