Skip to content

Commit 02778c4

Browse files
committed
Add tests and alter changelog
1 parent b9038a5 commit 02778c4

File tree

4 files changed

+200
-10
lines changed

4 files changed

+200
-10
lines changed

python/CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ In development
66

77
**New features**
88

9+
- Add classes to SVG drawings to allow easy adjustment and styling, and document the new
10+
draw_svg() methods. This also fixes a bug when using SVG drawings in Jupyter notebooks
11+
(:user:`hyanwong`, :pr:`555`)
12+
913
- Add an optional node traversal order in ``tskit.Tree`` that uses the minimum
1014
lexicographic order of leaf nodes visited. This ordering (``"minlex_postorder"``)
1115
adds more determinism because it constraints the order in which children of

python/requirements/CI/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ sphinx==2.4.4
2222
sphinx-argparse==0.2.5
2323
sphinx-issues==1.2.0
2424
sphinx_rtd_theme==0.4.3
25-
svgwrite==1.4
25+
svgwrite==1.4
26+
xmlunittest==0.5.0

python/requirements/development.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ sphinx==2.4.4 #Pinned as breathe v3 compatibility is rough for now.
2222
sphinx-argparse
2323
sphinx-issues
2424
sphinx_rtd_theme
25-
svgwrite
25+
svgwrite
26+
xmlunittest

python/tests/test_drawing.py

Lines changed: 192 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import xml.etree
3131

3232
import msprime
33+
from xmlunittest import XmlTestCase
3334

3435
import tests.tsutil as tsutil
3536
import tskit
@@ -44,18 +45,20 @@ def get_binary_tree(self):
4445
ts = msprime.simulate(10, random_seed=1, mutation_rate=1)
4546
return next(ts.trees())
4647

