short version:
I am using Axis Zooming and normal Zooming. I combine both together and the Zooming works fine. Only problem is, that the translation is not working as I want it. The translation one the axes is not 1 to 1. It depends on the scale factor of the normal zoom, how the translation behaves.
my status:
I have a line graph, which has normal zooming. Additional to that I have Axis-Zooming. So if I am in the Y-axis area, I only want to zoom the Y-axis and only move the Y-axis around. The same for the X-Axis.
For that I used d3.zoom instance and called(zoom) on 3 different rectangles.
is covering the whole graph area
is covering only x-axis
is only covering y-axis
The transform is saved on the elements.
The zoom function applies all 3 different zoom transforms to the scale, when triggered.
Setting everything up:
graph.zoomXY = d3.zoom()
.scaleExtent([-10, 1000]);
graph.overlayX = graph.g//.select(".axis.x")
.append("rect")
.attr("fill", "rgba(255,0,0,0.5)")
.attr("width", graph.rectWidth)
.attr("height", 15)
.attr("y", graph.rectHeight)
.call(graph.zoomXY);
graph.overlayY = graph.g//.select(".axis.y")
.append("rect")
.attr("fill", "rgba(255,0,0,0.5)")
.attr("width", 150)
.attr("height", graph.rectHeight)
.attr("x", -150)
.call(graph.zoomXY);
//append the rectangle to capture zoom
graph.overlayRect = graph.g.append("rect")
.attr("class", "overlay-rect")
.attr("width", graph.rectWidth)
.attr("height", graph.rectHeight)
.style("fill", "none")
.call(graph.zoomXY)
.on("dblclick.zoom", function() {
resetZoom();
} );
Calculating Scale:
function zoomed() {
getZoomedScales();
updateGraph();
}
function getZoomedScales() {
var transformX = d3.zoomTransform(graph.overlayX.node());
var transformY = d3.zoomTransform(graph.overlayY.node());
var transformXY = d3.zoomTransform(graph.overlayRect.node());
graph.yScaleTemp = transformXY.rescaleY(transformY.rescaleY(graph.yScale));
graph.xScaleTemp = transformXY.rescaleX(transformX.rescaleX(graph.xScale));
}
The Zooming is working fine. But the translation on the axes Zoom (graph.overlayY and graph.overlayX) is influenced by the Scaling factor of the zoom applied to graph.overlayRect. If I change the order, the issue will be just flipped. The axes Zoom's scale factor (graph.overlayY and graph.overlayX), messes up the translation of the Zoom on graph.overlayRect.
Open the fiddle and change the Zooming, while over the graph area. Then mousedown and mousemove on one of the axes. Repeat and see how it changes the translation.
Here is a fiddle:
https://jsfiddle.net/9j4kqq1v/
Related
I'm trying to remove the size update of my line when i zoom on my svg.
Basically my line is like that
points = [[this.x(first.longitude), this.y(0)], [this.x(first.longitude), this.y(first.altitude)]];
this.line[i] = this.chart
.append('path')
.attr('d', curveafter(points))
.attr('p', id.segmentName)
.attr('stroke', "black")
.attr('fill', 'none')
.on('click', this.filter)
.attr("stroke-width", 1)
and when i'm zooming, i'm updating the position of the line like that
updateChart(d) {
var newX = d.transform.rescaleX(this.x);
var newY = d.transform.rescaleY(this.y);
this.xAxis.call(d3.axisBottom(newX))
this.yAxis.call(d3.axisLeft(newY))
this.line.forEach((line) => {
line
.attr("transform", d.transform)
.attr("stroke-width", 1)
}
)
},
But when i'm zooming, the size of my line increases, and when I zoom out it decreases.
How do I block the stroke-width (size) and not move it no matter what I do?
You have 2 options:
Don't put your <line> under transformed <g>. When you handle a zoom event, recalculate the line endpoint coordinates and update the <line> attributes (x1,y1,x2,y2)
Set the line's stroke-width according to the zoom factor: strokeWidth = 1 / e.transform.k (I don't like this option but it can work)
jsfiddle DEMO
I am trying to add a drag to the circle and trying to apply a translateExtent. So how to restrict the drag boundary to the rectangle .?
var height = 500;
var width = 500;
//if extent is specified, sets the translate extent to the specified array of points [[x0, y0], [x1, y1]], where [x0, y0] is the top-left corner of the world and [x1, y1] is the bottom-right corner of the world, and returns this zoom behavior.
var zoom = d3.zoom()
.translateExtent([[100, 100], [400, 400]])
.on("zoom", zoomed);
// Feel free to change or delete any of the code you see in this editor!
var svg = d3.select("body")
.append("svg")
.attr("width", height)
.attr("height", width)
.append("g")
svg.append("rect")
.attr("x", 100)
.attr("y", 100)
.attr("height", 300)
.attr("width", 300);
var circle = svg.append("circle")
.attr("cx", 100)
.attr("cy", 100)
.attr("r", 20)
.style("fill", "red")
svg.call(zoom);
function zoomed() {
circle.attr("transform", d3.event.transform);
}
Any detailed explanation of how the https://github.com/d3/d3-zoom#zoom_translateExtent works ? How is the boundary calculated from the coordinates.
There are a few considerations here, and given I've certainly been tripped up by them in the past, I hope I can explain them clearly here.
Zoom Extent
Let's look at zoom extent (zoom.extent) - not translate extent. The default extent is "[[0, 0], [width, height]] where width is the client width of the element and height is its client height" (d3-zoom docs). Since you are calling the zoom on the svg, the default extent should be [0,0],[width,height], where width and height in your case are 500 each.
Your translate extent, [100,100],[400,400] is smaller than your zoom extent, this doesn't work, from Mike Bostock on a similar issue : "The problem is that the translateExtent you’ve specified is smaller than the zoom extent. So there’s no way to satisfy the requested constraint." (d3-zoom issue tracker).
TranslateExtent
The issue then, is that you are using translate extent incorrectly. The translate extent you have specified is the bounds that you want the circle to constrained to. But this is not equal to the translate extent, which is the bounds of the coordinate space you want to show (the bounds of the world in which the circle resides) given a zoom extent.
Let's consider the circle at [100,100], it is centered there with a zoom transfrom with translate(0,0): it is at its starting position. This marks the top left position for the bounding box that you hope to constrain the circle in. The top left coordinate of the zoom at this point is [0,0]. The bottom right of the zoom extent or viewport is [500,500].
If the circle is at [400,400], the bottom right of its intended movement, it is has a transform of translate(300,300) as it is 300 pixels right and 300 pixels down from where it started (originally positioned with cx/cy). Given everything is shifted 300 pixels down and right, the top left of the viewport or zoom extent is now [-300,-300] (a circle with cx,cy of -300 would have its center at the top left corner of the SVG given the zoom transform). And the bottom right is [200,200].
To start, when the circle cannot move further up or left, we have a shown extent of [0,0],[500,500], and when the circle is in the bottom right, when the circle cannot move further down or right, we have a shown extent of [-300,-300],[200,200].
Taking the extremes, the maximum extent we want then is: [-300,-300],[500,500], this is the extent of the world we want to show so that the circle remains overlapping with the rectangle:
var height = 500;
var width = 500;
var zoom = d3.zoom()
.translateExtent([[-300, -300], [500, 500]])
.on("zoom", zoomed);
var svg = d3.select("body")
.append("svg")
.attr("width", height)
.attr("height", width)
.append("g")
svg.append("rect")
.attr("x", 100)
.attr("y", 100)
.attr("height", 300)
.attr("width", 300);
var circle = svg.append("circle")
.attr("cx", 100)
.attr("cy", 100)
.attr("r", 20)
.style("fill", "red")
svg.call(zoom);
function zoomed() {
circle.attr("transform", d3.event.transform);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Possible Refinement
If we use a zoom extent with width and height equal to the width and height of the rectangle:
.extent([[0,0],[300,300]])
We don't have to extend our translateExtent to account for the empty space around the rectangle that is still within the SVG:
.translateExtent([[-300,-300],[300,300]])
var height = 500;
var width = 500;
//if extent is specified, sets the translate extent to the specified array of points [[x0, y0], [x1, y1]], where [x0, y0] is the top-left corner of the world and [x1, y1] is the bottom-right corner of the world, and returns this zoom behavior.
var zoom = d3.zoom()
.translateExtent([[-300,-300],[300,300]])
.extent([[0,0],[300,300]])
.on("zoom", zoomed);
console.log(zoom.extent());
// Feel free to change or delete any of the code you see in this editor!
var svg = d3.select("body")
.append("svg")
.attr("width", height)
.attr("height", width);
svg.append("rect")
.attr("x", 100)
.attr("y", 100)
.attr("height", 300)
.attr("width", 300);
var circle = svg.append("circle")
.attr("cx", 100)
.attr("cy", 100)
.attr("r", 20)
.style("fill", "red")
svg.call(zoom);
function zoomed() {
circle.attr("transform", d3.event.transform);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
I'm working in a project to visualize position in a map using D3JS.
I'm using d3.geoMercator() to convert position to pixels.
here is my code:
var projection = d3.geoMercator()
.scale(2600)
.center([-1.16455 , 47.15237]) // Pan north 40 degrees
.translate([width/2,height/2]);
then here is the drawing of the positions:
node = svg.selectAll("circle")
.data(this.myData)
.enter()
.append("circle")
.attr("class", "dot")
.attr("r", 10)
.attr("fill", this.col)
.attr("transform", function(d) {
return "translate(" + projection([d.x,d.y]) + ")";});
the positions are displayed correctly, but when i tried to do some zoom in the map, the position doesn't behave in a correct way so they take the correct place.
I need to do dynamic convert of position each time the user make a zoom.
Here the code that i used to do the zoom:
var zoom = d3.zoom()
.on("zoom", function() {
this.node.attr("transform", d3.event.transform);
});
svg.call(zoom);
Hello I do an sunburst or bilevel chart it's middle of a pie & donut chart ^^ When I append all path it works fine:
this.path = this.svg.selectAll("path")
.data(this.partition.nodes(rootData).slice(1))
.enter().append("path")
.attr("d", this.arc)
.style("fill", function(d) { return d.fill; })
.each(function(d){ this._current = thiss.updateArc(d);});
But the probleme is when I'm trying to add a circle in middle-extern of all my path so it didn't work, this code add circle in the middle-middle of all my path fine
var indicator = this.svg.selectAll('circle')
.data(this.partition.nodes(rootData))
.enter().append("circle")
.attr("cx", function(d){return thiss.arc.centroid(d)[0]})
.attr("cx", function(d){return thiss.arc.centroid(d)[1]})
.attr("r", 5).style('fill','#ff0000');
But I need to add this little circle in the midle but on extern border of the path.
I don't know how I can get the right cx and cy attributs, help please ?
This is screenshot of my goal (black points are what I had) and (red points are what I want to do)
http://i.stack.imgur.com/GXPYM.jpg
This is in part a repeat of Lars' equations from the comments, but I thought it was worth recapping all at once because the trig identities for converting from angles to x/y coordinates won't match your trig text book.
Most text books assume that angles start at the right horizontal axis and increase counter-clockwise, and that the vertical axis has larger values higher up on the page.
In SVG, larger y values are lower on the page, and the angles created by the pie chart layout (and the example code the OP is using for the sunburst layout) draw an angle of zero as vertical line to the top of the circle, with angles increasing clockwise.
With that information, you can convert to x and y values with the following trig equations:
g.append("circle") //circles inherit pie chart data from the <g>
.attr("r", 5)
.attr("cx", function(d) {
return Math.sin((d.startAngle + d.endAngle)/2) *radius;
})
.attr("cy", function(d) {
return -Math.cos((d.startAngle + d.endAngle)/2) *radius;
});
Live example: http://fiddle.jshell.net/4x9ap/1/
Again, this simple example uses a pie chart layout, so the data has startAngle and endAngle values, and the radius is constant. For a sunburst diagram made with the partition layout, you would replace (d.startAngle + d.endAngle)/2 with d.x + d.dx/2, and you would replace radius with a function based on d.depth.
As an alternative to trigonometry, you could use transformations to position the circles. If the first step in a transformation is a rotation, and then you apply a translation afterwards, the translation will be applied in the rotated coordinate system.
A little extra complication, though, is that the d3 pie chart gives angles in radians (since that's what the trigonometry functions use), but the rotation needs angles in degrees.
var degreesPerRadian = 180/Math.PI;
g.append("circle") //circles inherit pie chart data from the <g>
.attr("r", 5)
.attr("transform", function(d) {
return "rotate(" + degreesPerRadian*((d.startAngle + d.endAngle)/2)
+ ")" +
//rotate by the average of the start and end angle
//Note that d3 specifies angles in radians, but the rotate
//function needs them in degrees
"translate(0," + -radius + ")";
//then translate "up" the distance of the radius;
//"up" is interpretted according to the rotated coordinates,
//but for zero rotation it will position the dot at the top
//of the circle, which is the zero angle for d3
});
Live example: http://fiddle.jshell.net/4x9ap/
(based on this simple pie chart code )
I have a simple bar chart drawn in d3, with vertical bars: http://jsfiddle.net/philgyford/LjxaV/2/
However, it's drawing the bars down, with the baseline at the top of the chart.
I've read that to invert this, drawing up from the bottom, I should change the range() on the y-axis. So, change this:
.range([0, chart.style('height')]);
to this:
.range([chart.style('height'), 0]);
However, that looks like it's drawing the inverse of the chart - drawing in the space above each of the bars, and leaving the bars themselves (drawn from the bottom) transparent. What am I doing wrong?
Per the d3 basic bar chart :
http://bl.ocks.org/mbostock/3885304
You are correct in inverting the range.
Additionally, your rectangles should be added like this:
.attr('y', function(d) { return y(d.percent); } )
.attr('height', function(d,i){ return height - y(d.percent); });
The x and y coordinates for svg start in the top left. You want the y to start on the bottom. The code below assumes you're appending to some function along the lines of:
svg.selectAll('rect')
.data(dataset)
.enter()
.append('rect')
To make the bar plot act as you desire, set the y attribute to begin at distance data[i] above the axis:
.attr('y', function(d) { return height - d; })
Then, you must make the distance extend the remaining data[i] to the axis.
.attr('height', function(d) { return d; })
And that's it!
Setting the y attribute seems to work:
.attr('y', function(d){ return (height - parseInt(y(d.percent))); })
jsfiddle here