D3js Force-directed graph link intersections avoid - javascript

There is an example of force-directed graph i've tried to draw with the help of the d3.js.
I have 3 big questions at all. And this is the code (you can run code snippet below, it might works):
function getRandomInt(max, min = 0) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function fdSortShit(g, nodeDimensions) {
const gNodes = [];
const gLinks = [];
g.children().forEach(child => {
gNodes.push({
id: child,
w: nodeDimensions[child].w,
h: nodeDimensions[child].h,
radius:
Math.sqrt(
nodeDimensions[child].w * nodeDimensions[child].w + nodeDimensions[child].h * nodeDimensions[child].h
) / 2
});
});
g.edges().forEach(edge => {
gLinks.push({ source: edge.v, target: edge.w });
});
const data = {
nodes: gNodes,
links: gLinks
};
const nodes = data.nodes;
const links = data.links;
const linkNodeRad = 5;
const linkNodes = [];
links.forEach((link, idx) => {
if (link.source != link.target) {
linkNodes.push({
id: `link-node-${idx}`,
source: nodes.filter(e => {
return e.id == link.source;
})[0],
target: nodes.filter(e => {
return e.id == link.target;
})[0],
radius: linkNodeRad
});
}
});
const width = 800;
const height = 600;
var svg = d3
.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", "-400, -300, 800, 600");
function forceSimulation(nodes, links) {
return d3
.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter())
.force(
"collision",
d3.forceCollide().radius(function(d) {
return d.radius;
})
);
}
var link = svg
.selectAll(".link")
.attr("stroke", "#fff")
.data(links)
.enter()
.append("line")
.attr("class", "link");
var node = svg
.append("g")
.selectAll("g")
.data(nodes)
.enter()
.append("g");
var circles = node
.append("circle")
.attr("class", "node")
.attr("r", node => {
return node.radius;
});
var text = node
.append("text")
.text(d => {
return d.id;
})
.attr("class", "node-caption")
.attr("x", 0)
.attr("y", 0);
var linkNode = svg
.selectAll(".link-node")
.data(linkNodes)
.enter()
.append("circle")
.attr("class", "link-node")
.attr("r", linkNodeRad);
function ticked() {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
linkNode
.attr("cx", function(d) {
return (d.x = (d.source.x + d.target.x) * 0.5);
})
.attr("cy", function(d) {
return (d.y = (d.source.y + d.target.y) * 0.5);
});
}
forceSimulation(nodes.concat(linkNodes), links)
.on("tick", ticked)
.on("end", () => {
console.warn("END");
});
}
const coords = {};
const size = { min: 10, max: 30 };
const dotStr = "graph g { a--a;a--b;a--b;a--c;a--d;a--e;b--b1;c--c1;c--c2;d--d1;d--d2;d--d3;d--d4;e--e1;v--w;v--x;v--y;w--z;w--w1;x--x1;x--x2;y--y1;y--y2;y--y3;y--y4;z--z1;v--a; }";
const g = graphlibDot.read(dotStr);
g.children().forEach(child => {
const x = getRandomInt(1024 - 10, 10);
const y = getRandomInt(768 - 10, 10);
coords[child] = {
x: x,
y: y,
w: getRandomInt(size.max, size.min),
h: getRandomInt(size.max, size.min)
};
});
fdSortShit(g, coords);
svg {
background-color: lightgray;
}
circle.node {
fill: lightcoral;
}
circle.link-node {
fill: rgba(0, 0, 255, 0.2);
/* fill: transparent; */
}
line.link {
stroke: lightseagreen;
}
text.node-caption {
font: normal 10px courier new;
}
<script src="https://cdn.jsdelivr.net/npm/graphlib-dot#0.6.2/dist/graphlib-dot.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
The image looks like this:
The first question is: What about to avoid this intersections?
I know that I can't dodge all of edge intersections but I want to minimize them. This example is a tree-graph with no cycles. I know that there is a way to build it without edge crossings. But I don't know how to do it with this algorithm.
But still annoying intersection.
The second question is: What about NOT to simulate forces in-time (I need no animation) but just to draw final result? When I use forceSimulation.on("end", cb) it is great, but delay between start and stop is big.. but this is graph is just a small example. I can't wait so long on a bigger once.
And the third question is.. how to apply force-derected settings? Force energy, stiffness, repulsion, damping etc.? Can't find them on d3#5
The final result my project lead wants is:
no node overlap;
minimize edge-edge intersections;
minimize edge-node intersections.
I'm ready for dialog.

