javascript - 根据网页和 SQL 数据库中的坐标创建 svg 元素

标签 javascript html css svg graphics

大家好,

我有一个类似于 Drawing connective elements between fixed positions on several html elements 的问题,但我正在寻找一个适用于 svg 的解决方案(不幸的是,自从我发布问题到现在,我没有时间处理这个元素,所以我仍然是 Web 开发的初学者)。

我正在尝试在我的 Web 应用程序中可视化不同 DNA 序列的对齐情况(django 是我的 Web 框架)。每个 DNA 序列都显示为 <div> 中的图表。 。每个图都包含代表 DNA 序列中子序列的子元素(黄色框)。我通过循环遍历包含从数据库获取的原始值的字典来创建图表。字典是这样构建的:

{'1': {'organism': 'sequence 1', 'env_length': 10000, 'frame': '+', 'alns': {'aln_1': {'s_id': 2, 'q_id': 1, 'aln_len': 2700, 'q_start': 1800, 'q_end': 4500, 's_start': 1100, 's_end': 3800}, 'aln_2': {'s_id': 2, 'q_id': 1, 'aln_len': 500, 'q_start': '8000', 'q_end': 8500, 's_start': 7000, 's_end': 7500}}, 'env_genes': {'g1': {'env_name': 'gene_1', 'env_start': 1850, 'env_end': 4700, 'env_strand': '+', 'gene_class': 'normal'}, 'g2': {'env_name': 'gene_2', 'env_start': 8000, 'env_end': 8500, 'env_strand': '+', 'gene_class': 'normal'}}}, '2': {'organism': 'sequence 2', 'env_length': 8500, 'frame': '+', 'alns': {'aln_1': {'s_id': 3, 'q_id': 2, 'aln_len': 2700, 'q_start': 1100, 'q_end': 3800, 's_start': 1, 's_end': 2700, 's_strand': '+'}}, 'env_genes': {'g1': {'env_name': 'gene_1', 'env_start': 1100, 'env_end': 1500, 'env_strand': '+', 'gene_class': 'normal'}, 'g2': {'env_name': 'gene_2', 'env_start': 1600, 'env_end': 1800, 'env_strand': '+', 'gene_class': 'normal'}}}, '3': {'organism': 'sequence 3', 'env_length': 3000, 'frame': '+', 'env_genes': {'g1': {'env_name': 'gene_1', 'env_start': 1, 'env_end': 2700, 'env_strand': '+'}}}}

元素在 html 中是这样创建的:

<div class="graph" style='--graph-length: {{value.env_length}}'>
          <hr class="line seq">
          {% for key2, value2 in value.items %}
            {% for key3, value3 in value2.items %}
              {% if value3.env_start %}
                {% if value3.env_strand == '+' %}
                  <span class='env_gene_right {{ value3.gene_class }}' style="--start: {{ value3.env_start }}; --stop: {{ value3.env_end }};"></span>
                {% else %}
                  <span class='env_gene_left {{ value3.gene_class }}' style="--start: {{ value3.env_start }}; --stop: {{ value3.env_end }};"></span>
                {% endif %}
              {% endif %}
            {% endfor %}
          {% endfor %}
</div>

并使用 CSS 进行样式设置,如下所示:

.graph { position:relative; }

.env_gene_right {
  position: absolute;
  top: 0;
  width: calc((var(--stop) - var(--start)) / var(--graph-length)*100% - 18px);
  left: calc(var(--start) / var(--graph-length)*100%);
  height: 2em;
  background-color: cornflowerblue;
  display: block;
  border-radius: 3px;
  border: 1px solid cornflowerblue;
}

.env_gene_right:after {
  content: " ";
  position: absolute;
  right: -15px;
  top: 0px;
  border-top: 15px solid transparent;
  border-right: none;
  border-left: 15px solid cornflowerblue;
  border-bottom: 15px solid transparent;
}

.env_gene_left {
  position: absolute;
  top: 0;
  width: calc((var(--stop) - var(--start)) / var(--graph-length)*100% - 18px);
  left: calc(var(--start) / var(--graph-length) * 100%);
  height: 2em;
  background-color: cornflowerblue;
  display: block;
  border-radius: 3px;
  border: 1px solid cornflowerblue;
}

