I'm using D3 library to draw an interconnected graph of elements. My nodes are circles and rects connected by oriented line paths.
My problem is that lines pointing to rects element have an ugly visualisation because the line ends on the top-left corner of the rect rather then the center of it (as it does for circles).
How can I make path lines target the center of both circles elements and rect elements?
Code for definition of defs arrow heads:
svg.append('defs')
.append('marker')
.attr('id', 'arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 17) // As far as I understood this provides the distance from the end of the path line.
.attr('refY', -0.1)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.attr('fill', function() {
return 'red';
})
.append('path')
.attr('d', 'M0,-5L10,0L0,5');
Definition of oriented links:
let links = svg.selectAll('.link')
.data(data.links)
.enter()
.append('path')
.attr('id', function (d) {
return d.id;
})
.attr('class', 'link')
.attr('fill', 'none')
.attr('stroke-width', 1.2)
.attr('marker-end', 'url(#arrow)')
.attr('stroke', function() {
return 'blue';
})
.style('cursor', 'pointer');
Definition of squares
let squares = svg.selectAll('.square')
.data(data.squares, function(d) {
return d.id;
})
.enter().append('g')
.call(dragger)
.attr('class', 'square')
.style('cursor', 'pointer');
squares.append('rect')
.attr('width', 10)
.attr('height', 10)
.attr('fill', function (d) {
return '#fff';
})
.style('opacity', 0.1)
.style('stroke', function() {
return '#555';
})
.style('stroke-width', '2');
In following screenshot you can see how it behaves. Circles and rects have a low opacity to show up the issue with the path target.
UPDATE
Added tick function definition and usage.
simulation
.nodes(data.nodes)
.on('tick', _tick);
simulation
.force('link')
.distance(80)
.links(data.links);
simulation.alpha(1).restart();
function _tick() {
links.attr('d', function(d) {
let dx = d.target.x - d.source.x;
let dy = d.target.y - d.source.y;
let dr = Math.sqrt(dx * dx + dy * dy);
return ('M' + d.source.x + ',' + d.source.y +
'A' + dr + ',' + dr + ' 0 0,1 ' + d.target.x + ',' + d.target.y);
});
circles.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
squares.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
}
What you have right now is the expected behaviour. In a force simulation (I suppose you're running a force simulation), the tick function changes the x and y properties of the datum object, and you can use them the way you want.
As you didn't shared your tick function, I suppose that you are updating the rectangles' x and y position like this:
squares.attr("x", function(d) {
return d.x
}).attr("y", function(d) {
return d.y
});
If that in fact is correct, the top-left corner of the rectangles correspond to d.x and d.y coordinates. And, since you're using the same properties to draw the path, the paths will go from one top-left corner to the other.
This is easy to show, have a look at this demo:
var width = 200;
var height = 200;
var rectSide = 40;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var nodes = [{
name: "foo",
color: "blue"
}, {
name: "bar",
color: "green"
}, {
name: "baz",
color: "red"
}];
var links = [{
"source": 0,
"target": 1
}, {
"source": 0,
"target": 2
}];
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().distance(100))
.force("charge", d3.forceManyBody().strength(-50))
.force("center", d3.forceCenter(width / 2, height / 2));
var node = svg.selectAll(null)
.data(nodes)
.enter()
.append("rect")
.attr("width", rectSide)
.attr("height", rectSide)
.attr("fill", function(d) {
return d.color
});
var link = svg.selectAll(null)
.data(links)
.enter()
.append("line")
.style("stroke", "#222")
.style("stroke-width", 2);
simulation.nodes(nodes);
simulation.force("link")
.links(links);
simulation.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("x", function(d) {
return d.x
}).attr("y", function(d) {
return d.y
});
});
<script src="https://d3js.org/d3.v4.js"></script>
Solution: You can either move the rectangles or the paths.
As your question specifically asks about the paths, the solution is simple: add half-width and half-height to the target and source coordinates:
link.attr("x1", function(d) {
return d.source.x + rectangleWidth / 2;
})
.attr("y1", function(d) {
return d.source.y + rectangleHeight / 2;
})
.attr("x2", function(d) {
return d.target.x + rectangleWidth / 2;
})
.attr("y2", function(d) {
return d.target.y + rectangleHeight / 2;
})
Here is a demo:
var width = 200;
var height = 200;
var rectSide = 40;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var nodes = [{
name: "foo",
color: "blue"
}, {
name: "bar",
color: "green"
}, {
name: "baz",
color: "red"
}];
var links = [{
"source": 0,
"target": 1
}, {
"source": 0,
"target": 2
}];
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().distance(100))
.force("charge", d3.forceManyBody().strength(-50))
.force("center", d3.forceCenter(width / 2, height / 2));
var node = svg.selectAll(null)
.data(nodes)
.enter()
.append("rect")
.attr("width", rectSide)
.attr("height", rectSide)
.attr("fill", function(d) {
return d.color
});
var link = svg.selectAll(null)
.data(links)
.enter()
.append("line")
.style("stroke", "#222")
.style("stroke-width", 2);
simulation.nodes(nodes);
simulation.force("link")
.links(links);
simulation.on("tick", function() {
link.attr("x1", function(d) {
return d.source.x + rectSide / 2;
})
.attr("y1", function(d) {
return d.source.y + rectSide / 2;
})
.attr("x2", function(d) {
return d.target.x + rectSide / 2;
})
.attr("y2", function(d) {
return d.target.y + rectSide / 2;
})
node.attr("x", function(d) {
return d.x
}).attr("y", function(d) {
return d.y
});
});
<script src="https://d3js.org/d3.v4.js"></script>
Can you translate the square? If you can translate it half of the square
width.
Can you find the end of that path on square it like the x2,y2? Plus that half of your square width,.
"hope this inspired you"
squares.append('rect')
.attr('width', 10)
.attr('height', 10)
.attr("transform", "translate(-5,0)")
.attr('fill', function (d) {
return '#fff';
})
Related
I have been stuck on this problem from the other day, but unfortunately haven't been able to reach a solution. I am trying to achieve a behavior shown here, but in a way to generally do this for various containers depending on properties of each node. I wanted to reach out and ask if there was any known way to do this generally.
Below is my JSFiddle is an example of what I currently have - a number of nodes assigned to a random group number and a barView function which separates these nodes dependent on their groups. I hope to have these nodes be confined within the dimensions of each of their respective bars, such that dragging these nodes cannot remove them from their box, but they can move within it (bouncing off each other). I would really appreciate your help in this.
For simplicity, I made the bars related to a 'total' field in each node (to show the bars in the SVG dimensions), but these would be related to the size in my implementation, similar to a volume.
I have been able to organize the x-positions of the nodes by using the following code, where position is based on the group:
simulation.force('x', d3.forceX().strength(1).x(function(d) {
return xscale(d.group); // xvariable
}));
With this code, I am unsure how to work within the dimensions of the rectangles, or maintain a boundary that circles can bounce within. I would appreciate your help on this!
Thank you so much!
My fiddle: http://jsfiddle.net/abf2er7z/2/
One possible solution is setting a new tick function, which uses Math.max and Math.min to get the boundaries of those rectangles:
simulation.on("tick", function() {
node.attr("cx", function(d) {
return d.x = Math.min(Math.max(xscale(d.group) - 20 + d.radius, d.x), xscale(d.group) + 20 - d.radius);
})
.attr("cy", function(d) {
return d.y = Math.min(Math.max(0.9 * height - heightMap[d.group] + d.radius, d.y), height - d.radius);
});
});
Here is the demo:
var width = 900,
height = 400;
var groupList = ['Group A', 'Group B', 'Group C', 'Group D'];
var data = d3.range(200).map(d => ({
id: d,
group: groupList[getRandomIntegerInRange(0, 3)],
size: getRandomIntegerInRange(1, 100),
total: getRandomIntegerInRange(1, 10)
}))
var svg = d3.select("body")
.append("svg")
.attr("viewBox", "0 0 " + (width) + " " + (height))
.attr("preserveAspectRatio", "xMidYMid meet")
.attr('width', "100%")
.attr('height', height)
.attr('id', 'svg')
.append('g')
.attr('id', 'container')
.attr('transform', 'translate(' + 0 + ', ' + 0 + ')');
simulation = d3.forceSimulation();
data.forEach(function(d, i) {
d.radius = Math.sqrt(d['size']);
});
colorScale = d3.scaleOrdinal(d3.schemeCategory10);
node = svg.append("g")
.attr("class", "node")
.selectAll(".bubble")
.data(data, function(d) {
return d.id;
})
.enter().append("circle")
.attr('class', 'bubble')
.attr('r', function(d) {
return d.radius;
}) // INITIALIZED RADII TO 0 HERE
.attr("fill", function(d) {
// initially sets node colors
return colorScale(d.group);
})
.attr('stroke-width', 0.5)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
function dragstarted(d) {
if (!d3.event.active) {
simulation.alpha(.07).restart()
}
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alpha(0.07).restart()
d.fx = null;
d.fy = null;
// Update and restart the simulation.
simulation.nodes(data);
}
simulation
.nodes(data)
.force("x", d3.forceX().strength(0.1).x(width / 2))
.force("y", d3.forceY().strength(0.1).y(height / 2))
.force("collide", d3.forceCollide().strength(0.7).radius(function(d) {
return d.radius + 0.5;
}).iterations(2))
.on("tick", function() {
node
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
});
function barView() {
var buff = width * 0.12
var leftBuff = buff;
var rightBuff = width - buff;
var scale;
xscale = d3.scalePoint()
.padding(0.1)
.domain(groupList)
.range([leftBuff, rightBuff]);
// Save double computation below.
heightMap = {}
groupList.forEach(function(d) {
currVarTotal = data.filter(function(n) {
return n.group === d;
}).reduce(function(a, b) {
return a + +b.total;
}, 0);
heightMap[d] = currVarTotal;
})
var rects = svg.selectAll('.rect')
.data(groupList)
.enter()
.append('rect')
.attr('x', function(d) {
return xscale(d) - 20
})
.attr('y', function(d) {
return 0.9 * height - heightMap[d];
})
.attr('width', 40)
.attr('height', function(d) {
return heightMap[d];
})
.attr('fill', 'transparent')
.attr('stroke', function(d) {
return colorScale(d)
})
.attr('stroke-width', 2)
.attr('class', 'chartbars');
drawTheAxis(xscale);
simulation.force('x', d3.forceX().strength(1).x(function(d) {
return xscale(d.group); // xvariable
})).on("tick", function() {
node
.attr("cx", function(d) {
return d.x = Math.min(Math.max(xscale(d.group) - 20 + d.radius, d.x), xscale(d.group) + 20 - d.radius);
})
.attr("cy", function(d) {
return d.y = Math.min(Math.max(0.9 * height - heightMap[d.group] + d.radius, d.y), height - d.radius);
});
});
currHeights = {}
Object.keys(heightMap).forEach(d => {
currHeights[d] = 0.9 * height
});
// restart the simulation
simulation.alpha(0.07).restart();
function drawTheAxis(scale) {
var bottomBuffer = 0.9 * height;
// create axis objects
var xAxis = d3.axisBottom(xscale);
// Draw Axis
var gX = svg.append("g") // old: nodeG.append
.attr("class", "xaxis")
.attr('stroke-width', 2)
.attr("transform", "translate(0," + height + ")")
.attr('opacity', 0)
.call(xAxis)
.transition()
.duration(250)
.attr('opacity', 1)
.attr("transform", "translate(0," + bottomBuffer + ")");
}
}
function getRandomIntegerInRange(min, max) {
return Math.floor(Math.random() * (Math.floor(max) - Math.ceil(min) + 1)) + Math.ceil(min);
}
setTimeout(function() {
barView();
}, 1500);
<script src="https://d3js.org/d3.v5.min.js"></script>
Have in mind that this is not a final solution, but just a general guidance: the transition, the math (with those magic numbers) and the scales need to be improved.
I have created a force directed graph using d3.js. When new nodes are inserted to the graph, the graph starts moving on the white area as shown in this image. I would like to pinpoint the nodes (incoming and "old) dynamically in order to avoid problems of "cutting" some nodes of the viewBox, as shown in the image attached.
Any help?
My code so far is:
var w = 550;//1200,
h = 400;//750;
var zoom = d3.behavior.zoom()
.scaleExtent([0.1, 10])
.on("zoom", zoomed);
function zoomed() {
container.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
var testnode=[];
var color = d3.scale.category20();
var scale = 1200;
var vis = d3.select("#force")
.append("svg")
.attr("width", w) //w
.attr("height", h) //h
.attr("id", "svg")
.attr("group", "svg")
.attr("pointer-events", "all")
.attr("viewBox", "0 0 " + w + " " + h)
.call(zoom)
.attr("perserveAspectRatio", "xMinYMid meet")
.append('svg:g');
var force = d3.layout.force();
var nodes = force.nodes(),
links = force.links();
var update = function () {
var packetsReceivedArray = []
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 1e-6);
var link = vis.selectAll("line")
.data(links, function (d) {
return d.source.id + "-" + d.target.id;
});
var scale = d3.scale.linear()
.domain(d3.extent(links, function(d, i) {
return d.value;
}))
.range([1, 5]);
var scaleNode = d3.scale.linear()
.domain(d3.extent(nodes, function(d, i) {
return d.packetsReceived;
}))
.range([4, 16]);
link.enter().append("line")
.attr("id", function (d) {
return d.source.id + "-" + d.target.id;
})
.style("stroke-width", function(d) {
return scale(d.value) + "px";
})
.attr("class", "link");
link.append("title")
.text(function (d) {
return d.value;
});
link.exit().remove();
var node = vis.selectAll("g.node")
.data(nodes, function (d) {
return d.id;
});
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.call(force.drag);
nodeEnter.append("svg:circle")
.attr("x", 10)
.attr("y", ".31em")
.attr("class", "nodeStrokeClass")
.attr("data-legend",function(d) {
if(d.group == 0){
return 'Basic Node'
}
return 'Other nodes'
});
nodeEnter.append("svg:text")
.attr("class", "textClass")
.attr("x", 14)
.attr("y", ".31em")
legend = vis.append("g")
.attr("class","legend")
.attr("transform","translate(900,30)")
.style("font-size","14px")
.call(d3.legend)
node.exit().remove();
force.on("tick", function () {
node.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
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;
});
});
force
.gravity(.1)
.charge(-300)
.friction(0.15)
.linkDistance(60)
.size([w - 150, h - 150])
.start();
};
What I'm running now is showing a root node and bunch of nodes connected to it via d3 line element. I'd like to add a caption to the line, since I have it defined and can freely grab it from d.label (in the nodes section in JSON). I'm using the following code:
var width = 500,
height = 500;
var force = d3.layout.force()
.size([width, height])
.charge(-800)
.linkDistance(d => d.distance)
.on("tick", tick);
var svg = d3.select("#target").append("svg")
.attr("width", width)
.attr("height", height)
.attr("class", "mainsvg");
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
d3.json("test.json", function(error, graph) {
if (error) throw error;
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("a")
node.append("g")
.attr("class", "node");
node.append("circle")
.attr("class", "circle")
.attr("r", function(d) { return d.r })
.attr("fill", function(d) { return d.color })
.attr("stroke", function(d){ return d.color });
});
function tick() {
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; });
svg.selectAll(".circle").attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
svg.selectAll(".text").attr("x", function(d) { return d.x+d.xoffs; })
.attr("y", function(d) { return d.y+d.yoffs; });
}
Now I've tried using normal <text> elements appended to the edgepath in this fashion:
var edgepaths = svg.selectAll(".edgepath")
.data(graph.links)
.enter()
.append('path')
.attr({'d': function(d) {return 'M '+d.source.x+' '+d.source.y+' L '+ d.target.x +' '+d.target.y},
'class':'edgepath',
'fill-opacity':0,
'stroke-opacity':0,
'fill':'blue',
'stroke':'red',
'id':function(d,i) {return 'edgepath'+i}})
.style("pointer-events", "none");
var edgelabels = svg.selectAll(".edgelabel")
.data(graph.links)
.enter()
.append('text')
.style("pointer-events", "none")
.attr({'class':'edgelabel',
'id':function(d,i){return 'edgelabel'+i},
'dx':80,
'dy':10,
'font-size':10,
'fill':'black'});
edgelabels.append('textPath')
.attr('xlink:href',function(d,i) {return '#edgepath'+i})
.style("pointer-events", "none")
.text(function(d){return d.label});
However, the results I get are weird to say at best. Some labels are too close together, some are missing, but all of them are in the wrong place. How would I move them where they should be? To keep something in mind, I'm looking for a simple way to add caption to the paths I already have drawn out, nothing else. So the method I tried may have been too much, I'm not sure.
The problem is the dx in your edgelabels, that's always the same:
'dx': 80,
Assuming that you're using the code with the link distance (as in your last question), the solution is simple:
'dx':function(d){return d.distance/2},
Check the demo:
var nodes = [{
name: "a"
}, {
name: "b"
}, {
name: "c"
}, {
name: "d"
}, {
name: "e"
}, {
name: "f"
}];
var links = [{
source: 1,
target: 0,
distance: 80,
label: "foo"
}, {
source: 2,
target: 0,
distance: 40,
label: "bar"
}, {
source: 3,
target: 0,
distance: 180,
label: "baz"
}, {
source: 4,
target: 0,
distance: 80,
label: "foobar"
}, {
source: 5,
target: 0,
distance: 90,
label: "foobaz"
}]
var width = 400
height = 300;
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([width, height])
.linkDistance(d => d.distance)
.charge(-1000)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link")
.data(force.links())
.enter().append("line")
.attr("stroke", "black")
.attr("class", "link");
var edgepaths = svg.selectAll(".edgepath")
.data(force.links())
.enter()
.append('path')
.attr({
'd': function(d) {
return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y
},
'class': 'edgepath',
'fill-opacity': 0,
'stroke-opacity': 0,
'fill': 'blue',
'stroke': 'red',
'id': function(d, i) {
return 'edgepath' + i
}
})
.style("pointer-events", "none");
var edgelabels = svg.selectAll(".edgelabel")
.data(force.links())
.enter()
.append('text')
.style("pointer-events", "none")
.attr({
'class': 'edgelabel',
'id': function(d, i) {
return 'edgelabel' + i
},
'dx': function(d) {
return d.distance / 2
},
'dy': 10,
'font-size': 12,
'fill': 'black'
});
edgelabels.append('textPath')
.attr('xlink:href', function(d, i) {
return '#edgepath' + i
})
.style("pointer-events", "none")
.text(function(d) {
return d.label
});
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", (d, i) => i ? 10 : 16)
.style("fill", (d, i) => i ? "teal" : "brown");
function tick() {
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 + ")";
});
edgepaths.attr('d', function(d) {
var path = 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;
return path
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
text cannot be a child of path see this question path is not a container... per SVG spec.
best practice would be to first append g elements to your SVG as containers for the bound data, then append the path and text elements to the g so that they are siblings. Then you have complete control over relative positioning, etc.
I am trying to alter the traditional zooming feature on a sunburst chart. Traditionally when you click on a partition, that partition grows to cover 100% of the base layer while all other partitions on the same layer disappear. The children of the selected partition all grow to fill the newly created space.
My current code does just what I stated above. I would like to alter my code to allow for the selected partition to only take up 75% of the base layer. The children elements will grow to cover this new space but the remaining 25% will still contain all other non-selected partitions.
I have tried altering the 't' value that is returned from d3.interpolate() but I have had unpredictable results.
I hope my description is clear.
Does anyone have any thoughts on this?
<script>
var width = 960,
height = 700,
radius = Math.min(width, height) / 2;
var x = d3.scale.linear()
.range([0, 2 * Math.PI]);
var y = d3.scale.linear()
.range([0, radius]);
var color = d3.scale.category20c();
function percent(d) {
var percentage = (d.value / 956129) * 100;
return percentage.toFixed(2);
}
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return "<strong>" + d.name + "</strong> <span style='color:red'>" + percent(d) + "%</span>";
})
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 10) + ")");
svg.call(tip);
var partition = d3.layout.partition()
.value(function(d) { return d.size; });
var arc = d3.svg.arc()
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
.innerRadius(function(d) { return Math.max(0, y(d.y)) })
.outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)) });
d3.json("flare.json", function(error, root) {
var g = svg.selectAll("g")
.data(partition.nodes(root))
.enter().append("g");
var path = g.append("path")
.attr("d", arc)
// .attr("stroke", 'black')
// .style("fill", function(d) { return color((d.children ? d : d.parent).name); })
.style("fill", function(d, i) {
return color(i);
})
.on("click", click)
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
var text = g.append("text")
.attr("transform", function(d) { return "rotate(" + computeTextRotation(d) + ")"; })
.attr("x", function(d) { return y(d.y); })
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.text(function(d) {
if (percent(d) > 1.35) {
return d.name;
}
})
.attr('font-size', function(d) {
if (d.value < 100000) {
return '10px'
} else {
return '20px';
}
})
.on("click", click)
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
function click(d) {
console.log(d)
// fade out all text elements
text.transition().attr("opacity", 0);
path
.transition()
.duration(750)
.attrTween("d", arcTween(d))
.each("end", function(e, i) {
// check if the animated element's data e lies within the visible angle span given in d
if (e.x >= d.x && e.x < (d.x + d.dx)) {
// get a selection of the associated text element
var arcText = d3.select(this.parentNode).select("text");
// fade in the text element and recalculate positions
arcText.transition().duration(750)
.attr("opacity", 1)
.attr("transform", function() { return "rotate(" + computeTextRotation(e) + ")" })
.attr("x", function(d) { return y(d.y); });
}
});
}
});
d3.select(self.frameElement).style("height", height + "px");
// Interpolate the scales!
function arcTween(d) {
console.log(d.name, x.domain())
console.log(d.name, y.domain())
console.log(d.name, y.range())
var xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
yd = d3.interpolate(y.domain(), [d.y, 1]),
yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
return function(d, i) {
return i
? function(t) { return arc(d); }
: function(t) {
console.log(t)
x.domain(xd(t));
y.domain(yd(t)).range(yr(t));
return arc(d);
};
};
}
function computeTextRotation(d) {
return (x(d.x + d.dx / 2) - Math.PI / 2) / Math.PI * 180;
}
I found the solution here: https://bl.ocks.org/mbostock/1306365. This example manages the zoom without getting rid of the sibling nodes.
I have developed a force layout to represent relationships between social groups. Now I would like to get the nodes to be distributed in a circle with links joining them. What is the best way to do this?
The complete version of the code (without data) is here http://jsfiddle.net/PatriciaW/zZSJT/
(Why do I have to include code here too? Here is the main portion)
d3.json("/relationships?nocache=" + (new Date()).getTime(),function(error,members){
var links=members.organizations.map(function(members) {
return members.member;
});
var nodes = {};
links.forEach(function(link) {
link.source = nodes[link.xsource] || (nodes[link.xsource] = {source: link.xsource, name: link.xsource, category: link.categorysource, path: link.pathsource, desc: link.descsource, title: link.titlesource});
link.target = nodes[link.xtarget] || (nodes[link.xtarget] = {target: link.xtarget, name: link.xtarget, category: link.categorytarget, path: link.pathtarget, desc: link.desctarget, title: link.titletarget});
});
force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.charge(-120)
.linkDistance(function() {return (Math.random() * 200) + 100;})
.linkStrength(0.5)
.on("tick", tick)
.start();
var link = svg.selectAll(".link")
.data(force.links())
.enter().append("line")
.attr("class", "link");
var node_drag = d3.behavior.drag()
.on("dragstart", dragstart)
.on("drag", dragmove)
.on("dragend", dragend);
var loading = svg.append("text")
.attr("x", width / 2)
.attr("y", height / 2)
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text("Simulating. One moment pleaseā¦");
function dragstart(d, i) {
force.stop() // stops the force auto positioning before you start dragging
}
function dragmove(d, i) {
d.px += d3.event.dx;
d.py += d3.event.dy;
d.x += d3.event.dx;
d.y += d3.event.dy;
tick(); // this is the key to make it work together with updating both px,py,x,y on d !
}
function dragend(d, i) {
d.fixed = true; // of course set the node to fixed so the force doesn't include the node in its auto positioning stuff
tick();
force.resume();
};
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", clickAlert)
.call(node_drag);
node.append("circle")
.attr("r", 8)
.style("fill", function(d) {
return categoryColour [d.category];
})
// add an image marker
node.append("image")
.attr("x",-8)
.attr("y",-8)
.attr("width", 16)
.attr("height", 16)
.attr("xlink:href", function(d) {
return categoryImage [d.category]
})
.on("click", clickAlert)
.style("cursor", "pointer")
node.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function(d) {
return d.name;
});
// Use a timeout to allow the rest of the page to load first.
setTimeout(function() {
// Run the layout a fixed number of times.
// The ideal number of times scales with graph complexity.
force.start();
for (var i = n * n; i > 0; --i) force.tick();
force.stop();
svg.selectAll("line")
.data(links)
.enter().append("line")
.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; });
svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 4.5);
loading.remove();
}, 10);
function tick() {
link
.attr("x1", function(d) {
return d.source.x + xadj; })
.attr("y1", function(d) {
return d.source.y + yadj; })
.attr("x2", function(d) {
return d.target.x +xadj; })
.attr("y2", function(d) {
return d.target.y +yadj; });
node
.attr("transform", function(d) {
return "translate(" + (d.x + xadj) + "," + (d.y + yadj) + ")";
});
};
function mouseover() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 16);
d3.select(this).select("text")
.attr("font-size","34px")
.style("font-weight", "bold");
};
function mouseout() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 8);
d3.select(this).select("text")
.attr("font-size","12px")
.style("font-weight", "normal");
};
}) // end json
Here's someone else's solution:
This network graph uses the D3 force layout to draw nodes and links, but instead of using d3.force() to find the best node positions, we draw an invisible arc and evenly places nodes along the circumference.
<!DOCTYPE html>
<html>
<head>
<script src="http://d3js.org/d3.v3.min.js"></script>
<meta charset="utf-8">
<title>JS Bin</title>
<style>
line.node-link, path.node-link {
fill: none;
stroke: black
}
circle.node {
fill: white;
stroke: black
}
circle.node+text {
text-anchor: middle;
}
text {
font-family: sans-serif;
pointer-events: none;
}
</style>
</head>
<body>
<script type="text/javascript">
// number of random nodes (gets crowded at >25 unless you change node diameter)
var num = 20;
// returns random int between 0 and num
function getRandomInt() {return Math.floor(Math.random() * (num));}
// nodes returns a [list] of {id: 1, fixed:true}
var nodes = d3.range(num).map(function(d) { return {id: d}; });
// links returns a [list] of {source: 0, target: 1} (values refer to indicies of nodes)
var links = d3.range(num).map(function(d) { return {source: getRandomInt(), target: getRandomInt()}; });
var width = 500,
height = 500;
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([width, height]);
// evenly spaces nodes along arc
var circleCoord = function(node, index, num_nodes){
var circumference = circle.node().getTotalLength();
var pointAtLength = function(l){return circle.node().getPointAtLength(l)};
var sectionLength = (circumference)/num_nodes;
var position = sectionLength*index+sectionLength/2;
return pointAtLength(circumference-position)
}
// fades out lines that aren't connected to node d
var is_connected = function(d, opacity) {
lines.transition().style("stroke-opacity", function(o) {
return o.source === d || o.target === d ? 1 : opacity;
});
}
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// invisible circle for placing nodes
// it's actually two arcs so we can use the getPointAtLength() and getTotalLength() methods
var dim = width-80
var circle = svg.append("path")
.attr("d", "M 40, "+(dim/2+40)+" a "+dim/2+","+dim/2+" 0 1,0 "+dim+",0 a "+dim/2+","+dim/2+" 0 1,0 "+dim*-1+",0")
.style("fill", "#f5f5f5");
force.start();
// set coordinates for container nodes
nodes.forEach(function(n, i) {
var coord = circleCoord(n, i, nodes.length)
n.x = coord.x
n.y = coord.y
});
// use this one for straight line links...
// var lines = svg.selectAll("line.node-link")
// .data(links).enter().append("line")
// .attr("class", "node-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; });
// ...or use this one for curved line links
var lines = svg.selectAll("path.node-link")
.data(links).enter().append("path")
.attr("class", "node-link")
.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" +
d.source.x + "," +
d.source.y + "A" +
dr + "," + dr + " 0 0,1 " +
d.target.x + "," +
d.target.y;
});
var gnodes = svg.selectAll('g.gnode')
.data(nodes).enter().append('g')
.attr("transform", function(d) {
return "translate("+d.x+","+d.y+")"
})
.classed('gnode', true);
var node = gnodes.append("circle")
.attr("r", 25)
.attr("class", "node")
.on("mouseenter", function(d) {
is_connected(d, 0.1)
node.transition().duration(100).attr("r", 25)
d3.select(this).transition().duration(100).attr("r", 30)
})
.on("mouseleave", function(d) {
node.transition().duration(100).attr("r", 25);
is_connected(d, 1);
});
var labels = gnodes.append("text")
.attr("dy", 4)
.text(function(d){return d.id})
</script>
</body>
</html>