Live updating line chart - Wont smoothly scroll off screen (wiggles) - d3.js

I have a line chart that I've built for displaying live data. I've read through https://bost.ocks.org/mike/path/ and I can get it to smoothly come on screen, but for the life of me, I cant get it to not 'wiggle' when I remove a datapoint and the corresponding line section from the screen.
Here is the section of code where I am updating the line and x-axis.
lineselection.datum(data)
.transition()
.duration(1000)
.ease(d3.easeLinear)
.attr('d', line)
.attr('transform', 'translate(' + (xScale.range()[0]/data.length) + ',0)');
xScale = d3.scaleTime()
.domain([time, start_time])
.range([width - (margin * 2), 0])
.clamp(true);
g.selectAll('.x.axis')
.transition()
.duration(1000)
ease(d3.easeLinear)
.call(xAxis);
This produces a nice smooth line chart, where the new section of line is drawn outside of the clip-path, and then translated left into view. The x-axis scrolls along wonderfully with it.
In my data update method, I do a check to see if we are in my data view window (ie, we only want to view the last, say 60 seconds of data). If a point falls outside the time frame, we remove it, and update the start_time to reflect.
if ((time_frame + start_time) < time){
for (d = 0; d < data.length; d++){
if ((data[d].x + time_frame) < time){
data.shift();
}
else{
start_time = data[d].x;
break;
}
}
}
I am newer to both JS and D3, so please be gentle. Any help would be appreciated. Am I missing something obvious?
Here is a jsfiddle. https://jsfiddle.net/3kn56mb7/2/
Edit. Added a jsfiddle.

Here is the updated jsfiddle. https://jsfiddle.net/3kn56mb7/12/
Thank you elias for helping me get to this!
Here is my draw function. I used selection.interrupt() to overcome a jitter.
function draw(){
var line = d3.line()
.x(function(d) { return xScale(d.x); })
.y(function(d) { return yScale(d.y); });
var lineselection = svg.selectAll('.line_')
.select('path');
lineselection.interrupt()
.transition()
.duration(duration)
.ease(d3.easeLinear)
.attr('transform', 'translate(' + -(xScale.range()[0]/((duration / 100)-2)) + ',' + margin + ')');
if (data[0].x < time - time_frame - duration ){
console.log('shift');
data.shift();
}
lineselection.attr('d',line)
.attr('transform', 'translate(0,' + margin + ')');
start_time = time - (duration * 2) - time_frame;
xScale.domain([time, time + (duration * 2) - time_frame])
.range([width - (margin *2),0]);
d3.select('body')
.select('svg')
.select('g')
.selectAll('.x.axis')
.transition()
.duration(duration)
.ease(d3.easeLinear)
.call(xAxis);
}
EDIT: I updated the code to reflect the selection.interrupt() change as well as included the entire draw function. I also updated the jsfiddle link.
EDIT 2: Fixed a margin error and updated the jsfiddle link.
EDIT 3: I came back to use this as a reference. I am looking at this bit of code a year later, and couldnt figure out why I did some weird math. I must have been breathing diesel fumes. In anycase, I redid the math so that it made sense (why was I dividing duration by 100 and subtracting 2)?
Changed this: https://jsfiddle.net/3kn56mb7/11/
.attr('transform', 'translate(' + -(xScale.range()[0]/((duration / 100)-2)) + ',' + margin + ')');
To this: https://jsfiddle.net/3kn56mb7/12/
.attr('transform', 'translate(' + -(xScale(data[data.length-1].x) -xScale.range()[0]) + ',' + margin + ')');
I am now taking the last data point every draw cycle, and getting its time (x value) and converting that to a position in the range. Then I subtract the max range out (width-margin*2). Make the difference negative so it moves left.

The basic problem is that your data elements' indices are changing by removing the obsolete elements. I've faced the very same issue in a project of mine, and solved it in the following way:
I recommend that doing what you now in the transition in two separate steps:
when removing the element, draw a new path with the new indices - do this immediately, without a transition, so the wiggling does not appear
after this has been done, a transition can happen, which is then only a translation, during which no elements get deleted

Related

D3 Transition Path Left And Shift Up/Down Simultaneously

