Add text-rotation options to last-dotplot
authorMartin C. Frith
Tue Oct 03 18:25:07 2017 +0900 (2017-10-03)
changeset 87820f5c97a3cfd
parent 877 f5f93e51ef53
child 879 2c0cb7ef8842
Add text-rotation options to last-dotplot
doc/last-dotplot.txt
scripts/last-dotplot
     1.1 --- a/doc/last-dotplot.txt	Tue Oct 03 11:10:47 2017 +0900
     1.2 +++ b/doc/last-dotplot.txt	Tue Oct 03 18:25:07 2017 +0900
     1.3 @@ -2,8 +2,9 @@
     1.4  ============
     1.5  
     1.6  This script makes a dotplot, a.k.a. Oxford Grid, of pair-wise sequence
     1.7 -alignments in MAF or LAST tabular format.  It requires the Python
     1.8 -Imaging Library to be installed.  It can be used like this::
     1.9 +alignments in MAF or LAST tabular format.  It requires the `Python
    1.10 +Imaging Library <https://pillow.readthedocs.io/>`_ to be installed.
    1.11 +It can be used like this::
    1.12  
    1.13    last-dotplot my-alignments my-plot.png
    1.14  
    1.15 @@ -13,7 +14,11 @@
    1.16  
    1.17  To get a nicer font, try something like::
    1.18  
    1.19 -  last-dotplot -f /usr/share/fonts/truetype/freefont/FreeSans.ttf alns alns.png
    1.20 +  last-dotplot -f /usr/share/fonts/liberation/LiberationSans-Regular.ttf alns alns.png
    1.21 +
    1.22 +or::
    1.23 +
    1.24 +  last-dotplot -f /Library/Fonts/Arial.ttf alns alns.png
    1.25  
    1.26  If the fonts are located somewhere different on your computer, change
    1.27  this as appropriate.
    1.28 @@ -95,6 +100,10 @@
    1.29        TrueType or OpenType font file.
    1.30    -s SIZE, --fontsize=SIZE
    1.31        TrueType or OpenType font size.
    1.32 +  --rot1=ROT
    1.33 +      Text rotation for the 1st genome: h(orizontal) or v(ertical).
    1.34 +  --rot2=ROT
    1.35 +      Text rotation for the 2nd genome: h(orizontal) or v(ertical).
    1.36    --lengths1
    1.37        Show sequence lengths for the 1st (horizontal) genome.
    1.38    --lengths2
     2.1 --- a/scripts/last-dotplot	Tue Oct 03 11:10:47 2017 +0900
     2.2 +++ b/scripts/last-dotplot	Tue Oct 03 18:25:07 2017 +0900
     2.3 @@ -163,13 +163,17 @@
     2.4      parts[1::2] = map(int, parts[1::2])
     2.5      return parts
     2.6  
     2.7 -def get_text_sizes(my_strings, font, fontsize, image_mode):
     2.8 +def textDimensions(imageDraw, font, textRot, text):
     2.9 +    x, y = imageDraw.textsize(text, font=font)
    2.10 +    return (y, x) if textRot else (x, y)
    2.11 +
    2.12 +def get_text_sizes(my_strings, font, fontsize, image_mode, textRot):
    2.13      '''Get widths & heights, in pixels, of some strings.'''
    2.14      if fontsize == 0: return [(0, 0) for i in my_strings]
    2.15      image_size = 1, 1
    2.16      im = Image.new(image_mode, image_size)
    2.17      draw = ImageDraw.Draw(im)
    2.18 -    return [draw.textsize(i, font=font) for i in my_strings]
    2.19 +    return [textDimensions(draw, font, textRot, i) for i in my_strings]
    2.20  
    2.21  def sizeText(size):
    2.22      suffixes = "bp", "kb", "Mb", "Gb"
    2.23 @@ -184,7 +188,7 @@
    2.24      return seqName + ": " + sizeText(seqSize)
    2.25  
    2.26  def getSeqInfo(sortOpt, seqNames, seqLimits,
    2.27 -               font, fontsize, image_mode, isShowSize):
    2.28 +               font, fontsize, image_mode, isShowSize, textRot):
    2.29      '''Return miscellaneous information about the sequences.'''
    2.30      if sortOpt == 1:
    2.31          seqNames.sort(key=natural_sort_key)
    2.32 @@ -202,8 +206,9 @@
    2.33          seqLabels = map(seqNameAndSizeText, seqNames, seqSizes)
    2.34      else:
    2.35          seqLabels = seqNames
    2.36 -    labelSizes = get_text_sizes(seqLabels, font, fontsize, image_mode)
    2.37 -    margin = max(zip(*labelSizes)[1])  # maximum text height
    2.38 +    labelSizes = get_text_sizes(seqLabels, font, fontsize, image_mode, textRot)
    2.39 +    margin = max(i[1] for i in labelSizes)
    2.40 +    # xxx the margin may be too big, because some labels may get omitted
    2.41      return seqNames, seqSizes, seqLabels, labelSizes, margin
    2.42  
    2.43  def div_ceil(x, y):
    2.44 @@ -413,11 +418,11 @@
    2.45  
    2.46  def make_label(text, text_size, range_start, range_size):
    2.47      '''Return an axis label with endpoint & sort-order information.'''
    2.48 -    text_width  = text_size[0]
    2.49 +    text_width, text_height = text_size
    2.50      label_start = range_start + (range_size - text_width) // 2
    2.51      label_end   = label_start + text_width
    2.52      sort_key    = text_width - range_size
    2.53 -    return sort_key, label_start, label_end, text
    2.54 +    return sort_key, label_start, label_end, text, text_height
    2.55  
    2.56  def get_nonoverlapping_labels(labels, label_space):
    2.57      '''Get a subset of non-overlapping axis labels, greedily.'''
    2.58 @@ -428,21 +433,23 @@
    2.59              nonoverlapping_labels.append(i)
    2.60      return nonoverlapping_labels
    2.61  
    2.62 -def get_axis_image(seqNames, name_sizes, seq_starts, seq_pix,
    2.63 -                   font, image_mode, opts):
    2.64 +def get_axis_image(seqNames, name_sizes, seq_starts, seq_pix, textRot,
    2.65 +                   textAln, font, image_mode, opts):
    2.66      '''Make an image of axis labels.'''
    2.67      min_pos = seq_starts[0]
    2.68      max_pos = seq_starts[-1] + seq_pix[-1]
    2.69 -    height = max(zip(*name_sizes)[1])
    2.70 +    margin = max(i[1] for i in name_sizes)
    2.71      labels = map(make_label, seqNames, name_sizes, seq_starts, seq_pix)
    2.72      labels = [i for i in labels if i[1] >= min_pos and i[2] <= max_pos]
    2.73      labels.sort()
    2.74 -    labels = get_nonoverlapping_labels(labels, opts.label_space)
    2.75 -    image_size = max_pos, height
    2.76 +    minPixTweenLabels = 0 if textRot else opts.label_space
    2.77 +    labels = get_nonoverlapping_labels(labels, minPixTweenLabels)
    2.78 +    image_size = (margin, max_pos) if textRot else (max_pos, margin)
    2.79      im = Image.new(image_mode, image_size, opts.border_color)
    2.80      draw = ImageDraw.Draw(im)
    2.81      for i in labels:
    2.82 -        position = i[1], 0
    2.83 +        base = margin - i[4] if textAln else 0
    2.84 +        position = (base, i[1]) if textRot else (i[1], base)
    2.85          draw.text(position, i[3], font=font, fill=opts.text_color)
    2.86      return im
    2.87  
    2.88 @@ -466,12 +473,14 @@
    2.89      warn("done")
    2.90      if not alignments: raise Exception("there are no alignments")
    2.91  
    2.92 +    textRot1 = "vertical".startswith(opts.rot1)
    2.93      i1 = getSeqInfo(opts.sort1, seqNames1, seqLimits1,
    2.94 -                    font, opts.fontsize, image_mode, opts.lengths1)
    2.95 +                    font, opts.fontsize, image_mode, opts.lengths1, textRot1)
    2.96      seqNames1, seqSizes1, seqLabels1, labelSizes1, tMargin = i1
    2.97  
    2.98 +    textRot2 = "horizontal".startswith(opts.rot2)
    2.99      i2 = getSeqInfo(opts.sort2, seqNames2, seqLimits2,
   2.100 -                    font, opts.fontsize, image_mode, opts.lengths2)
   2.101 +                    font, opts.fontsize, image_mode, opts.lengths2, textRot2)
   2.102      seqNames2, seqSizes2, seqLabels2, labelSizes2, lMargin = i2
   2.103  
   2.104      warn("choosing bp per pixel...")
   2.105 @@ -530,10 +539,13 @@
   2.106  
   2.107      if opts.fontsize != 0:
   2.108          axis1 = get_axis_image(seqLabels1, labelSizes1, seq_starts1, seq_pix1,
   2.109 -                               font, image_mode, opts)
   2.110 +                               textRot1, False, font, image_mode, opts)
   2.111 +        if textRot1:
   2.112 +            axis1 = axis1.transpose(Image.ROTATE_90)
   2.113          axis2 = get_axis_image(seqLabels2, labelSizes2, seq_starts2, seq_pix2,
   2.114 -                               font, image_mode, opts)
   2.115 -        axis2 = axis2.transpose(Image.ROTATE_270)  # !!! bug hotspot
   2.116 +                               textRot2, textRot2, font, image_mode, opts)
   2.117 +        if not textRot2:
   2.118 +            axis2 = axis2.transpose(Image.ROTATE_270)
   2.119          im.paste(axis1, (0, 0))
   2.120          im.paste(axis2, (0, 0))
   2.121  
   2.122 @@ -592,6 +604,10 @@
   2.123                    help="TrueType or OpenType font file")
   2.124      og.add_option("-s", "--fontsize", metavar="SIZE", type="int", default=11,
   2.125                    help="TrueType or OpenType font size (default: %default)")
   2.126 +    og.add_option("--rot1", metavar="ROT", default="h",
   2.127 +                  help="text rotation for the 1st genome (default=%default)")
   2.128 +    og.add_option("--rot2", metavar="ROT", default="v",
   2.129 +                  help="text rotation for the 2nd genome (default=%default)")
   2.130      og.add_option("--lengths1", action="store_true",
   2.131                    help="show sequence lengths for the 1st (horizontal) genome")
   2.132      og.add_option("--lengths2", action="store_true",