Commits

James Taylor committed 9bab00b

A VERY ROUGH start on tools for building 'track' style user interfaces / plots.

  • Participants
  • Parent commits e4be951

Comments (0)

Files changed (7)

File bx/align/core.py

 import random
+#from bx import align
 
 # DNA reverse complement table
 DNA_COMP = "                                             -                  " \

File bx/bitset.pyx

     def get( self, index ):
         return bitReadOne( self.bits, index );
     
-    def count_range( self, start, count ):
+    def count_range( self, start=0, count=None ):
+        if count == None: count = self.bitCount
         return bitCountRange( self.bits, start, count )
 
     def next_set( self, start, end=None ):

File bx/tracks/__init__.py

+import gtk
+
+from matplotlib.axes import Subplot
+from matplotlib.figure import Figure
+from matplotlib.font_manager import fontManager, FontProperties
+from matplotlib.numerix import arange, sin, pi
+from matplotlib.patches import Rectangle
+from matplotlib.text import Text
+from matplotlib.transforms import Affine, Bbox, Value, Point, get_bbox_transform, unit_bbox, identity_transform
+
+# Some definitions related to display sizes
+DEFAULT_FONT_SIZE = 14.0 # points, should be determined on the fly from the font
+DEFAULT_CHAR_WIDTH = 8.0 # this is just a guess, need to determine it from font metrics
+POINTS_PER_INCH = 72.0
+
+# Height of a single 'track', based on character height
+TRACK_HEIGHT = DEFAULT_FONT_SIZE / POINTS_PER_INCH
+# Spacing between tracks
+TRACK_SPACE = TRACK_HEIGHT / 4.0
+
+class Track( object ):
+    """
+    Abstract base class for tracks
+    """
+    def __init__( self, name ):
+        self.name = name
+    def get_name( self ):
+        return self.name
+    def get_children( self ):
+        return []
+    def do_dialog( self ):
+        pass
+    
+class TrackManager( object ):
+    """
+    Manages a collection of tracks. Responsible for building a figure
+    and drawing / redrawing each track onto it.
+    """
+    
+    def __init__( self ):
+        self.tracks = []
+        
+    def set_range( self, start_pos, stop_pos ):
+        self.start_pos = start_pos
+        self.stop_pos = stop_pos
+        
+    def add_track( self, track ):
+        self.tracks.append( track )
+        
+    def build_figure( self ):
+    
+        total_track_height = sum( [ t.get_height( TRACK_HEIGHT ) for t in self.tracks ] )
+        total_track_height += TRACK_SPACE * ( len( self.tracks ) - 1 )
+    
+        # Build the figure
+        margin_l, margin_r, margin_t, margin_b = 2.5, 0.5, 0.5, 0.5
+    
+        # Width and height determined to fit the alignment
+        fig_height = margin_t + margin_b + total_track_height
+        fig_width = margin_r + margin_l + DEFAULT_CHAR_WIDTH * self.stop_pos / POINTS_PER_INCH
+
+        # Track height in figure relative coords
+        rel_track_height = TRACK_HEIGHT / fig_height
+        
+        fig = Figure( figsize=(fig_width,fig_height), frameon=True )
+    
+        top = 1 - ( margin_t / fig_height )
+    
+        for track in self.tracks:
+    
+            track_height = track.get_height( rel_track_height )
+    
+            ax_left = margin_l / fig_width
+            ax_width = ( fig_width - margin_l - margin_r ) / fig_width
+            ax_height = track_height
+            ax_bottom = top - ax_height
+            
+            ax = fig.add_axes( ( ax_left, ax_bottom, ax_width, ax_height ), frameon=False )
+            track.draw( ax, self.start_pos, self.stop_pos )
+            ax.set_xlim( ( self.start_pos, self.stop_pos ) )
+    
+            top -= ( ax_height + TRACK_SPACE )
+    
+        self.fig = fig
+        return fig
+        
+    def redraw( self ):
+        self.canvas.draw()

