javascript - implement General Update Pattern for d3.js force layout -
beginning this example (possibly not best choice), set trying develop application suit purposes, , learn d3.js in process. after lot of naive tinkering, managed toy force layout test data satisfied me in appearance , behavior. i've set trying understand mb's general update pattern in context of particular example, users can interactively modify graph. have not yet grasped principle.
starting small, thought create function add single additional link graph between nodes labeled "walteri" , "roberti de fonte" (there's button executes addedge(), or can execute js console). in broken sort of way, had desired result; however, existing graph remained in place while duplicate graph generated containing additional link. it's clear there general update pattern i'm still not understanding.
if has , can offer insight, i'd grateful.
managing updates
main issue have re-appending group elements every update.
can d3 manage this...
//nodes bag //update var circles = svg.selectall(".circles") .data(["circles_g"]); //enter circles.enter() .append("svg:g") .attr("class", "circles"); you need make 1 element array drive it. nice thing placed on __data__ member added g element d3, handy debugging well.
general pattern
speaking, defensive pattern...
//update var update = baseselection.selectall(elementselector) .data(values, key), //enter enter = update.enter().append(appendelement) .call(initstuff), //enter() has side effect of adding enter nodes update selection //so update include enter nodes //update+enter updateenter = update .call(stufftodoeverytimethedatachanges); //exit exit = update.exit().remove() first time through update array of nulls same structure data.
.selectall() returns 0 length selection in case , nothing useful.
on subsequent updates, .selectall not empty , compared values, using keys, determine nodes update, enter , exit nodes. that's why need select before data join.
the important thing understand has .enter().append(...), appending elements on enter selection. if append them on update selection (the 1 returned data join) re-enter same elements , see similar behaviour getting.
the enter selection array of simple objects of form { __data__: data }
update , exit selections arrays of arrays of references dom elements.
the data method in d3 keeps closure on enter , exit selections accessed .enter() , .exit() methods on update. both return objects which, among other things, 2-d arrays (all selections in d3 arrays of groups, groups arrays of nodes.). enter member given reference update can merge two. done because, in majority of cases, same stuff done both groups.
revised code
there strange bug links disappeared when edge added unrelated due nan's in d.x , d.y in nodes.
if don't rebuild force layout every time in showme , if this...
links.push({ "source": nodes[i], "target": nodes[j], "type": "is_a_tenant_of" }); force.start(); showme(); the bug goes away , works fine.
this because internal state layout not include links, particularly
strengths,distancesarrays. internalforce.tick()method uses these calculate new link lengths , if there more links members of these arrays, returnundefined, link, length calculation returnnan, multiplied nodex,yvalues calculate newd.x,d.y.
recalculated inforce.start()
also, can move force = d3.layout.force()....start(); separate function , call once @ start.
d3.json("force-directed-edges.json", function(error, data){ if (error) return console.warn(error) nodes = data.nodes, links = data.links, predicates = data.predicates, json = json.stringify(data, undefined, 2); (n in nodes) { // don't want require incoming data have links array each node nodes[n].links = [] } links.foreach(function(link, i) { // kept 'or' check, in case we're building nodes links link.source = nodes[link.source] || (nodes[link.source] = {name: link.source}); link.target = nodes[link.target] || (nodes[link.target] = { name: link.target }); // dijkstra searching, we'll need adjacency lists: node.links. (easier thought) link.source.links.push(link); link.target.links.push(link); }); nodes = d3.values(nodes); restart() showme(); }); function randomnode(i) { var j; { j = math.round(math.random() * (nodes.length - 1)) } while (j === (i ? : -1)) return j } function addedge() { var = randomnode(), j = randomnode(i); links.push({ "source": nodes[i], "target": nodes[j], "type": "is_a_tenant_of" }); force.start(); showme(); } function restart() { force = d3.layout.force() .nodes(nodes) .links(links) .size([w, h]) .linkdistance(function (link) { var wt = link.target.weight; return wt > 2 ? wt * 10 : 60; }) .charge(-600) .gravity(.01) .friction(.75) //.theta(0) .on("tick", tick) .start(); } function showme() { //marker types var defs = svg.selectall("defs") .data(["defs"], function (d) { return d }).enter() .append("svg:defs") .selectall("marker") .data(predicates) .enter().append("svg:marker") .attr("id", string) .attr("viewbox", "0 -5 10 10") .attr("refx", 30) .attr("refy", 0) .attr("markerwidth", 4) .attr("markerheight", 4) .attr("orient", "auto") .append("svg:path") .attr("d", "m0,-5l10,0l0,5"), //link bag //update paths = svg.selectall(".paths") .data(["paths_g"]); //enter paths.enter() .append("svg:g") .attr("class", "paths"); //links //update path = paths.selectall("path") .data(links); //enter path.enter() .append("svg:path"); //update+enter path .attr("indx", function (d, i) { return }) .attr("id", function (d) { return d.source.index + "_" + d.target.index; }) .attr("class", function (d) { return "link " + d.type; }) .attr("marker-end", function (d) { return "url(#" + d.type + ")"; }); //exit path.exit().remove(); //link labels bag //update var path_labels = svg.selectall(".labels") .data(["labels_g"]); //enter path_labels.enter() .append("svg:g") .attr("class", "labels"); //link labels //update var path_label = path_labels.selectall(".path_label") .data(links); //enter path_label.enter() .append("svg:text") .append("svg:textpath") .attr("startoffset", "50%") .attr("text-anchor", "middle") .style("fill", "#000") .style("font-family", "arial"); //update+enter path_label .attr("class", function (d, i) { return "path_label " + }) //edit******************************************************************* .selectall('textpath') //edit******************************************************************* .attr("xlink:href", function (d) { return "#" + d.source.index + "_" + d.target.index; }) .text(function (d) { return d.type; }), //exit path_label.exit().remove(); //nodes bag //update var circles = svg.selectall(".circles") .data(["circles_g"]); //enter circles.enter() .append("svg:g") .attr("class", "circles"); //nodes //update circle = circles.selectall(".nodes") .data(nodes); //enter circle.enter().append("svg:circle") .attr("class", function (d) { return "nodes " + d.index }) .attr("stroke", "#000"); //update+enter circle .on("click", clicked) .on("dblclick", dblclick) .on("contextmenu", cmdclick) .attr("fill", function (d, i) { console.log(i + " " + d.types[0] + " " + node_colors[d.types[0]]) return node_colors[d.types[0]]; }) .attr("r", function (d) { return d.types.indexof("document") == 0 ? 24 : 12; }) .call(force.drag); //exit circle.exit().remove(); //anchors bag //update var textbag = svg.selectall(".anchors") .data(["anchors_g"]); //enter textbag.enter() .append("svg:g") .attr("class", "anchors"), //anchors //update textupdate = textbag.selectall("g") .data(nodes, function (d) { return d.name; }), //enter textenter = textupdate.enter() .append("svg:g") .attr("text-anchor", "middle") .attr("class", function (d) { return "anchors " + d.index }); // copy of text thick white stroke legibility. textenter.append("svg:text") .attr("x", 8) .attr("y", ".31em") .attr("class", "shadow") .text(function (d) { return d.name; }); textenter.append("svg:text") .attr("x", 8) .attr("y", ".31em") .text(function (d) { return d.name; }); textupdate.exit().remove(); text = textupdate; // calling force.drag() here returns drag _behavior_ on set listener // node element event listeners force.drag().on("dragstart", function (d) { d3.selectall(".dbox").style("z-index", 0); d3.select("#dbox" + d.index).style("z-index", 1); }) } edit
in response comment below @jjon , own edification, here minimum changes original code same naming conventions , differential comments. mods required adding links unchanged , not discussed...
function showme() { svg ///////////////////////////////////////////////////////////////////////////////////// //problem // defs element added document every update //solution: // create data join on defs // append marker definitions on resulting enter selection // appended once ///////////////////////////////////////////////////////////////////////////////////// //add////////////////////////////////////////////////////////////////////////////////// .selectall("defs") .data(["defs"], function (d) { return d }).enter() /////////////////////////////////////////////////////////////////////////////////////// .append("svg:defs") .selectall("marker") .data(predicates) .enter().append("svg:marker") .attr("id", string) .attr("viewbox", "0 -5 10 10") .attr("refx", 30) .attr("refy", 0) .attr("markerwidth", 4) .attr("markerheight", 4) .attr("orient", "auto") .append("svg:path") .attr("d", "m0,-5l10,0l0,5"); ///////////////////////////////////////////////////////////////////////////////////// //problem // g element added document every update //solution: // create data join on g , class .paths // append path g on resulting enter selection // appeneded once ///////////////////////////////////////////////////////////////////////////////////// //add////////////////////////////////////////////////////////////////////////////////// //link bag //update paths = svg .selectall(".paths") .data(["paths_g"]); //enter paths.enter() /////////////////////////////////////////////////////////////////////////////////////// .append("svg:g") //add////////////////////////////////////////////////////////////////////////////////// .attr("class", "paths"); /////////////////////////////////////////////////////////////////////////////////////// //links //update path = paths //replace svg paths/////////////////////////////////////////////// .selectall("path") .data(links); path.enter().append("svg:path") .attr("id", function (d) { return d.source.index + "_" + d.target.index; }) .attr("class", function (d) { return "link " + d.type; }) .attr("marker-end", function (d) { return "url(#" + d.type + ")"; }); path.exit().remove(); ///////////////////////////////////////////////////////////////////////////////////// //problem // g structure added every update //solution: // create data join on g , class .labels // append labels g on resulting enter selection // appeneded once // include .exit().remove() defensive //note: // don't chain .enter() on object assigned path_label // .data(...) returns update selection includes enter() , exit() methods // .enter() returns standard selection doesn't have .exit() member // needed if links removed or if node indexing changes ///////////////////////////////////////////////////////////////////////////////////// //add////////////////////////////////////////////////////////////////////////////////// //link labels bag //update var path_labels = svg.selectall(".labels") .data(["labels_g"]); //enter path_labels.enter() /////////////////////////////////////////////////////////////////////////////////////// .append("svg:g") //add////////////////////////////////////////////////////////////////////////////////// .attr("class", "labels"); /////////////////////////////////////////////////////////////////////////////////////// //link labels //update var path_label = path_labels .selectall(".path_label") .data(links); //enter path_label .enter().append("svg:text") .attr("class", "path_label") .append("svg:textpath") .attr("startoffset", "50%") .attr("text-anchor", "middle") .attr("xlink:href", function (d) { return "#" + d.source.index + "_" + d.target.index; }) .style("fill", "#000") .style("font-family", "arial") .text(function (d) { return d.type; }); //add////////////////////////////////////////////////////////////////////////////////// path_label.exit().remove(); /////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////// //problem // g structure added every update //solution: // create data join on g , class .circles // append labels g on resulting enter selection // appeneded once // include .exit().remove() defensive ///////////////////////////////////////////////////////////////////////////////////// //add////////////////////////////////////////////////////////////////////////////////// //nodes bag //update var circles = svg.selectall(".circles") .data(["circles_g"]); //enter circles.enter() /////////////////////////////////////////////////////////////////////////////////////// .append("svg:g") //add////////////////////////////////////////////////////////////////////////////////// .attr("class", "circles"); /////////////////////////////////////////////////////////////////////////////////////// //nodes //update circle = circles .selectall(".node") //select on class instead of tag name////////////////////////// .data(nodes); circle //don't chain in order keep update selection//////////// .enter().append("svg:circle") .attr("class", "node") .attr("fill", function (d, i) { return node_colors[d.types[0]]; }) .attr("r", function (d) { return d.types.indexof("document") == 0 ? 24 : 12; }) .attr("stroke", "#000") .on("click", clicked) .on("dblclick", dblclick) .on("contextmenu", cmdclick) .call(force.drag); //add////////////////////////////////////////////////////////////////////////////////// circle.exit().remove(); /////////////////////////////////////////////////////////////////////////////////////// //add////////////////////////////////////////////////////////////////////////////////// //anchors bag //update var textbag = svg.selectall(".anchors") .data(["anchors_g"]); //enter textbag.enter() /////////////////////////////////////////////////////////////////////////////////////// .append("svg:g") //add////////////////////////////////////////////////////////////////////////////////// .attr("class", "anchors"); //anchors //update text = textbag /////////////////////////////////////////////////////////////////////////////////////// .selectall(".anchor") .data(nodes, function (d) { return d.name}); var textenter = text //don't chain in order keep update selection////////// .enter() .append("svg:g") .attr("class", "anchor") .attr("text-anchor", "middle"); //add////////////////////////////////////////////////////////////////////////////////// text.exit().remove; /////////////////////////////////////////////////////////////////////////////////////// // copy of text thick white stroke legibility. textenter.append("svg:text") .attr("x", 8) .attr("y", ".31em") .attr("class", "shadow") .text(function (d) { return d.name; }); textenter.append("svg:text") .attr("x", 8) .attr("y", ".31em") .text(function (d) { return d.name; }); // calling force.drag() here returns drag _behavior_ on set listener // node element event listeners force.drag().on("dragstart", function (d) { d3.selectall(".dbox").style("z-index", 0); d3.select("#dbox" + d.index).style("z-index", 1); }) }
Comments
Post a Comment