Package csb :: Package bio :: Package io :: Module svg |
[frames] | no frames]

# Source Code for Module csb.bio.io.svg

      1  """
      2  Build SVG diagrams from various csb objects.
      3  """
      4
      5  import math
      6  import csb.core
      7
      8  from csb.bio.structure import SecondaryStructure, SecStructures
      9
     10
     11 -class SSCartoonBuilder(object):
     12      """
     13      Creates 2D vector diagrams from L{SecondaryStructure} objects.
     14
     15      @param ss: source secondary structure (either a SS string or a SS object)
     16      @type ss: str or L{SecondaryStructure}
     17      @param width: output width of the diagram in pixels
     18      @type width: int
     19      @param height: output height of the diagram in pixels
     20      @type height: int
     21
     22      @param thickness: stroke-width (2px by default)
     23      @param helix: SVG color for helicies (red by default)
     24      @param strand: SVG color for strands (blue by default)
     25      @param coil: SVG color for coils (orange by default)
     26      @param gap: SVG color for gaps (grey by default)
     27      @param cap: stroke-linecap (round by default)
     28      """
     29
     30 -    def __init__(self, ss, width, height, thickness='2px',
     31                   helix='#C24641', strand='#6698FF', coil='#FF8C00', gap='#E0E0E0',
     32                   cap='round'):
     33
     34          if ss:
     35              if isinstance(ss, csb.core.string):
     36                  self._ss = SecondaryStructure(ss)
     37              else:
     38                  self._ss = ss.clone()
     39              self._ss.to_three_state()
     40              self._residues = sum(e.length for e in self._ss)
     41              if self._residues == 0:
     42                  raise ValueError('Zero-length secondary structure')
     43          else:
     44              raise ValueError('Invalid secondary structure')
     45
     46          self.thickness = thickness
     47          self.helixcolor = helix
     48          self.strandcolor = strand
     49          self.coilcolor = coil
     50          self.gapcolor = gap
     51          self.cap = cap
     52
     53          self._realwidth = float(width)
     54          self._width = self._realwidth - 2 # this is to compensate for antialiasing and rounded caps
     55          self._height = float(height)
     56          self._x = 0
     57          self._y = 0
     58
     59          self._svg = ''
     60
     61 -    def build(self):
     62          """
     63          Build a SVG image using the current size and color settings.
     64
     65          @return: SVG diagram
     66          @rtype: str (SVG document)
     67          """
     68
     69          self._x = 0
     70          self._y = 0
     71          self._svg = [r'''<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
     72      width="{0._realwidth}" height="{0._height}">
     73
     74      <g transform="translate(0, {1})">'''.format(self, self._height / 2.0)]
     75
     76          for e in self._ss:
     77
     78              if e.type == SecStructures.Helix:
     79                  cartoon = self._helix(e.length)
     80                  color = self.helixcolor
     81
     82              elif e.type == SecStructures.Strand:
     83                  cartoon = self._strand(e.length)
     84                  color = self.strandcolor
     85
     86              elif e.type == SecStructures.Coil:
     87                  cartoon = self._coil(e.length)
     88                  color = self.coilcolor
     89
     90              elif e.type == SecStructures.Gap:
     91                  cartoon = self._gap(e.length)
     92                  color = self.gapcolor
     93
     94              else:
     95                  assert False, "Unhandled SS Type: {0!r}".format(e.type)
     96
     97              path = r'''        <path fill="none" stroke="{0}" stroke-width="{1.thickness}" stroke-linecap="{1.cap}"
     98              d="{2}" />'''.format(color, self, cartoon)
     99
    100              self._svg.append(path)
    101
    102          self._svg.append('    </g>')
    103          self._svg.append('</svg>')
    104          return '\n'.join(self._svg)
    105
    106 -    def _format(self, path):
    107
    108          formatted = []
    109
    110          for i in path:
    111
    112              if i == -0:
    113                  i = 0
    114
    115              if isinstance(i, float):
    116                  i = round(i, ndigits=7)
    117                  if i == -0:
    118                      i = 0
    119                  formatted.append('{0:.7f}'.format(i))
    120              else:
    121                  formatted.append(str(i))
    122
    123          return ' '.join(formatted)
    124
    125 -    def _helix(self, length, arc_width=3.0):
    126
    127          if length < 1:
    128              return ''
    129
    130          helix_width = float(length) * self._width / self._residues
    131          helix_end = self._x + helix_width
    132          path = ['M', self._x, self._y, 'Q']
    133
    134          arcs = int(helix_width / arc_width)
    135          for i in range(1, arcs + 1):
    136
    137              # quadratic bezier control points: sine curve's min, max and inflection points (0, 1, 0, -1, 0, 1 ...)
    138              # one arc is the curve from 0 to pi/2
    139              if i < arcs:
    140                  # inner arc
    141                  self._x += arc_width
    142                  self._y = math.sin(math.pi * i / 2) * (self._height / 2.0)
    143                  path.append(self._x)
    144                  path.append(self._y)
    145              else:
    146                  # last arc; stretch it to make the helix pixel-precise, ending also at y=0
    147                  # also the number of arcs/controlpoints must be even, otherwise the path is broken
    148
    149                  # remaining pixels on x
    150                  remainder = helix_end - self._x
    151
    152                  if i % 2 == 0:
    153                      # even number of arcs, just extend the last arc with the remainder
    154                      self._x += remainder
    155                      self._y = 0
    156                      path.append(self._x)
    157                      path.append(self._y)
    158                  else:
    159                      # odd number of arcs
    160
    161                      #  1) keep this arc at the expected y, but stretch it half of the x remainder
    162                      self._x += remainder / 2.0
    163                      self._y = math.sin(math.pi * i / 2) * (self._height / 2.0)
    164                      path.append(self._x)
    165                      path.append(self._y)
    166
    167                      #  2) append a final arc, ending at [helix_end, 0]
    168                      self._x += remainder / 2.0
    169                      self._y = 0
    170                      path.append(self._x)
    171                      path.append(self._y)
    172
    173          return self._format(path)
    174
    175 -    def _strand(self, length, arrow_width=3.0):
    176
    177          offset = 1.0
    178          strand_width = float(length) * self._width / self._residues
    179          path = ['M', self._x, self._y, 'H']
    180
    181          self._x += strand_width
    182          path.append(self._x)
    183
    184          if offset < arrow_width < strand_width:
    185              arrow_start = self._x - offset - arrow_width
    186              path.extend(['M', self._x - offset, self._y])
    187              path.extend(['L', arrow_start, self._y + self._height / 9])
    188              path.extend(['L', arrow_start, self._y - self._height / 9])
    189              path.extend(['L', self._x - offset, self._y])
    190
    191          return self._format(path)
    192
    193 -    def _coil(self, length):
    194
    195          coil_width = float(length) * self._width / self._residues
    196          path = ['M', self._x, self._y, 'Q']
    197
    198          # first control point
    199          self._x += coil_width / 2.0
    200          self._y = self._height / -2.0
    201          path.append(self._x)
    202          path.append(self._y)
    203
    204          # second
    205          self._x += coil_width / 2.0
    206          self._y = 0
    207          path.append(self._x)
    208          path.append(self._y)
    209
    210          return self._format(path)
    211
    212 -    def _gap(self, length):
    213
    214          return self._strand(length, arrow_width=0)
    215