diff --git a/jcvi/apps/base.py b/jcvi/apps/base.py index 100642c3..b1734073 100644 --- a/jcvi/apps/base.py +++ b/jcvi/apps/base.py @@ -54,14 +54,14 @@ def debug(level=logging.DEBUG): debug() -def get_logger(name: str): +def get_logger(name: str, level: int = logging.DEBUG): """Return a logger with a default ColoredFormatter.""" logger = logging.getLogger(name) if logger.hasHandlers(): logger.handlers.clear() - logger.addHandler(RichHandler()) + logger.addHandler(RichHandler(console=Console(stderr=True))) logger.propagate = False - logger.setLevel(logging.INFO) + logger.setLevel(level) return logger diff --git a/jcvi/graphics/base.py b/jcvi/graphics/base.py index 5569a70a..7e9d0ef2 100644 --- a/jcvi/graphics/base.py +++ b/jcvi/graphics/base.py @@ -34,7 +34,6 @@ FancyArrowPatch, FancyBboxPatch, ) -from matplotlib.path import Path from typing import Optional from ..apps.base import datadir, glob, listify, logger, sample_N, which @@ -313,7 +312,6 @@ def savefig(figname, dpi=150, iopts=None, cleanup=True): format = "pdf" try: logger.debug("Matplotlib backend is: %s", mpl.get_backend()) - logger.debug("Attempting save as: %s", figname) plt.savefig(figname, dpi=dpi, format=format) except Exception as e: logger.error("savefig failed with message:\n%s", e) @@ -324,9 +322,9 @@ def savefig(figname, dpi=150, iopts=None, cleanup=True): remove(figname) sys.exit(1) - msg = "Figure saved to `{0}`".format(figname) + msg = f"Figure saved to `{figname}`" if iopts: - msg += " {0}".format(iopts) + msg += f" {iopts}" logger.debug(msg) if cleanup: diff --git a/jcvi/graphics/chromosome.py b/jcvi/graphics/chromosome.py index f726a76e..5f590283 100644 --- a/jcvi/graphics/chromosome.py +++ b/jcvi/graphics/chromosome.py @@ -5,20 +5,20 @@ Legacy script to plot distribution of certain classes onto chromosomes. Adapted from the script used in the Tang et al. PNAS 2010 paper, sigma figure. """ -import logging import sys from itertools import groupby from math import ceil -from natsort import natsorted from typing import Tuple import numpy as np -from jcvi.apps.base import OptionGroup, OptionParser, datafile, sample_N -from jcvi.formats.base import DictFile, get_number -from jcvi.formats.bed import Bed -from jcvi.formats.sizes import Sizes -from jcvi.graphics.base import ( +from natsort import natsorted + +from ..apps.base import OptionGroup, OptionParser, datafile, logger, sample_N +from ..formats.base import DictFile, get_number +from ..formats.bed import Bed +from ..formats.sizes import Sizes +from ..graphics.base import ( CirclePolygon, Polygon, Rectangle, @@ -29,7 +29,7 @@ set1_n, set3_n, ) -from jcvi.graphics.glyph import BaseGlyph, plot_cap +from ..graphics.glyph import BaseGlyph, plot_cap class Chromosome(BaseGlyph): @@ -236,7 +236,7 @@ def __init__( # tip = length of the ticks y1, y2 = sorted((y1, y2)) ax.plot([x, x], [y1, y2], "-", color=fc, lw=2) - max_marker_name, max_chr_len = max(markers, key=lambda x: x[-1]) + _, max_chr_len = max(markers, key=lambda x: x[-1]) r = y2 - y1 ratio = r / max_chr_len marker_pos = {} @@ -314,7 +314,7 @@ def write_ImageMapLine(tlx, tly, brx, bry, w, h, dpi, chr, segment_start, segmen """ tlx, brx = [canvas2px(x, w, dpi) for x in (tlx, brx)] tly, bry = [canvas2px(y, h, dpi) for y in (tly, bry)] - chr, bac_list = chr.split(":") + chr, _ = chr.split(":") return ( '", file=mapfh) mapfh.close() - logging.debug("Image map written to `{0}`".format(mapfh.name)) + logger.debug("Image map written to `%s`", mapfh.name) if gauge: xstart, ystart = 0.9, 0.85 diff --git a/jcvi/graphics/synteny.py b/jcvi/graphics/synteny.py index 841f26ba..d76969b5 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 - -from jcvi.graphics.glyph import ( - BasePalette, - Glyph, - OrientationPalette, - OrthoGroupPalette, - RoundLabel, -) -from jcvi.graphics.base import ( +import numpy as np +import matplotlib.transforms as transforms +from matplotlib.path import Path + +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,16 @@ 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 +126,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 +163,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 +171,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 +200,10 @@ def __init__( class Region(object): + """ + Draw a region of synteny. + """ + def __init__( self, ax, @@ -208,10 +228,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 +249,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( ( @@ -318,13 +338,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 @@ -351,18 +371,17 @@ 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, @@ -375,6 +394,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) @@ -398,6 +420,10 @@ def ymid_offset(samearc: Optional[str], pad: float = 0.05): class Synteny(object): + """ + Draw the synteny plot. + """ + def __init__( self, fig, @@ -408,16 +434,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, - genelabelrotation=25, - 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, ): @@ -436,21 +462,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 = [] @@ -507,7 +531,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 = ( @@ -534,11 +558,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 @@ -570,6 +592,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)