在 HTML 页面上绘制箭头以可视化文本跨度之间的语义链接

IT技术 javascript html visualization
2021-03-07 12:35:05

我有一个 HTML 页面,其中有一些文本跨度标记如下:

...
<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>
...

即每个跨度都有一个 ID,并通过它们的 ID 引用零个或多个跨度。

我想将这些参考可视化为箭头。

两个问题:

  • 如何将跨度的 ID 映射到跨度渲染的屏幕坐标?
  • 如何绘制从一个渲染到另一个渲染的箭头?

该解决方案应该在 Firefox 中工作,在其他浏览器中工作是一个加分项,但并不是真正必要的。该解决方案可以使用 jQuery 或其他一些轻量级 JavaScript 库。

6个回答

这引起了我足够长的兴趣以进行小测试。代码在下面,你可以看到它在行动

截屏

它列出了页面上的所有跨度(如果合适,可能希望将其限制为 id 以 T 开头的那些),并使用“ids”属性来构建链接列表。使用跨度后面的画布元素,它为每个源跨度在跨度上方和下方交替绘制弧形箭头。

<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> 

你只需要在某处调用 draw() 函数:

<body onload="draw();"> 

然后是一组跨度后面的画布。

<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> 

未来的修改,据我所知:

  • 展平较长箭头的顶部
  • 重构以能够绘制非水平箭头:为每个添加一个新画布?
  • 使用更好的例程来获取画布和跨度元素的总偏移量。

[编辑 2011 年 12 月:已修复,谢谢@Palo]

希望这既有趣又有用。

谢谢,我希望这些小挑战能更频繁地出现。几何、学习 API 和 HTML 挫折的完美结合(画布还没有文本渲染)。它使东西变得漂亮!
2021-04-21 12:35:05
谢谢,这看起来很令人印象深刻。并且似乎是我所追求的答案。可惜赏金比赛已经结束了。
2021-04-25 12:35:05
很好的答案。有没有人试过用来D3.js画箭头?
2021-04-25 12:35:05
不幸的是,全职生活没有给我时间在赏金结束之前完成它!呃,好吧。
2021-04-30 12:35:05
我将使用什么浏览器来查看此操作?Chrome 和 ie9 beta 不绘制任何箭头 - 虽然都支持 HtmlCanvas。
2021-05-03 12:35:05

您有几个选择:svgcanvas

从它的外观来看,您不需要这些箭头具有任何特定的数学形式,您只需要它们在元素之间移动即可。

试试WireIt看看这个WireIt Demo已被弃用)。canvas为浮动对话框之间的每条单独的电线使用一个标签div,然后调整每个canvas元素的大小和位置,以在正确的位置提供连接线的外观。您可能需要实现一个额外的旋转箭头,除非您不介意箭头以相同的角度进入每个元素。

编辑该演示已被弃用

编辑:忽略这个答案,@Phil H 搞定

Wire这是我的第一个想法,但你打败了我。
2021-05-04 12:35:05

一个很棒的箭头库是基于 Raphael 的JointJS,如上所示。使用 JointJS,您可以轻松地绘制带有曲线或顶点的箭头,而无需任何复杂的东西;-)

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

这定义了一个箭头'j34',它将两个js项目s3和s4连接起来。其他所有内容都可以在 JointJS 的文档中阅读。

如果您不需要弯曲箭头,则可以在列表上方或下方使用绝对定位的 div。然后,您可以使用 css 来设置这些 div 的样式以及构成箭头的几个图像。下面是一个使用 jQuery UI 项目中的图标集的示例(抱歉 URL 太长)。

这是让事情开始的 CSS:

<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>

现在我们可以开始组装箭头 div。例如,要在上面的示例中设置从“需要”到“发起人”的箭头样式,您可以在 div 上设置左、下和右边框,并在 div 的左上角设置朝上的箭头图形。

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

在您确定需要连接的事物的位置后,需要通过脚本应用内联样式。假设您的列表如下所示:

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

然后以下脚本将定位您的箭头:

<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>

继续并将上面的示例粘贴到一个空白的 html 页面中。它有点整洁。

嗯,我的主要问题是:我如何找出需要连接的东西的位置。HTML 文件将只有这些内容的 ID 属性。如何使用 Javascript 将 ID 映射到屏幕位置?
2021-04-23 12:35:05
您可以使用 jQuery 偏移或位置和宽度/高度函数来确定。( docs.jquery.com/CSS )
2021-04-27 12:35:05
编辑了我的答案以包含一个示例,使用 jQuery 找出要连接的项目的位置并在它们之间画一条线。
2021-04-29 12:35:05

你可以试试这个 JavaScript Vector Graphics Library - 它是非常聪明的东西,希望它有所帮助。

编辑:由于此链接已失效,这里是来自Archive.org 的另一个链接

-1 此链接不再可用,您应该详细了解该页面上的内容
2021-04-26 12:35:05
这个答案已经有将近三年的历史了,因为我没有互联网,所以不能保证链接的寿命。然而,在 Archive.org 上搜索了两秒钟,发现了 2009 年 2 月 20 日的版本......!
2021-05-01 12:35:05
+1 表示 Walter Zorn 的绘图库......它非常适合这种类型的应用程序(但不要尝试将它用于基于 Web 的 CAD 系统!)。
2021-05-04 12:35:05