Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
15 changes: 15 additions & 0 deletions src/jcvi/apps/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ def set_image_options(
GRAPHIC_FORMATS,
ImageOptions,
is_tex_available,
set_chinese_font,
setup_theme,
)

Expand Down Expand Up @@ -552,6 +553,12 @@ def set_image_options(
group.add_argument(
"--notex", default=False, action="store_true", help="Do not use tex"
)
group.add_argument(
"--chinese",
default=False,
action="store_true",
help="Render Chinese labels: implies --notex and selects a CJK font",
)
# https://github.com/tanghaibao/jcvi/issues/515#issuecomment-1327305211
if (
"--seed" not in self._option_string_actions
Expand All @@ -572,6 +579,11 @@ def set_image_options(
assert opts.dpi > 0
assert "x" in opts.figsize

# Chinese labels cannot be rendered through latex, so --chinese implies
# --notex and additionally selects a CJK-capable font below.
if opts.chinese:
opts.notex = True

iopts = ImageOptions(opts)

if opts.notex:
Expand All @@ -584,6 +596,9 @@ def set_image_options(

setup_theme(style=opts.style, font=opts.font, usetex=iopts.usetex)

if opts.chinese:
set_chinese_font()

return opts, args, iopts

def set_dotplot_opts(self, theme: int = 2):
Expand Down
18 changes: 15 additions & 3 deletions src/jcvi/compara/ks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
from typing import Optional

from Bio import AlignIO, SeqIO
from Bio.Align.Applications import ClustalwCommandline, MuscleCommandline

try:
from Bio.Align.Applications import ClustalwCommandline, MuscleCommandline
except ImportError: # Biopython >= 1.78 removed the Applications command wrappers
ClustalwCommandline = MuscleCommandline = None
import numpy as np

from ..apps.base import (
Expand Down Expand Up @@ -49,7 +53,11 @@ def run(self):
class YnCommandline(AbstractCommandline):
"""Little commandline for yn00."""

def __init__(self, ctl_file, command=PAML_BIN("yn00")):
def __init__(self, ctl_file, command=None):
# Resolve PAML path lazily so importing this module does not prompt for
# a yn00 binary that is only needed by the calc action.
if command is None:
command = PAML_BIN("yn00")
self.ctl_file = ctl_file
self.parameters = []
self.command = command
Expand All @@ -67,8 +75,12 @@ def __init__(
nuc_file,
output_file,
outfmt="paml",
command=PAL2NAL_BIN("pal2nal.pl"),
command=None,
):
# Resolve PAL2NAL lazily so importing this module does not prompt for a
# binary only needed by the calc action.
if command is None:
command = PAL2NAL_BIN("pal2nal.pl")
self.prot_align_file = prot_align_file
self.nuc_file = nuc_file
self.output_file = output_file
Expand Down
14 changes: 10 additions & 4 deletions src/jcvi/graphics/align.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,8 @@ def delete(self, a, b):

def duplicate(self, a, b):
ai, bi = self.get_endpoints(a, b)
ai += self.wiggle / 2
bi += self.wiggle / 2
ai += self.wiggle // 2
bi += self.wiggle // 2
ci, di = ai - self.wiggle, ai
bb = self.sizes[ai:bi]
bs = len(bb)
Expand Down Expand Up @@ -479,6 +479,12 @@ def main():
(mode,) = args
assert mode == "demo"

row_headers = ("Inversion", "Indel", "Duplication")
col_headers = ("Assembly alignment", "Read alignment", "Optical map alignment")
if opts.chinese:
row_headers = ("倒位", "插入缺失", "重复")
col_headers = ("拼接比对", "读段比对", "光学图谱比对")

a, b = 30, 70
pad = 0.08
w = 0.31
Expand All @@ -494,14 +500,14 @@ def main():
# Row headers
xx = pad * 0.6
yy = 1 - pad - 0.5 * w
for title in ("Inversion", "Indel", "Duplication"):
for title in row_headers:
root.text(xx, yy, title, ha="center", va="center")
yy -= w

# Column headers
xx = pad + 0.5 * w
yy = 1 - pad / 2
for title in ("Assembly alignment", "Read alignment", "Optical map alignment"):
for title in col_headers:
root.text(xx, yy, title, ha="center", va="center")
xx += w

Expand Down
45 changes: 44 additions & 1 deletion src/jcvi/graphics/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,22 @@ def markup(s: str):
Change the string to latex format, and italicize the text between *.
"""
if not rcParams["text.usetex"]:
# Without LaTeX, strip LaTeX-only markup so labels stay readable (and so
# CJK labels are not hidden behind \textit{}); keep $...$ mathtext intact.
s = s.replace(r"\noindent", "")

def _italic(m):
# Render \textit{...} as mathtext italic so Latin names stay italic
# without latex; fall back to plain for non-ASCII (e.g. CJK).
x = m.group(1)
if x.isascii():
return r"$\mathit{" + x.replace(" ", r"\ ") + r"}$"
return x

s = re.sub(r"\\textit\{([^{}]*)\}", _italic, s)
s = re.sub(r"\\text(?:bf|rm|sf)\{([^{}]*)\}", r"\1", s)
s = re.sub(r"\*([^*]+)\*", r"\1", s) # *italic* markup -> plain text
s = s.replace(r"\\", "\n") # LaTeX line break -> real newline
return s
if "$" in s:
return s
Expand All @@ -433,6 +449,31 @@ def append_percentage(s):
return s + "%"


# Cross-platform CJK-capable sans-serif fonts, tried in order (macOS / Windows /
# Linux). Used as a per-glyph fallback so Chinese labels render with --chinese.
CJK_FONTS = [
"Arial Unicode MS",
"PingFang SC",
"Hiragino Sans GB",
"Heiti SC",
"Songti SC",
"Microsoft YaHei",
"SimHei",
"SimSun",
"Noto Sans CJK SC",
"Noto Sans SC",
"Source Han Sans SC",
"WenQuanYi Zen Hei",
]


def set_chinese_font():
"""Select a CJK-capable font and disable latex so Chinese labels render."""
rc("text", usetex=False)
rc("font", **{"family": "sans-serif", "sans-serif": CJK_FONTS + ["DejaVu Sans"]})
rcParams["axes.unicode_minus"] = False


def setup_theme(
context="notebook",
style="darkgrid",
Expand All @@ -457,7 +498,9 @@ def setup_theme(
rc("text", usetex=False)

if font == "Helvetica":
rc("font", **{"family": "sans-serif", "sans-serif": ["Helvetica"]})
# CJK fonts appended as a per-glyph fallback so Chinese labels render
# (only used when text.usetex is False; Latin still comes from Helvetica)
rc("font", **{"family": "sans-serif", "sans-serif": ["Helvetica"] + CJK_FONTS})
elif font == "Palatino":
rc("font", **{"family": "serif", "serif": ["Palatino"]})
elif font == "Schoolbook":
Expand Down
3 changes: 2 additions & 1 deletion src/jcvi/graphics/coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .base import (
Rectangle,
adjust_spines,
markup,
mb_float_formatter,
mb_formatter,
plt,
Expand Down Expand Up @@ -195,7 +196,7 @@ def __init__(
xy.import_hlfile(hlfile, chr, diverge=diverge)
if plot_label:
label = labels_dict.get(label, label.capitalize())
label = r"\textit{{{0}}}".format(label)
label = markup(r"\textit{{{0}}}".format(label))
root.text(x - 0.015, yy + yinterval / 2, label, ha="right", va="center")
Comment on lines 197 to 202
xy.draw()
ax.set_xlim(*xlim)
Expand Down
18 changes: 13 additions & 5 deletions src/jcvi/graphics/dotplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ def dotplot(
stdpf: bool = True,
chpf: bool = True,
usetex: bool = True,
chinese: bool = False,
):
"""
Draw a dotplot from an anchor file.
Expand Down Expand Up @@ -381,11 +382,18 @@ def dotplot(
xstart += 0.1

if title is None:
title = f"Inter-genomic comparison: {gx} vs {gy}"
if is_self:
title = f"Intra-genomic comparison within {gx}"
npairs //= 2
title += f" ({thousands(npairs)} gene pairs)"
if chinese:
title = f"基因组间比较:{gx} vs {gy}"
if is_self:
title = f"{gx} 基因组内比较"
npairs //= 2
title += f"({thousands(npairs)} 个基因对)"
else:
title = f"Inter-genomic comparison: {gx} vs {gy}"
if is_self:
title = f"Intra-genomic comparison within {gx}"
npairs //= 2
title += f" ({thousands(npairs)} gene pairs)"
root.set_title(title, x=0.5, y=0.96, color="k")
if title:
logger.debug("Dot plot title: %s", title)
Expand Down
14 changes: 10 additions & 4 deletions src/jcvi/graphics/karyotype.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,10 +383,16 @@ def __init__(
if generank:
sz = dict((x, len(list(bed.sub_bed(x)))) for x in seqids)
else:
sz = sizes or dict(
(x, max(z.end for z in list(bed.sub_bed(x)))) for x in seqids
)
assert sz is not None, "sizes not available and cannot be inferred"
if sizes:
# Restrict the provided sizes to this track's seqids; otherwise
# every track inherits the genome-wide total and the per-chr
# gauge/coverage axes collapse.
sz = dict((x, sizes[x]) for x in seqids if x in sizes)
else:
sz = dict(
(x, max(z.end for z in list(bed.sub_bed(x)))) for x in seqids
)
assert sz, "sizes not available and cannot be inferred"
Comment on lines +386 to +398
t.seqids = seqids
# validate if all seqids are non-empty
for k, v in sz.items():
Expand Down
37 changes: 33 additions & 4 deletions src/jcvi/graphics/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ def draw_tree(
wgdinfo=None,
geoscale=False,
groups=[],
chinese=False,
):
"""
main function for drawing phylogenetic tree
Expand Down Expand Up @@ -240,6 +241,11 @@ def rescale_divergence(divergence):
xx = rescale(dist)

if n.is_leaf:
# On a time-calibrated (geoscale) tree all extant tips sit at the
# present, so align leaf tips at max_dist (keeps the outgroup leaf,
# e.g. the rerooted outgroup, flush with the others).
if geoscale:
xx = rescale(max_dist)
yy = ystart + i * yinterval
i += 1

Expand Down Expand Up @@ -337,7 +343,13 @@ def rescale_divergence(divergence):
# scale bar
if geoscale:
draw_geoscale(
ax, ytop, margin=margin, rmargin=rmargin, yy=ymargin, max_dist=max_dist
ax,
ytop,
margin=margin,
rmargin=rmargin,
yy=ymargin,
max_dist=max_dist,
chinese=chinese,
)
else:
br = 0.1
Expand Down Expand Up @@ -437,7 +449,14 @@ def read_trees(tree):


def draw_geoscale(
ax, ytop, margin=0.1, rmargin=0.2, yy=0.1, max_dist=3.0, contrast_epochs=True
ax,
ytop,
margin=0.1,
rmargin=0.2,
yy=0.1,
max_dist=3.0,
contrast_epochs=True,
chinese=False,
):
"""
Draw geological epoch on million year ago (mya) scale.
Expand All @@ -462,7 +481,7 @@ def cv(x):
ax.text(
(a + b) / 2,
yy - 5 * tick,
"Time before present (million years)",
"距今时间(百万年)" if chinese else "Time before present (million years)",
ha="center",
va="center",
)
Expand All @@ -478,6 +497,16 @@ def cv(x):
("Permian", 252.17, 298.9),
("Carboniferous", 298.9, 358.9),
)
# Chinese display names; English keys are kept for the contrast logic below
CN_ERA = {
"Neogene": "新近纪",
"Paleogene": "古近纪",
"Cretaceous": "白垩纪",
"Jurassic": "侏罗纪",
"Triassic": "三叠纪",
"Permian": "二叠纪",
"Carboniferous": "石炭纪",
}
h = 0.05
for (era, start, end), color in zip(Geo, set3_n(len(Geo))):
if maxx - start < 10: # not visible enough
Expand All @@ -488,7 +517,7 @@ def cv(x):
ax.text(
(start + end) / 2,
yy + (tick + h) / 2,
era,
CN_ERA.get(era, era) if chinese else era,
ha="center",
va="center",
size=8,
Expand Down
11 changes: 9 additions & 2 deletions src/jcvi/projects/jcvi.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
plt,
savefig,
set1,
set_chinese_font,
setup_theme,
)
from ..graphics.chromosome import draw_chromosomes
Expand All @@ -43,7 +44,11 @@ def synteny(args):
p = OptionParser(synteny.__doc__)
p.set_beds()
opts, args, iopts = p.set_image_options(args, figsize="14x7")
setup_theme(style="dark")
setup_theme(style="dark", usetex=iopts.usetex)

if opts.chinese:
# The extra setup_theme() above resets the font, so re-apply the CJK font
set_chinese_font()

if len(args) != 6:
sys.exit(not p.print_help())
Expand Down Expand Up @@ -71,6 +76,8 @@ def synteny(args):
is_self=is_self,
chrlw=0.5,
sepcolor=set1[3],
genomenames="葡萄_桃" if opts.chinese else None,
chinese=opts.chinese,
)

# Panel B
Expand All @@ -86,7 +93,7 @@ def synteny(args):
panel_labels(root, labels)
normalize_axes(root, ax1_root, ax2_root, ax3_root)

image_name = "synteny.pdf"
image_name = "synteny." + iopts.format
savefig(image_name, dpi=iopts.dpi, iopts=iopts)


Expand Down
Loading
Loading