D3.js rotate axis labels around the middle point - javascript

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+")");

Related

D3 - add labels to polar plot

I found this working example how to create a polar plot with D3. What I can not figure out is how to add labels (compass labels like N, E, S , W) to the principal axes outside of the plot.
Also scale labels for the distance from the radius inside the plot. All I do is end up overlapping and rotated into seems arbitrary directions.
I'd just create a dummy data structure, with the key being the name, and the value being the angle I want it at. Then use some basic trigonometry to position the nodes correctly.
var width = 960,
height = 500,
radius = Math.min(width, height) / 2 - 30;
var r = d3.scale.linear()
.domain([0, 1])
.range([0, radius]);
var line = d3.svg.line.radial()
.radius(function(d) {
return r(d[1]);
})
.angle(function(d) {
return -d[0] + Math.PI / 2;
});
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var gr = svg.append("g")
.attr("class", "r axis")
.selectAll("g")
.data(r.ticks(3).slice(1))
.enter().append("g");
gr.append("circle")
.attr("r", r);
var ga = svg.append("g")
.attr("class", "a axis")
.selectAll("g")
.data(d3.range(0, 360, 30))
.enter().append("g")
.attr("transform", function(d) {
return "rotate(" + -d + ")";
});
ga.append("line")
.attr("x2", radius);
var color = d3.scale.category20();
var line = d3.svg.line.radial()
.radius(function(d) {
return r(d[1]);
})
.angle(function(d) {
return -d[0] + Math.PI / 2;
});
var data = [
[Math.PI / 3, Math.random()],
[Math.PI / 6, Math.random()],
[0 * Math.PI, Math.random()],
[(11 * Math.PI) / 6, Math.random()],
[(5 * Math.PI / 3), Math.random()],
[(3 * Math.PI) / 2, Math.random()],
[(4 * Math.PI / 3), Math.random()],
[(7 * Math.PI) / 6, Math.random()],
[Math.PI, Math.random()],
[(5 * Math.PI) / 6, Math.random()],
[(2 * Math.PI) / 3, Math.random()],
[Math.PI / 2, Math.random()]
];
var angles = {
N: 0,
E: Math.PI / 2,
S: Math.PI,
W: 3 * Math.PI / 2,
};
svg.selectAll("point")
.data(data)
.enter()
.append("circle")
.attr("class", "point")
.attr("transform", function(d) {
var coors = line([d]).slice(1).slice(0, -1);
return "translate(" + coors + ")"
})
.attr("r", 8)
.attr("fill", function(d, i) {
return color(i);
});
svg.selectAll(".angle")
.data(Object.keys(angles))
.enter()
.append("text")
.attr("class", "angle")
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("transform", function(d) {
// Subtract because 0degrees is up at (0, 1) on the unit circle, but
// 0 radians is to the right, at (1, 0)
var angle = angles[d] - Math.PI / 2;
var textRadius = radius + 20;
var x = Math.cos(angle) * textRadius;
var y = Math.sin(angle) * textRadius;
return "translate(" + [x, y] + ")";
})
.text(function(d) { return d; })
.frame {
fill: none;
stroke: #000;
}
.axis text {
font: 10px sans-serif;
}
.axis line,
.axis circle {
fill: none;
stroke: steelblue;
stroke-dasharray: 4;
}
.axis:last-of-type circle {
stroke: steelblue;
stroke-dasharray: none;
}
.line {
fill: none;
stroke: orange;
stroke-width: 3px;
}
<script src="//d3js.org/d3.v3.min.js"></script>

d3js : how to put circles at the end of an arc

