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'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);