I'm attempting to draw a graph/network in which node contents should be filled with a third party library. So far, I was able to draw the graph with d3 with a container () for each node to be filled later on. My problem is that the containers seem not to yet exist when there are referred for drawing. Adding an onload event doesn't work either, but using a onclick event shows that everything is in place to work and most probably drawing starts before the DOM elements are actually created.
What is the best practice to make sure d3 generated DOM elements are created before starting assigning them content? The third party JS library I use only requires the div id to draw content in.
From arrays of nodes and relationships return by Neo4j, I build a graph with d3 as follow. Images to be displayed in nodes are stored in the neo4j data base as Base64 strings.
var width = 500, height = 500;
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var col = obj.columns;
var data = obj.data;
var nodes = obj.data[0][0].nodes;
var nodeMap = {};
nodes.forEach(function(x) { nodeMap[x.label] = x; });
var edges = obj.data[0][0].edges;
var links = edges.map(function(x) {
return { source: nodeMap[x.source], target: nodeMap[x.target], value: x.value };
});
var svg = d3.select("#graph").append("svg")
.attr("width", width)
.attr("height", height)
.attr("pointer-events", "all")
.append('g')
.call(d3.behavior.zoom().on("zoom", redraw))
.append('g');
var force = d3.layout.force()
.gravity(.3)
.distance(150)
.charge(-4000)
.size([width, height]);
var drag = force.drag()
.on("dragstart", dragstart);
force
.nodes(nodes)
.links(links)
.friction(0.8)
.start();
var link = svg.selectAll(".link")
.data(links)
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("svg:g")
.attr("class", "node")
.on("dblclick", dblclick)
.call(force.drag);
node.append("circle")
.attr("r", 50);
node.append("image")
// display structure in nodes
.attr("xlink:href", function(d){
if (d.imgB64) {
return 'data:image/png;base64, ' + d.imgB64 ;
}
})
.attr("x", -40)
.attr("y", -40)
.attr("width", 80)
.attr("height", 80);
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; });
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
});
That code works fine. Hence, what I'm attempting to do is to replace the Base64 images with canvas drawings generated by a third party library.
This library works as follow:
var myCanvas = newCanvasViewer(id, data);
This function knows how to generate an image from the given data and puts the result in the DOM element, if the element already exists in the DOM, or generate a new canvas element in the DOM, directly in but unfortunately erasing any other existing DOM elements.
So, I changed the d3 above code, replacing the node.append('image') block with:
node.append("canvas")
.attr("id", function(d) {
var cnv = newCanvasViewer(d.name, d.data);
return d.name;
});
This obviously doesn't work, canvas objects are displayed but not in the nodes, probably because the DOM element doesn't exist yet when calling newCanvasViewer. Furthermore, the d3 graph is overwritten.
When setting an onclick function calling newCanvasViewer, the drawing shows up within the nodes on click.
node.append("circle")
.attr("r", 50)
.attr("onclick", function(d) {
return "newCanvasViewer('"+d.name+"', '"+d.data+"')";
});
node.append("canvas")
.attr("id", function(d) {
return d.name;
});
Since I would like each node to display its canvas drawing from start, I was wondering when to call the newCanvasViewer function? I guess an oncreate function at each node would make it, but doesn't exist.
Would a call back function work? Should I call it once d3 is finished with the whole network drawing?
In order to be more comprehensive, here is the HTML and javascript code with callback function to attempt drawing canvas content once d3 d3 is done drawing. I'm still stuck, the canvas sample is actually displayed but no canvas content show in nodes, probably due to timing between generating canvas containers and using them.
HTML:
<body onload="init(); cypherQuery()">
<div id="graph" align="left" valign="top"></div>
<div id="sample">
<canvas id="mol" style="width: 160px; height: 160px;"></canvas>
</div>
</body>
Javascript (in the HTML header):
var xmlhttp;
function init() {
xmlhttp = new XMLHttpRequest();
}
function cypherQuery() {
// perform neo4j query, extract JSON and call the network drawing (d3).
// ...
var obj = JSON.parse(xmlhttp.responseText);
drawGraph(obj, drawCanvas);
// this is just to show that canvasViewer does the
// job if the <canvas id='...'> element exists in DOM
var nodes = obj.data[0][0].nodes;
var cnv = newCanvasViewer("sample", nodes[0].data);
}
function drawGraph(obj, drawCanvas) {
// see code above from...
var width = 500, height = 500;
// ... to
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; });
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
});
function redraw() {
svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
// node.attr("font-size", (nodeFontSize / d3.event.scale) + "px");
}
function dblclick(d) {
d3.select(this).classed("fixed", d.fixed = false);
}
if (callback && typeof(callback) === "function") {
callback(nodes);
}
}
Related
I'm working on a map (found here), that is using the svg viewbox attribute to scale with the size of the client.
Unfortunately the project I'm using, d3.geoAlbersUsa() does not seem to scale the tooltip correctly with the rest of the SVG. As in, it suddenly places the tooltip in the same spot it would be if the client width had been the original 960x500 specs.
Here's the tooltip code:
d3.tsv("CitiesTraveledTo.tsv",cityVisited, function(data) {
var cities = svg.selectAll(".city")
.data(data)
.enter()
.append("g")
.classed("city",true);
cities.append("line")
.attr("x1", function(d) {
return projection([d.Longitude, d.Latitude])[0];
})
.attr("x2", function(d) {
return projection([d.Longitude, d.Latitude])[0];
})
.attr("y1", function(d) {
return projection([d.Longitude, d.Latitude])[1]-pinLength;
})
.attr("y2", function(d) {
return (projection([d.Longitude, d.Latitude])[1]);
})
.attr("stroke-width",function(d) {
return 2;
})
.attr("stroke",function(d) {
return "grey";
});
cities.append("circle")
.attr("cx", function(d) {
return projection([d.Longitude, d.Latitude])[0];
})
.attr("cy", function(d) {
return projection([d.Longitude, d.Latitude])[1]-pinLength;
})
.attr("r", function(d) {
return 3;
})
.style("fill", function(d) {
if (d.Reason === "Work") {
return "rgb(214, 69, 65)";
}
else if (d.Reason === "Fun") {
return "rgb(245, 215, 110)";
}
else {
return "rgb(214, 69, 65)";
}
})
.style("opacity", 1.0)
// Modification of custom tooltip code provided by Malcolm Maclean, "D3 Tips and Tricks"
// http://www.d3noob.org/2013/01/adding-tooltips-to-d3js-graph.html
.on("mouseover", function(d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.text(d.City + ", " + d.State)
.style("left", function() {
var centerCircle = (projection([d.Longitude, d.Latitude])[0]);
return (centerCircle-26) + "px";
})
.style("top", function() {
var centerCircle = projection([d.Longitude, d.Latitude])[1];
var circleRadius = 3;
return ( centerCircle - circleRadius - 33-pinLength) + "px";
});
div.append("div").attr("class","arrow-down");
})
// fade out tooltip on mouse out
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
});
I thought that the scaling should just happen automatically for the tooltip as well. Wrong. I then tried to reset the height and width passed to the projection and that didn't work. What's the best way to get the element bound to a data "d" node?
I ask because it will likely be easier to say "for this node, get me this element, give me the bound html element", so that I can set the position of the tooltip relative to the new position of the bound element.
I have a treemap (simple) to which I want to add a new element (see DS0 and DS1 in the code below). I don't want to re-create the whole treemap, instead I want to "update" the data.
Sticky is false
update section adjusts the g transform (OK)
update section adjusts the contained rect width/height (not OK)
with devmode off (chrome) the rects width/heights are not being updated (looks like the d.x and d.y values are the old values - can be seen with the cosnole.log statements)
with devmode on (chrome) the 'debugger' statements kick in, and if I step through, the some width values update OK.
I can't see why this is not updating properly, but I can make a few observations. It appears that the data binding to the <g>, <rect> and <text> elements is not being adjusted when new data comes in. certainly the <text> data remains 'old'.
Perhaps the only intended way to do this is to re-create the whole SVG again?
diagrams
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Test Dynamic Treemap</title>
<style>
rect { fill: none; stroke: #fff;}
text { font: 10px sans-serif;}
</style>
<body>
<input type="button" id="add" value="add">
<input type="button" id="remove" value="remove">
<br>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var width = 960,
height = 500,
color = d3.scale.category20c();
var treemap = d3.layout.treemap()
.padding([15,5,5,5])
.size([width, height])
.sticky(false)
.value(function(d) { return 5; });
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(-.5,-.5)");
var DS0 =
{'name': 'data', 'children': [
{'name':'A', 'children':[
{'name':'A01' },
{'name':'A02'}
]},
]}
var DS1 =
{'name': 'data', 'children': [
{'name':'A', 'children':[
{'name':'A01' },
{'name':'A02'},
{'name':'A03'}
]},
]}
d3.select('#add').on('click',function() {withData(DS1)})
d3.select('#remove').on('click',function() {withData(DS0)})
withData(DS0);
function withData(json){
var cell = svg.data([json]).selectAll("g")
.data(treemap.nodes)
// BEGIN UPDATE SECTION
var updates = cell.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
// adjust the parameters of the updated objects
updates.selectAll("rect") // These only seem to use 'new' data when stepped through in debugger
// ======================================================================
.attr("width", function(d) { debugger; console.log(d.name + ' setting width to ' + d.dx); return d.dx; })
.attr("height", function(d) { debugger; console.log(d.name + ' setting height to ' + d.dy); return d.dy; })
.style("fill", function(d) { return d.children ? color(d.name) : "yellow"; })
// END UPDATE SECTION
// BEGIN ENTER SECTION (ADDS)
var adds = cell.enter().append("g")
.attr("class", function(d) {return "cell " + d.name})
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
adds.append("rect")
.attr("width", function(d) { return d.dx; })
.attr("height", function(d) { return d.dy; })
.style("fill", function(d) { return d.children ? color(d.name) : null; })
adds.append("text")
.attr("x", function(d) { return d.dx / 2; })
.attr("y", function(d) { return 5; })
.attr("dy", ".35em")
.attr("text-anchor", "top")
.text(function(d) { return d.children ? d.name : d.name; });
// END ENTER SECTION (ADDS)
// BEGIN UPDATE AND ENTER SECTION (Updates And ADDS)
//var updatesAndAdds = cell.attr("transform", function(d) { console.log("UpdateAndUpdate:" + d.name); return "translate(" + d.x + "," + d.y + ")"; });
// END UPDATE AND ENTER SECTION (Updates And ADDS)
// BEGIN EXIT SECTION (Updates And ADDS)
var rems = cell.exit().remove();
// END EXIT SECTION (Updates And ADDS)
};
</script>
This code:
var adds = cell.enter().append("g")
...
adds.append("text")
.attr("x", function(d) { return d.dx / 2; })
.attr("y", function(d) { return 5; })
Means you are only handling the enter selection for the text elements. The code above it:
updates.selectAll("rect")
Is handling the update selection for the rects (and you need something like it for the text elements).
Let's take a step back and try to clean this up a bit (untested code):
////////////
// this is the entire selection
/////////////
var cell = svg.data([json]).selectAll("g")
.data(treemap.nodes);
/////////////
// remove is easy, get it out of the way
/////////////
cell.exit().remove();
/////////////
// do all the entering first
////////////
// enter the gs
var eCell = cell.enter().append("g")
.attr("class", function(d) {return "cell " + d.name});
// and rect for that g
eCell.append("rect")
.style("fill", function(d) { return d.children ? color(d.name) : null; });
// and a text for that g
eCell.append("text")
.attr("y", function(d) { return 5; })
.attr("dy", ".35em")
.attr("text-anchor", "top");
/////////////
// now do all the updating, this is enter + update
////////////
// move the g
cell.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
// resize/move the rect
cell.select("rect")
.attr("width", function(d) { debugger; console.log(d.name + ' setting width to ' + d.dx); return d.dx; })
.attr("height", function(d) { debugger; console.log(d.name + ' setting height to ' + d.dy); return d.dy; });
// move the text and update value
cell.select("text")
.attr("x", function(d) { return d.dx / 2; })
.text(function(d) { return d.children ? d.name : d.name; });
I think I'm missing something very obvious here. Basically what I am trying to do is create a treemap that on button click will go to the server and retrieve the next level into the treemap...This is necessary because the treemap structure is too large and takes too long to calculate so jumping one level at a time is the only option we have.
[Note to IE users, in this example the treemap node names don't appear to be working. Try using Chrome]
http://plnkr.co/edit/simVGU
This code is taken almost exactly from
http://bost.ocks.org/mike/treemap/
I'm using vizData1.json for the "first" level and on mouse click I'm using vizData2.json as the "second" level. You can see that the two end up overlapping. I've tried to do svg.exit() as well as svg.clear() without any luck.
I should also note that I have already tried the sticky(false) suggestion from this post
Does the d3 treemap layout get cached when a root node is passed to it?
UPDATE:
To continue my hunt I have found an example that successfully adds new nodes to an existing treemap. However I am having trouble adapting this logic as the treemap I am attempting to fit this logic into has been heavily customized by Michael Bostock - #mbostock to allow for the nice breadcrumb trail bar at the top.
Code snippet that proves appending to existing treemap nodes is possible:
http://jsfiddle.net/WB5jh/3/
Also, Stackoverflow is forcing me to post code because I'm linking to plnkr so I have dumped my script.js here for those who would rather not interact with plunker
$(function() {
var margin = { top: 20, right: 0, bottom: 0, left: 0 },
width = 960,
height = 500,
formatNumber = d3.format(",d"),
transitioning;
var x = d3.scale.linear()
.domain([0, width])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, height])
.range([0, height]);
var treemap = d3.layout.treemap()
.children(function (d, depth) { return depth ? null : d._children; })
.sort(function (a, b) { return a.value - b.value; })
.ratio(height / width * 0.5 * (1 + Math.sqrt(5)))
.round(false)
.sticky(false);
var svg = d3.select("#treemap")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.bottom + margin.top)
.style("margin-left", -margin.left + "px")
.style("margin.right", -margin.right + "px")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.style("shape-rendering", "crispEdges");
var grandparent = svg.append("g")
.attr("class", "grandparent");
grandparent.append("rect")
.attr("y", -margin.top)
.attr("width", width)
.attr("height", margin.top);
grandparent.append("text")
.attr("x", 6)
.attr("y", 6 - margin.top)
.attr("dy", ".75em");
d3.json("vizData1.json", function (root) {
initialize(root);
accumulate(root);
layout(root);
display(root);
});
function initialize(root) {
root.x = root.y = 0;
root.dx = width;
root.dy = height;
root.depth = 0;
}
// Aggregate the values for internal nodes. This is normally done by the
// treemap layout, but not here because of our custom implementation.
// We also take a snapshot of the original children (_children) to avoid
// the children being overwritten when when layout is computed.
function accumulate(d) {
return (d._children = d.children)
? d.value = d.children.reduce(function (p, v) { return p + accumulate(v); }, 0)
: d.value;
}
// Compute the treemap layout recursively such that each group of siblings
// uses the same size (1×1) rather than the dimensions of the parent cell.
// This optimizes the layout for the current zoom state. Note that a wrapper
// object is created for the parent node for each group of siblings so that
// the parent’s dimensions are not discarded as we recurse. Since each group
// of sibling was laid out in 1×1, we must rescale to fit using absolute
// coordinates. This lets us use a viewport to zoom.
function layout(d) {
if (d._children) {
treemap.nodes({ _children: d._children });
d._children.forEach(function (c) {
c.x = d.x + c.x * d.dx;
c.y = d.y + c.y * d.dy;
c.dx *= d.dx;
c.dy *= d.dy;
c.parent = d;
layout(c);
});
}
}
function display(d) {
console.log(d);
grandparent
.datum(d.parent)
.on("click", transition)
.select("text")
.text(name(d));
var g1 = svg.insert("g", ".grandparent")
.datum(d)
.attr("class", "depth");
var g = g1.selectAll("g")
.data(d._children)
.enter().append("g");
g.filter(function (d) { return d._children; })
.classed("children", true)
.on("click", transition);
g.selectAll(".child")
.data(function (d) { return d._children || [d]; })
.enter().append("rect")
.attr("class", "child")
.call(rect);
g.append("rect")
.attr("class", "parent")
.call(rect)
.append("title")
.text(function (d) { return formatNumber(d.value); });
g.append("foreignObject")
.call(rect)
.attr("class", "foreignobj")
.append("xhtml:div")
.attr("dy", ".75em")
.html(function (d) { return d.name; })
.attr("class", "textdiv");
function transition(d) {
if (transitioning || !d) return;
transitioning = true;
d3.json("vizData2.json", function (root) {
initialize(root);
accumulate(root);
layout(root);
display(root);
});
var g2 = display(d),
t1 = g1.transition().duration(750),
t2 = g2.transition().duration(750);
// Update the domain only after entering new elements.
x.domain([d.x, d.x + d.dx]);
y.domain([d.y, d.y + d.dy]);
// Enable anti-aliasing during the transition.
svg.style("shape-rendering", null);
// Draw child nodes on top of parent nodes.
svg.selectAll(".depth").sort(function (a, b) { return a.depth - b.depth; });
// Fade-in entering text.
g2.selectAll("text").style("fill-opacity", 0);
g2.selectAll("foreignObject div").style("display", "none"); /*added*/
// Transition to the new view.
t1.selectAll("text").call(text).style("fill-opacity", 0);
t2.selectAll("text").call(text).style("fill-opacity", 1);
t1.selectAll("rect").call(rect);
t2.selectAll("rect").call(rect);
t1.selectAll(".textdiv").style("display", "none"); /* added */
t1.selectAll(".foreignobj").call(foreign);
t2.selectAll(".textdiv").style("display", "block"); /* added */
t2.selectAll(".foreignobj").call(foreign); /* added */
// Remove the old node when the transition is finished.
t1.remove().each("end", function () {
svg.style("shape-rendering", "crispEdges");
transitioning = false;
});
}
return g;
}
function text(text) {
text.attr("x", function (d) { return x(d.x) + 6; })
.attr("y", function (d) { return y(d.y) + 6; });
}
function rect(rect) {
rect.attr("x", function (d) { return x(d.x); })
.attr("y", function (d) { return y(d.y); })
.attr("width", function (d) { return x(d.x + d.dx) - x(d.x); })
.attr("height", function (d) { return y(d.y + d.dy) - y(d.y); });
}
function foreign(foreign) { /* added */
foreign.attr("x", function (d) { return x(d.x); })
.attr("y", function (d) { return y(d.y); })
.attr("width", function (d) { return x(d.x + d.dx) - x(d.x); })
.attr("height", function (d) { return y(d.y + d.dy) - y(d.y); });
}
function name(d) {
return d.parent
? name(d.parent) + "." + d.name
: d.name;
}
});
This is probably the simplest graph possible to create using d3js. And yet I am struggling.
The graph runs everything given to it in enter() and exit(). But everything in ENTER + UPDATE is completely ignored. WHY?
// Setup dimensions
var width = 200,
height = 200,
radius = Math.min(width, height) / 2,
// Setup a color function with 20 colors to use in the graph
color = d3.scale.category20(),
// Configure pie container
arc = d3.svg.arc().outerRadius(radius - 10).innerRadius(0), // Define the arc element
svg = d3.select(".pie").append("svg:svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"),
// This is the layout manager for the pieGraph
pie = d3.layout.pie()
.sort(null)
.value(function (d) {
return d.answers;
}),
// Allow two groups in the container. One overlapping the other, just to make sure that
// text labels never get hidden below pie arcs.
graphGroup = svg.append("svg:g").attr("class", "graphGroup"),
textGroup = svg.append("svg:g").attr("class", "labelGroup");
// Data is loaded upon user interaction. On angular $scope update, refresh graph...
$scope.$watch('answers', function (data) {
// === DATA ENTER ===================
var g = graphGroup.selectAll("path.arc").data(pie(data)),
gEnter = g.enter()
.append("path")
.attr("d", arc)
.attr("class", "arc"),
t = textGroup.selectAll("text.label").data(data),
tEnter = t.enter()
.append("text")
.attr("class", "label")
.attr("dy", ".35em")
.style("text-anchor", "middle");
// === ENTER + UPDATE ================
g.select("path.arc")
.attr("id", function (d) {
return d.data.id + "_" + d.data.selection;
})
.attr("fill", function (d, i) {
return color(i);
})
.transition().duration(750).attrTween("d", function (d) {
var i = d3.interpolate(this._current, d);
this._current = i(0);
return function (t) {
return arc(i(t));
};
});
t.select("text.label")
.attr("transform", function (d) {
return "translate(" + arc.centroid(d) + ")";
})
.text(function (d) {
return d.data.opt;
});
// === EXIT ==========================
g.exit().remove();
t.exit().remove();
});
This one example of the json structure given to the update function as "data":
[{"selection":"0","opt":"1-2 timer","answers":"7"},
{"selection":"1","opt":"3-4 timer","answers":"13"},
{"selection":"2","opt":"5-6 timer","answers":"5"},
{"selection":"3","opt":"7-8 timer","answers":"8"},
{"selection":"4","opt":"9-10 timer","answers":"7"},
{"selection":"5","opt":"11 timer eller mer","answers":"11"},
{"selection":"255","opt":"Blank","answers":"8"}]
You don't need the additional .select() to access the update selection. This will in fact return empty selections in your case, which means that nothing happens. To make it work, simply get rid of the additional .select() and do
g.attr("id", function (d) {
return d.data.id + "_" + d.data.selection;
})
// etc
t.attr("transform", function (d) {
return "translate(" + arc.centroid(d) + ")";
})
// etc
So I have the next force layout graph code for setting nodes, links and other elements:
var setLinks = function ()
{
link = visualRoot.selectAll("line.link")
.data(graphData.links)
.enter().append("svg:line")
.attr("class", "link")
.style("stroke-width", function (d) { return nodeStrokeColorDefault; })
.style("stroke", function (d) { return fill(d); })
.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; });
graphData.links.forEach(function (d)
{
linkedByIndex[d.source.index + "," + d.target.index] = 1;
});
};
var setNodes = function ()
{
node = visualRoot.selectAll(".node")
.data(graphData.nodes)
.enter().append("g")
.attr("id", function (d) { return d.id; })
.attr("title", function (d) { return d.name; })
.attr("class", "node")
.on("click", function (d, i) { loadAdditionalData(d.userID, this); })
.call(force.drag)
.on("mouseover", fadeNode(.1)).on("mouseout", fadeNode(1));
};
//append the visual element to the node
var appendVisualElementsToNodes = function ()
{
node.append("circle")
.attr("id", function (d) { return "circleid_" + d.id; })
.attr("class", "circle")
.attr("cx", function (d) { return 0; })
.attr("cy", function (d) { return 0; })
.attr("r", function (d) { return getNodeSize(d); })
.style("fill", function (d) { return getNodeColor(d); })
.style("stroke", function (d) { return nodeStrokeColorDefault; })
.style("stroke-width", function (d) { return nodeStrokeWidthDefault; });
//context menu:
d3.selectAll(".circle").on("contextmenu", function (data, index)
{
d3.select('#my_custom_menu')
.style('position', 'absolute')
.style('left', d3.event.dx + "px")
.style('top', d3.event.dy + "px")
.style('display', 'block');
d3.event.preventDefault();
});
//d3.select("svg").node().oncontextmenu = function(){return false;};
node.append("image")
.attr("class", "image")
.attr("xlink:href", function (d) { return d.profile_image_url; })//"Images/twitterimage_2.png"
.attr("x", -12)
.attr("y", -12)
.attr("width", 24)
.attr("height", 24);
node.append("svg:title")
.text(function (d) { return d.name + "\n" + d.description; });
};
Now, the colors and size dependencies changed and I need to redraw the graph circles (+all appended elements) with different color and radius. Having problem with it.
I can do this:
visualRoot.selectAll(".circle").remove();
but I have all the images that I attached to '.circles' still there.
In any way, any help will be appreciated, let me know if the explanation is not clear enough, I will try to fix it.
P.S. what is the difference between graphData.nodes and d3.selectAll('.nodes')?
Your answer will work, but for posterity, these methods are more generic.
Remove all children from HTML:
d3.select("div.parent").html("");
Remove all children from SVG/HTML:
d3.select("g.parent").selectAll("*").remove();
The .html("") call works with my SVG, but it might be a side effect of using innerSVG.
If you want to remove the element itself, just use element.remove(), as you did. In case you just want to remove the content of the element, but keep the element as-is, you can use f.ex.
visualRoot.selectAll(".circle").html(null);
visualRoot.selectAll(".image").html(null);
instead of .html("") (I wasn't sure which element's children you want deleted). This keeps the element itself, but cleans all included content. It the official way to do this, so should work cross-browser.
PS: you wanted to change the circle sizes. Have you tried
d3.selectAll(".circle").attr("r", newValue);
My first advice is that you should read the d3.js API about selections: https://github.com/mbostock/d3/wiki/Selections
You have to understand how the enter() command works (API). The fact that you have to use it to handle new nodes has a meaning which will help you.
Here is the basic process when you deal with selection.data():
first you want to "attach" some data to the selection. So you have:
var nodes = visualRoot.selectAll(".node")
.data(graphData.nodes)
Then you can modify all nodes each times data is changed (this will do exactly what you want). If for example you change the radius of old nodes which are in the new dataset you loaded
nodes.attr("r", function(d){return d.radius})
Then, you have to handle new nodes, for this you have to select the new nodes, this is what selection.enter() is made for:
var nodesEnter = nodes.enter()
.attr("fill", "red")
.attr("r", function(d){return d.radius})
Finally you certainly want to remove the nodes you don't want anymore, to do this, you have to select them, this is what selection.exit() is made for.
var nodesRemove = nodes.exit().remove()
A good example of the whole process can also be found on the API wiki: https://github.com/mbostock/d3/wiki/Selections#wiki-exit
in this way, I have resolved it very easily,
visualRoot.selectAll(".circle").remove();
visualRoot.selectAll(".image").remove();
and then I just re-added visual elements which were rendered differently because the code for calculating radius and color had changed properties. Thank you.
To remove all element from a node:
var siblings = element.parentNode.childNodes;
for (var i = 0; i < siblings.length; i++) {
for (var j = 0; j < siblings.length; j++) {
siblings[i].parentElement.removeChild(siblings[j]);
}
}`