import collections
from genomeview.svg import Renderer, SVG
class Document:
header = """<svg version="1.1" baseProfile="full" width="{width}" height="{height}" xmlns="http://www.w3.org/2000/svg">"""
footer = """</svg>"""
def __init__(self, width):
self.elements = []
self.width = width
self.renderer = SVG()
self.margin_x = 5
self.margin_y = 5
self.between_views = 5
def add_view(self, view):
self.elements.append(view)
def add_track(self, track):
for element in self.elements:
try:
element.add_track(track)
break
except AttributeError:
continue
else:
raise Exception("No GenomeView found in Document")
def get_tracks(self, name=None):
matching = []
for element in self.elements:
try:
matching.extend(element.get_tracks(name))
except AttributeError:
pass
return matching
def layout(self):
self.view_width = self.width - self.margin_x*2
for element in self.elements:
element.layout(self.view_width)
def render(self):
self.layout()
total_height = sum(element.height+self.between_views for element in self.elements) + self.margin_y*2
yield self.header.format(height=total_height, width=self.width)
cury = self.margin_y
for element in self.elements:
renderer = Renderer(self.renderer, self.margin_x, cury, self.view_width, element.height)
yield from renderer.render(element)
cury += element.height + self.between_views
yield self.footer
def _repr_svg_(self):
return "\n".join(self.render())
class ViewRow:
def __init__(self, name=None):
self.name = name
self.views = []
self.width = None
self.height = None
self.space_between = 5
def add_view(self, view):
self.views.append(view)
def get_views(self, name=None):
matching = []
for view in self.views:
if name is None or view.name==name:
matching.extend(view)
return matching
def get_tracks(self, name=None):
matching = []
for view in self.views:
try:
matching.extend(view.get_tracks(name))
except AttributeError:
pass
return matching
def layout(self, width):
self.width = width
n_views = len(self.views)
self.each_width = (self.width - self.space_between*(n_views-1)) / n_views
self.height = 0
for view in self.views:
view.layout(self.each_width)
self.height = max(self.height, view.height)
def render(self, renderer):
curx = 0
for view in self.views:
subrenderer = renderer.subrenderer(x=curx, width=self.each_width, height=view.height)
yield from subrenderer.render(view)
curx += self.each_width + self.space_between
class GenomeView:
def __init__(self, chrom, start, end, strand, source=None, name=None):
self.name = name
self.tracks = []
self.scale = Scale(chrom, start, end, strand, source)
self.pixel_width = None
self.pixel_height = None
self.margin_y = 10
def add_track(self, track):
self.tracks.append(track)
def get_tracks(self, name=None):
matching = []
for track in self.tracks:
if name is None or track.name == name:
matching.append(track)
return matching
def layout(self, width):
self.pixel_width = width
self.scale.pixel_width = width
self.height = 0
for track in self.tracks:
track.layout(self.scale)
self.height += track.height + self.margin_y
def render(self, renderer):
cury = 0
for track in self.tracks:
subrenderer = renderer.subrenderer(y=cury, height=track.height)
yield from subrenderer.render(track)
cury += track.height + self.margin_y
[docs]class Scale:
"""
Maintains information about a projection of a specific genomic
interval into screen coordinates.
That is, we're interested in visualizing an interval (chrom:start-end)
on a canvas of a specified pixel width. The scale enables converting
genomic coordinates into the display coordinates.
"""
def __init__(self, chrom, start, end, strand, source):
self.chrom = chrom
self.start = start
self.end = end
self.strand = strand
self.pixel_width = None
self._param = None
self.source = source
self._seq = None
if self.start >= self.end:
raise ValueError("End coordinate must be greater than start coordinate; you specified {}:{}-{}".format(chrom, start, end))
def __len__(self):
return self.end - self.start + 1
def _setup(self):
if self._param == self.pixel_width: return
self._param = self.pixel_width
self._seq = None
nt_width = self.end - self.start
self.bases_per_pixel = nt_width / self.pixel_width
[docs] def topixels(self, genomic_position):
"""
Converts a genomic position to a pixel location in the current
coordinate system.
"""
self._setup()
pos = (genomic_position - self.start) / float(self.bases_per_pixel)
return pos
[docs] def relpixels(self, genomic_size):
"""
Takes a genomic length (ie, a number of basepairs) and converts
it to a relative screen length in pixels.
"""
self._setup()
dist = genomic_size / float(self.bases_per_pixel)
return dist
[docs] def get_seq(self, start=None, end=None, strand="+"):
"""
Gets the nucleotide sequence of an interval. By default, returns the
sequence for the current genomic interval.
"""
self._setup()
assert self.source is not None
if start is None:
start = self.start
if end is None:
end = self.end
assert start >= self.start
assert end <= self.end
if self._seq is None:
self._seq = self.source.get_seq(self.chrom, self.start, self.end, self.strand).upper()
cur_seq = self._seq[start-self.start:end-self.start]
if strand != self.strand:
raise Exception("ack")
return cur_seq