D3.js répandant des étiquettes pour les tableaux circulaires

J'utilise d3.js – J'ai un graphique à secteurs ici. Le problème est cependant lorsque les tranches sont petites – les étiquettes se chevauchent. Quelle est la meilleure façon de répartir les étiquettes.

Étiquettes chevauchées

Une tarte 3D

Http://jsfiddle.net/BxLHd/16/

Voici le code pour les étiquettes. Je suis curieux – est-il possible de se moquer d'un diagramme 3d avec 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(); 

Comme l'a découvert le Old County, la réponse précédente que j'ai posée échoue dans Firefox, car elle repose sur la méthode SVG .getIntersectionList() pour trouver des conflits, et cette méthode n'a pas encore été mise en œuvre dans Firefox .

Cela signifie simplement que nous devons suivre les positions des étiquettes et tester les conflits nous-mêmes. Avec d3, la manière la plus efficace de vérifier les conflits de mise en page implique l'utilisation d'une structure de données quadtree pour stocker des positions, de sorte que vous ne devez pas vérifier toutes les étiquettes pour le chevauchement, juste celles dans une zone similaire de la visualisation.

La deuxième partie du code de la réponse précédente est remplacée par:

  /* 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 > dr + maxLabelWidth/2) //left edge of node is to the right of right edge of label ||(x2 < dl - maxLabelWidth/2) //right edge of node is to the left of left edge of label ||(y1 > db + maxLabelHeight/2) //top (minY) edge of node is greater than the bottom of label ||(y2 < dt - 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, ie, there is a value stored in this node h = ( ((pl > dl) && (pl <= dr)) || ((pr > dl) && (pr <= dr)) || ((pl < dl)&&(pr >=dr) ) ); //horizontal conflict v = ( ((pt > dt) && (pt <= db)) || ((pb > dt) && (pb <= db)) || ((pt < dt)&&(pb >=db) ) ); //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; }); dl = rightEdge; dx = dl + bbox.width / 2 + 5; dr = dl + 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); 

Notez que j'ai changé les noms de paramètres pour les bords de l'étiquette en l / r / b / t (gauche / droite / inférieure / supérieure) pour garder tout ce qui est logique dans mon esprit.

Violon en direct ici: http://jsfiddle.net/Qh9X5/1249/

Un avantage supplémentaire de le faire de cette façon est que vous pouvez vérifier les conflits en fonction de la position finale des étiquettes, avant de définir le poste. Cela signifie que vous pouvez utiliser des transitions pour déplacer les étiquettes en position après avoir calculé les positions pour toutes les étiquettes.

Devrait être possible de faire. La façon dont vous le souhaitez exactement dépendra de ce que vous voulez faire pour espacer les étiquettes. Il n'y a pas, cependant, un moyen de faire cela.

Le principal problème avec les étiquettes est que, dans votre exemple, ils dépendent des mêmes données de positionnement que vous utilisez pour les tranches de votre diagramme circulaire. Si vous voulez que l'espace soit plus comme Excel (c.-à-d. Leur donner de la place), vous devrez devenir créatif. L'information que vous avez est leur position de départ, leur hauteur et leur largeur.

Une façon vraiment amusante (ma définition de l'amusement) de résoudre cette solution serait de créer un solveur stochastique pour un arrangement optimal des étiquettes. Vous pouvez le faire avec une méthode basée sur l'énergie. Définir une fonction énergétique où l'énergie augmente en fonction de deux critères: distance du point de départ et chevauchement avec les étiquettes proches. Vous pouvez faire une descente en gradient simple en fonction de ces critères d'énergie pour trouver une solution optimale localement en ce qui concerne votre énergie totale, ce qui entraînerait que vos étiquettes soient aussi proches que possible de leurs points d'origine sans un chevauchement important et sans pousser plus Points loin de leurs points d'origine.

Combien le chevauchement est tolérable dépend de la fonction d'énergie que vous spécifiez, qui devrait être réglable pour donner une bonne répartition des points. De même, combien vous désirez se rapprocher de la proximité du point dépend de la forme de votre fonction d'augmentation d'énergie pour la distance du point original. (Une augmentation linéaire de l'énergie entraînera des points rapprochés, mais des valeurs aberrantes supérieures. Un quadrature ou un cubique aura une distance moyenne plus élevée, mais des valeurs aberrantes plus petites.)

Il pourrait également y avoir une manière analytique de résoudre les minimums, mais cela serait plus difficile. Vous pourriez probablement développer une heuristique pour positionner les choses, ce qui est probablement ce qu'est l'excellence, mais cela serait moins amusant.

Une façon de vérifier les conflits est d'utiliser la méthode getIntersectionList() l'élément <svg> . Cette méthode vous oblige à passer dans un objet SVGRect (qui est différent d'un élément <rect> !), .getBBox() que l'objet renvoyé par la méthode .getBBox() l'élément graphique.

Avec ces deux méthodes, vous pouvez déterminer où une étiquette est dans l'écran et si elle se chevauche quelque chose. Cependant, une complication est que les coordonnées rectangulaires passées à getIntersectionList sont interprétées dans les coordonnées de la racine SVG, tandis que les coordonnées renvoyées par getBBox trouvent dans le système de coordonnées local. Donc, vous avez également besoin de la méthode getCTM() (obtenir une matrice de transformation cumulative) à convertir entre les deux.

J'ai commencé avec l'exemple de Lars Khottof que @TheOldCounty avait posté dans un commentaire, car il comprenait déjà des lignes entre les segments d'arc et les étiquettes. J'ai fait une petite réorganisation pour mettre les étiquettes, les lignes et les segments d'arc dans des éléments <g> distincts. Cela évite les chevauchements étranges (arcs dessinés au-dessus des lignes de pointeur) sur la mise à jour, et il permet également de définir les éléments sur lesquels nous nous inquiétons: d'autres étiquettes, pas les lignes de pointeur ou les arcs, en passant le parent <g> élément comme deuxième paramètre pour getIntersectionList .

Les étiquettes sont positionnées une à la fois à l'aide d'une fonction, et elles doivent être effectivement positionnées (c'est-à-dire l'attribut défini sur sa valeur finale, pas de transition) au moment où la position est calculée, de sorte qu'elles soient en place lors de la getIntersectionList Est appelé pour la position par défaut de l'étiquette suivante.

La décision d' déplacer une étiquette si elle chevauche une étiquette antérieure est complexe, comme l'explique la réponse de @ckersch. Je le garde simple et je le déplace simplement à droite de tous les éléments superposés. Cela pourrait causer un problème au sommet de la tarte, où les étiquettes des derniers segments pourraient être déplacées afin qu'elles chevauchent les étiquettes des premiers segments, mais cela est peu probable si le diagramme est trié par taille de segment.

Voici le code clé:

  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; dx = Math.sin(a) * (labelRadius); d.sx = dx - bbox.width / 2 - 2; d.ox = dx + bbox.width / 2 + 2; dy = -Math.cos(a) * (that.radius - 20); d.sy = d.oy = dy + 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 = dx + matrix.e; bbox.y = dy + 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; }) dx = maxX + 13; d.sx = dx - bbox.width / 2 - 2; d.ox = dx + 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 dx; }) .attr("y", function (d) { return dy; }); }); 

Et voici le violon de travail: http://jsfiddle.net/Qh9X5/1237/