D3 Tree Diagram Curved Line Start Position - javascript

Summary
Using the example below I've created a database driven tree view with blocks of information instead of the circle nodes.
Interactive d3.js tree diagram
Please see the example screenshot below:
The idea is for the lines to start from where the block ends. I assume it's something to do with the following function:
// Custom projection
var linkProjection = d3.svg.diagonal()
.source(function (d) {
return { "y": d.source.y, "x": d.source.x };
})
.target(function (d) {
return { "y": d.target.y, "x": d.target.x };
})
.projection(function (d) {
return [d.y, d.x];
});
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.style("fill", "none")
.style("stroke", "#d1d6da")
.style("stroke-width", "1")
.attr("d", function (d) {
var s = { x: source.x0, y: source.y0 };
var t = { x: source.x0, y: source.y0 };
return linkProjection({ source: s, target: t });
});
I have tried adding the block width to the y coordinate but although it starts from the correct position with the drawing it ends at the start of the block again.
Any suggestions?

Without more code it's hard to say, but I think it has to do with the coordinate system. x,y point to the center of the object I think. So you'll have to translate y to the the right by half the length of your box.
i.e. The parents previous position (x,y) points to the center of the parent.

Related

Draw an arc between two points

I'm working on a proof of concept for an application that I think D3 might be a good fit for. Since I'm new to D3 I thought I would start off simple and build my way up to the application requirements. However, I seem to be hitting a snag on what I believe should be a very easy task with this library. I want to place two small circles on an SVG and then draw an arc or curve between them. Based on the documentation, I believe arcTo would be the best fit for this since I know the start and end points. For the life of me I cannot get it to draw the arc. The circles are drawn perfectly every time though.
var joints = [{x : 100, y : 200, r : 5},
{x : 150, y : 150, r : 5}];
var svg = d3.select("svg");
svg.selectAll("circle")
.data(joints)
.enter().append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", function(d) { return d.r; });
svg.selectAll("path").append("path").arcTo(100,200,150,150,50)
.attr("class", "link");
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="960" height="800" />
I'm either going about this the wrong way or I don't fully understand how to append a path to an SVG. Can someone point me in the right direction? I haven't been able to find many examples of arcTo. Thank you!
You misunderstood what d3.path() is. According to the API:
The d3-path module lets you take [a HTML Canvas] code and additionally render to SVG.
And for d3.path():
d3.path(): Constructs a new path serializer that implements CanvasPathMethods.
As you can see, the d3-path module has only a bunch of methods that allow you to take a HTML canvas code and use it to draw SVG elements.
That being said, you cannot use arcTo straight away in the SVG, as you are doing right now. It should be:
var path = d3.path();
path.moveTo(100, 200);
path.arcTo(100,200,150,150,50)
... and then:
svg.append("path")
.attr("d", path.toString())
However, as an additional problem, arcTo is more complicated than that: the first two values are not the x and y of the starting point, but the coordinates of the first tangent.
Here is a demo, using different values for arcTo, which I think is what you want:
var joints = [{
x: 100,
y: 200,
r: 5
}, {
x: 150,
y: 150,
r: 5
}];
var svg = d3.select("svg");
svg.selectAll("circle")
.data(joints)
.enter().append("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", function(d) {
return d.r;
});
var path = d3.path();
path.moveTo(100, 200);
path.arcTo(100, 150, 150, 150, 50);
svg.append("path")
.attr("d", path.toString())
.attr("stroke", "firebrick")
.attr("stroke-width", 2)
.attr("fill", "none");
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="400" height="250" />
An easy alternative is simply dropping the d3.path() and doing all this using just SVG code. There are plenty of examples showing how to draw an SVG arc from point A to point B with a given radius.

D3 multiple markers at the same coordinate [duplicate]

