How to limit draggable area in d3? - d3.js

I have a draggable element in d3, but I want to limit the draggable area to the borders of the svg, so that it never goes out of view. Is this possible? Here's a jsfiddle with no limit on where the element can be dragged:
https://jsfiddle.net/asicfr/34TKg/
var drag1 = d3.behavior.drag()
.origin(function() {
var t = d3.select(this);
return {x: t.attr("x") + d3.transform(t.attr("transform")).translate[0],
y: t.attr("y") + d3.transform(t.attr("transform")).translate[1]};
})
.on("drag", function(d,i) {
d3.select(this).attr("transform", function(d,i){
return "translate(" + [ d3.event.x,d3.event.y ] + ")"
})
});
group.call(drag1);

Nice clean JSFiddle - makes it easy to help you!
You can do so manually given that you know the dimensions of the container and the element you want to drag.
In essence you can check whether the potential new location of your dragged object will going beyond the bounds of the container. If so, then set the element's position to values that will make it be fully visible inside the contairer.
I have made a JSFiddle showcasing it: https://jsfiddle.net/0sga1ypc/
Most of the my updates are made in the callback function to the drag event:
.on("drag", function(d,i) {
var left = d3.event.x
if (left + groupWidth + strokeWidth > svgWidth) {
left = svgWidth - groupWidth - strokeWidth
} else if (d3.event.x < 0) {
left = 0
}
var top = d3.event.y
if (top + groupHeight + strokeWidth > svgHeight) {
top = svgHeight - groupHeight - strokeWidth
} else if (d3.event.y < 0) {
top = 0
}
d3.select(this).attr("transform", function(d,i){
return "translate(" + [ left,top ] + ")"
})
});
The only other changes I have made is to save relevant dimension values into variables, so that they are not writte out multiple times.
Hope this helps!

Related

d3: how to drag line elements independently of background

