Maximum distance limit for D3 Force Layout? - d3.js

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.

Related

D3 force-directed: why transform-translate tick is required when displaying labels

I am looking at samples of D3 force-directed graphs with node labels, and I realised that for the tick function, the function to governing the movement of the node and labels uses a transform-translate method:
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
instead of the usual without labels.
nodes.attr({"cx":function(d){return d.x;},
"cy":function(d){return d.y;}
});
Would anyone be able to help explain the rationle? Thanks.
When node is a single shape element, we just adjust its positional attributes in the tick function. For eg. if it is a circle, we update its cx and cy attributes.
But when we have multiple elements to be displayed in the same or relative position, we group the elements using <g> element and apply the positional attribute to the <g> element. The positional attribute of g element is transform. We can create different type of elements (here - circle and text) and adjust their positions independently in the tick function. However, it is better to group elements in such cases.

Graphing live re-syncing data in D3js in a fixed window with linear easing

I'm interested in graphing live-ish data in D3js. Now, when I say "live-ish" I mean that I'll be collecting data every 200ms +/- 10ms, but there may be several minute long periods of inactivity. Fortunately, the input data is time-stamped!
What I have so far: I've followed some line drawing in d3 guides (eg: this) and I have a Y axis with the value range/domain I want. I have an X axis with the range I want and a moving domain as per a standard time-series fixed-width graph. That is, if my graph's x axis domain is (0:15, 0:35) in 5 seconds it will be (0:20, 0:40). This transitions nicely as it's using linear easing.
I have mock-data being output each iteration of the graph tick. My domain is set up as such that new points are just out of the x-axis domain such as to allow the smooth effect as per 1. All in all, it looks great.
So where do I go from here? My desired result: data comes in asynchronously and is placed precisely at its x-axis time-stamped location. If data is up to date, it gets placed juuust outside the x-axis domain and has a smooth transition in. If data doesn't arrive in time, the graph continues without drawing any new points until data is received, at which time it adds each point at its appropriate time-stamp retroactively. If data for the missing period doesn't arrive at all, we just continue with a gap in the graph. I can emulate this by calling...
d3.select(window).on("click", .. )
Effectively, I can click to add random data at the current time-stamp using some anonymous function which allows me to mimic the data / event structure my code should handle.
I think my current confusion is due to how I add data and draw the path from it.
var line = d3.svg.line()
.interpolate("basis-open")
.x(function(d, i) { return x(now - (n-1-i)*duration); })
.y(function(d, i) { return y(d); });
var axis = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(x.axis = d3.svg.axis().scale(x).orient("bottom"));
svg.append("g")
.attr("class", "y axis")
.call(d3.svg.axis().scale(y).orient("left"));
var path = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("path")
.datum(data)
.attr("class", "line")
The big question: my y-values correspond to the path.datum(data) (data is just an array of values) appropriately, but when I push to the data array to draw the line, it always places each point graphically at equal distance apart. How do I break out of the mono-variable graph without destroying my time-series smooth scrolling animation? I could record a second array of timestamps alongside my data array, but how to I integrate those into the line? Ideally, I'd have them both be a part of the same array so I could sort by timestamp so when I call data.shift() truly the oldest data is gone. I tried changing the "duration" of the transition function but it made the graph accelerate weirdly and didn't actually break the equidistance of points on x.
How do I set up the y-axis graph to also take into account x-location without breaking my graph?
Alright, so I figured out a pretty straightforward way of doing exactly what I wanted while still using .datum(data) instead of .data(data) (where 'data' is my array). Instead of passing in an array of values to .datum, I pass in an array of objects. Or, what was once data = [value1, value2, ...] is now data = [{gx:timestamp1, gy:value1}, {gx:timestamp2, gy:value2}, ...].
My line x-axis / y-axis functions are now...
.x(function(d, i) { return x(d.gx); })
.y(function(d, i) { return y(d.gy); });
Which ends up being a bit neater than my initial run at it. My transition functions didn't have to change.
The final puzzle piece was what to do about data that comes in time-stamped but out of order. Fortunately, my display domain isn't very large and thus, I don't need to store many values in the array. As such, when new data comes in I simply sort it to make the line not a wobbly-bobbly mess.
data.sort(function(a,b){ return a.gx - b.gx});
And voila! If the size of the array is beyond the boundary, shift off data. This gives us the effect of a sliding timeseries window of the past n seconds where time-stamped data points may be arbitrarily dumped on and displayed properly.

Transition partition arcs back to original angles

How can I transition arc angles, initially defined by d3.svg.arc(), back to their original values when using d3.layout.partition?
I am trying to store the initial startAngle and endAngle values d.x and d.dx somewhere so that I can transition back to them at a later state. However, I do not know:
At what point d.x and d.dx are initialized, whether in d3.svg.arc() or when the arc paths are actually appended.
How to bind d.x and d.dx to the elements when I am using a partition to render them.
Normally I might bind the initial startAngle and endAngle to the elements with datum(). I believe I am looking for something similar to:
selectAll('path').data(function(d) {
return partition.values({
'initialStart': d.x,
'initialEnd': d.dx
}).nodes(d)
});
In principle this is the same as transitioning pie charts, where you need a custom tween function to get the animation right. For this it is necessary to save the original value in a separate attribute -- in your case you can do the same thing.
Attributes are set when the layout is run; this is completely independent of the rendering and appending elements to the DOM.

Painless method to zoom&pan so that all elements are within drawing area - d3js

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.

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