web-dev-qa-db-fra.com

Dessiner des flèches sur une page HTML pour visualiser les liens sémantiques entre les étendues textuelles

J'ai une page HTML avec quelques portions de texte marquées comme ceci:

...
<span id="T2" class="Protein">p50</span>
...
<span id="T3" class="Protein">p65</span>
...
<span id="T34" ids="T2 T3" class="Positive_regulation">recruitment</span>
...

C'est à dire. chaque étendue a un identifiant et fait référence à zéro ou plusieurs étendues via leurs identifiants.

Je voudrais visualiser ces références sous forme de flèches.

Deux questions:

  • Comment mapper un identifiant d'une étendue sur les coordonnées d'écran du rendu de l'étendue?
  • Comment dessiner des flèches allant d'un rendu à un autre?

La solution devrait fonctionner dans Firefox, travailler dans d'autres navigateurs est un atout, mais pas vraiment nécessaire. La solution pourrait utiliser jQuery ou une autre bibliothèque JavaScript légère.

35
Kaarel

Vous avez plusieurs options: svg ou canvas .

À première vue, vous n’avez pas besoin que ces flèches aient une forme mathématique particulière, vous avez simplement besoin qu’elles se placent entre les éléments.

Essayez WireIt . Jetez un oeil à cette WireIt Demo (qui est obsolète). Il utilise une balise canvas pour chaque fil entre le dialogue flottant divs, puis redimensionne et positionne chaque élément canvas pour donner l’apparence d’une ligne de connexion au bon endroit. Vous devrez peut-être implémenter une pointe de flèche rotative supplémentaire, à moins que les flèches arrivant à chaque élément ne se préoccupent pas du même angle.

Edit : la démo est obsolète .

Edit : Ignore cette réponse, @Phil H l'a cloué

23
Crescent Fresh

Cela a capturé mon intérêt assez longtemps pour produire un petit test. Le code est ci-dessous, et vous pouvez le voir en action

screenshot

Il répertorie toutes les étendues de la page (vous voudrez peut-être le limiter aux identificateurs commençant par T si cela convient) et utilise l'attribut "ids" pour construire la liste des liens. À l'aide d'un élément de canevas situé derrière les étendues, il trace des flèches d'arc alternativement au-dessus et au-dessous des étendues pour chaque étendue source. 

<script type="application/x-javascript"> 

function generateNodeSet() {
  var spans = document.getElementsByTagName("span");
  var retarr = [];
  for(var i=0;i<spans.length; i++) { 
     retarr[retarr.length] = spans[i].id; 
  } 
  return retarr; 
} 

function generateLinks(nodeIds) { 
  var retarr = []; 
  for(var i=0; i<nodeIds.length; i++) { 
    var id = nodeIds[i];
    var span = document.getElementById(id); 
    var atts = span.attributes; 
    var ids_str = false; 
    if((atts.getNamedItem) && (atts.getNamedItem('ids'))) { 
      ids_str = atts.getNamedItem('ids').value; 
    } 
    if(ids_str) { 
      retarr[id] = ids_str.split(" ");
    }
  } 
  return retarr; 
} 

