Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add back genelabelrotation in Synteny #610

Merged
merged 1 commit into from
Nov 24, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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