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

StackOverflow https://stackoverflow.com/questions/554167

  •  23-08-2019
  •  | 
  •  

Вопрос

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

Это было полезно?

Решение

У вас есть пара вариантов: svg или холст.

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

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

Редактировать: демо-версия устарела.

Редактировать:Игнорируйте этот ответ, @Фил Эйч прибил его к ногтю

Другие советы

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

screenshot

В нем перечислены все интервалы на странице (возможно, захочется ограничить их только теми, у которых идентификаторы начинаются с T, если это подходит), и используется атрибут 'ids' для построения списка ссылок.Используя элемент canvas за пролетами, он рисует дугообразные стрелки попеременно над и под пролетами для каждого исходного пролета.

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

Будущие модификации, насколько я могу видеть:

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

[Редактировать Декабрь 2011:Исправлено, спасибо @Palo]

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

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

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

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

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

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

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

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

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

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

Теперь мы можем приступить к сборке arrow divs.Например, чтобы изменить стиль стрелки с "требуется" на "промоутер" в вашем примере выше, вы могли бы сделать левую, нижнюю и правую границы 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-страницу.Это довольно опрятно.

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

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

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

Мне нужно было аналогичное решение, и я изучал Библиотека 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];
}

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top