Avoid dx/dy attributes in d3 axis labels - javascript

I experienced problems with the position of text elements when exporting SVG files and opening it in Corel Draw (some older version). I fixed it by setting every dx/dy attribute to zero and added its value to the corresponding x/y attribute.
I wrote a helper function which is called with .each on every text element I use.
transformDXYtoXY: function(d, i) {
var that = d3.select(this);
var y = that.attr("y") == null ? 0 : parseFloat(that.attr("y"));
var dy = that.attr("dy") == null ? 0 : parseFloat(that.attr("dy"));
that.attr("y", y + dy);
that.attr("dy", 0);
// doing the same with dx/x
...
},
This was working great until I decided to transition axis on input change instead of redraw them:
axis = d3.svg.axis().scale(someScale);
d3.select('.axis')
.transition()
.call(axis)
.selectAll("text")
.each(transformDXYtoXY);
Without the call to transformDXYtoXY() the tick label position is off
The y/dy attributes are not being set, even though when I check for it inside transformDXYtoXY() it seems allright.
Is there a way to tell d3 to avoid using dx/dy? It looks like the problem occurs during transition().

The use of the dx and dy attributes is hardcoded in the source of D3 -- changing it would be a significant effort. However, there's an easy workaround. D3 transitions allow you to set up a listener for the end of the transition. You can leverage this to run your code to fix the attribute values (with minimal changes to your existing code):
d3.select('.axis')
.transition()
.call(axis)
.selectAll("text")
.each("end", transformDXYtoXY);
To clarify, the code that you have at the moment runs the function to fix the attributes immediately after setting up the transition which then runs and overwrites the attribute values. The code above runs the function after the transition is complete, i.e. no further attribute changes will occur.

Related

Removing elements by class on scroll

Note: I asked this question about interrupting transitions during a scroll, but am trying a different technique now that is resulting in a similar issue that doesn't get resolved with the accepted (and working) answer.
This time, rather than initializing all the graphs with 0 opacity and having a separate function to change the opacity that gets called on each step, I'd like to use selection.remove() in each drawing function. I want to do this so that out-of-view graphs don't get in the way of any mouseover interactions that I might want on the current graph.
For example, I have functions that clear the existing graphs and then draw the current one with some transition:
var makeCircle0 = function() {
d3.selectAll(".nancygraphs").interrupt().remove()
g.append("circle")
.attr("cx", 50)
.attr("cy", 100)
.attr("r", 20)
.attr("fill", "red")
.attr("id", "red")
.attr("opacity", 0)
.transition()
.duration(1000)
.attr("opacity", 1)
.attr("class", "nancygraphs")
}
These functions are put in a list
var activateFunctions = [];
activateFunctions[0] = makeCircle0;
activateFunctions[1] = makeCircle1;
activateFunctions[2] = makeCircle2;
activateFunctions[3] = makeCircle3;
And depending on the step, the function gets called to draw the correct graph
function handleStepEnter(response) {
console.log(response)
step.classed('is-active', function(d, i) {
return i === response.index;
})
figure.select('p').text(response.index);
figure.call(activateFunctions[response.index]) // HERE!
}
Here is a jsfiddle to illustrate. Basically, if you scroll back-and-forth quickly then old graphs don't get cleared and you'll notice several graphics in view simultaneously. Why isn't d3.selectAll(".nancygraphs").interrupt().remove() doing the job?
Three observations regarding your approach:
First, according to the d3 manual on
transitions:
remove: remove the selected elements when the transition ends.
The remove will not interrupt already running transitions - it only removes when all transitions have stopped. More specifically it seems to act when __transition__.count of an element reaches 0. You could consider using a non-d3 remove implementation here, e. g. jQuery.
Second, from the same manual:
Interrupting a transition on an element has no effect on any transitions on any descendant elements. (...) you must therefore interrupt the descendants: selection.selectAll("*")
You should call interrupt on both by doing d3.selectAll(".nancygraphs").interrupt().selectAll("*").interrupt().
Third, it is never a good idea to directly couple mouse or scroll input to your logic (when you directly couple input events to e. g. attaching a transition, you might be doing so many thousands of times): did you use a debounce function? The lodash implementation is highly recommended.
After trying these modifications I would assume your current problem is solved. If it is not, a further way of debugging would be to log / overwrite the __transition__.count attribute of your elements.

