use d3.js to animate a gauge needle - d3.js

I am trying to use d3.js to animate a gauge needle, but end up with a weird animation. I have done some search from Internet, but I couldn't figure out what solution can I use to fix the problem.
Codepen
function createNeedle(sampleAngle){
topX = centerX - Math.cos(sampleAngle) * triLength
topY = centerY - Math.sin(sampleAngle) * triLength
leftX = centerX - 10 * Math.cos(sampleAngle - Math.PI / 2)
leftY = centerY - 10 * Math.sin(sampleAngle - Math.PI / 2)
rightX = centerX - 10 * Math.cos(sampleAngle + Math.PI / 2)
rightY = centerY - 10 * Math.sin(sampleAngle + Math.PI / 2)
return " M " + leftX + " " + leftY + " L " + topX + " " + topY + " L " + rightX + " " + rightY;
}
//animate the needle
d3.select('.moveNeedle')
.attr('d', createNeedle(sampleAngle1))
.transition()
.duration(2000)
.attr('d', createNeedle(sampleAngle2));

You can make your life so much easier if you apply a transform="rotate()" instead of redrawing the path.
Nonetheless, you need to supply a custom Tween function, as the standard d3.interpolateTransformSvg acts in unexpected ways.
var topX = centerX - triLength,
topY = centerY,
leftX = centerX,
leftY = centerY + 10,
rightX = centerX,
rightY = centerY - 10;
function rotateNeedle(sampleAngle){
return "rotate(" + sampleAngle + "," + centerX + "," + centerY + ")";
}
d3.select('.moveNeedle')
// only draw once
.attr('d', "M" + leftX + " " + leftY + " " + topX + " " + topY + " " + rightX + " " + rightY)
// supply angles in degrees!
.attr('transform', rotateNeedle(sampleAngle1))
.transition()
.duration(2000)
.attrTween('transform', function () {
var i = d3.interpolate(sampleAngle1, sampleAngle2)
return function (t) {
return rotateNeedle(i(t));
};
});

Related

d3 path - split into 2 new paths