I solved this issue by playing with the forceCollide and forceLink distance parameters :
var simulation = d3.forceSimulation()
.force('link', d3.forceLink().id(d => d.id).distance(100).strength(1))
.force('charge', d3.forceManyBody()) // ^ change this value
.force('collide', d3.forceCollide(110)) // change this value
.force('center', d3.forceCenter(width / 2, height / 2));
The idea is to make unrelated nodes to repel each other, while keeping the link distance short.
It works very well in my case, but my node graph is much simpler than yours.

You apply the force settings in the initialization portion. Here is an example -
var simulation = d3.forceSimulation() //Modify link distance/strength here
.force("link", d3.forceLink().id(function (d) { return d.id; }).distance(80).strength(1))
.force("charge", d3.forceManyBody().strength(-15)) //Charge strength is here
.force("center", d3.forceCenter(width / 2, height / 2));
This can be used to solve one of your problems...if you set the "charge" strength to some large negative number like -150, the nodes will be strongly repelled such that they don't overlap, nor do any of their links. If you aren't looking to drag the graph at all then this should be all you need to avoid overlaps.
A side effect of a highly negative charge is that the graph settles quite quickly, and as you don't want to simulate the forces in real time after the initial display, you can call simulation.stop() to freeze or stop the simulation.

Related

How to add a tooltip to D3.js subplot

I'm currently working on trying to create a "facet plot" within d3.js, which I realize in D3 language isn't really a thing. However, I found this thread which outlined how to accomplish the task. From this post, I've converted the line chart from vertical to horizontal, and added a few extra padding elements where needed.
Now I would like to add a tooltip to each plot. From working with other pieces of code, this seems quite challenging. For some reason I can't figure out how to attach the tooltip to each individual plot. Any thoughts on how I might be able to accomplish this?
What it currently looks like:
// Data and manipluation:
const data = d3.range(25).map(i => ({
bib: Math.floor(i / 5) + 1,
ratio: -1 + Math.random() * 5,
run: [1, 2, 3, 4, 5][i % 5],
run: [1, 2, 3, 4, 5][i % 5],
name: ['metric1', 'metric2', 'metric3', 'metric4', 'metric5'][Math.floor(i / 5)]
}));
const grouped = d3.group(data,d=>d.bib);
// Dimensions:
const height = 800;
const width = 700;
const margin = {
top: 10,
left: 50,
right: 50,
bottom: 50
}
const padding = 30;
const doublePadding = padding * 2;
const plotHeight = (height-doublePadding)/grouped.size - padding;
const plotWidth = width-padding*2;
// const plotWidth = (width-padding)/grouped.size - padding;
// const plotHeight = height-padding*2;
const svg = d3.select("#chart1")
.append("svg")
.attr("width", margin.left+width+margin.right)
.attr("height", margin.top+height+margin.bottom+(padding*grouped.size));
const g = svg.append("g")
.attr("transform","translate("+[margin.left,margin.top]+")");
//Scales:
const xScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.run))
.range([0, plotWidth]);
const yScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.ratio))
.range([plotHeight,0]);
// Place plots:
const plots = g.selectAll(null)
.data(grouped)
.enter()
.append("g")
.attr("transform", function(d,i) {
return "translate("+[padding,i*(doublePadding+plotHeight)+padding]+")";
})
//Optional plot background:
plots.append("rect")
.attr("width",plotWidth)
.attr("height",plotHeight)
.attr("fill","#ddd");
// Plot actual data
plots.selectAll(null)
.data(d=>d[1])
.enter()
.append("circle")
.attr("r", 4)
.attr("cy", d=>yScale(d.ratio))
.attr("cx", d=>xScale(d.run))
// Plot line if needed:
plots.append("path")
.attr("d", function(d) {
return d3.line()
.x(d=>xScale(d.run))
.y(d=>yScale(d.ratio))
(d[1])
})
.attr("stroke", "#333")
.attr("stroke-width", 1)
.attr("fill","none")
// Plot names if needed:
plots.append("text")
.attr("x", plotWidth/2)
.attr("y", -10)
.text(function(d) {
return d[1][0].name;
})
.attr("text-anchor","middle");
// Plot axes
plots.append("g")
.attr("transform","translate("+[0,plotHeight]+")")
.call(d3.axisBottom(xScale).ticks(4));
plots.append("g")
.attr("transform","translate("+[-padding,0]+")")
.call(d3.axisLeft(yScale))
<head>
<!-- Load d3.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.6.1/d3.min.js"></script>
</head>
<body>
<div class="graph-container">
<div id="chart1"></div>
</div>
</body>
See a solution in a fiddle:
Add a g element with background and text(s):
const tooltip = d3.select('svg')
.append('g')
.style('visibility', 'hidden');
tooltip.append('rect')
.attr('width', 100)
.attr('height', 50)
.style('fill', '#fff')
.style('stroke', '#000')
tooltip.append('text')
.attr('x', 20)
.attr('y', 20)
Update it on mouseenter, mousevove, mouseleave:
plots.append("rect")
.attr("width",plotWidth)
.attr("height",plotHeight)
.attr("fill","#ddd")
.on("mouseenter", (e, d) => {
tooltip.style('visibility', 'visible');
tooltip.select('text').text(d[0]);
})
.on("mouseleave", () => tooltip.style('visibility', 'hidden'))
.on("mousemove", e => tooltip.attr('transform', `translate(${e.clientX},${e.clientY})`))