How to define the node entering position in D3 js force layout

I am trying to implement a radial force layout in D3.js , I saw a similar example but i am stuck on how to initiate the node positions in the layout.
http://bl.ocks.org/vlandham/5087480
Thanks in Advance
Initialising a position is just done by setting the cx and cy positions. The most logical place is where the radius is currently being set i.e.
.attr("r", 10)
.attr("cx", 5) //added
.attr("cy", 5) //added
Of course, you can do something more exotic if you are using the bound data to initialise position.
This will only set the starting point though - the force layout will then take over and position elements. The advantage is that you can potentially reduce some of the initial node movement if you get it right.

Positioning SVG elements via .getBoundingClientRect() in variable-width div

I have a bit of an annoying problem.
I'm trying to position a bunch of SVG circle elements according to an existing bunch of SVG text elements that share similar properties.
The circle elements are created in a very separate process than the text elements, so positioning the new elements just using the same transforms etc. as the old one isn't a viable option.
I'm trying to use .getBoundingClientRect() to get the positions since the text elements are transformed into position (so .getBBox() isn't an option) rather than positioned by x and y attributes.
With .getBoundingClientRect(), I can get the correct size/arrangement of the new elements, but since the width of the svg-containing div is variable, there's always a bit of a weird offset that I can't quite account for.
I created a simplified example of my issue here. Resize and refresh the page to see the issue in action.
The code I use to position the circle elements is replicated below.
var circs = theSvg.selectAll("circle")
.data(theCircles)
.enter()
.append("circle")
.attr("r", 15)
.attr("fill", "#f00")
.style("opacity", 0.3)
.attr("transform", function(d){
var sizeDif = 800/(d3.select(".svgTestHolder")[0][0].getBoundingClientRect()["width"]);
var theNum = parseInt(d.split("&")[1]);
var thePosition = theSvg.selectAll("text").filter(function(e){
return e == theNum;})[0];
var theCoords = thePosition[0].getBoundingClientRect();
var leftOffset = d3.select(".svgTestHolder")[0][0].getBoundingClientRect()["left"];
var leftOffset2 = d3.select(".svgTest")[0][0].getBoundingClientRect()["left"];
var bottomOffset = d3.select(".svgTestHolder")[0][0].getBoundingClientRect()["top"];
var bottomOffset2 = d3.select(".svgTest")[0][0].getBoundingClientRect()["top"];
return
"translate(" + ((theCoords["left"] - leftOffset - leftOffset2)
* sizeDif) + "," + ((theCoords["top"] - bottomOffset - bottomOffset2)
* sizeDif) + ")";
})
EDIT:
This is a very delayed update just to note that while I was unable to answer my question as stated, I was able to make a workable solution based on Paul LeBeau's suggestion to extract the transforms from the target element.
In my case, I had to use a series of consecutive transforms rather than a combination of transforming and changing the x/y position (due to certain realities of the project not represented in the linked example). But I'm happy to have found an answer!
Your example works fine for me on Chrome. But really that's only because the SVG is the only thing on the page. If I add some text above the SVG everything goes wrong.
https://jsfiddle.net/rrpfmm6d/1/
Is this the problem you are talking about?
If so, the reason is because you are making the wrong choice in using getBoundingClientRect(). It provides coordinates in screen space. It's origin is the top left of the window (or iframe in the case of jsfiddle).
You should be using getBBox(). The values it returns are in the same coordinate space as the SVG elements. It's origin is (normally) at the top left of the SVG.
In summary, use the coordinates returned by calling getBBox() on your <text> element to calculate the position for your circle. If the circles are inserted into the same SVG as the text, there will be no need to do any adjusting with the div or svg offsets.

