javascripthtmlcsssvgvisualization

Drawing connective elements between fixed positions on several html elements


I am quite new to web programming, so please bear with me if I am missing something obvious. I am using html, css and javascript to visualize different DNA sequences. Each sequence is represented as a graph of html elements and has a fixed length corresponding to the length of the DNA sequence. Spans are positioned at fixed points in each graph to represent certain motifs. The code for one such graph looks like this (using django as my web framework):

<div class="graph" style='--graph-length: {{sequence_length}}'>
        <hr class="line">
      {% for key, value2 in value.items %}
        {% if value2.start_position %}
            <span class='motif' style="--start: {{ value2.start_position }}; --stop: {{ value2.stop_position }};"></span>
        {% endif %}
      {% endfor %}
      </div>

Like this, I create representations of several sequences on my webpage, one below the other. Now some of these sequences may be similar to one another. I want to graphically display the regions over which a sequence is similar to the one displayed below it, like this: enter image description here

In the above example, the sequences represented by graph1 and 2 and graph2 and 3 have a similar region, but the total length of the sequences (and thus the displayed size of motifs represented on a graph) differs, resulting in the wider blue element between graph2 and 3.

So what I have for each graph are the sequence length each graph should represent, the start and end positions for each motif on the respective graph, and the start and end position for the similar region on each graph if two graphs (sequences) contain a similar region (green in the picture).

So my question is: How can I create the elements representing a similar region between two graphs (blue in the picture) using html, css or javascript? (Or really anything else that I can implement in what I already have)

Any hints on how I could approach this are apprechiated, let me know if you have more questions!

EDIT: I would actually prefer a solution in svg, as I think its more suitable for integration in my application


Solution

  • You could use svg, or canvas.

    Here is a minimalist approach with canvas. You may want to rely on a library which are very likely to offer diagrams close to your ones.

    But scratch never hurts.

    Below, each graph is responsible of scaling properly its axis.

    Eventually, the trapezes rely on the graph they depend on to scale the corresponding vertices.

    You may want to display the text even more conditionally so play with Bar and potentially give it a boolean to indicate whether it should be displayed or not.

    You can further style the canvas by playing on the weight of the stroke line and all, but it is just a demo to show you you can get it up easily

    const canvas = document.querySelector('canvas')
      let ctx = canvas.getContext('2d')
      let bars = [
        [Bar(200, 300), Bar(1800,2300), Bar(2500, 4500), Bar(5000,5200), Bar(8000,8500)],
        [Bar(1100,1300), Bar(3000, 3800), Bar(4000, 4200), Bar(7000, 7500)],
        [Bar(1, 2700)]
      ]
      function Bar(a,b){return [a, b]}
      class Graph{
        constructor ({anchorY, width, min, max}) {
          this.anchorY = anchorY
          this.width = width
          this.dw = width / (max - min )
          this.min = min
          this.max = max
        }
    
        plot (bars) {
          // plot bars
          // resize bars dimension to fit canvas
          const e = 5
          ctx.fillStyle = 'orange'
          const scaledBars = bars.map(([a, b]) => [ a, b, a * this.dw, b * this.dw ])
          scaledBars.forEach(([_, dum, left, right])=>{
            ctx.fillRect(left, this.anchorY - e, right - left, 2*e)
          })
    
          // plot line
          ctx.strokeStyle = 'black'
          ctx.beginPath()
          ctx.moveTo(0, this.anchorY)
          ctx.lineTo(this.width, this.anchorY)
          ctx.closePath()
          ctx.stroke()
    
          ctx.strokeStyle = 'green'
          ctx.font = '10px serif'
          scaledBars.forEach(([origLeft, origRight, left, right]) => {
            ctx.strokeText(origLeft, left, this.anchorY - 10)
            if (right - left > 100 ) {
              ctx.strokeText(origRight, right, this.anchorY - 10)
            }
          })
          
        }
    
        //x will be shifted automatically
        moveTo (x) {
          ctx.moveTo(x * this.dw, this.anchorY)
        }
    
    
        lineTo (x) {
          ctx.lineTo(x * this.dw, this.anchorY)
        }
      }
      const graphs = [
        new Graph({anchorY:100, width: canvas.width, min: 1, max: 10000}),
        new Graph({anchorY:200, width: canvas.width, min: 1, max: 8500}),
        new Graph({anchorY:300, width: canvas.width, min: 1, max: 4000})
      ]
      
      // g first graph, (a,b) left, right anchors in first graph
      // g2 second graph, c right anchor, d left anchor in second graph
      function trapeze(g, a, b, g2, c, d){
        ctx.beginPath()
        g.moveTo(a)
        g.lineTo(b)
        g2.lineTo(c)
        g2.lineTo(d)
        ctx.closePath()
        ctx.fillStyle = 'rgba(84, 147, 158, 0.5)'
        ctx.fill()
      }
    
      const [g1, g2, g3] = graphs
      const trapezes = [
        [g1, 1800, 4500, g2, 3800, 1100],
        [g1, 8000, 8500, g2, 7500, 7000],
        [g2, 1100, 3800, g3, 2700, 1],
      ]
      trapezes.forEach(t => trapeze(...t))
    
      graphs.forEach((g, i) => {
        g.plot(bars[i])
      })
    canvas{background:#eeeeee;}
    <canvas width="400" height="400"></canvas>