how to obtain a complete clockwise rotation in D3 - javascript

I want a square to complete a full clockwise rotation on itself, after a pause on the half of the rotation.
The following code makes it doing an half rotation clockwise, and the other half counter-clockwise, contrary to what I expect.
var svg = d3.select('svg');
var s = svg.append("rect")
.attr("width", 50)
.attr("height", 50)
.attr("x", -25)
.attr("y", -25)
.attr("fill", "red")
.attr("transform", "translate(100,100)");
s
.transition()
.duration(1000)
.attr("transform", "translate(100,100) rotate(180)")
.transition()
.delay(1000)
.duration(1000)
.attr("transform", "translate(100,100) rotate(360)");
* {
margin: 0;
padding: 0;
border: 0;
}
body {
background: #ffd;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
I can hack such a code splitting the second half rotation in two quarter clockwise rotations, but I wish to know if there is a more elegant solution.

The culprit here is D3 itself, not any SVG spec.
The problem is that your transition uses d3.interpolateTransform, as we can see here:
var fullname = namespace(name), i = fullname === "transform" ? interpolateTransform : interpolate;
This is v4 source code, not v3, but the principle is the same, as you can see in the actual v3 code:
var interpolate = nameNS == "transform" ? d3_interpolateTransform : d3_interpolate, name = d3.ns.qualify(nameNS);
Then, if we look in the source code for interpolateTransform (again, v4, but v3 is almost the same), we'll see that it uses a function called parseSvg that calculates the matrix for the new transform:
function parseSvg(value) {
if (value == null) return identity;
if (!svgNode) svgNode = document.createElementNS("http://www.w3.org/2000/svg", "g");
svgNode.setAttribute("transform", value);
if (!(value = svgNode.transform.baseVal.consolidate())) return identity;
value = value.matrix;
return decompose(value.a, value.b, value.c, value.d, value.e, value.f);
}
That function is generating 0 as the final value in the matrix when you pass rotate(360) to it (the actual value is -2.4492935982947064e-16, which is practically zero).
Solution
There are several possible solutions here, the easiest one is using interpolateString instead of interpolateTransform.
Also, since your code uses D3 v3, you can take advantage of d3.transform(), which was removed in v4/v5:
d3.interpolateString(d3.transform(d3.select(this).attr("transform")), "translate(100,100) rotate(360)")
Here is your code with that change:
var svg = d3.select('svg');
var s = svg.append("rect")
.attr("width", 50)
.attr("height", 50)
.attr("x", -25)
.attr("y", -25)
.attr("fill", "red")
.attr("transform", "translate(100,100)");
s.transition()
.duration(1000)
.attr("transform", "translate(100,100) rotate(180)")
.transition()
.delay(1000)
.duration(1000)
.attrTween("transform", function() {
return d3.interpolateString(d3.transform(d3.select(this).attr("transform")), "translate(100,100) rotate(360)")
});
<svg></svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>

If I use D3v3 it does not rotate to 180 degrees, if I switch to D3v4 it rotates to 180.
You can interpolate to 359.99. It does not follow the string interpolator from the docs, because you also get scale() in the transform.
translate(100, 100) rotate(359.989990234375) scale(0.999,0.999)
This does not happen if you write your own interpolator.
var svg = d3.select('svg');
var s=svg.append("rect")
.attr("width",50)
.attr("height",50)
.attr("x",-25)
.attr("y",-25)
.attr("fill","red")
.attr("transform","translate(100,100) rotate(0)");
s
.transition()
.duration(3000)
.ease(d3.easeLinear)
.attr("transform","translate(100,100) rotate(180)")
.transition()
.delay(2000)
.duration(3000)
.ease(d3.easeLinear)
.attrTween("transform", () => d3.interpolate("translate(100,100) rotate(180)", "translate(100,100) rotate(360)") );
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>

Related

D3 transition not occurring on svgelement.enter()

I'm throwing in some fairly simple code to build out some rectangles from a data file which is all working fine, however I'm trying to add in a transition for the rectangles.enter() something akin to .transition().duration(1000)
I've looked at using the .on() function prior to the transition, but no matter where I put it in the code either no change, or the whole graph disappears. Is it possible to add in a transition on the enter function, or do I need to work around to use d3.select
d3.json("data/buildings.json").then(function(data){
data.forEach(function(d){
d.height = +d.height;
});
console.log(data);
var svg = d3.select("#chart-area").append("svg")
.attr("width", 400)
.attr("height", 400);
var rectangles = svg.selectAll("rect")
.data(data);
rectangles.enter()
.append("rect")
.attr("x", function(d,i){
return (i * 50) + 25;
})
.attr("y", 25)
.attr("width", 40)
.attr("height",function(d){
return d.height;
})
.attr("fill", "grey")
})
The simple answer is no, for a transition you would need to define two states: the initial state and the final state of the animation. Using the enter - update - exit cycle of d3 you could end up with something like this:
the rectangles fly in from the center of the SVG changing their sizes and color in one smooth transition.
The enter phase sets the initial state of the transition, the update phase performs the changes during the transition to reach the final state. Exit is not really needed for this example. It would take care of removing nodes that no longer exist after the update phase.
There are plenty of good examples and a tutorial about the topic over at https://bl.ocks.org for further reading.
d3.json("data/buildings.json").then(function(data){
data.forEach(function(d){
d.height = +d.height;
});
console.log(data);
var width = 400;
var height = 400;
var svg = d3.select("#chart-area").append("svg")
.attr("width", width)
.attr("height", height);
var rectangles = svg.selectAll("rect")
.data(data);
var rectEnter = rectangles.enter()
.append('rect')
.attr('x', width/2)
.attr('y', height/2)
.attr('width', 1e-6)
.attr('height', 1e-6)
.attr('fill', 'red')
var rectUpdate = rectEnter.merge(rectangles)
rectUpdate.transition()
.duration(1500)
.attr('x', function(d,i) { return (i * 50) + 25 })
.attr('y', 25)
.attr('width', 40)
.attr('height', function(d) { return d.height })
.attr('fill', 'grey')
var rectExit = rectangles.exit().remove()
})
and the dataset buildings.json
[
{
"id": 1,
"height": 20
}, {
"id": 2,
"height": 40
}, {
"id": 3,
"height": 10
}
]

D3 Modify Path's y

I have some user events that append different paths to an svg. What I would like is for each subsequent path to be drawn below the previous path (+ some padding). I would assume this would be done by style.top or .attr('y', my_value). However neither worked for me. And if nothing is done to address this, then the paths will be drawn on top of eachother, which is bad. Should be straight forward, but for added clarity, let me provide the crucial code:
graphGroup.append("path")
.datum(data)
.attr("fill", "none")
.attr("d", line);
I suppose I could programtically create n number of gs, (i.e. graphGroup2 = svg.append('g').attr('transform', 'translate(' + my_transformation +')') and so forth, but I think there should be an easier way. Maybe a dynamic y:
//var count = graphGroup....???
//count could be a way to count the existing number of paths in the g graphGroup
graphGroup.append("path")
.datum(data)
.attr("fill", "none")
.attr('y', count*200)
.attr("d", line);
Obviously, I don't know how to make my dynamic approach work. If you think it could work, you can build off it, or feel free to scrap it in favor of another way.
A SVG <path> element has no y attribute. You'll have to use translate to set the vertical position of those paths. You don't have to create additional < g> elements, though: all the paths can be in the same group.
Here is a demo, where I'm increasing a count variable inside a loop to translate the paths down:
var svg = d3.select("body")
.append("svg")
.attr("width", 300)
.attr("height", 300);
var d = "M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80";
var count = 0;
(function loop() {
if (count++ > 5) return;
setTimeout(function() {
svg.append("path")
.attr("stroke", "black")
.attr("d", d)
.attr("fill", "none")
.attr("transform", "translate(0," + count * 20 + ")")
loop();
}, 1000);
})();
<script src="https://d3js.org/d3.v4.min.js"></script>

D3JS oscillating bubbles

This might be trivial to some people, but I am a newbie to D3JS.
I am trying to plot two static bubbles with opacity changing with respect to an array. I am able to plot the bubbles but I can't make their opacity change continuously. I am using transition and delay and the opacity can only change once. Here is my code sample
(function() {
var dropsize = 100;
var gapsize = 20;
var osc = [[1, 1],[0.5, 0.5],[0, 0],[0.5, 0.5],[1, 1],[0.5, 0.5],[0, 0],[0.5, 0.5]];
var radius = dropsize / 2;
var h = 100;
var w = (4 * radius + 3 * gapsize);
var svg = d3.select("#chart").append("svg");
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h)
.style("background-color", "teal");
var circles = svg.selectAll("circle")
.data([radius, 3 * radius])
.enter()
.append("circle");
circles.attr("cx", function(d, i) {
return d + (i + 1) * gapsize;
})
.attr("cy", h / 2)
.attr("r", function(d, i) {
return radius;
})
.attr("fill", "orange")
.attr("class", "droplet")
.attr("id", function(d, i) {
return "c_" + (i + 1);
});
d3.select("#c_1")
.data(osc)
.transition().delay(function(d, i) {
return i * 1000;
})
.duration(1000)
.attr("opacity", function(d) {
return d[0]
});
})();
See the Pen Bubble Chart with D3.js using Realtime Data
If by "continuously" you mean that you want to run the transition infinitely, use on("end") to call the transition function again.
Here is an example:
var toggle;
var data = [0, 1];
transition();
function transition() {
toggle ^= 1;
d3.select("circle")
.transition()
.duration(1000)
.style("opacity", data[toggle])
.on("end", transition);
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg>
<circle cx="100" cy="100" r="50"></circle>
</svg>
I assume by continuously you mean smoothly transition rather than switch immediately from one opacity state to the next, as opposed to repeating the transition.
You first need to set an initial opacity on the circles when you create them:
.attr("opacity", 0)
And then use d3.selectAll rather than d3.select, or better your variable circles
...
circles
.data(osc)
.transition()
.delay(function(d,i){ return i*1000; })
.duration(1000)
.attr("opacity",function(d){ return d[0] });

Adding 'measures' label to D3 bullet chart

I'm working with D3's bullet chart and am trying to figure out how to display the actual measures number just to the right of the measures rectangle. Since I want to do this for every bullet chart, I figure it'd be best to do it right in the bullet.js code. I'm rather new to D3 so any help would be much appreciated! Here is the link to Mike Bostock's bullet chart example with the bullet.js included at the bottom.
It looks like the measures code is handled in this snippet:
// Update the measure rects.
var measure = g.selectAll("rect.measure")
.data(measurez);
measure.enter().append("rect")
.attr("class", function (d, i) { return "measure s" + i; })
.attr("width", w0)
.attr("height", height / 3)
.attr("x", reverse ? x0 : 0)
.attr("y", height / 3)
.transition()
.duration(duration)
.attr("width", w1)
.attr("x", reverse ? x1 : 0);
measure.transition()
.duration(duration)
.attr("width", w1)
.attr("height", height / 3)
.attr("x", reverse ? x1 : 0)
.attr("y", height / 3);
I thought I could just add something like this after the rect is appended but I've had no such luck.
measure.enter().append("text")
.attr("dy", "1em")
.text(function (d) { return d.measurez; })
.attr("x", reverse ? x0 : 0)
.attr("y", height / 3)
.transition()
.duration(duration)
.attr("width", w1)
.attr("x", reverse ? x1 : 0);
Thank you in advance for your consideration!
You almost got it -- there're just two small things to consider. First, you can't call .enter() twice. Once the enter selection has been operated on, it's merged into the update selection and your second selection will be empty. This is fixed easily by saving the selection in a variable, but in this case I would recommend making a separate selection for the text labels.
var measureLabel = g.selectAll("text.measure")
.data(measurez);
measureLabel.enter()....;
Second, to position the text to the right of the rect, you need to take not only the position, but also the width into account when computing the position of the text element. Also, you can omit a few elements that are not relevant to text elements.
measureLabel.enter()
.append("text")
.attr("class", function(d, i) { return "measure s" + i; })
.attr("dy", "1em")
.attr("dx", "1em")
.text(String)
.attr("x", reverse ? function(d) { return w0(d) + x0(d); } : w0)
.attr("y", height / 3);
measureLabel.transition()
.duration(duration)
.attr("x", reverse ? function(d) { return w1(d) + x1(d); } : w1);
Complete example here.

d3 path gradient stroke

Hello I am working with d3 diagonal diagram and would like to add a gradient to path which links my circles...
I am generating my tree with:
var width = 800,
height = 700;
element.html('');
var color = d3.interpolateLab("#008000", "#c83a22");
var scale = d3.scale.linear().domain([0, 100]).range(["red", "green"]);
var cluster = d3.layout.cluster()
.size([height, width - 160]);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
var svg = d3.select('#tab-manageAccess').append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(40,0)');
/*svg.append("linearGradient")
.attr("id", "line-gradient")
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", 0).attr("y1", y(0))
.attr("x2", 0).attr("y2", y(1000))
.selectAll("stop")
.data([
{offset: "0%", color: "red"},
{offset: "40%", color: "red"},
{offset: "40%", color: "black"},
{offset: "62%", color: "black"},
{offset: "62%", color: "lawngreen"},
{offset: "100%", color: "lawngreen"}
])
.enter().append("stop")
.attr("offset", function(d) { return d.offset; })
.attr("stop-color", function(d) { return d.color; });*/
var nodes = cluster.nodes(scope.accessTree),
links = cluster.links(nodes);
var link = svg.selectAll('.link')
.data(links)
.enter().append('path')
.attr('class', 'link')
.attr('d', diagonal);
var node = svg.selectAll('.node')
.data(nodes)
.enter().append('g')
.attr('class', 'node')
.attr('transform', function(d) { return 'translate(' + d.y + ',' + d.x + ')'; });
node.append('circle')
.attr('r', 4.5);
node.append('text')
.attr('dx', function(d) { return d.children ? -8 : 8; })
.attr('dy', 3)
.style('text-anchor', function(d) { return d.children ? 'end' : 'start'; })
.style('font-weight', 'bold')
.attr('fill', function (d) {
var color = '#4D7B88';
if (d.depth === 0) {
color = '#7F3762';
} else if(d.depth === 1) {
color = '#83913D';
}
return color;
})
.text(function(d) { return d.name; });
d3.select(self.frameElement).style('height', height + 'px');
I found this example: https://gist.github.com/mbostock/4163057 co I created variable color with d3.interpolateLab("#008000", "#c83a22"); and then added .style("fill", function(d) { return color(d.t); })
.style("stroke", function(d) { return color(d.t); }) to path element but it doesn't work :( can anyone help me?
The aspect of Mike Bostock's code that you're missing is where he divides the path up into hundreds of different sub-paths and sets the color on each one separately. Go to the live version at http://bl.ocks.org/mbostock/4163057 and check the DOM to see what's really going on.
Why does he do that? Because, while you can set the stroke of an SVG line or path to a gradient, you can't tell it to make the gradient follow the slope or curve of that line. The angle of the gradient is defined when the gradient is created, based on either:
the rectangular bounding box for the element that uses it
(if gradientUnits is set to ObjectBoundingBox), or
the user coordinate system where the object is drawn
(if gradientUnits is set to userSpaceOnUse).
The way you have it set up (in your commented out code) basically creates a hidden gradient background over the entire image, and then lets it show through wherever you draw your lines. Clearly not what you wanted.
Hence, Mike's complex function and the hundreds of sub-paths it creates. Probably not what you want, either, especially if you want the graph to be interactive.
For simple lines, there is another way to get gradients to line up correctly from start to finish of your line.
I've got a very simple example with plain SVG (no D3) up here: http://codepen.io/AmeliaBR/pen/rFtGs
In short, you have to define your line to go in the direction that matches up with the gradient, and then use transforms (scale/rotate/translate) to actually position the line where you want it.
How tricky that would be to implement in D3 depends on how complex your layout is. If you were just using simple lines, I think this would work:
calculate the length of the line and its slope using simple geometry from the (x1,y1) and (x2,y2) values,
draw the line from (0,0) to (0,length) (assuming a vertical gradient),
add a transform attribute of translate(x1,y1) rotate(slope)
With paths, you'd need to know what type of path you're dealing with and use regular expressions to parse and edit the path's d attribute. Very messy.
Maybe just try line markers for start and end?

Categories