I'm trying to create a donut chart in d3js where each arc has a circle at its end.
Circle's edge must fit on arc's one.
I tried both by appending a circle and a circle wrapped in marker but with no succes.
Trying to append a marker seems to be the closest solution to the desired one but I can't help the marker oveflowing the arc edges.
Code:
var data = [
{
name: "punti",
count: 3,
color: "#fff000"
},
{
name: "max",
count: 7,
color: "#f8b70a"
}
];
var totalCount = data.reduce((acc, el) => el.count + acc, 0);
var image_width = 32;
var image_height = 32;
var width = 540,
height = 540,
radius = 200,
outerRadius = radius - 10,
innerRadius = 100;
var cornerRadius = innerRadius;
var markerRadius = (outerRadius - innerRadius) / 2;
var arc = d3
.arc()
.outerRadius(outerRadius)
.innerRadius(innerRadius)
.cornerRadius(cornerRadius);
var pie = d3
.pie()
.sort(null)
.value(function(d) {
return d.count;
});
var svg = d3
.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var pieData = pie(data);
var g = svg
.selectAll(".arc")
.data(pieData)
.enter()
.append("g");
var path = g
.append("path")
.attr("d", arc)
.style("fill", function(d, i) {
return d.data.color;
});
var marker = svg
.append("defs")
.append("marker")
.attr("id", "endmarker")
.attr("overflow", "visible")
.append("circle")
.attr("cy", 0)
.attr("cx", 0)
.attr("r", markerRadius)
.attr("fill", "red");
g.attr("marker-end", "url(#endmarker)");
g
.append("circle")
.attr("cx", function(d) {
let path = d3.select(this.parentNode);
var x = arc.centroid(d)[0];
return x;
})
.attr("cy", function(d) {
var y = arc.centroid(d)[1];
console.log(d3.select(this).attr("cx"));
return y;
})
.attr("fill", d => d.data.color)
.attr("stroke", "black")
.attr("r", (outerRadius - innerRadius) / 2);
codepen here
Thanks to anyone who will help!
Assuming that you want your output like:
I found some code from Mike Bostock's Block here which shows how to add circles to rounded Arc Corners.
I adapted the following code for you which performs quite a bit of complex mathematics.
var cornerRadius = (outerRadius - innerRadius)/2;
svg.append("g")
.style("stroke", "#555")
.style("fill", "none")
.attr("class", "corner")
.selectAll("circle")
.data(d3.merge(pieData.map(function(d) {
return [
{angle: d.startAngle + d.padAngle / 2, radius: outerRadius - cornerRadius, start: +1},
{angle: d.endAngle - d.padAngle / 2, radius: outerRadius - cornerRadius, start: -1},
];
})))
.enter().append("circle")
.attr("cx", function(d) { return d.start * cornerRadius * Math.cos(d.angle) + Math.sqrt(d.radius * d.radius - cornerRadius * cornerRadius) * Math.sin(d.angle); })
.attr("cy", function(d) { return d.start * cornerRadius * Math.sin(d.angle) - Math.sqrt(d.radius * d.radius - cornerRadius * cornerRadius) * Math.cos(d.angle); })
.attr("r", cornerRadius);
Full snippet showing the output:
<div id="chart"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.12.0/d3.min.js"></script>
<script>
var data = [
{
name: "punti",
count: 3,
color: "#fff000"
},
{
name: "max",
count: 7,
color: "#f8b70a"
},
];
var totalCount = data.reduce((acc, el) => el.count + acc, 0);
var image_width = 32;
var image_height = 32;
var width = 540,
height = 540,
radius = 200,
outerRadius = radius - 10,
innerRadius = 100;
var cornerRadius = innerRadius;
var markerRadius = (outerRadius - innerRadius) / 2;
var arc = d3
.arc()
.outerRadius(outerRadius)
.innerRadius(innerRadius)
.cornerRadius(cornerRadius);
var pie = d3
.pie()
.sort(null)
.value(function(d) {
return d.count;
});
var svg = d3
.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var pieData = pie(data);
var g = svg
.selectAll(".arc")
.data(pieData)
.enter()
.append("g");
var path = g
.append("path")
.attr("d", arc)
.style("fill", function(d, i) {
return d.data.color;
});
var cornerRadius = (outerRadius - innerRadius)/2;
svg.append("g")
.style("stroke", "#555")
.style("fill", "none")
.attr("class", "corner")
.selectAll("circle")
.data(d3.merge(pieData.map(function(d) {
return [
{angle: d.startAngle + d.padAngle / 2, radius: outerRadius - cornerRadius, start: +1},
{angle: d.endAngle - d.padAngle / 2, radius: outerRadius - cornerRadius, start: -1},
];
})))
.enter().append("circle")
.attr("cx", function(d) { return d.start * cornerRadius * Math.cos(d.angle) + Math.sqrt(d.radius * d.radius - cornerRadius * cornerRadius) * Math.sin(d.angle); })
.attr("cy", function(d) { return d.start * cornerRadius * Math.sin(d.angle) - Math.sqrt(d.radius * d.radius - cornerRadius * cornerRadius) * Math.cos(d.angle); })
.attr("r", cornerRadius);
</script>

D3 Donut Chart with Connectors

