why does d3 rescale all scales during zoom? - d3.js

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.

Related

How does Mike Bostock's (D3js's creator) pan & zoom example work?

I'm having a hard time understanding this example:
https://bl.ocks.org/mbostock/4e3925cdc804db257a86fdef3a032a45
I understand the general concept, all the points in the graph are put into a svg g-element, which is useful because now you can apply a transform on the group and have all the points scale accordingly. This transforms the whole coordinate system that these points are in. But this does not matter for the mouse-coordinate that is passed along for panning because those coordinates come from an invisible rect in the svg that does not get transformed.
What I don't understand is this:
svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "none")
.style("pointer-events", "all")
.call(d3.zoom()
.scaleExtent([1 / 2, 4])
.on("zoom", zoomed));
function zoomed() {
g.attr("transform", d3.event.transform);
}
Is the .call a member function of the default svg-rectangle? What does it do here? And d3.zoom is according to the documentation a 'behaviour'. What does that mean and how does the coupling between these two work here? And what is the purpose of the function .on, which gets passed 'zoom'? I assume it's for passing the browser event of zooming on an element to the event handler zoomed. But then how does panning get handled here? And is it just me or would it have had more sense to have .scaleExtent and .on as parameters of d3.zoom()?
The rect receives the browser zoom events.
The zoomed function applies the transform to the g element
Note we have the rect after the g otherwise the rect would behind the g not receiving events.
svg
- g
- rect

d3 issues with entering elements after pan

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.

Adding a drag icon to Focus+Context via Brushing Chart

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.

How do you get selected datums in brush "brush" event handler?

I am attempting to create a vertical timeline using d3.js that is linked to a map so that any item(s) contained in the brush will also be displayed in the map. Kind of like http://code.google.com/p/timemap/ but with d3 instead of SIMILE and a vertical timeline rather than horizontal.
I can successfully create an svg with vertical bars representing time ranges, legend, ticks, and a brush. The function handling brush events is getting called and I can obtain the extent which contains the y-axis start and stop of the brush. So far so good...
How does one obtain the datums covered by the brush? I could iterate over my initial data set looking for items within the extent range but that feels hacky. Is there a d3 specific way of getting the datums highlighted by a brush?
var data = [
{
start: 1375840800,
stop: 1375844400,
lat: 0.0,
lon: 0.0
}
];
var min = 1375833600; //Aug 7th 00:00:00
var max = 1375919999; //Aug 7th 23:59:59
var yScale = d3.time.scale.utc().domain([min, max]).range([0, height])
var brush = d3.svg.brush().y(yScale).on("brush", brushmove);
var timeline = d3.select("#myDivId").append("svg").attr("width", width).attr("height", height);
timeline.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("x", function(datum, index) {return index * barSize})
.attr("y", function(datum, index) {return yScale(datum.start)})
.attr("height", function(datum, index) {return yScale(datum.end) - yScale(datum.start)})
.attr("width", function() {return barSize})
timeline.append("g")
.attr("class", "brush")
.call(brush)
.selectAll("rect")
.attr("width", width);
function brushmove() {
var extent = brush.extent();
//How do I get the datums contained inside the extent????
}
You'll need to do some kind of iteration to figure out what points live inside the brush extent. D3 doesn't automatically do this for you, probably because it can't know what shapes you're using to represent your data points. How detailed you get about what is considered "selected" and what isn't is quite application specific.
There are a few ways you can go about this:
As you suggest, you can iterate your data. The downside to this is that you would need to derive the shape information from the data again the same way you did when you created the <rect> elements.
Do a timeline.selectAll("rect") to grab all elements you potentially care about and use selection.filter to pare it down based on the x, y, height and width attributes.
If performance is a concern because you have an very large number of nodes, you can use the Quadtree helper to partition the surface and reduce the number of points that need to be looked at to find the selected ones.
Or try Crossfilter, there you pass the extent from the brush to a dimension filter and then you fetch filtered and sorted data by dimension.top(Infinity).
(A bit late answer, buy maybe useful for others, too.)

How to scale and translate together?

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.

Resources