Following the tutorial of Mike Bostock on Path transitions here, I am trying to create an interpolated line chart that not only shifts through time but also transitions the y-scale / y-axis, such that is always fits to the lower and upper bounds of the data.
Some background information: The line is clipped by a clipPath and is shifted to the left whenever a new data point is added. Each new data point is added by the tick function, which also transitions the path to slide to the left.
Now the problem is, when I update the y-axis domain, it jumps to the new position. However, I would like it to smoothly transition up or down, similar to how it shifts along the x-axis. The solution probably lies in transforming the scaling of the path based on the new maximum of the data. Is there any way to achieve this or would it require a different approach by building a custom d3.interpolator() for interpolating the path?
function tick() {
// Push a new data point onto the back.
data.push(random());
// Redraw the line.
d3.select(this)
.attr("d", line)
.attr("transform", null);
// Slide it to the left.
d3.active(this)
.attr("transform", "translate(" + x(0) + ",0)")
.transition()
.on("start", tick);
// Pop the old data point off the front.
data.shift();
let max = d3.max(data, (d) => {
return d;
});
y = d3.scaleLinear()
.domain([-1, max])
.range([height, 0]);
d3.select('g .axis.axis--y').transition().duration(500).call(d3.axisLeft(y))
d3.select('g .axis.axis--x').transition().duration(500)
.attr("transform", "translate(0," + y(0) + ")")
}
I have created a jsfiddle which demonstrates the problem here.
Found a solution by using the external d3-interpolate-path library from here. Instead of transitioning the path using a transform, I interpolate the path with the old data and the path with the new data using d3.interpolatePath. Where previous is the old path and current is the new path with the newly added data point.
d3.select(this)
.attr("d", lineOld)
.attr("transform", null)
.transition().duration(500).ease(d3.easeLinear).attrTween('d', (d) => {
let previous = d3.select(this).attr('d');
let current = line(d);
return d3.interpolatePath(previous, current)
}).on("end", tick);
The Jsfiddle with my solution can be found here

Add text in rect svg and append it to arc in donut chart

