diff --git a/python/CHANGELOG.rst b/python/CHANGELOG.rst index 9fc257c9d9..a6f5986e62 100644 --- a/python/CHANGELOG.rst +++ b/python/CHANGELOG.rst @@ -10,6 +10,9 @@ - ``draw_svg()`` methods now associate tree branches with edge IDs (:user:`hyanwong`, :pr:`3193`, :issue:`557`) +- ``draw_svg()`` methods now allow the y-axis to be placed on the right-hand side + using ``y_axis="right"`` (:user:`hyanwong`, :pr:`3201`) + **Bugfixes** - Fix bug in ``TreeSequence.pair_coalescence_counts`` when ``span_normalise=True`` diff --git a/python/tests/data/svg/tree_timed_muts.svg b/python/tests/data/svg/tree_timed_muts.svg index c13440881a..0a75b51bcb 100644 --- a/python/tests/data/svg/tree_timed_muts.svg +++ b/python/tests/data/svg/tree_timed_muts.svg @@ -4,35 +4,69 @@ + + + + Time ago + + + + + + + 0.00 + + + + + + 0.11 + + + + + + 1.11 + + + + + + 9.08 + + + + + - - - - + + + + 0 - - + + 1 - + 4 - - - + + + 2 - - + + 3 - + diff --git a/python/tests/data/svg/ts_mut_times.svg b/python/tests/data/svg/ts_mut_times.svg index 6e1f097a84..89e47eb69a 100644 --- a/python/tests/data/svg/ts_mut_times.svg +++ b/python/tests/data/svg/ts_mut_times.svg @@ -5,18 +5,18 @@ - - - - - + + + + + - + Genome position - + @@ -24,38 +24,38 @@ 0.00 - + 0.06 - + 0.79 - + 0.91 - + 0.91 - + 1.00 - + @@ -67,7 +67,7 @@ - + @@ -76,7 +76,7 @@ - + @@ -85,48 +85,98 @@ - + - + + + + Time ago + + + + + + + 0.00 + + + + + + 0.11 + + + + + + 1.11 + + + + + + 1.75 + + + + + + 5.31 + + + + + + 6.57 + + + + + + 9.08 + + + + - - - - + + + + 0 - - + + 1 - + 4 - - - + + + 2 - - + + 3 - + @@ -151,21 +201,21 @@ - + - - - - + + + + 0 - - + + 1 - + @@ -179,14 +229,14 @@ 4 - - - + + + 2 - - + + @@ -195,7 +245,7 @@ 3 - + 5 @@ -210,36 +260,36 @@ - + - - - - + + + + 0 - - + + 1 - + 4 - - - + + + 2 - - + + 3 - + 5 @@ -249,32 +299,32 @@ - + - - - - + + + + 0 - - + + 1 - + 4 - - - + + + 2 - - + + @@ -283,7 +333,7 @@ 3 - + 5 @@ -293,36 +343,36 @@ - + - - - - + + + + 0 - - + + 1 - + 4 - - - + + + 2 - - + + 3 - + 5 diff --git a/python/tests/test_drawing.py b/python/tests/test_drawing.py index 3be8ab4823..ce13a64c80 100644 --- a/python/tests/test_drawing.py +++ b/python/tests/test_drawing.py @@ -1578,7 +1578,7 @@ def test_draw_defaults(self): svg = t.draw_svg() self.verify_basic_svg(svg) - @pytest.mark.parametrize("y_axis", (True, False)) + @pytest.mark.parametrize("y_axis", ("left", "right", True, False)) @pytest.mark.parametrize("y_label", (True, False)) @pytest.mark.parametrize( "time_scale", @@ -1770,6 +1770,12 @@ def test_one_mutation_label_colour(self): self.verify_basic_svg(svg) assert svg.count(f'stroke="{colour}"') == 1 + def test_bad_y_axis(self): + t = self.get_binary_tree() + for bad_axis in ["te", "asdf", "", [], b"23"]: + with pytest.raises(ValueError): + t.draw_svg(y_axis=bad_axis) + def test_bad_time_scale(self): t = self.get_binary_tree() for bad_scale in ["te", "asdf", "", [], b"23"]: @@ -2750,7 +2756,8 @@ def test_known_svg_tree_mut_all_edge(self, overwrite_viz, draw_plotbox): def test_known_svg_tree_timed_root_mut(self, overwrite_viz, draw_plotbox): tree = self.get_simple_ts(use_mutation_times=True).at_index(0) - svg = tree.draw_svg(debug_box=draw_plotbox) + # Also look at y_axis=right + svg = tree.draw_svg(debug_box=draw_plotbox, y_axis="right") self.verify_known_svg(svg, "tree_timed_muts.svg", overwrite_viz) def test_known_svg_ts(self, overwrite_viz, draw_plotbox): @@ -2921,7 +2928,8 @@ def test_known_svg_ts_y_axis_log(self, overwrite_viz, draw_plotbox): def test_known_svg_ts_mutation_times(self, overwrite_viz, draw_plotbox): ts = self.get_simple_ts(use_mutation_times=True) - svg = ts.draw_svg(debug_box=draw_plotbox) + # also look at y_axis="right" + svg = ts.draw_svg(debug_box=draw_plotbox, y_axis="right") assert svg.count('class="site ') == ts.num_sites assert svg.count('class="mut ') == ts.num_mutations * 2 self.verify_known_svg( diff --git a/python/tskit/drawing.py b/python/tskit/drawing.py index 6a093e4076..64e3aaef1b 100644 --- a/python/tskit/drawing.py +++ b/python/tskit/drawing.py @@ -412,6 +412,19 @@ def check_x_lim(x_lim, max_x): return x_lim +def check_y_axis(y_axis): + """ + Checks the specified y_axis is valid and sets default if None. + """ + if y_axis is None: + y_axis = False + if y_axis is True: + y_axis = "left" + if y_axis not in ["left", "right", False]: + raise ValueError(f"Unknown y_axis specification: '{y_axis}'.") + return y_axis + + def create_tick_labels(tick_values, decimal_places=2): """ If tick_values are numeric, round the labels to X decimal_places, but do not print @@ -1019,7 +1032,7 @@ def __init__( dwg.defs.add(dwg.style(style)) self.debug_box = debug_box self.time_scale = check_time_scale(time_scale) - self.y_axis = y_axis + self.y_axis = check_y_axis(y_axis) self.x_axis = x_axis if x_label is None and x_axis: x_label = "Genome position" @@ -1049,8 +1062,12 @@ def set_spacing(self, top=0, left=0, bottom=0, right=0): self.y_axis_offset += self.line_height if self.x_axis: bottom += self.x_axis_offset - if self.y_axis: - left = self.y_axis_offset # Override user-provided, so y-axis is at x=0 + if self.y_axis == "left": + left = ( + self.y_axis_offset + ) # Override user-provided values, so y-axis is at x=0 + if self.y_axis == "right": + right = self.y_axis_offset self.plotbox.set_padding(top, left, bottom, right) if self.debug_box: self.root_groups["debug"] = self.dwg_base.add( @@ -1193,8 +1210,9 @@ def draw_y_axis( ticks, # A dict of pos->label upper=None, # In plot coords lower=None, # In plot coords - tick_length_left=default_tick_length, + tick_length_outer=default_tick_length, # Positive means towards the outside gridlines=None, + side="left", # 'left' or 'right', where the axis is drawn ): if not self.y_axis and not self.y_label: return @@ -1203,18 +1221,31 @@ def draw_y_axis( if lower is None: lower = self.plotbox.bottom dwg = self.drawing - x = rnd(self.y_axis_offset) + if side == "left": + x = rnd(self.y_axis_offset) + width = self.plotbox.right - x + direction = -1 + text_anchor = "end" + pos = (0, (upper + lower) / 2) + transform = "translate(11) rotate(-90)" + else: + x = rnd(self.plotbox.max_x - self.y_axis_offset) + width = x - self.plotbox.left + direction = 1 + text_anchor = "start" + pos = (self.plotbox.max_x, (upper + lower) / 2) + transform = "translate(-11) rotate(90)" axes = self.get_axes() y_axis = axes.add(dwg.g(class_="y-axis")) if self.y_label: self.add_text_in_group( self.y_label, y_axis, - pos=(0, (upper + lower) / 2), + pos=pos, group_class="title", class_="lab", text_anchor="middle", - transform="translate(11) rotate(-90)", + transform=transform, ) if self.y_axis: y_axis.add(dwg.line((x, rnd(lower)), (x, rnd(upper)), class_="ax-line")) @@ -1228,19 +1259,15 @@ def draw_y_axis( dwg.g(class_="tick", transform=f"translate({x} {rnd(y_pos)})") ) if gridlines: - tick.add( - dwg.line( - (0, 0), (rnd(self.plotbox.right - x), 0), class_="grid" - ) - ) - tick.add(dwg.line((0, 0), (rnd(-tick_length_left), 0))) + tick.add(dwg.line((0, 0), (rnd(width), 0), class_="grid")) + tick.add(dwg.line((0, 0), (rnd(direction * tick_length_outer), 0))) self.add_text_in_group( # place the origin at the left of the tickmark plus a single px space label, tick, - pos=(rnd(-tick_length_left - 1), 0), + pos=(rnd(direction * (tick_length_outer + 1)), 0), class_="lab", - text_anchor="end", + text_anchor=text_anchor, ) if len(tick_outside_axis) > 0: logging.warning( @@ -1485,8 +1512,9 @@ def __init__( ticks=check_y_ticks(y_ticks), upper=self.tree_plotbox.top, lower=y_low, - tick_length_left=self.default_tick_length, + tick_length_outer=self.default_tick_length, gridlines=y_gridlines, + side="right" if y_axis == "right" else "left", ) subplot_x = self.plotbox.left @@ -1878,8 +1906,9 @@ def __init__( self.draw_y_axis( ticks=check_y_ticks(y_ticks), lower=self.timescaling.transform(self.timescaling.min_time), - tick_length_left=self.default_tick_length, + tick_length_outer=self.default_tick_length, gridlines=y_gridlines, + side="right" if y_axis == "right" else "left", ) self.draw_tree() diff --git a/python/tskit/trees.py b/python/tskit/trees.py index 12c0355f37..7f6df93442 100644 --- a/python/tskit/trees.py +++ b/python/tskit/trees.py @@ -7552,9 +7552,11 @@ def draw_svg( draws a box, labelled with the name, on the X axis between the left and right positions, and can be used for annotating genomic regions (e.g. genes) on the X axis. If ``None`` (default) do not plot any regions. - :param bool y_axis: Should the plot have an Y axis line, showing time (or - ranked node time if ``time_scale="rank"``. If ``None`` (default) - do not plot a Y axis. + :param Union[bool, str] y_axis: Should the plot have an Y axis line, showing + time. If ``False`` do not plot a Y axis. If ``True``, plot the Y axis on + left hand side of the plot. Can also take the strings ``"left"`` or + ``"right"``, specifying the side of the plot on which to plot the Y axis. + Default: ``None``, treated as ``False``. :param str y_label: Place a label to the left of the plot. If ``None`` (default) and there is a Y axis, create and place an appropriate label. :param Union[list, dict] y_ticks: A list of Y values at which to plot