I want to scale and translate D3 force graph, both at the same time. E.g. On clicking a button it shoud scale to 400% and then make itself center on the screen. This should all happen with a smooth animation effect.
//animate vis to visible area
vis.transition()
.duration(2000)
.attr("transform", "scale(" + someScaleValue + ")" + "center("0,0)");
Doing this, scaling works fine, but graph is not centered. It shifts towards right-bottom corner.
vis.transition()
.duration(2000)
.attr("transform", "scale(" + someScaleValue + ")");
Why is scale is getting reset to 100% when I translate it second time.
I also tried using:
vis.transition()
.duration(2000)
.attr("transform", "scale(" + scaleValue + ")" + "translate(0,0)");`
This is not working too. Please help me.
center(0,0) is not a valid transform-definition to be used with transform, as per the spec.
If you want translate(0, 0) to take the object to the center of the screen (usually the top-left corner of vis), then you might want to set viewBox of the outer svg element to be: "-width/2 -height/2 width height". This would set the co-ordinate system inside the svg element such that the center lies at (0, 0). Alternatively, you can use translate(width/2, height/2).
Also, each time you call .attr('transform', ...), you overwrite the old value of the transform attribute. This is the possible reason why you are losing the original scaling on translating. The best solution would be to put the vis element inside a g which has the scaling in the transform attribute which remains constant.
Related
I use a transform when panning, copied from several examples.
zoom = d3.behavior.zoom()
.x(this.xScale)
.scaleExtent([0.5, 2])
.on("zoom", zoomFunction(this))
.on("zoomend", zoomEndFunction(this));
svg = histogramContainer.append("svg")
.attr('class', 'chart')
.attr('width', width)
.attr('height', height)
.call(zoom)
.append('g')
.attr('transform', 'translate(' + this.margin.left + ' , ' +
(height - this.margin.bottom) + ')');
function zoomFunction(scope) {
return function() {
var that = scope;
that.xDelta = d3.event.translate[0];
that.zoomScale = d3.event.scale;
// some other code removed for simplicity
svg.selectAll(".stackedBar").attr("transform", "translate(" +
that.xDelta + ",0)scale(" +
that.zoomScale + ", 1)");
};
}
The problem is that since new elements enter after the pan then 'old' elements have the transform attribute applied but the new elements don't.
This breaks future panning because the old elements will be transformed from where the pre-zoom xScale drew them while the new elements will be transformed from the zoom-adjusted xScale.
It seems to me that I could redraw the old elements with the zoom-adjusted xScale, though I'm unsure when and how to do that "behind the scenes".
Alternatively I could draw the new elements with the old xScale and apply the same transform on them that the old elements have. This seems messier since elements will come and go and I'll have to keep track of the 'current transform'. My gut tells me "too much state".
Usually if you're attaching a scale to the zoom behaviour, you use the modified scale to redraw the bars using the exact same code as how you position the bars initially, letting the scales do all the work.
I linked to this discussion in my previous answer, so you might have read it by now; if not, it might be a good start for getting your head around the different ways of approaching zooming in d3; it breaks down the each method step-by-step. You're currently using a mix of two approaches (transforms versus scales), and I think that's causing problems keeping track.
The problem is that my yScale changes upon panning.
Here's the definition of the scale:
this.yScale = d3.scale.linear()
.domain([0, this.maxY * this.yHeader])
.rangeRound([0, height * this.yHeightScalor]);
I need to keep hold of the scale (i.e. use this.yScale instead of just var yScale) because of my redraw function. The trick is, panning = zooming where d3.event.scale === 1 and zooming rescales domains as you can see if you put a breakpoint in D3's zoom.js rescale function.
I can get around it by making sure my yScale is defined correctly when used but it seems to me that somethings amiss.
UPDATE: I've included the code and removed the line causing the issues. Truth be told, I only needed the xScale to zoom for user feedback. I redraw everything after the zoom anyway (in zoomEndFunction).
zoom = d3.behavior.zoom()
.x(this.xScale)
.y(this.yScale) // <--------------------- I removed this
.scaleExtent([0.5, 2])
.on("zoom", zoomFunction(this))
.on("zoomend", zoomEndFunction(this));
svg = histogramContainer.append("svg")
.attr('class', 'chart')
.attr('width', width)
.attr('height', height)
.call(zoom)
.append('g')
.attr('transform', 'translate(' + this.margin.left + ' , ' +
(height - this.margin.bottom) + ')');
// everything else is in the svg
The d3 zoom behaviour primarily acts as a wrapper for handling multiple events (mousewheel, drag, and various touch gestures), and converting them into translate and scale values, which are passed to your zoom event handlers as properties of the d3.event object.
However, you can also register a quantitative (non-ordinal) scale on the zoom behaviour using zoom.x(xScale) and zoom.y(yScale). The zoom behaviour will adjust each scale's domain to reflect the translation and scale prior to triggering the zoom event, so that all you have to do is redraw your visualization with those scales.
You do not have to register your scales on the zoom behaviour, or you can register one scale but not the other. For example,
if you are using transformations to zoom the visualization (a "geometric zoom", in d3 parlance), you don't need to change your scales at all;
if you are using a polar coordinates system, instead of x/y coordinates, you'll need to calculate the adjustments directly;
if you have a graph with a meaningful baseline, but very dense data, you may want to zoom/pan the horizontal axis but not the vertical.
From the comments, it sounds like the last situation reflects your case.
I have a neat script to draw for me using d3, but sometimes, when I have lots of data some of my nodes go off the div. I could code something to handle this at the co-ordinates level, I guess, but I can amend this easily using zoom and pan manually and was wondering whether there's a good, simple way to have it done automatically.
I can consider any other solution too.
To zoom/pan automatically, you would need to get the extent of your node positions and calculate the scale and offset accordingly. To get the min/max coordinates, you can simply iterate over your nodes. Once you have these, scale and offset can be calculated as follows.
scale = Math.min(width / (maxX - minX), height / (maxY - minY));
where width and height denote the dimensions of the container (i.e. the SVG). Assuming that you're zooming/panning by setting the SVGs transform attribute, this is what you would need to do.
svg.attr("transform",
"translate(" + minX*scale + "," + (-minY)*scale + ") scale(" + scale + ")");
What this does is compute the scale such that the larger of the x/y dimensions fits into the respective dimension of the container and repositions the container such that the top left corner of the extent of node positions corresponds to the top left corner of the container.
I'm using a force layout with a small negative charge to avoid having SVG elements on top of each other. However, I need the items to remain within ~20px of their original location. Is there any means of limiting to the total net X/Y distance the items move?
Each SVG element represents a bus stop, so it's important that items do not overlap but also do not move too far from their original location.
There's no option for this in the force layout itself, but you can do that quite easily yourself in the function that handles the tick event:
force.on("tick", function() {
nodes.attr("transform", function(d) {
return "translate(" + Math.min(0, d.x) + "," + Math.min(0, d.y) + ")";
});
});
This would bound the positions to be to the top right of (0,0). You can obviously modify that to bound in any other way as well (potentially with a nested Math.min/Math.max). You could even do that dynamically by storing allowed position ranges with the element and referencing that.
See here for an example that uses this technique to restrict the position of labels floated using the force layout.
I am trying to adapt Mike Bostock's Focus+Context via Brushing chart at: bl.ocks.org/mbostock/1667367 to include a drag icon on both vertical lines of the brush rectangle. These should appear once a selection is made and act as a visual cue to shrink or expand the selected/brushed area. I see the placement of the images being dynamic i.e. moving fluidly with the brushed area as opposed to an update after the brushed area is reset. What seems most reasonable to me would be to add an svg image to the context rectangle like so:
//original code
context.append("g")
.attr("class", "x brush")
.call(brush)
.selectAll("rect")
.attr("y", -6)
.attr("height", height2 + 7)
//additional code
.append("svg:image")
.attr("xlink:href", "icon.png")
.attr("width", 10)
.attr("height", 10)
.style("opacity",1)
I've tried playing around with the x and y positioning of both images with no luck getting them to appear, but i conceptually see it working as
y axis: height of context chart divided by 2
x axis: each image respectively corresponding to the min and max x values of the brushed area
Any help would be appreciated.
Lars, thanks for the pointer which generally led me in the right direction although I ended up directly adapting from an example at https://engineering.emcien.com/2013/05/7-d3-advanced-brush-styling. That example is a bit more complex so i borrowed from a subset relevant to my purposes.
Steps i followed include
1.) Creating two images appended to the context g element and initializing their position somewhere that doesn't give the impression that the chart is brushed on loading {i put them halfway (vertically) and close together around the end of the context(horizontally)}.
var leftHandle = context.append("image")
.attr("xlink:href", "icon.gif")
.attr("width", 11)
.attr("height", 27)
.attr("x",x2(data[data.length-6].date))
.attr("y", (height2/2)-15);
var rightHandle = context.append("image")
.attr("xlink:href", "icon.gif")
.attr("width", 11)
.attr("height", 27)
.attr("x",x2(data[data.length-1].date))
.attr("y", (height2/2)-15);
2.) Within the brush function, i created a variable to hold brush.extent(), then tied the x attributes of the images to its min and max x values.
var ext = brush.extent();
leftHandle.attr("x",x2(ext[0]));
rightHandle.attr("x",x2(ext[1]));
One things i'm not completely satisfied with is that when i initially created the images after the brush rectangle, they sat on top of it, preventing me from being able to brush if i hovered over the images (which is the intuitive reaction desired). I ended up placing the images behind the rectangle which has a low opacity. Not the 100% accurate visual representation sought, but it works just fine.