File bx/tracks/align.py

+from bx.tracks import *
+
+from matplotlib.patches import *
+from matplotlib.transforms import *
+from matplotlib.colors import colorConverter
+
+from itertools import *
+
+class AlignmentTrack( Track ):
+    """
+    Draws an alignment block
+    """
+    def __init__( self, name, block ):
+        Track.__init__( self, name )
+        self.block = block
+        self.underlays = []
+        
+    def add_underlay( self, underlay ):
+        self.underlays.append( underlay )
+        
+    def get_children( self ):
+        return self.underlays
+            
+    def get_height( self, row_height ):
+        return len( self.block.components ) * row_height
+        
+    def draw( self, ax, xmin, xmax ):
+        """
+        Draw the MAF into an axes
+        """
+        # Input bbox is the range of data cordinates
+        boxin = Bbox( Point( Value( xmin ), Value( 0 ) ), Point( Value( xmax ), Value( 1 ) ) )
+        # We will draw a row for each component
+        nrows = len( self.block.components )
+        # This is the row height in terms of device cordinates
+        row_height = ( ax.bbox.ur().y() - ax.bbox.ll().y() ) / Value( nrows )
+        # Build a transformation for each row and then draw it in
+        ticklocs, ticklabels = [], []
+        for i, comp in enumerate( self.block.components ):
+            # Out box is a 1/nrows chunk of the total area
+            boxout = Bbox( Point(ax.bbox.ll().x(), ax.bbox.ll().y() + Value( nrows - i - 1 ) * row_height ),
+                           Point(ax.bbox.ur().x(), ax.bbox.ll().y() + Value( nrows - i ) * row_height ) )
+            # Make a transformation between the two boxes
+            trans = get_bbox_transform( boxin, boxout)
+            # Todo, draw any 'underlays'
+            for underlay in self.underlays:
+                underlay.draw( comp.src, ax, trans )
+            # Draw each character of the alignment onto the axes
+            for j, ch in enumerate( comp.text ):
+                ax.text( j + 0.5, 0.5, ch,
+                         verticalalignment='center', 
+                         horizontalalignment='center',
+                         transform=trans,
+                         fontdict=dict(family='monospace') )
+            # Add a label
+            offset = ( nrows - i ) / (nrows+0.0) - ( 1 / ( 2.0 * nrows ) )
+            ticklocs.append( offset )
+            ticklabels.append( comp.src )
+        # Set labels
+        ax.yaxis.set_ticks( ticklocs )
+        ax.yaxis.set_ticklabels( ticklabels )
+        for t in ax.yaxis.get_major_ticks(): t.tick1On = t.tick2On = False
+        ax.xaxis.set_ticks( [] )
+            
+class AlignmentTrackUnderlay( Track ):
+    def __init__( self, name, threshold=0.0 ):
+        Track.__init__( self, name )
+        self.intervals = {}
+        self.threshold = threshold
+        self.patches = []
+        self.patch_scores = []
+    def set_patch_attributes( self, **kwargs ):
+        self.patch_attributes = kwargs        
+    def add_interval( self, src, start_col, end_col, score=1.0 ):
+        try:
+            self.intervals[src].append( ( start_col, end_col, score ) )
+        except KeyError:
+            self.intervals[src] = [ ( start_col, end_col, score ) ]
+    def draw( self, src, axes, transform ):
+        if src in self.intervals:
+            for start_col, end_col, score in self.intervals[src]:
+                r = Rectangle( ( start_col, 0 ), end_col - start_col, 1, **self.patch_attributes )
+                r.set_transform( transform )
+                axes.add_patch( r )
+                r.set_visible( score >= self.threshold )
+                self.patches.append( r )
+                self.patch_scores.append( score )
+                
+    def set_threshold( self, threshold ):
+        self.threshold = threshold
+        for patch, score in izip( self.patches, self.patch_scores ):
+            patch.set_visible( score >= self.threshold )
+
+    def do_dialog( self, tc ):
+        dlg = AlignmentTrackUnderlayDialog( tc, self )
+        dlg.show()
+        dlg.run()
+        
+class AlignmentTrackUnderlayDialog(gtk.Dialog):
+    def __init__(self, tc, underlay ):
+        gtk.Dialog.__init__(self, 'Edit Underlay')
+
+        self.tc = tc
+        self.underlay = underlay
+        
+        self.rgb = colorConverter.to_rgb(self.underlay.patches[0].get_facecolor())
+        def set_color(button):
+            rgb = get_color(self.rgb)
+            if rgb is not None:
+                self.rgb = rgb
+        button = gtk.Button(stock=gtk.STOCK_SELECT_COLOR)
+        button.show()
+        button.connect('clicked', set_color)
+        
+        self.vbox.pack_start( button )
+
+        self.slider_adjustment = gtk.Adjustment( underlay.threshold, 0.0, 1.0 )
+        slider = gtk.HScale( self.slider_adjustment )
+        slider.show()
+        
+        self.vbox.pack_start( slider )
+
+        self.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
+        self.add_button(gtk.STOCK_APPLY, gtk.RESPONSE_APPLY)
+        self.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK)
+
+    def update(self):
+        for patch in self.underlay.patches:
+            patch.set_facecolor(self.rgb)
+        self.underlay.set_threshold( float( self.slider_adjustment.get_value() ) )
+        self.tc.redraw()
+
+    def run(self):
+        while 1:
+            response = gtk.Dialog.run(self)
+            if response==gtk.RESPONSE_APPLY:
+                self.update()
+            elif response==gtk.RESPONSE_OK:
+                self.update()
+                break
+            elif response==gtk.RESPONSE_CANCEL:
+                break
+        self.destroy()        
+        
+# This utility method for a color picker dialog should be shared
+         
+def get_color(rgb):
+    def rgb_to_gdk_color(rgb):
+        r,g,b = rgb
+        color = gtk.gdk.Color(int(r*65535), int(g*65535), int(b*65535))
+        return color
+
+    def gdk_color_to_rgb(color):
+        return color.red/65535.0, color.green/65535.0, color.blue/65535.0
+
+    dialog = gtk.ColorSelectionDialog('Choose color')
+
+    colorsel = dialog.colorsel
+    color = rgb_to_gdk_color(rgb)
+    colorsel.set_previous_color(color)
+    colorsel.set_current_color(color)
+    colorsel.set_has_palette(True)
+
+    response = dialog.run()
+
+    if response == gtk.RESPONSE_OK:
+        rgb = gdk_color_to_rgb(colorsel.get_current_color())
+    else:
+        rgb = None
+    dialog.destroy()
+    return rgb  
+        

