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.
Related
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 am creating a mapping application in d3 and want to tie some text to the top right corner of my view port. Additionally, I want the text to remain in the top right corner while I zoom and pan across the application.I think I can solve my problem by figuring out how to get the coordinates of the top right corner of my view. Knowing this information would allow me to then set the coordinates of my text element. I've tried manually setting the dimensions of the containing svg element and then moving the text to that location but interestingly this didn't work. I was hoping to be able to find the coordinates programatically rather than setting coordinates manually. How can I do this in d3/javascript?
EDIT:
My code is a modification of this code by Andy Barefoot: https://codepen.io/nb123456/pen/zLdqvM
My own zooming and panning code has essentially remained the same as the above example:
function zoomed() {
t = d3
.event
.transform
;
countriesGroup
.attr("transform","translate(" + [t.x, t.y] + ")scale(" + t.k + ")")
;
}
I'm trying to append the text at the very bottom of the code:
countriesGroup.append("text")
.attr("transform", "translate(" How do I get top right coordinates? ")")
.style("fill", "#ff0000")
.attr("font-size", "50px")
.text("This is a test");
My idea is to be able to get the top right coordinates of the view port through the code rather than setting it manually and then have the coordinates of the text update as the user zooms or pans.
To keep something in place while zooming and panning you could invert the zoom:
point == invertZoom(applyZoom(point))
This isn't particularly efficient, as we are using two operations to get to the original number. The zoom is applied here:
countriesGroup
.attr("transform","translate(" + [t.x, t.y] + ")scale(" + t.k + ")");
While the inversion would need to look something like:
text.attr("x", d3.zoom.transform.invert(point)[0])
.attr("y", d3.zoom.transform.invert(point)[1])
.attr("font-size", baseFontSize / d3.zoom.transform.k);
Where point and base font size are the original anchor point and font size. This means storing that data somewhere. In the example below I assign it as a datum to the text element:
var width = 500;
var height = 200;
var data = d3.range(100).map(function() {
return {x:Math.random()*width,y:Math.random()*height}
})
var zoom = d3.zoom()
.on("zoom",zoomed);
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height)
.call(zoom);
var g = svg.append("g")
var circles = g.selectAll()
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 5)
.attr("fill","steelblue")
var text = g.append("text")
.datum({x: width-10, y: 20, fontSize: 12})
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.style("text-anchor","end")
.attr("font-size",function(d) { return d.fontSize; })
.text("This is a test");
function zoomed() {
g.attr("transform", d3.event.transform);
var d = text.datum();
var p = d3.event.transform.invert([d.x,d.y]);
var x1 = p[0];
var y1 = p[1];
text.attr("x",x1)
.attr("y",y1)
.attr("font-size", d.fontSize / d3.event.transform.k)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Better Solution
The above is the solution to the approach you seem to be looking for. But the end result is best achieved by a different method. As I mention in my comment, the above approach goes through extra steps that can be avoided. There can also be some size/clarity changes in the text when zooming (quickly) using the above method
As noted above, you are applying the zoom here:
countriesGroup
.attr("transform","translate(" + [t.x, t.y] + ")scale(" + t.k + ")")
The zoom transform is applied only to countriesGroup, if your label happens to be in a different g (and not a child of countriesGroup), it won't be scaled or panned.
We wouldn't need to apply and invert the zoom, and we wouldn't need to update the position or font size of the text at all.
var width = 500;
var height = 200;
var data = d3.range(100).map(function() {
return {x:Math.random()*width,y:Math.random()*height}
})
var zoom = d3.zoom()
.on("zoom",zoomed);
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height)
.call(zoom);
var g = svg.append("g");
var g2 = svg.append("g"); // order does matter in layering
var circles = g.selectAll()
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 5)
.attr("fill","steelblue")
// position once and leave it alone:
var text = g2.append("text")
.attr("x", width - 10)
.attr("y", 20 )
.style("text-anchor","end")
.attr("font-size", 12)
.text("This is a test");
function zoomed() {
// apply the zoom to the g that has zoomable content:
g.attr("transform", d3.event.transform);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
I am trying to do a radial area plot with a colour gradient to reflect this. However the gradient does not appear to ever want to sit perfectly in line / at the correct side with the circle (see image below).
I have tried constraining the width and hight of the svg so that they are equal, but with no avail.Every refresh (with random data), the central ring for the gradient will shift and warp into a different shape, but never lie on the mean line where it should be.
I have ensured that the svg is square
var width = window.innerWidth , height = window.innerHeight;
width = height = d3.min([width,height])
var data = d3.range(0,100).map(d=>Math.random())
Given the gradient the following properties
var linearGradient = defs.append("radialGradient")
.attr("id", "rad-gradient")
.attr("cx", "50%") //The x-center of the gradient, same as a typical SVG circle
.attr("cy", "50%") //The y-center of the gradient
.style("r", "50%");
and my area mathematically as
var area = d3.area()
.x1(function(d,i) { return width/2+Math.cos(i\*angle)*(h(d)) }) .x0(function(d,i) { return > width/2+Math.cos(i\*angle)*(h(mean)) }) .y0(function(d,i) {> return height/2+Math.sin(i\*angle)*(h(mean)) }) .y1(function(d,i) { return height/2+Math.sin(i\*angle)*(h(d)) })
And appended the created gradient to the svg path
svg.append("path")
.data([data])
.attr("class", "area")
.attr("d", area)
.style("fill", "url(#rad-gradient)")
The solution lies in creating a circle and filling it with the correct gradient. This ensures that the radial gradient will always be centered around the origin.
You can then use clipPath to extract only the path you wish to use from the circle. After a little fiddling to align the gradient radius, we get the desired effect.
// previous definition for area path appended as a clip path
clip = defs.append("clipPath")
.attr('id','clip')
.append("path")
.data([data])
.attr("class", "area")
.attr("d", area)
//extracting this path from a circle with the desired gradient
svg.append("circle")
.attr("cx", width/2)
.attr("cy", height/2)
.attr("r", rmax)
.attr("clip-path","url(#clip)")
.style("fill", "url(#linear-gradient)")
(EDIT: viewBox and preserveAspectRatio are great, but aren't rendering properly in IE9, so I can't use that.)
I've been working on resizing this heatmap (here it is on a block). By selecting the width of a div, appending an SVG to it, and setting my SVG's width to that of the div, I've been able to make the heatmap responsive.
Nevertheless, I've run issues when I've tried to update the heatmap on resize. Here's the resize function:
d3.select(window).on("resize", resize);
function resize(){
marginHM = {top:35, right:0, bottom:50, left:30};
divWidthHM = parseInt(d3.select("#chartHM").style("width"));
widthHM = divWidthHM-marginHM.left-marginHM.right;
gridSize = Math.floor(widthHM/24);
legendElementWidth = gridSize*2.665;
heightHM = (9*gridSize)-marginHM.top-marginHM.bottom;
svgWidthHM = widthHM + marginHM.left+marginHM.right;
svgHeightHM = heightHM+marginHM.top+marginHM.bottom;
svgHM.select("svg").style("width", (svgWidthHM+20) + "px")
.style("height", (svgHeightHM+30) + "px");
dayLabels.attr("y", function (d, i){ return i*gridSize;})
.attr("transform", "translate(-6," + gridSize/1.5+")");
timeLabels.attr("x", function(d,i) {return i * gridSize;})
.attr("transform", "translate(" + gridSize/2+", -6)");
cards.attr("x", function(d) {return d.hour *gridSize; })
.attr("y", function(d) {return d.day * gridSize; })
.attr("width", gridSize)
.attr("height", gridSize);
d3.selectAll("rect.HMLegend")
.attr("x", function(d, i){ return legendElementWidth * i;})
.attr("y", 7.2*gridSize)
.attr("width", legendElementWidth)
.attr("height", gridSize/2);
d3.selectAll("text.HMLegendText")
.attr("x", function(d, i){ return legendElementWidth *i;})
.attr("y", 8*gridSize);
}
If you open the heatmap and drag the window to a wider size than it initially loaded in, you'll find that the heatmap grows, but the SVG's dimensions are cut off at the initial width; I find this really strange, considering I update the width in the resize function. You can decrease the width from its original value, and the map will size down, but you can't increase it above the original value of the width.
Additionally, I run into issues with the legend bar and legend labels. The the legend labels don't shift on window resize, and when you click back and forth between USA and New York, the legend colours often move off screen or show duplicates. Have been staring at this all evening, and am getting to the point where everything's blurring into one nonsensical line of code. Would really appreciate someone weighing in on exactly what it is I'm messing up!
I am trying get a few html elements to follow each other along a SVG path. I would like them to stay the same distance apart as they go around the path. I would also like the SVG image to scale to the container that holds it.
I have created a codepen that demonstrates what I have so far:
http://codepen.io/mikes000/pen/GIJab
The problem I am having is that when the elements move along the X axis they seem to get further apart than they do on the Y axis.
Is there a way to make them stay the same distance as they travel along the line?
Thanks!
Update**
After some further fiddling I have discovered that the distance variation seems to be caused by the aspect ratio of the SVG viewbox being increased for X greater than it is for Y. When it is stretched along the X axis 1px down the line may become 3px on the screen.
The position of the red squares is being set by moving them in front and behind by half the width of the black box. When traveling along the line if the viewbox aspect ratio is changed the distance between each point on the line increase or decreases based off of this.
I have tried creating a similar SVG with the exact viewbox of the size of the container div and the red dots are exactly on the ends of the black box all the way down the line. This doesn't solve problem because I would like the SVG with the line to scale to any size container it is placed inside.
I think if there is a way to calculate how many pixels the size of the black box is in relation to how many pixels down the line it covers the red dots would line up exactly.
Any ideas how to accomplish this or any ideas on a better way to approach this problem?
Take a look at http://jsfiddle.net/4LzK4/
var svg = d3.select("#line").append("svg:svg").attr("width", "100%").attr("height", "100%");
var data = d3.range(50).map(function(){return Math.random()*10})
var x = d3.scale.linear().domain([0, 10]).range([0, 700]);
var y = d3.scale.linear().domain([0, 10]).range([10, 290]);
var line = d3.svg.line()
.interpolate("cardinal")
.x(function(d,i) {return x(i);})
.y(function(d) {return y(d);})
var path = svg.append("svg:path").attr("d", line(data));
var circle =
svg.append("circle")
.attr("cx", 100)
.attr("cy", 350)
.attr("r", 3)
.attr("fill", "red");
var circleBehind =
svg.append("circle")
.attr("cx", 50)
.attr("cy", 300)
.attr("r", 3)
.attr("fill", "blue");
var circleAhead =
svg.append("circle")
.attr("cx", 125)
.attr("cy", 375)
.attr("r", 3)
.attr("fill", "green");
var pathEl = path.node();
var pathLength = pathEl.getTotalLength();
var BBox = pathEl.getBBox();
var scale = pathLength/BBox.width;
var offsetLeft = document.getElementById("line").offsetLeft;
var randomizeButton = d3.select("button");
svg.on("mousemove", function() {
var x = d3.event.pageX - offsetLeft;
var beginning = x, end = pathLength, target;
while (true) {
target = Math.floor((beginning + end) / 2);
pos = pathEl.getPointAtLength(target);
if ((target === end || target === beginning) && pos.x !== x) {
break;
}
if (pos.x > x) end = target;
else if (pos.x < x) beginning = target;
else break; //position found
}
circle
.attr("opacity", 1)
.attr("cx", x)
.attr("cy", pos.y);
posBehind = pathEl.getPointAtLength(target-10);
circleBehind
.attr("opacity", 1)
.attr("cx", posBehind.x)
.attr("cy", posBehind.y);
posAhead = pathEl.getPointAtLength(target+10);
circleAhead
.attr("opacity", 1)
.attr("cx", posAhead.x)
.attr("cy", posAhead.y);
});
randomizeButton.on("click", function(){
data = d3.range(50).map(function(){return Math.random()*10});
circle.attr("opacity", 0)
path
.transition()
.duration(300)
.attr("d", line(data));
});
Instead of calculating the positions of the circles behind and ahead on your own, use getPointAtLength relative to the centre of object that has to stay in the middle.
Inspired by: http://bl.ocks.org/duopixel/3824661