How to apply force repulsion on map's labels so they find their right places automatically ?
Bostock' "Let's Make a Map"
Mike Bostock's Let's Make a Map (screenshot below). By default, labels are put at the point's coordinates and polygons/multipolygons's path.centroid(d) + a simple left or right align, so they frequently enter in conflict.
Handmade label placements
One improvement I met requires to add an human made IF fixes, and to add as many as needed, such :
.attr("dy", function(d){ if(d.properties.name==="Berlin") {return ".9em"} })
The whole become increasingly dirty as the number of labels to reajust increase :
//places's labels: point objects
svg.selectAll(".place-label")
.data(topojson.object(de, de.objects.places).geometries)
.enter().append("text")
.attr("class", "place-label")
.attr("transform", function(d) { return "translate(" + projection(d.coordinates) + ")"; })
.attr("dy", ".35em")
.text(function(d) { if (d.properties.name!=="Berlin"&&d.properties.name!=="Bremen"){return d.properties.name;} })
.attr("x", function(d) { return d.coordinates[0] > -1 ? 6 : -6; })
.style("text-anchor", function(d) { return d.coordinates[0] > -1 ? "start" : "end"; });
//districts's labels: polygons objects.
svg.selectAll(".subunit-label")
.data(topojson.object(de, de.objects.subunits).geometries)
.enter().append("text")
.attr("class", function(d) { return "subunit-label " + d.properties.name; })
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("dy", function(d){
//handmade IF
if( d.properties.name==="Sachsen"||d.properties.name==="Thüringen"|| d.properties.name==="Sachsen-Anhalt"||d.properties.name==="Rheinland-Pfalz")
{return ".9em"}
else if(d.properties.name==="Brandenburg"||d.properties.name==="Hamburg")
{return "1.5em"}
else if(d.properties.name==="Berlin"||d.properties.name==="Bremen")
{return "-1em"}else{return ".35em"}}
)
.text(function(d) { return d.properties.name; });
Need for better solution
That's just not manageable for larger maps and sets of labels. How to add force repulsions to these both classes: .place-label and .subunit-label?
This issue is quite a brain storming as I haven't deadline on this, but I'am quite curious about it. I was thinking about this question as a basic D3js implementation of Migurski/Dymo.py. Dymo.py's README.md documentation set a large set of objectives, from which to select the core needs and functions (20% of the work, 80% of the result).
Initial placement: Bostock give a good start with left/right positionning relative to the geopoint.
Inter-labels repulsion: different approach are possible, Lars & Navarrc proposed one each,
Labels annihilation: A label annihilation function when one label's overall repulsion is too intense, since squeezed between other labels, with the priority of annihilation being either random or based on a population data value, which we can get via NaturalEarth's .shp file.
[Luxury] Label-to-dots repulsion: with fixed dots and mobile labels. But this is rather a luxury.
I ignore if label repulsion will work across layers and classes of labels. But getting countries labels and cities labels not overlapping may be a luxury as well.
In my opinion, the force layout is unsuitable for the purpose of placing labels on a map. The reason is simple -- labels should be as close as possible to the places they label, but the force layout has nothing to enforce this. Indeed, as far as the simulation is concerned, there is no harm in mixing up labels, which is clearly not desirable for a map.
There could be something implemented on top of the force layout that has the places themselves as fixed nodes and attractive forces between the place and its label, while the forces between labels would be repulsive. This would likely require a modified force layout implementation (or several force layouts at the same time), so I'm not going to go down that route.
My solution relies simply on collision detection: for each pair of labels, check if they overlap. If this is the case, move them out of the way, where the direction and magnitude of the movement is derived from the overlap. This way, only labels that actually overlap are moved at all, and labels only move a little bit. This process is iterated until no movement occurs.
The code is somewhat convoluted because checking for overlap is quite messy. I won't post the entire code here, it can be found in this demo (note that I've made the labels much larger to exaggerate the effect). The key bits look like this:
function arrangeLabels() {
var move = 1;
while(move > 0) {
move = 0;
svg.selectAll(".place-label")
.each(function() {
var that = this,
a = this.getBoundingClientRect();
svg.selectAll(".place-label")
.each(function() {
if(this != that) {
var b = this.getBoundingClientRect();
if(overlap) {
// determine amount of movement, move labels
}
}
});
});
}
}
The whole thing is far from perfect -- note that some labels are quite far away from the place they label, but the method is universal and should at least avoid overlap of labels.
One option is to use the force layout with multiple foci. Each foci must be located in the feature's centroid, set up the label to be attracted only by the corresponding foci. This way, each label will tend to be near of the feature's centroid, but the repulsion with other labels may avoid the overlapping issue.
For comparison:
M. Bostock's "Lets Make a Map" tutorial (resulting map),
my gist for an Automatic Labels Placement version (resulting map) implementing the foci's strategy.
The relevant code:
// Place and label location
var foci = [],
labels = [];
// Store the projected coordinates of the places for the foci and the labels
places.features.forEach(function(d, i) {
var c = projection(d.geometry.coordinates);
foci.push({x: c[0], y: c[1]});
labels.push({x: c[0], y: c[1], label: d.properties.name})
});
// Create the force layout with a slightly weak charge
var force = d3.layout.force()
.nodes(labels)
.charge(-20)
.gravity(0)
.size([width, height]);
// Append the place labels, setting their initial positions to
// the feature's centroid
var placeLabels = svg.selectAll('.place-label')
.data(labels)
.enter()
.append('text')
.attr('class', 'place-label')
.attr('x', function(d) { return d.x; })
.attr('y', function(d) { return d.y; })
.attr('text-anchor', 'middle')
.text(function(d) { return d.label; });
force.on("tick", function(e) {
var k = .1 * e.alpha;
labels.forEach(function(o, j) {
// The change in the position is proportional to the distance
// between the label and the corresponding place (foci)
o.y += (foci[j].y - o.y) * k;
o.x += (foci[j].x - o.x) * k;
});
// Update the position of the text element
svg.selectAll("text.place-label")
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });
});
force.start();
While ShareMap-dymo.js may work, it does not appear to be very well documented. I have found a library that works for the more general case, is well documented and also uses simulated annealing: D3-Labeler
I've put together a usage sample with this jsfiddle.The D3-Labeler sample page uses 1,000 iterations. I have found this is rather unnecessary and that 50 iterations seems to work quite well - this is very fast even for a few hundred data points. I believe there is room for improvement both in the way this library integrates with D3 and in terms of efficiency, but I wouldn't have been able to get this far on my own. I'll update this thread should I find the time to submit a PR.
Here is the relevant code (see the D3-Labeler link for further documentation):
var label_array = [];
var anchor_array = [];
//Create circles
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("id", function(d){
var text = getRandomStr();
var id = "point-" + text;
var point = { x: xScale(d[0]), y: yScale(d[1]) }
var onFocus = function(){
d3.select("#" + id)
.attr("stroke", "blue")
.attr("stroke-width", "2");
};
var onFocusLost = function(){
d3.select("#" + id)
.attr("stroke", "none")
.attr("stroke-width", "0");
};
label_array.push({x: point.x, y: point.y, name: text, width: 0.0, height: 0.0, onFocus: onFocus, onFocusLost: onFocusLost});
anchor_array.push({x: point.x, y: point.y, r: rScale(d[1])});
return id;
})
.attr("fill", "green")
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r", function(d) {
return rScale(d[1]);
});
//Create labels
var labels = svg.selectAll("text")
.data(label_array)
.enter()
.append("text")
.attr("class", "label")
.text(function(d) {
return d.name;
})
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "black")
.on("mouseover", function(d){
d3.select(this).attr("fill","blue");
d.onFocus();
})
.on("mouseout", function(d){
d3.select(this).attr("fill","black");
d.onFocusLost();
});
var links = svg.selectAll(".link")
.data(label_array)
.enter()
.append("line")
.attr("class", "link")
.attr("x1", function(d) { return (d.x); })
.attr("y1", function(d) { return (d.y); })
.attr("x2", function(d) { return (d.x); })
.attr("y2", function(d) { return (d.y); })
.attr("stroke-width", 0.6)
.attr("stroke", "gray");
var index = 0;
labels.each(function() {
label_array[index].width = this.getBBox().width;
label_array[index].height = this.getBBox().height;
index += 1;
});
d3.labeler()
.label(label_array)
.anchor(anchor_array)
.width(w)
.height(h)
.start(50);
labels
.transition()
.duration(800)
.attr("x", function(d) { return (d.x); })
.attr("y", function(d) { return (d.y); });
links
.transition()
.duration(800)
.attr("x2",function(d) { return (d.x); })
.attr("y2",function(d) { return (d.y); });
For a more in depth look at how D3-Labeler works, see "A D3 plug-in for automatic label placement using simulated
annealing"
Jeff Heaton's "Artificial Intelligence for Humans, Volume 1" also does an excellent job at explaining the simulated annealing process.
You might be interested in the d3fc-label-layout component (for D3v5) that is designed exactly for this purpose. The component provides a mechanism for arranging child components based on their rectangular bounding boxes. You can apply either a greedy or simulated annealing strategy in order to minimise overlaps.
Here's a code snippet which demonstrates how to apply this layout component to Mike Bostock's map example:
const labelPadding = 2;
// the component used to render each label
const textLabel = layoutTextLabel()
.padding(labelPadding)
.value(d => d.properties.name);
// a strategy that combines simulated annealing with removal
// of overlapping labels
const strategy = layoutRemoveOverlaps(layoutGreedy());
// create the layout that positions the labels
const labels = layoutLabel(strategy)
.size((d, i, g) => {
// measure the label and add the required padding
const textSize = g[i].getElementsByTagName('text')[0].getBBox();
return [textSize.width + labelPadding * 2, textSize.height + labelPadding * 2];
})
.position(d => projection(d.geometry.coordinates))
.component(textLabel);
// render!
svg.datum(places.features)
.call(labels);
And this is a small screenshot of the result:
You can see a complete example here:
http://bl.ocks.org/ColinEberhardt/389c76c6a544af9f0cab
Disclosure: As discussed in the comment below, I am a core contributor of this project, so clearly I am somewhat biased. Full credit to the other answers to this question which gave us inspiration!
For 2D case
here are some examples that do something very similar:
one http://bl.ocks.org/1691430
two http://bl.ocks.org/1377729
thanks Alexander Skaburskis who brought this up here
For 1D case
For those who search a solution to a similar problem in 1-D i can share my sandbox JSfiddle where i try to solve it. It's far from perfect but it kind of doing the thing.
Left: The sandbox model, Right: an example usage
Here is the code snippet which you can run by pressing the button in the end of the post, and also the code itself. When running, click on the field to position the fixed nodes.
var width = 700,
height = 500;
var mouse = [0,0];
var force = d3.layout.force()
.size([width*2, height])
.gravity(0.05)
.chargeDistance(30)
.friction(0.2)
.charge(function(d){return d.fixed?0:-1000})
.linkDistance(5)
.on("tick", tick);
var drag = force.drag()
.on("dragstart", dragstart);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.on("click", function(){
mouse = d3.mouse(d3.select(this).node()).map(function(d) {
return parseInt(d);
});
graph.links.forEach(function(d,i){
var rn = Math.random()*200 - 100;
d.source.fixed = true;
d.source.px = mouse[0];
d.source.py = mouse[1] + rn;
d.target.y = mouse[1] + rn;
})
force.resume();
d3.selectAll("circle").classed("fixed", function(d){ return d.fixed});
});
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
var graph = {
"nodes": [
{"x": 469, "y": 410},
{"x": 493, "y": 364},
{"x": 442, "y": 365},
{"x": 467, "y": 314},
{"x": 477, "y": 248},
{"x": 425, "y": 207},
{"x": 402, "y": 155},
{"x": 369, "y": 196},
{"x": 350, "y": 148},
{"x": 539, "y": 222},
{"x": 594, "y": 235},
{"x": 582, "y": 185}
],
"links": [
{"source": 0, "target": 1},
{"source": 2, "target": 3},
{"source": 4, "target": 5},
{"source": 6, "target": 7},
{"source": 8, "target": 9},
{"source": 10, "target": 11}
]
}
function tick() {
graph.nodes.forEach(function (d) {
if(d.fixed) return;
if(d.x<mouse[0]) d.x = mouse[0]
if(d.x>mouse[0]+50) d.x--
})
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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
function dblclick(d) {
d3.select(this).classed("fixed", d.fixed = false);
}
function dragstart(d) {
d3.select(this).classed("fixed", d.fixed = true);
}
force
.nodes(graph.nodes)
.links(graph.links)
.start();
link = link.data(graph.links)
.enter().append("line")
.attr("class", "link");
node = node.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 10)
.on("dblclick", dblclick)
.call(drag);
.link {
stroke: #ccc;
stroke-width: 1.5px;
}
.node {
cursor: move;
fill: #ccc;
stroke: #000;
stroke-width: 1.5px;
opacity: 0.5;
}
.node.fixed {
fill: #f00;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<body></body>

Edges for Collapsible tree D3

I am having some issues with the edges for the collapsible tree in D3. I have tried swapping the x and y positions and was able to move the nodes but not the edges? I tried to swap the x0 and y0 but still not working?
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {x: source.x0, y: source.y0};
return diagonal({source: o, target: o});
});
http://jsfiddle.net/6FkBd/383/
You had to change this values of x and y too.
Check this one out http://jsfiddle.net/6FkBd/394/
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.x, d.y]; });
By the way, I think we both working on same thing, I am trying to create OrgChart that will support collapsing/expanding. But not being lucky yet.

