Using d3-3d with pan & zoom while retaining rotation - javascript

I am using the d3-3d plugin to graph 3d bar charts, but I'd like to add the pan & zoom functionality while keeping the rotation. Just adding in d3.zoom() seems to conflict with the d3.drag() behavior - it appears to be random which one takes precedence and adds a lot of "jitter".
var origin = [100, 85], scale = 5, j = 10, cubesData = [];
var alpha = 0, beta = 0, startAngle = Math.PI/6;
var svg = d3.select('svg')
.call(d3.drag()
.on('drag', dragged)
.on('start', dragStart)
.on('end', dragEnd))
.append('g');
var color = d3.scaleOrdinal(d3.schemeCategory20);
var cubesGroup = svg.append('g').attr('class', 'cubes');
var mx, my, mouseX, mouseY;
var cubes3D = d3._3d()
.shape('CUBE')
.x(function(d){ return d.x; })
.y(function(d){ return d.y; })
.z(function(d){ return d.z; })
.rotateY( startAngle)
.rotateX(-startAngle)
.origin(origin)
.scale(scale);
var zoom = d3.zoom()
.scaleExtent([1, 40])
.on("zoom", zoomed);
cubesGroup.call(zoom);
function zoomed() {
cubesGroup.attr("transform", d3.event.transform);
}
function processData(data, tt){
/* --------- CUBES ---------*/
var cubes = cubesGroup.selectAll('g.cube')
.data(data, function(d){ return d.id });
var ce = cubes
.enter()
.append('g')
.attr('class', 'cube')
.attr('fill', function(d){ return color(d.id); })
.attr('stroke', function(d){
return d3.color(color(d.id)).darker(2);
})
.merge(cubes)
.sort(cubes3D.sort);
cubes.exit().remove();
/* --------- FACES ---------*/
var faces = cubes.merge(ce)
.selectAll('path.face')
.data(function(d){ return d.faces; },
function(d){ return d.face; }
);
faces.enter()
.append('path')
.attr('class', 'face')
.attr('fill-opacity', 0.95)
.classed('_3d', true)
.merge(faces)
.transition().duration(tt)
.attr('d', cubes3D.draw);
faces.exit().remove();
/* --------- TEXT ---------*/
var texts = cubes.merge(ce)
.selectAll('text.text').data(function(d){
var _t = d.faces.filter(function(d){
return d.face === 'top';
});
return [{height: d.height, centroid: _t[0].centroid}];
});
texts.enter()
.append('text')
.attr('class', 'text')
.attr('dy', '-.7em')
.attr('text-anchor', 'middle')
.attr('font-family', 'sans-serif')
.attr('font-weight', 'bolder')
.attr('x', function(d){
return origin[0] + scale * d.centroid.x
})
.attr('y', function(d){
return origin[1] + scale * d.centroid.y
})
.classed('_3d', true)
.merge(texts)
.transition().duration(tt)
.attr('fill', 'black')
.attr('stroke', 'none')
.attr('x', function(d){
return origin[0] + scale * d.centroid.x
})
.attr('y', function(d){
return origin[1] + scale * d.centroid.y
})
.tween('text', function(d){
var that = d3.select(this);
var i = d3.interpolateNumber(+that.text(), Math.abs(d.height));
return function(t){
that.text(i(t).toFixed(1));
};
});
texts.exit().remove();
/* --------- SORT TEXT & FACES ---------*/
ce.selectAll('._3d').sort(d3._3d().sort);
}
function init(){
cubesData = [];
var cnt = 0;
for(var z = -j/2; z <= j/2; z = z + 5){
for(var x = -j; x <= j; x = x + 5){
var h = d3.randomUniform(-2, -7)();
var _cube = makeCube(h, x, z);
_cube.id = 'cube_' + cnt++;
_cube.height = h;
cubesData.push(_cube);
}
}
processData(cubes3D(cubesData), 1000);
}
function dragStart(){
mx = d3.event.x;
my = d3.event.y;
}
function dragged(){
mouseX = mouseX || 0;
mouseY = mouseY || 0;
beta = (d3.event.x - mx + mouseX) * Math.PI / 230 ;
alpha = (d3.event.y - my + mouseY) * Math.PI / 230 * (-1);
processData(cubes3D.rotateY(beta + startAngle)
.rotateX(alpha - startAngle)(cubesData), 0);
}
function dragEnd(){
mouseX = d3.event.x - mx + mouseX;
mouseY = d3.event.y - my + mouseY;
}
function makeCube(h, x, z){
return [
{x: x - 1, y: h, z: z + 1}, // FRONT TOP LEFT
{x: x - 1, y: 0, z: z + 1}, // FRONT BOTTOM LEFT
{x: x + 1, y: 0, z: z + 1}, // FRONT BOTTOM RIGHT
{x: x + 1, y: h, z: z + 1}, // FRONT TOP RIGHT
{x: x - 1, y: h, z: z - 1}, // BACK TOP LEFT
{x: x - 1, y: 0, z: z - 1}, // BACK BOTTOM LEFT
{x: x + 1, y: 0, z: z - 1}, // BACK BOTTOM RIGHT
{x: x + 1, y: h, z: z - 1}, // BACK TOP RIGHT
];
}
d3.selectAll('button').on('click', init);
init();
button {
position: absolute;
right: 10px;
top: 10px;
}
<!DOCTYPE html>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/d3-3d/build/d3-3d.min.js"></script>
<body>
<svg width="200" height="175"></svg>
</body>
I'd like to mimic the behavior from vis.js.
(1) Ctrl+drag would translate the origin (two finger drag on mobile)
(2) drag would rotate (one finger drag on mobile)
(3) zoom would scale (two finger pinch on mobile)
How do I stop the propagation and only handle these events specifically?
Edit: It appears that the bar chart example has a scale() and origin() that can be set - but I would prefer to work with transforms for speed and efficiency of the update (as opposed to re-drawing).

