summaryrefslogtreecommitdiff
path: root/bubble.js
diff options
context:
space:
mode:
authorPeter Wu <peter@lekensteyn.nl>2014-05-21 15:13:16 +0200
committerPeter Wu <peter@lekensteyn.nl>2014-05-21 15:13:16 +0200
commit0334aa5e0051b59ce4050fd306b26119466e2991 (patch)
tree656de6f8ab686b3340abdd4dd86d196048d47ff3 /bubble.js
parent0d2a9c9fd6c16ced9c684354690cc62dfd3d58f0 (diff)
downloadd3viz-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.js407
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();
-}