I'm trying to move an element in D3, in order to correspond to a circle underneath. Basically, when the user zooms on the page, the circles shrink (which allows them to remain visually appealing and separated).
I want to build a function that fires with the zoom event, that keeps the images centered within the circles. The circles are centered on their center points. However, as the images shrink, they appear to move to the left because their anchors are in the top-left corner.
I need a solution that might involve adding their sacrificed width and height to their relative "x" and "y" attributes. How would I implement a function like this? Or is there a better way?
The blockbuilder is here: http://blockbuilder.org/KingOfCramers/125cc79bce7dea48b21786b37302d258
Here is the relevant bit of code (the icon variable is the starting width of the image):
function zoom() {
var iconMove = icon/d3.event.transform.k;
g.attr("transform", d3.event.transform)
d3.selectAll(".storyImages")
.attr("width", `${iconMove}px`)
.attr("height", `${iconMove}px`)
.attr("x", // Keep this centered)
.attr("y", // Keep this centered)
d3.selectAll("circle")
.attr("r", function(){
return cirSize/d3.event.transform.k
})
}
Thanks for any help you can provide!
If you can position them to start, you can update them the same way on zoom, just with the new width/height of each item. You initially append each item with these attributes:
.attr("x", (d) => projection([d.lat,d.lon])[0] - icon/2)
.attr("y", (d) => projection([d.lon,d.lat])[1] - icon/2)
.attr("width", `${icon}px`)
.attr("height", `${icon}px`)
Which offsets the icon from the x,y values returned by the projection by half the icon's width and height - centering it on the projected point. Note: Your x value is set with d.lat, d.lon rather than d.lon, d.lat, also, your csv has lng, rather than lon as a header, so d.lng should be used).
To keep the icon centered on the point, just update the icon using the new icon width/height (which in your case is located in iconMove) and the new projected point:
.attr("x", (d) => projection([d.lng,d.lat])[0] - iconMove/2)
.attr("y", (d) => projection([d.lng,d.lat])[1] - iconMove/2)
.attr("width", iconMove)
.attr("height", iconMove);
Here's an updated block (I wasn't able to figure out how to save a new block builder block).
Related
I'm having an issue with my graph. When I zoom, the line goes over the edge of the canvas area and over the x/y axis. I tried adding a clippath but that doesn't seem to work. If I inspect the DOM in the debugger I can see the clippath rectangle is position exactly where it needs to be.
//The canvasGroup is the area between the axis
var clipGroup = canvasGroup.append("clipPath")
.attr("id", "canvasGroup-clipPath");
var clipRect = clipGroup.append("rect")
.attr("width", width)
.attr("height", height);
//Function that does the zooming
function doZoom()
{
paths.forEach(function(path)
{
path.attr("transform", d3.event.transform);
});
gX.call(xAxis.scale(d3.event.transform.rescaleX(xScale)));
gY.call(yAxis.scale(d3.event.transform.rescaleY(yScale)));
}
var zoom = d3.zoom()
.scaleExtent([1, 5])
.translateExtent([[0, 0], [width, height]])
.on("zoom", doZoom);
//Register the event listener
canvasGroup.call(zoom);
//now loop over the data sets and plot each one
//For brevity, I'm skipping the loop and only showing the call to create the line
var path = canvasGroup.append("path")
.attr("clip-path", "url(#canvasGroup-clipPath)")
.attr("fill", "steelblue")
.attr("class", "line")
.attr("id", lineId + "-line")
.style("stroke", lineColor)
.style("stroke-width", 1)
.style("fill", "none");
paths.push(path);
path.attr("d", function(d) { return plotline(i)});
Here is how it looks.
Before zoom:
After zoom:
The problem is caused by setting the clipping path on the path representing your data. When applying the clipping path the browser needs to decide which coordinate system should be used for the contents of <clipPath> element. This is controlled by the clipPathUnits attribute which defaults to userSpaceOnUse:
userSpaceOnUse
The contents of the <clippath> element represent values in the current user coordinate system in place at the time when the <clippath> element is referenced (i.e., the user coordinate system for the element referencing the <clippath> element via the clip-path attribute).
When transforming the path in your doZoom() function, you are actually establishing a new coordinate system for the path. And, apart from drawing the path itself, this coordinate system will be used to compute the position and dimension of the clipping path. Thus, by transforming the path according to the zoom behavior, you are applying the same transformation to the clipping path whereby moving it away from the desired position.
Although it is tempting to set the clipPathUnits attribute to its other valid value objectBoundingBox, this is most likely not what you want for this case as it further complicates matters. When setting this value the positions and lengths of the <clipPath>'s contents need to be specified as fractions of the bounding box!
Knowing all this, there is a much easier solution to it! You just need to apply the clip-path to an element which will not be transformed during zooming ,e.g. a parent group. Given the incomplete code you provided, this might very well work by setting the clipping path to canvasGroup:
// Set the clipping path on the parent group.
canvasGroup.attr("clip-path", "url(#canvasGroup-clipPath)")
// Append the path to the group as before.
var path = canvasGroup.append("path")
.attr("fill", "steelblue")
.attr("class", "line")
// ...
I'm trying to change the size of dots on a map in D3. Basically, just want to re-size the circle SVG in D3. I just want all the circles to be smaller, not trying to create proportional symbols. Here's my circles:
var centroid = map.selectAll(".centroid")
.data(centroid.features)
.enter()
.append("path")
.attr("d",path)
.attr("r", 100)
.attr("class", function(d){
return "centroid "+d.properties.info_city;
})
It's not working. I know that I can't do this in CSS, any thoughts on how to accomplish this in javascript as I'm trying to do? Thanks all!
Take a look at this plunker and let's move on from there: http://plnkr.co/edit/lKtCBKFS764MThajrqxX?p=preview
This map has similar centroids like yours. These are defined in:
g.selectAll(".centroid").data(centroids)
.enter().append("circle")
.attr("class", "centroid")
.attr("fill", fill)
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.attr("r", radius)
.attr("cx", function (d){ return d[0]; })
.attr("cy", function (d){ return d[1]; });
By changing .attr("r", radius) line (which is 6 in the config) to .attr("r", 2) you will get smaller circles
Here's the changed one: http://plnkr.co/edit/JUpMlnhZvZNacAIzI502?p=preview
I think you are trying to change the wrong part of the code since you should change the "r" of the circle elements not the "path" element ( I don't think path element even has a "r" attribute).
If your circles are drawn by a path then you must change the algorithm that draws those circles.
You are using the centroid coordinates to append a path as if it was a circle. This is not usual, the most common choice would be using a SVG circle, but it's OK... actually, Bostock does the same here.
The only problem is, as these are paths, not circles, changing the radius (r) will have no effect. So, to change the size of these "circles", this is what you have to do: find your d3.geo.path and add pointRadius to it.
var path = d3.geo.path()
.projection(projection)
.pointRadius(someValue);
Where someValue is, of course, some numeric value that fits your needs.
PS: for this to work properly, the types in your TopoJSON have to be "Point" or "MultiPoint".
I'm trying to determine that when I hover over a certain legend item, how do I know which one I hover over.
// draw legend colored rectangles
legend.append("rect")
.attr("x", width + 170)
.attr("width", 18)
.attr("height", 18)
.on("mouseover", function(d) {
})
.style("fill", color);
Right now, there are 3 rects in the legend. How do I get the id of the rect that I hover over?
Inside the mouseover handler, this is the DOM element that triggered the event. So you can do something like
.on("mouseover", function(d) {
d3.select(this).attr('id');// presumes that <rect> has an id!
})
To assign id to rect, you call .attr('id', 'some_id') on it.
But if you're at a stage where you don't already have ids on the rects (despite what you titled your post), then consider using d3's data binding and the enter, update, (exit) selections in order to create your legend, and use d in the "mouseover" function to determine which legend element is being interacted with (instead of using ids on the DOM element).
I have a map of the US, with cities plotted. I'm trying to use d3-tip to provide tooltips to my cities on hover. The problem I'm running into is that d3-tip isn't accounting for the projection, and I'm not sure how to apply it.
The projection I'm using:
var projection = d3.geo.mercator()
.scale(150)
.translate([width / 2, height / 1.5]);
I apply it to my circle elements by the following:
.attr("cx", function(d) {
return projection([d.Longitude, d.Latitude])[0];
})
.attr("cy", function(d) {
return projection([d.Longitude, d.Latitude])[1];
})
Problem is d3-tip doesn't seem to take x and y attributes, it grabs them from the node issuing the event and doesn't seem to account for it having the projection applied; which doesn't make sense to me.
I usually do tooltips with d3 by appending a div with absolute positioning and hiding it in the body.
Then on mousing over an element, I move the div to the location of the mouse pointer and unhide it. I then hide the div once the mouseout event occurs.
This allows me to control X and Y as well as the styling and text of the tooltip.
Here's a great example: https://gist.github.com/biovisualize/1016860
Here is jsfiddle of a Sankey diagram:
I am trying to modify colors of the links so that the color of each link is actually gradient from its source node color to its target node color. (it is assumed that opacity will remain 0.2 or 0.5 depending whether a mouse hovers or not over the link; so links will remain a little "paler" than nodes)
I took a look at this nice and instructive example, which draws this gradient filled loop:
However, I simply couldn't integrate that solution to mine, it looks too complex for the given task.
Also, note that links in original Sankey diagram move while node is being dragged, and must display gradient even in those transitory states. A slight problem is also transparency of links and nodes, and order of drawing. I would appreciate ideas, hints.
#VividD: Just saw your comment, but I was about done anyway. Feel free to ignore this until you've figured it out on the own, but I wanted to make sure I knew how to do it, too. Plus, it's a really common question, so good to have for reference.
How to get a gradient positioned along a line
With the caveat for anyone reading this later, that it will only work because the paths are almost straight lines, so a linear gradient will look half-decent -- setting a path stroke to a gradient does not make the gradient curve with the path!
In initialization, create a <defs> (definitions) element in the SVG and save the selection to a variable:
var defs = svg.append("defs");
Define a function that will create a unique id for your gradient from a link data object. It's also a good idea to give a name to the function for determining node colour:
function getGradID(d){return "linkGrad-" + d.source.name + d.target.name;}
function nodeColor(d) { return d.color = color(d.name.replace(/ .*/, ""));}
Create a selection of <linearGradient> objects within <defs> and join it to your link data, then set the stop offsets and line coordinates according to the source and target data objects.
For your example, it probably will look fine if you just make all the gradients horizontal. Since that's conveniently the default I thought all we would have to do is tell the gradient to fit to the size of the path it is painting:
var grads = defs.selectAll("linearGradient")
.data(graph.links, getLinkID);
grads.enter().append("linearGradient")
.attr("id", getGradID)
.attr("gradientUnits", "objectBoundingBox"); //stretch to fit
grads.html("") //erase any existing <stop> elements on update
.append("stop")
.attr("offset", "0%")
.attr("stop-color", function(d){
return nodeColor( (d.source.x <= d.target.x)? d.source: d.target)
});
grads.append("stop")
.attr("offset", "100%")
.attr("stop-color", function(d){
return nodeColor( (d.source.x > d.target.x)? d.source: d.target)
});
Unfortunately, when the path is a completely straight line, its bounding box doesn't exist (no matter how wide the stroke width), and the net result is the gradient doesn't get painted.
So I had to switch to the more general pattern, in which the gradient is positioned and angled along the line between source and target:
grads.enter().append("linearGradient")
.attr("id", getGradID)
.attr("gradientUnits", "userSpaceOnUse");
grads.attr("x1", function(d){return d.source.x;})
.attr("y1", function(d){return d.source.y;})
.attr("x2", function(d){return d.target.x;})
.attr("y2", function(d){return d.target.y;});
/* and the stops set as before */
Of course, now that the gradient is defined based on the coordinate system instead of based on the length of the path, you have to update those coordinates whenever a node moves, so I had to wrap those positioning statements in a function that I could call in the dragmove() function.
Finally, when creating your link paths, set their fill to be a CSS url() function referencing the corresponding unique gradient id derived from the data (using the pre-defined utility function):
link.style("stroke", function(d){
return "url(#" + getGradID(d) + ")";
})
And Voila!