How to transition D3 axis without tick text attribute reset?

I have a D3 project where I'm drawing a time axis along the left side of the screen. I want to have it smoothly transition on window resize so I'm using D3 transitions. However the axis setup appears to be changing the "dy" attribute on the tick labels immediately causing the tick labels to jump downward and then transition back into their normal place any time the SVG is transitioned. Is there any way to set the "dy" attribute of the tick text as part of the axis call or a better way to transition?
My initial (one-time) axis setup:
var timeScale = d3.time.scale().domain([minTime, maxTime]);
var yAxis = d3.svg.axis().scale(timeScale).tickFormat(d3.time.format("%-m/%-d %-I:%M%p")).orient("right");
I have a function to update/transition the SVG elements I'm using. The first time the SVG is drawn init is set to true, false afterwards.
function updateSVG(init) {
...
timeScale.rangeRound([topPadding, svgHeight]);
// Use a transition to update the axis if this is an update
var t = (init) ? svgContainer : svgContainer.transition().duration(750);
// {1}: Create Y axis
t.select("g.axis").call(yAxis);
// {2}: Move Y axis labels to the left side
t.selectAll("g.tick > text")
.attr("x", 4)
.attr("dy", -4);
...
}
On an update at {1} tick labels all have a "dy" attribute of "-4" from the previous attr() call. At {2} applying the axis resets the "dy" attribute of these elements to a default of ".32em" after which they transition slowly back to "-4" causing them to jitter up and down as the window is resized and the axis is redrawn.
Here is a working JSFiddle that demonstrates the jump on the y-axis when the Result box is resized, resize just by a few pixels and it should be obvious: http://jsfiddle.net/YkDk4/1/
Just figured this out. By applying a "transform" attribute instead of a "dy" attribute the axis call() does not overwrite the value. So:
t.selectAll("g.tick > text")
.attr("x", 4)
.attr("dy", -4);
becomes:
t.selectAll("g.tick > text")
.attr("x", 4)
.attr("transform", "translate(0,-4)");
and everything transitions smoothly.
According to the bug fix made in response to this problem with the text-anchor attribute:
How to tweak d3 axis attributes when transition is present?
It looks like the dy attribute is supposed to update immediately during transitions...but it's not.
In any case, the easiest solution is simply to take the dy update OUT of the transition and apply it directly:
t.select(".y")
.call(yAxis);
chartSvg.selectAll(".y g.tick > text")
.attr("dy", -4);
That should avoid the "bounce".

Why are my stacked bars not being deleted when my graph changes in d3?

I'm working with this jsfiddle. I expect that when I click the WeekView button it should change the bar colors to reflect the same colors that are in my legend. But for some reason the colors are different.
I don't think
var layer = svg.selectAll(".layer")
.data(stack);
layer.enter()
.append("g")
.attr("class", "layer")
.style("fill", function (d, i) {
return color(i);
});
layer.exit()
.remove();
is being called when I switch to weekview, therefore its not replacing the old bars with the new ones its just reusing the bars from the previous views.
How can I get d3.js to replace the bars with the proper colors?
Indeed, the problem is in that part of the code: Demo
var layer = svg.selectAll(".layer")
.data(stack);
layer.enter()
.append("g")
.attr("class", "layer");
// Set the colors in the `update` cycle, not the `enter` cycle.
layer.style("fill", function (d, i) {
return color(i);
});
layer.exit()
.remove();
There is an interesting history of why this behaves this way. In earlier versions of D3, the enter and update set of elements were kept separate, just like update and exit events are still kept separate, i.e. operations you performed on the update set would not be performed on the exit set and vice-versa.
However, in version 2.0 of D3, it was decided that any element appended in the enter phase would also become a part of the update set. This was done because often the enter set of elements and the update set of elements needed to have the exact same operation performed on them (like in your case). To avoid this effect, you'll need to write the update phase before the enter phase.
Hence, in the enter cycle, elements should be appended and their initial attributes should should be set while their final values (which they should have in static state) should be set in the update cycle.

Categories

Resources