I'm trying to create a static d3 donut chart with labels and connectors from a json object. I've been able to get it to work with an array in this fiddle but can't get the connectors or label text to appear with the data object that I need.
The donut chart is working and the labels are appearing with the percentages, but I need them to appear with the labels and connectors. I think that it has something to do with the way that I am trying to map the connectors but can't figure out the error.
Code is below and also here is a link to a working fiddle: https://jsfiddle.net/hef1u71o/
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
var data = [{
percentage: 19,
label: 'Consulting'
},{
percentage: 3,
label: 'Consumer Goods'
},{
percentage: 5,
label: 'Energy/Chemical/Gas'
},{
percentage: 3,
label: 'Entrepreneurship'
},{
percentage: 1,
label: 'Environment & Sustainability'
},{
percentage: 19,
label: 'Financial Services'
},{
percentage: 3,
label: 'General Management'
},{
percentage: 6,
label: 'Government'
},{
percentage: 7,
label: 'Hospital/Health Care/Health Services'
},{
percentage: 2,
label: 'Human Resources'
},{
percentage: 4,
label: 'IT'
},{
percentage: 2,
label: 'International Development'
},{
percentage: 3,
label: 'Manufacturing/Operations'
},{
percentage: 4,
label: 'Marketing/PR/Advertising'
},{
percentage: 1,
label: 'Media/Sports/Entertainment'
},{
percentage: 7,
label: 'Nonprofit/Education/Special Org.'
},{
percentage: 6,
label: 'Other'
},{
percentage: 2,
label: 'Research & Development'
},{
percentage: 4,
label: 'Sales/Business Development'
},];
var width = 300,
height = 300,
radius = Math.min(width, height) / 2;
var color = d3.scale.ordinal()
.range(["#243668", "#2b7eb4", "#186b97", "#6391a1", "#d2c5b7", "#9c9286", "#5b5b59"]);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.percentage; });
var arc = d3.svg.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("path")
.data(pie(data))
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc);
svg.selectAll("text").data(pie(data))
.enter()
.append("text")
.attr("text-anchor", "middle")
.attr("x", function(d) {
var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
d.cx = Math.cos(a) * (radius - 75);
return d.x = Math.cos(a) * (radius - 20);
})
.attr("y", function(d) {
var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
d.cy = Math.sin(a) * (radius - 75);
return d.y = Math.sin(a) * (radius - 20);
})
.text(function(d) { return d.value; })
.each(function(d) {
var bbox = this.getBBox();
d.sx = d.x - bbox.width/2 - 2;
d.ox = d.x + bbox.width/2 + 2;
d.sy = d.oy = d.y + 5;
});
svg.append("defs").append("marker")
.attr("id", "circ")
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("refX", 3)
.attr("refY", 3)
.append("circle")
.attr("cx", 3)
.attr("cy", 3)
.attr("r", 3);
svg.selectAll("path.pointer").data(pie(data)).enter()
.append("path")
.attr("class", "pointer")
.style("fill", "none")
.style("stroke", "black")
.attr("marker-end", "url(#circ)")
.attr("d", function(d) {
if(d.cx > d.ox) {
return "M" + d.sx + "," + d.sy + "L" + d.ox + "," + d.oy + " " + d.cx + "," + d.cy;
} else {
return "M" + d.ox + "," + d.oy + "L" + d.sx + "," + d.sy + " " + d.cx + "," + d.cy;
}
});
</script>
</body>
</html>
Save your data into variable:
var pieData = pie(data);
And use this variable here:
svg.selectAll("text").data(pieData)
.enter()
.append("text")
.attr("text-anchor", "middle")
.attr("x", function(d) {
var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
d.cx = Math.cos(a) * (radius - 75);
return d.x = Math.cos(a) * (radius - 20);
})
.attr("y", function(d) {
var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
d.cy = Math.sin(a) * (radius - 75);
return d.y = Math.sin(a) * (radius - 20);
})
.text(function(d) { return d.value; })
.each(function(d) { // !!! you extent the dataset here
var bbox = this.getBBox();
d.sx = d.x - bbox.width/2 - 2;
d.ox = d.x + bbox.width/2 + 2;
d.sy = d.oy = d.y + 5;
});
and here:
svg.selectAll("path.pointer").data(pieData).enter()
.append("path")
.attr("class", "pointer")
...
It's important because of you extend the data (see each method). You will use extended properties for calculating of connectors position and you should use the same dataset for both cases.
Check working demo.

How to rotate text around its centroid (vertically flip) in SVG / D3?

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>

Append svg icon always right behind the text

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/
&#xf014 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.

Categories