From e1173b4c39b5bd87edfb36aea3df0d7b06dedddf Mon Sep 17 00:00:00 2001 From: Yan Wong Date: Tue, 8 Oct 2024 22:54:07 +0100 Subject: [PATCH] Add optional title to SVG --- python/CHANGELOG.rst | 2 + python/tests/data/svg/tree_muts_all_edge.svg | 53 +-- python/tests/data/svg/ts_title.svg | 340 +++++++++++++++++++ python/tests/test_drawing.py | 20 +- python/tskit/drawing.py | 39 ++- python/tskit/trees.py | 8 + 6 files changed, 431 insertions(+), 31 deletions(-) create mode 100644 python/tests/data/svg/ts_title.svg diff --git a/python/CHANGELOG.rst b/python/CHANGELOG.rst index 9815792123..a961514d65 100644 --- a/python/CHANGELOG.rst +++ b/python/CHANGELOG.rst @@ -64,6 +64,8 @@ ``pack_untracked_polytomies`` allows large polytomies involving untracked samples to be summarised as a dotted line (:user:`hyanwong`, :issue:`3011` :pr:`3010`, :pr:`3012`) +- Added a ``title`` parameter to ``.draw_svg()`` methods (:user:`hyanwong`, :pr:`3015`) + -------------------- [0.5.8] - 2024-06-27 -------------------- diff --git a/python/tests/data/svg/tree_muts_all_edge.svg b/python/tests/data/svg/tree_muts_all_edge.svg index 2777e7115b..a45bc96e7b 100644 --- a/python/tests/data/svg/tree_muts_all_edge.svg +++ b/python/tests/data/svg/tree_muts_all_edge.svg @@ -5,7 +5,7 @@ - + @@ -57,60 +57,60 @@ - - - - + + + + 0 - - + + 1 - - - + + + 3 - - + + 4 4 - - - + + + 2 - - - - + + + + 6 - - + + 7 3 - + 5 - - - + + + 5 @@ -119,4 +119,7 @@ + + All mutations tree: background shading shown + diff --git a/python/tests/data/svg/ts_title.svg b/python/tests/data/svg/ts_title.svg new file mode 100644 index 0000000000..49956b21f3 --- /dev/null +++ b/python/tests/data/svg/ts_title.svg @@ -0,0 +1,340 @@ + + + + + + + + + + + + + + + + + Genome position + + + + + + + 0.00 + + + + + + 0.06 + + + + + + 0.79 + + + + + + 0.91 + + + + + + 0.91 + + + + + + 1.00 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + 1 + + + + 4 + + + + + + 2 + + + + + 3 + + + + + + 2 + + + 5 + + + + + + 0 + + + + + 1 + + + 9 + + + + + + + + + + + 0 + + + + + 1 + + + + + + 3 + + + + + 4 + + + 4 + + + + + + 2 + + + + + + + 6 + + + 3 + + + + 5 + + + + + + 5 + + + 7 + + + + + + + + + + + 0 + + + + + 1 + + + + 4 + + + + + + 2 + + + + + 3 + + + + 5 + + + + 6 + + + + + + + + + + + 0 + + + + + 1 + + + + 4 + + + + + + 2 + + + + + + + 7 + + + 3 + + + + 5 + + + + 7 + + + + + + + + + + + 0 + + + + + 1 + + + + 4 + + + + + + 2 + + + + + 3 + + + + 5 + + + + 8 + + + + + + + The main plot title + + diff --git a/python/tests/test_drawing.py b/python/tests/test_drawing.py index 5504c73981..8da436dab3 100644 --- a/python/tests/test_drawing.py +++ b/python/tests/test_drawing.py @@ -2560,6 +2560,13 @@ def test_bad_x_regions(self): with pytest.raises(ValueError, match="Invalid coordinates"): ts.draw_svg(x_regions={(1, 0): "bad"}) + def test_title(self): + ts = msprime.sim_ancestry(1, sequence_length=100, random_seed=1) + svg = ts.draw_svg(title="This is a title") + assert "This is a title" in svg + svg = ts.first().draw_svg(title="This is another title") + assert "This is another title" in svg + def test_bad_ts_order(self): ts = msprime.sim_ancestry(1, sequence_length=100, random_seed=1) with pytest.raises(ValueError, match="Unknown display order"): @@ -2684,7 +2691,11 @@ def test_known_svg_tree_mut_all_edge(self, overwrite_viz, draw_plotbox): tree = self.get_simple_ts().at_index(1) size = (300, 400) svg = tree.draw_svg( - size=size, debug_box=draw_plotbox, all_edge_mutations=True, x_axis=True + size=size, + debug_box=draw_plotbox, + all_edge_mutations=True, + x_axis=True, + title="All mutations tree: background shading shown", ) self.verify_known_svg( svg, "tree_muts_all_edge.svg", overwrite_viz, width=size[0], height=size[1] @@ -2706,6 +2717,13 @@ def test_known_svg_ts(self, overwrite_viz, draw_plotbox): assert svg_no_css.count('class="mut ') == ts.num_mutations * 2 self.verify_known_svg(svg, "ts.svg", overwrite_viz, width=200 * ts.num_trees) + def test_known_svg_ts_title(self, overwrite_viz, draw_plotbox): + ts = self.get_simple_ts() + svg = ts.draw_svg(title="The main plot title", debug_box=draw_plotbox) + self.verify_known_svg( + svg, "ts_title.svg", overwrite_viz, width=200 * ts.num_trees + ) + def test_known_svg_ts_no_axes(self, overwrite_viz, draw_plotbox): ts = self.get_simple_ts() svg = ts.draw_svg(x_axis=False, debug_box=draw_plotbox) diff --git a/python/tskit/drawing.py b/python/tskit/drawing.py index dc5ff716b7..a1c0e5cfee 100644 --- a/python/tskit/drawing.py +++ b/python/tskit/drawing.py @@ -1059,14 +1059,14 @@ def shade_background( # For tree sequences, we need to add on the background shaded regions self.root_groups["background"] = self.dwg_base.add(dwg.g(class_="background")) - y = self.image_size[1] - self.x_axis_offset + y = self.image_size[1] - self.x_axis_offset - self.plotbox.top for i in range(1, len(breaks)): break_x = plot_breaks[i] prev_break_x = plot_breaks[i - 1] tree_x = i * tree_width + self.plotbox.left prev_tree_x = (i - 1) * tree_width + self.plotbox.left # Shift diagonal lines between tree & axis into the treebox a little - diag_height = y - (self.image_size[1] - bottom_padding) + diag_height = y - (self.image_size[1] - bottom_padding) + self.plotbox.top self.root_groups["background"].add( # NB: the path below draws straight diagonal lines between the tree boxes # and the X axis. An alternative implementation using bezier curves could @@ -1074,10 +1074,11 @@ def shade_background( # "l0,{box_h:g} c0,{diag_h} {rdiag_x},0 {rdiag_x},{diag_h} " # "c0,-{diag_h} {ldiag_x},0 {ldiag_x},-{diag_h} l0,-{box_h:g}z" dwg.path( - "M{start_x:g},0 l{box_w:g},0 " # Top left to top right of tree box + "M{start_x:g},{top:g} l{box_w:g},0 " # Top left to top right of tree "l0,{box_h:g} l{rdiag_x:g},{diag_h:g} " # Down to axis "l0,{tick_h:g} l{ax_x:g},0 l0,-{tick_h:g} " # Between axis ticks "l{ldiag_x:g},-{diag_h:g} l0,-{box_h:g}z".format( # Up from axis + top=rnd(self.plotbox.top), start_x=rnd(prev_tree_x), box_w=rnd(tree_x - prev_tree_x), box_h=rnd(y - diag_height), @@ -1136,6 +1137,7 @@ def __init__( tree_height_scale=None, max_tree_height=None, max_num_trees=None, + title=None, **kwargs, ): if max_time is None and max_tree_height is not None: @@ -1196,7 +1198,9 @@ def __init__( ) # TODO add general padding arguments following matplotlib's terminology. - self.set_spacing(top=0, left=20, bottom=10, right=20) + self.set_spacing( + top=0 if title is None else self.line_height, left=20, bottom=10, right=20 + ) subplot_size = (self.plotbox.width / num_plotboxes, self.plotbox.height) subplots = [] for tree, use, summary in zip(ts.trees(), use_tree, use_skipped): @@ -1233,6 +1237,15 @@ def __init__( ) ) y = self.plotbox.top + if title is not None: + self.add_text_in_group( + title, + self.drawing, + pos=(self.plotbox.max_x / 2, 0), + dominant_baseline="hanging", + group_class="title", + text_anchor="middle", + ) self.tree_plotbox = subplots[0].plotbox tree_is_used, breaks, skipbreaks = self.find_used_trees() self.draw_x_axis( @@ -1423,6 +1436,7 @@ def __init__( y_axis=None, x_label=None, y_label=None, + title=None, x_regions=None, y_ticks=None, y_gridlines=None, @@ -1605,7 +1619,22 @@ def __init__( self.mutation_label_attrs[m].update(mutation_label_attrs[m]) add_class(self.mutation_label_attrs[m], "lab") - self.set_spacing(top=10, left=20, bottom=15, right=20) + self.set_spacing( + top=10 if title is None else 10 + self.line_height, + left=20, + bottom=15, + right=20, + ) + if title is not None: + self.add_text_in_group( + title, + self.drawing, + pos=(self.plotbox.max_x / 2, 0), + dominant_baseline="hanging", + group_class="title", + text_anchor="middle", + ) + self.assign_x_coordinates() self.assign_y_coordinates(max_time, min_time, force_root_branch) tick_length_lower = self.default_tick_length # TODO - parameterize diff --git a/python/tskit/trees.py b/python/tskit/trees.py index c28c590e6b..fee125caa7 100644 --- a/python/tskit/trees.py +++ b/python/tskit/trees.py @@ -1799,6 +1799,7 @@ def draw_svg( size=None, time_scale=None, tree_height_scale=None, + title=None, max_time=None, min_time=None, max_tree_height=None, @@ -1845,6 +1846,8 @@ def draw_svg( heights are spaced equally according to their ranked times. :param str tree_height_scale: Deprecated alias for time_scale. (Deprecated in 0.3.6) + :param str title: A title string to be included in the SVG output. If ``None`` + (default) no title is shown, which gives more vertical space for the tree. :param str,float max_time: The maximum plotted time value in the current scaling system (see ``time_scale``). Can be either a string or a numeric value. If equal to ``"tree"`` (the default), the maximum time @@ -1940,6 +1943,7 @@ def draw_svg( size, time_scale=time_scale, tree_height_scale=tree_height_scale, + title=title, max_time=max_time, min_time=min_time, max_tree_height=max_tree_height, @@ -7181,6 +7185,7 @@ def draw_svg( x_scale=None, time_scale=None, tree_height_scale=None, + title=None, node_labels=None, mutation_labels=None, node_titles=None, @@ -7232,6 +7237,8 @@ def draw_svg( ``"rank"``, node heights are spaced equally according to their ranked times. :param str tree_height_scale: Deprecated alias for time_scale. (Deprecated in 0.3.6) + :param str title: A title string to be included in the SVG output. If ``None`` + (default) no title is shown, which gives more vertical space for the tree. :param node_labels: If specified, show custom labels for the nodes (specified by ID) that are present in this map; any nodes not present will not have a label. @@ -7326,6 +7333,7 @@ def draw_svg( x_scale=x_scale, time_scale=time_scale, tree_height_scale=tree_height_scale, + title=title, node_labels=node_labels, mutation_labels=mutation_labels, node_titles=node_titles,