create svg elements based on coordinates from webpage and sql database

Viewed 186

Hej all,

I have a question that is similar to Drawing connective elements between fixed positions on several html elements, but I am looking for a solution that works for svg (I had no time to work on this project since I posted the question until now unfortunately, so I am still a total beginner in web development).

I am trying to visualize the alignment of different DNA sequences in my web application (django is my web framework). Each DNA sequence is displayed as a graph in a <div>. Each graph contains subelements (the yellow boxes) that represent subsequences in the DNA sequence. I create a graph by looping through a dictionary containing the raw values fetched from the database. The dictionary is built like this:

{'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': '+'}}}}

The elements are created like this in 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>

and styled using css like this:

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

Each of those graphs may represent sequences of different actual length, but the length of the graph displayed in the browser is the same. As these sequences may be similar, I want to visualize where the sequences are similar through svg elements (blue) that connects two graphs, like this:

enter image description here

My problem is: How do I best create the connecting svg figures (blue)? I was thinking to use an <svg polygon\>, but then I do not really know how to get the actual coordinates for the polygon relative to where the graph is placed on the website (e.g the first blue element: if my start value for the block is 1800 and the end is 4500 (these values are saved in my sqlite3 database), how do I calculate the coordinates where the polygon should be drawn on the website?). I have tried to use some javascript, e.g document.getElementsByClassName to get the start and end positions of the graph (in px) in the browser window, then recalculated how many pixels correspond to one basepair (the length unit for dna sequences) and tried to get the positions from that, but the svg did not show at all.

Another idea would be to create all divs within an svg element, and then create svg polygons inside this larger svg. But in that case, how do I get the positions of the graphs in the svg? Could I use Javascripts query selector?

So finally: what is the best way to do this using svg, html and javascript/jquery? E.g a tutorial covering similar things would also help. The reason I want to do this using svg is that the user should be able to interact with each specific element (such as performing click or on hover actions on the yellow boxes, blue polygons etc). If that is possible using another javascript library I am absolutely open to that.

Every hint is apprechiated!

EDIT: The svgs have to be able to overlap one another!

1 Answers

More than trying to combine DIVs and images (which means to face several alignment and scaling problems) I strongly suggest to use a single library to draw the entire image.

My example uses the JS canvas library (built-in in all newer browsers), which needs to provide all the raw data required to draw the image to the JavaScript in the page: in my example I used a single input JSON.

Before going to my example:

  • I have to admit I didn't understood the meaning of the yellow rectangles, I called them evidences;
  • I think in graph3 there is a typo: the similar ending edge is at 2700 while the length is 2000, in my example I changed the length to 4000
  • I taken the freedom to add another similar couple from graph1 and 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)" />

This will provide you a single image which could be copied to the clipboard too; to try:

  1. left click on "Run code snippet" button
  2. right click on the result image
  3. select "copy image" context menu (windows chrome)
  4. paste in any program which accepts images from the clipboard

Edit:

Based on new request (users need to interact with the image) it doesn't change that using one single library makes everything much simpler.

Using the awesome point-in-polygon algorithm we can achieve for easy this more target as well.

I added a simple console log as a proof-of-concept, you are free to attach anything else.

Unfortunately the console.log takes almost all the space of "Run code snippet", for a better experience:

  1. edit this answer
  2. click the "edit the above snippet" link beyond the snippet in the edit preview
  3. press the "Run" button
Related