I am trying to make a force graph with 2 types of shapes as nodes: rect and circle, the shape information is in d.shape. There are several threads out there, but the solutions are not very clear to me.
I tried first to use merge method, which does not work: in this jsbin, var circlesANDrects = rects shows rectangles and var circlesANDrects = circles shows circles, whereas var circlesANDrects = circles.merge(rects) does not show both.
Does anyone know how to fix this?
Otherwise, I think the idea solution would be to use one block and append different shapes according to the shape information:
var circlesANDrects = svg.append("g").selectAll("rect circle")
.data(force.nodes())
.enter()
<!-- a function that appends different shapes according to shape information -->
Does anyone know how to add cases / condition to append?
You can pass a function as an argument to append. According to the API:
If the specified type is a string, appends a new element of this type (tag name) as the last child of each selected element [...] Otherwise, the type may be a function which is evaluated for each selected element
The problem is, if you use a function, you cannot simply return "circle" or "rect", like this:
.append(function(d){
if(d.shape == "rect"){
return "rect";
} else {
return "circle";
}
});//this don't work...
Instead, you have to return the DOM element, something like this:
.append(function(d){
if(d.shape == "rect"){
return document.createElementNS("http://www.w3.org/2000/svg", "rect");
} else {
return document.createElementNS("http://www.w3.org/2000/svg", "circle");
}
});//this works...
As it is a little complicated, an easier solution (but not exactly following what you asked) is simply using a symbol here:
var circlesOrRects = svg.append("g").selectAll(".foo")
.data(force.nodes())
.enter()
.append("path")
.attr("d", d3.svg.symbol()
.type(function(d) { return d.shape == "rect" ? "circle" : "square"; }))
.call(force.drag);
Here is your Bin: https://jsbin.com/povuwulipu/1/edit
So, by the understanding the tick method in force layout.
tick method is a call back of force when each updation.
So, we need to update both rect and circle separately in your case. Because, You created and stored the circle and rect into two variables.
the working version of your code is here: https://jsbin.com/disayafube/1/edit?html,output
Related
I am drawing some complex interactive SVGs with D3 v4 and running into some problems. My goals are:
Each data element corresponds to a group with multiple SVG shape elements (e.g. <g><circle></circle><circle></circle></g>)
The multiple SVG shape elements have to be drawn in a certain order (because they overlap)
Certain shape elements are updated without data elements being added or removed (e.g. when clicking on a shape, change the shape color)
I am running into trouble because the .data() -> .exit().remove() -> .enter() -> .merge() process requires a specific order and that order conflicts with the necessary draw order as well as the ability to update styles on the fly. This is what I started with, which does not work because of draw order:
function updateGraph() {
let eachNodeG = allNodesG
.selectAll('.eachNodeG')
.data(graphData._nodes, function (d) {
return d._id;
})
eachNodeG.exit().remove();
let eachNodeGEnter = eachNodeG.enter()
.append('g')
.attr("class", "eachNodeG")
eachNodeGEnter
.append('circle')
.classed('interactivecircle', true)
.on('click', function (d) {...})
let eachNodeG = eachNodeGEnter
.merge(eachNodeG)
.style('fill', function (d) {...}) //this is here b/c it needs to change
// when data change (without them being added/removed)
// this must be separate because the background circle needs to change even
// when nodes are not added and removed; but this doesn't work here because
// the circle needs to be in the background
eachNodeG
.append('circle')
.classed('bgcircle', true)
}
I thought maybe I could separate the data update process from the data drawing process entirely, by doing enter() exit() merge() just on the groups containing the data and then drawing everything afterward. But here I run into a different problem: either I remove and re-add all of the shapes on every update (which makes double-clicking difficult and seems like a waste of processing power), or I have to figure out some way to update only the shapes that have changed. Does it using the remove and re-add method looks like this:
// add/remove individual groups based on updated data
let eachNodeG = allNodesG
.selectAll('.eachNodeG')
.data(graphData._nodes)
eachNodeG.exit().remove();
let eachNodeGEnter = eachNodeG.enter()
.append('g')
.attr("class", "eachNodeG")
eachNodeG = eachNodeGEnter
.merge(eachNodeG)
// draw (or remove and re-draw) elements within individual groups
d3.selectAll('.bgcircle').remove()
eachNodeG.append('circle')
.classed('bgcircle', true)
d3.selectAll('.interactivecircle').remove()
eachNodeG.append('circle')
.classed('interactivecircle', true)
.style('fill', function (d) {...})
.on('click',function(d){...})
})
Is there a better way to draw the shapes in order while keeping them updateable?
You could use selection.raise or selection.lower to move circles after they have been created.
I'm new to [d3.js][1], so my question may be stupid.
I divided my svg into regions, and I appended circles for the user to drag around. Every region has an id, and every circle has the id of the region in which it was created.
What I need is to update the DATUM linked to the circle in the drop event. The other way around is quite easy, since when you change the data, the update() event does all the work. But is there a way for the svg element to change the data?
EDIT:
Some of the code. I edited so it is cleaner and more direct. The circles call the drag object and everything works, but the TODO section needs to be... hum... done:
var drag;
function configDrag () {
drag = d3.behavior.drag();
drag.on("dragstart", function() {
d3.event.sourceEvent.stopPropagation();
})
.origin(function(d){
return d;
})
.on("dragstart", draggrab)
.on("drag", dragmove)
.on("dragend", dragdrop);
}
function dragdrop(d){
reg = regionOf(d.x, d.y); // null if in undefined region
if(reg){
d3.select(this)
.attr("cy", parseInt(reg.y1 + reg.y2) / 2);
var regionId = getRegionId(d.x, d.y);
// TODO update data.tsv with regionId
}
}
}
When you bind data to a selection using selection.data(values), values is always an array and normally an array of object elements. Those elements, or references to them, are bound to the DOM elements and can be retrieved using selection.datum(). They are also the d argument that is normally the first argument passed callbacks by the various d3 operator methods. Because they are objects, when you change the value of their members, you are changing the values of the members of the associated element, in the original values array that was bound.
I want to achieve something like a growing arc which indicates 5 levels (see picture). My data has only an integer value which is between 1-5. You can ignore the icon in the middle for now. Is there any possibility to achieve something like that in d3? I couldn't find any example for this. Moreover I tried it with a cut off pie (donut) chart approach, but I couldn't make the growing arc... I would appreciate any help! Thanks for that.
You can do this with d3 without dependency on external images, SVG sprites or anything in the DOM — just d3.js.
Here's a working fiddle. The implementation is explained below.
But also, here's a more advanced fiddle that animates a clip-path over the growing arc. Check out its predecessor to see how the mask looks without clipping.
First, you need to represent the graphics as an array of data that you bind to with d3. Specifically, you need a color and a "line command" (the string you assign to d as in <path d="...">. Something like this:
var segmentData = [
{ color:"#ED6000", cmd:"M42.6,115.3c5.2,1.5,11,2.4,16.8,2.4c1.1,0,2.7,0,3.7-0.1v-2.2c-7,0-13.1-1.3-18.8-3.6L42.6,115.3z" },
{ color:"#EF7D00", cmd:"M25.7,99.3c4.3,4.7,9.5,8.6,15.3,11.3l-1.4,3.8c-6.9-2.4-13.2-6.1-18.6-10.8L25.7,99.3z" },
{ color:"#F4A300", cmd:"M23.7,97c-5.2-6.4-8.8-14-10.3-22.4L2.9,75.7c2.9,10,8.5,18.9,15.8,25.9L23.7,97z" },
{ color:"#F7BC00", cmd:"M13,71.5c-0.2-2-0.4-4-0.4-6c0-10.7,3.4-20.6,9.2-28.8L9.4,28.3c-5.6,9-8.9,19.6-8.9,30.9 c0,4.6,0.6,9.1,1.6,13.5L13,71.5z" },
{ color:"#FFCF36", cmd:"M63,15.7V0.8c-1-0.1-2.5-0.1-3.7-0.1c-19.9,0-37.5,9.9-48.1,25l12.7,8.6C33.1,23,46,15.7,63,15.7z" }
];
Then you need an empty <svg> and probably a <g> within it, into which to draw the graphics:
var svg = d3.select("body").append("svg")
.attr("width", 125)
.attr("height", 125);
var gauge = svg.append("g");
Then you use d3 binding to create the segments:
var segments = gauge.selectAll(".segment")
.data(segmentData);
segments.enter()
.append("path")
.attr("fill", function(d) { return d.color; })
.attr("d", function(d) { return d.cmd; });
This just creates the graphic, but doesn't color it based on an integer value. For that, you can define an update function:
function update(value) {
segments
.transition()
.attr("fill", function(d, i) {
return i < value ? d.color : "#ccc";
})
}
Calling update(4) will color all but the last segment. Calling update(0) color none (leaving all of them gray).
In the fiddle, there's also a tick() function that calls update with a new value on a setTimeout basis, but that's just for demo.
Finally, if you wish, you can wrap all that code up and create a reusable component by following the advice in [this article].(http://bost.ocks.org/mike/chart/)
since it is relatively simple picture, I'd use a sprite, with 5 variations.
That would be much easier than using d3 and gives the same result.
(you could use some online tool like http://spritepad.wearekiss.com/ )
If you want to mimic duolingo progress images you can just simply copy their solution with own images. They are using sprites as this one: http://d7mj4aqfscim2.cloudfront.net/images/skill-strength-sprite2.svg not the d3.js approach. This will save you a lot of time and effort.
I am new to d3 and am using 'Interactive Data Visualization for the Web' by Scott Murray (which is great btw) to get me started. Now everything I saw so far works as described but something got me confused when looking at the procedure to create a new element. Simple example (from Scott Murray):
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle");
The name "circle" is used for the selectAll which returns an empty selection (which is ok as I learned). Then circles are appended by putting the same name into the .append. Great!
Now what got me confused was what happens when you want to do the same thing again. So you have a second dataset and want to generate new circles in the same way. Using the same code just replacing the dataset will obviously not work as the selectAll("circle") will not return an empty selection anymore. So I played around and found out that I can use any name in the selectAll and even leave it empty like this: selectAll()
Scott Murrays examples always just use one type (circle, text, etc.) per dataset. Finally I found in the official examples something like
svg.selectAll("line.left")
.data(dataset)
.enter()
.append("line")
.attr ...
svg.selectAll("line.right")
.data(dataset)
.enter()
.append("line");
.attr ...
Now my question: How is this entry in selectAll("ENTRY") really used? Can it be utilized later to again reference those elements in any way or is it really just a dummy name which can be chosen in any way and just needs to return an empty selection? I could not find this entry anywhere in the resulting DOM or object structure anymore.
Thank you for de-confusing me.
What you put in the selectAll() call before the call to .data() really only matters if you're changing/updating what's displayed. Imagine that you have a number of circles already and you want to change their positions. The coordinates are determined by the data, so initially you would do something like
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return d; })
.attr("cy", function(d) { return d; });
Now your new data has the same number of elements, but different coordinates. To update the circle positions, all you need to do is
svg.selectAll("circle")
.data(newData)
.attr("cx", function(d) { return d; })
.attr("cy", function(d) { return d; });
What happens is that D3 matches the elements in newData to the existing circles (what you selected in selectAll). This way you don't need to append the circles again (they are there already after all), but only update their coordinates.
Note that in the first call, you didn't technically need to select circles. It is good practice to do so however just to make clear what you're trying to do and to avoid issues with accidentally selecting other elements.
You can find more on this update pattern here for example.
I'm very new to D3 and Javascript, so forgive me if my code looks a little ugly or poorly organized.
I have been working on a plot that utilizes 3 metrics: an x and y axis, and the radius of the circle as a data metric for the plot. The data I am reading is a two dimensional array, with each row being a different metric, and each column being a new data point. I have successfully implemented a method to change the radius of the circle dynamically by picking a different metric from a drop box, but this was after struggling endlessly with a very particular issue - my data was being assigned to the wrong circle!
When I initially create my circles, I first use sort() to sort the circles in descending order from the default radius metric (in my code, its "impactcpu"). This was done to fix an issue where larger circles that were drawn after smaller circles were obstructing the smaller circles, so I wanted to "paint" the largest circles first.
I was able to get past this issue by first sorting my calculated data array before assignign it to the circles, which preserved the default order. However, I am now trying to do something similar with my X and Y axis. While my dropdown menu is correctly assigning metric values to circles, it is doing so to the WRONG circles. I have yet to figure out a solution to this issue, as re-sorting the array before assignign is like I was doing for the radius isn't working (which I expected). Are there any suggestions as to how I could ensure the right data point is assigned to the correct circle? Preferably one that wouldn't require an overhaul of the rest of my code :)
Please take a look at my jsfiddle for an example of my above situation:
http://jsfiddle.net/kingernest/YDQR4/3/
Example of how I am creating my circles initially:
var circles = svg.selectAll("circle")
.data(dataset, function(d) { return d.id })
.enter()
.append("circle")
.sort(function(a, b){ //Sort by radius size, helps reduce obstruction of circles
return d3.descending(a[14], b[14]);
})
.attr("cx", function(d){ //x axis is Req IO, col index 9
return xScale(d[9]);
})
.attr("cy", function(d){ //y axis is Req CPU, col index 8
return yScale(d[8]);
})
.attr("r", function(d){ //radius is based off Impact CPU, col 14
console.log("Rad: " + d[14])
return d[14] * 1.5;
})
.attr("class", "dataCircle")
etc
How I am currently altering my radius:
function changeRad() {
console.log(this.value);
var myRadData = [];
var index = metricHash[this.value];
var weight; //to adjust data to fit appropriately in graph
switch(this.value)
{
case "impactcpu":
weight = 1.5;
break;
case "spool":
weight = .0000001; //spool is normally a very large value
break;
case "pji":
weight = 8;
break;
case "unnecio":
weight = 12;
break;
case "duration":
weight = .0002;
break;
default: alert("Invalid value: " + this.value);
break;
}
for(var i=0; i < dataset.length; i++)
{
console.log(dataset[i][index]);
myRadData.push(dataset[i][index] * weight);
}
myRadData.sort(function(a,b){return b-a});
d3.selectAll("circle")
.data(myRadData)
.transition().duration(500)
.attr("r", function(d){
return d;
});
circles.data(dataset); //reassign old data set (with all data values)
}
There are two things I see with your code:
Inconsistent key function on your data binding - you correctly use it on your initial creation of the circles(.data(dataset, function(d) { return d.id })), but do not reference it when updating them, adding the same key on the updates will make sure that you are updating the same elements.
DOM sorting - Your use of selection.sort when initially creating your circles seems logical and appropriate(.sort(function(a, b){ return d3.descending(a[14], b[14]); })) I would recommending extending this to your update functions, rather than re-binding data.
I have made these quick updates to your code and it appears to solve your issues:
http://jsfiddle.net/AbHfk/3/
I'm not sure I grasp your entire code (it's quite long), but I think the solution lies along these lines:
1 - When you initially create the circles, use a keys function. Good, you're doing this:
.data(dataset, function(d) { return d.id; })
2 - Give the circles an ID attribute using the same function:
.attr("ID", function(d) { return d.id; })
3 - Then when you need to modify a particular circle individually you can select it like so:
svg.select('#' + myCircleID).attr('blahblah', somevalue)
I also notice that you've lost the ID attribute as you build up the myRadData array. This will prevent the code from joining them to the correct circles. Since you have an ID attribute at the beginning, you're better off using the keys function throughout, rather than trying to use sorting to make things line up.
If you want a more specific answer I think you need to boil the example down to the simplest possible form that reproduces the issue.