You can get the type of event using d3.event.sourceEvent. In the code you shared, the dragging anywhere in the white space will rotate and dragging on the bars will move.
With d3.event.sourceEvent you can check whether the ctrl key is pressed and move/rotate accordingly. You don't even need the drag function for your svg. It can be handled using the zoom functions alone.
Here's the fiddle:
var origin = [100, 85],
scale = 5,
j = 10,
cubesData = [];
var alpha = 0,
beta = 0,
startAngle = Math.PI / 6;
var zoom = d3.zoom()
.scaleExtent([1, 40])
.on("zoom", zoomed)
.on('start', zoomStart)
.on('end', zoomEnd);
var svg = d3.select('svg').call(zoom)
.append('g');
var color = d3.scaleOrdinal(d3.schemeCategory20);
var cubesGroup = svg.append('g').attr('class', 'cubes').attr('transform', 'translate(0,0) scale(1)');
var mx, my, mouseX, mouseY;
var cubes3D = d3._3d()
.shape('CUBE')
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
})
.z(function(d) {
return d.z;
})
.rotateY(startAngle)
.rotateX(-startAngle)
.origin(origin)
.scale(scale);
function zoomStart() {
mx = d3.event.sourceEvent.x;
my = d3.event.sourceEvent.y;
if (d3.event.sourceEvent !== null && d3.event.sourceEvent.type == 'mousemove' && d3.event.sourceEvent.ctrlKey == true) {
cubesGroup.attr("transform", d3.event.transform);
}
}
function zoomEnd() {
if (d3.event.sourceEvent == null) return;
mouseX = d3.event.sourceEvent.x - mx + mouseX
mouseY = d3.event.sourceEvent.y - my + mouseY
}
function zoomed(d) {
if (d3.event.sourceEvent == null) return;
if (d3.event.sourceEvent !== null && d3.event.sourceEvent.type == 'wheel') {
cubesGroup.attr("transform", "scale(" + d3.event.transform['k'] + ")");
} else if (d3.event.sourceEvent !== null && d3.event.sourceEvent.type == 'mousemove' && d3.event.sourceEvent.ctrlKey == true) {
cubesGroup.attr("transform", "translate(" + d3.event.transform['x'] + "," + d3.event.transform['y'] + ") scale(" + d3.event.transform['k'] + ")");
} else if (d3.event.sourceEvent !== null && d3.event.sourceEvent.type == 'mousemove' && d3.event.sourceEvent.ctrlKey == false) {
mouseX = mouseX || 0;
mouseY = mouseY || 0;
beta = (d3.event.sourceEvent.x - mx + mouseX) * Math.PI / 230;
alpha = (d3.event.sourceEvent.y - my + mouseY) * Math.PI / 230 * (-1);
processData(cubes3D.rotateY(beta + startAngle)
.rotateX(alpha - startAngle)(cubesData), 0);
};
}
function processData(data, tt) {
/* --------- CUBES ---------*/
var cubes = cubesGroup.selectAll('g.cube')
.data(data, function(d) {
return d.id
});
var ce = cubes
.enter()
.append('g')
.attr('class', 'cube')
.attr('fill', function(d) {
return color(d.id);
})
.attr('stroke', function(d) {
return d3.color(color(d.id)).darker(2);
})
.merge(cubes)
.sort(cubes3D.sort);
cubes.exit().remove();
/* --------- FACES ---------*/
var faces = cubes.merge(ce)
.selectAll('path.face')
.data(function(d) {
return d.faces;
},
function(d) {
return d.face;
}
);
faces.enter()
.append('path')
.attr('class', 'face')
.attr('fill-opacity', 0.95)
.classed('_3d', true)
.merge(faces)
.transition().duration(tt)
.attr('d', cubes3D.draw);
faces.exit().remove();
/* --------- TEXT ---------*/
var texts = cubes.merge(ce)
.selectAll('text.text').data(function(d) {
var _t = d.faces.filter(function(d) {
return d.face === 'top';
});
return [{
height: d.height,
centroid: _t[0].centroid
}];
});
texts.enter()
.append('text')
.attr('class', 'text')
.attr('dy', '-.7em')
.attr('text-anchor', 'middle')
.attr('font-family', 'sans-serif')
.attr('font-weight', 'bolder')
.attr('x', function(d) {
return origin[0] + scale * d.centroid.x
})
.attr('y', function(d) {
return origin[1] + scale * d.centroid.y
})
.classed('_3d', true)
.merge(texts)
.transition().duration(tt)
.attr('fill', 'black')
.attr('stroke', 'none')
.attr('x', function(d) {
return origin[0] + scale * d.centroid.x
})
.attr('y', function(d) {
return origin[1] + scale * d.centroid.y
})
.tween('text', function(d) {
var that = d3.select(this);
var i = d3.interpolateNumber(+that.text(), Math.abs(d.height));
return function(t) {
that.text(i(t).toFixed(1));
};
});
texts.exit().remove();
/* --------- SORT TEXT & FACES ---------*/
ce.selectAll('._3d').sort(d3._3d().sort);
}
function init() {
cubesData = [];
var cnt = 0;
for (var z = -j / 2; z <= j / 2; z = z + 5) {
for (var x = -j; x <= j; x = x + 5) {
var h = d3.randomUniform(-2, -7)();
var _cube = makeCube(h, x, z);
_cube.id = 'cube_' + cnt++;
_cube.height = h;
cubesData.push(_cube);
}
}
processData(cubes3D(cubesData), 1000);
}
function dragStart() {
console.log('dragStart')
mx = d3.event.x;
my = d3.event.y;
}
function dragged() {
console.log('dragged')
mouseX = mouseX || 0;
mouseY = mouseY || 0;
beta = (d3.event.x - mx + mouseX) * Math.PI / 230;
alpha = (d3.event.y - my + mouseY) * Math.PI / 230 * (-1);
processData(cubes3D.rotateY(beta + startAngle)
.rotateX(alpha - startAngle)(cubesData), 0);
}
function dragEnd() {
console.log('dragend')
mouseX = d3.event.x - mx + mouseX;
mouseY = d3.event.y - my + mouseY;
}
function makeCube(h, x, z) {
return [{
x: x - 1,
y: h,
z: z + 1
}, // FRONT TOP LEFT
{
x: x - 1,
y: 0,
z: z + 1
}, // FRONT BOTTOM LEFT
{
x: x + 1,
y: 0,
z: z + 1
}, // FRONT BOTTOM RIGHT
{
x: x + 1,
y: h,
z: z + 1
}, // FRONT TOP RIGHT
{
x: x - 1,
y: h,
z: z - 1
}, // BACK TOP LEFT
{
x: x - 1,
y: 0,
z: z - 1
}, // BACK BOTTOM LEFT
{
x: x + 1,
y: 0,
z: z - 1
}, // BACK BOTTOM RIGHT
{
x: x + 1,
y: h,
z: z - 1
}, // BACK TOP RIGHT
];
}
d3.selectAll('button').on('click', init);
init();
button {
position: absolute;
right: 10px;
top: 10px;
}
<!DOCTYPE html>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/d3-3d/build/d3-3d.min.js"></script>
<body>
<svg width="500" height="500"></svg>
</body>
On JSFiddle

