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