I wanted to add labels to each arc in donut chart. I've added by taking the centroid of each arc and adding, but somehow it is not adding in correct position. I can't figure it out so I need some help regarding it. I've added my code in codepen. The link is here.
My donut should look like this.
Sample code is:
svg.selectAll(".dataText")
.data(data_ready)
.enter()
.each(function (d) {
var centroid = arc.centroid(d);
d3.select(this)
.append('rect')
.attr("class", "dataBG_" + d.data.value.label)
.attr('x', (centroid[0]) - 28)
.attr('y', (centroid[1]) - 5)
.attr('rx', '10px')
.attr('ry', '10px')
.attr("width", 50)
.attr("height", 20)
.style('fill', d.data.value.color)
.style("opacity", 1.0);
d3.select(this)
.append('text')
.attr("class", "dataText_" + d.data.value.label)
.style('fill', 'white')
.style("font-size", "11px")
.attr("dx", (centroid[0]) - 7)
.attr("dy", centroid[1] + 7)
.text(Math.round((d.data.value.value)) + "%");
});
Thanks in advance.
The difference between the "bad" state on codepen and the desired state is that in the one you don't like, you take the centroid and then you center your text on it. The centroid of a thick arc is the midpoint of the arc that runs from the midpoint of one line-segment cap to the other. This is roughly "center of mass" of the shape if it had some finite thickness and were a physical object. I don't think it's what you want. What you want is the midpoint of the outer arc. There's no function to generate it, but it's easy enough to calculate. Also, I think you want to justify your text differently for arcs whose text-anchor point is on the left hand of the chart from those on the right half. I'm going copy your code and modify it, with comments explaining.
// for some reason I couldn't get Math.Pi to work in d3.js, so
// I'm just going to calculate it once here in the one-shot setup
var piValue = Math.acos(-1);
// also, I'm noting the inner radius here and calculating the
// the outer radius (this is similar to what you do in codepen.)
var innerRadius = 40
var thickness = 30
var outerRadius = innerRadius + thickness
svg.selectAll(".dataText")
.data(data_ready)
.enter()
.each(function (d) {
// I'm renaming "centroid" to "anchor - just a
// point that relates to where you want to put
// the label, regardless of what it means geometrically.
// no more call to arc.centroid
// var centroid = arc.centroid(d);
// calculate the angle halfway between startAngle and
// endAngle. We can just average them because the convention
// seems to be that angles always increase, even if you
// if you pass the 2*pi/0 angle, and that endAngle
// is always greater than startAngle. I subtract piValue
// before dividing by 2 because in "real" trigonometry, the
// convention is that a ray that points in the 0 valued
// angles are measured against the positive x-axis, which
// is angle 0. In D3.pie conventions, the 0-angle points upward
// along the y-axis. Subtracting pi/2 to all angles before
// doing any trigonometry fixes that, because x and y
// are handled normally.
var bisectAngle = (d.startAngle + d.endAngle - piValue) / 2.0
var anchor = [ outerRadius * Math.cos(bisectAngle), outerRadius * Math.sin(bisectAngle) ];
d3.select(this)
.append('rect')
.attr("class", "dataBG_" + d.data.value.label)
// now if you stopped and didn't change anything more, you'd
// have something kind of close to what you want, but to get
// it closer, you want the labels to "swing out" from the
// from the circle - to the left on the left half of the
// the chart and to the right on the right half. So, I'm
// replacing your code with fixed offsets to code that is
// sensitive to which side we're on. You probably also want
// to replace the constants with something related to the
// the dynamic size of the label background, but I leave
// that as an "exercise for the reader".
// .attr('x', anchor[0] - 28)
// .attr('y', anchor[1] - 5)
.attr('x', anchor[0] < 0 ? anchor[0] - 48 : anchor[0] - 2)
.attr('y', anchor[1] - 10
.attr('rx', '10px')
.attr('ry', '10px')
.attr("width", 50)
.attr("height", 20)
.style('fill', d.data.value.color)
.style("opacity", 1.0);
d3.select(this)
.append('text')
.attr("class", "dataText_" + d.data.value.label)
.style('fill', 'white')
.style("font-size", "11px")
// changing the text centering code to match the box
// box-centering code above. Again, rather than constants,
// you're probably going to want something a that
// that adjusts to the size of the background box
// .attr("dx", anchor[0] - 7)
// .attr("dy", anchor[1] + 7)
.attr("dx", anchor[0] < 0 ? anchor[0] - 28 : anchor[0] + 14)
.attr("dy", anchor[1] + 4)
.text(Math.round((d.data.value.value)) + "%");
});
I tested. this code on your codepen example. I apologize if I affected your example for everyone - I'm not familiar with codepen and I don't know the collaboration rules. This is all just meant by way of suggestion, it can be made a lot more efficient with a few tweaks, but I wanted to keep it parallel to make it clear what I was changing and why. Hope this gives you some good ideas.

How to make d3 milestone Shape

I'm trying to align a down triangle with a rectangle to make a group that can be used to represent a milestone. Any ideas why this code only shows 2 triangles and how to move them to centre bottom of rectangle or any other methods to achieve the same goal?
http://jsfiddle.net/sjp700/Pej4M/
tri.enter().append("path")
.attr("d", d3.svg.symbol().type("triangle-down"))
.style("fill", "black")
.attr("transform", function (d) { return "translate(" + xRange(d.start) + "," + yRange(d.Duration) + ")"; });
As pointed out in the comments, the reason you're seeing only two rectangles is that some of the data is bound to existing paths. To fix, assign a special class to the symbols that you can select by:
var tri = vis.selectAll("path.tri").data(datar);
For the positioning of the symbols, you need to use the same values you use for the rectangles. The y position needs to be offset by a constant so that the symbols appear at the bottom and the x position by half the duration -- I'm guessing that this is what you really want to show as you're currrently hardcoding everything to length 50.
.attr("transform", function (d) { return "translate(" + (xRange(d.start) + 25) + "," + (yRange(d.start) + 15) + ")"; });
Complete demo here.

Smooth transitioning between tree, cluster, radial tree, and radial cluster layouts

For a project, I need to interactively change hierarchical data layout of a visualization - without any change of the underlying data whatsoever. The layouts capable of switching between themselves should be tree, cluster, radial tree, and radial cluster. And transitioning should be preferably an animation.
I thought that would be relatively easy task with D3. I started, but I got lost in translations and rotations, data bindings, and similar, so I am asking you for help. Also, probably I am doing something not in the spirit of D3, which is bad since I am seeking a clean solution.
I put together a jsfidle, but it is just a starting point, with added radio buttons, convenient small data set, and initial cluster layout - just to help anybody who wants to take a look at this. Thanks in advance!
UPDATE:
I wanted to focus on links only, so I temporary disabled other elements. Building on #AmeliaBR method, following animations are obtained:
Here is updated jsfiddle.
UPDATE 2:
Now with circles: (excuse my choice of colors)
{doom-duba-doom}
Here is one more updated jsfiddle.
I don't see why it would be that hard so long as all your layouts have the same overall structure of link-paths, circle nodes and text labels.
Just make sure all your objects, including your link paths, have a good data-key that is independent of the data attributes created by the layout functions. Then for each transition, update the data with the results of the appropriate layout function and draw that layout.
I've got the transition to radial tree implemented here: http://jsfiddle.net/YV2XX/5/
Key code:
//Radial Tree layout//
var diameter = 500;
var radialTree = d3.layout.tree()
.size([360, diameter / 2 ])
.separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
var radialDiagonal = d3.svg.diagonal.radial()
.projection(function(d) { return [d.y, d.x / 180 * Math.PI]; });
function transitionToRadialTree() {
var nodes = radialTree.nodes(root), //recalculate layout
links = radialTree.links(nodes);
svg.transition().duration(1500)
.attr("transform", "translate(" + (diameter/2)
+ "," + (diameter/2) + ")");
//set appropriate translation (origin in middle of svg)
link.data(links, function(d){
return d.source.name + d.target.name;})
.transition().duration(1500)
.attr("d", radialDiagonal); //get the new radial path
node.data(nodes, function(d){
return d.name ;})
.transition().duration(1500)
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; })
node.select("circle")
.transition().duration(1500)
.attr("r", 4.5);
node.select("text")
.transition().duration(1500)
.attr("dy", ".31em")
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
.attr("transform", function(d) { return d.x < 180 ? "translate(8)" : "rotate(180)translate(-8)"; });
};
The layout code is all from http://bl.ocks.org/mbostock/4063550, I've just changed it to be an update instead of an initialization.
Also note that I have moved the variable declaration for root outside of the data-reading method, so it can be re-accessed by the transition functions.
Layout still needs some finessing, but you get the idea.
Now, if you wanted one of the transitions to be a partition, treemap or other layout that doesn't use the node-link structure, they it gets more complicated...
I don't have enough reputation to make a comment...so, I am just giving this tiny contribution as a pseudo-answer. After looking at this post, and based on #VividD's perfect comment on how simple the transitions turned out to be, I simply added the Tree Vertical option to the transformations in this fiddle.
The addition is simply this:
var diagonalVertical = d3.svg.diagonal()
.projection(function (d) {
return [d.x, d.y];
});
Anyways, I have bookmarked this highly instructional interaction.