I am working with a Force-directed node chart using d3js. I would like to use marker-mid attribute on the paths but apparently there is a bug in FireFox and Safari so the markers fail to render. This bug seems to be limited to marker-mid, as marker-end and marker-start render just fine.
So unless someone shares a solution I am resorting to a work-around.
My idea is to split the arc into 2 separate paths and use the marker-end attribute.
The following is the called in the Tick function
function linkArc(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
}`
As I read it, linkArc is generates the points between the 2 nodes. How can I change this function to generate a path from start to mid point ? ( then in another call, from mid-point to end )
I am thinking of calling 2 functions instead of one
linkArcA = PathA starts at the source node and ends at the mid point
linkArcB = PathB starts at the mid point and ends at the target node
Otherwise I am open to any alternative solution.
I was able to produce a working solution which might be helpful for others trying to overcome this bug.
Basics Steps
Create 2 paths for each node - pathA as first half path , path B as
the second half path.
Run linkArc for PathA. Creates the full curved path
Iterate over all PathA and push their mid-points to an array
Run linkArcStart creating paths from start to mid-points
Run LinkArcEnd creating paths from mid-points to start
Here are the snippets of the code used
pathA = svg.append("g").selectAll("path")
.data(force.links())
.enter()
.append("path")
pathB = svg.append("g").selectAll("path")
.data(force.links())
.enter()
.append("path")
var mpts = [];
function tick() {
pathA.attr("d", linkArc);
pathA.attr("mpts",function(d,i){
var p = d3.select(this).node()
var pt = p.getPointAtLength(p.getTotalLength()/2)
mpts[i] = [pt.x,pt.y];
})
pathA.attr("d", linkArcStart);
pathB.attr("d",linkArcEnd)
}
function linkArc(d) {
var dx = (d.target.x - d.source.x) ,
dy = (d.target.y - d.source.y),
dr = Math.sqrt(dx * dx + dy * dy),
pts = "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y
return pts;
}
function linkArcStart(d,i) {
dx = (d.target.x - d.source.x) ,
dy = (d.target.y - d.source.y),
dr = Math.sqrt(dx * dx + dy * dy),
pts = "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + mpts[i][0]+ "," + mpts[i][1]
return pts;
}
function linkArcEnd(d,i) {
dx = (d.target.x - d.source.x) ,
dy = (d.target.y - d.source.y),
dr = Math.sqrt(dx * dx + dy * dy),
pts = "M" + + mpts[i][0]+ "," + mpts[i][1] + "A" + dr + "," + dr + " 0 0,1 " + d.target.x+ "," + d.target.y
return pts;
}
Hope this helps others who are struggling to create marker-mid that render in all browsers (on Mac)

Is it possible to create an SVG rect element with top left and top right rounded corners for use in D3? [duplicate]

This question already has answers here:
svg / d3.js rounded corners on one side of a rectangle
(5 answers)
Closed 6 years ago.
Is there a simple way to place rounded corners just on the top of the Bar(s) in a D3 Vertical Bar Chart? I've been playing around with .attr("rx", 3) and that seems to affect all four corners of a Bar.
You cannot specify which corners you want to make round in SVG: rx will affect all 4 corners.
The only solution is using a path for simulating a rectangle. This function returns a path with top corners round:
function rectangle(x, y, width, height, radius){
return "M" + (x + radius) + "," + y + "h" + (width - 2*radius)
+ "a" + radius + "," + radius + " 0 0 1 " + radius + "," + radius + "v" +
(height - 2*radius) + "v" + radius + "h" + -radius + "h" +
(2*radius - width) + "h" + -radius + "v" + -radius + "v" +
(2*radius - height) + "a" + radius + "," + radius + " 0 0 1 "
+ radius + "," + -radius + "z";
};
Here is a demo snippet showing a "bar chart" with those paths, with a radius (the rx equivalent here) of 5px:
function rectangle(x, y, width, height, radius){
return "M" + (x + radius) + "," + y + "h" + (width - 2*radius) + "a" + radius + "," + radius + " 0 0 1 " + radius + "," + radius + "v" + (height - 2*radius) + "v" + radius + "h" + -radius + "h" + (2*radius - width) + "h" + -radius + "v" + -radius + "v" + (2*radius - height) + "a" + radius + "," + radius + " 0 0 1 " + radius + "," + -radius + "z";
};
var data = [40, 50, 30, 40, 90, 54, 20, 35, 60, 42];
var svg = d3.select("body")
.append("svg")
.attr("width", 400)
.attr("height", 120);
var rects = svg.selectAll(".paths").data(data).enter().append("path");
rects.attr("d", function(d,i){ return rectangle(10+40*i,100-d,20,d,5)});
var texts = svg.selectAll(".text").data("ABCDEFGHIJ".split("")).enter().append("text").attr("y",114).attr("x", function(d,i){return 16+40*i}).text(function(d){return d});
path {
fill:teal;
}
text {
fill:darkslategray;
font-size: 12px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
PS: I didn't write that function, it was based on these answers by M. Bostock and R. Longson.

Existing Triangles Cannot be Removed

I bind triangles with data in D3.js. But the triganle cannot be removed with the data. The rectangles are okay. Complete code is attached!
var svg = d3.select("body").append("svg")
.attr("width", 250)
.attr("height", 250);
function render(data){
var tris = svg.selectAll("tri").data(data);
tris.enter().append("path");
tris.attr("d", function(d) {
var x1 = (0.4 - 0.2 * (d - 1)) * 250, y1 = 0.3 * 250;
var x2 = (0.5 - 0.2 * (d - 1)) * 250, y2 = 0.1 * 250;
var x3 = (0.6 - 0.2 * (d - 1)) * 250, y3 = 0.3 * 250;
return "M" + x1 + " " + y1 + " L" + x2 + " " + y2 + " L" + x3 + " " + y3 + "Z";
});
tris.exit().remove();
var rects = svg.selectAll("rect").data(data);
rects.enter().append("rect");
rects.attr("y", 50)
.attr("width", 20)
.attr("height", 20)
.attr("x", function(d) { return d * 40; });
rects.exit().remove();
}
render([1, 2, 3]);
render([1, 2]);
render([1]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
There is no SVG element called <tri>. Your enter selection works because you can select anything in your data binding function. As there is nothing named "tri" in the whole DOM (be it a tag, a class, an ID, whatever...), your enter selection is never empty and your exit selection is never populated.
That being said, an easy solution is selecting by class...
var tris = svg.selectAll(".tri").data(data);
And setting this class in your enter selection:
tris.enter().append("path").attr("class", "tri");
Here is your code with the changes:
var svg = d3.select("body").append("svg")
.attr("width", 250)
.attr("height", 250);
function render(data){
var tris = svg.selectAll(".tri").data(data);
tris.enter().append("path").attr("class","tri");
tris.attr("d", function(d) {
var x1 = (0.4 - 0.2 * (d - 1)) * 250, y1 = 0.3 * 250;
var x2 = (0.5 - 0.2 * (d - 1)) * 250, y2 = 0.1 * 250;
var x3 = (0.6 - 0.2 * (d - 1)) * 250, y3 = 0.3 * 250;
return "M" + x1 + " " + y1 + " L" + x2 + " " + y2 + " L" + x3 + " " + y3 + "Z";
});
tris.exit().remove();
var rects = svg.selectAll("rect").data(data);
rects.enter().append("rect");
rects.attr("y", 50)
.attr("width", 20)
.attr("height", 20)
.attr("x", function(d) { return d * 40; });
rects.exit().remove();
}
render([1, 2, 3]);
setTimeout(() => render([1, 2]), 1000);
setTimeout(() => render([1]), 2000)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

display axes in a circle with equal spacing in between

As you can see from this example by Mike Bostock, it is possible to display the axis to a graph in a circle. In the linked to example, there are three axis (with the areas in between of equal size) which seemed to be created by this line of code
var angle = d3.scale.ordinal().domain(d3.range(4)).rangePoints([0, 2 * Math.PI]),
plus
svg.selectAll(".axis")
.data(d3.range(3))
.enter().append("line")
.attr("class", "axis")
.attr("transform", function(d) { return "rotate(" + degrees(angle(d)) + ")"; })
.attr("x1", radius.range()[0])
.attr("x2", radius.range()[1]);
Playing around with that example, I was able to create a graph with six axes (with equal spacing between each axis) that covered the whole circle using this code
var angle = d3.scale.ordinal().domain(["one", "two", "three", "four", "five", "six"]).range([0, 45, 90, 135, 180, 225])
and then
svg.selectAll(".axis").data(d3.range(7))
//code omitted
However, I haven't been able to create a circle with 9 axes (with equal spacing in between) by doing this (as I expected it would)
var angle = d3.scale.ordinal()
.domain(["one", "two", "three", "four", "five", "six", "seven", "eight", "nine"])
.range([0, 45, 90, 135, 180, 225, 270, 315, 360]);
svg.selectAll(".axis").data(d3.range(10))
//code omitted
The result of doing this is that the axes start to go around the circle a second time.
Question: is there a pattern that can be followed to have an arbitrary number of axes displayed in a circle with equal spacing in between each? If so, please explain the d3 principles behind the two successful attempts and the one unsuccessful attempt shown and linked to above.
Update
Although removing the calls to degrees, puts the axes in the right position, (and then removing the call to degrees in the nodes code puts the nodes on the axes in the right axes), the links are not lining up properly i.e. they are not starting and ending on the axes, but rather floating unanchored. You can see the problem in this image
This is the code for the links (notice that it doesn't have a call to degrees)
svg.selectAll(".link")
.data(linx)
.enter().append("path")
.attr("class", "link")
.attr("class", function(d) { return "link " + d.Class})
.attr("d", link()
.angle(function(d) { return angle(d.X); })
.radius(function(d) { return radius(d.Y); }))
.on("mouseover", linkMouseover)
.on("mouseout", mouseout);
It calls a link function (which also doens't have a call to degrees, so I don't know why the links are starting and ending at the old position of the axes) that I got from Mike Bostock's hive implementation
function link() {
var source = function(d) { return d.Source; },
target = function(d) { return d.Target; },
angle = function(d) { return d.angle; },
startRadius = function(d) { return d.radius; },
endRadius = startRadius,
arcOffset = -Math.PI / 2;
function link(d, i) {
// console.log(d, i, "interior link func");
var s = node(source, this, d, i),
t = node(target, this, d, i),
x;
if (t.a < s.a) x = t, t = s, s = x;
if (t.a - s.a > Math.PI) s.a += 2 * Math.PI;
var a1 = s.a + (t.a - s.a) / 3,
a2 = t.a - (t.a - s.a) / 3;
return s.r0 - s.r1 || t.r0 - t.r1
? "M" + Math.cos(s.a) * s.r0 + "," + Math.sin(s.a) * s.r0
+ "L" + Math.cos(s.a) * s.r1 + "," + Math.sin(s.a) * s.r1
+ "C" + Math.cos(a1) * s.r1 + "," + Math.sin(a1) * s.r1
+ " " + Math.cos(a2) * t.r1 + "," + Math.sin(a2) * t.r1
+ " " + Math.cos(t.a) * t.r1 + "," + Math.sin(t.a) * t.r1
+ "L" + Math.cos(t.a) * t.r0 + "," + Math.sin(t.a) * t.r0
+ "C" + Math.cos(a2) * t.r0 + "," + Math.sin(a2) * t.r0
+ " " + Math.cos(a1) * s.r0 + "," + Math.sin(a1) * s.r0
+ " " + Math.cos(s.a) * s.r0 + "," + Math.sin(s.a) * s.r0
: "M" + Math.cos(s.a) * s.r0 + "," + Math.sin(s.a) * s.r0
+ "C" + Math.cos(a1) * s.r1 + "," + Math.sin(a1) * s.r1
+ " " + Math.cos(a2) * t.r1 + "," + Math.sin(a2) * t.r1
+ " " + Math.cos(t.a) * t.r1 + "," + Math.sin(t.a) * t.r1;
}
function node(method, thiz, d, i) {
var node = method.call(thiz, d, i),
a = +(typeof angle === "function" ? angle.call(thiz, node, i) : angle) + arcOffset,
r0 = +(typeof startRadius === "function" ? startRadius.call(thiz, node, i) : startRadius),
r1 = (startRadius === endRadius ? r0 : +(typeof endRadius === "function" ? endRadius.call(thiz, node, i) : endRadius));
return {r0: r0, r1: r1, a: a};
}
link.source = function(_) {
if (!arguments.length) return source;
source = _;
return link;
};
link.target = function(_) {
if (!arguments.length) return target;
target = _;
return link;
};
link.angle = function(_) {
if (!arguments.length) return angle;
angle = _;
return link;
};
link.radius = function(_) {
if (!arguments.length) return startRadius;
startRadius = endRadius = _;
return link;
};
link.startRadius = function(_) {
if (!arguments.length) return startRadius;
startRadius = _;
return link;
};
link.endRadius = function(_) {
if (!arguments.length) return endRadius;
endRadius = _;
return link;
};
return link;
}
You have angles in degrees already, you don't need to use the degree() function. If you remove the call to that everything will work fine.

d3js link arc filled useless part

here is how my link draw currently.
here is below picture code (that comes from my real putout html)
<svg height="1210" width="1400">
<path class="link" id="linkId_2" opacity="0.9468531468531468" style="stroke-width: 5px; marker-end: url(#end-arrow);" d="M652.3404429062306,276.08144057675605A166.53959040529506,166.53959040529506 0 0,1 767.3217566577334,315.4663301140565"></path>
</svg>
But I want to draw like this. (should be like link, not part of circle)
here is my real part of code.
function updateLink() {
this.attr('d', function(d) {
var deltaX = d.target.x - d.source.x,
deltaY = d.target.y - d.source.y,
dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
normX = deltaX / dist,
normY = deltaY / dist;
if( !normX) normX = 0;
if( !normY) normY = 0;
var sourcePadding = d.left ? circleSize(d.source.inDegree) + 10 : 20,
targetPadding = d.right ? circleSize(d.target.inDegree) + 5 : 20,
sourceX = d.source.x + (sourcePadding * normX),
sourceY = d.source.y + (sourcePadding * normY),
targetX = d.target.x - (targetPadding * normX),
targetY = d.target.y - (targetPadding * normY);
return "M" + sourceX + "," + sourceY + "A" + dist + "," + dist + " 0 0,1 " + targetX + "," + targetY;
});
}
Here is example I used (http://bl.ocks.org/mbostock/1153292)
If you want the stroke to be unfilled then simply set the fill to none and the stroke to a colour e.g.
<svg height="1210" width="1400" viewBox="600 230 600 600">
<path class="link" id="linkId_2" opacity="0.9468531468531468" style="fill:none;stroke:black;stroke-width: 5px; marker-end: url(#end-arrow);" d="M652.3404429062306,276.08144057675605A166.53959040529506,166.53959040529506 0 0,1 767.3217566577334,315.4663301140565"></path>
</svg>

Resources