.env_gene_left:before {
  content: " ";
  position: absolute;
  left: -15px;
  top: 0px;
  border-top: 15px solid transparent;
  border-right: 15px solid cornflowerblue;
  border-left: none;
  border-bottom: 15px solid transparent;
}
.line {
  position: absolute;
  left: 0;
  right: 0;
  height: 0px;
}

每个图表可能代表不同实际长度的序列,但浏览器中显示的图表长度是相同的。由于这些序列可能相似,我想通过连接两个图的 svg 元素(蓝色)来可视化序列相似的位置,如下所示:

enter image description here

我的问题是:如何最好地创建连接 svg 图形(蓝色)?我想使用 <svg polygon\> ,但是我真的不知道如何获取相对于图形在网站上放置位置的多边形的实际坐标(例如第一个蓝色元素:如果我的 block 的起始值为 1800,结束值为 4500(这些值保存在我的 sqlite3 数据库中),如何计算应在网站上绘制多边形的坐标?)。我尝试使用一些javascript,例如 document.getElementsByClassName获取浏览器窗口中图形的开始和结束位置(以 px 为单位),然后重新计算对应于一个碱基对(DNA 序列的长度单位)的像素数,并尝试从中获取位置,但 svg 没有完全显示。

另一个想法是创建所有 divssvg内元素,然后创建 svg polygons在这个更大的里面svg 。但在这种情况下,我如何获得 svg 中图形的位置?我可以使用 Javascript 查询选择器吗?

最后:使用 svg、html 和 javascript/jquery 执行此操作的最佳方法是什么?例如,涵盖类似内容的教程也会有所帮助。我想使用 svg 执行此操作的原因是用户应该能够与每个特定元素进行交互(例如在黄色框、蓝色多边形等上执行单击或悬停操作)。如果使用另一个 javascript 库可以实现这一点,我绝对愿意。

每一个提示都值得赞赏!

编辑:svgs 必须能够相互重叠!

最佳答案

不仅仅是尝试组合 DIV 和图像(这意味着面临一些对齐和缩放问题),我强烈建议使用单个库来绘制整个图像。

我的示例使用 JS Canvas 库(内置在所有较新的浏览器中),它需要提供将图像绘制到页面中的 JavaScript 所需的所有原始数据:在我的示例中,我使用了单个 输入 JSON。

在开始我的示例之前:

  • 我必须承认我不明白黄色矩形的含义,我称它们为证据
  • 我认为在 graph3 中存在一个拼写错误:相似的结束边缘位于 2700,而长度为 2000,在我的示例中,我将长度更改为 4000
  • 我自由地添加了 graph1 和 graph3 中的另一对类似的夫妇

const input = {
  graphs: [{
      name: "graph1",
      length: 10000,
      evidences: [{
          from: 100,
          length: 1000
        },
        {
          from: 1800,
          length: 1000
        },
        {
          from: 3000,
          length: 1000
        },
        {
          from: 6000,
          length: 1000
        },
        {
          from: 8000,
          length: 500
        },
        {
          from: 9300,
          length: 500
        },
      ],
    },
    {
      name: "graph2",
      length: 8500,
      evidences: [{
          from: 1100,
          length: 1000
        },
        {
          from: 2500,
          length: 1000
        },
        {
          from: 5000,
          length: 1000
        },
        {
          from: 7000,
          length: 500
        },
      ],
    },
    {
      name: "graph3",
      length: 4000,
      evidences: [{
          from: 1,
          length: 2700
        },
        {
          from: 3200,
          length: 500
        },
      ],
    },
  ],
  similar: [{
      first: 0,
      second: 1,
      from: [1800, 1100],
      length: 2700,
      color: "#8080ff",
    },
    {
      first: 1,
      second: 2,
      from: [1100, 1],
      length: 2700,
      color: "#8080ff",
    },
    {
      first: 0,
      second: 1,
      from: [8000, 7000],
      length: 500,
      color: "#8080ff",
    },
    {
      first: 0,
      second: 2,
      from: [9300, 3200],
      length: 500,
      color: "#ff8080",
    },
  ],
};

const imgScale = window.devicePixelRatio;