D3.js nodes jump when restarting simulation to add or remove nodes

I'm using d3.js v6 with a force layout to represent a network graph.
I'm adding and removing nodes but when I restart the simulation all the nodes jump to an upper left position and then come back to the original position.
I have this following code snippet that shows exactly what I mean, I've seen other examples online that work fine but haven't been able to find what I am doing wrong, any help is really appreciated.
var dataset = {
nodes: [
{
id: 1
},
{
id: 2
}
],
links: [{
id: 1,
source: 1,
target: 2
}]
};
let switchBool = false;
let svg = d3.select('svg')
.attr('width', '100%')
.attr('height', '100%');
const width = svg.node()
.getBoundingClientRect().width;
const height = svg.node()
.getBoundingClientRect().height;
console.log(`${width}, ${height}`);
svg = svg.append('g');
svg.append('g')
.attr('class', 'links');
svg.append('g')
.attr('class', 'nodes');
const simulation = d3.forceSimulation();
initSimulation();
let link = svg.select('.links')
.selectAll('line');
loadLinks();
let node = svg.select('.nodes')
.selectAll('.node');
loadNodes();
restartSimulation();
function initSimulation() {
simulation
.force('link', d3.forceLink())
.force('charge', d3.forceManyBody())
.force('collide', d3.forceCollide())
.force('center', d3.forceCenter())
.force('forceX', d3.forceX())
.force('forceY', d3.forceY());
simulation.force('center')
.x(width * 0.5)
.y(height * 0.5);
simulation.force('link')
.id((d) => d.id)
.distance(100)
.iterations(1);
simulation.force('collide')
.radius(10);
simulation.force('charge')
.strength(-100);
}
function loadLinks() {
link = svg.select('.links')
.selectAll('line')
.data(dataset.links, (d) => d.id)
.join(
enter => enter.append('line').attr('stroke', '#000000'),
);
}
function loadNodes() {
node = svg.select('.nodes')
.selectAll('.node')
.data(dataset.nodes, (d) => d.id)
.join(
enter => {
const nodes = enter.append('g')
.attr('class', 'node')
nodes.append('circle').attr('r', 10);
return nodes;
},
);
}
function restartSimulation() {
simulation.nodes(dataset.nodes);
simulation.force('link').links(dataset.links);
simulation.alpha(1).restart();
simulation.on('tick', ticked);
}
function ticked() {
link
.attr('x1', (d) => d.source.x)
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y);
node.attr('transform', (d) => `translate(${d.x},${d.y})`);
}
function updateData() {
switchBool = !switchBool;
if (switchBool) {
dataset.nodes.push({id: 3});
dataset.links.push({id: 2, source: 1, target: 3});
} else {
dataset.nodes.pop();
dataset.links.pop();
}
loadLinks();
loadNodes();
restartSimulation();
}
<script src="https://d3js.org/d3.v6.min.js"></script>
<div>
<button onclick="updateData()">Add/Remove</button>
<svg></svg>
</div>
It's because you use d3.forceCenter() which does not coerce nodes to a center point:
The centering force translates nodes uniformly so that the mean
position of all nodes (the center of mass if all nodes have equal
weight) is at the given position ⟨x,y⟩. (docs)
So if your two nodes are located at directly and equally below/above the centering point for d3.forceCenter, the mass is balanced. Introduce a new node and the entire force has to be transalted so that the center of mass is the center. This translation is the jump you are seeing.
Remove forceCenter and specify the center values with d3.forceX and d3.forceY, which do nudge nodes towards the specified x and y values:
var dataset = {
nodes: [
{
id: 1
},
{
id: 2
}
],
links: [{
id: 1,
source: 1,
target: 2
}]
};
let switchBool = false;
let svg = d3.select('svg')
.attr('width', '100%')
.attr('height', '100%');
const width = svg.node()
.getBoundingClientRect().width;
const height = svg.node()
.getBoundingClientRect().height;
console.log(`${width}, ${height}`);
svg = svg.append('g');
svg.append('g')
.attr('class', 'links');
svg.append('g')
.attr('class', 'nodes');
const simulation = d3.forceSimulation();
initSimulation();
let link = svg.select('.links')
.selectAll('line');
loadLinks();
let node = svg.select('.nodes')
.selectAll('.node');
loadNodes();
restartSimulation();
function initSimulation() {
simulation
.force('link', d3.forceLink())
.force('charge', d3.forceManyBody())
.force('collide', d3.forceCollide())
.force('forceX', d3.forceX().x(width/2))
.force('forceY', d3.forceY().y(height/2));
simulation.force('link')
.id((d) => d.id)
.distance(100)
.iterations(1);
simulation.force('collide')
.radius(10);
simulation.force('charge')
.strength(-100);
}
function loadLinks() {
link = svg.select('.links')
.selectAll('line')
.data(dataset.links, (d) => d.id)
.join(
enter => enter.append('line').attr('stroke', '#000000'),
);
}
function loadNodes() {
node = svg.select('.nodes')
.selectAll('.node')
.data(dataset.nodes, (d) => d.id)
.join(
enter => {
const nodes = enter.append('g')
.attr('class', 'node')
nodes.append('circle').attr('r', 10);
return nodes;
},
);
}
function restartSimulation() {
simulation.nodes(dataset.nodes);
simulation.force('link').links(dataset.links);
simulation.alpha(1).restart();
simulation.on('tick', ticked);
}
function ticked() {
link
.attr('x1', (d) => d.source.x)
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y);
node.attr('transform', (d) => `translate(${d.x},${d.y})`);
}
function updateData() {
switchBool = !switchBool;
if (switchBool) {
dataset.nodes.push({id: 3});
dataset.links.push({id: 2, source: 1, target: 3});
} else {
dataset.nodes.pop();
dataset.links.pop();
}
loadLinks();
loadNodes();
restartSimulation();
}
<script src="https://d3js.org/d3.v6.min.js"></script>
<div>
<button onclick="updateData()">Add/Remove</button>
<svg></svg>
</div>
I've actually just found the solution to the problem.
I had both forceX and forceY with default paramaters, which meant that there was a force pushing node towards (0,0), changing this bit of code I was able to fix it:
.force('x', d3.forceX().x(width * 0.5))
.force('y', d3.forceY().y(height * 0.5));

