javascript - ejemplos - d3.js extendiendo etiquetas para gráficos circulares
d3.js ejemplos (3)
Debería ser posible hacer La forma exacta en que quiera hacerlo dependerá de lo que quiera hacer con el espaciado de las etiquetas. Sin embargo, no existe una forma de hacerlo.
El principal problema con las etiquetas es que, en su ejemplo, se basan en los mismos datos de posicionamiento que está utilizando para las porciones de su gráfico circular. Si quieres que se espacien más como Excel (es decir, dales espacio), tendrás que ser creativo. La información que tiene es su posición inicial, su altura y su ancho.
Una manera realmente divertida (mi definición de diversión) de resolver esto sería crear un solucionador estocástico para una disposición óptima de las etiquetas. Podrías hacer esto con un método basado en la energía. Defina una función de energía donde la energía aumenta según dos criterios: distancia desde el punto inicial y superposición con etiquetas cercanas. Puede hacer un descenso simple de gradiente basado en ese criterio de energía para encontrar una solución localmente óptima con respecto a su energía total, lo que resultaría en que sus etiquetas estén lo más cerca posible de sus puntos originales sin una cantidad significativa de solapamiento, y sin empujar más puntos lejos de sus puntos originales.
Cuánta superposición es tolerable dependerá de la función de energía que especifique, la cual debe ser optimizable para proporcionar una buena distribución de los puntos. Del mismo modo, la cantidad que esté dispuesto a ceder en la cercanía del punto dependerá de la forma de su función de aumento de energía para la distancia desde el punto original. (Un aumento de energía lineal dará como resultado puntos más cercanos, pero mayores valores atípicos. Un cuadrático o un cubo tendrá una distancia promedio mayor, pero valores atípicos más pequeños).
También podría haber una forma analítica de resolver los mínimos, pero eso sería más difícil. Probablemente puedas desarrollar una heurística para posicionar las cosas, que es lo que Excel probablemente hace, pero eso sería menos divertido.
Estoy usando d3.js - Aquí tengo un gráfico circular. El problema es cuando las rebanadas son pequeñas, las etiquetas se superponen. ¿Cuál es la mejor forma de distribuir las etiquetas?
Aquí está el código para las etiquetas. Tengo curiosidad: ¿es posible burlarse de un gráfico circular 3d con d3?
//draw labels
valueLabels = label_group.selectAll("text.value").data(filteredData)
valueLabels.enter().append("svg:text")
.attr("class", "value")
.attr("transform", function(d) {
return "translate(" + Math.cos(((d.startAngle+d.endAngle - Math.PI)/2)) * (that.r + that.textOffset) + "," + Math.sin((d.startAngle+d.endAngle - Math.PI)/2) * (that.r + that.textOffset) + ")";
})
.attr("dy", function(d){
if ((d.startAngle+d.endAngle)/2 > Math.PI/2 && (d.startAngle+d.endAngle)/2 < Math.PI*1.5 ) {
return 5;
} else {
return -7;
}
})
.attr("text-anchor", function(d){
if ( (d.startAngle+d.endAngle)/2 < Math.PI ){
return "beginning";
} else {
return "end";
}
}).text(function(d){
//if value is greater than threshold show percentage
if(d.value > threshold){
var percentage = (d.value/that.totalOctets)*100;
return percentage.toFixed(2)+"%";
}
});
valueLabels.transition().duration(this.tweenDuration).attrTween("transform", this.textTween);
valueLabels.exit().remove();
Una forma de verificar si hay conflictos es usar el método getIntersectionList()
del elemento <svg>
. Ese método requiere que pase un objeto SVGRect
(que es diferente de un elemento <rect>
!), Como el objeto devuelto por el método .getBBox()
un elemento gráfico.
Con esos dos métodos, puede averiguar dónde está una etiqueta dentro de la pantalla y si se superpone a algo. Sin embargo, una complicación es que las coordenadas rectangulares pasadas a getIntersectionList
se interpretan dentro de las coordenadas de la raíz SVG, mientras que las coordenadas devueltas por getBBox
están en el sistema de coordenadas local. Entonces también necesita el método getCTM()
(obtener matriz de transformación acumulativa) para convertir entre los dos.
Comencé con el ejemplo de Lars Khottof que @TheOldCounty había publicado en un comentario, ya que incluía líneas entre los segmentos de arco y las etiquetas. Hice una pequeña reorganización para poner las etiquetas, líneas y segmentos de arco en elementos <g>
separados. Esto evita superposiciones extrañas (arcos dibujados en la parte superior de las líneas del puntero) en la actualización, y también hace que sea fácil definir qué elementos nos preocupa superponer - otras etiquetas solamente, no las líneas de puntero o arcos - al pasar el padre <g>
elemento como el segundo parámetro para getIntersectionList
.
Las etiquetas se colocan una por una utilizando each
función, y tienen que estar realmente posicionadas (es decir, el atributo establecido en su valor final, sin transiciones) en el momento en que se calcula la posición, para que estén en su lugar cuando getIntersectionList
se llama para la posición predeterminada de la próxima etiqueta.
La decisión de dónde mover una etiqueta si se superpone a una etiqueta anterior es compleja, como lo resume la respuesta de @ ckersch. Lo mantengo simple y solo lo muevo a la derecha de todos los elementos superpuestos. Esto podría causar un problema en la parte superior del gráfico, donde las etiquetas de los últimos segmentos podrían moverse para que se superpongan con las etiquetas de los primeros segmentos, pero eso es poco probable si el gráfico circular se ordena por tamaño de segmento.
Aquí está el código de la llave:
labels.text(function (d) {
// Set the text *first*, so we can query the size
// of the label with .getBBox()
return d.value;
})
.each(function (d, i) {
// Move all calculations into the each function.
// Position values are stored in the data object
// so can be accessed later when drawing the line
/* calculate the position of the center marker */
var a = (d.startAngle + d.endAngle) / 2 ;
//trig functions adjusted to use the angle relative
//to the "12 o''clock" vector:
d.cx = Math.sin(a) * (that.radius - 75);
d.cy = -Math.cos(a) * (that.radius - 75);
/* calculate the default position for the label,
so that the middle of the label is centered in the arc*/
var bbox = this.getBBox();
//bbox.width and bbox.height will
//describe the size of the label text
var labelRadius = that.radius - 20;
d.x = Math.sin(a) * (labelRadius);
d.sx = d.x - bbox.width / 2 - 2;
d.ox = d.x + bbox.width / 2 + 2;
d.y = -Math.cos(a) * (that.radius - 20);
d.sy = d.oy = d.y + 5;
/* check whether the default position
overlaps any other labels*/
//adjust the bbox according to the default position
//AND the transform in effect
var matrix = this.getCTM();
bbox.x = d.x + matrix.e;
bbox.y = d.y + matrix.f;
var conflicts = this.ownerSVGElement
.getIntersectionList(bbox, this.parentNode);
/* clear conflicts */
if (conflicts.length) {
console.log("Conflict for ", d.data, conflicts);
var maxX = d3.max(conflicts, function(node) {
var bb = node.getBBox();
return bb.x + bb.width;
})
d.x = maxX + 13;
d.sx = d.x - bbox.width / 2 - 2;
d.ox = d.x + bbox.width / 2 + 2;
}
/* position this label, so it will show up as a conflict
for future labels. (Unfortunately, you can''t use transitions.) */
d3.select(this)
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
});
});
Y aquí está el violín de trabajo: http://jsfiddle.net/Qh9X5/1237/
Como @The Old County descubrió, la respuesta anterior que publiqué falla en Firefox porque se basa en el método SVG .getIntersectionList()
para encontrar conflictos, y ese método aún no se ha implementado en Firefox .
Eso solo significa que tenemos que hacer un seguimiento de las posiciones de las etiquetas y probar los conflictos nosotros mismos. Con d3, la forma más eficiente de verificar los conflictos de diseño implica el uso de una estructura de datos de cuatribanda para almacenar las posiciones, de esa manera no es necesario verificar todas las etiquetas para superposición, solo aquellas en un área similar de la visualización.
La segunda parte del código de la respuesta anterior se reemplaza por:
/* check whether the default position
overlaps any other labels*/
var conflicts = [];
labelLayout.visit(function(node, x1, y1, x2, y2){
//recurse down the tree, adding any overlapping labels
//to the conflicts array
//node is the node in the quadtree,
//node.point is the value that we added to the tree
//x1,y1,x2,y2 are the bounds of the rectangle that
//this node covers
if ( (x1 > d.r + maxLabelWidth/2)
//left edge of node is to the right of right edge of label
||(x2 < d.l - maxLabelWidth/2)
//right edge of node is to the left of left edge of label
||(y1 > d.b + maxLabelHeight/2)
//top (minY) edge of node is greater than the bottom of label
||(y2 < d.t - maxLabelHeight/2 ) )
//bottom (maxY) edge of node is less than the top of label
return true; //don''t bother visiting children or checking this node
var p = node.point;
var v = false, h = false;
if ( p ) { //p is defined, i.e., there is a value stored in this node
h = ( ((p.l > d.l) && (p.l <= d.r))
|| ((p.r > d.l) && (p.r <= d.r))
|| ((p.l < d.l)&&(p.r >=d.r) ) ); //horizontal conflict
v = ( ((p.t > d.t) && (p.t <= d.b))
|| ((p.b > d.t) && (p.b <= d.b))
|| ((p.t < d.t)&&(p.b >=d.b) ) ); //vertical conflict
if (h&&v)
conflicts.push(p); //add to conflict list
}
});
if (conflicts.length) {
console.log(d, " conflicts with ", conflicts);
var rightEdge = d3.max(conflicts, function(d2) {
return d2.r;
});
d.l = rightEdge;
d.x = d.l + bbox.width / 2 + 5;
d.r = d.l + bbox.width + 10;
}
else console.log("no conflicts for ", d);
/* add this label to the quadtree, so it will show up as a conflict
for future labels. */
labelLayout.add( d );
var maxLabelWidth = Math.max(maxLabelWidth, bbox.width+10);
var maxLabelHeight = Math.max(maxLabelHeight, bbox.height+10);
Tenga en cuenta que he cambiado los nombres de los parámetros para los bordes de la etiqueta a l / r / b / t (izquierda / derecha / abajo / arriba) para mantener todo lo lógico en mi mente.
Violín vivo aquí: http://jsfiddle.net/Qh9X5/1249/
Un beneficio adicional de hacerlo de esta manera es que puede verificar los conflictos en función de la posición final de las etiquetas, antes de establecer realmente la posición. Lo que significa que puede usar transiciones para mover las etiquetas a su posición después de descubrir las posiciones para todas las etiquetas.