This is a hard one to explain but hopefully its an easy one to answer...bear with me...i considered js fiddling it but I would have to rewrite a lot of code as the data it uses is from my internal server.
I have written a long amount of js to model a network map and its too long, and the essence of the issue is that I can draw the map with initial data fine but it corrupts sometimes when I update it - namely when I remove nodes from the original map (though I can add new nodes fine)
I believe the issues are due to the way I have drawn the svg objects.
I am trying to work d3 out on the fly a bit so I may have deviated unintentionally from what is right.
First off I define vis (window.vis as it is now so I can test it with the console) - which I think some examples use the variable svg for.
window.vis = d3.select("body")
.append("svg:svg")
.attr("pointer-events", "all")
.append('svg:g')
.call(d3.behavior.zoom().on("zoom", redraw))
.append('svg:g');
then I do some other stuff, creating nodeArray (an array of the nodes I want) and linkArray( an array of links I want) then I draw the nodes and links:
link = vis.selectAll("line").data(linkArray);
link.enter().append("line")
.attr("stroke-opacity", function (d, i) {
if (d.class == 'link reallink') {
return '0.8';
} else {
return '0';
};
})
.attr("stroke-width", function (d, i) {
if (d.class == 'link reallink') {
return '3';
} else {
return '0';
};
})
.style("stroke", function (d, i) { return d.color; });
node = vis.selectAll("g.node").data(nodeArray);
node.enter().append("svg:g")
.attr("class", function (d) { return d.class })
.attr("id", function (d) { return d.id })
.call(force.drag);
//append to each node an svg circle element
vis.selectAll(".realnode").append("svg:circle")
.attr("r", function (d, i) {
if (d.status != "0") {
return r*2;
} else {
if (d.iconimage == "") { return r; } else { return 1; }
}
})
.style("fill", function (d, i) { if (d.status != "0") { if (d.status == "1") { return "#ff0000"; } else { return "#FFBF00"; } } else { return "#FFFFFF"; }})
.style("stroke", function (d) {
if (d.style !== 'filled') { return d.color; };
})
.style("stroke-width", "4");
//attach images to the nodes
vis.selectAll(".realnode").append("image")
.attr("xlink:href", function (d) { return d.iconimage; })
.attr("x", -16)
.attr("y", -16)
.attr("width", 32)
.attr("height", 32);
NB: node classes I use are node (all nodes) and realnode (real objects) (as I also model the text labels later on as a different class labelnode). nodeArray is just an array of objects formatted as nodes and linkArray is just an array of links.
You can see that I define 'link' and 'node' - pretty much as all the examples do - but I found circles and images did not append properly if I used node.selectAll(".realnode").append... but works fine if I use vis.selectAll(".realnode").append... so I just did that and moved on
However I think I need to solve this lack of understanding!
Later on when I delete nodes out of nodeArray and links out of linkArray, and update the display I am again utilising the 'node' and 'link' objects in a haphazard mix with 'vis' - and at that point it all goes wrong and corrupts. Although the svg element on the page still has the right objects defined, images swap, text swaps and the links float off on their own without the nodes! I have checked nodeArray and linkArray and they are completely correct, and if I use the "new" data after the update as the original data when I first load the page it renders fine so I am fairly confident of my data objects.
I think the best way is for me to answer any questions and update this as I go along, as I hope someone will just look at this and see what I am doing wrong. The really annoying thing is the initial page load always works perfectly but the updating of data is taking me longer than the whole initial page draw code from scratch!
Thanks
--Chris
If I understand the code and the description right, the nodeArray has some items with class node and others with class realnode. When you create the nodes, you are selecting only the nodes of class node, this allows the creation of groups of each class, but bounds the selection only to the groups of class node. I think that you need to create the nodes with both classes
nodes = vis.selectAll('g')
.data(nodeArray)
...
.call(force.drag);
And then operate over selections based in the class of the elements:
nodes.selectAll('g.realnode')
// ... set attributes here
Also, I would suggest using css for setting the attributes of link and node classes:
links = vis.selectAll('line')
.classed('real-link', function(d) { return d.class == 'link reallink'; });
and in the css:
svg .real-link {
stroke-opacity: 0.8;
stroke-width: 3;
}
Related
I am attempting to create a fully dynamic Sunburst graph using d3.js.
The examples and tutorials I have located tend to use existing/fully-populated data structures which may have the ability to modify the value of existing arcs but does not allow the ability to add child arcs as needed.
Likewise the tutorials I have located which allow new datasets simply replace the existing structure and begin drawing from scratch.
This is not the behavior I am trying to implement.
What I need is a dynamically built graph based on incoming data as it is provided.
I am able to append children to the end of the data set, transition and render the results without issue. The problem occurs any time I insert a child somewhere within the existing structure, d3’s selectAll() does not function as expected. It includes the new arc (which has yet to be drawn) resulting in any remaining arcs being rendered incorrectly. Then when transitioning the arcs it seems to get the arcs Dom ID and data it supposedly represents gets mixed up. The new arc is not visible and an empty space exists where new arc should be placed.
To be clear my intent is:
Add to the existing data structure allowing new children to be added when new information is provided
To transition existing arcs opening space for the new arcs before they are created and drawn
Broken down into four steps of the jsfiddle example:
Initialization of the graph (draws an invisible “root” arc)
{ name:"a_0", children: [] }
Adding First Child data and it’s children to root
{ name:"a_0", children:[
{ name:"a_1", children:[ { name:"a_2", children:[ { name:"a_3" } ] } ] }
] }
Adding Second Child and underlying children to root
{ name:"a_0", children:[
{ name:"a_1", children:[ { name:"a_2", children:[ { name:"a_3" } ] } ] },
{ name:"a_4", children:[ { name:"a_5", children:[ { name:"a_6" } ] } ] }
] }
Inserting another child within the existing arc a_2
{ name:"a_0", children:[
{ name:"a_1", children:[
{ name:"a_2", children:[
{ name:"a_3" },
{ name:"a_7" }
] }
] },
{ name:"a_4", children:[
{ name:"a_5", children:[
{ name:"a_6" }
] }
] }
] }
Step 1 works just fine
Step 2 draws the arcs properly
Step 3 transitions the existing arcs and adds the new arcs to the graph
Step 4 results some unexpected behavior.
During the transition of existing and entering of new arcs some of the arcs "jump around" losing the proper association with their respective data
The end result appears to be:
a_0 - is correct
a_1 & a_2 - look correct
a_3 - has shrunk to accommodate the new sibling a_7 - expected behavior
a_4 - disappears
a_5 - jumps down where a_4 should be
a_6 - (looks like) it is duplicated and exists once where it should be and where a_5 should be
a_7 - not displayed, location where it should be is empty space and appears to be associated with a_6 data
What the end result looks like and what is really going on are not the same.
In the attempt to update the graph the selectAll() for the existing arcs includes (a_0, a_1, a_2, a_3, a_4, a_5, a_7). Where the existing a_6 is not included in the selectAll() but a_7 (which has not been drawn) is.
The enter() function appears to operate on the existing a_6 which is then treated as a new arc
It looked like I was on the right track getting all the way to a_6, but I have not figured out the reason for the behavior when adding a_7.
The jsFidde executes the steps as described above including:
Unique colors for each arc
A table displaying the name of each arc,
If the arc is being handled by d3js' selectAll() (i.e. "existing") or enter() (i.e. "new"),
The d3 Index as it is currently being assigned when drawing existing or new arcs.
Expected target position where each arc should appear after any transitioning,
Arctween information as an Arc is being transitioned from its former location to the new location and
Questions:
What is going on that would cause this behavior in Step 4?
Is there a way to ensure the integrity between each arc and the data it represents?
Is there a way to insert children into the existing structure or update the graph in this dynamic manor?
Working example on jsfiddle https://jsfiddle.net/mfitzgerald/j2eowwya/
var dataObj = { name:"a_0", color: "none" };
var height = 300;
var width = 500;
var radius = Math.min(width, height) / 2;
var graph = d3.select("#graph")
.attr('height', height)
.attr('width', width)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var partition = d3.layout.partition()
.sort(null)
.size([2 * Math.PI, radius * radius])
.value(function(d, i) { return 1; });
var arc = d3.svg.arc()
.startAngle(function(d) { if (isNaN(d.x)) { d.x = 0; } d.x0 = d.x; return d.x; })
.endAngle(function(d) { if (isNaN(d.dx)) { d.dx = 0; } d.dx0 = d.dx; return d.x + d.dx; })
.innerRadius(function(d) { if (isNaN(d.y)) { d.y = 0; } d.y0 = d.y; return Math.sqrt(d.y); })
.outerRadius(function(d) { if (isNaN(d.dy)) { d.dy = 0; } d.dy0 = d.dy; return Math.sqrt(d.y + d.dy); });
var arcTween = function(a) {
var i = d3.interpolate({x: a.x0, dx: a.dx0, y: a.y0, dy: a.dy0}, a);
return function(t) {
var b = i(t);
a.x0 = b.x;
a.dx0 = b.dx;
a.y0 = b.y;
a.dy0 = b.dy;
displayStats("arctween", b);
return arc(b);
};
}
// Root Arc
graph.datum(dataObj).selectAll('path.arc')
.data(partition.nodes)
.enter()
.append('path')
.attr('class', function(d) { return "arc " + d.name; })
.attr("d", arc)
.attr("id", function(d, i) { return "path_"+i; })
.attr("name", function(d) { return d.name; })
.style("fill", "none");
function updateGraph() {
console.log("Update Graph");
console.log(dataObj);
var update = graph.datum(dataObj).selectAll('path.arc')
.data(partition.nodes);
// Move existing Arcs
update.each(function(d, i) {
displayStats("target", d, i, "existing");
var domId = $(this).attr("id");
console.log("["+i+"] Exist Arc name:"+d.name+", dom_id:"+domId);
})
.transition()
.delay(function(d, i) { return i * 250; })
.duration(1500)
.attrTween("d", arcTween);
// Add New Arcs
update.enter().append('path')
.attr('class', function(d, i) { return "arc "+d.name; })
.attr("d", arc)
.attr("id", function(d, i) {
var domId = "path_"+i;
console.log("["+i+"] NEW Arc name:"+d.name+", dom_id:"+domId);
displayStats("target", d, i, "new");
return domId;
})
.style("stroke", "#fff")
.style("fill", function(d) { return d.color; })
.style("opacity", 0)
.transition()
.delay(function(d, i) { return i * 250; })
.duration(1500)
.style("opacity", .5)
.attrTween("d", arcTween);
}
#Gordon has answered the question. The issue was resolved by adding a key function when joining with .data() in the updateGraph code.
Forked example on jsfiddle
var update = graph.datum(dataObj).selectAll('path.arc')
.data(partition.nodes, function(d) { return d.name; } );
I believe the answers to the questions are:
The .data() function uses an indexed array which only uniquely identifies each arc given any new arcs are appended to the end of the array. Once one is inserted this would cause the data, graphed arcs and associated DOM ids to be misaligned.
Using the key function, as suggested by Gordon, allows unique identification of specific nodes keeping the Data and Graph in sync as expected.
Update
An additional modification would need to be made as the DOM id was set by the array index of the data element there would still be an invalid association with the DOM and the underlying graph/data.
This would result in 2 a_4 DOM id's. Instead of using the array index using the Node Name as the DOM id should keep this association correct.
I am getting data from backend, where some of the values are 0. I don't want to create the circle for those values.
I understand, I need to use a filter, but I don't have any idea how to use that.
Here is my code:
var circles = city.selectAll("circle")
.data(function(d) {
return d.values //this is a array length 20 how to filter that? i need only with values more than 0
})
.enter()
.append("circle")
.attr("r", 3.5)
.attr("cx", function(d, i) {
return x(d.date)
})
.attr("cy", function(d) {
return y(d.temperature)
})
.style('cursor', 'pointer')
.attr("fill", "transparent")
.attr("stroke", "yellow")
.attr("stroke-width", 0);
If d.values is an array you can use Array.prototype.filter() to extract all values different from 0 from it. The following will bind only non-zero values and create circles only for these:
.data(function(d) {
return d.values.filter(function(v) { return v != 0; });
})
Firstly, I'm assuming that you meant something more like .data(dataFromBackend, function (d) { return d.values; }), otherwise there's no bind!
Anyway, to do what you're describing, and making the assumption that you're not bothered about having the 0-values in the bind at all, you can simply do:
filteredData = backendData.filter(function (d) {
return d.value != 0;
});
There's a detailed reference for .filter here, but briefly, the function passed to .filter is called for every element in backendData, with each element referred to by d in the usual way. The return value should be boolean (true or false), but will work with anything, using 'truthiness'. Any element the function returns something 'truthy' for will be included in the final array (filteredData in my example), any element the function returned something 'falsey' will not.
I want to implement stack bar with toggle legend using D3.js ,on click on the legend, stack bar should get redrawn.If the legend was active,rectangle slab corresponding to the legend should get disappear and vise versa.
On click on the legend, I am not able to update the data binded with the group element and rect element present inside the group element properly.
In the DOM tree,on click on the legend,rect element is getting appended and added to first group element, rect element should actually get updated only.
You can view the source code in Jsfiddle here
I want something similar to stack bar with legend selection as implemented here in nvd3
function redraw() {
var legendselector = d3.selectAll("g.rect");
var legendData = legendselector.data();
var columnObj = legendData.filter(function(d, i) {
if (d.active == true)
return d;
});
var remapped = columnObj.map(function(cause) {
return dataArch.map(function(d, i) {
return {
x : d.timeStamp,
y : d[cause.errorType]
};
});
});
var stacked = d3.layout.stack()(remapped);
valgroup = stackBarGroup.selectAll("g.valgroup").data(stacked, function(d) {
return d;
}).attr("class", "valgroup");
valgroup.enter().append("svg:g").attr("class", "valgroup").style("fill",
function(d, i) {
return columnObj[i].color;
}).style("stroke", function(d, i) {
return d3.rgb(columnObj[i].color).darker();
});
valgroup.exit().remove();
rect = valgroup.selectAll("rectangle");
// Add a rect for each date.
rect = valgroup.selectAll("rectangle").data(function(d, i) {
return d;
}).enter().append('rect');
valgroup.exit().remove();
rect.attr("x", function(d) {
return x(d.x);
}).attr("y", function(d) {
return y(d.y0 + d.y);
}).attr("height", function(d) {
return y(d.y0) - y(d.y0 + d.y);
}).attr("width", 6);
}
function redraw() did not use transition inside it
You need to get more understanding about object constancy. (Three state described by the author)
I wrote an example of group chart in d3, the legend is interactable and works well, because i am new to d3, maybe the pattern or standard used is not very formal.
Listed it below only for you reference, hope it helps, good luck :-p
fiddle
I'm using a force directed graph that appends circles to each node.
As part of the node creation, I first set the radius "r" of each node circle to a default and consistent value (defaultNodeSize = 10). This successfully draws a cluster where all related nodes are the same size.
// Append circles to Nodes
node.append("circle")
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.attr("r", function(d) { if (d.id==focalNodeID) { return centerNodeSize; } else { return defaultNodeSize; } } ) // <---------------------------- Radius "r" set HERE!!!!
.style("fill", "White") // Make the nodes hollow looking
.attr("type_value", function(d, i) { return d.type; })
.attr("color_value", function(d, i) { return color_hash[d.type]; })
.attr("rSize", function(d, i) { return d.rSize; }) // <------------------ rSize HERE!!!!
.attr("id", "NODE" )
.attr("class", function(d, i) {
var str = d.type;
var strippedString = str.replace(/ /g, "_")
//return "nodeCircle-" + strippedString; })
if (d.id==focalNodeID) { return "focalNodeCircle"; }
else { return "nodeCircle-" + strippedString; }
})
.style("stroke-width", 5) // Give the node strokes some thickness
.style("stroke", function(d, i) { return color_hash[d.type]; } ) // Node stroke colors
.call(force.drag);
Also, upon creation, I set an attribute called "rSize", which specifies that node's absolute magnitude. Each node has a different rSize and rSize is different than the defaultNodeSize. The purpose of rSize is so that I can access it, later, and dynamically change the circle's radius from it's defaultNodeSize to it's rSize (or the reverse) allowing each node to expand or contract, based on controllers.
In a separate controller function, I later select all nodes I want to apply the new rSize to. Selecting them is easy...
var selectedNodeCircles = d3.selectAll("#NODE");
However, I don't know what the syntax is to read each node's rSize and apply rSize to that specific node that's being changed. I would think that it's something like...
selectedNodeCircles.("r", function(){ return this.attr("rSize"); });
In other word's, I'd like to retrieve that specific node's "rSize" attribute value and set the attribute "r" to the value retrieved from "rSize".
Any idea of what the correct syntax is to do this?
Thanks for any help you can offer!
You are looking for the getAttribute() function.
So something like this should work for you:
selectedNodeCircles.attr("r", function() {return this.getAttribute("rSize")})
Remember that this in the function, is the circle itself and hence simply an element in the DOM, to the best of my understanding.
You can confirm this by simply printing out this using console.log(this) right before the result statement.
Hope this helps.
I'm just starting out with D3.js. I've created a simple enough donut chart using this example. My problem is, if I have an array of objects as my data source - data points for ex. would be a1.foo or a1.bar - and I want to switch between them, how would i go about doing this? My current solution looks ugly and it can't be the proper way of doing it - code below.
//Call on window change event
//Based on some parameter, change the data for the document
//vary d.foo to d.bar and so on
var donut = d3.layout.pie().value(function(d){ return d.foo})
arcs = arcs.data(donut(data)); // update the data
Is there a way I can set the value accessor at run time other than defining a new pie function?
Generally to switch the data that is being displayed you would create a redraw() function that would then update the data for the chart. In the redraw you'll need to make sure to handle the three cases - what should be done when data elements are modified, what should be done when new data elements are added, and what should be done when data elements are removed.
It usually looks something like this (this example changes the data set through panning, but it doesn't really matter). See the full code at http://bl.ocks.org/1962173.
function redraw () {
var rects, labels
, minExtent = d3.time.day(brush.extent()[0])
, maxExtent = d3.time.day(brush.extent()[1])
, visItems = items.filter(function (d) { return d.start < maxExtent && d.end > minExtent});
...
// upate the item rects
rects = itemRects.selectAll('rect')
.data(visItems, function (d) { return d.id; }) // update the data
.attr('x', function(d) { return x1(d.start); })
.attr('width', function(d) { return x1(d.end) - x1(d.start); });
rects.enter().append('rect') // draw the new elements
.attr('x', function(d) { return x1(d.start); })
.attr('y', function(d) { return y1(d.lane) + .1 * y1(1) + 0.5; })
.attr('width', function(d) { return x1(d.end) - x1(d.start); })
.attr('height', function(d) { return .8 * y1(1); })
.attr('class', function(d) { return 'mainItem ' + d.class; });
rects.exit().remove(); // remove the old elements
}