select data based on multiple attributes d3 - javascript

I have a grid represented using d3 and svg. I am trying to select the neighbouring (adjacent) tiles to any specific tile on the grid. the tiles are accessed via their x and y coordinates on the grid.
What I have feels fairly messy, and doesn't do exactly what I want, I don't want the clicked tile to be selected, or the tiles diagonal to it.
var w = 960,
h = 500,
z = 20,
x = w / z,
y = h / z;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(d3.range(x * y))
.enter().append("rect")
.attr("width", z)
.attr("height", z)
.attr("clicked", false)
.attr('x', horizontalpos)
.attr('y', verticalpos)
.on("click", test)
.style("stroke", "rgb(6,120,155)")
.style("stroke-width", 2)
.style("fill", "rgb(255, 255, 255)")
function translate(d) {
return "translate(" + (d % x) * z + "," + Math.floor(d / x) * z + ")";
}
function verticalpos(d) {
return ((d % x) * z);
}
function horizontalpos(d) {
return (Math.floor(d / x) * z );
}
function test(){
var d = d3.selectAll("[x='40']").filter("[y='40']");
d3.selectAll("[x=" + "'"+ (parseInt(d.attr("x")) +20).toString() +"'" +"],[x=" + "'"+ (parseInt(d.attr("x")) -20).toString() +"'" +"],"+ "[x=" + "'"+ (parseInt(d.attr("x"))).toString() +"'" +"]")
.filter("[y=" + "'"+ (parseInt(d.attr("y"))).toString() +"'" +"],[y=" + "'"+ (parseInt(d.attr("y")) +20).toString() +"'" +"]"+",[y=" + "'"+ (parseInt(d.attr("y")) -20).toString() +"'" +"]")
.transition()
.style("fill", "black");
}
jsfiddle - https://jsfiddle.net/wkencq2w/15/
What I'm wondering is - Is there a way to select the data via two attributes, like this:
d3.select("[x='40'], [y='40']")
This does not work for me, but the logic behind it is how I would like to select the data.

Because it's D3, I won't do this based on calculations of positions but rather on data binding. This will simplify matters a lot and reduce the amount and the complexity of your code. One possible way might be to define a two-dimensional array of objects having x and y properties which is then bound to a D3 selection:
var grid = d3.range(y).map(function(dy) {
return d3.range(x).map(function(dx) {
return {x: dx, y: dy};
});
});
var g = svg.selectAll("g")
.data(grid)
.enter().append("g") // Group each row's rects in a svg:g
.selectAll("rect") // Do a nested selection
.data(function(d) { return d; }) // Bind the sub-array for this row
The part which benefits most from this approach is your test() function which may now act on the data bound to each rect rather than having to get attribute values and doing calculation with them.
function test(d) {
var clicked = d3.select(this).datum(); // Object bound to the rect.
d3.selectAll("rect").filter(function(d) {
// Do the filtering based on data rather than on positions.
return d.x === clicked.x && Math.abs(d.y - clicked.y) === 1 ||
d.y === clicked.y && Math.abs(d.x - clicked.x) === 1;
})
.transition()
.style("fill", "black");
}
Have a look at this JSFiddle for a full example.

