Подтвердить что ты не робот

Рисование стрелок на странице HTML для визуализации семантических связей между текстовыми пробелами

У меня есть 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>
...

т.е. каждый пролет имеет идентификатор и ссылается на ноль или более пролетов через их идентификаторы.

Я хотел бы визуализировать эти ссылки как стрелки.

Два вопроса:

  • Как я могу сопоставить идентификатор диапазона с экранными координатами рендеринга диапазона?
  • Как рисовать стрелки, идущие от одного рендеринга к другому?

Решение должно работать в Firefox, работа в других браузерах - это плюс, но не очень необходимый. Решение может использовать jQuery или другую легкую библиотеку JavaScript.

4b9b3361

Ответ 1

У вас есть несколько вариантов: svg или canvas.

По внешнему виду вам не нужны эти стрелки для какой-либо конкретной математической формы, вам просто нужно, чтобы они проходили между элементами.

Попробуйте WireIt. Посмотрите на WireIt Demo (который устарел). Он использует тег canvas для каждого отдельного провода между плавающим диалогом div s, затем размеры и позиции каждого элемента canvas, чтобы создать видимость соединительной линии в нужном месте. Возможно, вам придется реализовать дополнительную вращающуюся стрелку, если вы не возражаете против стрелок, входящих в каждый элемент под одним углом.

Изменить: демонстрация устарела.

Изменить. Игнорируйте этот ответ, @Phil H пригвоздил его

Ответ 2

Это привлекло мое внимание достаточно долго, чтобы произвести небольшой тест. Код ниже, и вы можете видеть его в действии

screenshot

В нем перечислены все промежутки на странице (возможно, это нужно ограничить только теми, у которых есть идентификаторы, начинающиеся с 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> 

Будущие изменения, насколько я вижу:

  • Сглаживание верхней части стрелок
  • Рефакторинг для рисования не горизонтальных стрелок: добавьте новый холст для каждого?
  • Используйте лучшую процедуру, чтобы получить общие смещения элементов холста и диапазона.

[Edit Dec 2011: Исправлено, спасибо @Palo]

Надеюсь, что это так полезно, как это было весело.

Ответ 3

Большая библиотека для стрелок JointJS, которая основана на Рафаэле, как показано выше. С JointJS вы можете легко рисовать стрелки с кривыми или вершинами без каких-либо сложных вещей; -)

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

Это определяет стрелку 'j34', которая соединяет два js-элемента s3 с s4. Все остальное можно прочитать в документации JointJS.

Ответ 4

Вы можете попробовать эту библиотеку - это очень умно, надеюсь, что это поможет.

EDIT: Поскольку эта ссылка мертва, вот еще одна ссылка из Archive.org.

Ответ 5

Я стараюсь использовать открытые веб-технологии, где это возможно, но правда в том, что HTML и JavaScript (или jQuery) не являются инструментами для этой конкретной работы (грустно, но верно), тем более, что диаграммы, которые вы рисуете, увеличиваются сложность.

С другой стороны, для этого была создана Flash. Значительно меньше кода ActionScript 3.0 потребуется для синтаксического анализа этого XML, макета вашего текста (с большим контролем над шрифтами и супер/индексами) и визуализации кривых (см. flash.display.Graphics class, например curveTo). В целом вы будете искать меньше кода, лучшую ремонтопригодность, меньшее количество хаков, более широкую совместимость и более стабильные библиотеки чертежей.

Удачи вам в проекте.

Ответ 6

Если вам не нужны изогнутые стрелки, вы можете использовать абсолютно позиционированные div сверху или снизу списка. Затем вы можете использовать css для стилей этих divs и пару изображений, составляющих голову стрелки. Ниже приведен пример использования набора значков из проекта 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>

Встраиваемые стили необходимо будет применять с помощью script после того, как вы выясните расположение вещей, которые вам нужно будет подключить. Скажем, что ваш список выглядит следующим образом:

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

Затем следующая script позиционирует вашу стрелку:

<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. Это немного опрятно.

Ответ 7

Как уже упоминалось, Javascript и html не являются хорошими инструментами для такого рода вещей.

Джон Ресиг написал реализацию Processing.org в JavaScript. Он использует элемент canvas, поэтому он будет работать в современных версиях Firefox, но он не будет работать во всех браузерах. Если вы только заботитесь об Firefox, возможно, это будет путь.

Возможно, вы сможете использовать SVG, но опять же, это не поддерживается во всех браузерах.

Ответ 8

Вы можете получить закругленные концы стрелок, используя несколько разделов position:absolute с background-image, установленными в прозрачные GIF... набор для начала (сверху и снизу)... a bacground:repeat div для расширяемой середины, и другую пару для концов (сверху и снизу).

Ответ 9

Мне нужно было аналогичное решение, и я изучал библиотеку JavaScript RaphaelJS. Например, вы можете нарисовать прямую стрелку от (x1,y1) до (x2,y2) с помощью:

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

Я не понимаю, как нарисовать изогнутую стрелу, но я уверен, что это возможно.

Ответ 10

Вы можете использовать эту библиотеку: просто аннотировать строки SVG с идентификаторами исходного и целевого элементов. Он использует MutationObserver для наблюдения за изменениями связанных элементов.