Related

How to rotate text around its centroid (vertically flip) in SVG / D3?

I have text objects labeling points that are evenly spaced around a circle. Thanks to this article, I am able to correctly position both the points and text objects but the labels on the left hemisphere of the circle need to be rotated 180 degrees (flipped vertically) to be more legible.
I thought I could rotate the text object about its own origin before rotating it to the appropriate position around the circle but was unable to determine how to locate the center position of each text object.
How can I rotate text objects about their center for those on the left hemisphere of the circle (angle>= PI/2 && angle<=PI*1.5)? Or is there a better technique to use?
<style type="text/css">
* {
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 13px;
}
circle {
fill: steelblue;
fill-opacity: .8;
}
circle:hover {
fill: orange;
fill-opacity: .8;
}
</style>
<div id="canvas"></div>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.v2.min.js"></script>
<script type="text/javascript">
(function () {
var paddding = 250;
var createNodes = function () {
var nodeData = [
{ id: 0, label: 'AAA' },
{ id: 1, label: 'BBB' },
{ id: 2, label: 'CCC' },
{ id: 3, label: 'DDD' },
{ id: 4, label: 'EEE' },
{ id: 5, label: 'FFF' },
{ id: 6, label: 'GGG' },
{ id: 7, label: 'HHH' }
];
var radius = 100;
var nodes = [],
width = (radius * 2) + paddding,
height = (radius * 2) + paddding,
angle,
x,
y,
i;
var numNodes = nodeData.length;
for (i = 0; i < numNodes; i++) {
angle = (i / (numNodes / 2)) * Math.PI;
x = (radius * Math.cos(angle)) + (width / 2);
y = (radius * Math.sin(angle)) + (width / 2);
nodes.push({ 'id': i, 'x': x, 'y': y, 'label': nodeData[i].label, 'angle': angle });
}
return nodes;
}
var createSvg = function (radius, callback) {
d3.selectAll('svg').remove();
var svg = d3.select('#canvas').append('svg:svg')
.attr('width', (radius * 2) + paddding)
.attr('height', (radius * 2) + paddding);
callback(svg);
}
var createElements = function (svg, nodes, elementRadius) {
element = svg.selectAll('circle')
.data(nodes)
.enter().append('svg:circle')
.attr('r', elementRadius)
.attr('cx', function (d, i) { return d.x; })
.attr('cy', function (d, i) { return d.y; });
element = svg.selectAll('text')
.data(nodes)
.enter().append('svg:text')
.text(function (d, i) { return d.label + " - " + d.angle.toFixed(2) + ", " + (d.angle*180/Math.PI); })
.attr('x', function (d, i) { return nodes[0].x + 15; }) // add 15 for spacing off point
.attr('y', function (d, i) { return nodes[0].y; })
.attr("dy", ".35em")
.style("alignment-baseline","middle")
.style("text-anchor", "start")
.attr("transform", function(d,i) {
return "rotate(" + (d.angle * 180) / Math.PI + ", 225, 225)";})
;
}
var draw = function () {
var radius = 100;
var nodes = createNodes();
createSvg(radius, function (svg) {
createElements(svg, nodes, 10);
});
}
$(document).ready(function () {
draw();
});
})();
</script>
If you want to reverse the labels for those on the left side of the circle. You can achieve different ways. One way is by modifying three attributes of the text as you append it:
.attr('x', function (d, i) { return nodes[0].x + 15; })
.style("text-anchor", "start")
.attr("transform", function(d,i) {
return "rotate(" + (d.angle * 180) / Math.PI + ", 225, 225)"
})
If you modify only some of these, you might not get the results you are looking for.
Modification of text-end
This is needed as your text will start away from the point you are defining, and as the text may have variable length, defining a start point will be more complex than necessary. For points you need to flip, you'll need to use:
.style("text-anchor", "end")
Modification of the transform and x
The text needs to be rotated 180 degrees so that it is right way up; however, if you modify this function to add 180 degrees to any text, then the text will appear on the wrong side of the display. So, you'll need to set x to a new value too, so that it appears on the correct side of the display:
.attr('x', function (d, i) { return nodes[0].x - 215; }) // radius * 2, add 15 for spacing off point
.attr("transform", function(d,i) {
return "rotate(" + ((d.angle * 180) / Math.PI - 180) + ", 225, 225)"
})
All together, that looks like:
(function () {
var paddding = 250;
var createNodes = function () {
var nodeData = [
{ id: 0, label: 'AAA' },
{ id: 1, label: 'BBB' },
{ id: 2, label: 'CCC' },
{ id: 3, label: 'DDD' },
{ id: 4, label: 'EEE' },
{ id: 5, label: 'FFF' },
{ id: 6, label: 'GGG' },
{ id: 7, label: 'HHH' }
];
var radius = 100;
var nodes = [],
width = (radius * 2) + paddding,
height = (radius * 2) + paddding,
angle,
x,
y,
i;
var numNodes = nodeData.length;
for (i = 0; i < numNodes; i++) {
angle = (i / (numNodes / 2)) * Math.PI;
x = (radius * Math.cos(angle)) + (width / 2);
y = (radius * Math.sin(angle)) + (width / 2);
nodes.push({ 'id': i, 'x': x, 'y': y, 'label': nodeData[i].label, 'angle': angle });
}
return nodes;
}
var createSvg = function (radius, callback) {
d3.selectAll('svg').remove();
var svg = d3.select('#canvas').append('svg:svg')
.attr('width', (radius * 2) + paddding)
.attr('height', (radius * 2) + paddding);
callback(svg);
}
var createElements = function (svg, nodes, elementRadius) {
element = svg.selectAll('circle')
.data(nodes)
.enter().append('svg:circle')
.attr('r', elementRadius)
.attr('cx', function (d, i) { return d.x; })
.attr('cy', function (d, i) { return d.y; });
element = svg.selectAll('text')
.data(nodes)
.enter().append('svg:text')
.text(function (d, i) { return d.label + " - " + d.angle.toFixed(2) + ", " + (d.angle*180/Math.PI); })
.attr('x', function (d, i) { return nodes[0].x - 215; }) // radius * 2, add 15 for spacing off point
.attr('y', function (d, i) { return nodes[0].y; })
.attr("dy", ".35em")
.style("alignment-baseline","middle")
.style("text-anchor", "end")
.attr("transform", function(d,i) {
return "rotate(" + ((d.angle * 180) / Math.PI - 180) + ", 225, 225)";})
;
}
var draw = function () {
var radius = 100;
var nodes = createNodes();
createSvg(radius, function (svg) {
createElements(svg, nodes, 10);
});
}
$(document).ready(function () {
draw();
});
})();
* {
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 13px;
}
circle {
fill: steelblue;
fill-opacity: .8;
}
circle:hover {
fill: orange;
fill-opacity: .8;
}
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.v2.min.js"></script>
<div id="canvas"></div>
However, now the labels on the right are upside down. All that is left is to determine is whether a label falls on the right half or the left half and assign the appropriate attributes based on this.
Zero degrees points to the right, it is not the top of the diagram. Therefore, you need to ascertain if d.angle is less than 90 degrees (bottom right) or more than 270 degrees (top right), if so, your original code can be applied. If not, then you need to flip the label using the above code:
(function () {
var paddding = 250;
var createNodes = function () {
var nodeData = [
{ id: 0, label: 'AAA' },
{ id: 1, label: 'BBB' },
{ id: 2, label: 'CCC' },
{ id: 3, label: 'DDD' },
{ id: 4, label: 'EEE' },
{ id: 5, label: 'FFF' },
{ id: 6, label: 'GGG' },
{ id: 7, label: 'HHH' }
];
var radius = 100;
var nodes = [],
width = (radius * 2) + paddding,
height = (radius * 2) + paddding,
angle,
x,
y,
i;
var numNodes = nodeData.length;
for (i = 0; i < numNodes; i++) {
angle = (i / (numNodes / 2)) * Math.PI;
x = (radius * Math.cos(angle)) + (width / 2);
y = (radius * Math.sin(angle)) + (width / 2);
nodes.push({ 'id': i, 'x': x, 'y': y, 'label': nodeData[i].label, 'angle': angle });
}
return nodes;
}
var createSvg = function (radius, callback) {
d3.selectAll('svg').remove();
var svg = d3.select('#canvas').append('svg:svg')
.attr('width', (radius * 2) + paddding)
.attr('height', (radius * 2) + paddding);
callback(svg);
}
var createElements = function (svg, nodes, elementRadius) {
element = svg.selectAll('circle')
.data(nodes)
.enter().append('svg:circle')
.attr('r', elementRadius)
.attr('cx', function (d, i) { return d.x; })
.attr('cy', function (d, i) { return d.y; });
element = svg.selectAll('text')
.data(nodes)
.enter().append('svg:text')
.text(function (d, i) { return d.label + " - " + d.angle.toFixed(2) + ", " + (d.angle*180/Math.PI); })
.attr('x', function (d, i) {
if (d.angle > Math.PI/2 && d.angle < 1.5 * Math.PI) {
return nodes[0].x - 215 }
else {
return nodes[0].x + 15;
}
})
.attr('y', function (d, i) { return nodes[0].y; })
.attr("dy", ".35em")
.style("alignment-baseline","middle")
.style("text-anchor", function(d) {
if (d.angle > Math.PI/2 && d.angle < 1.5 * Math.PI) {
return "end"
}
else {
return "start";
}
})
.attr("transform", function(d,i) {
if (d.angle > Math.PI/2 && d.angle < 1.5 * Math.PI) {
return "rotate(" + ((d.angle * 180) / Math.PI - 180) + ", 225, 225)";
}
else {
return "rotate(" + ((d.angle * 180) / Math.PI) + ", 225, 225)"
}
})
;
}
var draw = function () {
var radius = 100;
var nodes = createNodes();
createSvg(radius, function (svg) {
createElements(svg, nodes, 10);
});
}
$(document).ready(function () {
draw();
});
})();
* {
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 13px;
}
circle {
fill: steelblue;
fill-opacity: .8;
}
circle:hover {
fill: orange;
fill-opacity: .8;
}
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.v2.min.js"></script>
<div id="canvas"></div>

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>