I have developed an applet that shows a d3 diagonal tree. The graph is navigatable by dragging the background.
It is based on the code found at the following link:
https://bl.ocks.org/adamfeuer/042bfa0dde0059e2b288
I am trying to have vertical lines across the page to further annotate the tree/ graph (based on the following link: https://bl.ocks.org/dimitardanailov/99950eee511375b97de749b597147d19).
See below:
See here: https://jsfiddle.net/chrisclarkson100/opfq6ve8/28/
I append the lines to the graph as follows:
var data_line = [
{
'x1': 300,
'y1': 700,
'x2': 300,
'y2': 700
},
////....
];
// Generating the svg lines attributes
var lineAttributes = {
....
'x1': function(d) {
return d.x1;
},
'y1': function(d) {
return screen.availHeight;
},
'x2': function(d) {
return d.x2;
},
'y2': function(d) {
return 0;
}
};
var drag_line = d3.behavior.drag()
.origin(function(d) { return d; })
.on('drag', dragged_line);
// Pointer to the d3 lines
var svg = d3.select('body').select('svg');
var lines = svg
.selectAll('line')
.data(data_line)
.enter().append('g')
.attr('class', 'link');
links_lines=lines.append('line')
.attr(lineAttributes)
.call(drag_line);
lines.append('text')
.attr('class','link_text')
.attr("x", d => d.x1)
.attr("y", d => 350)
.style('fill', 'darkOrange')
.style("font-size", "30px")
function dragged_line() {
var x = d3.event.dx;
var y = d3.event.dy;
var line = d3.select(this);
// Update the line properties
var attributes = {
x1: parseInt(line.attr('x1')) + x,
y1: parseInt(line.attr('y1')) + y,
x2: parseInt(line.attr('x2')) + x,
y2: parseInt(line.attr('y2')) + y,
};
line.attr(attributes);
}
The lines display as I wanted and are draggable. However, when I drag them, the background/ tree network moves with them... I want the different draggable elements to be independent of eachother.
Can anybody spot how I'm failing to make the dragging interactivity of each element independent of eachother?
Here's a JSFiddle that seems to do what you want– you can move the vertical lines without moving the tree; and move the tree without moving the vertical lines:
https://jsfiddle.net/adamfeuer/gd4ouvez/125/
(That JSFiddle uses a much smaller dataset of tree nodes; the one you linked to was too big to easily iterate and debug.)
The issue with the code you posted is that the zoom (pan) function for the tree is active at the same time the zoom() for the lines is active, so the tree and the active line drag at the same time.
I added a simple mechanism to separate the two – a boolean called lineDragActive. The code then checks for that in the tree zoom(), sets it true when a line drag starts, and false when the line drag ends:
// Define the zoom function for the zoomable tree
// flag indicates if line dragging is active...
// if so, we don't want to drag the tree
var lineDragActive = false;
function zoom() {
if (lineDragActive == false) {
// not line dragging, so we can zaoom (drag) the tree
svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
}
[...]
function drag_linestarted() {
// tell others not to zoom while we are zooming (dragging)
lineDragActive = true;
d3.select(this).classed(activeClassName, true);
}
function drag_lineended() {
// tell others that zooming (dragging) is allowed
lineDragActive = false;
d3.select(this).classed(activeClassName, false);
label = baseSvg.selectAll('.link_text').attr("transform", "translate(" + String(Number(this.x1.baseVal.value) - 400) + "," + 0 + ")");
}

How to position labels on dc.js pie chart?

I'd like to show labels for tiny slices (chart.minAngleForLabel(0.05)) avoiding text overlap.
I added a renderlet that shifts labels toward outer edge:
.on('renderlet', function(chart) {
chart.selectAll('text').attr('transform', function(d, i) {
var old = this.getAttribute('transform');
if (d.endAngle-d.startAngle > 0.3) { return old; }
var xy = old.slice(10,-1).split(',');
var m = 1.25 + (i%3) * 0.25;
return 'translate(' + (+xy[0]*m) + ',' + (+xy[1]*m) + ')';
})
})
and i'm rather happy with it (the second image is after renderlet):
but it makes annoying transitions -- labels move toward centroid and then jump back. Is there a workaround for this?
My solution is a bit excessive, but I wanted to know if it's now possible to replaced transitioned positions, now that we have the pretransition event in dc.js 2.0 beta 11.
In fact, it is. The impractical part is that your code relies on already having the final positions, which we're not going to have if we replace the transitions. Instead, we have to calculate the positions from scratch, which means copying a bunch of code out of the pie chart.
I wasn't able to get your code to work, so I'm just testing this by offsetting all label positions by -25, -25. But it's the same idea, we use the original code to get the centroid, and then modify that position:
// copied from pieChart
function buildArcs(chart) {
return d3.svg.arc().outerRadius(chart.radius()).innerRadius(chart.innerRadius());
}
function labelPosition(d, arc) {
var centroid = arc.centroid(d);
if (isNaN(centroid[0]) || isNaN(centroid[1])) {
return [0,0];
} else {
return centroid;
}
}
//
.on('pretransition', function(chart) {
chart.selectAll('text.pie-slice').transition().duration(chart.transitionDuration())
.attr('transform', function(d, i) {
var arc = buildArcs(chart);
var xy = labelPosition(d, arc);
return 'translate(' + (+xy[0] - 25) + ',' + (+xy[1] - 25) + ')';
})
});
The big idea here is that if you specify a new transition for an element, it will replace the transition that was already active. So we are completely removing the original position and transition, and replacing it with our own. No "jump"!
Not really solving your problem, but might look better with a transition on the position?
chart.selectAll('text')
.transition()
.delay(800)
.attr("transform", ...
I have a solution for this problem. Try this once , this will works to avoid overlapping of label names in pie charts.
function buildArcs(chart) {
return
d3.svg.arc().outerRadius(chart.radius()).innerRadius(chart.innerRadius());
}
function labelPosition(d, arc) {
var centroid = arc.centroid(d);
if (isNaN(centroid[0]) || isNaN(centroid[1])) {
return [0,0];
} else {
return centroid;
}
}
.on('pretransition', function(chart) {
chart.selectAll('text.pieslice').transition()
.duration(chart.transitionDuration())
.attr('transform', function(d, i) {
var j = 0;
var arc = buildArcs(chart);
var xy = labelPosition(d, arc);
if (xy[1] < 0) {
j = -(10 * (i + 1));
}
else {
j = 10 * (i + 1);
}
return 'translate(' + (+xy[0] - 25) + ',' + (j) + ')';
})
});

d3 circles shifting position on click-to-zoom

I'm trying to implement zooming on a d3 graphic with a bunch of data as circles. The data is made up of 1 large circle and many smaller circles plotted inside it. I want to click on the smaller circles and zoom to them. I'm using a variation of the zoom from the zoomable circle packing demo. I don't want to use the demo code directly but I've got it mostly working.
Initially all the circles are in their correct positions. However when I click on the smaller circles, they shift position right before the zoom. When I click on the white circle to zoom back, you can see they are now permanently shifted. And when it does zoom, the circles don't zoom into the center of the viewport, like they do in the demo.
I've noticed that when I comment out the transform line
node.attr("transform", function(d) { return "translate(" + (xscale(d.cx) - v[0]) * k + "," + (yscale(d.cy) - v[1]) * k + ")"; });
the circles now remain in their correct positions. Their sizes scale up as they should, but now they just merge into one another as they get bigger, because I'm no longer translating them. So the problem must be something in my transform attribute, but I can't figure out what it is. Or maybe it's something with my initial view? When I uncomment the zoomTo(view) the circles immediately move to the incorrect positions.
How do I get their positions to remain in the right positions? And how do I get the circles to zoom to the center of the viewpoint? I thought I followed the demo code pretty closely, but it's not quite working right. Any ideas?
I'd also like the axes to zoom as well but I haven't gotten that far into my problem yet.
Here's my jsfiddle.
And my full javascript code
function loadPlateDesign(){
var width = 400;
var height = 400;
var padding = 55;
var plateid = 7443;
var plateCen = {'ra': 230.99167, 'dec': 42.68736 };
var data = [{'name':7443,'color': 'white', 'cx': 0.0, 'cy': 0.0, 'r': 200},
{'color': 'red', 'cx': 8.23066, 'cy': -134.645, 'ra':231.1,'dec':42.1,'name': '1901', 'r': 10.0,
'children':[{'color': 'red', 'cx': 8.23066, 'cy': -134.645, 'ra':231.1,'dec':42.1,'name': 'a', 'r': 2.0}]},
{'color': 'blue', 'cx': -167.524, 'cy': -90.009, 'name': '711', 'r': 5.0}];
var xscale = d3.scale.linear().domain([330.,-330.]).range([0,400]);
var yscale = d3.scale.linear().domain([330.,-330.]).range([0,400]);
// initial focus and view
var focus = {'name':7443,'color': 'white', 'cx': 0.0, 'cy': 0.0, 'r': 200};
var view = [xscale(0.0),yscale(0.0),200*2];
// make the main svg element
var svg = d3.select('#platedesign').append('svg')
.attr('width',width+padding)
.attr('height',height+padding);
// add the plate and ifu data
var ifus=svg.selectAll('circle').data(data).enter().append('circle')
.attr('id',function(d){return d.name;})
.attr('cx',function(d,i){return xscale(d.cx);})
.attr('cy',function(d,i){return yscale(d.cy);})
.attr('r',function(d,i){return d.r;})
.style('fill',function(d,i){return d.color;})
.style('stroke','black')
.on('click',function(d){
if (focus != d) zoom(d), d3.event.stopPropagation();
});
// add the axes
var rascale = d3.scale.linear().domain([plateCen.ra+1.5,plateCen.ra-1.5]).range([0,400]);
var decscale = d3.scale.linear().domain([plateCen.dec+1.5,plateCen.dec-1.5]).range([0,400]);
xaxis = d3.svg.axis().scale(rascale).orient('bottom');
yaxis = d3.svg.axis().scale(decscale).orient('right').ticks(5);
svg.append('g').attr('class','x axis')
.attr('transform','translate(0,'+(height+5)+')')
.call(xaxis)
.append('text')
.attr('x',width/2)
.attr('y',35)
.style('text-anchor','middle')
.text('RA');
svg.append('g').attr('class','y axis')
.attr('transform','translate('+(width+5)+',0)')
.call(yaxis)
.append('text')
.attr('transform','rotate(90)')
.attr('x',height/2)
.attr('y',-35)
.style('text-anchor','middle')
.text('Dec');
var node = svg.selectAll("circle");
//zoomTo(view);
function zoom(d){
console.log('zooming to', d.name);
var focus0 = focus; focus=d;
var newview = [xscale(d.cx), yscale(d.cy), d.r*2+20];
var transition = d3.transition()
.duration(d3.event.altKey ? 7500 : 750)
.tween('zoom', function(d){
var i = d3.interpolateZoom(view, newview);
return function(t) {zoomTo(i(t)); };
});
}
function zoomTo(v) {
var k = height / v[2]; view = v;
console.log(height, v);
node.attr("transform", function(d) { return "translate(" + (xscale(d.cx) - v[0]) * k + "," + (yscale(d.cy) - v[1]) * k + ")"; });
ifus.attr("r", function(d) { return d.r * k; });
}
}
Looks like you are mixing positioning methods. You set an initial cx and cy but then you zoom on a transform. Re-factoring a bit to get all the positioning done with transform fixes it up. I also found that you should initiate the view and do the zoom calculations on your d.cx and d.cy instead of on the xscale(d.cx).
function zoom(d){
console.log('zooming to', d.name);
var focus0 = focus; focus=d;
var newview = [d.cx, d.cy, d.r*2+20];
var transition = d3.transition()
.duration(d3.event.altKey ? 7500 : 750)
.tween('zoom', function(d){
var i = d3.interpolateZoom(view, newview);
return function(t) {zoomTo(i(t)); };
});
}
function zoomTo(v) {
var k = height / v[2]; view = v;
console.log(height, v);
node.attr("transform", function(d) { return "translate(" + xscale((d.cx - v[0]) * k) + "," + yscale((d.cy - v[1]) * k) + ")"; });
ifus.attr("r", function(d) { return d.r * k; });
}
Updated fiddle.

D3.JS - Rotate a pie chart on mouse down event

I am looking for an example for to rotate a pie chart on mouse down event. On mouse down, I need to rotate the pie chart either clock wise or anti clock wise direction.
If there is any example how to do this in D3.js, that will help me a lot. I found an example using FusionChart and I want to achieve the same using D3.js
Pretty easy with d3:
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var g = svg.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.style("fill", function(d) {
return color(d.data.age);
});
var curAngle = 0;
var interval = null;
svg.on("mousedown", function(d) {
interval = setInterval(goRotate,10);
});
svg.on("mouseup", function(d){
clearInterval(interval);
})
function goRotate() {
curAngle += 1;
svg.attr("transform", "translate(" + width / 2 + "," + height / 2 + ") rotate(" + curAngle + "," + 0 + "," + 0 + ")");
}
Working example.
I did a similar thing with a compass instead of pie chart. You mainly need three methods - each bound to a different mouse event.
Bind this to the mousedown event on your compass circle:
function beginCompassRotate(el) {
var rect = compassCircle[0][0].getBBox(); //compassCircle would be your piechart d3 object
compassMoving = true;
compassCenter = {
x: (rect.width / 2),
y: (rect.height / 2)
}
}
Bind this to the mouse move on your canvas or whatever is holding your pie chart - you can bind it to the circle (your pie chart) but it makes the movement a little glitchy. Binding it to the circle's container keeps it smooth.
function rotateCompass() {
if (compassMoving) {
var mouse = d3.mouse(svg[0][0]);
var p2 = {
x: mouse[0],
y: mouse[1]
};
var newAngle = getAngle(compassCenter, p2) + 90;
//again this v is your pie chart instead of compass
compass.attr("transform", "translate(90,90) rotate(" + newAngle + "," + 0 + "," + 0 + ")");
}
}
Finally bind this to the mouseup on your canvas - again you can bind it to the circle but this way you can end the rotation without the mouse over the circle. If it is on the circle you will keep rotating the circle until you have a mouse up event over the circle.
function endCompassRotate(el) {
compassMoving = false;
}
Here is a jsfiddle showing it working: http://jsfiddle.net/4oy2ggdt/

How do I adjust zoom size for a point in D3?

This could be a classic case of "you're doing it wrong", but all of my searching to date hasn't warranted any help.
Here's my scenario:
I'm using an albersUSA map projection in conjunction with the national and county GeoJson files to draw everything.
I also have a self created "cities" file that contains major cities for each state. The coordinates are accurate and everything looks good.
When a user clicks on a given state, I hide all state shapes and then calculate the transform needed to get the county shapes for that state to fit within my viewport. I then apply that transform to all the necessary county shapes in order to get the "zoomed" view. My code is as follows:
function CalculateTransform(objectPath)
{
var results = '';
// Define bounds/points of viewport
var mapDimensions = getMapViewportDimensions();
var baseWidth = mapDimensions[0];
var baseHeight = mapDimensions[1];
var centerX = baseWidth / 2;
var centerY = baseHeight / 2;
// Get bounding box of object path and calculate centroid and zoom factor
// based on viewport.
var bbox = objectPath.getBBox();
var centroid = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
var zoomScaleFactor = baseHeight / bbox.height;
var zoomX = -centroid[0];
var zoomY = -centroid[1];
// If the width of the state is greater than the height, scale by
// that property instead so that state will still fit in viewport.
if (bbox.width > bbox.height) {
zoomScaleFactor = baseHeight / bbox.width;
}
// Calculate how far to move the object path from it's current position to
// the center of the viewport.
var augmentX = -(centroid[0] - centerX);
var augmentY = -(centroid[1] - centerY);
// Our transform logic consists of:
// 1. Move the state to the center of the screen.
// 2. Move the state based on our anticipated scale.
// 3. Scale the state.
// 4. Move the state back to accomodate for the scaling.
var transform = "translate(" + (augmentX) + "," + (augmentY) + ")" +
"translate(" + (-zoomX) + "," + (-zoomY) + ")" +
"scale(" + zoomScaleFactor + ")" +
"translate(" + (zoomX) + "," + (zoomY) + ")";
return results;
}
...and the binding function
// Load county data for the state specified.
d3.json(jsonUrl, function (json) {
if (json === undefined || json == null || json.features.length == 0)
{
logging.error("Failed to retrieve county structure data.");
showMapErrorMessage("Unable to retrieve county structure data.");
return false;
}
else
{
counties.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("id", function (d, i) {
return "county_" + d.properties.GEO_ID
})
.attr("data-id", function (d, i) { return d.properties.GEO_ID })
.attr("data-name", function (d, i) { return countyLookup[d.properties.GEO_ID] })
.attr("data-stateid", function (d, i) { return d.properties.STATE })
.attr("d", path);
// Show all counties for state specified and apply zoom transform.
d3.selectAll(countySelector).attr("visibility", "visible");
d3.selectAll(countySelector).attr("transform", stateTransform);
// Show all cities for the state specified and apply zoom transform
d3.selectAll(citySelector).attr("visibility", "visible");
d3.selectAll(citySelector).attr("transform", stateTransform);
}
});
This works fine here, except for really small states, the zoom factor is much larger, and the circles get distored.
Is there a way to force the size of the points to be a fixed size (say a 15px radius) even after the transform occurs?
For things you don't want to scale, just make them divided by 'scale' . In my case,
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+d3.event.translate.join(",")+")scale("+d3.event.scale+")");
g.selectAll(".mapmarker")
.attr("r",6/d3.event.scale)
.attr("stroke-width",1/d3.event.scale);
});
This is happening because you are setting a scale transform instead of scaling the positions. You can see the difference here Basically it is the difference between:
// Thick lines because they are scaled too
var bottom = svg.append('g').attr('transform', 'scale('+scale+','+scale+')');
bottom.selectAll('circle')
.data(data)
.enter().append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
and
// line thicknesses are nice and thin
var top = svg.append('g');
top.selectAll('circle')
.data(data)
.enter().append('circle')
.attr('cx', function(d) { return d.x * scale; })
.attr('cy', function(d) { return d.y * scale; });
With mapping probably you best solution is to compute your offset and scale as you do and then add them into your projection function - you want to directly modify the post-projection x and y values. If you update your projection function properly you should not have to do anything else to apply the appropriate zoom to your map.

Resources