Adding multiple objects to svg and adding drag behaviour issue

I am totally new to SVG and d3 library. I need to add dynamically add 5 circle to svg which all contains draggable event handler. I have written code for adding a single circle and adding draggable behaviour to the same and its working fine. Now I am trying the same thing inside for loop in order to add 5 circle. It displays all circle but when I drag a particular circle and put it some where then it stays there and again when i touch another circle old circle get vanished from the position where we placed and appears on new circle where we started next. Please have a look at below mentioned code. Any help regarding this would be appreciated.
function addCircles()
{
var box = d3.select(".box");
for(var i = 0;i<5;i++)
{
var drag = d3.behavior.drag()
.on('dragstart', function() { console.log("dragstart"); circle.style('fill', 'red'); })
.on('drag', function() { console.log("drag X - " + d3.event.x + " Y - " + d3.event.y); circle.attr('cx', d3.event.x)
.attr('cy', d3.event.y); })
.on('dragend', function() { console.log("dragend - " + d3.event.x);
circle.style('fill', 'green'); });
var circle = box.selectAll('.draggableCircle'+i)
.data([{ x: i*15, y: i*15, r: 10 }])
.enter()
.append('svg:circle')
.attr('class', 'draggableCircle'+i)
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', function(d) { return d.r; })
.call(drag)
.style('fill', 'green');
}
}
I have checked out after debugging code in chrome and found out that position of dragEnd is not being detected.
Your fiddle didnt work so I had to make my own from the code provided : http://jsfiddle.net/Qh9X5/6932/
Create the data first then draw circles from that data, rather than doing it all at once.
var nodeData = [];
for(var i = 1;i<15;i++) //change the value 15 to however many circles you want
{
nodeData.push({
x:i*15,
y:i*15,
r:10
})
}
Then use this data to create circles :
var circle = box.selectAll('.draggableCircle'+i)
//.data([{ x: i*15, y: i*15, r: 10 }])
.data(nodeData)
.enter()
.append('svg:circle')
.attr('class', function(d,i){
return 'draggableCircle'+i; //changed this to use i in the loop
//through the nodes not i in the for loop
})
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', function(d) { return d.r; })
.style('fill', 'green')
.call(drag)
Also your drag wasn't done correctly. You had this line :
circle.attr('cx', d3.event.x).attr('cy', d3.event.y);
You dont want this as your going through each circle and calling the drag on all of them. You only want to call it on the element you're 'dragging' like so :
function dragmove(d, i) //-updates the co-ordinates
{
d.x += d3.event.dx;
d.y += d3.event.dy;
d3.select(this).attr("transform", function(d,i)
{
return "translate(" + [ d.x,d.y ] + ")";
});
}
I think that was all the changes I made to make it work.
On a side note, with JSFiddle, you have to include the D3 library on the left hand side, other wise it wont work. Also, when calling your 'drawCircles()' function in the html, you have to change the loading of the fiddle otherwise it wont be able to find the function. Also, with all this said, if you were to use JSFiddle again in a question, please make sure it works before sending SO users a link.
EDIT
I added this line to get the correct circle positions on load :
circle.attr("transform", function(d){
return "translate(" + [ d.x,d.y ] + ")";
})
Now the drag works perfectly :)) Hope this helps

Display D3 Tree Layout in Linear Format

I am following the D3 Collapsible Tree Layout guide and am trying to change the layout of the tree from
To a linear format, where the 'root' node is the left-most node on the tree
I don't know much about D3 just yet but I assume the d3.diagonal() function along with the nodes x & y parameters control the lines and node position. Any input or guides on this that would point me in the right direction?
do this
self.diagonal = d3.svg.line().interpolate('step')
.x(function (d) { return d.x; })
.y(function (d) { return d.y; });
And then use the generator like this:
link.enter().append('svg:path', 'g')
.duration(self.duration)
.attr('d', function (d) {
return self.diagonal([{
y: d.source.x,
x: d.source.y
}, {
y: d.target.x,
x: d.target.y
}]);
});

Categories

Resources