I have a rect holding a clip path, which I'm applying to a group (holding a tree). I have a zoom function bound to the rect which transforms the group, which works fine. I've applied the clip path to the group, and when it first renders it looks like it should. However, after panning or zooming, the drawn tree extends beyond the bounds of the clip path while maintaining its previously-clipped appearance.
var svg = d3.select(this.$.chart);
var svg2 = svg.select("svg");
var main = svg2.append("g")
.attr("class","main")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var treeContainer = svg2.append('g')
.attr("transform", "translate(" + margin2.left + "," + margin2.top + ")")
var treeBaseRect = treeContainer.append("rect") // the rectangle which holds the clip path and zoom actions for the tree.
.attr("width", width + margin2.right)
.attr("height", height2)
.style("fill", "#eee")
.style("pointer-events", "all")
.call(d3.zoom().scaleExtent([0.1, 3]).on("zoom", function () {
svgGroup.attr("transform", d3.event.transform)
}));
treeContainer.append('defs').append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width + margin2.right)
.attr("height", height2);
var svgGroup = treeContainer.append("g")
.attr("clip-path","url(#clip)");
Here's some screenshots. First one shows the initial render, which is fine (the clip area is the darker grey rectangle):
Then after doing a scroll zoom or pan, note how the tree is still 'originally' clipped, and not being clipped properly outside of the gray area:
And this is my clip path rect in the DOM structure:
You can tell that the clip rect is still where it's meant to be, but the tree is completely ignoring it. No idea why.
Apparently I hadn't added enough groups. The easy solution was to add another append("g") to the svgGroup I was creating, for a last line looking like this:
var svgGroup = treeContainer.append("g")
.attr("clip-path","url(#clip)")
.append("g");
Related
I've built a D3 force graph largely based on these really helpful examples.
I wanted to add pan and zoom functionality, which I tried to do using another example (looks like I can only include two links, but Google "d3 force zoom eyaler" to find it).
Unfortunately, when I zoom out on a graph that is larger than the initial SVG, I get something like this:
Result of dragging and dropping
Here's the relevant code:
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.behavior.zoom().scaleExtent([0.5,2]).on("zoom", redraw));
function redraw() {
svg.attr("transform",
"translate(" + d3.event.translate + ")"
+ " scale(" + d3.event.scale + ")");
}
How can I change the pan and zoom behaviour so that it scrolls and makes it possible to see the rest of the graph, rather than just allowing me to move the square that was originally visible?
OK, looks like I worked it out... you need to perform the transform on a <g> rather than on the SVG itself. So:
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.behavior.zoom().scaleExtent([0.5,2]).on("zoom", redraw));
var g = svg.append("g"); // add <g>
function redraw() {
g.attr("transform", // perform transform on g, not svg
"translate(" + d3.event.translate + ")"
+ " scale(" + d3.event.scale + ")");
}
Just putting this here in case anyone else made the same mistake!
I am trying to get the legend of my chart outside of the charting area.
Here are the margins:
var margin = {top: 50, right: 200, bottom: 50, left: 40};
var width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
First I create the svg:
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
So, from my understanding, I now have the svg canvas element and a g inside of it which holds the chart. I am trying to add more to the right margin, so I can get some space between the svg canvas and the g appended to it, which holds the chart. Then I want to put my legend in that empty space.
Here is where I add my legend:
//add legend
var legend = svg.append("g")
.attr("class", "legend")
.attr("height", 300)
.attr("width", 200)
.attr("transform", "translate(-1000,50");
Even though I am appending to the SVG element, it is appending to the g within the svg element. So, no matter how much I translate it or try to get it to go more right on the screen, it never goes past the width of the inner g.
When troubleshooting, I see that the outer SVG element has a height of 960 and width of 500. The g appended to that has a transform/translate of 40,50. The width ends up being 839px by 433.223px (not sure I understand this). The outer svg has a bunch of space to the right now because of the margin built in.
So I'm trying to either increase the width of the g appended to the svg so I can put my legend as a child of the g and move it to the empty space created by the margin. Or, I'm trying to create another g that is a sibling to the first g and then I can use the empty space created by the margin.
I can't get either to work and don't know which way is best.
Notice that the var svg is being assigned to a <g> nested inside the <svg>
svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g") // <-- This is what svg is currently being assigned to
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
And so, when you later execute var legend = svg.append("g"), you're actually appending the legend as a child of the aforementioned <g>. And that's what you described seeing in the dev tools.
One implication is that the translate() transform you applied to the outer <g> affects the inner <g> (i.e. the translation of the innter <g> of legend is added to that of the outer <g>).
Likely, you want split things up like so:
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var inner = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
Then change your code to draw the existing chart into inner rather than svg.
As a result, var legend = svg.append("g") will append legend as a sibling of inner, and any translation you apply to legend would be relative to the svg's top left (as opposed to inner's top left, which is translated by margin)
And likely you want to translate legend like so:
var legend = svg.append("g")
.attr("transform", "translate(" + width - margin.right + "," + margin.top + ")");
That moves the legend to the right end of the chart, MINUS margin.right. That way, you can tweak margin.right to create enough room for legend.
Finally, note that calling
legend
.attr("height", 300)
.attr("width", 200)
doesn't do anything, because for svg <g>, there isn't a way to explicitly set the width and height. Those wouldn't mean much anyway, because svg doesn't have a the "flow" behavior of html layouts. The width and height shown in dev tools are the implicit bounds resulting from the bounds of the children of the <g>. (If needed, there's a way to get those computed bounds in javascript, using the getBBox() function).
by looking at the code you provided, you are actually attaching the legend var to your group "g", instead of "svg", var legend = svg.append("g")
in this line you are telling d3 to get your legend variable to "g" which is append to svg, if i understand correctly you should try something like this:
var legend = svg.selectAll(".legend")
.enter().append("g")
to create another group "g" for your legends.
i apologize for my bad english.
I have a bubble force chart application, and I've created various instances of it. However I can not seem to scale the bubbles in relation to the height/width of the svg.
http://jsfiddle.net/pPMqQ/138/
I've placed an inverse on the heights/widths to alter the viewport - but it doesn't feel like the right approach to me. Does anyone have any experience as to how to correct this?
var inverseHeight = 1/h * 100000;
var inverseWidth = 1/w * 100000;
var svg = d3.select(selector)
.append("svg")
.attr("class", "bubblechart")
.attr("width", parseInt(w + padding,10))
.attr("height", parseInt(h + padding,10))
.attr('viewBox', "0 0 "+parseInt(inverseWidth,10)+" "+parseInt(inverseHeight,10))
.attr('perserveAspectRatio', "xMinYMid")
.append("g")
.attr("transform", "translate(" + (w/4) + "," + (h/4) + ")");
I've tried to scale the actual bubbles
http://jsfiddle.net/pPMqQ/143/
var scale = methods.width*.005;
Its producing an ideal effect - but the chart is not always central.
I am implementing a geometric zoom behaviour as seen in this example
The problem is that if the cursor is on a white spot outside the green overlay rect or any other SVG element (line, circle etc.) the mousewheel event gets intercepted by the browser and scrolls down the page.
I would like to be able to freely zoom independently of where I am on the visualisation.
Here is a simplified jsFiddle recreating the problem.
var width = 300,
height = 300;
var randomX = d3.random.normal(width / 2, 80),
randomY = d3.random.normal(height / 2, 80);
var data = d3.range(2000).map(function() {
return [
randomX(),
randomY()
];
});
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.call(d3.behavior.zoom().scaleExtent([-8, 8]).on("zoom", zoom))
.append("g");
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height);
svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", 2.5)
.attr("transform", function(d) { return "translate(" + d + ")"; });
function zoom() {
svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
Hope this isn't too late, I missed this question the first time around.
The reason it isn't working under Chrome is because Chrome hasn't yet implemented the standard CSS transform on html elements -- and as strange as it is to understand, the outermost <svg> tag on an SVG element embedded in a webpage is treated as an HTML element for layout purposes.
You have two options:
Use Chrome's custom transform syntax, -webkit-transform in addition to the regular transform syntax:
http://jsfiddle.net/aW9xC/5/
Kind of jumpy, since you are transforming the entire SVG and readjusting the page layout accordingly. For reasons I don't understand neither the CSS/webkit transform nor the SVG attribute transform work when applied to the "innerSVG" element.
Replace the nested SVG structure with an SVG <g> group element, which Chrome has no problem transforming:
http://jsfiddle.net/aW9xC/4/
Stick a transparent rect in front of everything so the mouse event has something to latch on to. In SVG events are only sent to rendered elements such as rects and not to the general unrendered background.
svg.append("rect")
.attr("fill", "none")
.attr("pointer-events", "all")
.attr("width", "100%")
.attr("height", "100%");
In order to make this work properly the SVG would have to cover the whole area so to get the same look as your original fiddle you'd want to clip to the original area which can be done either by setting a clipPath or (as I've done in the fiddle) by creating an innser <svg> element which will clip.
var svg = d3.select("body").append("svg")
.attr("width", "100%")
.attr("height", "100%")
.call(d3.behavior.zoom().scaleExtent([-8, 8]).on("zoom", zoom));
svg = svg.append("svg")
.attr("width", width)
.attr("height", height)
So altogether it looks like this...
Basically, I want my graph to start at the x-axis and grow over two seconds to the actual data values. This is probably a simple thing, but I can't seem to get it to work.
I'm appending an area element, in which the d="" attribute is built by a function (area) and I'm not sure where to add a transition.
First I thought to do this in the area function, but this fails. I've also tried to do this when the area element is added without success.
Here is my code:
// Create the area element for each object - a function that will be passed to "path"
var area = d3.svg.area()
.x(function(d) { return x(d.year); })
.y0(height)
//.y1(height)
//.transition()
//.duration(2000)
.y1(function(d) { return y(d.average); });
// build the container SVG element
var svg = d3.select("#co2").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
svg.append("path")
// pull in data to the path
.datum(data)
.attr("class", "area")
// passing in the area function, for each object
.attr("d", area)
// starts the graph opacity at 0 and fades to 1 over 2.5 seconds
.style("fill-opacity", "0")
.transition()
.duration(2500)
.style("fill-opacity", "1");
Rather than try to use transition on the shape of the area graph, you could apply a scale(x,y) transform against the whole svg element that you want to animate. The advantage of this approach is that it is not limited to a particular rendering implementation (eg: not path/d3.area specific).
There are a couple of gotchas to note though:
To avoid the transition() behaviour, working on the margin adjustments, make sure you have a separate 'g' element for the transition() transforms to act on
SVG has its origin (0,0) in the top-left, so in addition to scaling the SVG area, you need to set the base of the graph
This is put together below:
'g' element:
var svg = d3.select("#co2").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left, "," + margin.top + ")")
// define a new 'g' to scope the transition, and keep separate from the margin adjusting transform above
.append("g");
transition() including base adjustment:
svg
.attr("transform", "translate(0," + height + ") scale(1, 0)")
.transition().duration(2000)
.attr("transform", "translate(0,0) scale(1, 1)");
As ever, this is best illustrated with the complete example: http://bl.ocks.org/4239516