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).
D3 uses SVG to create and modify the graphical elements of the visualization. Because SVG has a structured form, D3 can make stylistic and attribute changes to the shapes being drawn.
D3 cannot easily conceal original data. If you're using data that you don't want shared, it can be challenging to use D3. D3 doesn't generate predetermined visualizations for you.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With