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>
Related
Trying to work my head around how I can rotate this rectangle from the dead centre only using D3.js and Javascript.
const height = window.innerHeight - 20,
width = window.innerWidth - 20;
var body = d3
.select("body")
.style("text-align", "center")
.style("background", "#3c3c3c");
var svg = d3
.select("body")
.append("svg")
.attr("height", height)
.attr("width", width)
.style("background", "red")
.style("display", "inline-block");
var g = svg.append("g")
// Currently the point of origin is the top left. How can I move this to the center?
var rect = g
.append("rect")
.attr("width", width / 5)
.attr("height", width / 5)
.attr("transform", `translate(${width/ 2}, ${height/2}) rotate(0)`)
.style("fill", "white")
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
You are missing the x and y attributes of the rect. Since these give the top-left point, you can set the rect at the centre of the svg with:
// get centre of svg and rect width and height
var svgCentre = {x: width/ 2, y: height / 2};
var rectDims = {w: width / 5, h: height / 5};
var rect = g
.append("rect")
.attr("x", svgCentre.x - (rectDims.w / 2)) // offset left by half rect width
.attr("y", svgCentre.y - (rectDims.h / 2)) // offset up by half rect height
.attr("width", rectDims.w)
.attr("height", rectDims.h)
.style("fill", "white");
Where svgCentre is half the width and half the height of the svg. Then you can offset the top-left point of the rect by half the width and half the height of the rect.
You mention you want to rotate the rect around the centre point - you can re-use the svgCentre coordinates in the rotate transform:
.attrTween("transform", () => {
var i = d3.interpolate(0, 360); // full circle
// set centre of rotation at centre of svg
return t => `rotate(${i(t)}, ${svgCentre.x}, ${svgCentre.y})`;
});
attrTween is figuring out that over the course of the transition you need to rotate through 360 degrees i.e. 5.55 degrees per millisecond. The return t => ... is saying return a function with an argument t which is some time in the duration that when passed to the d3.interpolate function (assigned to i) will resolve to the amount of degrees to rotate based on the time elapsed. This is then put into the string attribute of the transform for that moment in time.
Example:
const height = window.innerHeight - 20,
width = window.innerWidth - 20;
var body = d3
.select("body")
.style("text-align", "center")
.style("background", "#3c3c3c");
var svg = d3
.select("body")
.append("svg")
.attr("height", height)
.attr("width", width)
.style("background", "red")
.style("display", "inline-block");
var g = svg.append("g")
// get centre of svg and rect width and height
var svgCentre = {x: width/ 2, y: height / 2};
var rectDims = {w: width / 5, h: height / 5};
var rect = g
.append("rect")
.attr("x", svgCentre.x - (rectDims.w / 2)) // offset left by half rect width
.attr("y", svgCentre.y - (rectDims.h / 2)) // offset up by half rect height
.attr("width", rectDims.w)
.attr("height", rectDims.h)
.style("fill", "white")
// rotate rect through 360 degrees with centre of rotation at svg centre
rect.transition()
.duration(2000)
.attrTween("transform", () => {
var i = d3.interpolate(0, 360); // full circle
// set centre of rotation at centre of svg
return t => `rotate(${i(t)}, ${svgCentre.x}, ${svgCentre.y})`;
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
I am trying to draw some circles on an a responsive svg.
Here is the code for the svg:
const width = window.innerWidth;
const circleWidth = width / 2;
let h = 700;
const svgBackground = d3.select("#container")
.append("svg")
.attr("viewBox", `0 0 ${width} ${h}`)
.classed("svg-content", true)
.on("mouseleave", function () {
d3.selectAll("circle.neo")
.style("stroke", ringColour);
d3.select("div#container")
.selectAll("p")
.remove();
})
It scales responsively but I can't figure out how to draw the circles so they are centered vertically
let height = svgBackground.style("height");
height = height.slice(0, (height.length - 2));
const halfHeight = height / 2;
let circles = svgBackground.selectAll("circle.neo")
.data(radius)
.enter()
.append("circle")
.attr("class", "neo")
let circleAttributes = circles
.attr("cx", circleWidth)
.attr("cy", halfHeight)
.attr("r", function (d) { return d })
.style("stroke", ringColour)
.style("fill", "none")
If anyone has any tips for how to do this, I would appreciate it. Here's the full code on js fiddle http://jsfiddle.net/ncbtdk8m/1/
I think your problem is that a part of the bottom of your SVG is not visible because .svg-container has a overflow:hidden property. This makes it appear like it was not in the center.
If you remove that CSS property and configure your height correctly things start to look different: http://jsfiddle.net/9dprfwj1/
Part of the problem is the styling of the div#container and the svg.svg-content.
No need to use the position and the display attributes.
You don't set a width and height to the svg and you don't have a resize handler, so how can you be responsive.
Why add a new center circle each time you use the sliders.
You only get 80 stars with radius 0.5.
I am using D3.js v4.
I have a minimum example working with zooming in and out on a single axis, with the following code:
// Create dummy data
var data = [];
for (var i = 0; i < 100; i++) {
data.push([Math.random(), Math.random()]);
}
// Set window parameters
var width = 330
var height = 200
// Append div, svg
d3.select('body').append('div')
.attr('id', 'div1')
d3.select('#div1')
.append("svg").attr("width", width).attr("height",height)
.attr('id','chart')
// Create scaling factors
var x = d3.scaleLinear()
.domain([0,1])
.range([0, (width - 30)])
var y = d3.scaleLinear()
.domain([0,1])
.range([0,height])
// Create group, then append circles
d3.select('#chart').append('g')
.attr('id','circlesplot')
d3.select('#circlesplot')
.selectAll('circles')
.data(data)
.enter().append('circle')
.attr('cx', function(d,i){ return x(d[0]); })
.attr('cy', function(d,i){ return y(d[1]); })
.attr('r', 4)
// Create y axis, append to chart
var yaxis = d3.axisRight(y)
.ticks(10)
var yaxis_g = d3.select('#chart').append('g')
.attr('id', 'yaxis_g')
.attr('transform','translate(' + (width - 30) +',0)')
.call(yaxis)
// Create zoom svg to the right
var svg = d3.select('#div1')
.append('svg')
.attr('width', 30)
.attr('height', height)
.attr('transform', 'translate('+ width + ',0)')
.call(d3.zoom()
.on('zoom', zoom))
function zoom() {
// Rescale axis during zoom
yaxis_g.transition()
.duration(50)
.call(yaxis.scale(d3.event.transform.rescaleY(y)))
// re-draw circles using new y-axis scale
var new_y = d3.event.transform.rescaleY(y);
d3.selectAll('circle').attr('cy', function(d) { return new_y(d[1])})
}
fiddle here: https://jsfiddle.net/v0aw9Ler/#&togetherjs=2wg7s8xfhC
Putting the mouse just to the right of the yaxis and scrolling gives the zooming function on the y axis.
What I'd like to happen is for the y axis maximum (in this case 1.0) to stay fixed, while zooming only in the other direction. You can kind of see what I mean by placing the mouse at the very bottom and just to the right of the y axis, and see the points cluster at the bottom of the graph.
I think it has to do with using zoom.extent(), but I'm just really not sure where to go from here; advice is greatly appreciated.
Source for this min working example:
http://bl.ocks.org/feyderm/03602b83146d69b1b6993e5f98123175
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/
I'm trying to get a multi-line date based chart to pan nicely across the X date axis and I simply cannot figure out what the problem is.
I have the zoom behaviour set up in the code but it's just not performing as expected. If you click on a point in a line and scroll it appears to be scrolling the axis, if it click on the labels in the axis it also scrolls but the actual visualisation of data doesn't scroll.
var zoom = d3.behavior.zoom()
.x(x)
.scaleExtent([1, 1])
.on("zoom", function () {
svg.select(".x.axis").call(xAxis);
svg.select(".lines").call(xAxis);
});
svg.call(zoom);
Also if you click directly on the back ground the mouse event doesn't seem to make it's way to the control at all.
I read a few examples on this and each seem to take a vastly different approach which I've tried but none have worked for my chart.
There are possibly a number of issues that exist as barriers to getting this working so I thought the best way to illustrate the problem was in a JsFiddle.
D3 Chart Panning Fiddle
What I'm trying to achieve is when there is a lot of data to visualise the chart can adapt to the data set and allow the data to extend beyond the bounds of the chart.
Currently clicking on the background does not allow panning because you have applied zoom behavior to the g element not to the svg.
var svg = d3.select('#host')
.data(plotData)
.append("svg")
.attr("id", "history-chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom);
Right now on zoom you have updated x and y axes but not the visualization. So you have update the lines and circles also like this.
var zoom = d3.behavior.zoom()
.x(x)
.scaleExtent([1, 1])
.on("zoom", function () {
svg.select(".x.axis").call(xAxis);
svg.select(".lines").call(xAxis);
svg.selectAll("path.lines")
.attr("d", function(d) { return line(d.values); });
svg.selectAll("circle")
.attr("cx", function(d) { return x(d.date); })
.attr("cy", function(d) { return y(d.value); });
});
Since you are panning the map you will have to use clip path for restricting visualization from moving outside the chart
var clip = svg.append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
Apply clip path to the g elment which contains lines and cicrles.
var attribute = svg.selectAll(".attribute")
.data(plotData)
.enter().append("svg:g")
.attr("clip-path", "url(#clip)")
.attr("class", "attribute");