So I work with D3.js and I have a radar chart with data which are represented by text positioned within a circle.
Texts have always different length and I would like to directly after each of these texts append an icon in this order: chart, text label, trash can.
I already tried to calculate length within an attribute where the text is rendered, which gives me length of each label correctly, but then I did not manage to append icon:
.attr("lenght", function() {
var datalong = this.getComputedTextLength();
})
Then I as well tried to append icon separately which works, but I don't know how to then get the length:
axis.append("svg:image")
.attr("xlink:href","http://svgur.com/i/121.svg")
.attr("y",-7)
.attr("opacity",1)
.attr("transform", function(d, i,e) {
var angleI = angleSlice * i * 180 / Math.PI - 90;
var distance = radius * options.trash.trashFactor;
var flip = angleI > 90 ? 180 : 0;
return "rotate(" + angleI + ") translate(" + distance + ")" + "rotate(" + flip + ")"
});
Then I tried to append icon in method where I write data (text), but the icon did not show up. I tried to wrap them into groups and position them next to each other, but also without succeed.
I have a JSFiddle with somewhat closest option I was able to get. I hope it is possible to understand what I am trying to do, if not please refer to fiddle and where you can easily understand what I am trying to achieve.
JSFiddle: https://jsfiddle.net/fsb47ndf/5/
Any help is highly appreciated!
Here is another idea, to use FontAwesome font and icons. This way you avoid the appending of svg:image.
.html(function (d, i) {
var angleI = angleSlice * i * 180 / Math.PI - 90;
if (angleI > 90) {
return ' ' + d;
} else {
return d + ' '
}
});
https://jsfiddle.net/fsb47ndf/25/
 is FA unicode for trash icon (http://fontawesome.io/icon/trash-o/). Rotate the icon the same way you rotate text.
You can get the length of the texts with this cumbersome math:
var textLength = d3.select(this.parentNode).select("text").node().getComputedTextLength();
Basicaly, this is what the code does:
d3.select(this.parentNode): It selects the parent of the icon, then...
select("text").node(): It selects the text which is child of that parent, and finally...
getComputedTextLength(): It gets the length of that text.
Then, you use it to set the distance of the icons:
var distance = angleI > 90 ? radius + textLength + 40 : radius + textLength + 30;
Here is your updated fiddle: https://jsfiddle.net/0L4xzmfo/
And here the same code in the snippet:
data = [{
name: 'DATA11111',
value: 22,
}, {
name: 'DATA2',
value: 50,
}, {
name: 'DATA33333333',
value: 0,
}, {
name: 'DATA444444',
value: 24,
}, {
name: 'DATA55',
value: 22,
}, {
name: 'DATA6666',
value: 30,
}, {
name: 'DATA7',
value: 20,
}, {
name: 'DATA8',
value: 41,
}, {
name: 'DATA9',
value: 31,
}, {
name: 'DATA10',
value: 30,
}, {
name: 'DATA1121213213',
value: 30,
}, {
name: 'DATA12',
value: 30,
}, {
name: 'DATA1123123212313',
value: 30,
}, {
name: 'DATA14',
value: 30,
}, ];
var options = {
width: 600,
height: 600,
margins: {
top: 100,
right: 100,
bottom: 100,
left: 100
},
circles: {
levels: 6,
maxValue: 100,
labelFactor: 1.15,
dataFactor: 1.09,
opacity: 0.2,
},
trash: {
trashFactor: 1.32
}
};
var allAxis = (data.map(function(i, j) {
return i.name
})),
total = allAxis.length,
radius = Math.min(options.width / 2, options.height / 2),
angleSlice = Math.PI * 2 / total,
Format = d3.format('');
var rScale = d3.scale.linear()
.domain([0, options.circles.maxValue])
.range([50, radius]);
var svg = d3.select("body").append("svg")
.attr("width", options.width + options.margins.left + options.margins.right)
.attr("height", options.height + options.margins.top + options.margins.bottom);
var g = svg.append("g")
.attr("transform", "translate(" + (options.width / 2 + options.margins.left) + "," + (options.height / 2 + options.margins.top) + ")");
var axisGrid = g.append("g")
.attr("class", "axisWraper");
var axis = axisGrid.selectAll(".axis")
.data(allAxis)
.enter()
.append("g")
.attr("class", "axis")
//append them lines
axis.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", function(d, i) {
var tempX2 = radius * Math.cos(angleSlice * i - Math.PI / 2);
return tempX2;
})
.attr("y2", function(d, i) {
var tempY = radius * Math.sin(angleSlice * i - Math.PI / 2);
return tempY;
})
.attr("class", "line")
.attr("stroke", "black")
.attr("fill", "none");
//Draw background circles
axisGrid.selectAll(".levels")
.data([6, 5, 4, 3, 2, 1])
.enter()
.append("circle")
.attr("class", "gridCircle")
.attr("r", function(d, i) {
return parseInt(radius / options.circles.levels * d, 10);
})
.attr("stroke", "black")
.attr("fill-opacity", options.circles.opacity);
//Write data
axis.append("text")
.attr("class", "labels")
.attr("font-size", "12px")
.attr("font-family", "Montserrat")
.attr("text-anchor", function(d, i) {
var angleI = angleSlice * i * 180 / Math.PI - 90;
return angleI > 90 ? "end" : "start"
})
.attr("dy", ".35em")
.attr("fill", "black")
.attr("transform", function(d, i) {
var angleI = angleSlice * i * 180 / Math.PI - 90; // the angle to rotate the label
var distance = radius * options.circles.dataFactor; // the distance from the center
var flip = angleI > 90 ? 180 : 0; // 180 if label needs to be flipped
return "rotate(" + angleI + ") translate(" + distance + ")" + "rotate(" + flip + ")"
})
.text(function(d) {
return d;
});
axis.append("svg:image")
.attr("xlink:href", "http://svgur.com/i/121.svg")
.attr("class", "trash")
.attr("y", -7)
.attr("text-anchor", "end")
.attr("transform", function(d, i) {
var textLength = d3.select(this.parentNode).select("text").node().getComputedTextLength();
var angleI = angleSlice * i * 180 / Math.PI - 90; // the angle to rotate the label
var distance = angleI > 90 ? radius + textLength + 40 : radius + textLength + 30; // the distance from the center
var flip = angleI > 90 ? 180 : 0; // 180 if label needs to be flipped
return "rotate(" + angleI + ") translate(" + distance + ")" + "rotate(" + flip + ")"
});
.trash {
position: absolute;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
PS: I'm using a magic number because I didn't look at the code to find the exact padding of the text. Please change that magic number accordingly.
Related
I have text objects labeling points that are evenly spaced around a circle. Thanks to this article, I am able to correctly position both the points and text objects but the labels on the left hemisphere of the circle need to be rotated 180 degrees (flipped vertically) to be more legible.
I thought I could rotate the text object about its own origin before rotating it to the appropriate position around the circle but was unable to determine how to locate the center position of each text object.
How can I rotate text objects about their center for those on the left hemisphere of the circle (angle>= PI/2 && angle<=PI*1.5)? Or is there a better technique to use?
<style type="text/css">
* {
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 13px;
}
circle {
fill: steelblue;
fill-opacity: .8;
}
circle:hover {
fill: orange;
fill-opacity: .8;
}
</style>
<div id="canvas"></div>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.v2.min.js"></script>
<script type="text/javascript">
(function () {
var paddding = 250;
var createNodes = function () {
var nodeData = [
{ id: 0, label: 'AAA' },
{ id: 1, label: 'BBB' },
{ id: 2, label: 'CCC' },
{ id: 3, label: 'DDD' },
{ id: 4, label: 'EEE' },
{ id: 5, label: 'FFF' },
{ id: 6, label: 'GGG' },
{ id: 7, label: 'HHH' }
];
var radius = 100;
var nodes = [],
width = (radius * 2) + paddding,
height = (radius * 2) + paddding,
angle,
x,
y,
i;
var numNodes = nodeData.length;
for (i = 0; i < numNodes; i++) {
angle = (i / (numNodes / 2)) * Math.PI;
x = (radius * Math.cos(angle)) + (width / 2);
y = (radius * Math.sin(angle)) + (width / 2);
nodes.push({ 'id': i, 'x': x, 'y': y, 'label': nodeData[i].label, 'angle': angle });
}
return nodes;
}
var createSvg = function (radius, callback) {
d3.selectAll('svg').remove();
var svg = d3.select('#canvas').append('svg:svg')
.attr('width', (radius * 2) + paddding)
.attr('height', (radius * 2) + paddding);
callback(svg);
}
var createElements = function (svg, nodes, elementRadius) {
element = svg.selectAll('circle')
.data(nodes)
.enter().append('svg:circle')
.attr('r', elementRadius)
.attr('cx', function (d, i) { return d.x; })
.attr('cy', function (d, i) { return d.y; });
element = svg.selectAll('text')
.data(nodes)
.enter().append('svg:text')
.text(function (d, i) { return d.label + " - " + d.angle.toFixed(2) + ", " + (d.angle*180/Math.PI); })
.attr('x', function (d, i) { return nodes[0].x + 15; }) // add 15 for spacing off point
.attr('y', function (d, i) { return nodes[0].y; })
.attr("dy", ".35em")
.style("alignment-baseline","middle")
.style("text-anchor", "start")
.attr("transform", function(d,i) {
return "rotate(" + (d.angle * 180) / Math.PI + ", 225, 225)";})
;
}
var draw = function () {
var radius = 100;
var nodes = createNodes();
createSvg(radius, function (svg) {
createElements(svg, nodes, 10);
});
}
$(document).ready(function () {
draw();
});
})();
</script>
If you want to reverse the labels for those on the left side of the circle. You can achieve different ways. One way is by modifying three attributes of the text as you append it:
.attr('x', function (d, i) { return nodes[0].x + 15; })
.style("text-anchor", "start")
.attr("transform", function(d,i) {
return "rotate(" + (d.angle * 180) / Math.PI + ", 225, 225)"
})
If you modify only some of these, you might not get the results you are looking for.
Modification of text-end
This is needed as your text will start away from the point you are defining, and as the text may have variable length, defining a start point will be more complex than necessary. For points you need to flip, you'll need to use:
.style("text-anchor", "end")
Modification of the transform and x
The text needs to be rotated 180 degrees so that it is right way up; however, if you modify this function to add 180 degrees to any text, then the text will appear on the wrong side of the display. So, you'll need to set x to a new value too, so that it appears on the correct side of the display:
.attr('x', function (d, i) { return nodes[0].x - 215; }) // radius * 2, add 15 for spacing off point
.attr("transform", function(d,i) {
return "rotate(" + ((d.angle * 180) / Math.PI - 180) + ", 225, 225)"
})
All together, that looks like:
(function () {
var paddding = 250;
var createNodes = function () {
var nodeData = [
{ id: 0, label: 'AAA' },
{ id: 1, label: 'BBB' },
{ id: 2, label: 'CCC' },
{ id: 3, label: 'DDD' },
{ id: 4, label: 'EEE' },
{ id: 5, label: 'FFF' },
{ id: 6, label: 'GGG' },
{ id: 7, label: 'HHH' }
];
var radius = 100;
var nodes = [],
width = (radius * 2) + paddding,
height = (radius * 2) + paddding,
angle,
x,
y,
i;
var numNodes = nodeData.length;
for (i = 0; i < numNodes; i++) {
angle = (i / (numNodes / 2)) * Math.PI;
x = (radius * Math.cos(angle)) + (width / 2);
y = (radius * Math.sin(angle)) + (width / 2);
nodes.push({ 'id': i, 'x': x, 'y': y, 'label': nodeData[i].label, 'angle': angle });
}
return nodes;
}
var createSvg = function (radius, callback) {
d3.selectAll('svg').remove();
var svg = d3.select('#canvas').append('svg:svg')
.attr('width', (radius * 2) + paddding)
.attr('height', (radius * 2) + paddding);
callback(svg);
}
var createElements = function (svg, nodes, elementRadius) {
element = svg.selectAll('circle')
.data(nodes)
.enter().append('svg:circle')
.attr('r', elementRadius)
.attr('cx', function (d, i) { return d.x; })
.attr('cy', function (d, i) { return d.y; });
element = svg.selectAll('text')
.data(nodes)
.enter().append('svg:text')
.text(function (d, i) { return d.label + " - " + d.angle.toFixed(2) + ", " + (d.angle*180/Math.PI); })
.attr('x', function (d, i) { return nodes[0].x - 215; }) // radius * 2, add 15 for spacing off point
.attr('y', function (d, i) { return nodes[0].y; })
.attr("dy", ".35em")
.style("alignment-baseline","middle")
.style("text-anchor", "end")
.attr("transform", function(d,i) {
return "rotate(" + ((d.angle * 180) / Math.PI - 180) + ", 225, 225)";})
;
}
var draw = function () {
var radius = 100;
var nodes = createNodes();
createSvg(radius, function (svg) {
createElements(svg, nodes, 10);
});
}
$(document).ready(function () {
draw();
});
})();
* {
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 13px;
}
circle {
fill: steelblue;
fill-opacity: .8;
}
circle:hover {
fill: orange;
fill-opacity: .8;
}
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.v2.min.js"></script>
<div id="canvas"></div>
However, now the labels on the right are upside down. All that is left is to determine is whether a label falls on the right half or the left half and assign the appropriate attributes based on this.
Zero degrees points to the right, it is not the top of the diagram. Therefore, you need to ascertain if d.angle is less than 90 degrees (bottom right) or more than 270 degrees (top right), if so, your original code can be applied. If not, then you need to flip the label using the above code:
(function () {
var paddding = 250;
var createNodes = function () {
var nodeData = [
{ id: 0, label: 'AAA' },
{ id: 1, label: 'BBB' },
{ id: 2, label: 'CCC' },
{ id: 3, label: 'DDD' },
{ id: 4, label: 'EEE' },
{ id: 5, label: 'FFF' },
{ id: 6, label: 'GGG' },
{ id: 7, label: 'HHH' }
];
var radius = 100;
var nodes = [],
width = (radius * 2) + paddding,
height = (radius * 2) + paddding,
angle,
x,
y,
i;
var numNodes = nodeData.length;
for (i = 0; i < numNodes; i++) {
angle = (i / (numNodes / 2)) * Math.PI;
x = (radius * Math.cos(angle)) + (width / 2);
y = (radius * Math.sin(angle)) + (width / 2);
nodes.push({ 'id': i, 'x': x, 'y': y, 'label': nodeData[i].label, 'angle': angle });
}
return nodes;
}
var createSvg = function (radius, callback) {
d3.selectAll('svg').remove();
var svg = d3.select('#canvas').append('svg:svg')
.attr('width', (radius * 2) + paddding)
.attr('height', (radius * 2) + paddding);
callback(svg);
}
var createElements = function (svg, nodes, elementRadius) {
element = svg.selectAll('circle')
.data(nodes)
.enter().append('svg:circle')
.attr('r', elementRadius)
.attr('cx', function (d, i) { return d.x; })
.attr('cy', function (d, i) { return d.y; });
element = svg.selectAll('text')
.data(nodes)
.enter().append('svg:text')
.text(function (d, i) { return d.label + " - " + d.angle.toFixed(2) + ", " + (d.angle*180/Math.PI); })
.attr('x', function (d, i) {
if (d.angle > Math.PI/2 && d.angle < 1.5 * Math.PI) {
return nodes[0].x - 215 }
else {
return nodes[0].x + 15;
}
})
.attr('y', function (d, i) { return nodes[0].y; })
.attr("dy", ".35em")
.style("alignment-baseline","middle")
.style("text-anchor", function(d) {
if (d.angle > Math.PI/2 && d.angle < 1.5 * Math.PI) {
return "end"
}
else {
return "start";
}
})
.attr("transform", function(d,i) {
if (d.angle > Math.PI/2 && d.angle < 1.5 * Math.PI) {
return "rotate(" + ((d.angle * 180) / Math.PI - 180) + ", 225, 225)";
}
else {
return "rotate(" + ((d.angle * 180) / Math.PI) + ", 225, 225)"
}
})
;
}
var draw = function () {
var radius = 100;
var nodes = createNodes();
createSvg(radius, function (svg) {
createElements(svg, nodes, 10);
});
}
$(document).ready(function () {
draw();
});
})();
* {
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 13px;
}
circle {
fill: steelblue;
fill-opacity: .8;
}
circle:hover {
fill: orange;
fill-opacity: .8;
}
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.v2.min.js"></script>
<div id="canvas"></div>
We're searching for a radial chart that looks like the following. What's most interesting about this chart is that it has the percentage. We've been searching for about three days and we haven't found anything that uses the d3.js library.
We found these two solid gauge charts, one from amcharts and the other from anycharts, but neither have the percentage as clear. Highcharts also has something similar, but not labels. Plus, they don't use d3 and not open-source.
Any help is appreciated.
Thanks.
It can be easily coded from scratch
It's possible to get coordinate of segment's end or start, using centroid function, after that you can add text here and rotate it accordingly
var data = [45,33,66,50,90]
var svg = d3.select('#result').append('svg').attr('width',500).attr('height',500)
var arcs = data.map((v,i)=>{
return d3.svg.arc().innerRadius(i*20+60).outerRadius((i+1)*20-5+60)
});
var pieData = data.map((v,i)=>{
return [{value:v*0.75,arc:arcs[i]},{value:(100-v)*0.75,arc:arcs[i]},{value:100*0.25,arc:arcs[i]}]
})
var pie = d3.layout.pie()
.sort(null)
.value(d=>d.value)
var g = svg.selectAll('g').data(pieData).enter().append('g').attr('transform','translate(250,250) rotate(180)').attr('fill-opacity',(d,i)=>2/(i+1))
// progress
g.selectAll('path').data(d=>{return pie(d)}).enter().append('path').attr('d',d=>{return d.data.arc(d)})
.attr('fill',(d,i)=>i==0?'blue':'none')
svg.selectAll('g').each(function(d){
var el = d3.select(this);
el.selectAll('path').each((r,i)=>{
if(i==1){
var centroid = r.data.arc.centroid({startAngle:r.startAngle+0.05,endAngle:r.startAngle+0.001+0.05})
g.append('text').text(100-Math.floor(r.value)+'%').attr('transform',`translate(${centroid[0]},${centroid[1]}) rotate(${180/Math.PI*(r.startAngle)+7})`).attr('alignment-baseline','middle')
}
})
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id='result'></div>
I have also need to this type chart.
My code is as following this make responsive graph according to parent width.
for it you need to get parent width and assign it to var width variable
Hope this code help you.
var width = 300;
var arcSize = (6 * width / 100);
var innerRadius = arcSize * 3;
var data = [
{value: 45, label: "label_1", color: '#ff0000'},
{value: 33, label: "label_2", color: '#00ff00'},
{value: 66, label: "label_3", color: '#0000ff'},
{value: 50, label: "label_4", color: '#ffff00'},
{value: 90, label: "label_5", color: '#ff0099'}
];
function render() {
var svg = d3.select('#result').append('svg').attr('width', width).attr('height', width);
var arcs = data.map(function (obj, i) {
return d3.svg.arc().innerRadius(i * arcSize + innerRadius).outerRadius((i + 1) * arcSize - (width / 100) + innerRadius);
});
var arcsGrey = data.map(function (obj, i) {
return d3.svg.arc().innerRadius(i * arcSize + (innerRadius + ((arcSize / 2) - 2))).outerRadius((i + 1) * arcSize - ((arcSize / 2)) + (innerRadius));
});
var pieData = data.map(function (obj, i) {
return [
{value: obj.value * 0.75, arc: arcs[i], object: obj},
{value: (100 - obj.value) * 0.75, arc: arcsGrey[i], object: obj},
{value: 100 * 0.25, arc: arcs[i], object: obj}];
});
var pie = d3.layout.pie().sort(null).value(function (d) {
return d.value;
});
var g = svg.selectAll('g').data(pieData).enter()
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + width / 2 + ') rotate(180)');
var gText = svg.selectAll('g.textClass').data([{}]).enter()
.append('g')
.classed('textClass', true)
.attr('transform', 'translate(' + width / 2 + ',' + width / 2 + ') rotate(180)');
g.selectAll('path').data(function (d) {
return pie(d);
}).enter().append('path')
.attr('id', function (d, i) {
if (i == 1) {
return "Text" + d.data.object.label
}
})
.attr('d', function (d) {
return d.data.arc(d);
}).attr('fill', function (d, i) {
return i == 0 ? d.data.object.color : i == 1 ? '#D3D3D3' : 'none';
});
svg.selectAll('g').each(function (d, index) {
var el = d3.select(this);
var path = el.selectAll('path').each(function (r, i) {
if (i === 1) {
var centroid = r.data.arc.centroid({
startAngle: r.startAngle + 0.05,
endAngle: r.startAngle + 0.001 + 0.05
});
var lableObj = r.data.object;
g.append('text')
.attr('font-size', ((5 * width) / 100))
.attr('dominant-baseline', 'central')
/*.attr('transform', "translate(" + centroid[0] + "," + (centroid[1] + 10) + ") rotate(" + (180 / Math.PI * r.startAngle + 7) + ")")
.attr('alignment-baseline', 'middle')*/
.append("textPath")
.attr("textLength", function (d, i) {
return 0;
})
.attr("xlink:href", "#Text" + r.data.object.label)
.attr("startOffset", '5')
.attr("dy", '-3em')
.text(lableObj.value + '%');
}
if (i === 0) {
var centroidText = r.data.arc.centroid({
startAngle: r.startAngle,
endAngle: r.startAngle
});
var lableObj = r.data.object;
gText.append('text')
.attr('font-size', ((5 * width) / 100))
.text(lableObj.label)
.attr('transform', "translate(" + (centroidText[0] - ((1.5 * width) / 100)) + "," + (centroidText[1] + ") rotate(" + (180) + ")"))
.attr('dominant-baseline', 'central');
}
});
});
}
render()
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="result"></div>
I am working with D3.js now and even though I found very similar cases I am not really able to put them together and move on.
I have something similar as a radar chart and I would like to append to each ax I create (number of axes is not fixed could be 4, but also 40) text, which I already have, but rotate the text around center point and turn them as soon as they reach 180 degrees, actually 0 degrees.
The result should look like this:
What I have now is this:
I only know how to reach this within arc, which is also nicely shown here, but I did not really figure it out for my case.
This is my code snippet, where I append those text criteria:
//Write criterias
axis.append("text")
.attr("class","labels")
.attr("font-size","12px")
.attr("font-family","Montserrat")
.attr("text-anchor","middle")
.attr("fill","white")
.attr("x",function (d, i) {
return radius * Math.cos(angleSlice * i - Math.PI/2)*options.circles.labelFactor;
})
.attr("y",function (d, i) {
return radius * Math.sin(angleSlice * i - Math.PI/2)*options.circles.labelFactor;
})
.text(function (d) {
return d;
});
EDIT:
Here is my fiddle: https://jsfiddle.net/fsb47ndf/
Thank you for any advise
Some people find it difficult to rotate an SVG element, because the rotate function of the transform attribute rotates the element around the origin (0,0), not around its center:
If optional parameters and are not supplied, the rotate is about the origin of the current user coordinate system (source)
Thus, an easy option is dropping the x and the y attributes of the texts, and positioning them using transform. That way, we can easily calculate the rotation:
.attr("transform", function(d, i) {
var rotate = angleSlice * i > Math.PI / 2 ?
(angleSlice * i * 180 / Math.PI) - 270 :
(angleSlice * i * 180 / Math.PI) - 90;
return "translate(" + radius * Math.cos(angleSlice * i - Math.PI / 2) * options.circles.labelFactor +
"," + radius * Math.sin(angleSlice * i - Math.PI / 2) * options.circles.labelFactor +
") rotate(" + rotate + ")"
})
Here is your code:
data = [{
name: 'DATA1',
value: 22,
}, {
name: 'DATA2',
value: 50,
}, {
name: 'DATA3',
value: 0,
}, {
name: 'DATA4',
value: 24,
}, {
name: 'DATA5',
value: 22,
}, {
name: 'DATA6',
value: 30,
}, {
name: 'DATA7',
value: 20,
}, {
name: 'DATA8',
value: 41,
}, {
name: 'DATA9',
value: 31,
}, {
name: 'DATA10',
value: 30,
}, {
name: 'DATA11',
value: 30,
}, {
name: 'DATA12',
value: 30,
}, {
name: 'DATA13',
value: 30,
}, {
name: 'DATA14',
value: 30,
}, ];
var options = {
width: 600,
height: 600,
margins: {
top: 100,
right: 100,
bottom: 100,
left: 100
},
circles: {
levels: 6,
maxValue: 100,
labelFactor: 1.15,
opacity: 0.2,
},
};
var allAxis = (data.map(function(i, j) {
return i.name
})),
total = allAxis.length,
radius = Math.min(options.width / 2, options.height / 2),
angleSlice = Math.PI * 2 / total,
Format = d3.format('');
var rScale = d3.scale.linear()
.domain([0, options.circles.maxValue])
.range([50, radius]);
var svg = d3.select("body").append("svg")
.attr("width", options.width + options.margins.left + options.margins.right)
.attr("height", options.height + options.margins.top + options.margins.bottom);
var g = svg.append("g")
.attr("transform", "translate(" + (options.width / 2 + options.margins.left) + "," + (options.height / 2 + options.margins.top) + ")");
var axisGrid = g.append("g")
.attr("class", "axisWraper");
var axis = axisGrid.selectAll(".axis")
.data(allAxis)
.enter()
.append("g")
.attr("class", "axis")
//append them lines
axis.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", function(d, i) {
var tempX2 = radius * Math.cos(angleSlice * i - Math.PI / 2);
return tempX2;
})
.attr("y2", function(d, i) {
var tempY = radius * Math.sin(angleSlice * i - Math.PI / 2);
return tempY;
})
.attr("class", "line")
.attr("stroke", "black")
.attr("fill", "none");
//Draw background circles
axisGrid.selectAll(".levels")
.data([6, 5, 4, 3, 2, 1])
.enter()
.append("circle")
.attr("class", "gridCircle")
.attr("r", function(d, i) {
return parseInt(radius / options.circles.levels * d, 10);
})
.attr("stroke", "black")
.attr("fill-opacity", options.circles.opacity);
//Write data
axis.append("text")
.attr("class", "labels")
.attr("font-size", "12px")
.attr("font-family", "Montserrat")
.attr("text-anchor", "middle")
.attr("fill", "black")
.attr("transform", function(d, i) {
var rotate = angleSlice * i > Math.PI ? (angleSlice * i * 180 / Math.PI) - 270 : (angleSlice * i * 180 / Math.PI) - 90;
return "translate(" + radius * Math.cos(angleSlice * i - Math.PI / 2) * options.circles.labelFactor + "," + radius * Math.sin(angleSlice * i - Math.PI / 2) * options.circles.labelFactor + ") rotate(" + rotate + ")"
})
.text(function(d) {
return d;
});
<script src="https://d3js.org/d3.v3.min.js"></script>
Like already mentioned by Gerardo Furtado in his answer life can get easier if you ditch your x and y attributes in favor of doing all positioning and rotation via the transform attribute. However, you can take his approach even a step further by letting the browser do all the trigonometry. All you need to do is specify a list of appropriate transform definitions.
.attr("transform", function(d, i) {
var angleI = angleSlice * i * 180 / Math.PI - 90; // the angle to rotate the label
var distance = radius * options.circles.labelFactor; // the distance from the center
var flip = angleI > 90 ? 180 : 0; // 180 if label needs to be flipped
return "rotate(" + angleI + ") translate(" + distance + ")" + "rotate(" + flip + ")");
// ^1.^ ^2.^ ^3.^
})
If you omit the x and y attributes, they will default to 0, which means, that the texts will all start off at the origin. From there it is easy to move and rotate them to their final position applying just three transformations:
rotate the texts to the angle according to their position on the perimeter
translate the rotated texts outwards to their final position
Flip the texts on the left hand side of the circle using another rotate.
Have a look at the following snippet for a working demo:
data = [{
name: 'DATA1',
value: 22,
},
{
name: 'DATA2',
value: 50,
},
{
name: 'DATA3',
value: 0,
},
{
name: 'DATA4',
value: 24,
},
{
name: 'DATA5',
value: 22,
},
{
name: 'DATA6',
value: 30,
},
{
name: 'DATA7',
value: 20,
},
{
name: 'DATA8',
value: 41,
},
{
name: 'DATA9',
value: 31,
},
{
name: 'DATA10',
value: 30,
},
{
name: 'DATA11',
value: 30,
},
{
name: 'DATA12',
value: 30,
},
{
name: 'DATA13',
value: 30,
},
{
name: 'DATA14',
value: 30,
},
];
var options = {
width: 600,
height: 600,
margins: {
top: 100,
right: 100,
bottom: 100,
left: 100
},
circles: {
levels: 6,
maxValue: 100,
labelFactor: 1.15,
opacity: 0.2,
},
};
var allAxis = (data.map(function(i, j) {
return i.name
})),
total = allAxis.length,
radius = Math.min(options.width / 2, options.height / 2),
angleSlice = Math.PI * 2 / total,
Format = d3.format('');
var rScale = d3.scale.linear()
.domain([0, options.circles.maxValue])
.range([50, radius]);
var svg = d3.select("body").append("svg")
.attr("width", options.width + options.margins.left + options.margins.right)
.attr("height", options.height + options.margins.top + options.margins.bottom);
var g = svg.append("g")
.attr("transform", "translate(" + (options.width / 2 + options.margins.left) + "," + (options.height / 2 + options.margins.top) + ")");
var axisGrid = g.append("g")
.attr("class", "axisWraper");
var axis = axisGrid.selectAll(".axis")
.data(allAxis)
.enter()
.append("g")
.attr("class", "axis")
//append them lines
axis.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", function(d, i) {
var tempX2 = radius * Math.cos(angleSlice * i - Math.PI / 2);
return tempX2;
})
.attr("y2", function(d, i) {
var tempY = radius * Math.sin(angleSlice * i - Math.PI / 2);
return tempY;
})
.attr("class", "line")
.attr("stroke", "black")
.attr("fill", "none");
//Draw background circles
axisGrid.selectAll(".levels")
.data([6, 5, 4, 3, 2, 1])
.enter()
.append("circle")
.attr("class", "gridCircle")
.attr("r", function(d, i) {
return parseInt(radius / options.circles.levels * d, 10);
})
.attr("stroke", "black")
.attr("fill-opacity", options.circles.opacity);
//Write data
axis.append("text")
.attr("class", "labels")
.attr("font-size", "12px")
.attr("font-family", "Montserrat")
.attr("text-anchor", "middle")
.attr("fill", "black")
.attr("dy", ".35em")
.attr("transform", function(d, i) {
var angleI = angleSlice * i * 180 / Math.PI - 90; // the angle to rotate the label
var distance = radius * options.circles.labelFactor; // the distance from the center
var flip = angleI > 90 ? 180 : 0; // 180 if label needs to be flipped
return "rotate(" + angleI + ") translate(" + distance + ")" + "rotate(" + flip + ")"
})
.text(function(d) {
console.log(d);
return d;
});
<script src="https://d3js.org/d3.v3.js"></script>
You can use something like this to rotate all the labels. You probably have to adjust the positioning and rotation angle based on exactly how you want it.
var angle = 180;
svg.selectAll(".labels")
.attr("transform", "translate(300,0) rotate("+angle+")");
I am trying to arrange the squares around the circle but i am unable to get the correct output.
Can any one help me?
// largely based on http://bl.ocks.org/4063550
// some made-up data
var data = [2,2,2,2,2,2];
// tree-ify our fake data
var dataTree = {
children: data.map(function(d) { return { size: d }; })
};
// basic settings
var w = 300,
h = 300,
maxRadius = 75;
// size scale for data
var radiusScale = d3.scale.sqrt().domain([0, d3.max(data)]).range([0, maxRadius]);
// determine the appropriate radius for the circle
var roughCircumference = d3.sum(data.map(radiusScale)) * 2,
radius = roughCircumference / (Math.PI * 2);
// make a radial tree layout
var tree = d3.layout.tree()
.size([360, radius])
.separation(function(a, b) {
return radiusScale(a.size) + radiusScale(b.size);
});
// make the svg
var svg = d3.select("body").append("svg")
.attr("width", w )
.attr("height", h )
.append("g")
.attr("transform", "translate(" + (w / 2 ) + "," + (h /2) + ")");
var c = svg.append('circle').attr({r:75})
// apply the layout to the data
var nodes = tree.nodes(dataTree);
// create dom elements for the node
var node = svg.selectAll(".node")
.data(nodes.slice(1)) // cut out the root node, we don't need it
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
console.log(d.x);
return "rotate(" + (d.x - 90) + ") translate(" + d.y + ")";
})
node.append("rect")
.attr({
width: 25,
height:25,
fill : 'red',
"transform":function(d) {
return "rotate(" + (-1 * d.x + 90) + ") translate(" +0+ ")";
}
});
node.append("text")
.attr({"transform":function(d) {
return "rotate(" + (-1 * d.x + 90) + ")";
},
"text-anchor": "middle"
})
.text("testing a word");
svg {
border:1px solid gray;
}
circle {
fill: steelblue;
stroke: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I am looking the output like this:
A sample code to work with.
I have assumed 8 nodes to be plotted so that the circle can be divided into 8 segments. Each square to placed at the distance of Pi/4 radians. You can compute the x,y as xSin , y Cos. Then you will need to transform the rectangle to centre at x,y rather than the top left corner.
// largely based on http://bl.ocks.org/4063550
// some made-up data
var data = [2,2,2,2,2,2,2,2];
// tree-ify our fake data
var dataTree = {
children: data.map(function(d) { return { size: d }; })
};
// basic settings
var w = 300,
h = 300,
maxRadius = 75;
// size scale for data
var radiusScale = d3.scale.sqrt().domain([0, d3.max(data)]).range([0, maxRadius]);
// determine the appropriate radius for the circle
var roughCircumference = d3.sum(data.map(radiusScale)) * 2,
radius = roughCircumference / (Math.PI * 2);
// make a radial tree layout
var tree = d3.layout.tree()
.size([360, radius])
.separation(function(a, b) {
return radiusScale(a.size) + radiusScale(b.size);
});
// make the svg
var svg = d3.select("body").append("svg")
.attr("width", w )
.attr("height", h )
.append("g")
.attr("transform", "translate(" + (w / 2 ) + "," + (h /2) + ")");
var c = svg.append('circle').attr({r:75})
var r = 75;
// apply the layout to the data
var nodes = tree.nodes(dataTree);
// create dom elements for the node
var node = svg.selectAll(".node")
.data(nodes.slice(1)) // cut out the root node, we don't need it
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d,i) {
return "translate(" + (r * Math.sin(Math.PI * i * 0.25)) + "," + (r * Math.cos(Math.PI * i * 0.25)) + ")";
})
node.append("rect")
.attr({
width: 25,
height:25,
fill : 'red',
"transform":function(d) {
return "translate(" +(-12.5)+ ","+ (-12.5) + ")";
}
});
node.append("text")
.attr({"transform":function(d) {
return "rotate(" + (-1 * d.x + 90) + ")";
},
"text-anchor": "middle"
})
.text("testing a word");
svg {
border:1px solid gray;
}
circle {
fill: steelblue;
stroke: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Here's a fiddle.
Okay, so this was basically some pixel level trial-and-error translate manipulation. This is for node.
.attr("transform", function(d) {
console.log(d.x);
return "rotate(" + (d.x - 90) + ") translate(" + (d.y - 65 ) + ")";
})
and this for rect:
.attr({
width: 25,
height:25,
fill : 'red',
"transform":function(d) {
return "rotate(" + -(d.x - 90) + ") translate(" +(-10)+ ","+ (-10) + ")";
}
});
I'm drawing a pie chart using D3.js with a quite simple script. The problem is that when slices are small, their labels overlap.
What options do I have to prevent them from overlapping? Does D3.js have built-in mechanisms I could exploit?
Demo: http://jsfiddle.net/roxeteer/JTuej/
var container = d3.select("#piechart");
var data = [
{ name: "Group 1", value: 1500 },
{ name: "Group 2", value: 500 },
{ name: "Group 3", value: 100 },
{ name: "Group 4", value: 50 },
{ name: "Group 5", value: 20 }
];
var width = 500;
var height = 500;
var radius = 150;
var textOffset = 14;
var color = d3.scale.category20();
var svg = container.append("svg:svg")
.attr("width", width)
.attr("height", height);
var pie = d3.layout.pie().value(function(d) {
return d.value;
});
var arc = d3.svg.arc()
.outerRadius(function(d) { return radius; });
var arc_group = svg.append("svg:g")
.attr("class", "arc")
.attr("transform", "translate(" + (width/2) + "," + (height/2) + ")");
var label_group = svg.append("svg:g")
.attr("class", "arc")
.attr("transform", "translate(" + (width/2) + "," + (height/2) + ")");
var pieData = pie(data);
var paths = arc_group.selectAll("path")
.data(pieData)
.enter()
.append("svg:path")
.attr("stroke", "white")
.attr("stroke-width", 0.5)
.attr("fill", function(d, i) { return color(i); })
.attr("d", function(d) {
return arc({startAngle: d.startAngle, endAngle: d.endAngle});
});
var labels = label_group.selectAll("path")
.data(pieData)
.enter()
.append("svg:text")
.attr("transform", function(d) {
return "translate(" + Math.cos(((d.startAngle + d.endAngle - Math.PI) / 2)) * (radius + textOffset) + "," + Math.sin((d.startAngle + d.endAngle - Math.PI) / 2) * (radius + textOffset) + ")";
})
.attr("text-anchor", function(d){
if ((d.startAngle +d.endAngle) / 2 < Math.PI) {
return "beginning";
} else {
return "end";
}
})
.text(function(d) {
return d.data.name;
});
D3 doesn't offer anything built-in that does this, but you can do it by, after having added the labels, iterating over them and checking if they overlap. If they do, move one of them.
var prev;
labels.each(function(d, i) {
if(i > 0) {
var thisbb = this.getBoundingClientRect(),
prevbb = prev.getBoundingClientRect();
// move if they overlap
if(!(thisbb.right < prevbb.left ||
thisbb.left > prevbb.right ||
thisbb.bottom < prevbb.top ||
thisbb.top > prevbb.bottom)) {
var ctx = thisbb.left + (thisbb.right - thisbb.left)/2,
cty = thisbb.top + (thisbb.bottom - thisbb.top)/2,
cpx = prevbb.left + (prevbb.right - prevbb.left)/2,
cpy = prevbb.top + (prevbb.bottom - prevbb.top)/2,
off = Math.sqrt(Math.pow(ctx - cpx, 2) + Math.pow(cty - cpy, 2))/2;
d3.select(this).attr("transform",
"translate(" + Math.cos(((d.startAngle + d.endAngle - Math.PI) / 2)) *
(radius + textOffset + off) + "," +
Math.sin((d.startAngle + d.endAngle - Math.PI) / 2) *
(radius + textOffset + off) + ")");
}
}
prev = this;
});
This checks, for each label, if it overlaps with the previous label. If this is the case, a radius offset is computed (off). This offset is determined by half the distance between the centers of the text boxes (this is just a heuristic, there's no specific reason for it to be this) and added to the radius + text offset when recomputing the position of the label as originally.
The maths is a bit involved because everything needs to be checked in two dimensions, but it's farily straightforward. The net result is that if a label overlaps a previous label, it is pushed further out. Complete example here.
#LarsKotthoff
Finally I have solved the problem. I have used stack approach to display the labels. I made a virtual stack on both left and right side. Based the angle of the slice, I allocated the stack-row. If stack row is already filled then I find the nearest empty row on both top and bottom of desired row. If no row found then the value (on the current side) with least share angle is removed from the stack and labels are adjust accordingly.
See the working example here:
http://manicharts.com/#/demosheet/3d-donut-chart-smart-labels
The actual problem here is one of label clutter.
So, you could try not displaying labels for very narrow arcs:
.text(function(d) {
if(d.endAngle - d.startAngle<4*Math.PI/180){return ""}
return d.data.key; });
This is not as elegant as the alternate solution, or codesnooker's resolution to that issue, but might help reduce the number of labels for those who have too many. If you need labels to be able to be shown, a mouseover might do the trick.
For small angles(less than 5% of the Pie Chart), I have changed the centroid value for the respective labels. I have used this code:
arcs.append("text")
.attr("transform", function(d,i) {
var centroid_value = arc.centroid(d);
var pieValue = ((d.endAngle - d.startAngle)*100)/(2*Math.PI);
var accuratePieValue = pieValue.toFixed(0);
if(accuratePieValue <= 5){
var pieLableArc = d3.svg.arc().innerRadius(i*20).outerRadius(outer_radius + i*20);
centroid_value = pieLableArc.centroid(d);
}
return "translate(" + centroid_value + ")";
})
.text(function(d, i) { ..... });