Skip to content

Instantly share code, notes, and snippets.

@cool-Blue
Last active August 29, 2015 14:25
Show Gist options
  • Save cool-Blue/42514ca0daad76ecfde9 to your computer and use it in GitHub Desktop.
Save cool-Blue/42514ca0daad76ecfde9 to your computer and use it in GitHub Desktop.
d3 zoomable FDG
41f89fa9b807699f7d0eeeb7a47fec1c60a55b80
orientdb.js
getFamilytreeSingle
- This queries the db for the selected net and returns it: works fine
- Then the sub-net is returned to familytree-d3.js
- findDuplicatesAndSetEmpty checks if the subnet nodes are already in the main net and, if they are, deletes them from the sub-net
- then the remaining nodes on the sub-net are concatinated onto the main net.
- if the main net already contains the selected net the end result is nothing
getFamilytreeAll2
- added this version with the call-back specified by the caller: #seperation of concerns principle
- issues:
- this module should not make the decision about familytree.getAlreadyThere(): violates Shy principle
familytree-d3.js
- updateGraphByRemoveElement
- json4Splicing does not do anything, just use currentJason
- initializeGraph
- moved .attr("xlink:href", and .on("error", from node to newNode
- add indexing function to ensure nodes have the same href
- add imgHref as a seperate function so can use it as index as well as for making the node href
- showAll
- use getFamilytreeAll2 with callback
- can eliminate the decision in familytree.js and there, just call familytree.createGraph
var familytree = (function () {
var alreadyThere = false;
var nodeCircles = {};
var svg, link = {}, node = {};
var force = d3.layout.force();
var zoom;
var width = 1200, height = 900;
var initJSON, currentJSON;
var container;
var scale = 1;
var trans = "0,0";
return {
cleanPresentation: function () {
nodeCircles = {};
force.nodes([]);
force.links([]);
familytree.initializeGraph();
alreadyThere = false;
},
getAlreadyThere: function () {
return alreadyThere;
},
showAll: function(){
initJSON = null;
orientdb.getFamilytreeAll(this.createGraph)
},
createGraph: function (newJSON) {
// create a new graph or reinstate the saved graph
if (alreadyThere) {
this.updateForce(this.generateObjects(currentJSON = JSON.parse(initJSON)));
return;
}
initJSON = JSON.stringify(newJSON);
this.updateForce(this.generateObjects(newJSON));
currentJSON = newJSON;
alreadyThere = true;
},
updateGraph: function (newJSON) {
// merge newJSON with existing graph
this.findDuplicatesAndSetEmpty(newJSON);
this.deleteEmptyObjectsInJSON(newJSON);
currentJSON = currentJSON.concat(newJSON);
this.updateForce(this.generateObjects(currentJSON));
},
findDuplicatesAndSetEmpty: function (newJSON) {
for (var i = 0; i < currentJSON.length; i++) {
for (var o = 0; o < newJSON.length; o++) {
if ((currentJSON[i].source.ID == newJSON[o].source) && (currentJSON[i].target.ID == newJSON[o].target)
|| (currentJSON[i].source.ID == newJSON[o].target) && (currentJSON[i].target.ID == newJSON[o].source)) {
newJSON[o] = {};
}
}
}
},
deleteEmptyObjectsInJSON: function (json) {
for (var i = 0; i < json.length; i++) {
var y = json[i].source;
if (y === "null" || y === null || y === "" || typeof y === "undefined") {
json.splice(i, 1);
i--;
}
}
},
updateGraphByRemoveElement: function (clickedNode, index) {
// remove links from or to clicked node
for (var i = 0; i < currentJSON.length; i++) {
if (currentJSON[i].source.ID == clickedNode.ID) {
currentJSON[i] = {};
} else if (currentJSON[i].target.ID == clickedNode.ID) {
currentJSON[i] = {};
}
}
familytree.deleteEmptyObjectsInJSON(currentJSON);
familytree.deleteNode(force.nodes(), clickedNode);
familytree.updateForce(familytree.generateObjects(currentJSON));
},
deleteNode: function (allNodes, clickedNode) {
allNodes.forEach(function (node) {
if (node == clickedNode) {
force.links().forEach(function (link) {
if (node.ID == link.source.ID) {
link.target.linkCount--;
}
if (node.ID == link.target.ID) {
link.source.linkCount--;
}
});
node.linkCount = 0;
}
});
},
generateObjects: function (json) {
json.forEach(function (link) {
if (typeof(link.source) == "string") {
link.source = nodeCircles[link.source] || (
nodeCircles[link.source] = {
name: link.sourceName,
significance: link.sourceSign,
uniquename: link.sourceUName,
ID: link.source,
class: link.sourceClass,
relation: link.relation,
race: link.sourceRace,
linkCount: 0
}
);
link.source.linkCount++;
}
if (typeof(link.target) == "string") {
link.target = nodeCircles[link.target] || (
nodeCircles[link.target] = {
name: link.targetName,
significance: link.targetSign,
uniquename: link.targetUName,
ID: link.target,
class: link.targetClass,
relation: link.relation,
race: link.targetRace,
linkCount: 0}
);
link.target.linkCount++;
}
});
return json;
},
updateForce: function (links) {
updateData(links, force, nodeCircles)
},
initializeGraph: (function(){
force
.size([width, height])
.gravity(.2)
.charge(-400)
.friction(0.9)
.theta(0.9)
.linkStrength(1)
.distance(100)
.on("tick", tick)
return function () {
svg = zoomableSVG({width: "100%", height: "100%"}, "#familytreecontentsvg", {extent: [0.4, 4], dblclk: null});
svg.onZoom(zoomed);
hookDrag(force.drag(), "dragstart.force", function(d) {
//prevent dragging on the nodes from dragging the canvas
var e = d3.event.sourceEvent;
if(e.button != 2) {
//left click only
e.stopPropagation();
d.fixed = e.shiftKey || e.touches && (e.touches.length > 1);
familytree.click(d)
}
});
hookDrag(force.drag(), "dragend.force", function(d) {
//prevent dragging on the nodes from dragging the canvas
var e = d3.event.sourceEvent;
d.fixed = e.shiftKey || d.fixed;
});
var varsvgMarker = svg.selectAll("defs")
.data([["end"]])
.enter().append("defs")
.selectAll("marker")
.data(id)
.enter();
this.createMarker(varsvgMarker);
container = svg.selectAll("#container").data([{nodes: [force.nodes()], links: [force.links()]}]);
container.enter().append("g").attr("id", "container");
var links = container.selectAll(".links").data(function(d){
return d.links
});
links.enter().append("g").attr("class", "links");
link = links.selectAll("line").data(id);
link.enter().append("line")/*.attr("class", "link")*/;
link.exit().remove();
link.attr("class", function (d) {
if (d.relation == "BEGETS") {
return "linkBEGETS";
}
if (d.relation == "LOVES") {
return "linkLOVES";
}
if (d.relation == "HASSIBLING") {
return "linkHASSIBLING";
}
})
.attr("marker-end", function (d) {
if (d.relation == "BEGETS") {
switch (d.targetSign) {
case 1: return "url(#end1)"; break;
case 2: return "url(#end2)"; break;
case 3: return "url(#end3)"; break;
case 4: return "url(#end4)"; break;
case 5: return "url(#end5)"; break;
case 6: return "url(#end6)"; break;
case 7: return "url(#end7)"; break;
case 8: return "url(#end8)"; break;
case 9: return "url(#end9)"; break;
default:
return "url(#end1)";
}
}
});
var nodes = container.selectAll(".nodes").data(function(d){return d.nodes});
nodes.enter().append("g").attr("class", "nodes");
node = nodes.selectAll(".node").data(id, nodeKey);
var newNode = node.enter().append("g").attr("class", "node");
node.exit().remove();
newNode.on("mouseover", this.mouseover)
.on("mouseout", this.mouseout)
//.on("click", function (d) {
// familytree.click(d);
//})
.on("dblclick", function (d) {
familytree.dblclick(d);
})
.on('contextmenu', function (data, index) {
d3.event.preventDefault();
familytree.updateGraphByRemoveElement(data, index);
})
.call(force.drag);
newNode
.append("circle")
.attr("class", "bgcircle");
node.select("circle")
.attr("r", function (d) {
return Math.abs(familytree.posXY(d));
})
.style("fill", function (d) {
return familytree.colourRace(d);
})
.style("stroke", function (d) {
return familytree.colourRace(d);
});
newNode
.append("svg:image")
.attr("class", "circle")
.attr("xlink:href", imgHref)
.attr("width", function (d) {
return familytree.sizeXY(d);
})
.attr("height", function (d) {
return familytree.sizeXY(d);
})
.on("error", function () {
d3.select(this).style("visibility", "hidden");
});
node.select("image")
.attr("x", function (d) {
return familytree.posXY(d);
})
.attr("y", function (d) {
return familytree.posXY(d);
});
newNode
.append("text")
.attr("class", "nodetext");
node.select("text")
.attr("x", function (d) {
return Math.abs(familytree.posXY(d) - 5);
})
.attr("y", 4)
.text(function (d) {
return d.name;
});
force
.alpha(0.4)
.start();
function hookDrag(target, event, hook) {
//hook force.drag behaviour
var stdDragStart = target.on(event);
target.on(event, function(d) {
hook.call(this, d);
stdDragStart.call(this, d);
});
}
function zoomed(){
var e = d3.event.sourceEvent,
isWheel = e && ((e.type == "mousewheel") || (e.type == "wheel"));
force.alpha(0.01);
return isWheel ? zoomWheel.call(this) : zoomInst.call(this)
}
function zoomInst(){
var t = d3.transform(container.attr("transform"));
t.translate = d3.event.translate; t.scale = d3.event.scale;
container.attr("transform", t.toString());
}
function zoomWheel(){
var t = d3.transform(container.attr("transform"));
t.translate = d3.event.translate; t.scale = d3.event.scale;
container.transition().duration(450).attr("transform", t.toString());
}
function imgHref(d) {
return "/pics/arda/creature/" + d.uniquename + "_familytree.png";
}
function nodeKey(d, i) {
return this.href ? d3.select(this).attr("href") : imgHref(d);
}
};
function tick() {
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;
});
node
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
})(),
createMarker: function (svg) {
//http://stackoverflow.com/questions/15495762/linking-nodes-of-variable-radius-with-arrows
var obj = [38,43,50,54,60,65,70,80,85];
for (var i = 0; i < obj.length; i++) {
svg.append("svg:marker")
.attr("id", "end" + (i + 1))
.attr("viewBox", "0 -5 10 10")
.attr("refX", obj[i])
.attr("refY", -0.05)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-4L10,0L0,4");
}
},
sizeXY: function (d) {
var deflt = -10;
return [deflt,20,24,28,32,36,40,44,48,52][d.significance || 0] || deflt;
},
posXY: function (d) {
var deflt = 10;
return [deflt, -10, -12, -14, -16, -18, -20, -22, -24, -26][d.significance || 0] || deflt;
},
colourRace: function (d) {
switch ((d.race)) {
case "Ainu":
return "#000"; break;
case "Arnorian":
return "#5D8AA8"; break;
case "Balrog":
return "#000"; break;
case "Dragon":
return "#900"; break;
case "Dwarf":
return "#996515"; break;
case "Elf":
return "#900020"; break;
case "Ent":
return "#5b3"; break;
case "Falmar/Falas Elf":
return "#0099CC"; break;
case "God":
return "#fff"; break;
case "Gondorian":
return "#393939"; break;
case "Half-Elf":
return "#900020"; break;
case "Hobbit":
return "#006600"; break;
case "Maia":
return "#9A03B5"; break;
case "Man":
return "#993D00"; break;
case "Nando":
return "#355E3B"; break;
case "Nazgûl":
return "#000"; break;
case "Noldo":
return "#090A67"; break;
case "Númenórean":
return "#007BA7"; break;
case "Orc":
return "#736326"; break;
case "Rohir":
return "#80461B"; break;
case "Sinda":
return "#949494"; break;
case "Spider":
return "#000"; break;
case "Teleri":
return "#4B0101"; break;
case "Tree":
return "#2b6"; break;
case "Troll":
return "#000"; break;
case "Vala":
return "#440D60"; break;
case "Vanya":
return "#FFCC00"; break;
case "Werewolf":
return "#000"; break;
default:
return "#aaa"; break;
}
},
mouseover: function () {
d3.select(this).select("text").transition()
.duration(750)
.style("font-size", "15px")
.style("fill", "black");
d3.select(this).moveToFront();
},
mouseout: function () {
d3.select(this).select("text").transition()
.duration(750)
.style("font-size", "8px")
.style("fill", "#ccc");
},
click: function (d) {
orientdb.getInfo4CreatureByRID(d.ID);
},
dblclick: function (d) {
orientdb.getFamilytreeSingle(d.ID + "|" + d.class, false);
}
};
function updateData(links, force, nodeCircles) {
force.nodes(d3.values(nodeCircles).filter(function (d) {
return d.linkCount;
}));
force.links(d3.values(links)); //TODO links is already an array, this does nothing
familytree.initializeGraph();
}
function zoomableSVG (size, selector, z){
//delivers an svg background with zoom/drag context in the selector element
//if height or width is NaN, assume it is percentage and ignore margin
var margin = size.margin || {top: 0, right: 0, bottom: 0, left: 0},
percentW = isNaN(size.width), percentH = isNaN(size.height),
w = percentW ? size.width : size.width - margin.left - margin.right,
h = percentH ? size.height : size.height - margin.top - margin.bottom,
zoomed = function(){return this},
zoom = zoom || d3.behavior.zoom().scaleExtent(z && z.extent || [0.4, 4])
.on("zoom", function(d, i, j){
zoomed.call(this, d, i, j);
});
var svg = d3.select(selector).selectAll("svg").data([["transform root"]]);
svg.enter().append("svg");
svg.attr({width: size.width, height: size.height});
var g = svg.selectAll("#zoom").data(id),
gEnter = g.enter().append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom)
.attr({class: "outline", id: "zoom"}),
surface = gEnter.append("rect")
.attr({width: w, height: h, fill: "none"})
.style({"pointer-events": "all"});
if(z && (typeof z.dblclk != "undefined")) gEnter.on("dblclick.zoom", z.dblclk);
g.h = h;
g.w = w;
g.onZoom = function(cb){zoomed = cb;};
return g;
}
function id(d){
return d;
};
})();
d3.selection.prototype.moveToFront = function () {
return this.each(function () {
this.parentNode.appendChild(this);
});
};
var familytreesearchsuggestions = "#familytreesearchsuggestions";
var familytreesearchsuggestionsFamilycreatures = "#familytreesearchsuggestionsFamilycreatures";
$("#familytreecontentclose a").click(function () {
$("#familytree").hide();
$("#familytreecontent").children().hide();
$("#illustrator").css("z-index", "19");
});
$("#familytreeShowallbutton").click(function () {
orientdb.getFamilytreeAll();
});
$("#familytreeHideallbutton").click(function () {
familytree.cleanPresentation();
});
d3.select('#familytreeUnfixallbutton').on('click', function () {
d3.selectAll('#familytreecontentsvg .node')
.each(function (d) {
d.fixed = false;
})
.classed("fixed", false)
});
$("#familytreesearch").on('input', function () {
if ($("#familytreesearch").val() == ""){
$(familytreesearchsuggestions).hide();
} else {
$(familytreesearchsuggestions).show();
orientdb.search4Creature("#familytreesearch", familytreesearchsuggestionsFamilycreatures);
}
});
$("ul" + familytreesearchsuggestionsFamilycreatures).on('click', 'li', function () {
orientdb.getFamilytreeSingle(this.id);
$(this).addClass("active");
});
$("ul" + familytreesearchsuggestionsFamilycreatures).on('mouseenter mouseleave', 'li', function () {
$(this).toggleClass("highlight");
});
$("#familytreesearch").attr('autocomplete', 'off');
$("#familytreecontentclose").mouseenter(function () {
if (ardamap.getCurrentAge() == ""){
$("#familytreeinner a").attr("href", "/");
}
if (ardamap.getCurrentAge() == "first"){
$("#familytreeinner a").attr("href", "/ages/first/");
}
if (ardamap.getCurrentAge() == "second"){
$("#familytreeinner a").attr("href", "/ages/second/");
}
if (ardamap.getCurrentAge() == "third"){
$("#familytreeinner a").attr("href", "/ages/third/");
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment