javascript 带有角度/水平标签的 d3.js 饼图
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/21769872/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
d3.js pie chart with angled/horizontal labels
提问by The Old County
I'm working on a pie chart mock. That I need to try and match the designs to have the label extruding out with a horizontal line attached to the slice ticks. Is this possible? It would be a bonus to have the black dots form on the segments.
我正在制作饼图模拟。我需要尝试匹配设计以使标签以水平线连接到切片刻度上。这可能吗?在段上形成黑点将是一个奖励。
Here is the code for the tick marks. Would it be a case of creating another set of lines that intersect?
这是刻度线的代码。是否会创建另一组相交的线?
//draw tick marks
var label_group = d3.select('#'+pieId+' .label_group');
lines = label_group.selectAll("line").data(filteredData);
lines.enter().append("svg:line")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", function(d){
if(d.value > threshold){
return -that.r-3;
}else{
return -that.r;
}
})
.attr("y2", function(d){
if(d.value > threshold){
return -that.r-8;
}
else{
return -that.r;
}
})
.attr("stroke", "gray")
.attr("transform", function(d) {
return "rotate(" + (d.startAngle+d.endAngle)/2 * (180/Math.PI) + ")";
});
lines.transition()
.duration(this.tweenDuration)
.attr("transform", function(d) {
return "rotate(" + (d.startAngle+d.endAngle)/2 * (180/Math.PI) + ")";
});
lines.exit().remove();
采纳答案by The Old County
To conclude I've wrapped the very latest code for this in a jquery plugin. Its now possible to develop multiple pie charts with these labels.
总而言之,我已经在 jquery 插件中包装了最新的代码。现在可以使用这些标签开发多个饼图。
LATEST CODE - ** http://jsfiddle.net/Qh9X5/1336/- removes label properly on exit.
最新代码 - ** http://jsfiddle.net/Qh9X5/1336/- 在退出时正确删除标签。
$(document).ready(function() {
(function( $ ){
var methods = {
el: "",
init : function(options) {
var clone = jQuery.extend(true, {}, options["data"]);
methods.el = this;
methods.setup(clone, options["width"], options["height"], options["r"], options["ir"]);
},
getArc: function(radius, innerradius){
var arc = d3.svg.arc()
.innerRadius(innerradius)
.outerRadius(radius);
return arc;
},
setup: function(dataset, w, h, r, ir){
var padding = 80;
this.width = w;
this.height = h;
this.radius = r
this.innerradius = ir;
this.color = d3.scale.category20();
this.pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.total; });
this.arc = this.getArc(this.radius, this.innerradius);
this.svg = d3.select(methods.el["selector"]).append("svg")
.attr("width", this.width + padding)
.attr("height", this.height + padding)
.append("g")
.attr("class", "piechart")
.attr("transform", "translate(" + ((this.width/2) + (padding/2)) + "," + ((this.height/2) + (padding/2)) + ")");
this.segments = this.svg.append("g")
.attr("class", "segments");
this.labels = this.svg.append("g")
.attr("class", "labels");
this.pointers = this.svg.append("g")
.attr("class", "pointers");
},
oldPieData: "",
pieTween: function(r, ir, d, i){
var that = this;
var theOldDataInPie = methods.oldPieData;
// Interpolate the arcs in data space
var s0;
var e0;
if(theOldDataInPie[i]){
s0 = theOldDataInPie[i].startAngle;
e0 = theOldDataInPie[i].endAngle;
} else if (!(theOldDataInPie[i]) && theOldDataInPie[i-1]) {
s0 = theOldDataInPie[i-1].endAngle;
e0 = theOldDataInPie[i-1].endAngle;
} else if(!(theOldDataInPie[i-1]) && theOldDataInPie.length > 0){
s0 = theOldDataInPie[theOldDataInPie.length-1].endAngle;
e0 = theOldDataInPie[theOldDataInPie.length-1].endAngle;
} else {
s0 = 0;
e0 = 0;
}
var i = d3.interpolate({startAngle: s0, endAngle: e0}, {startAngle: d.startAngle, endAngle: d.endAngle});
return function(t) {
var b = i(t);
return methods.getArc(r, ir)(b);
};
},
removePieTween: function(r, ir, d, i) {
var that = this;
s0 = 2 * Math.PI;
e0 = 2 * Math.PI;
var i = d3.interpolate({startAngle: d.startAngle, endAngle: d.endAngle}, {startAngle: s0, endAngle: e0});
return function(t) {
var b = i(t);
return methods.getArc(r, ir)(b);
};
},
update: function(dataSet){
var that = this;
methods.el = this;
var r = $(methods.el["selector"]).data("r");
var ir = $(methods.el["selector"]).data("ir");
methods.svg = d3.select(methods.el["selector"] + " .piechart");
methods.segments = d3.select(methods.el["selector"] + " .segments");
methods.labels = d3.select(methods.el["selector"] + " .labels");
methods.pointers = d3.select(methods.el["selector"] + " .pointers");
dataSet.forEach(function(d) {
d.total = +d.value;
});
this.piedata = methods.pie(dataSet);
//__slices
this.path = methods.segments.selectAll("path.pie")
.data(this.piedata);
this.path.enter().append("path")
.attr("class", "pie")
.attr("fill", function(d, i) {
return methods.color(i);
})
.transition()
.duration(300)
.attrTween("d", function(d, i) {
return methods.pieTween(r, ir, d, i);
});
this.path
.transition()
.duration(300)
.attrTween("d", function(d, i) {
return methods.pieTween(r, ir, d, i);
});
this.path.exit()
.transition()
.duration(300)
.attrTween("d", function(d, i) {
return methods.removePieTween(r, ir, d, i);
})
.remove();
//__slices
//__labels
var labels = methods.labels.selectAll("text")
.data(this.piedata);
labels.enter()
.append("text")
.attr("text-anchor", "middle")
labels
.attr("x", function(d) {
var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
d.cx = Math.cos(a) * (ir+((r-ir)/2));
return d.x = Math.cos(a) * (r + 20);
})
.attr("y", function(d) {
var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
d.cy = Math.sin(a) * (ir+((r-ir)/2));
return d.y = Math.sin(a) * (r + 20);
})
.text(function(d) {
return d.data.label;
})
.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;
})
.transition()
.duration(300)
labels
.transition()
.duration(300)
labels.exit()
.transition()
.duration(300)
//__labels
//__pointers
methods.pointers.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);
var pointers = methods.pointers.selectAll("path.pointer")
.data(this.piedata);
pointers.enter()
.append("path")
.attr("class", "pointer")
.style("fill", "none")
.style("stroke", "black")
.attr("marker-end", "url(#circ)");
pointers
.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;
}
})
.transition()
.duration(300)
pointers
.transition()
.duration(300)
pointers.exit()
.transition()
.duration(300)
//__pointers
this.oldPieData = this.piedata;
}
};
$.fn.piechart = function(methodOrOptions) {
if ( methods[methodOrOptions] ) {
return methods[ methodOrOptions ].apply( this, Array.prototype.slice.call( arguments, 1 ));
} else if ( typeof methodOrOptions === 'object' || ! methodOrOptions ) {
// Default to "init"
return methods.init.apply( this, arguments );
} else {
$.error( 'Method ' + methodOrOptions + ' does not exist' );
}
};
})(jQuery);
var dataCharts = [
{
"data": [
{
"segments": [
{
"label": "apple",
"value": 53245
},
{
"label": "cherry",
"value": 145
},
{
"label": "pear",
"value": 2245
},
{
"label": "bananana",
"value": 15325
}
]
}
]
},
{
"data": [
{
"segments": [
{
"label": "milk",
"value": 532
},
{
"label": "cheese",
"value": 145
},
{
"label": "grapes",
"value": 22
}
]
}
]
},
{
"data": [
{
"segments": [
{
"label": "pineapple",
"value": 1532
},
{
"label": "orange",
"value": 1435
},
{
"label": "grapes",
"value": 22
}
]
}
]
},
{
"data": [
{
"segments": [
{
"label": "lemons",
"value": 133
},
{
"label": "mango",
"value": 435
},
{
"label": "melon",
"value": 2122
}
]
}
]
}
];
var clone = jQuery.extend(true, {}, dataCharts);
//__invoke concentric
$('[data-role="piechart"]').each(function(index) {
var selector = "piechart"+index;
$(this).attr("id", selector);
var options = {
data: clone[0].data,
width: $(this).data("width"),
height: $(this).data("height"),
r: $(this).data("r"),
ir: $(this).data("ir")
}
$("#"+selector).piechart(options);
$("#"+selector).piechart('update', clone[0].data[0].segments);
});
$(".testers a").on( "click", function(e) {
e.preventDefault();
var clone = jQuery.extend(true, {}, dataCharts);
var min = 0;
var max = 3;
//__invoke pie chart
$('[data-role="piechart"]').each(function(index) {
pos = Math.floor(Math.random() * (max - min + 1)) + min;
$("#"+$(this).attr("id")).piechart('update', clone[pos].data[0].segments);
});
});
});
回答by Lars Kotthoff
Here's a proof of concept (using a different example than yours as a basis as there's quite a lot of code in yours). This is the basic approach:
这是一个概念证明(使用与您不同的示例作为基础,因为您的代码中有很多)。这是基本方法:
- For each label, compute the start and end of the line underneath it. This is done by drawing the label and getting its bounding box.
- This gives two points on the pointer path, the third is the center of the respective segment. This is computed while computing the positions of the labels.
- These three points become part of the data. Now draw
path
s for each of the data elements, using the three points computed before. - Add an SVG marker at the end of each path for the dot.
- 对于每个标签,计算其下方线的起点和终点。这是通过绘制标签并获取其边界框来完成的。
- 这给出了指针路径上的两个点,第三个是相应段的中心。这是在计算标签位置时计算的。
- 这三个点成为数据的一部分。现在
path
使用之前计算的三个点为每个数据元素绘制s。 - 在每个路径的末尾为点添加一个 SVG 标记。
Here's the code to do it, step by step.
这是一步一步执行此操作的代码。
.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);
})
This is computing the x and y positions of the labels outside the segments. We also compute the position of the final point of the pointer path, in the center of the segment. That is, both in the middle between start and end angle and between inner and outer radii. This is added to the data.
这是计算段外标签的 x 和 y 位置。我们还计算了指针路径终点的位置,在段的中心。即,在起始角和终止角之间以及内半径和外半径之间的中间。这被添加到数据中。
.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;
});
After adding the text label (in this case, simply the value), we get for each the bounding box and compute the remaining two points for the path, just below the text to the left and just below to the right.
添加文本标签(在这种情况下,只是值)后,我们为每个边界框获取并计算路径的剩余两个点,位于文本左侧和右侧的下方。
svg.selectAll("path.pointer").data(piedata).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;
}
});
Now we can actually add the paths. They are a straightforward connection of the three points computed before, with a marker added at the end. The only thing to watch out for is that, depending on whether the label is on the left or the right of the chart, the path needs to start at the lower left of the label or the lower right. This is the if statement here.
现在我们可以实际添加路径。它们是之前计算的三个点的直接连接,最后添加了一个标记。唯一需要注意的是,根据标签是在图表的左侧还是右侧,路径需要从标签的左下角或右下角开始。这是这里的 if 语句。
Complete demo here.
在这里完成演示。
回答by The Old County
Here is the plugin code that should allow multiple instances of the pie chart - along with being able to update each pie chart with a new set of data.
这是应该允许饼图的多个实例的插件代码 - 以及能够使用一组新数据更新每个饼图。
I am open to ways to enhance the code. I feel it still looks a bit bulky - especially the way I am reseting the selector on update. Any suggestions to streamline this?
我对增强代码的方法持开放态度。我觉得它看起来仍然有点笨重 - 特别是我在更新时重置选择器的方式。有什么建议可以简化这个吗?
http://jsfiddle.net/Qh9X5/1318/
http://jsfiddle.net/Qh9X5/1318/
$(document).ready(function() {
(function( $ ){
var methods = {
el: "",
init : function(options) {
var clone = jQuery.extend(true, {}, options["data"]);
methods.el = this;
methods.setup(clone);
},
setup: function(dataset){
this.width = 300;
this.height = 300;
this.radius = Math.min(this.width, this.height) / 2;
this.color = d3.scale.category20();
this.pie = d3.layout.pie()
.sort(null);
this.arc = d3.svg.arc()
.innerRadius(this.radius - 100)
.outerRadius(this.radius - 50);
this.svg = d3.select(methods.el["selector"]).append("svg")
.attr("width", this.width)
.attr("height", this.height)
.append("g")
.attr("class", "piechart")
.attr("transform", "translate(" + this.width / 2 + "," + this.height / 2 + ")");
//this.update(dataset[0].segments);
},
oldPieData: "",
pieTween: function(d, i){
var that = this;
var theOldDataInPie = methods.oldPieData;
// Interpolate the arcs in data space
var s0;
var e0;
if(theOldDataInPie[i]){
s0 = theOldDataInPie[i].startAngle;
e0 = theOldDataInPie[i].endAngle;
} else if (!(theOldDataInPie[i]) && theOldDataInPie[i-1]) {
s0 = theOldDataInPie[i-1].endAngle;
e0 = theOldDataInPie[i-1].endAngle;
} else if(!(theOldDataInPie[i-1]) && theOldDataInPie.length > 0){
s0 = theOldDataInPie[theOldDataInPie.length-1].endAngle;
e0 = theOldDataInPie[theOldDataInPie.length-1].endAngle;
} else {
s0 = 0;
e0 = 0;
}
var i = d3.interpolate({startAngle: s0, endAngle: e0}, {startAngle: d.startAngle, endAngle: d.endAngle});
return function(t) {
var b = i(t);
return methods.arc(b);
};
},
removePieTween: function(d, i) {
var that = this;
s0 = 2 * Math.PI;
e0 = 2 * Math.PI;
var i = d3.interpolate({startAngle: d.startAngle, endAngle: d.endAngle}, {startAngle: s0, endAngle: e0});
return function(t) {
var b = i(t);
return methods.arc(b);
};
},
update: function(dataSet){
var that = this;
methods.el = this;
methods.svg = d3.select(methods.el["selector"] + " .piechart");
this.piedata = methods.pie(dataSet);
//__slices
this.path = methods.svg.selectAll("path.pie")
.data(this.piedata);
this.path.enter().append("path")
.attr("class", "pie")
.attr("fill", function(d, i) {
return methods.color(i);
})
.transition()
.duration(300)
.attrTween("d", methods.pieTween);
this.path
.transition()
.duration(300)
.attrTween("d", methods.pieTween);
this.path.exit()
.transition()
.duration(300)
.attrTween("d", methods.removePieTween)
.remove();
//__slices
//__labels
var labels = methods.svg.selectAll("text")
.data(this.piedata);
labels.enter()
.append("text")
.attr("text-anchor", "middle")
labels
.attr("x", function(d) {
var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
d.cx = Math.cos(a) * (methods.radius - 75);
return d.x = Math.cos(a) * (methods.radius - 20);
})
.attr("y", function(d) {
var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
d.cy = Math.sin(a) * (methods.radius - 75);
return d.y = Math.sin(a) * (methods.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;
})
.transition()
.duration(300)
labels
.transition()
.duration(300)
labels.exit()
.transition()
.duration(300)
//__labels
//__pointers
methods.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);
var pointers = methods.svg.selectAll("path.pointer")
.data(this.piedata);
pointers.enter()
.append("path")
.attr("class", "pointer")
.style("fill", "none")
.style("stroke", "black")
.attr("marker-end", "url(#circ)");
pointers
.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;
}
})
.transition()
.duration(300)
pointers
.transition()
.duration(300)
pointers.exit()
.transition()
.duration(300)
//__pointers
this.oldPieData = this.piedata;
}
};
$.fn.piechart = function(methodOrOptions) {
if ( methods[methodOrOptions] ) {
return methods[ methodOrOptions ].apply( this, Array.prototype.slice.call( arguments, 1 ));
} else if ( typeof methodOrOptions === 'object' || ! methodOrOptions ) {
// Default to "init"
return methods.init.apply( this, arguments );
} else {
$.error( 'Method ' + methodOrOptions + ' does not exist' );
}
};
})(jQuery);
var dataCharts = [
{
"data": [
{
"segments": [
53245, 28479, 19697, 24037, 40245
]
}
]
},
{
"data": [
{
"segments": [
855, 79, 97, 237, 245
]
}
]
},
{
"data": [
{
"segments": [
22, 79, 97, 12, 245
]
}
]
},
{
"data": [
{
"segments": [
122, 279, 197, 312, 545
]
}
]
}
];
var clone = jQuery.extend(true, {}, dataCharts);
//__invoke concentric
$('[data-role="piechart"]').each(function(index) {
var selector = "piechart"+index;
$(this).attr("id", selector);
var options = {
data: clone[0].data
}
$("#"+selector).piechart(options);
$("#"+selector).piechart('update', clone[0].data[0].segments);
});
$(".testers a").on( "click", function(e) {
e.preventDefault();
var clone = jQuery.extend(true, {}, dataCharts);
var min = 0;
var max = 3;
//__invoke pie chart
$('[data-role="piechart"]').each(function(index) {
pos = Math.floor(Math.random() * (max - min + 1)) + min;
$("#"+$(this).attr("id")).piechart('update', clone[pos].data[0].segments);
});
});
});