47-
def get_nonbinary_tree(self):
48+
def get_nonbinary_ts(self, sample_size=10):
4849
demographic_events = [
4950
msprime.SimpleBottleneck(time=0.1, population=0, proportion=0.5)
5051
]
51-
ts = msprime.simulate(
52-
10,
52+
return msprime.simulate(
53+
sample_size,
5354
recombination_rate=5,
5455
mutation_rate=10,
5556
demographic_events=demographic_events,
5657
random_seed=1,
5758
)
58-
for t in ts.trees():
59+
60+
def get_nonbinary_tree(self, sample_size=10):
61+
for t in self.get_nonbinary_ts(sample_size).trees():
5962
for u in t.nodes():
6063
if len(t.children(u)) > 2:
6164
return t
@@ -1134,16 +1137,44 @@ def test_max_tree_height(self):
11341137
t.draw_text(max_tree_height=bad_max_tree_height)
11351138

11361139

1137-
class TestDrawSvg(TestTreeDraw):
1140+
class TestDrawSvg(TestTreeDraw, XmlTestCase):
11381141
"""
11391142
Tests the SVG tree drawing.
11401143
"""
11411144

1142-
def verify_basic_svg(self, svg, width=200, height=200):
1145+
def verify_basic_svg(self, svg, width=200, height=200, new_routine=True):
1146+
prefix = "{http://www.w3.org/2000/svg}"
11431147
root = xml.etree.ElementTree.fromstring(svg)
1144-
self.assertEqual(root.tag, "{http://www.w3.org/2000/svg}svg")
1148+
self.assertEqual(root.tag, prefix + "svg")
11451149
self.assertEqual(width, int(root.attrib["width"]))
11461150
self.assertEqual(height, int(root.attrib["height"]))
1151+
if new_routine:
1152+
# Verify the class structure of the svg
1153+
root_group = root.find(prefix + "g")
1154+
self.assertIn("class", root_group.attrib)
1155+
self.assertRegexpMatches(
1156+
root_group.attrib["class"], r"\b(tree|tree-sequence)\b"
1157+
)
1158+
if "tree-sequence" in root_group.attrib["class"]:
1159+
trees = root_group.find(prefix + "g")
1160+
self.assertIn("class", trees.attrib)
1161+
self.assertRegexpMatches(trees.attrib["class"], r"\btrees\b")
1162+
first_tree = trees.find(prefix + "g")
1163+
self.assertIn("class", first_tree.attrib)
1164+
self.assertRegexpMatches(first_tree.attrib["class"], r"\btree\b")
1165+
else:
1166+
first_tree = root_group
1167+
# Check that we have edges, symbols, and labels groups
1168+
for group in first_tree.findall(prefix + "g"):
1169+
self.assertIn("class", group.attrib)
1170+
cls = group.attrib["class"]
1171+
self.assertRegexpMatches(cls, r"\b(edges|symbols|labels)\b")
1172+
if "symbols" in cls or "labels" in cls:
1173+
# Check that we have nodes & mutations subgroups
1174+
for subgroup in group.findall(prefix + "g"):
1175+
self.assertIn("class", subgroup.attrib)
1176+
subcls = subgroup.attrib["class"]
1177+
self.assertRegexpMatches(subcls, r"\b(nodes|mutations)\b")
11471178

11481179
def test_draw_file(self):
11491180
t = self.get_binary_tree()
@@ -1177,7 +1208,7 @@ def test_draw_file(self):
11771208
def test_draw_defaults(self):
11781209
t = self.get_binary_tree()
11791210
svg = t.draw()
1180-
self.verify_basic_svg(svg)
1211+
self.verify_basic_svg(svg, new_routine=False)
11811212
svg = t.draw_svg()
11821213
self.verify_basic_svg(svg)
11831214

@@ -1483,3 +1514,156 @@ def test_tree_height_scale(self):
14831514
for bad_scale in [0, "", "NOT A SCALE"]:
14841515
with self.assertRaises(ValueError):
14851516
ts.draw_svg(tree_height_scale=bad_scale)
1517+
1518+
def test_known_svg_tree(self):
1519+
tree = self.get_nonbinary_tree(sample_size=3)
1520+
svg = tree.draw_svg(
1521+
root_svg_attributes={"id": "AB"}, style=".edges {stroke: red}"
1522+
)
1523+
expected_svg = b"""<?xml version="1.0" encoding="utf-8" ?>
1524+
<svg baseProfile="full" height="200" id="AB" version="1.1" width="200"
1525+
xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events"
1526+
xmlns:xlink="http://www.w3.org/1999/xlink">
1527+
<defs><style type="text/css"><![CDATA[.edges {stroke: red}]]></style></defs>
1528+
<g class="tree t0">
1529+
<g class="edges" fill="none" stroke="black">
1530+
<path class="p4 c0" d="M 55.0 170.0 V 30.0 H 100.0"/>
1531+
<path class="p4 c1" d="M 100.0 170.0 V 30.0 H 100.0"/>
1532+
<path class="p4 c2" d="M 145.0 170.0 V 30.0 H 100.0"/>
1533+
</g>
1534+
<g class="symbols">
1535+
<g class="nodes">
1536+
<circle class="n4" cx="100.0" cy="30.0" r="3"/>
1537+
<circle class="n0 sample" cx="55.0" cy="170.0" r="3"/>
1538+
<circle class="n1 sample" cx="100.0" cy="170.0" r="3"/>
1539+
<circle class="n2 sample" cx="145.0" cy="170.0" r="3"/>
1540+
</g>
1541+
<g class="mutations" fill="red"/>
1542+
</g>
1543+
<g class="labels" dominant-baseline="middle" font-size="14">
1544+
<g class="nodes">
1545+
<g text-anchor="start"/>
1546+
<g text-anchor="middle">
1547+
<g transform="translate(100.0, 25.0)"><text class="n4">4</text></g>
1548+
<g transform="translate(55.0, 190.0)"><text class="n0 sample">0</text></g>
1549+
<g transform="translate(100.0, 190.0)"><text class="n1 sample">1</text></g>
1550+
<g transform="translate(145.0, 190.0)"><text class="n2 sample">2</text></g>
1551+
</g>
1552+
<g text-anchor="end"/>
1553+
</g>
1554+
<g class="mutations" font-style="italic">
1555+
<g text-anchor="start"/>
1556+
<g text-anchor="end"/>
1557+
</g>
1558+
</g>
1559+
</g>
1560+
</svg>"""
1561+
self.assertXmlEquivalentOutputs(svg, expected_svg)
1562+
1563+
def test_known_svg_ts(self):
1564+
ts = self.get_nonbinary_ts(sample_size=3)
1565+
svg = ts.draw_svg(
1566+
root_svg_attributes={"id": "AB"}, style=".edges {stroke: red}"
1567+
)
1568+
self.verify_basic_svg(svg, width=200 * ts.num_trees)
1569+
expected_svg = b"""<?xml version="1.0" encoding="utf-8" ?>
1570+
<svg baseProfile="full" id="AB" height="200" version="1.1" width="400"
1571+
xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events"
1572+
xmlns:xlink="http://www.w3.org/1999/xlink">
1573+
<defs><style type="text/css"><![CDATA[.edges {stroke: red}]]></style></defs>
1574+
<g class="tree-sequence">
1575+
<g class="trees">
1576+
<g class="tree t0" transform="translate(20 15)">
1577+
<g class="edges" fill="none" stroke="black">
1578+
<path class="p4 c0" d="M 50.0 140.0 V 30.0 H 90.0"/>
1579+
<path class="p4 c1" d="M 90.0 140.0 V 30.0 H 90.0"/>
1580+
<path class="p4 c2" d="M 130.0 140.0 V 30.0 H 90.0"/>
1581+
</g>
1582+
<g class="symbols">
1583+
<g class="nodes">
1584+
<circle class="n4" cx="90.0" cy="30.0" r="3"/>
1585+
<circle class="n0 sample" cx="50.0" cy="140.0" r="3"/>
1586+
<circle class="n1 sample" cx="90.0" cy="140.0" r="3"/>
1587+
<circle class="n2 sample" cx="130.0" cy="140.0" r="3"/>
1588+
</g>
1589+
<g class="mutations" fill="red"/>
1590+
</g>
1591+
<g class="labels" dominant-baseline="middle" font-size="14">
1592+
<g class="nodes">
1593+
<g text-anchor="start"/>
1594+
<g text-anchor="middle">
1595+
<g transform="translate(90.0, 25.0)"><text class="n4">4</text></g>
1596+
<g transform="translate(50.0, 160.0)"><text class="n0 sample">0</text></g>
1597+
<g transform="translate(90.0, 160.0)"><text class="n1 sample">1</text></g>
1598+
<g transform="translate(130.0, 160.0)"><text class="n2 sample">2</text></g>
1599+
</g>
1600+
<g text-anchor="end"/>
1601+
</g>
1602+
<g class="mutations" font-style="italic">
1603+
<g text-anchor="start"/>
1604+
<g text-anchor="end"/>
1605+
</g>
1606+
</g>
1607+
</g>
1608+
<g class="tree t1" transform="translate(200.0 15)">
1609+
<g class="edges" fill="none" stroke="black">
1610+
<path class="p4 c1" d="M 50.0 140.0 V 30.0 H 80.0"/>
1611+
<path class="p4 c3" d="M 110.0 100.38696392754377 V 30.0 H 80.0"/>
1612+
<path class="p3 c0" d="M 90.0 140.0 V 100.38696392754377 H 110.0"/>
1613+
<path class="p3 c2" d="M 130.0 140.0 V 100.38696392754377 H 110.0"/>
1614+
</g>
1615+
<g class="symbols">
1616+
<g class="nodes">
1617+
<circle class="n4" cx="80.0" cy="30.0" r="3"/>
1618+
<circle class="n1 sample" cx="50.0" cy="140.0" r="3"/>
1619+
<circle class="n3" cx="110.0" cy="100.38696392754377" r="3"/>
1620+
<circle class="n0 sample" cx="90.0" cy="140.0" r="3"/>
1621+
<circle class="n2 sample" cx="130.0" cy="140.0" r="3"/>
1622+
</g>
1623+
<g class="mutations" fill="red">
1624+
<rect class="m0 s0 n1" height="6" transform="translate(-3 -3)" width="6"
1625+
x="50.0" y="85.0"/>
1626+
</g>
1627+
</g>
1628+
<g class="labels" dominant-baseline="middle" font-size="14">
1629+
<g class="nodes">
1630+
<g text-anchor="start">
1631+
<g transform="translate(115.0, 95.38696392754377)">
1632+
<text class="n3">3</text>
1633+
</g>
1634+
</g>
1635+
<g text-anchor="middle">
1636+
<g transform="translate(80.0, 25.0)"><text class="n4">4</text></g>
1637+
<g transform="translate(50.0, 160.0)"><text class="n1 sample">1</text></g>
1638+
<g transform="translate(90.0, 160.0)"><text class="n0 sample">0</text></g>
1639+
<g transform="translate(130.0, 160.0)"><text class="n2 sample">2</text></g>
1640+
</g>
1641+
<g text-anchor="end"/>
1642+
</g>
1643+
<g class="mutations" font-style="italic">
1644+
<g text-anchor="start"/>
1645+
<g text-anchor="end">
1646+
<g transform="translate(45.0, 89.0)"><text class="m0 s0 n1">0</text></g>
1647+
</g>
1648+
</g>
1649+
</g>
1650+
</g>
1651+
</g>
1652+
<g class="axis">
1653+
<line stroke="black" x1="20" x2="380" y1="180" y2="180"/>
1654+
<line stroke="black" x1="20" x2="20" y1="175" y2="185"/>
1655+
<g transform="translate(20, 200)">
1656+
<text font-size="14" font-weight="bold" text-anchor="middle">0.00</text>
1657+
</g>
1658+
<line stroke="black" x1="200.0" x2="200.0" y1="175" y2="185"/>
1659+
<g transform="translate(200.0, 200)">
1660+
<text font-size="14" font-weight="bold" text-anchor="middle">0.16</text>
1661+
</g>
1662+
<line stroke="black" x1="380.0" x2="380.0" y1="175" y2="185"/>
1663+
<g transform="translate(380.0, 200)">
1664+
<text font-size="14" font-weight="bold" text-anchor="middle">1.00</text>
1665+
</g>
1666+
</g>
1667+
</g>
1668+
</svg>"""
1669+
self.assertXmlEquivalentOutputs(svg, expected_svg)

0 commit comments

Comments
 (0)