// degrees to radians, because most people think in degrees
function degToRad(angle_degrees) {
   return angle_degrees/180*Math.PI;
}
// draw a horizontal arc
//   ctx: canvas context;
//   inax: first x point
//   inbx: second x point
//   y: y value of start and end
//   alpha_degrees: (tangential) angle of start and end
//   upside: true for arc above y, false for arc below y.
function drawHorizArc(ctx, inax, inbx, y, alpha_degrees, upside)
{
  var alpha = degToRad(alpha_degrees);
  var startangle = (upside ? ((3.0/2.0)*Math.PI + alpha) : ((1.0/2.0)*Math.PI - alpha));
  var endangle = (upside ? ((3.0/2.0)*Math.PI - alpha) : ((1.0/2.0)*Math.PI + alpha));

  var ax=Math.min(inax,inbx);
  var bx=Math.max(inax,inbx);

  // tan(alpha) = o/a = ((bx-ax)/2) / o
  // o = ((bx-ax)/2/tan(alpha))
  // centre of circle is (bx+ax)/2, y-o
  var circleyoffset = ((bx-ax)/2)/Math.tan(alpha);
  var circlex = (ax+bx)/2.0;
  var circley = y + (upside ? 1 : -1) * circleyoffset;
  var radius = Math.sqrt(Math.pow(circlex-ax,2) + Math.pow(circley-y,2));

  ctx.beginPath();
  if(upside) {
      ctx.moveTo(bx,y);
    ctx.arc(circlex,circley,radius,startangle,endangle,1);
  } else {
    ctx.moveTo(bx,y);
    ctx.arc(circlex,circley,radius,startangle,endangle,0);
  }
  ctx.stroke();
}


// draw the head of an arrow (not the main line)
//  ctx: canvas context
//  x,y: coords of arrow point
//  angle_from_north_clockwise: angle of the line of the arrow from horizontal
//  upside: true=above the horizontal, false=below
//  barb_angle: angle between barb and line of the arrow
//  filled: fill the triangle? (true or false)
function drawArrowHead(ctx, x, y, angle_from_horizontal_degrees, upside, //mandatory
                       barb_length, barb_angle_degrees, filled) {        //optional
   (barb_length==undefined) && (barb_length=13);
   (barb_angle_degrees==undefined) && (barb_angle_degrees = 20);
   (filled==undefined) && (filled=true);
   var alpha_degrees = (upside ? -1 : 1) * angle_from_horizontal_degrees; 

   //first point is end of one barb
   var plus = degToRad(alpha_degrees - barb_angle_degrees);
   a = x + (barb_length * Math.cos(plus));
   b = y + (barb_length * Math.sin(plus));

   //final point is end of the second barb
   var minus = degToRad(alpha_degrees + barb_angle_degrees);
   c = x + (barb_length * Math.cos(minus));
   d = y + (barb_length * Math.sin(minus));

   ctx.beginPath();
   ctx.moveTo(a,b);
   ctx.lineTo(x,y);
   ctx.lineTo(c,d);
   if(filled) {
    ctx.fill();
   } else {
    ctx.stroke();
   }
   return true;
}

// draw a horizontal arcing arrow
//  ctx: canvas context
//  inax: start x value
//  inbx: end x value
//  y: y value
//  alpha_degrees: angle of ends to horizontal (30=shallow, >90=silly)
function drawHorizArcArrow(ctx, inax, inbx, y,                 //mandatory
                           alpha_degrees, upside, barb_length) { //optional
   (alpha_degrees==undefined) && (alpha_degrees=45);
   (upside==undefined) && (upside=true);
   drawHorizArc(ctx, inax, inbx, y, alpha_degrees, upside);
   if(inax>inbx) { 
    drawArrowHead(ctx, inbx, y, alpha_degrees*0.9, upside, barb_length); 
   } else { 
    drawArrowHead(ctx, inbx, y, (180-alpha_degrees*0.9), upside, barb_length); 
   }
   return true;
}


function drawArrow(ctx,fromelem,toelem,    //mandatory
                     above, angle) {        //optional
  (above==undefined) && (above = true);
  (angle==undefined) && (angle = 45); //degrees 
  midfrom = fromelem.offsetLeft + (fromelem.offsetWidth / 2) - left - tofromseparation/2; 
  midto   =   toelem.offsetLeft + (  toelem.offsetWidth / 2) - left + tofromseparation/2;
  //var y = above ? (fromelem.offsetTop - top) : (fromelem.offsetTop + fromelem.offsetHeight - top);
  var y = fromelem.offsetTop + (above ? 0 : fromelem.offsetHeight) - canvasTop;
  drawHorizArcArrow(ctx, midfrom, midto, y, angle, above);
}

    var canvasTop = 0;
