diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f717486 --- /dev/null +++ b/.gitignore @@ -0,0 +1,125 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/tagore.py b/tagore.py new file mode 100755 index 0000000..6174ca4 --- /dev/null +++ b/tagore.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +''' +tagore: a utility for illustrating human chromosomes +https://github.com/jordanlab/tagore +''' +__author__ = ["Lavanya Rishishar", "Aroon Chande"] +__copyright__ = "Copyright 2019, Applied Bioinformatics Lab" +__license__ = "GPLv3" + +import os +import pickle +import re +import shutil +import subprocess +import sys +from argparse import ArgumentParser, HelpFormatter + + +VERSION = '1.0.1' + +COORDINATES = { + "1": {"cx": 128.6, "cy": 1.5, "ht": 1654.5, "width": 118.6}, + "2": {"cx": 301.4, "cy": 43.6, "ht": 1612.4, "width": 118.6}, + "3": {"cx": 477.6, "cy": 341.4, "ht": 1314.7, "width": 118.6}, + "4": {"cx": 655.6, "cy": 517.9, "ht": 1138.1, "width": 118.6}, + "5": {"cx": 835.4, "cy": 461, "ht": 1195.1, "width": 118.6}, + "6": {"cx": 1012.4, "cy": 524.2, "ht": 1131.8, "width": 118.6}, + "7": {"cx": 1198.2, "cy": 608.5, "ht": 1047.5, "width": 118.6}, + "8": {"cx": 1372.9, "cy": 692.8, "ht": 963.2, "width": 118.6}, + "9": {"cx": 1554.5, "cy": 724.4, "ht": 931.6, "width": 118.6}, + "10": {"cx": 1733.8, "cy": 766.6, "ht": 889.4, "width": 118.6}, + "11": {"cx": 1911.5, "cy": 766.6, "ht": 889.4, "width": 118.6}, + "12": {"cx": 2095.6, "cy": 769.7, "ht": 886.3, "width": 118.6}, + "13": {"cx": 129.3, "cy": 2068.8, "ht": 766.1, "width": 118.6}, + "14": {"cx": 301.6, "cy": 2121.5, "ht": 713.4, "width": 118.6}, + "15": {"cx": 477.5, "cy": 2153.1, "ht": 681.8, "width": 118.6}, + "16": {"cx": 656.7, "cy": 2232.2, "ht": 602.8, "width": 118.6}, + "17": {"cx": 841.2, "cy": 2290.7, "ht": 544.3, "width": 118.6}, + "18": {"cx": 1015.7, "cy": 2313.9, "ht": 521.1, "width": 118.6}, + "19": {"cx": 1199.5, "cy": 2437.2, "ht": 397.8, "width": 118.6}, + "20": {"cx": 1374.4, "cy": 2416.1, "ht": 418.9, "width": 118.6}, + "21": {"cx": 1553, "cy": 2510.9, "ht": 324.1, "width": 118.6}, + "22": {"cx": 1736.9, "cy": 2489.8, "ht": 345.1, "width": 118.6}, + "X": {"cx": 1911, "cy": 1799.6, "ht": 1035.4, "width": 59}, +} + +CHROM_SIZES = { + "hg37": { + "1": 249250621, "2": 243199373, "3": 198022430, "4": 191154276, + "5": 180915260, "6": 171115067, "7": 159138663, "8": 146364022, + "9": 141213431, "10": 135534747, "11": 135006516, "12": 133851895, + "13": 115169878, "14": 107349540, "15": 102531392, "16": 90354753, + "17": 81195210, "18": 78077248, "19": 59128983, "20": 63025520, + "21": 48129895, "22": 51304566, "X": 155270560, "Y": 59373566 + }, + "hg38": { + "1": 248956422, "2": 242193529, "3": 198295559, "4": 190214555, + "5": 181538259, "6": 170805979, "7": 159345973, "8": 145138636, + "9": 138394717, "10": 133797422, "11": 135086622, "12": 133275309, + "13": 114364328, "14": 107043718, "15": 101991189, "16": 90338345, + "17": 83257441, "18": 80373285, "19": 58617616, "20": 64444167, + "21": 46709983, "22": 50818468, "X": 156040895, "Y": 57227415 + }, +} + +def printif(statement, condition): + ''' + Print statements if a boolean (e.g. verbose) is true + ''' + if condition: + print(statement) + +def draw(arguments): + ''' + Create the SVG object + ''' + polygons = "" + try: + input_fh = open(arguments.input, 'r') + except (IOError, EOFError) as input_fh_e: + print("Error opening input file!") + raise input_fh_e + svg_fn = f"{arguments.prefix}.svg" + try: + svg_fh = open(svg_fn, 'w') + svg_fh.write(__head__) + except (IOError, EOFError) as svg_fh_e: + print("Error opening output file!") + raise svg_fh_e + line_num = 1 + for entry in input_fh: + if entry.startswith("#"): + continue + entry = entry.rstrip().split("\t") + if len(entry) != 7: + print(f"Line number {line_num} does not have 7 columns") + sys.exit() + chrm, start, stop, feature, size, col, chrcopy = entry + chrm = chrm.replace('chr', '') + start = int(start) + stop = int(stop) + size = float(size) + feature = int(feature) + chrcopy = int(chrcopy) + if 0 > size > 1: + print(f"Feature size, {size},on line {line_num} unclear. \ + Please bound the size between 0 (0%) to 1 (100%). Defaulting to 1.") + size = 1 + if not re.match("^#.{6}", col): + print(f"Feature color, {col}, on line {line_num} unclear. \ + Please define the color in hex starting with #. Defaulting to #000000.") + col = "#000000" + if chrcopy not in [1, 2]: + print(f"Feature chromosome copy, {chrcopy}, on line {line_num}\ + unclear. Skipping...") + line_num = line_num + 1 + continue + line_num = line_num + 1 + if feature == 0: # Rectangle + feat_start = start*COORDINATES[chrm]["ht"]/CHROM_SIZES[arguments.build][chrm] + feat_end = stop*COORDINATES[chrm]["ht"]/CHROM_SIZES[arguments.build][chrm] + width = COORDINATES[chrm]["width"]*size/2 + if chrcopy == 1: + x_pos = COORDINATES[chrm]["cx"] - width + else: + x_pos = COORDINATES[chrm]["cx"] + y_pos = COORDINATES[chrm]["cy"] + feat_start + height = feat_end-feat_start + svg_fh.write(f"" + "\n") + elif feature == 1: # Circle + feat_start = start*COORDINATES[chrm]["ht"]/CHROM_SIZES[arguments.build][chrm] + feat_end = stop*COORDINATES[chrm]["ht"]/CHROM_SIZES[arguments.build][chrm] + radius = COORDINATES[chrm]["width"]*size/4 + if chrcopy == 1: + x_pos = COORDINATES[chrm]["cx"] - COORDINATES[chrm]["width"]/4 + else: + x_pos = COORDINATES[chrm]["cx"] + COORDINATES[chrm]["width"]/4 + y_pos = COORDINATES[chrm]["cy"]+(feat_start+feat_end)/2 + svg_fh.write(f"" + "\n") + elif feature == 2: # Triangle + feat_start = start*COORDINATES[chrm]["ht"]/CHROM_SIZES[arguments.build][chrm] + feat_end = stop*COORDINATES[chrm]["ht"]/CHROM_SIZES[arguments.build][chrm] + if chrcopy == 1: + x_pos = COORDINATES[chrm]["cx"] - COORDINATES[chrm]["width"]/2 + sx_pos = 38.2*size + else: + x_pos = COORDINATES[chrm]["cx"] + COORDINATES[chrm]["width"]/2 + sx_pos = -38.2*size + y_pos = COORDINATES[chrm]["cy"]+(feat_start+feat_end)/2 + sy_pos = 21.5*size + polygons += f"" + "\n" + elif feature == 3: # Line + y_pos1 = start*COORDINATES[chrm]["ht"]/CHROM_SIZES[arguments.build][chrm] + y_pos2 = stop*COORDINATES[chrm]["ht"]/CHROM_SIZES[arguments.build][chrm] + y_pos = (y_pos1+y_pos2)/2 + y_pos += COORDINATES[chrm]["cy"] + if chrcopy == 1: + x_pos1 = COORDINATES[chrm]["cx"] - COORDINATES[chrm]["width"]/2 + x_pos2 = COORDINATES[chrm]["cx"] + svg_fh.write(f"" + "\n") + else: + x_pos1 = COORDINATES[chrm]["cx"] + x_pos2 = COORDINATES[chrm]["cx"] + COORDINATES[chrm]["width"]/2 + svg_fh.write(f"" + "\n") + else: + print(f"Feature type, {feature}, unclear. Please use either 0, 1, 2 or 3. Skipping...") + continue + svg_fh.write(__tail__) + svg_fh.write(polygons) + svg_fh.write("") + svg_fh.close() + printif(f"\033[92mSuccessfully created SVG\033[0m", ARGS.verbose) + + +if __name__ == "__main__": + PARSER = ArgumentParser(prog="tagore", + add_help=True, + description=''' + tagore: a utility for illustrating human chromosomes + https://github.com/jordanlab/tagore + ''', + formatter_class=lambda prog: HelpFormatter(prog, width=120, + max_help_position=120)) + + PARSER.add_argument('--version', action='version', + help='Print the software version', + version='tagore (version {})'.format(VERSION)) + + # Input arguments + PARSER.add_argument('-i', '--input', required=True, default=None, metavar='', + help='Input BED-like file') + PARSER.add_argument('-p', '--prefix', required=False, default="out", + metavar='[output file prefix]', + help='Output prefix [Default: "out"]') + PARSER.add_argument('-b', '--build', required=False, default="hg38", + metavar='[hg78/hg38]', + help="Human genome build to use [Default: hg38]") + PARSER.add_argument('-f', '--force', required=False, default=False, + help="Overwrite output files if they exist already", + action="store_true") + PARSER.add_argument('-v', '--verbose', required=False, default=False, + help="Display verbose output", + action="store_true") + ARGS, UNKARGS = PARSER.parse_known_args() + if UNKARGS: + print(f"\033[93mOne or more unknown arguments were supplied:\033[0m {' '.join(UNKARGS)}\n") + PARSER.print_help() + sys.exit() + if ARGS.build not in ['hg37', 'hg38']: + print(f"\033[91mBuild must be either 'hg37' or 'hg38', you supplied {ARGS.build}.\033[0m") + sys.exit() + if shutil.which("rsvg", mode=os.X_OK) is None: + print(f"\033[91mCould not find `rsvg` in PATH.\033[0m") + sys.exit() + BASE_PATH = os.path.join(sys.prefix, 'lib', 'tagore-data', 'base.svg.p') + try: + BASE = open(BASE_PATH, 'rb') + except (IOError, EOFError) as base_e: + print(f"\033[91mCould not open {BASE_PATH}. Please reinstall tagore\033[0m") + raise base_e + __head__, __tail__ = pickle.load(BASE) + printif(f"\033[94mDrawing chromosome ideogram using {ARGS.input}\033[0m", ARGS.verbose) + if os.path.exists(f"{ARGS.prefix}.svg") and ARGS.force is False: + print(f"\033[93m'{ARGS.prefix}.svg' already exists.\033[0m") + OW = input(f"Overwrite {ARGS.prefix}.svg? [Y/n]: ") or "y" + if OW.lower() != "y": + print(f"\033[93m'tagore will now exit...\033[0m") + sys.exit() + else: + print(f"\033[94mOverwriting existing file and saving to: {ARGS.prefix}.svg\033[0m") + else: + printif(f"\033[94mSaving to: {ARGS.prefix}.svg\033[0m", ARGS.verbose) + draw(ARGS) + printif(f"\033[94mConverting {ARGS.prefix}.svg -> {ARGS.prefix}.png\033[0m", ARGS.verbose) + try: + subprocess.check_output(f"rsvg {ARGS.prefix}.svg {ARGS.prefix}.png", shell=True) + except subprocess.CalledProcessError as rsvg_e: + printif(f"\033[91mFailed SVG to PNG conversion...\033[0m", ARGS.verbose) + raise rsvg_e + finally: + printif(f"\033[92mSuccessfully converted SVG to PNG\033[0m", ARGS.verbose) diff --git a/tests/00_printif_test.py b/tests/00_printif_test.py new file mode 100644 index 0000000..d14a062 --- /dev/null +++ b/tests/00_printif_test.py @@ -0,0 +1,20 @@ +#!/bin/env python3 +import pytest +import sys +from os import getcwd, path, pardir +sys.path.append(path.abspath(path.join(getcwd()))) +from tagore import printif + +def test_printif_true(): + from io import StringIO + output = StringIO() + sys.stdout = output + printif("Test passed", True) + assert output.getvalue().strip() == "Test passed" + +def test_printif_false(): + from io import StringIO + output = StringIO() + sys.stdout = output + printif("Test false", False) + assert output.getvalue().strip() == '' \ No newline at end of file