File maf_block_track_ui.py

+#!/usr/bin/env python2.4
+
+from matplotlib.axes import Subplot
+from matplotlib.figure import Figure
+from matplotlib.font_manager import fontManager, FontProperties
+from matplotlib.numerix import arange, sin, pi
+from matplotlib.patches import Rectangle
+from matplotlib.text import Text
+from matplotlib.transforms import Affine, Bbox, Value, Point, get_bbox_transform, unit_bbox, identity_transform
+
+# Use GTKAgg directly
+from matplotlib.backends.backend_gtkagg import FigureCanvasGTKAgg as FigureCanvas
+from matplotlib.backends.backend_gtkagg import NavigationToolbar2GTKAgg as NavigationToolbar
+
+import gobject, gtk
+import sys
+
+from bx.tracks import *
+from bx.tracks.align import *
+
+import bx.align.maf
+
+def build_tc():
+
+    tc = TrackManager()
+
+    # Build the maf_track
+    reader = bx.align.maf.Reader( sys.stdin )
+    block = reader.next()
+    lo, hi = 0, block.text_size
+    t = AlignmentTrack( "Test Alignment", block )
+    
+    u = AlignmentTrackUnderlay( "Test underlay" )
+    u.add_interval( 'rfbat.1', 20, 40, 0.5 )
+    u.add_interval( 'rfbat.1', 50, 60, 1.0 )
+    u.set_patch_attributes( facecolor='red', linewidth=0 )
+    
+    t.add_underlay( u )
+
+    tc.set_range( lo, hi )
+    tc.add_track( t )
+    
+    return tc
+
+def set_axes_position( ax, left, bottom, right, top ):
+    left, bottom, right, top = map( Value, ( left, bottom, right, top ) )
+    ax.left = left
+    ax.bottom = bottom
+    ax.right = right
+    ax.top = top
+    ax.bbox = Bbox( Point( left, bottom ), Point( right, top ) )
+    ax._set_lim_and_transforms()
+
+def main():
+    
+    win = gtk.Window()
+    win.connect("destroy", lambda x: gtk.main_quit())
+    win.set_default_size(600,400)
+    win.set_title("Embedding in GTK")
+
+    # Use a Vbox to stack up the widgets in the window
+    hpaned = gtk.HPaned()
+    win.add( hpaned )
+
+    # Build the actual figure
+    tc = build_tc()
+    fig = tc.build_figure()
+
+    # Wrap the figure in a GTK widget
+    # TODO: Make size of figure depend on size of tracks
+    canvas = FigureCanvas( fig )
+    tc.canvas = canvas
+    
+    canvas.set_size_request( fig.ur.x().get(), fig.ur.y().get() )
+
+    # Wrap canvas in an 'Alignment' to prevent it from stretching
+    alignment = gtk.Alignment( 0.5, 0.5  )
+    alignment.add( canvas )
+    
+    # Wrap the alignment in a scroll area (so it scrolls when window is smaller than figure)
+    scroll = gtk.ScrolledWindow()
+    scroll.add_with_viewport( alignment )
+    scroll.set_shadow_type( gtk.SHADOW_NONE )
+    scroll.get_child().set_shadow_type( gtk.SHADOW_NONE )
+    hpaned.add2( scroll )
+    
+    # Build a tree model
+    model = gtk.TreeStore( gobject.TYPE_STRING, gobject.TYPE_PYOBJECT )
+    fill_tree_model( model, None, tc.tracks )
+
+    treeview = gtk.TreeView( model )   
+    renderer = gtk.CellRendererText()
+    column = gtk.TreeViewColumn( "Track", renderer, text=0 )
+    treeview.append_column(column)
+    
+    selection = treeview.get_selection()
+    selection.set_mode(gtk.SELECTION_BROWSE)
+    
+    def treeview_callback( treeview, path, col ):
+        model = treeview.get_model()
+        iter = model.get_iter( path )
+        track = model.get_value( iter, 1 )
+        track.do_dialog( tc )
+    
+    treeview.connect( 'row-activated', treeview_callback)
+    treeview.set_size_request(200, -1)
+
+    # Create scrollbars around the view.
+    scrolled = gtk.ScrolledWindow()
+    scrolled.add(treeview)
+    hpaned.add1( scrolled )
+
+    # toolbar = NavigationToolbar(canvas, win)
+    # vbox.pack_start(toolbar, False, False)
+
+    win.show_all()
+    
+    gtk.main()
+
+def fill_tree_model( model, parent, tracks ):
+    for t in tracks:
+        iter = model.append( parent )
+        model.set( iter, 0, t.get_name() )
+        model.set( iter, 1, t )
+        children = t.get_children()
+        if children:
+            fill_tree_model( model, iter, children )
+
+if __name__ == "__main__":
+    main()
+[aliases]
+snapshot = egg_info -rb_DEV bdist_egg rotate -m.egg -k1
         author = "James Taylor, Bob Harris, David King, and others in Webb Miller's Lab",
         author_email = "james@bx.psu.edu",
         description = "Tools for manipulating biological data, particularly multiple sequence alignments",
-        url = "http://www.bx.psu.edu/miller_lab/"
+        url = "http://www.bx.psu.edu/miller_lab/",
+        zip_safe = True
      )