function draw() { 
  var canvasdiv = document.getElementById("canvas");
  var spanboxdiv = document.getElementById("spanbox");
  var ctx = canvasdiv.getContext("2d");

  nodeset = generateNodeSet(); 
  linkset = generateLinks(nodeset);
  tofromseparation = 20;

  left = canvasdiv.offsetLeft - spanboxdiv.offsetLeft;
  canvasTop = canvasdiv.offsetTop - spanboxdiv.offsetTop; 
  for(var key in linkset) {  
    for (var i=0; i<linkset[key].length; i++) {  
      fromid = key; 
      toid = linkset[key][i]; 
      var above = (i%2==1);
      drawArrow(ctx,document.getElementById(fromid),document.getElementById(toid),above);
    } 
  } 
} 

</script> 

Et vous avez juste besoin d’un appel quelque part à la fonction draw ():

<body onload="draw();"> 

Puis une toile derrière l'ensemble des travées.

<canvas style='border:1px solid red' id="canvas" width="800" height="7em"></canvas><br /> 
<div id="spanbox" style='float:left; position:absolute; top:75px; left:50px'>
<span id="T2">p50</span>
...
<span id="T3">p65</span> 
...
<span id="T34" ids="T2 T3">recruitment</span>
</div> 

Les modifications futures, pour autant que je puisse voir: 

  • Aplatir le haut des flèches plus longues
  • Refactoring pour pouvoir dessiner des flèches non horizontales: ajouter un nouveau canevas pour chacun?
  • Utilisez une meilleure routine pour obtenir le total des compensations de la zone de dessin et des éléments étendus.

[Edit déc 2011: corrigé, merci @Palo]

J'espère que c'est aussi utile que c'était amusant.

68
Phil H

JointJS est basé sur Raphael, comme indiqué ci-dessus. Avec JointJS, vous pouvez facilement dessiner des flèches avec des courbes ou des sommets sans complication ;-)

var j34 = s3.joint(s4, uml.arrow).setVertices(["170 130", "250 120"]);

Ceci définit une flèche 'j34' qui connecte deux éléments js s3 avec s4. Tout le reste peut être lu dans la documentation de JointJS.

4
eraser

Si vous n’avez pas besoin de flèches incurvées, vous pouvez utiliser des divisions absolues placées au-dessus ou au-dessous de la liste. Vous pouvez ensuite utiliser css pour styler ces divs et quelques images qui constituent la pointe de la flèche. Vous trouverez ci-dessous un exemple d'utilisation du jeu d'icônes du projet d'interface utilisateur jQuery (désolé pour l'URL longue).

Voici le CSS pour faire avancer les choses:

