Skip to content

Commit

Permalink
Add back genelabelrotation in Synteny (#610)
Browse files Browse the repository at this point in the history
  • Loading branch information
tanghaibao authored Nov 24, 2023
1 parent 316439c commit 0c8bebf
Showing 1 changed file with 84 additions and 57 deletions.
141 changes: 84 additions & 57 deletions jcvi/graphics/synteny.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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] == "#":
Expand All @@ -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__(
Expand Down Expand Up @@ -147,15 +162,15 @@ 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
ax2, ay2 = a2
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":
Expand Down Expand Up @@ -184,6 +199,10 @@ def __init__(


class Region(object):
"""
Draw a region of synteny.
"""

def __init__(
self,
ax,
Expand All @@ -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
Expand All @@ -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(
(
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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,
):
Expand All @@ -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 = []
Expand All @@ -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,
Expand Down Expand Up @@ -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 = (
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 0c8bebf

Please sign in to comment.