When I zoom to a specific location on a mouse click and then try to pan or when I'm using the mouse wheel, the zoom behavior jumps. It seems that my zoom level is being restored like it was before the mouse click.
Here is my event handler:
function click(d) {
var x, y, k;
if (d && centered !== d) {
var centroid = path.centroid(d);
x = centroid[0];
y = centroid[1];
k = 4;
centered = d;
} else {
x = width / 2;
y = height / 2;
k = 1;
centered = null;
}
svgContainer.transition()
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")");
}
And this is how I "activate" my zoom and pan functionalities.
var svgContainer = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function () {
svgContainer.attr("transform", d3.event.transform)
}))
.append("g")
...
svgContainer.selectAll(null)
.data(feat.features)
.enter()
.append("path")
.on("click", click)
...
Background
You are manipulating the transform applied to your elements in the click function but not updating the zoom state to reflect this. d3.zoom does not track an element's transform at all. So when you modify the transform attribute of an element independently of d3.zoom, d3.zoom no longer "knows" what the transform is of that element - it remains unchanged. D3.zoom does track zoom state - internally and independently of any element's transform attribute.
It might seem odd that d3.zoom doesn't track an element's transform, but there is good reason. d3.zoom isn't always used to manipulate the transform of an element, it may alter something like element width or a scale while that element's transform remains unchanged. Here's a bl.ock of mine where d3.zoom here manipulates only the radius of circles on canvas.
Problem
As you don't update the zoom state in your click event, d3.zoom picks up where it was last left when applying a new zoom event, which explains your symptom: "It seems that my zoom level being restored like it was before the mouse click."
Solution
So, what do you need to do? You need to pass your zoom transform to d3.zoom and trigger a zoom event. This way d3.zoom is apprised of the current zoom state. Luckily there is a method for this. We take a d3.zoomIdentity (k=1,x=0,y=0) and translate and scale as appropriate, we can translate, scale, and then translate again as you have done too:
// Create a zoom transform from d3.zoomIdentity
var transform = d3.zoomIdentity
.translate(250,150)
.scale(k)
.translate(-x,-y);
And then we apply the zoom by calling zoom.transform which according to the docs:
sets the current zoom transform of the selected elements to the
specified transform, instantaneously emitting start, zoom and end
events. If selection is a transition, defines a “zoom” tween to the
specified transform using d3.interpolateZoom, emitting a start event
when the transition starts, zoom events for each tick of the
transition, and then an end event when the transition ends (or is
interrupted). (link)
We can call zoom.transform with:
// Apply the zoom and trigger a zoom event with a provided zoom transform:
svg.call(zoom.transform, transform);
So if this is analagous to what you have:
var zoom = d3.zoom()
.scaleExtent([1,8])
.translateExtent([[0,0],[500,300]])
.on("zoom",zoomed);
var svg = d3.select("div")
.append("svg")
.attr("width",500)
.attr("height",300)
.call(zoom);
var g = svg.append("g");
var rects = g.selectAll(null)
.data(d3.range(750))
.enter()
.append("rect")
.attr("width",17)
.attr("height",17)
.attr("fill","#eee")
.attr("y", function(d) { return Math.floor(d/50) * 20; })
.attr("x", function(d) { return d%50 * 20; })
.on("click", click);
function zoomed() {
g.attr("transform", d3.event.transform);
}
function click(d) {
rects.attr("fill","#eee");
var clicked = d3.select(this);
clicked.attr("fill","orange");
var x = +clicked.attr("x")+10;
var y = +clicked.attr("y")+10;
var k = 5;
var transform = "translate(" + 250 + "," + 150 + ")scale(" + k + ")translate(" + -x + "," + -y + ")";
g.transition()
.attr("transform",transform)
.duration(1000);
}
rect {
stroke-width: 1px;
stroke: #ccc;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div>
This can be analagous to your solution:
var zoom = d3.zoom()
.scaleExtent([1,8])
.translateExtent([[0,0],[500,300]])
.on("zoom",zoomed);
var svg = d3.select("div")
.append("svg")
.attr("width",500)
.attr("height",300)
.call(zoom);
var g = svg.append("g");
var rects = g.selectAll(null)
.data(d3.range(750))
.enter()
.append("rect")
.attr("width",17)
.attr("height",17)
.attr("fill","#eee")
.attr("y", function(d) { return Math.floor(d/50) * 20; })
.attr("x", function(d) { return d%50 * 20; })
.on("click", click);
function zoomed() {
g.attr("transform", d3.event.transform);
}
function click(d) {
rects.attr("fill","#eee");
var clicked = d3.select(this);
clicked.attr("fill","orange");
var x = +clicked.attr("x")+10;
var y = +clicked.attr("y")+10;
var k = 5;
// Create a zoom transform from d3.zoomIdentity
var transform = d3.zoomIdentity
.translate(250,150)
.scale(k)
.translate(-x,-y);
// Apply the zoom and trigger a zoom event:
svg.call(zoom.transform, transform);
}
rect {
stroke-width: 1px;
stroke: #ccc;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div>
Related
I've been trying to rotate some gears in an svg image clockwise repeatedly using D3.js but I can't seem to understand what's wrong with the code or the image. I was able to translate and rotate each gear using this code below...
d3.selectAll(".st6")
.transition()
.delay(function(d, i, n) { return i * 50; })
.on("start", function repeat() {
d3.active(this)
.transition()
.duration(2500)
.attr('transform', 'rotate(0)')
.transition() //And rotate back again
.duration(2500)
.attr('transform' , 'rotate(90) ')
.on("start", repeat); //at end, call it again to create infinite loop
});
But when I tried using the same code that did rotate a text repeatedly for me, the image became static and not moving again...
here is the code that rotate a text repeatedly for me...
var width = 600;
var height = 300;
var holder = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
//draw the text
holder.append("text")
.style("fill", "black")
.style("font-size", "56px")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.attr("transform", "translate(300,150) rotate(0)")
.text("Hi, how r u doing");
var i = 0;
var timeInterval = 10;
setInterval(function(){
i += 1;
update(i % 360)
}, timeInterval);
var n;
// update the element
function update(n) {
// rotate the text
holder.select("text")
.attr("transform", "translate(300,150) rotate("+n+")");
}
Here is what I tried replicating the above method but to no avail...
var width = 600;
var height = 300;
var holder = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
holder.append("svg:image")
.attr("image-anchor", "middle")
.attr("xlink:href", "produc.svg")
.attr("transform", "translate(300,150) rotate(0)")
// Initial starting angle of the text
var i = 0;
var timeInterval = 10;
setInterval(function(){
i += 1;
update(i % 360)
},timeInterval);
var n;
// update the element
function update(n) {
// rotate the text
holder.select("text")
.attr("transform", "translate(300,150) rotate("+n+")");
}
Here is a working example on CodePenclick here
As for the problem with your code (without considering if it is "a sledgehammer to crack a nut"):
You're taking working code that rotates text and not fully updating it to reflect that you are now working with an image. You have updated the append statement to append an image, but you haven't updated the update function to select that image - it's still looking for a text element:
function update(n) {
// rotate the text
holder.select("text")
.attr("transform", "translate(300,150) rotate("+n+")");
}
Since there is no longer a text element, this function doesn't select anything and consequently, doesn't set any element's transform. AS noted in the comment you need to select the image:
function update(n) {
// rotate the text
holder.select("image")
.attr("transform", "translate(300,150) rotate("+n+")");
}
As seen below:
var width = 600;
var height = 300;
var holder = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
holder.append("svg:image")
.attr("image-anchor", "middle")
.attr("xlink:href", "https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/compass.svg")
.attr("transform", "translate(200,20) rotate(0)")
.attr("width", 100)
// Initial starting angle of the text
var i = 0;
var timeInterval = 10;
setInterval(function(){
i += 1;
update(i % 360)
},timeInterval);
// update the element
function update(n) {
// rotate the text
holder.select("image")
.attr("transform", "translate(200,20) rotate("+n+",50,50)");
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
I have a question about D3 cartography.
I am working on a little project and I am new to D3.
I have started out from this example: http://bl.ocks.org/mbostock/5914438
Instead of the showing the state-mesh, I would like to show circles on the map in certain locations (lon/lat). I am currently facing a problem that the circles are not on the correct spots on the map. I suspect the problem lies in the special projection that Mike uses. He uses a 1x1 square projection. Probably this is necessary for displaying the tiles. When I project the coordinates, the values are all between -1 and 1. I thought I could fix it by multiplying it width the height and width but it didn't work. Below is my code (snippet does not run because it is missing a file). Thanks for the assistance!
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
margin: 0;
}
path {
fill: none;
stroke: red;
stroke-linejoin: round;
stroke-width: 1.5px;
}
circle {
fill: #fff;
fill-opacity: 0.4;
stroke: #111;
}
</style>
<svg>
</svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="//d3js.org/d3-tile.v0.0.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script>
var pi = Math.PI,
tau = 2 * pi;
var width = Math.max(960, window.innerWidth),
height = Math.max(500, window.innerHeight);
// Initialize the projection to fit the world in a 1×1 square centered at the origin.
var projection = d3.geoMercator()
.scale(1 / tau)
.translate([0, 0]);
var path = d3.geoPath()
.projection(projection);
var tile = d3.tile()
.size([width, height]);
var zoom = d3.zoom()
.scaleExtent([1 << 9, 1 << 23])
.on("zoom", zoomed);
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
var raster = svg.append("g");
var vector = svg.append("path");
var circle = svg.append("g")
d3.json("/data/flyingsites/AD.json", function(error, flyingsites) {
if (error) console.log(error);
// Compute the projected initial center.
var center = projection([6.2, 45.8]);//45,809718, 6,252314
// Apply a zoom transform equivalent to projection.{scale,translate,center}.
svg
.call(zoom)
.call(zoom.transform, d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(1 << 12)
.translate(-center[0], -center[1]));
//add flying sites
circle.selectAll("circle")
.data(flyingsites.features)
.enter().append("circle")
.attr('r',5)
.attr('cx',function(d) { return projection(d.geometry.coordinates)[0]*width})
.attr('cy',function(d) { return projection(d.geometry.coordinates)[1]*height})
.style('fill','red')
//console.log(flyingsites.features);
//console.log(circle);
});
function zoomed() {
var transform = d3.event.transform;
var tiles = tile
.scale(transform.k)
.translate([transform.x, transform.y])
();
vector
.attr("transform", transform)
.style("stroke-width", 1 / transform.k);
circle
.attr("transform", "translate(" + transform.x + "," + transform.y + ")");
var image = raster
.attr("transform", stringify(tiles.scale, tiles.translate))
.selectAll("image")
.data(tiles, function(d) { return d; });
image.exit().remove();
image.enter().append("image")
.attr("xlink:href", function(d) { return "http://" + "abc"[d[1] % 3] + ".tile.openstreetmap.org/" + d[2] + "/" + d[0] + "/" + d[1] + ".png"; })
.attr("x", function(d) { return d[0] * 256; })
.attr("y", function(d) { return d[1] * 256; })
.attr("width", 256)
.attr("height", 256);
}
function stringify(scale, translate) {
var k = scale / 256, r = scale % 1 ? Number : Math.round;
return "translate(" + r(translate[0] * scale) + "," + r(translate[1] * scale) + ") scale(" + k + ")";
}
</script>
The approach you are taking won't work, for one, it doesn't consider the scale (just translate). This is critical as d3-tile uses geometric zooming - it applies a zoom transform (scale and translate) to all the vector elements (not the tiles), this is why the projection projects everything to a one pixel square area and never changes with the zoom.
To solve this, place your circles the same as the example places (and sizes) the polygons:
vector
.attr("d", path(topojson.mesh(us, us.objects.counties)));
circle
.attr("cx", projection(coord)[0])
.attr("cy", projection(coord)[1])
.attr("r", 5/(1<<12));
Both of these position features the same way: with the projection only, projecting to a one pixel square. The zoom applies the transform to cover the whole svg. Also, since we are scaling that one pixel to fit the svg, we want the radius to be scaled appropriately too.
Now we can apply a transform to the circles the same as the polygons:
circle
.attr("transform", transform);
Of course, we could scale the radius down each zoom too, using the zoom k to modify the size of the circle:
var pi = Math.PI,
tau = 2 * pi;
var width = Math.max(960, window.innerWidth),
height = Math.max(500, window.innerHeight);
// Initialize the projection to fit the world in a 1×1 square centered at the origin.
var projection = d3.geoMercator()
.scale(1 / tau)
.translate([0, 0]);
var path = d3.geoPath()
.projection(projection);
var tile = d3.tile()
.size([width, height]);
var zoom = d3.zoom()
.scaleExtent([1 << 11, 1 << 14])
.on("zoom", zoomed);
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
var raster = svg.append("g");
var vector = svg.append("circle");
// Compute the projected initial center.
var center = projection([-98.5, 39.5]);
// Apply a zoom transform equivalent to projection.{scale,translate,center}.
svg
.call(zoom)
.call(zoom.transform, d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(1 << 12)
.translate(-center[0], -center[1]));
var coord = [-100,40]
vector
.attr("cx", projection(coord)[0])
.attr("cy", projection(coord)[1])
.attr("r", 5/(1<<12));
function zoomed() {
var transform = d3.event.transform;
var tiles = tile
.scale(transform.k)
.translate([transform.x, transform.y])
();
vector
.attr("transform", transform)
.attr("r", 5/transform.k);
var image = raster
.attr("transform", stringify(tiles.scale, tiles.translate))
.selectAll("image")
.data(tiles, function(d) { return d; });
image.exit().remove();
image.enter().append("image")
.attr("xlink:href", function(d) { return "http://" + "abc"[d[1] % 3] + ".tile.openstreetmap.org/" + d[2] + "/" + d[0] + "/" + d[1] + ".png"; })
.attr("x", function(d) { return d[0] * 256; })
.attr("y", function(d) { return d[1] * 256; })
.attr("width", 256)
.attr("height", 256);
}
function stringify(scale, translate) {
var k = scale / 256, r = scale % 1 ? Number : Math.round;
return "translate(" + r(translate[0] * scale) + "," + r(translate[1] * scale) + ") scale(" + k + ")";
}
<svg></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-tile.v0.0.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
Ultimately d3-tile can be a bit confusing to start with because you are using quite a few coordinate systems (tile, zoom, pixel, projected, geographic), and normally aren't projecting the entire map to a 1 pixel square.
If you click the red button in this example:
https://bl.ocks.org/interwebjill/fe782e6f195b17f6fe6798a24c390d90
you can see that the chart translates so that the circle is in the center and then zooms in to a specified level (reclicking on the button zooms back out). Translating and then zooming in this way leaves a gap on the left that I would rather not have. How might I change the code so that the chart zooms first and then translates to center so that I don't have this gap in the chart?
I have tried reversing the order of the scale and translate in both the zoom definition and the zoomToExtent function but there is no different in effect.
The ultimate source of the problem is d3.interpolateZoom. This interpolator has scale interpolate faster than translate - even though they mostly both are transitioning at the same time. The pattern implemented with d3.interpolateZoom is based on this paper.
Because scale and translate both interpolate differently in d3.interpolateZoom, you get a gap in the side of your chart as the scale decreases/increases more rapidly than the translate values.
d3.interpolateZoom is used when you call the zoom on a transition.
However, if you apply a transform directly on a transition using .attr(), the d3 transition will use d3.interpolateString, which will search the start and end strings for corresponding numbers and use d3.interpolateNumber on those. This will apply the same interpolation to both scale and translate.
Using both methods we can compare the discrepancy between d3.interpolateZoom and d3.interpolateString. Below the black rectangle uses d3.interpolateString while the orange rectangle uses d3.interpolateZoom. Click on a rectangle to start the transition:
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 300);
var g1 = svg.append("g"), g2 = svg.append("g");
var zoom1 = d3.zoom().on("zoom", function() {
g1.attr("transform", d3.event.transform);
});
var zoom2 = d3.zoom().on("zoom", function() {
g2.attr("transform", d3.event.transform);
});
g1.call(zoom1.transform, d3.zoomIdentity
.translate(150, 100)
.scale(2));
g2.call(zoom2.transform, d3.zoomIdentity
.translate(150,100)
.scale(2));
g1.append("rect")
.attr("x", 20)
.attr("y", 20)
.attr("width", 50)
.attr("height", 50);
g2.append("rect")
.attr("x", 22)
.attr("y", 22)
.attr("width", 46)
.attr("height",46)
.attr("fill","orange");
d3.selectAll("rect").on("click", function() {
g1.transition()
.duration(6000)
.attr("transform", d3.zoomIdentity)
.on("end", function() {
d3.select(this).call(zoom1.transform, d3.zoomIdentity);
})
g2.transition()
.duration(6000)
.call(zoom2.transform, d3.zoomIdentity)
});
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>
Where the first rectangle transitions the transform with .attr(), we need to call the zoom afterwards to ensure the zoom has the current transform, we don't need to in this example, but if you wanted to use the zoom after the transform you need to do this
Comparing these two we get:
(Y axis indicates percentage remaining in transition from start attribute to end attribute)
You want scale and translate to move simultaneously at the same rate when transitioning. We can do this if we use a tweening function. Unlike above we can't just use transition().attr("transform",newTransfrom) because you are also drawing canvas and updating the axis. So we'll need to create our own tweening function that can use the current transform and scale, apply it to the axis, canvas, and markers.
For example, rather than calling the zoom (which will use d3.interpolateZoom):
function zoomToExtent(d0, d1) {
zoomRect.call(zoom).transition()
.duration(1500)
.call(zoom.transform, d3.zoomIdentity
.translate(-xSVG(d0), 0)
.scale(width / (xSVG(d1) - xSVG(d0))));
}
Instead, we can use a tweening function which controls the element's transform and applies the same interpolator to scale and translate:
function zoomToExtent(d0, d1) {
//get transition start and end values:
var startScale = d3.zoomTransform(zoomRect.node()).k;
var startTranslate = d3.zoomTransform(zoomRect.node()).x;
var endTranslate = -xSVG(d0);
var endScale = width / (xSVG(d1) - xSVG(d0));
zoomRect.call(zoom).transition()
.duration(1500)
.tween("transform", function() {
var interpolateScale = d3.interpolateNumber(startScale,endScale);
var interpolateTranslate = d3.interpolateNumber(startTranslate,endTranslate);
return function(t) {
var t = d3.zoomIdentity.translate(interpolateTranslate(t),0).scale(interpolateScale(t));
zoomed(t);
}
})
.on("end", function() { // update the zoom identity on end:
d3.select(this).call(zoom.transform, d3.zoomIdentity
.translate(endTranslate, 0)
.scale(endScale));
})
}
You may notice I'm passing a transform value to the zoomed function, since there is no d3.event.transform for this, we need to modify the zoomed function to use the passed parameter if available, otherwise to fall back on the event transform:
function zoomed(transform) {
var t = transform || d3.event.transform;
...
Altogether, that might look something like this.
For another comparison between the two transitioning methods, I've created a gridded comparison that can be toggled between the two zoom identities:
var svg = d3.select("body").append("svg")
.attr("width", 510)
.attr("height", 310);
var g1 = svg.append("g");
var g2 = svg.append("g");
var rectangles1 = g1.selectAll()
.data(d3.range(750))
.enter()
.append("rect")
.attr("x", function(d) { return d%25*20; })
.attr("y", function(d) { return Math.floor(d/25)*20; })
.attr("width", 20)
.attr("height", 20)
.attr("fill","#ccc")
.attr("stroke","white")
.attr("stroke-width", 2);
var rectangles2 = g2.selectAll()
.data(d3.range(750))
.enter()
.append("rect")
.attr("x", function(d) { return d%25*20; })
.attr("y", function(d) { return Math.floor(d/25)*20; })
.attr("width", 20)
.attr("height", 20)
.attr("fill","none")
.attr("stroke","#444")
.attr("stroke-width", 1);
var startZoom = d3.zoomIdentity
.translate(-250,-200)
.scale(4);
var endZoom = d3.zoomIdentity
.translate(-100,-100)
.scale(5);
var zoom1 = d3.zoom().on("zoom", function() { g1.attr("transform", d3.event.transform); });
var zoom2 = d3.zoom().on("zoom", function() { g2.attr("transform", d3.event.transform); });
g1.call(zoom1.transform, startZoom);
g2.call(zoom2.transform, startZoom);
var toggle = true;
svg.on("click", function() {
toggle = !toggle;
g1.transition()
.duration(5000)
.call(zoom1.transform, toggle ? startZoom: endZoom)
g2.transition()
.duration(5000)
.attr("transform", toggle ? startZoom: endZoom)
.on("end", function() {
d3.select(this).call(zoom2.transform, toggle ? startZoom: endZoom);
})
})
rect {
opacity: 0.5;
}
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>
I have a D3.js map based on this: https://bl.ocks.org/mbostock/2374239.
I have added a custom marker and a text on the feature that a user zooms in to a county. However, the marker and text do not stay on the same position as the county when I zoom in or out. My zoom function is as follows:
function zoomToCounty(stname, cntyname) {
d3.json("/topo/us-wgs84.json", function (us) {
var t = projection.translate(); // the projection's default translation
var s = projection.scale() // the projection's default scale
//initialize marker
d3.selectAll(".mark").remove();
d3.selectAll("text").remove();
//reset active to inactive size and color
d3.select("#svgMap2").select("g").select(".active")
.style("stroke-width", "0.5px")
.style("stroke", "#808080");
d3.selectAll(".county")
.classed("active", function (d) {
if (d.properties.StateName === stname && d.properties.County === cntyname) {
var zoom = d3.zoom()
.scaleExtent([1, 6])
.on("zoom", zoomed3);
svg.select("rect")
.call(zoom);
var bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = 0.9 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
//get centroid
var center = path.centroid(d);
//create marker
d3.select("#svgMap2").select("g")
.append("image")
.attr("xlink:href", "/images/marker2.png")
.attr("width", 14)
.attr("height", 20)
.attr("class", "mark")
.attr("transform", function (d) {
return "translate(" + center + ")";
});
//add text
d3.select("#svgMap2").select("g")
.append("text")
.style("fill", "#000")
.attr("x", x)
.attr("y", y)
.attr("dy", ".05em") //set offset y position
.attr("text-anchor", "middle") //set anchor y justification
.text(cntyname);
svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale));
return true;
}
})
}); //end d3.json
Working website can be found at: http://realtimeceap.brc.tamus.edu/
Thanks in advance.
01-28-2018 Status: I'm still unable to fix this one. I just need help on how to keep my image marker and text on the same coordinates as the selected feature when I zoom in/out using the mouse wheel. Initial zoom is at the middle of the svg with scale = 8. How do I make the marker "STICK" to specified coordinates once a user moves the wheel? HELP!
Solved this problem by:
1. I called the zoom function on the "g" element then
2. I created a function for when user moves the wheel; called this function from the "rect" element.
View the working codes at: http://realtimeceap.brc.tamus.edu
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/