<style>
 .below{
     border-bottom:1px solid #000;
     border-left:1px solid #000;
     border-right:1px solid #000;
 }
 .below span{
    background-position:0px -16px;
    top:-8px;
 }
 .above{
     border-top:1px solid #000;
     border-left:1px solid #000;
     border-right:1px solid #000;
 }
 .above span{
    background-position:-64px -16px;
    bottom:-8px;
 }

 .arrow{
    position:absolute;
    display:block;
    background-image:url(http://jquery-ui.googlecode.com/svn/trunk/themes/base/images/ui-icons_454545_256x240.png);
    width:16px;
    height:16px;
    margin:0;
    padding:0;
 }

.left{left:-8px;}

.right{right:-9px;}

</style>

Nous pouvons maintenant commencer à assembler les div arrow. Par exemple, pour appeler la flèche de "requiert" à "promoteur" dans l'exemple ci-dessus, vous pouvez créer des bordures gauche, inférieure et droite sur la div avec un graphique en forme de flèche orientée vers le haut en haut à gauche de la div. 

<div class='below' style="position:absolute;top:30px;left:30px;width:100px;height:16px">
   <span class='arrow left'></span>
</div>

Les styles en ligne doivent être appliqués par script après avoir déterminé l'emplacement des éléments à connecter. Disons que votre liste ressemble à ceci:

<span id="promoter">Promoter</span><span>Something Else</span><span id="requires">Requires</span>

Ensuite, le script suivant positionnera votre flèche:

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js"></script> 
<script>
$(function(){
 var promoterPos=$("#promoter").offset();
 var requiresPos=$("#requires").offset();
 $("<div class='below'><span class='arrow left'></span></div>")
 .css({position:"absolute",left:promoterPos.left,right:promoterPos.top+$("#promoter").height()})
 .width(requiresPos.left-promoterPos.left)
 .height(16)
 .appendTo("body");
});
</script>

Continuez et collez les exemples ci-dessus dans une page html vierge. C'est plutôt chouette.

2
Josh Bush

J'essaie d'utiliser les technologies Web ouvertes dans la mesure du possible, mais la vérité est que HTML et JavaScript (ou jQuery) ne sont pas les outils de ce travail (triste mais vrai), d'autant plus que la complexité des diagrammes que vous dessinez augmente.

D'autre part, Flash a été conçu pour cela. Il faudrait beaucoup moins de code ActionScript 3.0 pour analyser ce code XML, mettre en forme votre texte (avec davantage de contrôle sur les polices et les super/indices) et restituer les courbes (voir les méthodes flash.display.Graphics telles que curveTo). Globalement, vous obtiendrez moins de code, une meilleure facilité de maintenance, moins de piratages, une compatibilité plus étendue et des bibliothèques de dessins plus stables.

Bonne chance pour ton projet.

2
Jaysen Marais

Vous pouvez essayer cette bibliothèque - c'est très intelligent, espérons que cela vous aidera.

EDIT: Comme ce lien est mort, voici un autre lien de Archive.org .

2
Kieron

Comme d'autres l'ont mentionné, Javascript et le HTML ne sont pas de bons outils pour ce genre de chose.

John Resig a écrit une implémentation de Processing.org en JavaScript . Il utilise l'élément canvas, donc cela fonctionnera dans les versions modernes de Firefox, mais cela ne fonctionnera pas dans tous les navigateurs. Si vous ne vous souciez que de Firefox, ce serait probablement la voie à suivre.

Vous pourrez peut-être utiliser SVG, mais encore une fois, cela n'est pas supporté par tous les navigateurs.

1
Buddy

J'avais besoin d'une solution similaire et je cherchais dans RaphaelJS JavaScript Library . Par exemple, vous pouvez dessiner une flèche droite allant de (x1,y1) à (x2,y2) avec:

Raphael.fn.arrow = function (x1, y1, x2, y2, size) {
  var angle = Math.atan2(x1-x2,y2-y1);
  angle = (angle / (2 * Math.PI)) * 360;
  var arrowPath = this.path(“M” + x2 + ” ” + y2 + ” L” + (x2 - size) + ” ” + (y2 - size) + ” L” + (x2 - size) + ” ” + (y2 + size) + ” L” + x2 + ” ” + y2 ).attr(“fill”,”black”).rotate((90+angle),x2,y2);
  var linePath = this.path(“M” + x1 + ” ” + y1 + ” L” + x2 + ” ” + y2);
  return [linePath,arrowPath];
}

Je n'ai pas trouvé comment dessiner une flèche courbe, mais je suis sûr que c'est possible.

0
Panagiotis Panagi

Vous pouvez obtenir les extrémités fléchées en utilisant une poignée de position:absolute divs avec background-image défini sur des fichiers GIF transparents ... un ensemble pour début (haut et bas) ... un bacground:repeat div pour un milieu extensible, et une autre paire pour les extrémités (haut et bas). bas).

0
Scott Evernden

Vous pouvez utiliser cette bibliothèque : simplement annoter vos lignes SVG avec les identifiants de l’élément source & target. Il utilise MutationObserver pour observer les changements dans les éléments connectés.

0
thSoft