From 2dacd964440110f677a1934cfe434ffbb3e7ef89 Mon Sep 17 00:00:00 2001 From: Haibao Tang Date: Thu, 23 Nov 2023 20:15:16 -0800 Subject: [PATCH] Add back genelabelrotation in Synteny --- jcvi/graphics/synteny.py | 141 +++++++++++++++++++++++---------------- 1 file changed, 84 insertions(+), 57 deletions(-) diff --git a/jcvi/graphics/synteny.py b/jcvi/graphics/synteny.py index bb209cdc..a344b039 100644 --- a/jcvi/graphics/synteny.py +++ b/jcvi/graphics/synteny.py @@ -13,47 +13,53 @@ With the row ordering corresponding to the column ordering in the MCscan output. -For "ha" (horizontal alignment), accepted values are: left|right|leftalign|rightalign|center|""(empty) +For "ha" (horizontal alignment), accepted values are: left|right|leftalign|rightalign|center|"" For "va" (vertical alignment), accepted values are: top|bottom|center|""(empty) """ import sys -import logging -import numpy as np - from typing import Optional -from jcvi.compara.synteny import BlockFile -from jcvi.formats.bed import Bed -from jcvi.formats.base import DictFile -from jcvi.utils.cbook import human_size -from jcvi.utils.validator import validate_in_choices, validate_in_range -from jcvi.apps.base import OptionParser +import numpy as np +import matplotlib.transforms as transforms -from jcvi.graphics.glyph import ( - BasePalette, - Glyph, - OrientationPalette, - OrthoGroupPalette, - RoundLabel, -) -from jcvi.graphics.base import ( +from ..apps.base import OptionParser, logger +from ..compara.synteny import BlockFile +from ..formats.base import DictFile +from ..formats.bed import Bed +from ..graphics.base import ( markup, - mpl, plt, savefig, Path, PathPatch, AbstractLayout, ) +from ..graphics.glyph import ( + BasePalette, + Glyph, + OrientationPalette, + OrthoGroupPalette, + RoundLabel, +) +from ..graphics.tree import draw_tree, read_trees + +from ..utils.cbook import human_size +from ..utils.validator import validate_in_choices, validate_in_range HorizontalAlignments = ("left", "right", "leftalign", "rightalign", "center", "") VerticalAlignments = ("top", "bottom", "center", "") -CanvasSize = 0.65 +CANVAS_SIZE = 0.65 class LayoutLine(object): + """ + Parse a line in the layout file. The line is in the following format: + + *0.5, 0.6, 0, left, center, g, 1, chr1 + """ + def __init__(self, row, delimiter=","): self.hidden = row[0] == "*" if self.hidden: @@ -84,10 +90,15 @@ def __init__(self, row, delimiter=","): else: self.label_fontsize = 10 + class Layout(AbstractLayout): + """ + Parse the layout file. + """ + def __init__(self, filename, delimiter=",", seed: Optional[int] = None): super(Layout, self).__init__(filename) - fp = open(filename) + fp = open(filename, encoding="utf-8") self.edges = [] for row in fp: if row[0] == "#": @@ -114,6 +125,10 @@ def __init__(self, filename, delimiter=",", seed: Optional[int] = None): class Shade(object): + """ + Draw a shade between two tracks. + """ + Styles = ("curve", "line") def __init__( @@ -147,7 +162,7 @@ def __init__( zorder (int, optional): Z-order. Defaults to 1. """ fc = fc or "gainsboro" # Default block color is grayish - assert style in self.Styles, "style must be one of {}".format(self.Styles) + assert style in self.Styles, f"style must be one of {self.Styles}" a1, a2 = a b1, b2 = b ax1, ay1 = a1 @@ -155,7 +170,7 @@ def __init__( bx1, by1 = b1 bx2, by2 = b2 if ax1 is None or ax2 is None or bx1 is None or bx2 is None: - logging.warning("Shade: None found in coordinates, skipping") + logger.warning("Shade: None found in coordinates, skipping") return M, C4, L, CP = Path.MOVETO, Path.CURVE4, Path.LINETO, Path.CLOSEPOLY if style == "curve": @@ -184,6 +199,10 @@ def __init__( class Region(object): + """ + Draw a region of synteny. + """ + def __init__( self, ax, @@ -208,10 +227,10 @@ def __init__( scale /= ratio self.y = y lr = layout.rotation - tr = mpl.transforms.Affine2D().rotate_deg_around(x, y, lr) + ax.transAxes + tr = transforms.Affine2D().rotate_deg_around(x, y, lr) + ax.transAxes inv = ax.transAxes.inverted() - start, end, si, ei, chr, orientation, span = ext + start, end, si, ei, chrom, orientation, span = ext flank = span / scale / 2 xstart, xend = x - flank, x + flank self.xstart, self.xend = xstart, xend @@ -229,9 +248,9 @@ def __init__( startbp, endbp = endbp, startbp if switch: - chr = switch.get(chr, chr) + chrom = switch.get(chrom, chrom) if layout.label: - chr = layout.label + chrom = layout.label label = "-".join( ( @@ -312,13 +331,13 @@ def __init__( xx = xstart - hpad ha = "right" elif ha == "leftalign": - xx = 0.5 - CanvasSize / 2 - hpad + xx = 0.5 - CANVAS_SIZE / 2 - hpad ha = "right" elif ha == "right": xx = xend + hpad ha = "left" elif ha == "rightalign": - xx = 0.5 + CanvasSize / 2 + hpad + xx = 0.5 + CANVAS_SIZE / 2 + hpad ha = "left" else: xx = x @@ -345,18 +364,18 @@ def __init__( ha=ha, va="center", rotation=trans_angle, bbox=bbox, zorder=10 ) - # TODO: I spent several hours on trying to make this work - with no - # good solutions. To generate labels on multiple lines, each line - # with a different style is difficult in matplotlib. The only way, - # if you can tolerate an extra dot (.), is to use the recipe below. - # chr_label = r"\noindent " + markup(chr) + r" \\ ." if chr_label else None - # loc_label = r"\noindent . \\ " + label if loc_label else None - - chr_label = markup(chr) if chr_label else None + chr_label = markup(chrom) if chr_label else None loc_label = label if loc_label else None if chr_label: if loc_label: - ax.text(lx, ly + vpad, chr_label, size=layout.label_fontsize, color=layout.color, **kwargs) + ax.text( + lx, + ly + vpad, + chr_label, + size=layout.label_fontsize, + color=layout.color, + **kwargs, + ) ax.text( lx, ly - vpad, @@ -369,6 +388,9 @@ def __init__( ax.text(lx, ly, chr_label, color=layout.color, **kwargs) def get_coordinates(self, gstart, gend, y, cv, tr, inv): + """ + Get coordinates of a gene. + """ x1, x2 = cv(gstart), cv(gend) a, b = tr.transform((x1, y)), tr.transform((x2, y)) a, b = inv.transform(a), inv.transform(b) @@ -392,6 +414,10 @@ def ymid_offset(samearc: Optional[str], pad: float = 0.05): class Synteny(object): + """ + Draw the synteny plot. + """ + def __init__( self, fig, @@ -402,15 +428,16 @@ def __init__( switch=None, tree=None, extra_features=None, - chr_label=True, - loc_label=True, + chr_label: bool = True, + loc_label: bool = True, gene_labels: Optional[set] = None, - genelabelsize=0, - pad=0.05, - vpad=0.015, - scalebar=False, - shadestyle="curve", - glyphstyle="arrow", + genelabelsize: int = 0, + genelabelrotation: int = 25, + pad: float = 0.05, + vpad: float = 0.015, + scalebar: bool = False, + shadestyle: str = "curve", + glyphstyle: str = "arrow", glyphcolor: BasePalette = OrientationPalette(), seed: Optional[int] = None, ): @@ -429,21 +456,19 @@ def __init__( ext = bf.get_extent(i, order) exts.append(ext) if extra_features: - start, end, si, ei, chr, orientation, span = ext + start, end, _, _, chrom, _, span = ext start, end = start.start, end.end # start, end coordinates - ef = list(extra_features.extract(chr, start, end)) + ef = list(extra_features.extract(chrom, start, end)) # Pruning removes minor features with < 0.1% of the region ef_pruned = [x for x in ef if x.span >= span / 1000] - print( - "Extracted {0} features " - "({1} after pruning)".format(len(ef), len(ef_pruned)), - file=sys.stderr, + logger.info( + "Extracted %d features (%d after pruning)", len(ef), len(ef_pruned) ) extras.append(ef_pruned) maxspan = max(exts, key=lambda x: x[-1])[-1] - scale = maxspan / CanvasSize + scale = maxspan / CANVAS_SIZE self.gg = gg = {} self.rr = [] @@ -465,6 +490,7 @@ def __init__( switch, gene_labels=gene_labels, genelabelsize=genelabelsize, + genelabelrotation=genelabelrotation, chr_label=chr_label, loc_label=loc_label, vpad=vpad, @@ -499,7 +525,7 @@ def __init__( ) if scalebar: - print("Build scalebar (scale={})".format(scale), file=sys.stderr) + logger.info("Build scalebar (scale=%.3f)", scale) # Find the best length of the scalebar ar = [1, 2, 5] candidates = ( @@ -526,11 +552,9 @@ def __init__( ) if tree: - from jcvi.graphics.tree import draw_tree, read_trees - trees = read_trees(tree) ntrees = len(trees) - logging.debug("A total of {0} trees imported.".format(ntrees)) + logger.debug("A total of %d trees imported.", ntrees) xiv = 1.0 / ntrees yiv = 0.3 xstart = 0 @@ -562,6 +586,9 @@ def draw_gene_legend( repeat=False, glyphstyle="box", ): + """ + Draw a legend for gene glyphs. + """ forward, backward = OrientationPalette.forward, OrientationPalette.backward ax.plot([x1, x1 + d], [ytop, ytop], ":", color=forward, lw=2) ax.plot([x1 + d], [ytop], ">", color=forward, mec=forward)