From 77754f3d0bab59094c08bb391b7e6ea820a9ad75 Mon Sep 17 00:00:00 2001 From: donkirkby Date: Fri, 25 Oct 2019 14:48:28 -0700 Subject: [PATCH] Add an Arrow element to genome coverage diagrams, as part of #442. Add an image comparison to the unit tests. --- micall/core/plot_contigs.py | 35 ++++++++++++++ micall/tests/svg_diffs/.gitignore | 2 + micall/tests/test_plot_contigs.py | 79 ++++++++++++++++++++++++++++++- requirements.txt | 2 +- 4 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 micall/tests/svg_diffs/.gitignore diff --git a/micall/core/plot_contigs.py b/micall/core/plot_contigs.py index 178c3e50b..d78909c27 100644 --- a/micall/core/plot_contigs.py +++ b/micall/core/plot_contigs.py @@ -11,6 +11,7 @@ from genetracks import Figure, Track, Multitrack, Coverage # noinspection PyPep8Naming import drawSvg as draw +from genetracks.elements import Element from matplotlib import cm, colors from matplotlib.colors import Normalize @@ -67,6 +68,40 @@ def get_color(self, coverage): return colors.to_hex(rgba) +class Arrow(Element): + def __init__(self, start, end, h=30, label=None): + super().__init__(x=start, y=0, w=end-start, h=h) + self.label = label + + def draw(self, x=0, y=0, xscale=1.0): + h = self.h + a = self.x * xscale + b = (self.x + self.w) * xscale + x = x * xscale + direction = 1 + r = 10 * xscale + arrow_size = 7 * xscale + arrow_end = b + arrow_start = arrow_end - arrow_size*direction + centre = (a + b - direction*arrow_size)/2 + centre_start = centre - direction*r + centre_end = centre + direction*r + group = draw.Group(transform="translate({} {})".format(x, y)) + group.append(draw.Circle(centre, h/2, r, fill='ivory', stroke='black')) + group.append(draw.Line(a, h/2, centre_start, h/2, stroke='black')) + group.append(draw.Line(centre_end, h/2, arrow_start, h/2, stroke='black')) + group.append(draw.Lines(arrow_end, h/2, + arrow_start, (h + arrow_size)/2, + arrow_start, (h - arrow_size)/2, + fill='black')) + group.append(draw.Text(self.label, + 15, + centre, h/2, + text_anchor='middle', + dy="0.3em")) + return group + + def plot_genome_coverage(genome_coverage_csv, genome_coverage_svg_path): f = build_coverage_figure(genome_coverage_csv) f.show(w=970).saveSvg(genome_coverage_svg_path) diff --git a/micall/tests/svg_diffs/.gitignore b/micall/tests/svg_diffs/.gitignore new file mode 100644 index 000000000..8c5843783 --- /dev/null +++ b/micall/tests/svg_diffs/.gitignore @@ -0,0 +1,2 @@ +*.svg +*.png \ No newline at end of file diff --git a/micall/tests/test_plot_contigs.py b/micall/tests/test_plot_contigs.py index 407eeaa75..696e41906 100644 --- a/micall/tests/test_plot_contigs.py +++ b/micall/tests/test_plot_contigs.py @@ -1,10 +1,15 @@ -from io import StringIO +from base64 import standard_b64encode +from io import StringIO, BytesIO +from pathlib import Path +from turtle import Turtle import pytest +from PIL import Image, ImageChops +from drawSvg import Drawing, Line, Lines, Circle, Text from genetracks import Figure, Track, Multitrack, Label, Coverage from micall.core.plot_contigs import summarize_figure, \ - build_coverage_figure, SmoothCoverage, add_partial_banner + build_coverage_figure, SmoothCoverage, add_partial_banner, Arrow HCV_HEADER = ('C[342-915], E1[915-1491], E2[1491-2580], P7[2580-2769], ' 'NS2[2769-3420], NS3[3420-5313], NS4A[5313-5475], ' @@ -15,6 +20,54 @@ pol[2085-5096], vpr[5559-5850], rev[5970-6045], env[6225-8795]''' +class SvgDiffer: + def __init__(self): + self.work_dir: Path = Path(__file__).parent / 'svg_diffs' + self.work_dir.mkdir(exist_ok=True) + for work_file in self.work_dir.iterdir(): + if work_file.name == '.gitignore': + continue + assert work_file.suffix in ('.svg', '.png') + work_file.unlink() + + def assert_equal(self, + svg_actual: Drawing, + svg_expected: Drawing, + name: str): + # Display image when in live turtle mode. + display_image = getattr(Turtle, 'display_image', None) + if display_image is not None: + encoded = standard_b64encode(svg_actual.rasterize().pngData) + display_image(0, 0, image=encoded.decode('UTF-8')) + + png_actual = drawing_to_image(svg_actual) + png_expected = drawing_to_image(svg_expected) + png_diff = ImageChops.difference(png_actual, png_expected) + + extrema = png_diff.getextrema() + if extrema == ((0, 0), (0, 0), (0, 0), (0, 0)): + return + text_actual = svg_actual.asSvg() + (self.work_dir / (name+'_actual.svg')).write_text(text_actual) + text_expected = svg_expected.asSvg() + (self.work_dir / (name+'_expected.svg')).write_text(text_expected) + with (self.work_dir / (name+'_diff.png')) as f: + png_diff.save(f) + assert text_actual == text_expected + + +def drawing_to_image(drawing: Drawing) -> Image: + png = drawing.rasterize() + png_bytes = BytesIO(png.pngData) + image = Image.open(png_bytes) + return image + + +@pytest.fixture(scope='session') +def svg_differ(): + return SvgDiffer() + + def test_summarize_labels(): figure = Figure() figure.add(Track(1, 200, label="Foo")) @@ -398,3 +451,25 @@ def test_plot_genome_coverage_insertion(): figure = build_coverage_figure(genome_coverage_csv) assert expected_figure == summarize_figure(figure) + + +def test_arrow(svg_differ): + expected_svg = Drawing(200.0, 60.0, origin=(0, 0)) + expected_svg.append(Circle(168/2, 40, 10, stroke='black', fill='ivory')) + expected_svg.append(Text('X', + 15, + 168/2, 40, + text_anchor='middle', + dy="0.3em")) + expected_svg.append(Line(0, 40, 74, 40, stroke='black')) + expected_svg.append(Line(94, 40, 168, 40, stroke='black')) + expected_svg.append(Lines(175, 40, + 168, 43.5, + 168, 36.5, + 175, 40, + fill='black')) + f = Figure() + f.add(Arrow(0, 175, label='X')) + svg = f.show() + + svg_differ.assert_equal(svg, expected_svg, 'test_arrow') diff --git a/requirements.txt b/requirements.txt index f2a770a20..2e0db5b6c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,4 @@ python-Levenshtein==0.12.0 PyYAML==5.1 reportlab==3.4.0 pysam==0.15.2 -git+https://github.com/jeff-k/genetracks.git \ No newline at end of file +git+https://github.com/cfe-lab/genetracks.git@v0.2.dev0