d3 donut/pie chart - drawing a line between arcs - d3.js

can't figure to find the endpoint of the arc to draw a line from (0,0) to the arc's endpoint..image attached
I could find the centroid of the arc and draw a line but here I want to pull a line to end of arc so that I can extend that line to the left /right side (and then append the circle at line's endpoint)...could't find any such solution over whole google. Any help will be appreciated. Just a hint will do.

When you pass a data array to the pie generator, it returns an array of objects with the following properties:
data - the input datum; the corresponding element in the input data array.
value - the numeric value of the arc.
index - the zero-based sorted index of the arc.
startAngle - the start angle of the arc.
endAngle - the end angle of the arc.
padAngle - the pad angle of the arc.
From these, you can use startAngle or endAngle to draw your lines, since they hold the arcs' starting points (and endpoints).
But there is a catch: unlike the regular trigonometric representation, D3 pie generator puts the 0 angle at 12 o'clock:
The angular units are arbitrary, but if you plan to use the pie generator in conjunction with an arc generator, you should specify angles in radians, with 0 at -y (12 o’clock) and positive angles proceeding clockwise.
Therefore, we have to subtract Math.PI/2 to get the correct angles.
In the following demo, the coordinates are calculates using sine and cosine:
.attr("y2", function(d) {
return Math.sin(d.startAngle - Math.PI / 2) * (outerRadius)
})
.attr("x2", function(d) {
return Math.cos(d.startAngle - Math.PI / 2) * (outerRadius)
})
Check the demo:
var data = [10, ,12, 50, 15, 20, 40, 6, 32, 17];
var width = 500,
height = 400,
radius = Math.min(width, height) / 2;
var color = d3.scaleOrdinal(d3.schemeCategory10)
var pie = d3.pie()
.sort(null);
var arc = d3.arc()
.innerRadius(radius - 100)
.outerRadius(radius - 50);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll(null)
.data(pie(data))
.enter().append("path")
.attr("fill", function(d, i) {
return color(i);
})
.attr("d", arc);
var lines = svg.selectAll(null)
.data(pie(data))
.enter()
.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("y2", function(d) {
return Math.sin(d.startAngle - Math.PI / 2) * (radius - 50)
})
.attr("x2", function(d) {
return Math.cos(d.startAngle - Math.PI / 2) * (radius - 50)
})
.attr("stroke", "black")
.attr("stroke-width", 1)
<script src="https://d3js.org/d3.v4.min.js"></script>

Once you apply pie layout to your dataset by doing
var pieData = myPieLayout(myDataset)
inside pieData you will find, for each element of your dataset, two properties called startAngle and endAngle. Using that, you can find the position of the point you want, from the center of the pie by iterating through pieData elements and doing
var x = Math.cos(d.endAngle)*radius
var y = Math.sin(d.endAngle)*radius

Related

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.

D3.js fixed position group in chord diagram

I am looking for a way to have a fixed position and width of a group in a d3.js chord diagram. Is there a way to do this?
This is how it is shown by default when drawing the chord diagram:
This is how I would like the top (1st group) to show (wider than the other groups and centered at the top):
If you a fixed data set then you know how much to rotate so that you get your desired position.
I applied rotation on this example:
http://bl.ocks.org/mbostock/4062006
So that it become very much of your proposed chart fiddle below:
http://jsfiddle.net/cyril123/L9s2dpxt/
To achieve this you will need to give rotation to the main g group like this:
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")rotate(100)");
//Rotating this by 100 degrees but this can be your choice depending on your dataset.
Next is the rotation of the text this is governed by this code
ticks.append("text")
.attr("x", 8)
.attr("dy", ".35em")
.attr("transform", function(d) { return d.angle > Math.PI ? "translate(16)" : null; })//I am just giving translate no rotation
.style("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; })
.text(function(d) { return d.label; });

D3 Geo Tile Baselayer Offset

I'm using d3.geo.tile() and have used it successfully before but this time the tile layer doesn't seem to draw at the same scale and translate as the point layer. The below code creates a map that pans and draws just fine, but draws the circles, which should be in the Mediterranean, in Africa. If I zoom in, it scales the tiles and circles just fine, it's as if my xy coordinates are off, but they aren't.
I get the feeling that it's actually drawing the base layer without offsetting and scaling it properly because it should be centering on the coordinates 12,42, but it's a great big mystery to me since this exact same code works fine in a different application.
If someone can spot some problem, or just a hint, that would help.
function createNewMap(){
width = 1200, height = 800;
var tile = d3.geo.tile()
.size([1200, 800]);
var projection = d3.geo.mercator()
.scale((1 << 12) / 2 / Math.PI)
.translate([width / 2, height / 2]);
var center = projection([12, 42]);
var zoom = d3.behavior.zoom()
.scale(projection.scale() * 2 * Math.PI)
.scaleExtent([1 << 10, 1 << 17])
.translate([width - center[0], height - center[1]])
.on("zoom", zoomed);
projection
.scale(1 / 2 / Math.PI)
.translate([0, 0]);
var svg = d3.select("#newMapId").append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom);
var raster = svg.append("g");
var vector = svg.append("g");
vector.selectAll("g").data(dataModule.polisData).enter().append("g")
.attr("class", "sites")
.attr("transform", function(d) {return "translate(" + (projection([d.xcoord,d.ycoord])[0]) + "," + (projection([d.xcoord,d.ycoord])[1]) + ")scale("+(projection.scale())+")"})
.append("circle")
.attr("class", "sitecirc");
zoomed();
function zoomed() {
var tiles = tile
.scale(zoom.scale())
.translate(zoom.translate())
();
var image = raster
.attr("transform", "scale(" + tiles.scale + ")translate(" + tiles.translate + ")")
.selectAll("image")
.data(tiles, function(d) { return d; });
image.exit()
.remove();
image.enter().append("image")
.attr("xlink:href", function(d) { return "http://" + ["a", "b", "c", "d"][Math.random() * 4 | 0] + ".tiles.mapbox.com/v3/elijahmeeks.map-zm593ocx/" + d[2] + "/" + d[0] + "/" + d[1] + ".png"; })
.attr("width", 1)
.attr("height", 1)
.attr("x", function(d) { return d[0]; })
.attr("y", function(d) { return d[1]; });
vector
.attr("transform", "translate(" + zoom.translate() + ")scale(" + zoom.scale() + ")");
d3.selectAll(".sitecirc")
.attr("r", 10 / zoom.scale());
}
Your code appears to be based on my example that changes the SVG transform on zoom. Changing the transform is a nice technique when you have complex geometry that you just want to scale and translate when you pan or zoom — it’s typically faster than reprojecting everything — but it’s also more complex than changing the projection on zoom.
The code doesn’t change very much if you want to change the projection on zoom. In essence:
projection
.scale(zoom.scale() / 2 / Math.PI)
.translate(zoom.translate());
And then re-run your d3.geo.path to re-render. As shown in bl.ocks.org/9535021:
Also, fixing the projection and changing the transform can cause precision problems if you zoom in a lot. Another reason to only use that technique when it offers substantial performance gains by avoid reprojection. And here reprojecting is super-cheap because it’s just a handful of points.

make a circle progress bar, but animate arc.endAngle (angle >180 degree) stops working in d3.js

I am running D3.js to draw a progress bar in circle shape, which you will see the demo on jsfiddle , the progress bar has a transition animation.
The main code is
var width = 960,
height = 500,
twoPi = 2 * Math.PI,
progress = 0,
total = 1308573, // must be hard-coded if server doesn't report Content-Length
formatPercent = d3.format(".0%");
var arc = d3.svg.arc()
.startAngle(0)
.innerRadius(0)
.outerRadius(240);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var meter = svg.append("g")
.attr("class", "progress-meter");
meter.append("path")
.attr("class", "background")
.attr("d", arc.endAngle(twoPi));
var foreground = meter.append("path")
.attr("class", "foreground");
foreground.attr("d", arc.endAngle(twoPi * 0))
foreground.transition().duration(1500).attr("d", arc.endAngle( twoPi * 2/3 ));
var text = meter.append("text")
.attr("text-anchor", "middle")
.attr("dy", ".35em");
to make the progress bar move, we only need to change to the arc.endAngle(), which is on the line.
foreground.transition().duration(1500).attr("d", arc.endAngle( twoPi * 2/3 ));
if the angle is less than 180, ( endangle < twoPi*1/2), then the animation works fine, but when the angle is larger than 180, so means endangle >= twoPi*1/2. then the animation would not show, and if you look at the console, you will find many errors on d3.js
Error: Problem parsing d="M1.1633760361312584e-14,-190A190,190 0 1.481481481481482e-7,1 -0.000022772330200401806,-189.9999883969182L0,0Z" meeting.html:1
2
Error: Problem parsing d="M1.1633760361312584e-14,-190A190,190 0 2.56e-7,1 -0.00003935058659476369,-189.99997994987467L0,0Z"
so what is the exact problem for this, how to solve it
It doesn't work because you can't use the standard transition for radial paths. By default, it simply interpolates the numbers without knowing what they represent, so in your case, you end up with some really small numbers (e.g. 1.1633760361312584e-14) which Javascript represents in exponential notation which is not valid for SVG paths.
The solution is to use a custom tween function that knows how to interpolate arcs:
function arcTween() {
var i = d3.interpolate(0, twoPi * 2/3);
return function(t) {
return arc.endAngle(i(t))();
};
}
Complete example here. You may also be interested in this example, which shows how to do it with data bound to the paths.

Detect if rect is completely inside circle d3js

I'm trying to detect if my rectangle is completely enclosed inside the circle. If the rectangle is completely enclosed in the circle I'd like it to stay "steelblue" if it touches or crosses the line I'd like it to switch to red.
I've figured out a way based of the x, y, cx, cy, and r to determine if it is within the bounding box of the circle, but I need to check it against the actual circle.
I have a running example here: http://jsfiddle.net/TheMcMurder/T92jF/
my code is below:
var drag = d3.behavior.drag()
.on("drag", function(){
var self = d3.select(this)
var dx = d3.event.dx;
var dy = d3.event.dy;
var x = self.attr("x")
var y = self.attr("y")
self.attr("x", (+x + dx))
self.attr("y", (+y + dy))
detection(d3.select(".circle"), d3.select(".rect"))
})
var svg = d3.select("body").append("svg")
.attr("width", 400)
.attr("height", 400)
.attr("class", "parent_svg")
svg.append("rect")
.attr("width", 400)
.attr("height", 400)
.style("fill", "#e4e5e5")
var rect = svg.append("rect")
.attr("width", 100)
.attr("height", 48)
.attr("x", 50)
.attr("y", 50)
.style("fill", "steelblue")
.attr("class", "rect")
.style("cursor", "all-scroll")
.call(drag)
var circle = svg.append("circle")
.attr("class", "circle")
.attr("r", 300/2)
.attr("cx", 350/2)
.attr("cy", 350/2)
.style("fill", "none")
.attr("stroke", "orange")
.attr("stroke-width", 1)
detection(circle, rect)
function detection(circle, rect){
var cx = (+circle.attr("cx"))
var cy = (+circle.attr("cy"))
var r = (+circle.attr("r"))
var x = (+rect.attr("x"))
var y = (+rect.attr("y"))
var width = (+rect.attr("width"))
var height = (+rect.attr("height"))
var x_range = false
var y_range = false
if ( x > (cx-r) && (x+width) < (cx+r)){
x_range = true;
}
if ( y > (cy-r) && (y+height) < (cy+r)){
y_range = true;
}
if (x_range && y_range){
rect.style("fill", "steelblue")
}else{
rect.style("fill", "red")
}
}
One way of doing this is to iterate over the four corners of the rectangle and check if the distance to the centre of the circle is less than the radius. If this is true for all four points, the rectangle is within the circle. If this is true for 1-3 points, the rectangle touches or intersects the circle.
var sum = 0;
[[x,y], [x+width,y], [x,y+height], [x+width,y+height]].forEach(function(c) {
sum += Math.sqrt(Math.pow(cx - c[0], 2) + Math.pow(cy - c[1], 2)) < r ? 1 : 0;
});
This code constructs the coordinates of the four corner points and counts the number of corners that are within the circle (i.e. distance to centre is less than radius). All you need to do now is to check whether that number is 4 or something else.
Complete demo here.

Resources