function main() {
  // init
  const nameWidth = 100;
  const lengthWidth = 70;
  const graphHight = 50;
  const canvas = document.getElementById("canvas");
  canvas.style.height = graphHight * input.graphs.length + "px";
  const {
    clientWidth,
    clientHeight
  } = canvas;
  const width = (canvas.width = clientWidth * imgScale);
  const height = (canvas.height = clientHeight * imgScale);
  const graphWidth = width - (nameWidth + lengthWidth) * imgScale;
  var ctx = canvas.getContext("2d");
  // white fill canvas
  ctx.fillStyle = "#ffffff";
  ctx.fillRect(0, 0, clientWidth, clientHeight);
  // draw each similar
  for (let i = 0; i < input.similar.length; ++i) {
    const {
      first,
      second,
      from,
      length,
      color
    } = input.similar[i];
    const middleFirst = graphHight * (first + 0.5) * imgScale;
    const middleSecond = graphHight * (second + 0.5) * imgScale;
    const fromScaleFirst =
      (from[0] * graphWidth) / input.graphs[first].length;
    const lengthScaleFirst =
      (length * graphWidth) / input.graphs[first].length;
    const fromScaleSecond =
      (from[1] * graphWidth) / input.graphs[second].length;
    const lengthScaleSecond =
      (length * graphWidth) / input.graphs[second].length;
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.moveTo(nameWidth * imgScale + fromScaleFirst, middleFirst);
    ctx.lineTo(
      nameWidth * imgScale + fromScaleFirst + lengthScaleFirst,
      middleFirst
    );
    ctx.lineTo(
      nameWidth * imgScale + fromScaleSecond + lengthScaleSecond,
      middleSecond
    );
    ctx.lineTo(nameWidth * imgScale + fromScaleSecond, middleSecond);
    ctx.closePath();
    ctx.fill();
    populatePolygons({
      points: [
        [nameWidth * imgScale + fromScaleFirst, middleFirst],
        [
          nameWidth * imgScale + fromScaleFirst + lengthScaleFirst,
          middleFirst,
        ],
        [
          nameWidth * imgScale + fromScaleSecond + lengthScaleSecond,
          middleSecond,
        ],
        [nameWidth * imgScale + fromScaleSecond, middleSecond],
      ],
      object: {
        type: "similar",
        first,
        second,
        from,
        length
      },
    });
  }
  // write each similar edge
  ctx.fillStyle = "#008000";
  ctx.font = "20px Arial";
  ctx.textBaseline = "middle";
  ctx.textAlign = "center";
  for (let i = 0; i < input.similar.length; ++i) {
    const {
      first,
      second,
      from,
      length,
      color
    } = input.similar[i];
    const middleFirst = graphHight * (first + 0.5) * imgScale;
    const middleSecond = graphHight * (second + 0.5) * imgScale;
    const fromScaleFirst =
      (from[0] * graphWidth) / input.graphs[first].length;
    const lengthScaleFirst =
      (length * graphWidth) / input.graphs[first].length;
    const fromScaleSecond =
      (from[1] * graphWidth) / input.graphs[second].length;
    const lengthScaleSecond =
      (length * graphWidth) / input.graphs[second].length;
    ctx.fillText(
      from[0],
      nameWidth * imgScale + fromScaleFirst,
      middleFirst - 20
    );
    ctx.fillText(
      from[0] + length,
      nameWidth * imgScale + fromScaleFirst + lengthScaleFirst,
      middleFirst - 20
    );
    ctx.fillText(
      from[1],
      nameWidth * imgScale + fromScaleSecond,
      middleSecond - 20
    );
    ctx.fillText(
      from[1] + length,
      nameWidth * imgScale + fromScaleSecond + lengthScaleSecond,
      middleSecond - 20
    );
  }
  //draw each graph
  ctx.strokeStyle = "#000000";
  for (let i = 0; i < input.graphs.length; ++i) {
    const graph = input.graphs[i];
    const middle = graphHight * (i + 0.5) * imgScale;
    ctx.beginPath();
    ctx.moveTo(nameWidth * imgScale, middle);
    ctx.lineTo(nameWidth * imgScale + graphWidth, middle);
    ctx.stroke();
    ctx.fillStyle = "#000000";
    ctx.textAlign = "left";
    ctx.fillText(graph.name, 10, middle);
    ctx.textAlign = "right";
    ctx.fillText("1", (nameWidth - 10) * imgScale, middle);
    ctx.fillText(graph.length, width - 10, middle);
    // draw each evidence
    ctx.fillStyle = "#ffff80";
    for (let l = 0; l < graph.evidences.length; ++l) {
      const {
        from,
        length
      } = graph.evidences[l];
      const fromScale = (from * graphWidth) / graph.length;
      const lengthScale = (length * graphWidth) / graph.length;
      ctx.fillRect(
        fromScale + nameWidth * imgScale,
        middle - 10,
        lengthScale,
        20
      );
      ctx.beginPath();
      ctx.rect(
        fromScale + nameWidth * imgScale,
        middle - 10,
        lengthScale,
        20
      );
      ctx.stroke();
      populatePolygons({
        points: [
          [fromScale + nameWidth * imgScale, middle - 10],
          [fromScale + nameWidth * imgScale + lengthScale, middle - 10],
          [fromScale + nameWidth * imgScale + lengthScale, middle + 10],
          [fromScale + nameWidth * imgScale, middle + 10],
        ],
        object: {
          type: "evidence",
          graph: i,
          evidence: l,
        },
      });
    }
  }
}