You can select data via two attributes just by putting them together e.g. [x='40'][y='40']. This together with the , css operator allows the generation of a css selection string that gives you what you asked for.
var w = 960,
h = 500,
z = 20,
x = w / z,
y = h / z;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(d3.range(x * y))
.enter().append("rect")
.attr("width", z)
.attr("height", z)
.attr("clicked", false)
.attr('x', horizontalpos)
.attr('y', verticalpos)
.on("click", test)
.style("stroke", "rgb(6,120,155)")
.style("stroke-width", 2)
.style("fill", "rgb(255, 255, 255)")
function translate(d) {
return "translate(" + (d % x) * z + "," + Math.floor(d / x) * z + ")";
}
function verticalpos(d) {
return ((d % x) * z);
}
function horizontalpos(d) {
return (Math.floor(d / x) * z );
}
function test(d) {
x = parseInt(d3.select(this).attr("x"));
y = parseInt(d3.select(this).attr("y"));
var selector = ""
for (var dx=-20;dx<=20;dx+=20) {
for (var dy=-20;dy<=20;dy+=20) {
selector += "[x='"+ (x + dx) +"'][y='"+ (y + dy) +"'],"
}
}
// cut off the final extraneous comma
selector = selector.substring(0, selector.length - 1);
d3.selectAll(selector)
.transition()
.style("fill", "black");
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Or if you just want a cross without the centre as you describe in the question you could do this...
var w = 960,
h = 500,
z = 20,
x = w / z,
y = h / z;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(d3.range(x * y))
.enter().append("rect")
.attr("width", z)
.attr("height", z)
.attr("clicked", false)
.attr('x', horizontalpos)
.attr('y', verticalpos)
.on("click", test)
.style("stroke", "rgb(6,120,155)")
.style("stroke-width", 2)
.style("fill", "rgb(255, 255, 255)")
function translate(d) {
return "translate(" + (d % x) * z + "," + Math.floor(d / x) * z + ")";
}
function verticalpos(d) {
return ((d % x) * z);
}
function horizontalpos(d) {
return (Math.floor(d / x) * z );
}
function test(d) {
x = parseInt(d3.select(this).attr("x"));
y = parseInt(d3.select(this).attr("y"));
var selector = ""
var deltas = [[-20, 0], [20, 0], [0, 20], [0, -20]];
for (var i=0;i < deltas.length;i++) {
selector += "[x='"+ (x + deltas[i][0]) +"'][y='"+ (y + deltas[i][1]) +"'],"
}
// cut off the final extraneous comma
selector = selector.substring(0, selector.length - 1);
d3.selectAll(selector)
.transition()
.style("fill", "black");
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

I tried to fix this problem using filter:
function test(d){
var x = d3.select(this);
var x1 = (parseInt(x.attr("x")) +20);
var x2 = (parseInt(x.attr("x")) -20);
var y1 = (parseInt(x.attr("y")) +20);
var y2 = (parseInt(x.attr("y")) -20);
var f = d3.selectAll("rect")[0].filter(function(d){
//left rect
if (d3.select(d).attr("x") == x1 && d3.select(d).attr("y") == parseInt(x.attr("y")))
return true;
//right rect
if (d3.select(d).attr("x") == x2 && d3.select(d).attr("y") == parseInt(x.attr("y")))
return true;
//bottom rect
if (d3.select(d).attr("y") == y1 && d3.select(d).attr("x") == parseInt(x.attr("x")))
return true;
//top rect
if (d3.select(d).attr("y") == y2 && d3.select(d).attr("x") == parseInt(x.attr("x")))
return true;
return false;
});
//select all filtered and make their fill black
d3.selectAll(f).transition().delay(100).style("fill", "black");
}
Working code here
Hope this helps!

Related

d3 js triangle in svg path

i must create 30 triangles that move away from current mouse position. i try with this code:
var body = d3.select("body");
var mouse = [];
var width = 1000;
var height = 600;
var numberOfTriangles = 30;
var isMouseMoving = false;
var triangle = d3.svg.symbolType["triangle-up"]
function drawTriangles(number) {
for (var i = 0; i < number; i++) {
var dim = Math.random() * 400;
svg.append("path")
.attr("d", triangle.size(dim))
.attr("transform", function(d) {
return "translate(" + Math.random() * width + "," + Math.random() * height + ")";
})
.attr("fill", "rgb(" + parseInt(Math.random() * 255) + "," + parseInt(Math.random() * 255) + "," + parseInt(Math.random() * 255) + ")")
.attr("opacity", 2)
.attr("class", "path" + i);
}
}
function moveMouse() {
if (isMouseMoving) {
svg.selectAll('path').each(function(d, i) {
var self = d3.select(this);
self.attr('transform', function() {
return "translate(" + mouse[0] + "," + mouse[1] + ")";
})
})
}
}
var svg = body.append("svg")
.attr("width", width)
.attr("height", height)
.style("border", "1px solid black")
.on("mousemove", function() {
mouse = d3.mouse(this);
isMouseMoving = true;
});
drawTriangles(numberOfTriangles);
d3.timer(function() {
moveMouse()
});
but i have this error: "Uncaught TypeError: Cannot read property 'size' of undefined at drawTriangles".
Can someone help me? Thanks.
Your error is because of:
var triangle = d3.svg.symbolType["triangle-up"];
If you fix the typo on symbolTypes, this returns undefined. d3.svg.symbolTypes simply returns an array of available symbols, it is not a mechanism to create a new symbol path generator. That said, what you really wanted is:
var triangle = d3.svg.symbol().type("triangle-up");
This creates a proper triangle symbol generator.
Taking this a little further, I'm not sure what you mean by
that move away from current mouse position
Your code does the exact opposite and puts all the triangles on the mouse cursor...
EDITS
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
var width = 300,
height = 300;
var nodes = d3.range(200).map(function() { return {radius: Math.random() * 12 + 4}; }),
root = nodes[0],
color = d3.scale.category10();
root.radius = 0;
root.fixed = true;
var force = d3.layout.force()
.gravity(0.05)
.charge(function(d, i) { return i ? 0 : -1000; })
.nodes(nodes)
.size([width, height]);
force.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.style("border", "1px solid black")
.style("margin","20px");
var triangle = d3.svg.symbol().type("triangle-up");
svg.selectAll("path")
.data(nodes.slice(1))
.enter().append("path")
.attr("d", function(d) {
triangle.size(d.radius);
return triangle();
})
.style("fill", function(d, i) { return color(i % 3); });
force.on("tick", function(e) {
var q = d3.geom.quadtree(nodes),
i = 0,
n = nodes.length;
while (++i < n) q.visit(collide(nodes[i]));
svg.selectAll("path")
.attr("transform", function(d){
return "translate(" + d.x + "," + d.y + ")";
});
});
svg.on("mousemove", function() {
var p1 = d3.mouse(this);
root.px = p1[0];
root.py = p1[1];
force.resume();
});
function collide(node) {
var r = node.radius + 16,
nx1 = node.x - r,
nx2 = node.x + r,
ny1 = node.y - r,
ny2 = node.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = node.radius + quad.point.radius;
if (l < r) {
l = (l - r) / l * .5;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
if (node.x > width) node.x = width;
if (node.x < 0) node.x = 0;
if (node.y > height) node.y = height;
if (node.y < 0) node.y = 0;
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
}
</script>

dragenter and dragleave event listener D3

I would like to change the color of tiles in a grid as they are being 'dragged over'. The grid is created using d3 and svg. I have tried several different methods, but have had little success.
Here is my code as of my latest attempt:
var w = 960,
h = 520,
z = 40,
x = w / z,
y = h / z;
var svg = d3.select("#grid").append("svg")
.attr("width", w)
.attr("height", h);
var grid = d3.range(y).map(function(dy) {
return d3.range(x).map(function(dx) {
return {x: dx, y: dy};
});
});
var g = svg.selectAll("g")
.data(grid)
.enter().append("g") // Group each row's rects in a g
.selectAll("rect") // Do a nested selection
.data(function(d) { return d; }) // Bind the sub-array for this row
.enter().append("rect")
.attr("width", z)
.attr("height", z)
.attr("start", false)
.attr("clicked", false)
.attr("previous", null)
.attr('x', function(d) { return d.x * z; })
.attr('y', function(d) { return d.y * z; })
.on("dragover", draggedOver)
.style("stroke", "rgb(6,120,155)")
.style("stroke-width", 2)
.style("fill", "rgb(255, 255, 255)");
function draggedOver(){
d3.select(this).transition().style("fill","green");
}
The focus is on the "on('dragover')" event. Thanks in advance

Understanding a d3 Multi-Foci Force Layout

I'm trying to understand how this beautiful example works...
http://bl.ocks.org/mbostock/1804919
I see that clustering is done by the color of the nodes, but I'm confused by the line in question in the collision detection function...
r = d.radius + quad.point.radius + (d.color !== quad.point.color) * padding;
How can you "add" the product of a comparison of the colors "d.color" and "quad.point.color"? I would have assumed this would return nothing more than a true/false? Either way, I'm not sure I follow only this reference to color will have the desired effect of clustering by color?
Anyway, I haven't been able to find any line-by-line description of the workings of the collision detection function, so I'm really hoping that someone here understands it well enough to help explain this bit to me.
All I'm ultimately trying to achieve is to adapt the example to cluster by another non-numeric node attribute (e.g. d.person_name !== quad.point.person_name).
Thanks!
The line you are asking about is calculating the allowable distance between nodes, the distance between nodes (l) is compared to r to determine if there is a collision between d and quad.point. The value padding is added to the allowable distance between nodes if they are of the same colour. The boolean result is coerced into a Number type by the context.
Instead of assuming what JS does its really easy to open the browser tools and just type the expression in to see what the result is...
But the collision detection is not involved in the clustering, that is handled by this code...
// Move nodes toward cluster focus.
function gravity(alpha) {
return function(d) {
d.y += (d.cy - d.y) * alpha;
d.x += (d.cx - d.x) * alpha;
};
}
If you have some data and you want to use the same code to group them by a particular attribute, then you need to add a cx and cy property to your data such that items with the same attribute value (the value does not need to be numeric) have the same cx and cy values.
Example (modified version of this)
var width = 600,
height = 200,
padding = 6, // separation between nodes
maxRadius = 6;
var n = 200, // total number of nodes
names = ["Givens", "Crowder", "Lannister", "Baratheon", "Stark"],
m = names.length; // number of distinct clusters
var color = d3.scale.category10()
.domain(d3.range(m));
var x = d3.scale.ordinal()
.domain(names)
.rangePoints([0, width], 1),
legend = d3.svg.axis()
.scale(x)
.orient("top")
var nodes = d3.range(n).map(function() {
var i = Math.floor(Math.random() * m),
v = (i + 1) / m * -Math.log(Math.random());
return {
radius: Math.sqrt(v) * maxRadius,
color: color(i),
cx: x(names[i]),
cy: height / 2
};
});
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(0)
.charge(0)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height),
gLegend = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0, " + height * 0.9 + ")")
.call(legend);
gLegend.selectAll(".tick text")
.attr("fill", function(d, i) {
return color(i);
});
var circle = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", function(d) {
return d.radius;
})
.style("fill", function(d) {
return d.color;
})
.call(force.drag);
function tick(e) {
circle
.each(gravity(.2 * e.alpha))
.each(collide(.5))
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
// Move nodes toward cluster focus.
function gravity(alpha) {
return function(d) {
d.y += (d.cy - d.y) * alpha;
d.x += (d.cx - d.x) * alpha;
};
}
// Resolve collisions between nodes.
function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function(d) {
var r = d.radius + maxRadius + padding,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius + (d.color !== quad.point.color) * padding;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
circle {
stroke: #000;
}
.x.axis path {
fill: none;
}
.x.axis text {
font-family: Papyrus, Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, sans-serif;
}
body {
background-color: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>

adding text to circles in a rectangle

I am trying to draw circles in a rectangular div. I have followed the advice from Question 13339615(the answer I used is also made available in this fiddle, and this works perfectly.
However, being completely new to d3, I cannot work out how to label the circles. What I would basically like to recreate is similar to the visualisation in this article.
I have tried the following modifications to the fiddle:
var bubbles = bubbleGroup.selectAll("circle")
.data(data)
.enter()
.append("circle")
.append("text").attr("dy", ".3em")
.style("text-anchor", "middle").text("test");
but this breaks the visualisation.
Following question 13615381 I have also tried things like:
var bubbles = bubbleGroup.selectAll("circle")
.data(data)
.enter()
.append("circle");
bubbleGroup.append("text")
.attr("dx", function(d){return -20})
.text(function(d){return "test"})
but the text does not display. I'm imaging the code should be some variation of these, but I cannot figure it out.
Thank you!
Fixed by putting the circle and text inside a g and adjusting the g css-transform.
JSFiddle
var bubbles = bubbleGroup.selectAll("g")
.data(data)
.enter().append("g").attr("class","gBubble");
bubbles.append("circle")
.on("mouseover",function(){
$(this).attr("cursor","pointer")
})
.on("click",function(){alert("clicked")});
bubbles.append("text").text(function(d){return d.name;}).style("opacity","1");
(function() {
//D3 program to fit circles of different sizes
//in a rectangle of fixed aspect ratio
//as tightly as reasonable.
//
//By Amelia Bellamy-Royds, in response to
//http://stackoverflow.com/questions/13339615/packing-different-sized-circles-into-rectangle-d3-js
//Based on Mike Bostock's
//"http://bl.ocks.org/mbostock/7882658" example:
//http://bl.ocks.org/mbostock/7882658
//parameters//
var N = 25; //number of nodes
var sortOrder = -1;
//>0 for ascending, <0 for descending, 0 for no sort
//create data array//
var data = [], i = N;
var randNorm = d3.random.normal(1,0.6);
while(i--) data.push({
"size": Math.max(randNorm(), 0.1) });
//circle area will be proportional to size
var dataMax = d3.max(data, function(d){return d.size;});
var totalSize = d3.sum(data, function(d){return d.size;});
//________________//
//Set up SVG and rectangle//
var svg = d3.select("svg");
var digits = /(\d*)/;
var margin = 50; //space in pixels from edges of SVG
var padding = 4; //space in pixels between circles
var svgStyles = window.getComputedStyle(svg.node());
var width = parseFloat(svgStyles["width"]) - 2*margin;
var height = parseFloat(svgStyles["height"]) - 2*margin;
var usableArea = Math.PI*
Math.pow( Math.min(width,height)/2 ,2)*0.667;
var scaleFactor = Math.sqrt(usableArea)/
Math.sqrt(totalSize)/Math.PI;
var rScale = d3.scale.sqrt()
//make radius proportional to square root of data r
.domain([0, dataMax]) //data range
.range([0, Math.sqrt(dataMax)*scaleFactor]);
//The rScale range will be adjusted as necessary
//during packing.
//The initial value is based on scaling such that the total
//area of the circles is 2/3 the area of the largest circle
//you can draw within the box.
/*
console.log("Dimensions: ", [height, width]);
console.log("area", width*height);
console.log("Usable area: ", usableArea);
console.log("TotalSize: ", totalSize);
console.log("Initial Scale: ", scaleFactor);
console.log("RScale: ",rScale.domain(), rScale.range());
console.log("r(1)", rScale(1) );
// */
var box = svg.append("rect")
.attr({ "height": height, "width":width,
"x":margin, "y":margin,
"class":"box"
});
var bubbleGroup = svg.append("g")
.attr("class", "bubbles")
.attr("transform",
"translate(" + [margin,margin] + ")");
//__Initialize layout objects__//
// Use the pack layout to initialize node positions:
d3.layout.pack()
.sort((
sortOrder?
( (sortOrder<0)?
function(a,b){return b.size - a.size;} : //descending
function(a,b){return a.size - b.size;} ) : //ascending
function(a,b){return 0;} //no sort
))
.size([width/scaleFactor, height/scaleFactor])
.value(function(d) { return d.size; })
.nodes({children:data});
//Use the force layout to optimize:
var force = d3.layout.force()
.nodes(data)
.size([width/scaleFactor, height/scaleFactor])
.gravity(.5)
.charge(0) //don't repel
.on("tick", updateBubbles);
//Create circles!//
var bubbles = bubbleGroup.selectAll("circle")
.data(data)
.enter()
.append("circle");
//Create text
var text = bubbleGroup.selectAll("text")
.data(data).enter().append("text")
.attr("dy", function(d){
return d.y;
})
.attr("dx", function(d){
return d.x;
}).style("text-anchor", "middle").text("test");
// Create a function for this tick round,
// with a new quadtree to detect collisions
// between a given data element and all
// others in the layout, or the walls of the box.
//keep track of max and min positions from the quadtree
var bubbleExtent;
function collide(alpha) {
var quadtree = d3.geom.quadtree(data);
var maxRadius = Math.sqrt(dataMax);
var scaledPadding = padding/scaleFactor;
var boxWidth = width/scaleFactor;
var boxHeight = height/scaleFactor;
//re-set max/min values to min=+infinity, max=-infinity:
bubbleExtent = [[Infinity, Infinity],[-Infinity, -Infinity]];
return function(d) {
//check if it is pushing out of box:
var r = Math.sqrt(d.size) + scaledPadding,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
if (nx1 < 0) {
d.x = r;
}
if (nx2 > boxWidth) {
d.x = boxWidth - r;
}
if (ny1 < 0) {
d.y = r;
}
if (ny2 > boxHeight) {
d.y = boxHeight - r;
}
//check for collisions
r = r + maxRadius,
//radius to center of any possible conflicting nodes
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = Math.sqrt(d.size) + Math.sqrt(quad.point.size)
+ scaledPadding;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
//update max and min
r = r-maxRadius; //return to radius for just this node
bubbleExtent[0][0] = Math.min(bubbleExtent[0][0],
d.x - r);
bubbleExtent[0][1] = Math.min(bubbleExtent[0][1],
d.y - r);
bubbleExtent[1][0] = Math.max(bubbleExtent[1][0],
d.x + r);
bubbleExtent[1][1] = Math.max(bubbleExtent[1][1],
d.y + r);
};
}
function updateBubbles() {
bubbles
.each( collide(0.5) ); //check for collisions
text.each(collide(0.5));//check for text collisions
//update the scale to squeeze in the box
//to match the current extent of the bubbles
var bubbleWidth = bubbleExtent[1][0] - bubbleExtent[0][0];
var bubbleHeight = bubbleExtent[1][1] - bubbleExtent[0][1];
scaleFactor = (height/bubbleHeight +
width/bubbleWidth)/2; //average
/*
console.log("Box dimensions:", [height, width]);
console.log("Bubble dimensions:", [bubbleHeight, bubbleWidth]);
console.log("ScaledBubble:", [scaleFactor*bubbleHeight,
scaleFactor*bubbleWidth]);
//*/
rScale
.range([0, Math.sqrt(dataMax)*scaleFactor]);
//shift the bubble cluster to the top left of the box
bubbles
.each( function(d){
d.x -= bubbleExtent[0][0];
d.y -= bubbleExtent[0][1];
});
//update positions and size according to current scale:
bubbles
.attr("r", function(d){return rScale(d.size);} )
.attr("cx", function(d){return scaleFactor*d.x;})
.attr("cy", function(d){return scaleFactor*d.y;});
text
.attr("dy", function(d){
return (scaleFactor*d.y)+4;
})
.attr("dx", function(d){
return scaleFactor*d.x*2;
});
}
force.start();
})();
rect.box {
fill:none;
stroke:royalblue;
stroke-width:5;
shape-rendering: crispEdges;
}
g.bubbles circle {
fill:rgba(255,0,64,0.5);
stroke:rgb(255,0,64);
stroke-width:3;
}
g.bubbles text {
fill:royalblue;
font-family:sans-serif;
text-anchor:middle;
alignment-baseline:middle;
opacity:1;
pointer-events:all;
transition:1s;
}
g.bubbles text:hover {
opacity:1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg height=500 width=500></svg>
I've added text to circles, and also collision behavior too.
Initially the text is invisible because in the CSS they mentioned like below
g.bubbles text {
fill:royalblue;
font-family:sans-serif;
text-anchor:middle;
alignment-baseline:middle;
opacity:0;//See this value, this makes text to invisible
pointer-events:all;
transition:1s;
}
g.bubbles text:hover {
opacity:1;
}
In my snippet I changed it visible by making it's opacity to 1.
And updated fiddle

Convert d3.js bubbles into forced/gravity based layout

I have a set of data that I am visualizing using d3.js. I am representing data points in the form of bubbles, where the configuration for bubbles is as follows:
var dot = svg.selectAll("g")
.data(data)
.enter()
.append("g");
dot.append("circle")
.attr("class", "dot")
.attr("cx", function(d) { return xp(x(d)); })
.attr("cy", function(d) { return yp(y(d)); })
.style("fill", function(d) { return colorp(color(d)); })
.attr("r", function(d) { return radiusp(radius(d)*2000000); });
dot.append("text")
.attr("x", function(d) { return xp(x(d)); })
.attr("y", function(d) { return yp(y(d)); })
.text(function(d) { return d.name; })
Where xp, yp, colorp and radiusp are defined as follows:
var xp = d3.scale.log().domain([300, 1e5]).range([0, width]),
yp = d3.scale.linear().domain([10, 85]).range([height, 0]),
radiusp = d3.scale.sqrt().domain([0, 5e8]).range([0, 40]),
colorp = d3.scale.category10();
At this point, the bubbles are being displayed as static on their positions (where position is defined by xp and yp), while the size of the bubble is basically coming from radiusp and color is defined by colorp.
Right now I am showing them exactly as this example:
http://bl.ocks.org/mbostock/4063269
What I need is to display them in this form:
http://jsfiddle.net/andycooper/PcjUR/1/
That is: They should be packed using gravity function, have some charge, can be dragged and repel each other to some extent. I can see that there is a way through d3.layout.force() but not really able to integrate that into this.. I will be really thankful if you can suggest me the right path or some working example or even a hint. Thank you.
I think you were almost there but the specification of your dot variable is not the best one. I would transform it like this:
var dot = svg.selectAll(".dot")
.data(data)
.enter()
Afterwards, once the circles have been plotted, what you do is that you create a force layout, instantiate it with the nodes you just created, add a on("tick") method, and then start the layout. An example is the following:
var force = d3.layout.force().nodes(data).size([width, height])
.gravity(0)
.charge(0)
.on("tick", function(e){
dot
.each(gravity(.2 * e.alpha))
.each(collide(.5))
.attr("cx", function (d) {return d.x;})
.attr("cy", function (d) {return d.y;});
})
.start();
To have a complete answer, I will add also the gravity and collide methods from your fiddle (with adjusted variable names)
function gravity(alpha) {
return function (d) {
d.y += (d.cy - d.y) * alpha;
d.x += (d.cx - d.x) * alpha;
};
}
function collide(alpha) {
var padding = 6
var quadtree = d3.geom.quadtree(dot);
return function (d) {
var r = d.r + radiusp.domain()[1] + padding,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function (quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.r + quad.point.r + (d.color !== quad.point.color) * padding;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
I think the problem you had was that perhaps you were applying the force layout to the g element of each of the circles, which unfortunately was not working. I hope this will give you an idea how to proceed. Your last line of the dot declaration was adding a g element for each circle, which was a little difficult to handle.
Thanks.
PS I assume that the x, y, and r attributes of your data contain the x,y, and radius.

Categories