import numpy as np
from matplotlib import pyplot as plt
try:
import seaborn as sns
except ImportError:
sns = None
[docs]class Track(object):
"""Abstract base class representing a Geneviz track.
Specifies two methods, **draw** and **get_height**, that form the main
interface of a track and should be overridden in each subclass. The
method **get_height** is used to determine the height of a given track,
which determine the (relative) amount of vertical space the track is
given when drawn. The **draw** method called by plot_tracks to draw the
track on a given axis for a given region.
"""
def __init__(self):
super().__init__()
# pylint: disable=unused-argument
[docs] def get_height(self, region, ax):
"""Returns the height of the track within the plotting region.
Parameters
----------
region : Tuple[str, int, int]
The genomic region that will be drawn. Specified as a tuple of
(chromosome, start, end).
ax : matplotlib.Axes
Axis that the track will be drawn on. Used to determine the
size of some features that may be dependent on the axis (such
as the space required to draw labels etc.).
Returns
-------
height : int
Height of the track within the given region.
"""
return 1
[docs] def draw(self, region, ax):
"""Draws the track on the given axis.
Parameters
----------
region : Tuple[str, int, int]
Genomic region to draw.
ax : matplotlib.Axes
Axis to draw track on.
"""
raise NotImplementedError()
[docs]class DummyTrack(Track):
"""Dummy track that doesn't draw anything.
This track can be used to create a blank axis that can be drawn on
manually at a later time point.
Parameters
----------
height : int
Height of the dummy track.
"""
def __init__(self, height=1):
super().__init__()
self._height = height
[docs] def get_height(self, region, ax):
"""Returns the (fixed) height of the dummy track.
Parameters
----------
region : Tuple[str, int, int]
The genomic region that will be drawn. Specified as a tuple of
(chromosome, start, end).
ax : matplotlib.Axes
Axis that the track will be drawn on.
Returns
-------
height : int
Height of the dummy track.
"""
return self._height
[docs] def draw(self, region, ax):
"""Draws the track on the given axis.
This is effectively a no-op for the dummy track.
Parameters
----------
region : Tuple[str, int, int]
Genomic region to draw.
ax : matplotlib.Axes
Axis to draw track on.
"""
pass
[docs]def plot_tracks(tracks,
region,
figsize=None,
height_ratios=None,
tick_top=False,
padding=(0, 0),
reverse=False,
despine=False):
"""Plots given tracks over the specified range on shared axes.
Parameters
----------
tracks : List[Track]
List of tracks to draw.
region : Tuple[str, int, int]
Genomic region to draw.
figsize : Tuple[int, int]
Size of resulting figure, specified as a tuple of (width, height).
Height may be omitted (by passing None), in which case the height
of the figure will be scaled depending on the heights of the tracks.
height_ratios : List[int]
Relative heights of each track.
tick_top : bool
Whether xticks should be plotted along top.
padding : Tuple[int, int]
Amount of padding to add on the x-axis (in genomic space).
reverse : bool
Whether the x-axis should be reversed, useful for drawing features
on the reverse strand from left to right.
Returns
-------
Tuple[matplotlib.Figure, matplotlib.Axes]
Figure and axes on which was drawn.
"""
if height_ratios is None:
height_ratios = _calc_height_ratios(tracks, region, figsize, reverse)
# Create shared axes.
figsize = _calc_figsize(figsize, height_ratios)
fig, axes = plt.subplots(
nrows=len(tracks),
sharex=True,
figsize=figsize,
gridspec_kw={'height_ratios': height_ratios})
axes = [axes] if len(tracks) == 1 else axes.flatten()
# Remove spacing between tracks.
fig.subplots_adjust(hspace=0.1)
# Set xlim to required region.
_, start, end = region
if reverse:
x_end, x_start = start - padding[1], end + padding[0]
else:
x_start, x_end = start - padding[0], end + padding[1]
axes[0].set_xlim(x_start, x_end)
# Plot tracks.
for track, ax in zip(tracks, axes):
track.draw(region, ax)
# Move x-ticks to the top of the figure if requested.
if tick_top:
axes[0].xaxis.tick_top()
for lab in axes[-1].get_xticklabels():
lab.set_visible(False)
else:
for ax in axes[:-1]:
for lab in ax.get_xticklabels():
lab.set_visible(False)
# Turn off scientific notation on axes.
axes[0].xaxis.get_major_formatter().set_useOffset(False)
axes[0].xaxis.get_major_formatter().set_scientific(False)
if despine:
# Adjust spines and labels for more white space.
_despine_axes(axes, tick_top)
return fig
def _calc_height_ratios(tracks, region, figsize, reverse):
"""Calculates height ratios based on heights of given tracks."""
# Create dummy figure + axes for drawing.
figsize = _calc_figsize(figsize)
dummy_fig, dummy_axes = plt.subplots(figsize=figsize, nrows=len(tracks))
dummy_axes = [dummy_axes] if len(tracks) == 1 else dummy_axes.flatten()
# Set xlimits.
if reverse:
xlim = region[2], region[1]
else:
xlim = region[1], region[2]
dummy_axes[0].set_xlim(*xlim)
# Calculate heights of the tracks.
height_ratios = [
t.get_height(region, ax) for t, ax in zip(tracks, dummy_axes)
]
# Close dummy figure to prevent drawing.
plt.close(dummy_fig)
return height_ratios
def _calc_figsize(figsize, height_ratios=None):
"""Calculates figsize, optionally taking height_ratios into account."""
if figsize is None:
fig_width, fig_height = None, None
else:
fig_width, fig_height = figsize
if fig_height is None:
if height_ratios is not None:
fig_height = sum(height_ratios)
else:
fig_height = 1
if fig_width is None:
fig_width = plt.rcParams['figure.figsize'][0]
return fig_width, fig_height
def _despine_axes(axes, tick_top):
"""Despines track axes using Seaborn, accounting for tick location."""
if sns is None:
raise ImportError('Seaborn library is required for despine')
sns.despine(ax=axes[0], top=not tick_top, left=True, bottom=True)
for ax in axes[1:-1]:
sns.despine(ax=ax, bottom=True, left=True)
sns.despine(ax=axes[-1], bottom=tick_top, left=True)