d3.js diffusione di etichette per grafici a torta

Sto usando d3.js – Ho un grafico a torta qui. Il problema però è quando le fette sono piccole – le etichette si sovrappongono. Qual è il modo migliore per diffondere le etichette.

Etichette sovrapposte

una torta 3d

http://jsfiddle.net/BxLHd/16/

Ecco il codice per le etichette. Sono curioso – è ansible prendere in giro un grafico a torta 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  threshold){ var percentage = (d.value/that.totalOctets)*100; return percentage.toFixed(2)+"%"; } }); valueLabels.transition().duration(this.tweenDuration).attrTween("transform", this.textTween); valueLabels.exit().remove(); 

Come @The Old County ha scoperto, la risposta precedente che ho postato fallisce in firefox perché si basa sul metodo SVG .getIntersectionList() per trovare i conflitti, e quel metodo non è ancora stato implementato in Firefox .

Questo significa solo che dobbiamo tenere traccia delle posizioni delle etichette e testare noi stessi i conflitti. Con d3, il modo più efficace per verificare i conflitti tra i layout consiste nell’utilizzare una struttura di dati quadrangolo per memorizzare le posizioni, in questo modo non è necessario controllare ogni sovrapposizione di etichette, solo quelle in un’area simile della visualizzazione.

La seconda parte del codice della risposta precedente viene sostituita con:

  /* 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); 

Nota che ho cambiato i nomi dei parametri per i bordi dell’etichetta in l / r / b / t (sinistra / destra / basso / alto) per mantenere tutto nella mia mente.

Vivi il violino qui: http://jsfiddle.net/Qh9X5/1249/

Un ulteriore vantaggio di farlo in questo modo è che è ansible verificare i conflitti in base alla posizione finale delle etichette, prima di impostare effettivamente la posizione. Ciò significa che è ansible utilizzare le transizioni per spostare le etichette in posizione dopo aver individuato le posizioni per tutte le etichette.

Dovrebbe essere ansible fare. Quanto esattamente vuoi farlo dipenderà da cosa vuoi fare con la spaziatura delle etichette. Non c’è, tuttavia, un modo integrato per farlo.

Il problema principale con le etichette è che, nel tuo esempio, si basano sugli stessi dati per il posizionamento che stai utilizzando per le sezioni del grafico a torta. Se vuoi che spazino di più come fa Excel (vale a dire dar loro spazio), devi essere creativo. Le informazioni che hai sono la loro posizione di partenza, la loro altezza e la loro larghezza.

Un modo davvero divertente (la mia definizione di divertimento) per risolvere il problema sarebbe quello di creare un solutore stocastico per una disposizione ottimale delle etichette. Potresti farlo con un metodo basato sull’energia. Definire una funzione energetica in cui l’energia aumenta in base a due criteri: distanza dal punto iniziale e sovrapposizione con etichette vicine. Puoi fare una semplice discesa del gradiente in base a quel criterio energetico per trovare una soluzione localmente ottimale per quanto riguarda la tua energia totale, il che farebbe sì che le etichette si avvicinino il più ansible ai loro punti originali senza una significativa quantità di sovrapposizione e senza spingere di più punta lontano dai loro punti originali.

Quanta sovrapposizione è tollerabile dipende dalla funzione energetica specificata, che dovrebbe essere regolabile per fornire una buona distribuzione dei punti. Allo stesso modo, quanto sei disposto a spostarti sulla prossimità dei punti dipenderà dalla forma della tua funzione di aumento di energia per la distanza dal punto originale. (Un aumento di energia lineare si tradurrà in punti più vicini, ma maggiori valori anomali: un quadratico o un cubo avrà una distanza media maggiore, ma valori più piccoli).

Potrebbe anche esserci un modo analitico per risolvere i minimi, ma sarebbe più difficile. Probabilmente potresti sviluppare un’euristica per posizionare le cose, che è probabilmente ciò che fa Excel, ma sarebbe meno divertente.

Un modo per verificare i conflitti è utilizzare il metodo getIntersectionList() dell’elemento . Questo metodo richiede di passare un object SVGRect (che è diverso da un elemento !), Come l’object restituito dal metodo .getBBox() di un elemento grafico.

Con questi due metodi, puoi capire dove si trova un’etichetta all’interno dello schermo e se si sovrappone a qualcosa. Tuttavia, una complicazione è che le coordinate del rettangolo passate a getIntersectionList sono interpretate all’interno delle coordinate di SVG della radice, mentre le coordinate restituite da getBBox trovano nel sistema di coordinate locale. Quindi è necessario anche il metodo getCTM() (ottieni matrice di trasformazione cumulativa) per convertire tra i due.

Ho iniziato con l’esempio di Lars Khottof che @TheOldCounty aveva pubblicato in un commento, poiché includeva già le linee tra i segmenti di arco e le etichette. Ho fatto una piccola riorganizzazione per mettere etichette, linee e segmenti di arco in elementi separati. Questo evita strane sovrapposizioni (archi disegnati sulla cima delle linee del puntatore) durante l’aggiornamento, e rende anche facile definire quali elementi siamo preoccupati di sovrapporre – solo le altre etichette, non le linee o gli archi del puntatore – passando il genitore elemento come secondo parametro per getIntersectionList .

Le etichette vengono posizionate una alla volta utilizzando each funzione e devono essere posizionate effettivamente (ovvero l’attributo impostato sul suo valore finale, senza transizioni) nel momento in cui viene calcasting la posizione, in modo che siano posizionate quando getIntersectionList viene chiamato per la posizione predefinita dell’etichetta successiva.

La decisione su dove spostare un’etichetta se si sovrappone a un’etichetta precedente è complessa, come i contorni di risposta di @ ckersch. Lo tengo semplice e lo sposto a destra di tutti gli elementi sovrapposti. Ciò potrebbe causare un problema nella parte superiore della torta, in cui le etichette degli ultimi segmenti potrebbero essere spostate in modo che si sovrappongano alle etichette dei primi segmenti, ma è improbabile che il grafico a torta sia ordinato per dimensione del segmento.

Ecco il codice chiave:

  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; }); }); 

Ed ecco il funzionamento del fiddle: http://jsfiddle.net/Qh9X5/1237/