diff --git a/README.md b/README.md index 571266d..1021649 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ ```bash pip install tagore tagore --version -# tagore (version 1.0.1) +# tagore (version 1.1.2) ``` ### Requirements @@ -60,13 +60,13 @@ Helper scripts for converting RFMix and ADMIXTURE outputs are included in the `s A more complete example of a full chromosome painting using an RFMix output can be seen by running: ```bash -./scripts/rfmix2tagore.py --chr1 example_ideogram/1KGP-MXL104_chr1.bed \ +rfmix2tagore --chr1 example_ideogram/1KGP-MXL104_chr1.bed \ --chr2 example_ideogram/1KGP-MXL104_chr2.bed \ --out example_ideogram/1KGP-MXL104_tagore.bed tagore --input example_ideogram/1KGP-MXL104_tagore.bed \ --prefix example_ideogram/1KGP-MXL104 \ - --build hg37 + --build hg37 \ --verbose ``` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7fd26b9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/scripts/rfmix2tagore.py b/scripts/rfmix2tagore.py deleted file mode 100755 index 1a18a38..0000000 --- a/scripts/rfmix2tagore.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/env python3 -__author__ = "Aroon Chande" -__copyright__ = "Copyright 2019, Applied Bioinformatics Lab" -__license__ = "MIT" - -import os -import click - -commentBlock = ''' -# Each column is explained below: -# 1. chr - The chromosome on which a feature has to be drawn -# 2. start - Start position (in bp) for feature -# 3. stop - Stop position (in bp) for feature -# 4. feature - The shape of the feature to be drawn -# 0 - will draw a rectangle -# 1 - will draw a circle -# 2 - will draw a triangle pointing to the genomic location -# 3 - will draw a line at that genomic location -# 5. size - The horizontal size of the feature. Should range between 0 and 1. -# 6. color - Specify the color of the genomic feature with a hex value (#FF0000 for red, etc.) -# 7. chrCopy - Specify the chromosome copy on which the feature should be drawn (1 or 2). To draw the same feature on both chromosomes, you must specify the feature twice -''' - - -@click.command() -@click.option('--chr1', '-1', 'chr1', default=None, type=click.File('r'), help="Chromosome 1 RFMix painting") -@click.option('--chr2', '-2', 'chr2', default=None, type=click.File('r'), help="Chromosome 2 RFMix painting") -@click.option('--afr', 'afr', default="#0000ff ", help="Color for African blocks") -@click.option('--eur', 'eur', default="#F4A500", help="Color for European blocks") -@click.option('--nat', 'nat', default="#D92414", help="Color for Native American / Asian blocks") -@click.option('--unk', 'unk', default="#808080 ", help="Color for Unknown regions") -@click.option('--out', '-o', 'output', default=None, type=click.File('w'), help="Output da Vinci bed") -def main(chr1, chr2, afr, eur, nat, unk, output): - colors = {"UNK" : unk, "AFR" : afr, "IBS" : eur, "NAT" : nat} - output.write("#chr\tstart\tstop\tfeature\tsize\tcolor\tchrCopy") - output.write(commentBlock) -# chr1 10000000 20000000 0 1 #FF0000 1 - for line in chr1: - line = line.split('\t') - bedLine = f"chr{line[0]}\t{line[1]}\t{line[2]}\t0\t1\t{colors[line[3]]}\t1" - output.write(bedLine + "\n") - for line in chr2: - line = line.split('\t') - bedLine = f"chr{line[0]}\t{line[1]}\t{line[2]}\t0\t1\t{colors[line[3]]}\t2" - output.write(bedLine + "\n") - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..791c947 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,43 @@ +[metadata] +name = tagore +version = 1.1.2 +author = Applied Bioinformatics Laboratory +author_email = abil@ihrc.com +description = A simple way to visualize features on human chromosome ideograms +keywords = + Human + chromosome + ideogram + visualization +url = https://github.com/jordanlab/tagore +long_description = file: README.md +long_description_content_type = text/markdown +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: GNU General Public License v3 (GPLv3) + Operating System :: MacOS + Operating System :: POSIX + Environment :: Console + Development Status :: 5 - Production/Stable + Topic :: Scientific/Engineering :: Visualization + +[options] +name = tagore +packages = find_namespace: +include_package_data = True +install_requires = click +python_requires = >=3.6 +# scripts = tagore/scripts/rfmix2tagore.py + +[options.packages.find] +where=src +exclude=tagore.tests* + +[options.entry_points] +console_scripts = + tagore = tagore.main:run + rfmix2tagore = tagore.scripts.rfmix2tagore:main + + +[options.package_data] +* = *.svg.p \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 023b443..0000000 --- a/setup.py +++ /dev/null @@ -1,30 +0,0 @@ -import setuptools - -with open("README.md", "r") as fh: - long_description = fh.read() - -setuptools.setup( - name="tagore", - version="1.1.1", - author="Applied Bioinformatics Laboratory", - author_email="abil@ihrc.com", - description="A simple way to visualize features on human chromosome ideograms", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/jordanlab/tagore", - packages=setuptools.find_packages(), - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - "Operating System :: MacOS", - "Operating System :: POSIX", - "Environment :: Console", - "Development Status :: 5 - Production/Stable", - "Topic :: Scientific/Engineering :: Visualization" - ], - python_requires='>=3.6', - install_requires=['click'], - keywords = ['Human', 'chromosome', 'ideogram', 'visualization'], - scripts = ['tagore', 'scripts/rfmix2tagore.py'], - data_files = [('lib/tagore-data/', ['base.svg.p'])] -) \ No newline at end of file diff --git a/src/tagore/__init__.py b/src/tagore/__init__.py new file mode 100644 index 0000000..0f802cf --- /dev/null +++ b/src/tagore/__init__.py @@ -0,0 +1 @@ +# from . import run diff --git a/base.svg.p b/src/tagore/base.svg.p similarity index 100% rename from base.svg.p rename to src/tagore/base.svg.p diff --git a/src/tagore/main.py b/src/tagore/main.py new file mode 100755 index 0000000..0aa5847 --- /dev/null +++ b/src/tagore/main.py @@ -0,0 +1,395 @@ +#!/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 pkgutil +import re +import shutil +import subprocess +import sys +from argparse import ArgumentParser, HelpFormatter + +VERSION = "1.1.2" + +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": 1915.7, "cy": 1799.21, "ht": 1035.4, "width": 118.6}, + "Y": {"cx": 2120.9, "cy": 2451.6, "ht": 382.7, "width": 118.6}, +} + +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, svg_header, svg_footer): + """ + 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(svg_header) + 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(svg_footer) + svg_fh.write(polygons) + svg_fh.write("") + svg_fh.close() + printif(f"\033[92mSuccessfully created SVG\033[0m", arguments.verbose) + + +def run(): + 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( + "-ofmt", + "--oformat", + required=False, + default="png", + help="Output format for conversion (pdf requires rsvg-convert)", + metavar="[png/pdf]", + ) + parser.add_argument( + "-v", + "--verbose", + required=False, + default=False, + help="Display verbose output", + action="store_true", + ) + parsed_args, unknown_args = parser.parse_known_args() + if unknown_args: + print( + f"\033[93mOne or more unknown arguments were supplied:\033[0m {' '.join(unknown_args)}\n" + ) + parser.print_help() + sys.exit() + if parsed_args.build not in ["hg37", "hg38"]: + print( + f"\033[91mBuild must be either 'hg37' or 'hg38', you supplied {parsed_args.build}.\033[0m" + ) + sys.exit() + + if shutil.which("rsvg-convert", mode=os.X_OK) is None: + if shutil.which("rsvg", mode=os.X_OK) is None: + print(f"\033[91mCould not find `rsvg` or `rsvg-convert` in PATH.\033[0m") + sys.exit() + else: + is_rsvg_installed = True + if parsed_args.oformat != "png": + print(f"\033[93m`rsvg` only supports PNG output, using png\033[0m") + parsed_args.oformat = "png" + else: + is_rsvg_installed = False + if parsed_args.oformat not in ["png", "pdf"]: + print(f"\033[93m{parsed_args.oformat} is not PNG or PDF, using PNG\033[0m") + parsed_args.oformat = "png" + svg_pkl_data = pkgutil.get_data("tagore", "base.svg.p") + svg_header, svg_footer = pickle.loads(svg_pkl_data) + printif( + f"\033[94mDrawing chromosome ideogram using {parsed_args.input}\033[0m", + parsed_args.verbose, + ) + if os.path.exists(f"{parsed_args.prefix}.svg") and parsed_args.force is False: + print(f"\033[93m'{parsed_args.prefix}.svg' already exists.\033[0m") + OW = input(f"Overwrite {parsed_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: {parsed_args.prefix}.svg\033[0m" + ) + else: + printif( + f"\033[94mSaving to: {parsed_args.prefix}.svg\033[0m", parsed_args.verbose + ) + draw(parsed_args, svg_header, svg_footer) + printif( + f"\033[94mConverting {parsed_args.prefix}.svg -> {parsed_args.prefix}.{parsed_args.oformat} \033[0m", + parsed_args.verbose, + ) + try: + if is_rsvg_installed: + subprocess.check_output( + f"rsvg {parsed_args.prefix}.svg {parsed_args.prefix}.{parsed_args.oformat} ", + shell=True, + ) + else: + subprocess.check_output( + f"rsvg-convert -o {parsed_args.prefix}.{parsed_args.oformat} -f {parsed_args.oformat} {parsed_args.prefix}.svg ", + shell=True, + ) + except subprocess.CalledProcessError as rsvg_e: + printif(f"\033[91mFailed SVG to PNG conversion...\033[0m", parsed_args.verbose) + raise rsvg_e + finally: + printif( + f"\033[92mSuccessfully converted SVG to {parsed_args.oformat.upper()}\033[0m", + parsed_args.verbose, + ) diff --git a/scripts/LICENSE b/src/tagore/scripts/LICENSE similarity index 100% rename from scripts/LICENSE rename to src/tagore/scripts/LICENSE diff --git a/src/tagore/scripts/rfmix2tagore.py b/src/tagore/scripts/rfmix2tagore.py new file mode 100755 index 0000000..ec92123 --- /dev/null +++ b/src/tagore/scripts/rfmix2tagore.py @@ -0,0 +1,73 @@ +#!/bin/env python3 +__author__ = "Aroon Chande" +__copyright__ = "Copyright 2019, Applied Bioinformatics Lab" +__license__ = "MIT" + +import os + +import click + +commentBlock = """ +# Each column is explained below: +# 1. chr - The chromosome on which a feature has to be drawn +# 2. start - Start position (in bp) for feature +# 3. stop - Stop position (in bp) for feature +# 4. feature - The shape of the feature to be drawn +# 0 - will draw a rectangle +# 1 - will draw a circle +# 2 - will draw a triangle pointing to the genomic location +# 3 - will draw a line at that genomic location +# 5. size - The horizontal size of the feature. Should range between 0 and 1. +# 6. color - Specify the color of the genomic feature with a hex value (#FF0000 for red, etc.) +# 7. chrCopy - Specify the chromosome copy on which the feature should be drawn (1 or 2). To draw the same feature on both chromosomes, you must specify the feature twice +""" + + +@click.command() +@click.option( + "--chr1", + "-1", + "chr1", + default=None, + type=click.File("r"), + help="Chromosome 1 RFMix painting", +) +@click.option( + "--chr2", + "-2", + "chr2", + default=None, + type=click.File("r"), + help="Chromosome 2 RFMix painting", +) +@click.option("--afr", "afr", default="#0000ff ", help="Color for African blocks") +@click.option("--eur", "eur", default="#F4A500", help="Color for European blocks") +@click.option( + "--nat", "nat", default="#D92414", help="Color for Native American / Asian blocks" +) +@click.option("--unk", "unk", default="#808080 ", help="Color for Unknown regions") +@click.option( + "--out", + "-o", + "output", + default=None, + type=click.File("w"), + help="Output da Vinci bed", +) +def main(chr1, chr2, afr, eur, nat, unk, output): + colors = {"UNK": unk, "AFR": afr, "IBS": eur, "NAT": nat} + output.write("#chr\tstart\tstop\tfeature\tsize\tcolor\tchrCopy") + output.write(commentBlock) + # chr1 10000000 20000000 0 1 #FF0000 1 + for line in chr1: + line = line.split("\t") + bedLine = f"chr{line[0]}\t{line[1]}\t{line[2]}\t0\t1\t{colors[line[3]]}\t1" + output.write(bedLine + "\n") + for line in chr2: + line = line.split("\t") + bedLine = f"chr{line[0]}\t{line[1]}\t{line[2]}\t0\t1\t{colors[line[3]]}\t2" + output.write(bedLine + "\n") + + +if __name__ == "__main__": + main() diff --git a/tests/00_printif_test.py b/src/tagore/tests/00_printif_test.py similarity index 73% rename from tests/00_printif_test.py rename to src/tagore/tests/00_printif_test.py index d14a062..2b835d8 100644 --- a/tests/00_printif_test.py +++ b/src/tagore/tests/00_printif_test.py @@ -1,20 +1,26 @@ #!/bin/env python3 -import pytest import sys -from os import getcwd, path, pardir +from os import getcwd, pardir, path + +import pytest + 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" + 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 + assert output.getvalue().strip() == "" diff --git a/tagore b/tagore deleted file mode 100755 index bace594..0000000 --- a/tagore +++ /dev/null @@ -1,273 +0,0 @@ -#!/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.1.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": 1915.7, "cy": 1799.21, "ht": 1035.4, "width": 118.6}, - "Y": {"cx": 2120.9, "cy": 2451.6, "ht": 382.7, "width": 118.6}, - -} - -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('-ofmt', '--oformat', required=False, default='png', - help="Output format for conversion (pdf requires rsvg-convert)", - metavar='[png/pdf]') - 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-convert", mode=os.X_OK) is None: - if shutil.which("rsvg", mode=os.X_OK) is None: - print(f"\033[91mCould not find `rsvg` or `rsvg-convert` in PATH.\033[0m") - sys.exit() - else: - RSVG = True - if ARGS.oformat != "png": - print(f"\033[93m`rsvg` only supports PNG output, using png\033[0m") - ARGS.oformat = "png" - else: - RSVG = False - if ARGS.oformat not in ["png", "pdf"]: - print(f"\033[93m{ARGS.oformat} is not PNG or PDF, using PNG\033[0m") - ARGS.oformat = "png" - 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: - BASE_PATH = os.path.join(site.USER_BASE, '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}.{ARGS.oformat} \033[0m", - ARGS.verbose) - try: - if RSVG: - subprocess.check_output(f"rsvg {ARGS.prefix}.svg {ARGS.prefix}.{ARGS.oformat} ", - shell=True) - else: - subprocess.check_output(f"rsvg-convert -o {ARGS.prefix}.{ARGS.oformat} -f {ARGS.oformat} {ARGS.prefix}.svg ", 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 {ARGS.oformat.upper()}\033[0m", - ARGS.verbose)