diff --git a/pygenometracks/tests/generateAllOutput.sh b/pygenometracks/tests/generateAllOutput.sh index f1d50bb6..a6d61f1f 100644 --- a/pygenometracks/tests/generateAllOutput.sh +++ b/pygenometracks/tests/generateAllOutput.sh @@ -132,6 +132,7 @@ bin/pgt --tracks ./pygenometracks/tests/test_data/arcs_no_score.ini --region X:3 bin/pgt --tracks ./pygenometracks/tests/test_data/links_squares.ini --region X:3000000-3300000 --trackLabelFraction 0.2 --width 38 --dpi 130 -o pygenometracks/tests/test_data/master_links_squares.png bin/pgt --tracks ./pygenometracks/tests/test_data/arcs_overlay.ini --region X:3000000-3300000 --trackLabelFraction 0.2 --width 38 --dpi 130 -o pygenometracks/tests/test_data/master_arcs_overlay.png bin/pgt --tracks ./pygenometracks/tests/test_data/links_squares_overlay.ini --region X:3000000-3300000 --trackLabelFraction 0.2 --width 38 --dpi 130 -o pygenometracks/tests/test_data/master_links_squares_overlay.png +bin/pgt --tracks ./pygenometracks/tests/test_data/short_long_arcs_plot_arrows.ini --region chr11:40000000-46000000 --trackLabelFraction 0.2 --width 38 --dpi 130 -o pygenometracks/tests/test_data/master_short_long_arcs_plot_arrows.png # tests scaleBar bin/pgt --tracks pygenometracks/tests/test_data/scale_bar.ini --region X:3200000-3300000 --trackLabelFraction 0.2 --width 38 --dpi 130 -o pygenometracks/tests/test_data/master_scale_bar_zoom.png diff --git a/pygenometracks/tests/test_arcs.py b/pygenometracks/tests/test_arcs.py index e7e6f5da..5af61747 100644 --- a/pygenometracks/tests/test_arcs.py +++ b/pygenometracks/tests/test_arcs.py @@ -106,6 +106,72 @@ with open(os.path.join(ROOT, "short_long_arcs_incorrect2.ini"), 'w') as fh: fh.write(browser_tracks.replace('compact_arcs_level = 2', 'compact_arcs_level = 2\nregion2=chrX:0-10')) +browser_tracks = """ +[arcs] +title = default +file = short_long.arcs +color = bwr +height = 5 +plot_arrows = true + +[spacer] + +[arcs] +file = short_long.arcs +color = bwr +height = 5 +title = ylim = 6000000 (6Mb) +ylim = 6000000 +plot_arrows = true + +[spacer] + +[arcs] +file = short_long.arcs +color = bwr +height = 5 +title = ylim = 200000 (200kb) +ylim = 200000 +plot_arrows = true + +[spacer] + +[arcs] +title = compact_arcs_level = 1 +file = short_long.arcs +color = bwr +height = 5 +compact_arcs_level = 1 +plot_arrows = true + +[spacer] + +[arcs] +title = compact_arcs_level = 1 ylim = 6000000 (6Mb) +ylim = 6000000 +file = short_long.arcs +color = bwr +height = 5 +compact_arcs_level = 1 +plot_arrows = true + +[spacer] + +[arcs] +title = compact_arcs_level = 2 line_style = dashed +file = short_long.arcs +color = bwr +height = 5 +compact_arcs_level = 2 +plot_arrows = true +line_style = dashed + +[x-axis] +where = bottom +""" +with open(os.path.join(ROOT, "short_long_arcs_plot_arrows.ini"), 'w') as fh: + fh.write(browser_tracks) + browser_tracks = """ [x-axis] where = top @@ -382,6 +448,24 @@ def test_short_long_arcs(): os.remove(ini_file) +def test_use_plot_arrows(): + + outfile = NamedTemporaryFile(suffix='.png', prefix='pyGenomeTracks_test_', + delete=False) + ini_file = os.path.join(ROOT, "short_long_arcs_plot_arrows.ini") + region = "chr11:40000000-46000000" + expected_file = os.path.join(ROOT, 'master_short_long_arcs_plot_arrows.png') + args = f"--tracks {ini_file} --region {region} "\ + "--trackLabelFraction 0.2 --width 38 --dpi 130 "\ + f"--outFileName {outfile.name}".split() + pygenometracks.plotTracks.main(args) + res = compare_images(expected_file, + outfile.name, tolerance) + assert res is None, res + + os.remove(outfile.name) + + def test_use_middle_arcs(): outfile = NamedTemporaryFile(suffix='.png', prefix='pyGenomeTracks_test_', diff --git a/pygenometracks/tests/test_data/master_short_long_arcs_plot_arrows.png b/pygenometracks/tests/test_data/master_short_long_arcs_plot_arrows.png new file mode 100644 index 00000000..7e14aa57 Binary files /dev/null and b/pygenometracks/tests/test_data/master_short_long_arcs_plot_arrows.png differ diff --git a/pygenometracks/tests/test_data/short_long_arcs_plot_arrows.ini b/pygenometracks/tests/test_data/short_long_arcs_plot_arrows.ini new file mode 100644 index 00000000..7a773b9d --- /dev/null +++ b/pygenometracks/tests/test_data/short_long_arcs_plot_arrows.ini @@ -0,0 +1,62 @@ + +[arcs] +title = default +file = short_long.arcs +color = bwr +height = 5 +plot_arrows = true + +[spacer] + +[arcs] +file = short_long.arcs +color = bwr +height = 5 +title = ylim = 6000000 (6Mb) +ylim = 6000000 +plot_arrows = true + +[spacer] + +[arcs] +file = short_long.arcs +color = bwr +height = 5 +title = ylim = 200000 (200kb) +ylim = 200000 +plot_arrows = true + +[spacer] + +[arcs] +title = compact_arcs_level = 1 +file = short_long.arcs +color = bwr +height = 5 +compact_arcs_level = 1 +plot_arrows = true + +[spacer] + +[arcs] +title = compact_arcs_level = 1 ylim = 6000000 (6Mb) +ylim = 6000000 +file = short_long.arcs +color = bwr +height = 5 +compact_arcs_level = 1 +plot_arrows = true + +[spacer] + +[arcs] +title = compact_arcs_level = 2 line_style = dashed +file = short_long.arcs +color = bwr +height = 5 +compact_arcs_level = 2 +plot_arrows = true +line_style = dashed + +[x-axis] +where = bottom diff --git a/pygenometracks/tracks/LinksTrack.py b/pygenometracks/tracks/LinksTrack.py index aaa1e907..03febf21 100644 --- a/pygenometracks/tracks/LinksTrack.py +++ b/pygenometracks/tracks/LinksTrack.py @@ -3,7 +3,8 @@ from intervaltree import IntervalTree, Interval import matplotlib import numpy as np -from matplotlib.patches import Arc, Polygon +import math +from matplotlib.patches import Arc, Polygon, FancyArrowPatch from .. utilities import opener, to_string, change_chrom_names, temp_file_from_intersect, get_region from tqdm import tqdm @@ -66,6 +67,10 @@ class LinksTrack(GenomeTrack): # The unit is bp. This corresponds to the longest arc you will see. # This option is incompatible with compact_arcs_level = 2 #ylim = 100000 +# You can add arrows on your arcs to indicate orientation +#plot_arrows = true +# You can adjust the size of arrowhead by playing with mutation_scale +#mutation_scale = 20 file_type = {TRACK_TYPE} """ DEFAULTS_PROPERTIES = {'links_type': 'arcs', @@ -80,7 +85,9 @@ class LinksTrack(GenomeTrack): 'ylim': None, 'compact_arcs_level': '0', 'use_middle': False, - 'region2': None} + 'region2': None, + 'plot_arrows': False, + 'mutation_scale': 20} NECESSARY_PROPERTIES = ['file'] SYNONYMOUS_PROPERTIES = {'max_value': {'auto': None}, 'min_value': {'auto': None}, @@ -90,7 +97,7 @@ class LinksTrack(GenomeTrack): 'line_style': ['solid', 'dashed', 'dotted', 'dashdot'], 'compact_arcs_level': ['0', '1', '2']} - BOOLEAN_PROPERTIES = ['use_middle'] + BOOLEAN_PROPERTIES = ['use_middle', 'plot_arrows'] STRING_PROPERTIES = ['file', 'file_type', 'overlay_previous', 'orientation', 'links_type', 'line_style', 'title', 'color', 'compact_arcs_level', @@ -100,7 +107,8 @@ class LinksTrack(GenomeTrack): 'ylim': [0, np.inf], 'alpha': [0, 1], 'line_width': [0, np.inf], - 'height': [0, np.inf]} + 'height': [0, np.inf], + 'mutation_scale': [0, np.inf]} INTEGER_PROPERTIES = {} # The color can be a color or a colormap (if there is a score) @@ -202,7 +210,7 @@ def plot(self, ax, chrom_region, region_start, region_end): for idx, interval in enumerate(arcs_in_region): if self.properties['links_type'] == 'squares': plotting_sides = {'as_in_data': False, 'mirrored': False} - start1, end1, start2, end2, _ = interval.data + start1, end1, start2, end2, _, _ = interval.data if self.properties['region2'] is None: temp_region2 = [chrom_region, region_start, region_end] else: @@ -298,10 +306,44 @@ def plot_arcs(self, ax, interval): rgb = self.colormap.to_rgba(interval.data[4]) else: rgb = self.properties['color'] - ax.add_patch(Arc((center, 0), width, - 2 * half_height, 0, 0, 180, color=rgb, - linewidth=self.current_line_width, - ls=self.properties['line_style'])) + if not self.properties['plot_arrows']: + ax.add_patch(Arc((center, 0), width, + 2 * half_height, 0, 0, 180, color=rgb, + linewidth=self.current_line_width, + ls=self.properties['line_style'])) + else: + is_flipped = interval.data[5] + angle_in_rad = 0.01 + rel_A_point = (width / 2 * math.cos(angle_in_rad), + half_height * math.sin(angle_in_rad)) + angle_on_ellipse_in_rad = math.atan(rel_A_point[1] / rel_A_point[0]) + if is_flipped: + ax.add_patch(Arc((center, 0), width, + 2 * half_height, 0, 0, + 180 - math.degrees(angle_on_ellipse_in_rad), + color=rgb, + linewidth=self.current_line_width, + ls=self.properties['line_style'])) + ax.add_patch(FancyArrowPatch(posA=(center - rel_A_point[0], rel_A_point[1]), + posB=(interval.begin, 0), + arrowstyle='simple,tail_width=0', + mutation_scale=self.properties['mutation_scale'], + color=rgb, connectionstyle=f'arc3,rad={angle_in_rad * 0.2}', + linewidth=self.current_line_width / 2, + ls=self.properties['line_style'])) + else: + ax.add_patch(Arc((center, 0), width, + 2 * half_height, 0, math.degrees(angle_on_ellipse_in_rad), + 180, color=rgb, + linewidth=self.current_line_width, + ls=self.properties['line_style'])) + ax.add_patch(FancyArrowPatch(posA=(center + rel_A_point[0], rel_A_point[1]), + posB=(interval.end, 0), + arrowstyle='simple,tail_width=0', + mutation_scale=self.properties['mutation_scale'], + color=rgb, connectionstyle=f'arc3,rad=-{angle_in_rad * 0.2}', + linewidth=self.current_line_width / 2, + ls=self.properties['line_style'])) def plot_triangles(self, ax, interval): x1 = interval.begin @@ -499,23 +541,25 @@ def process_link_file(self, plot_regions): if chrom1 not in interval_tree: interval_tree[chrom1] = IntervalTree() + is_flipped = False if start2 < start1 and not is_trans: + is_flipped = True start1, start2 = start2, start1 end1, end2 = end2, end1 if self.properties['use_middle']: mid1 = (start1 + end1) / 2 mid2 = (start2 + end2) / 2 if mid1 < mid2: - interval_tree[chrom1].add(Interval(mid1, mid2, [start1, end1, start2, end2, score])) + interval_tree[chrom1].add(Interval(mid1, mid2, [start1, end1, start2, end2, score, is_flipped])) else: - interval_tree[chrom1].add(Interval(mid2, mid1, [start2, end2, start1, end1, score])) + interval_tree[chrom1].add(Interval(mid2, mid1, [start2, end2, start1, end1, score, is_flipped])) else: if not is_trans: # each interval spans from the smallest start to the largest end - interval_tree[chrom1].add(Interval(start1, max(end1, end2), [start1, end1, start2, end2, score])) + interval_tree[chrom1].add(Interval(start1, max(end1, end2), [start1, end1, start2, end2, score, is_flipped])) else: # For the trans we keep start1 and end1 - interval_tree[chrom1].add(Interval(start1, end1, [start1, end1, start2, end2, score])) + interval_tree[chrom1].add(Interval(start1, end1, [start1, end1, start2, end2, score, is_flipped])) valid_intervals += 1 if valid_intervals == 0: diff --git a/setup.py b/setup.py index 32e315a4..08def4a3 100644 --- a/setup.py +++ b/setup.py @@ -123,6 +123,6 @@ def checkProgramIsInstalled(self, program, args, where_to_download, 'Topic :: Scientific/Engineering :: Bio-Informatics'], install_requires=install_requires_py, zip_safe=False, - python_requires='>=3.7.*, <4', + python_requires='>=3.7,<4', cmdclass={'sdist': sdist, 'install': install} )