Source code for genometools.expression.visualize.heatmap

# Copyright (c) 2016 Florian Wagner
#
# This file is part of GenomeTools.
#
# GenomeTools is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License, Version 3,
# as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Module containing the `ExpHeatmap` class.

"""

from __future__ import (absolute_import, division,
                        print_function, unicode_literals)
_oldstr = str
from builtins import *

import os
import logging

from plotly import graph_objs as go
import numpy as np

import genometools
from .. import ExpMatrix
from . import read_colorscale

from collections import Iterable

logger = logging.getLogger(__name__)


[docs]class ExpHeatmap(object): """An expression heatmap. An expression heatmap visualizes a gene expression matrix, which is a two-dimensional numerical array with rows corresponding to genes, and columns corresponding to samples. Parameters ---------- matrix : `ExpMatrix` See :attr:`matrix` attribute. gene_annotations : list of `HeatmapGeneAnnotation`, or None, optional A list of gene annotations. [None] sample_annotations : list of `HeatmapSampleAnnotation`, or None, optional A list of sample annotations. [None] colorscale : list or None, optional A plotly colorscale (see :func:`read_colorscale`). If None, load the default red-blue colorscale that is included in this package. [None] colorbar_label : str or None, optional The colorbar label. If None, "Expression" will be used. [None] title : str or None, optional The figure title. If None, the figure will have no title. Notes ----- This class provides an intermediate layer between the underlying expression data, which is represented by an `ExpMatrix` object, and the visualization itself, which corresponds to a plotly figure. Its purpose is to store specific additional data such as the figure title, colorbar label, and (visual) annotations, but no data that only concerns the layout or visual appearance of the figure (e.g., its dimensions, margins, font choices, or the expression values corresponding to the lower and upper end of the colorscale. This information is provided by the user when he/she calls the :func:`get_figure` function, and is not stored anywhere beside the plotly figure object itself. Gene and sample annotations are represented by `HeatmapGeneAnnotation` and `HeatmapSampleAnnotations` objects, which can be used to highlight individual rows and columns in the heatmap, respectively. """ _default_cmap_file = genometools._root.rstrip(os.sep) + os.sep + \ os.sep.join(['data', 'RdBu_r_colormap.tsv']) def __init__(self, matrix, gene_annotations=None, sample_annotations=None, colorscale=None, colorbar_label='Expression', title=None): if gene_annotations is None: gene_annotations = [] if sample_annotations is None: sample_annotations = [] if colorscale is None: # use default colorscale colorscale = read_colorscale(self._default_cmap_file) assert isinstance(matrix, ExpMatrix) assert isinstance(gene_annotations, Iterable) assert isinstance(sample_annotations, Iterable) assert isinstance(colorscale, Iterable) if colorbar_label is not None: assert isinstance(colorbar_label, (str, _oldstr)) self.matrix = matrix self.gene_annotations = gene_annotations self.sample_annotations = sample_annotations self.colorscale = colorscale self.colorbar_label = colorbar_label self.title = title def __str__(self): return '<%s of %d-by-%d matrix>' % \ (self.__class__.__name__, self.matrix.p, self.matrix.n)
[docs] def get_figure( self, emin=None, emax=None, width=800, height=400, margin_left=100, margin_bottom=60, margin_top=30, margin_right=0, colorbar_size=0.4, xaxis_label=None, yaxis_label=None, xaxis_nticks=None, yaxis_nticks=None, xtick_angle=30, font='"Droid Serif", "Open Serif", serif', font_size=12, title_font_size=None, show_sample_labels=True, **kwargs): """Generate a plotly figure of the heatmap. Parameters ---------- emin : int, float, or None, optional The expression value corresponding to the lower end of the colorscale. If None, determine, automatically. [None] emax : int, float, or None, optional The expression value corresponding to the upper end of the colorscale. If None, determine automatically. [None] margin_left : int, optional The size of the left margin (in px). [100] margin_right : int, optional The size of the right margin (in px). [0] margin_top : int, optional The size of the top margin (in px). [30] margin_bottom : int, optional The size of the bottom margin (in px). [60] colorbar_size : int or float, optional The sze of the colorbar, relative to the figure size. [0.4] xaxis_label : str or None, optional X-axis label. If None, use `ExpMatrix` default. [None] yaxis_label : str or None, optional y-axis label. If None, use `ExpMatrix` default. [None] xtick_angle : int or float, optional X-axis tick angle (in degrees). [30] font : str, optional Name of font to use. Can be multiple, separated by comma, to specify a prioritized list. [' "Droid Serif", "Open Serif", "serif"'] font_size : int or float, optional Font size to use throughout the figure, in points. [12] title_font_size : int or float or None, optional Font size to use for labels on axes and the colorbar. If None, use `font_size` value. [None] show_sample_labels : bool, optional Whether to show the sample labels. [True] Returns ------- `plotly.graph_objs.Figure` The plotly figure. """ # emin and/or emax are unspecified, set to data min/max values if emax is None: emax = self.matrix.X.max() if emin is None: emin = self.matrix.X.min() title = self.title if title_font_size is None: title_font_size = font_size colorbar_label = self.colorbar_label or 'Expression' colorbar = go.ColorBar( lenmode='fraction', len=colorbar_size, title=colorbar_label, titlefont=dict( size=title_font_size, ), titleside='right', xpad=0, ypad=0, outlinewidth=0, # no border thickness=20, # in pixels # outlinecolor = '#000000', ) def fix_plotly_label_bug(labels): """ This fixes a bug whereby plotly treats labels that look like numbers (integers or floats) as numeric instead of categorical, even when they are passed as strings. The fix consists of appending an underscore to any label that looks like a number. """ assert isinstance(labels, Iterable) fixed_labels = [] for l in labels: try: float(l) except (ValueError, TypeError): fixed_labels.append(str(l)) else: fixed_labels.append(str(l) + '_') return fixed_labels x = fix_plotly_label_bug(self.matrix.samples) y = fix_plotly_label_bug(self.matrix.genes) data = [ go.Heatmap( z=self.matrix.X, x=x, y=y, zmin=emin, zmax=emax, colorscale=self.colorscale, colorbar=colorbar, hoverinfo='x+y+z', **kwargs ), ] xticks = 'outside' if not show_sample_labels: xticks = '' if xaxis_label is None: if self.matrix.samples.name is not None: xaxis_label = self.matrix.samples.name else: xaxis_label = 'Samples' xaxis_label = xaxis_label + ' (n = %d)' % self.matrix.n if yaxis_label is None: if self.matrix.genes.name is not None: yaxis_label = self.matrix.genes.name else: yaxis_label = 'Genes' yaxis_label = yaxis_label + ' (p = %d)' % self.matrix.p layout = go.Layout( width=width, height=height, title=title, titlefont=go.Font( size=title_font_size ), font=go.Font( size=font_size, family=font ), xaxis=go.XAxis( title=xaxis_label, titlefont=dict(size=title_font_size), showticklabels=show_sample_labels, ticks=xticks, nticks=xaxis_nticks, tickangle=xtick_angle, showline=True ), yaxis=go.YAxis( title=yaxis_label, titlefont=dict(size=title_font_size), nticks=yaxis_nticks, autorange='reversed', showline=True ), margin=go.Margin( l=margin_left, t=margin_top, b=margin_bottom, r=margin_right, pad=0 ), ) # add annotations # we need separate, but overlaying, axes to place the annotations layout['xaxis2'] = go.XAxis( overlaying = 'x', showline = False, tickfont = dict(size=0), autorange=False, range=[-0.5, self.matrix.n-0.5], ticks='', showticklabels=False ) layout['yaxis2'] = go.YAxis( overlaying='y', showline=False, tickfont=dict(size=0), autorange=False, range=[self.matrix.p-0.5, -0.5], ticks='', showticklabels=False ) # gene (row) annotations for ann in self.gene_annotations: i = self.matrix.genes.get_loc(ann.gene) xmn = -0.5 xmx = self.matrix.n-0.5 ymn = i-0.5 ymx = i+0.5 #logger.debug('Transparency is %.1f', ann.transparency) data.append( go.Scatter( x=[xmn, xmx, xmx, xmn, xmn], y=[ymn, ymn, ymx, ymx, ymn], mode='lines', hoverinfo='none', showlegend=False, line=dict(color=ann.color), xaxis='x2', yaxis='y2', #opacity=0.5, opacity=1-ann.transparency, ) ) if ann.label is not None: layout.annotations.append( go.Annotation( text=ann.label, x=0.01, y=i-0.5, #y=i+0.5, xref='paper', yref='y2', xanchor='left', yanchor='bottom', showarrow=False, bgcolor='white', #opacity=1-ann.transparency, opacity=0.8, borderpad=0, #textangle=30, font=dict(color=ann.color) ) ) # sample (column) annotations for ann in self.sample_annotations: j = self.matrix.samples.get_loc(ann.sample) xmn = j-0.5 xmx = j+0.5 ymn = -0.5 ymx = self.matrix.p-0.5 data.append( go.Scatter( x=[xmn, xmx, xmx, xmn, xmn], y=[ymn, ymn, ymx, ymx, ymn], mode='lines', hoverinfo='none', showlegend=False, line=dict(color=ann.color), xaxis='x2', yaxis='y2', opacity=1.0) ) if ann.label is not None: layout.annotations.append( go.Annotation( text=ann.label, y=0.99, x=j+0.5, #y=i+0.5, xref='x2', yref='paper', xanchor='left', yanchor='top', showarrow=False, bgcolor='white', opacity=1-ann.transparency, borderpad=0, textangle=90, font=dict(color=ann.color) ) ) fig = go.Figure( data=data, layout=layout ) return fig