Try as I might, I can't seem to get meteor and d3 to play nicely when it comes to a directional force diagram. Since I'm new to both Meteor and d3, I'm not sure where my failing is..
What I'm trying to do is to get (re)create just the following sample graph, but one which is reactive to the mongo data source. Any help at all would be most appreciated!
// get the data
d3.csv("force.csv", function(error, links) {
var nodes = {};
// Compute the distinct nodes from the links.
links.forEach(function(link) {
link.source = nodes[link.source] ||
(nodes[link.source] = {name: link.source});
link.target = nodes[link.target] ||
(nodes[link.target] = {name: link.target});
link.value = +link.value;
});
var width = 960,
height = 500;
var force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(60)
.charge(-300)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// build the arrow.
svg.append("svg:defs").selectAll("marker")
.data(["end"]) // Different link/path types can be defined here
.enter().append("svg:marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", -1.5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
// add the links and the arrows
var path = svg.append("svg:g").selectAll("path")
.data(force.links())
.enter().append("svg:path")
// .attr("class", function(d) { return "link " + d.type; })
.attr("class", "link")
.attr("marker-end", "url(#end)");
// define the nodes
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.call(force.drag);
// add the nodes
node.append("circle")
.attr("r", 5);
// add the text
node.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
// add the curvy lines
function tick() {
path.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" +
d.source.x + "," +
d.source.y + "A" +
dr + "," + dr + " 0 0,1 " +
d.target.x + "," +
d.target.y;
});
node
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"; });
}
});
</script>
force.csv:
source,target,value
Harry,Sally,1.2
Harry,Mario,1.3
Sarah,Alice,0.2
Eveie,Alice,0.5
Peter,Alice,1.6
Mario,Alice,0.4
James,Alice,0.6
Harry,Carol,0.7
Harry,Nicky,0.8
Bobby,Frank,0.8
Alice,Mario,0.7
Harry,Lynne,0.5
Sarah,James,1.9
Roger,James,1.1
Maddy,James,0.3
Sonny,Roger,0.5
James,Roger,1.5
Alice,Peter,1.1
Johan,Peter,1.6
Alice,Eveie,0.5
Harry,Eveie,0.1
Eveie,Harry,2.0
Henry,Mikey,0.4
Elric,Mikey,0.6
James,Sarah,1.5
Alice,Sarah,0.6
James,Maddy,0.5
Peter,Johan,0.7
Below is my attempt using the parties example as a starting point:
parties.html:
<head>
<title>All Tomorrow's Parties</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
{{> page}}
</body>
<template name="page">
<div class="span6">
{{> map}}
</div>
{{>updateNetwork}}
</template>
<template name="map">
</template>
<template name="details">
<div class="details">
</div>
</template>
<template name="updateNetwork">
<div align="right">
<br/>
<input id="addNodeBtn" type="button" value = "Add Some Nodes">
</div>
<div id="svgDiv">
</div>
</template>
client.js:
Template.map.rendered = function () {
var self = this;
self.node = self.find("svg");
if (! self.handle) {
self.handle = Deps.autorun(function () {
if (!Session.equals("alreadyRun", true))
{
_links = Links.find({}).fetch();
// The nodes array just contains name information.
// Sample values:
// nodes["Jack"] = {name: "Jack"}
// nodes["Jill"] = {name: "Jill"}
var nodes = {};
// Compute the distinct nodes from the links.
// Go through all links, and update the total weight of the edge as well for each link,
// within the links object.
//
_links.forEach(function(link) {
link.source = nodes[link.source] ||
(nodes[link.source] = {name: link.source});
link.target = nodes[link.target] ||
(nodes[link.target] = {name: link.target});
link.value = +link.value;
});
console.log("links: " + JSON.stringify(_links));
var links = _links;
// At this point, the "links" object cotains info on the entire network, and
// is prepared to be rendered
var width = 300,
height = 200;
force = d3.layout.force();
force
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(60)
.charge(-300)
.on("tick", tick)
.start();
Session.set("forceVariable", force);
Session.set("nodeArray", nodes);
//Session.set("linkArray", d3.layout.force().links());
Session.set("linkArray", links);
// Set the range
var v = d3.scale.linear().range([0, 100]);
// Scale the range of the data
v.domain([0, d3.max(links, function(d) { return d.value; })]);
// asign a type per value to encode opacity
links.forEach(function(link) {
if (v(link.value) <= 25) {
link.type = "twofive";
} else if (v(link.value) <= 50 && v(link.value) > 25) {
link.type = "fivezero";
} else if (v(link.value) <= 75 && v(link.value) > 50) {
link.type = "sevenfive";
} else if (v(link.value) <= 100 && v(link.value) > 75) {
link.type = "onezerozero";
}
});
var svg = d3.select("body").select("#svgDiv").append("svg")
.attr("width", width)
.attr("height", height);
//Session.set("currentSVG", svg);
// build the arrow.
svg.append("svg:defs").selectAll("marker")
.data(["end"]) // Different link/path types can be defined here
.enter().append("svg:marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", -1.5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
// add the links and the arrows
var path = svg.append("svg:g").selectAll("path")
.data(force.links())
.enter().append("svg:path")
.attr("class", function(d) { return "link " + d.type; })
.attr("marker-end", "url(#end)");
console.log("At this point, force.nodes() is..." + JSON.stringify(force.nodes()));
// define the nodes
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.on("click", click)
.on("dblclick", dblclick)
.call(force.drag);
console.log("And now, at this point, force.nodes() is..." + JSON.stringify(force.nodes()));
// add the nodes
node.append("circle")
.attr("r", 5);
// add the text
node.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
} // end if (Session.equals("alreadyRun", true))
else
{
console.log("nodeArrray: " + JSON.stringify(Session.get("nodeArray")));
console.log("linkArray:" + Session.get("linkArray"));
//console.log("currentSVG: " + JSON.stringify(Session.get("currentSVG")));
//console.log("currentNodes: " + JSON.stringify(Session.get("currentSVG").nodes()));
// Session.set("nodeArray", nodes);
// Session.set("linkArray", d3.layout.force().links());
var svg = d3.select("body").select("#svgDiv").select("svg");
//var svg = Session.get("currentSVG");
// Try to access the force.nodes() object
//
//console.log("force.nodes: "+ JSON.stringify(svg.selectAll(".node").data(Session.get();)));
// build the arrow.
svg.append("svg:defs").selectAll("marker")
.data(["end"]) // Different link/path types can be defined here
.enter().append("svg:marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", -1.5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
newNodes = Session.get("nodeArray");
newLinks = Session.get("linkArray");
newLinks.push({source:"Kobeeley" , target:"Cluff", value:0.8});
newLinks.forEach(function(link) {
link.source = newNodes[link.source] ||
(newNodes[link.source] = {name: link.source});
link.target = newNodes[link.target] ||
(newNodes[link.target] = {name: link.target});
link.value = +link.value;
});
console.log("newNodes is now...." + JSON.stringify(newNodes));
//var force = d3.layout.force();
// add the links and the arrows
var path = svg.append("svg:g").selectAll("path")
.data(newLinks)
.enter().append("svg:path")
.attr("class", function(d) { return "link " + d.type; })
.attr("marker-end", "url(#end)");
// define the nodes
var node = svg.selectAll(".node")
.data(d3.values(newNodes))
.enter().append("g")
.attr("class", "node")
.call(force.drag);
// .on("click", click)
// .on("dblclick", dblclick)
// .call(force.drag);
// add the nodes
node.append("circle")
.attr("r", 5);
// add the text
node.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
var width = 960,
height = 500;
force.start();
/*
force
.nodes(d3.values(newNodes))
.links(newLinks)
.size([width, height])
.linkDistance(60)
.charge(-300)
.on("tick", tick)
.start();
*/
/*
var force = d3.layout.force()
.nodes(d3.values(newNodes))
.links(newLinks)
.size([width, height])
.linkDistance(60)
.charge(-300)
.on("tick", tick)
.start();
*/
} // end of if..else firstrun
// add the curvy lines
function tick() {
path.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" +
d.source.x + "," +
d.source.y + "A" +
dr + "," + dr + " 0 0,1 " +
d.target.x + "," +
d.target.y;
});
node
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"; });
}
// action to take on mouse click
function click() {
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 22)
.style("fill", "steelblue")
.style("stroke", "lightsteelblue")
.style("stroke-width", ".5px")
.style("font", "20px sans-serif");
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 16)
.style("fill", "lightsteelblue");
}
// action to take on mouse double click
function dblclick() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 6)
.style("fill", "#ccc");
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 12)
.style("stroke", "none")
.style("fill", "black")
.style("stroke", "none")
.style("font", "10px sans-serif");
}
}); // End of deps.autorun function
}// end of (! self.handle)
};// end of Template.map.rendered
Template.updateNetwork.events({
'click #addNodeBtn': function (event, template) {
//if (! Meteor.userId()) // must be logged in to create events
// return;
//var coords = coordsRelativeToElement(event.currentTarget, event);
//openCreateDialog(coords.x / 500, coords.y / 500);
var _linksToAdd = [
{source: "Sully", target: "Roy", value: 0.2 },
{source: "Roy", target: "Jack", value: 0.8},
{source:"Juhuff", target: "Cluff", value: 0.9}
];
Session.set("alreadyRun", true);
// Update the collection
//
_linksToAdd.forEach(function (link) {
Links.insert(link);
})
}
});
model.js:
Links = new Meteor.Collection("links");
Links.allow({
insert: function (userId, doc) {
return true; // no cowboy inserts -- use createParty method
},
update: function (userId, doc, fields, modifier) {
// if (userId !== party.owner)
// return true; // not the owner
// var allowed = ["title", "description", "x", "y"];
// if (_.difference(fields, allowed).length)
// return true; // tried to write to forbidden field
// A good improvement would be to validate the type of the new
// value of the field (and if a string, the length.) In the
// future Meteor will have a schema system to makes that easier.
return true;
},
remove: function (userId, doc) {
// You can only remove parties that you created and nobody is going to.
// return party.owner === userId && attending(party) === 0;
return true;
}
});