diff --git a/python/CHANGELOG.rst b/python/CHANGELOG.rst index 1e16134048..38fbc2c3cb 100644 --- a/python/CHANGELOG.rst +++ b/python/CHANGELOG.rst @@ -4,8 +4,8 @@ **Features** -- SVG visualization now uses red crosses for mutations, and squares for sample nodes - (:user:`hyanwong`,:issue:`1155`, :pr:`1182`). +- SVG visualization now uses red crosses for mutations and squares for sample nodes, and + an x-axis label can be set (:user:`hyanwong`,:issue:`1155`, :pr:`1182`, :pr:`1213`). - Add ``parents`` column to the individual table to allow recording of pedigrees (:user:`ivan-krukov`, :user:`benjeffery`, :issue:`852`, :pr:`1125`, :pr:`866`, :pr:`1153`, :pr:`1177` :pr:`1192`). diff --git a/python/tests/data/svg/mut_tree.svg b/python/tests/data/svg/mut_tree.svg index b0ac6cddac..5cfe55b840 100644 --- a/python/tests/data/svg/mut_tree.svg +++ b/python/tests/data/svg/mut_tree.svg @@ -1,41 +1,41 @@ - + - - - - + + + + 0 - - + + 1 - + 4 - + 2 - - - + + + 2 - - + + 3 - + 5 diff --git a/python/tests/data/svg/tree.svg b/python/tests/data/svg/tree.svg index 0a41b1dcfc..1b93c4ea74 100644 --- a/python/tests/data/svg/tree.svg +++ b/python/tests/data/svg/tree.svg @@ -1,37 +1,37 @@ - + - - - - + + + + 0 - - + + 1 - + 4 - - - + + + 2 - - + + 3 - + 5 diff --git a/python/tests/data/svg/ts.svg b/python/tests/data/svg/ts.svg index 828226cb9a..9bc12cccfc 100644 --- a/python/tests/data/svg/ts.svg +++ b/python/tests/data/svg/ts.svg @@ -1,7 +1,7 @@ - + @@ -10,208 +10,208 @@ - + - - - - + + + + 0 - - + + 1 - + 4 - + 2 - - - + + + 2 - - + + 3 - + 5 - + 9 - + 1 - + 0 - + - - - - + + + + 0 - - + + 1 - + 4 - - - + + + 2 - - + + 3 - + 5 - + 7 - + - - - - + + + + 0 - - + + 1 - + 4 - - - + + + 2 - - + + 3 - + 5 - + 6 - + - - - - + + + + 0 - - + + 1 - + 4 - - - + + + 2 - - + + 3 - + 5 - + 7 - + - - - - + + + + 0 - - + + 1 - + 4 - - - + + + 2 - - + + 3 - + 5 - + 8 @@ -221,28 +221,28 @@ - - 0.00 + + 0.00 - - 0.06 + + 0.06 - - 0.79 + + 0.79 - - 0.91 + + 0.91 - - 0.91 + + 0.91 - - 1.00 + + 1.00 diff --git a/python/tests/data/svg/ts_plain.svg b/python/tests/data/svg/ts_plain.svg new file mode 100644 index 0000000000..c070a26944 --- /dev/null +++ b/python/tests/data/svg/ts_plain.svg @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + 0 + + + + + 1 + + + + 4 + + + 2 + + + + + + + 2 + + + + + 3 + + + + 5 + + + + 9 + + + 1 + + + + 0 + + + + + + + + + + + + 0 + + + + + 1 + + + + 4 + + + + + + 2 + + + + + 3 + + + + 5 + + + + 7 + + + + + + + + + + + 0 + + + + + 1 + + + + 4 + + + + + + 2 + + + + + 3 + + + + 5 + + + + 6 + + + + + + + + + + + 0 + + + + + 1 + + + + 4 + + + + + + 2 + + + + + 3 + + + + 5 + + + + 7 + + + + + + + + + + + 0 + + + + + 1 + + + + 4 + + + + + + 2 + + + + + 3 + + + + 5 + + + + 8 + + + + + + + + + 0.00 + + + + 0.06 + + + + 0.79 + + + + 0.91 + + + + 0.91 + + + + 1.00 + + + + diff --git a/python/tests/data/svg/ts_xlabel.svg b/python/tests/data/svg/ts_xlabel.svg new file mode 100644 index 0000000000..128697981c --- /dev/null +++ b/python/tests/data/svg/ts_xlabel.svg @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + 0 + + + + + 1 + + + + 4 + + + 2 + + + + + + + 2 + + + + + 3 + + + + 5 + + + + 9 + + + 1 + + + + 0 + + + + + + + + + + + + 0 + + + + + 1 + + + + 4 + + + + + + 2 + + + + + 3 + + + + 5 + + + + 7 + + + + + + + + + + + 0 + + + + + 1 + + + + 4 + + + + + + 2 + + + + + 3 + + + + 5 + + + + 6 + + + + + + + + + + + 0 + + + + + 1 + + + + 4 + + + + + + 2 + + + + + 3 + + + + 5 + + + + 7 + + + + + + + + + + + 0 + + + + + 1 + + + + 4 + + + + + + 2 + + + + + 3 + + + + 5 + + + + 8 + + + + + + + + + 0.00 + + + + 0.06 + + + + 0.79 + + + + 0.91 + + + + 0.91 + + + + 1.00 + + + genomic position (bp) + + + + diff --git a/python/tests/test_drawing.py b/python/tests/test_drawing.py index c867435ac1..795ae5767b 100644 --- a/python/tests/test_drawing.py +++ b/python/tests/test_drawing.py @@ -1918,6 +1918,36 @@ def test_known_svg_ts(self): expected_svg = file.read() self.assertXmlEquivalentOutputs(svg, expected_svg) + def test_known_svg_ts_plain(self): + """ + Plain style, with no background shading and a variable X axis + """ + ts = self.get_simple_ts() + svg = ts.draw_svg(x_scale="treewise") + # Prettify the SVG code for easy inspection + svg = xml.dom.minidom.parseString(svg).toprettyxml() + svg_fn = pathlib.Path(__file__).parent / "data" / "svg" / "ts_plain.svg" + self.verify_basic_svg(svg, width=200 * ts.num_trees) + with open(svg_fn, "rb") as file: + expected_svg = file.read() + self.assertXmlEquivalentOutputs(svg, expected_svg) + + def test_known_svg_ts_with_xlabel(self): + """ + Style with X axis label + """ + ts = self.get_simple_ts() + x_label = "genomic position (bp)" + svg = ts.draw_svg(x_label=x_label) + # Prettify the SVG code for easy inspection + svg = xml.dom.minidom.parseString(svg).toprettyxml() + svg_fn = pathlib.Path(__file__).parent / "data" / "svg" / "ts_xlabel.svg" + self.verify_basic_svg(svg, width=200 * ts.num_trees) + assert x_label in svg + with open(svg_fn, "rb") as file: + expected_svg = file.read() + self.assertXmlEquivalentOutputs(svg, expected_svg) + class TestRounding: def test_rnd(self): diff --git a/python/tskit/drawing.py b/python/tskit/drawing.py index f8ebde36f3..3413cb64cc 100644 --- a/python/tskit/drawing.py +++ b/python/tskit/drawing.py @@ -247,6 +247,7 @@ def __init__( order=None, force_root_branch=None, symbol_size=None, + x_label=None, ): self.ts = ts if size is None: @@ -278,11 +279,11 @@ def __init__( ) # TODO add general padding arguments following matplotlib's terminology. self.axes_x_offset = 15 - self.axes_y_offset = 10 + self.axes_y_offset = 20 if x_label is None else 34 self.treebox_x_offset = self.axes_x_offset + 5 self.treebox_y_offset = self.axes_y_offset + axis_top_pad treebox_width = size[0] - 2 * self.treebox_x_offset - treebox_height = size[1] - 2 * self.treebox_y_offset + treebox_height = size[1] - self.treebox_y_offset tree_width = treebox_width / ts.num_trees svg_trees = [ SvgTree( @@ -305,7 +306,7 @@ def __init__( ] ticks = [] # svg_x_pos of drawn trees, svg_x_pos of breakpoints, & labels - y = self.treebox_y_offset + y = 0 trees = root_group.add(dwg.g(class_="trees")) drawing_scale = float(tree_width * ts.num_trees) / ts.sequence_length tree_x = self.treebox_x_offset @@ -336,7 +337,7 @@ def __init__( axes_left = self.treebox_x_offset axes_right = self.image_size[0] - self.treebox_x_offset - y = self.image_size[1] - 2 * self.axes_y_offset + y = self.image_size[1] - self.axes_y_offset axis = root_group.add(dwg.g(class_="axis")) axis.add(dwg.line((axes_left, y), (axes_right, y))) integer_ticks = all(round(label) == label for _, _, label in ticks) @@ -377,11 +378,20 @@ def __init__( dwg, axis, x, - y + 20, + y + 18, f"{genome_coord:.{label_precision}f}", - font_size=14, + class_="x-tick-label", + text_anchor="middle", + ) + if x_label is not None: + add_text_in_group( + dwg, + axis, + (axes_left + axes_right) / 2, + y + 30, + x_label, + class_="x-label", text_anchor="middle", - font_weight="bold", ) @@ -394,7 +404,8 @@ class SvgTree: standard_style = ( ".tree-sequence .background path {fill: #808080; fill-opacity:.1}" - ".tree-sequence .axis {font-weight: bold}" + ".tree-sequence .axis {font-size: 14px}" + ".tree-sequence .x-tick-label {font-weight: bold}" ".tree-sequence .axis, .tree {font-size: 14px; text-anchor:middle}" ".tree-sequence .axis line, .edge {stroke:black; fill:none}" ".node > .sym {fill: black; stroke: none}" @@ -569,7 +580,7 @@ def assign_y_coordinates( # TODO should make this a parameter somewhere. This is padding to keep the # node labels within the treebox - label_padding = 10 + label_padding = 6 y_padding = self.treebox_y_offset + 2 * label_padding height = self.image_size[1] self.root_branch_length = 0 diff --git a/python/tskit/trees.py b/python/tskit/trees.py index 1d31943474..58e2e9050f 100644 --- a/python/tskit/trees.py +++ b/python/tskit/trees.py @@ -5276,6 +5276,7 @@ def draw_svg( style=None, order=None, force_root_branch=None, + x_label=None, **kwargs, ): """ @@ -5338,8 +5339,10 @@ def draw_svg( :param bool force_root_branch: If ``True`` plot a branch (edge) above every tree root in the tree sequence. If ``None`` (default) then only plot such root branches if any root in the tree sequence has a mutation above it. + :param str x_label: A string to display on the X axis, e.g. "Genomic position". + If ``None`` (default) do not label the X axis in this tree sequence. - :return: An SVG representation of a tree. + :return: An SVG representation of a tree sequence. :rtype: str """ draw = drawing.SvgTreeSequence( @@ -5353,6 +5356,7 @@ def draw_svg( style=style, order=order, force_root_branch=force_root_branch, + x_label=x_label, **kwargs, ) output = draw.drawing.tostring()