Using the zoom and pan functionality of d3

I'm trying to use the pan/zoom ability of d3 to draw boxes on the screen so that when you click on a box a new box appears and shifts the rest of the boxes to the right so that the new box is on the center of the canvas. The panning would allow me to scroll through all the boxes I've drawn.
Here is my jsfiddle: http://jsfiddle.net/uUTBE/1/
And here is my code for initializing the zoom/pan:
svg.call(d3.behavior.zoom().on("zoom", redraw));
function redraw() {
d3.select(".canvas").attr("transform",
"translate(" + d3.event.translate + ")"
+ " scale(" + d3.event.scale + ")");
}
And here is my code for drawing the boxes:
function drawBox(x, y) {
var boxGroup = canvas.append("g");
boxGroup.append("rect")
.attr("x", x)
.attr("y", y)
.attr("height", 100)
.attr("width", 100)
.attr("fill", function () {
var i = Math.floor(Math.random() * 4);
if (i === 1) return "red";
else if (i === 2) return "blue";
else if (i === 3) return "yellow";
else return "green";
})
.on("click", function () {
counter++;
d3.select(".canvas")
.transition()
.duration(1000)
.attr('transform', "translate(300,0)");
drawBox(x - counter * 120, y);
});
}
I have multiple problems with this fiddle, but two of my main concerns is:
1) How do I make it so that when I click on a new box a second time the boxes move accordingly (i.e. when I click on the box initially the old box shifts to the right and a new box appears, but when I click on the new box, the older boxes doesnn't shift to the right).
2)Why is it that when I click on the new box, the newer box has a big spacing between it? (only happens after trying to put 3 boxes on the screen).
Thanks any hints are appreciated!
I think there's some confusion here around transform. The transform attribute is static, not cumulative, for a single element - so setting .attr('transform', "translate(300,0)") more than once will have no effect after the first time. It also looks like your placement logic for the new boxes is off.
The positioning logic required here is pretty straightforward if you take a step back (assuming I understand what you're trying to do):
Every time a new box is added, the frame all boxes are in moves right 120px, so it needs a x-translation of 120 * counter.
New boxes need to be offset from the new frame position, so they need an x setting of -120 * counter.
Zoom needs to take the current canvas offset into account.
(1) above can be done in your click handler:
canvas
.transition()
.duration(1000)
.attr('transform', "translate(" + (offset * counter) + ",0)");
(2) is pretty easily applied to the g element you're wrapping boxes in:
var boxGroup = canvas.append("g")
.attr('transform', 'translate(' + (-offset * counter) + ',0)');
(3) can be added to your redraw handler:
function redraw() {
var translation = d3.event.translate,
newx = translation[0] + offset * counter,
newy = translation[1];
canvas.attr("transform",
"translate(" + newx + "," + newy + ")" + " scale(" + d3.event.scale + ")");
}
See the working fiddle here: http://jsfiddle.net/nrabinowitz/p3m8A/

Resources