D3js collision stops working after a while

I am trying to dynamically update nodes on a d3js force chart, but the collision stops working at some point. It seems the issue is caused by a node which gets placed exactly at the center of the svg canvas, but I can't figure out why. Collision should be able to solve this, right? Any ideas?
I'll attach a minimal reproduction
var width = 400,
height = 200;
var svg = d3.select("svg");
svg.attr("viewBox", [-width / 2, -height / 2, width, height]);
var nodes = [{
radius: 20
}];
var simulation = d3
.forceSimulation(nodes)
.force("charge", d3.forceManyBody().strength(-50))
.force(
"collision",
d3.forceCollide().radius(function(d) {
return d.radius;
})
)
.on("tick", ticked);
function ticked() {
var u = d3.select("svg").selectAll("circle").data(nodes);
u.enter()
.append("circle")
.attr("r", function(d) {
return d.radius;
})
.merge(u)
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
u.exit().remove();
}
function update() {
nodes.push({
radius: Math.random() * (20 - 5) + 5
});
node = svg.selectAll("circle").data(nodes);
node
.enter()
.append("circle")
.attr("r", (d) => d.radius);
simulation.nodes(nodes);
}
setInterval(update, 100);
svg {
background: blue
}
circle {
fill: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="400" height="200" />
Your simulation cools down and then stops, at which point the tick function is no longer called. This is the default behavior. However, you can change this by modifying the alpha decay (simulation.alphaDecay()).
The alpha decay rate determines how quickly the current alpha
interpolates towards the desired target alpha; since the default
target alpha is zero, by default this controls how quickly the
simulation cools. Higher decay rates cause the simulation to stabilize
more quickly, but risk getting stuck in a local minimum; lower values
cause the simulation to take longer to run, but typically converge on
a better layout. To have the simulation run forever at the current
alpha, set the decay rate to zero... (docs)
The default settings for alpha, alpha decay, and alpha min allow the simulation to run about 300 iterations, over this time the simulation cools down and eventually comes to a stop.
If we set alpha decay to zero, the simulation continues forever. However you might want to dial down the forces as the simulation remains hot and/or remove nodes that are no longer visible (as the simulation still tracks them). I've done neither in my demo below:
var width = 400,
height = 200;
var svg = d3.select("svg");
svg.attr("viewBox", [-width / 2, -height / 2, width, height]);
var nodes = [{
radius: 20
}];
var simulation = d3
.forceSimulation(nodes)
.force("charge", d3.forceManyBody().strength(-50))
.alphaDecay(0)
.force(
"collision",
d3.forceCollide().radius(function(d) {
return d.radius;
})
)
.on("tick", ticked);
function ticked() {
var u = d3.select("svg").selectAll("circle").data(nodes);
u.enter()
.append("circle")
.attr("r", function(d) {
return d.radius;
})
.merge(u)
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
u.exit().remove();
}
function update() {
nodes.push({
radius: Math.random() * (20 - 5) + 5
});
node = svg.selectAll("circle").data(nodes);
node
.enter()
.append("circle")
.attr("r", (d) => d.radius);
simulation.nodes(nodes);
}
setInterval(update, 100);
svg {
background: blue
}
circle {
fill: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="400" height="200" />
You can also manipulate the alpha value directly with simulation.alpha(), which is another way to reheat the simulation if you want to do so when a new node is entered, for example.
I forgot to restart the simulation after the nodes array is updated:
simulation.nodes(nodes).restart();

Bounding circle nodes within rectangle containers

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.

D3 Circle Pack Layout with a horizontal arrangement

I'm trying to create a wordcloud with the D3 pack layout with a horizontal arrangement.
Instead of limiting the width, I am limiting the height.
The pack layout automatically disposes the circles with the larger one in the center and the others around him. If the height is limited, instead of expanding the circles disposition horizontally, it reduces the size of each circle.
How can I stop the layout from resizing the circles and start adding them to the sides if there is no more space around the larger one.
I want something like this:
http://imgur.com/7MDnKHF
But I'm only achieving this:
http://jsfiddle.net/v9xjra6c/
This is my current code:
var width,
height,
diameter,
padding,
format,
pack,
svg,
node;
var initSizes = function() {
var dimensions = { width: 900, height: 288 };
width = dimensions.width;
height = dimensions.height;
diameter = Math.min(width, height);
padding = 12;
format = d3.format(',d');
};
var initLayout = function() {
pack = d3.layout.pack()
.sort(null)
.size([width, height])
.padding(padding);
};
var createSVG = function() {
svg = d3.select('.chart-container').append('svg')
.attr('width', width)
.attr('height', height)
.attr('class', 'bubble');
};
var createBubbles = function() {
var dataset = pack.nodes(DATA);
node = svg.selectAll('.node')
.data(dataset.filter(function(d) { return !d.children; }))
.enter().append('g')
.attr('class', 'node')
.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; });
node.append('title')
.text(function(d) { return d.name + ': ' + format(d.value); });
node.append('circle')
.attr('r', function(d) { return d.r; });
node.append('text')
.attr('dy', '.3em')
.style('text-anchor', 'middle')
.text(function(d) { return d.name.substring(0, d.r / 3); });
};
initSizes();
initLayout();
createSVG();
createBubbles();
Thanks!
Your solution would be like merging this Example1 + Example2
So from Example 1 I have taken the mechanism to restrict the circles with in the bounds, such that it does not go beyond the svg height and width:
function tick(e) {
node
.each(cluster(10 * e.alpha * e.alpha))
.each(collide(.5))
//max radius is 50 restricting on the width
.attr("cx", function(d) { return d.x = Math.max(50, Math.min(width - 50, d.x)); })
//max radius is 50 restricting on the height
.attr("cy", function(d) { return d.y = Math.max(50, Math.min(height - 50, d.y)); }); }
Creating a scale for making radius
//so now for your data value which ranges from 0 to 100 you will have radius range from 5 to 500
var scale = d3.scale.linear().domain([0,100]).range([5, 50]);
Make the data as per Example2
var nodes = data.map(function(d){
var i = 0,
r = scale(d.value),
d = {cluster: i, radius: r, name: d.name};
if (!clusters[i] || (r > clusters[i].radius)) {clusters[i] = d;}
return d
});
Finally result will be looking like this
Note: You can reduce the height in the code and the circles will rearrange as per the space available.
Note: You can also play around the cluster to group similar nodes as in example in my case I have made a single group cluster.
Hope this helps!

Categories

Resources