Automated collision detection and rectification for a force directed wordcloud in D3.js

I have been experimenting with creating a force directed layout using D3.js. In the following code, I populate a series of objects which are later rendered into text elements. I wish to avoid collision between the words, and adjust their representation on the grid accordingly.
I utilised the code written by Eric Dobbs here http://bl.ocks.org/dobbs/1d353282475013f5c156
but it is still not working for me. The objects end up flying all over the screen. I have spent many hours puzzling over this and I would greatly appreciate any help available.
here is my code
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<script>
//arguments passed in when this class is modularised
var metaDataObject = {"type":"wordcloud","label":"data1","data":"data2","color":"color"};
var dataObject = {"data1":["apple","orange","pear","grapes","mango","papaya","kiwi","banana", "watermelon","strawberry","honeydew","dragonfruit","durian","pineapple","jackfruit","lychee","mangosteen","passionfruit","raspberry","blueberry","rockmelon","coconut","lemon","lime","pomelo","rambutan","aguave","longan","mandarin","calamansi","sugarcane","avocado","bittergourd","wintermelon","dates"],"data2":[100,50,150,40,70,60,30,35,95,120,60,70,80,15,30,140,100,170,200,40,90,20,180,99,66,55,130,20,50,55,100,120,30,20,90],"color":["#23af50"]};
//transform raw data
var frequency_list = transformData(metaDataObject, dataObject);
frequency_list.sort(function(a, b) {
return b.size-a.size;
});
//set cloud container variables
var cloudWidth = 600;
var cloudHeight = 400;
var cloudContainer = d3.select("body").append("svg")
.attr("width", cloudWidth)
.attr("height", cloudHeight);
//set approximate scaling
var largestQty = d3.max(dataObject[metaDataObject.data]);
var rangeCap = cloudWidth*cloudHeight/7500;
var scale = d3.scale.linear()
.domain([0, largestQty])
.range([5, rangeCap]);
var color = d3.scale.linear()
.domain([0,1,2,3,4,5,6,10,15,20,30,largestQty])
.range(["#ddd", "#ccc", "#bbb", "#aaa", "#999", "#888", "#777", "#666", "#555", "#444", "#333", "#222"]);
var words = createText(frequency_list);
var rendered = cloudContainer.selectAll("node")
.data(words)
.enter()
.append("text")
.attr("id", function(d, i) {
return "t"+i;
})
.text(function(d) {
return d.text;
})
.attr("font-family", "sans-serif")
.attr("fill", function(d, i) {
return color(i);
})
.attr("font-size", function(d) {
return scale(d.size)+"px";
})
.on("mouseover", handleMouseOver)
.on("mouseout", handleMouseOut);
var force = d3.layout.force()
.nodes(words)
.size([cloudWidth, cloudHeight])
.charge(-50)
.gravity(0.1)
.on("tick", tick)
.start();
rendered.call(force.drag);
function tick(e) {
var q = d3.geom.quadtree(words);
for(var i=0; i<words.length; i++) {
var word = words[i];
q.visit(collide(word));
}
// console.log(d.x2);
rendered
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });
}
function collide(node) {
var nx1, nx2, ny1, ny2, padding;
padding = 32;
nx1 = node.x - padding;
nx2 = node.x2 + padding;
ny1 = node.y - padding;
ny2 = node.y2 + padding;
return function(quad, x1, y1, x2, y2) {
var dx, dy;
console.log(node.x2);
if (quad.point && (quad.point !== node)) {
if (overlap(node, quad.point)) {
dx = Math.min(node.x2 - quad.point.x, quad.point.x2 - node.x) / 2;
node.x -= dx;
quad.point.x += dx;
dy = Math.min(node.y2 - quad.point.y, quad.point.y2 - node.y) / 2;
node.y -= dy;
quad.point.y += dy;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
};
function overlap(a, b) {
return !(a.x2 < b.x ||
a.x > b.x2 ||
a.y2 < b.y ||
a.y > b.y2);
}
function createText(frequency_list) {
var words = [];
for(var wordIndex=0; wordIndex<frequency_list.length; wordIndex++) {
var word = {
x: Math.random() * (cloudHeight - 40) +20,
y: Math.random() * (cloudWidth -40) +20,
text: frequency_list[wordIndex].text,
size: frequency_list[wordIndex].size,
// x2: word.x + word.text.length * word.size /1.5,
// y2: word.y + scale(word.size) * 1.1
};
word.x2 = word.x + word.text.length * word.size /1.5;
word.y2 = word.y + scale(word.size) * 1.1;
words.push(word);
}
return words;
}
function transformData(metaDataObject, dataObject) {
//To transform data to this format:
// var frequency_list = [{"text":"apple","size":100}, {"text":"orange","size":100}, {"text":"pear","size":25}, {"text":"grapes","size":301}, {"text":"mango","size":56}];
var frequency_list = [];
var wordFieldName = metaDataObject.label;
var valuesFieldName = metaDataObject.data;
var wordList = dataObject[wordFieldName];
var valuesList = dataObject[valuesFieldName];
for(var itemIndex=0; itemIndex<wordList.length; itemIndex++) {
var item = {
text: wordList[itemIndex].toUpperCase(),
size: valuesList[itemIndex]
}
frequency_list.push(item);
}
return frequency_list;
}
function handleMouseOver(d, i) {
d3.select(this).transition().attr({
fill: "black",
"font-size": scale(d.size) + 5 + "px"
});
cloudContainer.append("text").attr({
id: "t" + d.text + "-" + d.size,
x: 10,
y:20
})
.text(function() {
return ["weight: " + d.size];
// return [""+occupiedSpaces[1].top];
})
}
function handleMouseOut(d, i) {
d3.select(this).transition().attr({
fill: color(i),
"font-size": scale(d.size) + "px"
});
d3.select("#t" + d.text + "-" + d.size).remove();
}
</script>
</body>
</html>

Rotate rect around axis in D3 graphs

I am trying to get a rectangle with four handles on all corners to rotate the rect in angle needed. Could someone assist me with how I would go about rotating the rect on its axis on the drag of the handles?
I did find an example which is for an ellipse and I tried modifying it for a rect but was not successful.
var w = 400,
h = 400,
data = {
x: 150,
y: 100,
rx: 50,
ry: 50,
angle: 0
};
// Returns radians
function angleBetweenPoints(p1, p2) {
if (p1[0] == p2[0] && p1[1] == p2[1])
return Math.PI / 2;
else
return Math.atan2(p2[1] - p1[1], p2[0] - p1[0] );
}
function distanceBetweenPoints(p1, p2) {
return Math.sqrt( Math.pow( p2[1] - p1[1], 2 ) + Math.pow( p2[0] - p1[0], 2 ) );
}
var svg = d3.select("body")
.append("svg")
.attr("width", 400)
.attr("height", 300);
var group = svg.append("svg:g").attr('id' , 'id123');
var handles = group.selectAll("circle")
.data([
{x:data.x, y:data.y + data.ry, name:"n"},
{x:data.x + data.rx, y:data.y, name:"e"},
{x:data.x, y:data.y - data.ry, name:"s"},
{x:data.x - data.rx, y:data.y, name:"w"}
], function (d) { return d.name; })
.enter()
.append("svg:circle")
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
.attr("r", 6.5)
.call(d3.behavior.drag()
.on("drag", function (d) {
// Resizing
var exy = [d3.event.x, d3.event.y],
dxy = [data.x, data.y],
dist = distanceBetweenPoints(exy, dxy),
angle = data.angle + angleBetweenPoints(dxy, exy);
switch(d.name) {
case "e":
case "w":
break;
case "s":
case "n":
angle += Math.PI/2;
break;
};
data.angle = angle;
update();
})
);
var ellipse = group.append("svg:ellipse");
function toDegrees(rad) {
return rad * (180/Math.PI);
}
function update() {
ellipse
.attr("cx", data.x)
.attr("cy", data.y)
.attr("rx", data.rx)
.attr("ry", data.ry);
group.attr("transform", "rotate(" + toDegrees(data.angle) + "," + data.x + "," + data.y + ")");
}
update();
http://jsfiddle.net/roug3/hon4kxp6/2/
First you make a rectangle
var rect = group.append("svg:rect");
Then place the rectangle accordingly.
//place the rectangle in its place
function update() {
rect
.attr("x", data.x - data.rx +5)//the x coordinate
.attr("y", data.y - data.ry +5)
.attr("width", (data.rx*2)-10)//the width
.attr("height", (data.ry*2)-10);//the height
group.attr("transform", "rotate(" + toDegrees(data.angle) + "," + data.x + "," + data.y + ")");
}
Working code here
Working code of a rectangle rotation here
Hope this helps!

How to draw circles around circular path with D3js

How to draw circles with random sizes around circular path with D3js, so that small circles will be randomly distributed and not overlapping one with each other.
Here is how it should look:
and here is what i was able to get
jQuery(document).ready(function () {
var angle, offset, data,
size = [8, 15],
width = 500,
color = d3.scale.category10(),
height = 600,
radius = 200,
dispersion = 10,
svgContainer = d3.select('body').append("svg")
.attr("width", width)
.attr("height", height);
data = d3.range(100).map(function () {
angle = Math.random() * Math.PI * 2;
offset = Math.max(size[0], size[1]) + radius + dispersion;
return {
cx : offset + Math.cos(angle) * radius + rand(-dispersion, dispersion),
cy : offset + Math.sin(angle) * radius + rand(-dispersion, dispersion),
r : rand(size[0], size[1])
};
});
svgContainer.selectAll("circle")
.data(data)
.enter().append("circle")
.attr({
r : function (d) {return d.r},
cx : function (d) {return d.cx},
cy : function (d) {return d.cy},
fill : function (d, i) {return color(i % 3)}
});
function rand(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
});
http://jsfiddle.net/yb8bgcrn/1/
UPDATED QUESTION
Is there a way to display it with d3 force layout but without using links ?
I have made some updates to your fiddle and applied collision detection as in the demo I mentioned in the comment. Hope this helps.
var angle, offset, data,
size = [8, 15],
width = 500,
color = d3.scale.category10(),
height = 600,
radius = 200,
dispersion = 10,
svgContainer = d3.select('body').append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.gravity(0.05)
.charge(function(d, i) {
return i ? 0 : -2000;
})
.distance(500)
.size([width, height]);
data = d3.range(100).map(function() {
angle = Math.random() * Math.PI * 2;
offset = Math.max(size[0], size[1]) + radius + dispersion;
return {
x: offset + Math.cos(angle) * radius + rand(-dispersion, dispersion),
y: offset + Math.sin(angle) * radius + rand(-dispersion, dispersion),
radius: rand(size[0], size[1])
};
});
force
.nodes(data)
.start();
root = data[0],
color = d3.scale.category10();
root.radius = 0;
root.fixed = true;
root.px = 250; //Center x
root.py = 275; //Center y
var nodes = svgContainer.selectAll("circle")
.data(data)
.enter().append("circle")
.attr({
r: function(d) {
return d.radius
},
cx: function(d) {
return d.x
},
cy: function(d) {
return d.y
},
fill: function(d, i) {
return color(i % 3)
}
});
force.on("tick", function(e) {
var q = d3.geom.quadtree(data),
i = 0,
n = data.length;
while (++i < n) q.visit(collide(data[i]));
svgContainer.selectAll("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
});
function rand(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
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;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
EDIT:
Do you mean something like this?
var angle, offset, data,
size = [8, 15],
width = 500,
color = d3.scale.category10(),
height = 600,
radius = 200,
dispersion = 10,
svgContainer = d3.select('body').append("svg")
.attr("width", width)
.attr("height", height).append("g");
var force = d3.layout.force()
.gravity(0.05)
.charge(function(d, i) {
return i ? -20 : -2000;
})
.distance(500)
.size([width, height]);
data = d3.range(100).map(function() {
angle = Math.random() * Math.PI * 2;
offset = Math.max(size[0], size[1]) + radius + dispersion;
return {
x: offset + Math.cos(angle) * radius + rand(-dispersion, dispersion),
y: offset + Math.sin(angle) * radius + rand(-dispersion, dispersion),
radius: rand(size[0], size[1])
};
});
force
.nodes(data)
.start();
root = data[0],
color = d3.scale.category10();
root.radius = 0;
root.fixed = true;
root.px = 250; //Center x
root.py = 275; //Center y
var nodes = svgContainer.selectAll("circle")
.data(data)
.enter().append("circle")
.attr({
r: function(d) {
return d.radius
},
cx: function(d) {
return d.x
},
cy: function(d) {
return d.y
},
fill: function(d, i) {
return color(i % 3)
}
});
var rotation = 0;
setInterval(function(){
if(force.alpha()==0){
if(!rotation)
rotation = Math.random() * 50;
else
rotation = rotation+1;
svgContainer.attr("transform","rotate("+rotation+", "+(width/2)+","+(height/2)+")");
}
//force.theta(0.5);
},250);
force.on("tick", function(e) {
var q = d3.geom.quadtree(data),
i = 0,
n = data.length;
while (++i < n) q.visit(collide(data[i]));
svgContainer.selectAll("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
});
function rand(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
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;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Yes, you can achieve this by force layout.
The idea is to keep all the links display none and center node display none none.
something like this:
nodeEnter.append("circle")
.style("display", function (d) {
return d.children ? "none" : ""; //ceneter node has children thus display will be none
})
In such a case it will just look like the visualization you want to have.
Working code here
Hope this helps!
I found solution in detecting collision function when generating random coordinates, here is the jsfiddle link and code:
(function () {
var angle, offset, data, x, y, r,
collision, circle1, circle2,
circles = [],
size = [8, 15],
width = 500,
color = d3.scale.category10(),
height = 600,
radius = 130,
dispersion = 10,
svgContainer = d3.select('body').append("svg")
.attr("width", width)
.attr("height", height);
function detectCollision(c2, c1) {
var dx = c1.cx - c2.cx;
var dy = c1.cy - c2.cy;
var rSum = c1.r + c2.r;
return ((Math.pow(dx, 2) + Math.pow(dy, 2)) < Math.pow(rSum, 2));
}
var sh = 2, elements = 55;
data = d3.range(elements).map(function (i) {
do {
// dispersion += i / 50;
angle = Math.random() * Math.PI * 2;
offset = Math.max(size[0], size[1]) + radius + dispersion + (elements/sh);
x = offset + Math.cos(angle) * radius + rand(- (dispersion + i / sh), dispersion + i / sh);
y = offset + Math.sin(angle) * radius + rand(- (dispersion + i / sh), dispersion + i / sh);
r = rand(size[0], size[1]);
circle2 = {cx : x, cy : y, r : r};
collision = false;
if (circles.length > 1) {
circles.forEach(function (d) {
circle1 = {cx : d.cx, cy : d.cy, r : d.r};
if (detectCollision(circle1, circle2)) {
collision = true;
}
});
}
} while (collision);
circles.push(circle2);
return circles[circles.length - 1];
});
svgContainer.selectAll("circle")
.data(data)
.enter().append("circle")
.attr({
r : function (d) {
return d.r
},
cx : function (d) {
return d.cx
},
cy : function (d) {
return d.cy
},
fill : function (d, i) {
return color(i % 3)
}
});
function rand(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
})();

Categories