const polygons = [];

function populatePolygons(polygon) {
  polygons.push(polygon);
}

function inside(point, vs) {
  var x = point[0],
    y = point[1];

  var inside = false;
  for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
    var xi = vs[i][0],
      yi = vs[i][1];
    var xj = vs[j][0],
      yj = vs[j][1];

    var intersect =
      yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
    if (intersect) inside = !inside;
  }

  return inside;
}

function mouseEvent(kind, event) {
  const {
    offsetX,
    offsetY
  } = event;

  for (polygon in polygons) {
    const {
      object,
      points
    } = polygons[polygon];
    if (inside([offsetX * imgScale, offsetY * imgScale], points))
      interaction(kind, object);
  }
}

function interaction(kind, object) {
  const {
    type,
    graph,
    evidence,
    first,
    second,
    from,
    length
  } = object;

  if (type == "evidence")
    console.log(
      `${kind} on evidence nr ${evidence + 1} of graph nr ${graph + 1}`
    );
  else
    console.log(
      `${kind} on similar between graph nr ${first + 1} from ${
            from[0]
          } and graph nr ${second + 1} from ${from[1]} long ${length}`
    );
}

main();
<canvas id="canvas" style="width: 100%" onclick="mouseEvent('click', event)" onmousemove="mouseEvent('move', event)" />

这将为您提供一个也可以复制到剪贴板的图像;尝试:

  1. 左键单击“运行代码片段”按钮
  2. 右键单击结果图像
  3. 选择“复制图像”上下文菜单(Windows chrome)
  4. 粘贴到任何接受剪贴板图像的程序中

编辑:

基于新的请求(用户需要与图像交互),使用单个库使一切变得更加简单,这并没有改变。

使用很棒的point-in-polygon我们也可以通过算法轻松实现这个更多目标。

我添加了一个简单的控制台日志作为概念验证,您可以随意附加任何其他内容。

不幸的是,console.log 几乎占用了“运行代码片段”的所有空间,以获得更好的体验:

  1. 编辑此答案
  2. 点击编辑预览中代码段之外的“编辑上述代码段”链接
  3. 按“运行”按钮

关于javascript - 根据网页和 SQL 数据库中的坐标创建 svg 元素,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64878820/

相关文章:

javascript - `\n` 字符不在 jquery 确认对话框中呈现新行

html - nav div 不会全屏显示

javascript - 具有给定 html 类 ID 参数的动态 slideToggle 函数

html - 用透明背景颜色覆盖背景图像(全屏)

javascript - 如何停止 .mouseenter() 效果

css - 将元素放在绝对 div 元素之后

java - 如何为 NetBeans 创建自定义语言解析器?

javascript - NodeJS 中超出范围的对象

html - 已部署的 Steroid.js 应用未出现在 AppGyver 服务中

javascript - 使用纯javascript获取li元素onclick而不将onClick应用于每个元素