Using Angular & D3 force layout as a chat/forum interface

317 views
Skip to first unread message

j...@thedebatecompany.com

unread,
Feb 24, 2016, 8:41:46 PM2/24/16
to d3-js
Greetings, all. 

I'm re-building a structured chat product in Angular and Firebase, using D3. I'm looking for advice on how to structure a large data-driven forum app using Angularfire & D3.

I threw together a force-directed chat program using the Angularfire-seed package at markvtestbed.firebaseapp.com/#/chat .

My current Angularfire demo looks like this: dialectica.firebaseapp.com

I'm a relative newbie at Angular, D3 and for that matter Javascript; I've had to pick up a lot recently. Nonetheless in the past two weeks I've been focusing on D3 and I've been very impressed - it's like a data-fied Jquery.

The use case I'm serving is a graphical notation format for debate called a debate flow. Formally, it resembles acyclic tree graph allowing for multiple parents. A column is a speech; a row is a line of argumentation. It's easier to see in operation than it is to explain; this or this are probably closer to what it's supposed to look like than the demo.
Force directed layout solves big structural problems for debate flows.

A large portion of the basic controller logic I need is already there in my current demo - I have code for loading, adding and removing data and adding it to an Angular scope. Now that I've proven I can get data out of a Firebase and push it to a D3 array, my remaining task is to put a pretty directive face on all that controller logic.

I've looked at a few ways of doing the Angular D3 thing. The way it's running right now is what the D3 on Angular book advises - essentially, you're looking at D3 in the link function of a directive, fed by a wrapper object around a Firebase ref. 

The entirety of the force layout logic, including the tick function, is in a scope.$watch. The force layout is reinvoked when the 'messages' variable changes.

The questions I have are:

1. Are there any good solutions for "saving" from within D3 visualizations? I want to implement the advanced force directed graph editing functions in this block: http://bl.ocks.org/rkirsling/5001347. Specifically, I want my users to be able to add links (connections between statements), nodes (unconnected statements), linked nodes (directed statements) and arrays of links and nodes (preset statement chains). I think this will require controllers in my directive containing functions for $add (Firebase's version of push) and $remove events?

2. Can I ng-include a template and its isolate scope into an svg foreignObject? I'm gonna tentatively say 'no' - the research I've done so far suggests that the foreignObject thing is so limited that it's barely worth actually investigating. 

3. Are there any combinations of Sankey charts and force-directed graphs? Assiduous Googling has not turned any up thus far. In an ideal world, I'd be able to use the neat collision-detection layout algorithm in the sankey.js along with a force-layout to achieve a dynamic interface.

Appreciate the thought. 

For ref, the entirety of my controller, factory and directive code is pasted below for ref (replace "chat.js" in the AngularFire seed and add a <graphy messages="messages"></graphy> tag to the chat.html).

(function (angular) {
"use strict";

var app = angular.module('myApp.chat', ['ngRoute', 'firebase.utils', 'firebase']);

app.controller('ChatCtrl', ['$scope', 'messageList', function($scope, messageList) {
$scope.messages = messageList;
$scope.addMessage = function(newMessage) {
if( newMessage ) {
$scope.messages.$add({text: newMessage}).then(function(ref) {
var id = ref.key();
console.log("added record with id " + id);
});
}
};
}]);

app.factory('messageList', ['fbutil', '$firebaseArray', function(fbutil, $firebaseArray) {
var ref = fbutil.ref('messages');
return $firebaseArray(ref);
}]);

app.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/chat', {
templateUrl: 'chat/chat.html',
controller: 'ChatCtrl'
});
}]);

app.directive('graphy', function(){

function link(scope, el, attr){
var messages = scope.messages; // necessary to bring in scope variable

// original working append('p).text(accessor) function
/* d3.select('body').selectAll('p')
.data(messages)
.enter()
.append('p')
.append('text')
.text(function (d) {
return d.text;
});
*/
var w = 800, h = 600;

var color = d3.scale.category20();

var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);

var force = d3.layout.force()
.charge(-120)
.linkDistance(80)
.size([w, h]);

// watch the last 10 caused this to fire repeatedly and seize up the sim
scope.$watch('messages', function(messages){

force.nodes(messages)
//.links(graph.links)
.start();

//Create all the line svgs but without locations yet
/* var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function (d) {
return Math.sqrt(d.value);
});
*/
//Do the same with the circles for the nodes - no
//Changed
var node = svg.selectAll(".node")
.data(messages)
.enter().append("g")
.attr("class", "node")
.call(force.drag);

node.append("circle")
.attr("r", 8)
.attr("fill",function(d,i){ return color(i) });

node.append("text")
.attr("dx", 10)
.attr("dy", ".35em")
.attr('stroke', 'black')
.text(function(d) { return d.text });
//End changed


//Now we are giving the SVGs co-ordinates - the force layout is generating the co-ordinates which this code is using to update the attributes of the SVG elements
force.on("tick", function () {
/*link.attr("x1", function (d) {
return d.source.x;
})
.attr("y1", function (d) {
return d.source.y;
})
.attr("x2", function (d) {
return d.target.x;
})
.attr("y2", function (d) {
return d.target.y;
});
*/
//Changed

d3.selectAll("circle").attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});

d3.selectAll("text").attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
});

//End Changed

});


// original working scope.$watch function
/*
d3.select('body').append('p')
.data(messages)
.enter()
.append('p')
.append('text')
.text(function (d) {
return d.text;
});
*/
}, true);

}
return {
link: link, // link function necessary, compile is too early
restrict: 'E',
// replace: 'false', // this is largely a housekeeping matter doesn't affect function
scope: { messages: '=' } // critical - messages: '=' passes in data from scope
}
});

})(angular);
Reply all
Reply to author
Forward
0 new messages