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")
// ...
Related
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).
I'm having an issue specific to scale behavior with a default mercator projection scale value, in my case a pretty ridiculous value (75000), since somehow it was the only way I was able to get my TOPOJson map to be project as anything other than a dot.
I created a projection of a map and want to add zoom and pan. My functions work, all except for the default zoom value. As soon as you scroll to zoom, the value jumps back to the default projection scale value, rather than scaling based on the mouse wheel. I have a feeling that the reason is because in order to get the map to display I needed to make my projection scale value really high (75000).
So when the page loads, the zoom looks fine, like this... but when you try to pan or zoom in or out one mouse wheel click, it jumps to this
I tried to limit the code to just the projection and zoom logic bellow.
<body>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<div id = "map" style = "float : left; min-width:320px;width :50%;height : 600px;"></div>
<script>
var width = 900,
height = 800;
var projection = d3.geo.mercator()
.center([-73.94, 40.70])
.scale(75000)
.translate([(width/2), (height/2)]);
var svg = d3.select("#map").append("svg")
.attr("viewBox", "0 0 900 800")
.attr("preserveAspectRatio", "xMidYMid meet");
var path = d3.geo.path()
.projection(projection);
var g = svg.append("g")
.attr("transform", "translate(-793.0489643729818,-630.1796382734433)scale(3.01487286910262)");
d3.json("ny.json", function(error, ny) {
g.append("path")
.attr("id", "boroughs")
.datum(topojson.feature(ny, ny.objects.boroughs))
.attr("d", path);
// zoom and pan
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
g.selectAll("path")
.attr("d", path.projection(projection));
});
svg.call(zoom)
});
I believe your issue is that you set initial scaling and translate values for your g and then reset them on zoom. For scale, for example:
You set a scale for the g to 3.01 to start:
var g = svg.append("g")
.attr("transform", "translate(-793.0489643729818,-630.1796382734433)scale(3.01487286910262)");
However, the first time you zoom with d3.event, it'll return 0.607 or 1.647, (zoom out, zoom in) both are zoomed out relative to 3.01:
g.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
This creates the jump you are seeing, as both scale and translate are reset and thus independent of your initial values. I think the translate value would be ok but you call it on the svg and not the g on which you specify the initial translate.
In any event, it is probably easier to not set an initial transform on your g, instead using the projection to do this (instead of a two step center and zoom with both a projection and a transform). That way you can leave all your code unchanged, save for removing the line setting the initial g transform and a modification to your projection.
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".
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!
I'm trying to modify the focus+ context example to study the blackbody spectrum with my students.
http://bl.ocks.org/1667367
The problem is that for me the only way to color segment is to plot bar colored charts.
I'd like to do it with the area generator, like this I can have all the benefits of the interpolation when I zoom on.
I can't access to each area element to fill it in the rgb color I want.
Is there any way to acces to the individual area elements and fill it with a function?
Here's my files with an orange filled area(focus) that I'd like to color as I do with the bar chart(context).
http://bl.ocks.org/4345931
In advance thanks.
I think the right approach here is to use a linearGradient element (specs). You can see my implementation here: http://jsfiddle.net/nrabinowitz/JZACC/
The main point here is that instead of drawing each color as a separate element, you define a gradient of spectrum colors and apply the same gradient to both the area and the context bar:
// set up gradient elements
var gradient = defs.append('linearGradient')
.attr('id', 'spectrumGradient');
// set up gradient scale
var gx = d3.scale.linear().range([0, 1]);
Then, when the data is loaded, you create stop elements for each color:
// update gradient scale
gx.domain(x.domain());
// create gradient
gradient.selectAll('stop')
// remove duplicates
.data(data)
.enter().append('stop')
.attr('offset', function(d) { return gx(d.wl); })
.attr('stop-color', toRGB);
And apply it using url(#id) notation:
focus.append("path")
.datum(data)
.attr("clip-path", "url(#clip)")
.attr("d", area)
.attr('fill', 'url(#spectrumGradient)');