From 228ac33047ef5d056013d4447f9cca28220c4447 Mon Sep 17 00:00:00 2001 From: Yan Wong Date: Tue, 5 May 2020 12:54:17 +0100 Subject: [PATCH 1/6] Give mutations groups too --- python/tskit/drawing.py | 210 +++++++++++++++++++++++----------------- 1 file changed, 120 insertions(+), 90 deletions(-) diff --git a/python/tskit/drawing.py b/python/tskit/drawing.py index d7efed0917..f6464bec92 100644 --- a/python/tskit/drawing.py +++ b/python/tskit/drawing.py @@ -371,6 +371,9 @@ def __init__( ) +SvgGroupInfo = collections.namedtuple("SvgGroupInfo", "g edge_dxy node mutation") + + class SvgTree: """ A class to draw a tree in SVG format. @@ -381,16 +384,17 @@ class SvgTree: standard_style = ( ".axis {font-weight: bold}" ".tree, .axis {font-size: 14px; text-anchor:middle;}" - ".edge {stroke: black; fill: none}" - ".node > circle {r: 3px; fill: black; stroke: none}" ".tree text {dominant-baseline: middle}" # not inherited in css 1.1 - ".mut > text.lft {transform: translateX(0.5em); text-anchor: start}" - ".mut > text.rgt {transform: translateX(-0.5em); text-anchor: end}" - ".root > text {transform: translateY(-0.8em)}" # Root - ".leaf > text {transform: translateY(1em)}" # Leaves - ".node > text.lft {transform: translate(0.5em, -0.5em); text-anchor: start}" - ".node > text.rgt {transform: translate(-0.5em, -0.5em); text-anchor: end}" - ".mut {fill: red; font-style: italic}" + ".edge {stroke: black; fill: none}" + ".node > .edge + * {r: 3px; fill: black; stroke: none}" + ".mut > text.rgt {transform: translateX(0.5em); text-anchor: start}" + ".mut > text.lft {transform: translateX(-0.5em); text-anchor: end}" + ".node > text {transform: translateY(-0.8em)}" # Root + ".node.leaf > text {transform: translateY(1em)}" # Leaves + ".node > text.rgt {transform: translate(0.5em, -0.5em); text-anchor: start}" + ".node > text.lft {transform: translate(-0.5em, -0.5em); text-anchor: end}" + ".mut > text {fill: red; font-style: italic}" + ".mut > .edge + * {fill: red;}" ) @staticmethod @@ -448,6 +452,7 @@ def __init__( self.edge_attrs[u] = {} if edge_attrs is not None and u in edge_attrs: self.edge_attrs[u].update(edge_attrs[u]) + self.add_class(self.edge_attrs[u], "edge") self.node_attrs[u] = {} if node_attrs is not None and u in node_attrs: self.node_attrs[u].update(node_attrs[u]) @@ -536,13 +541,14 @@ def assign_y_coordinates(self, tree_height_scale, max_tree_height): # node labels within the treebox label_padding = 10 y_padding = self.treebox_y_offset + 2 * label_padding - mutations_over_root = any( + # TODO - in the case of max_tree_height == "ts" we need to look at all trees + self.mutations_over_root = any( any(tree.parent(mut.node) == NULL for mut in tree.mutations()) for tree in ts.trees() ) root_branch_length = 0 height = self.image_size[1] - if mutations_over_root: + if self.mutations_over_root: # Allocate a fixed about of space to show the mutations on the # 'root branch' root_branch_length = height / 10 # FIXME just draw branch?? @@ -577,35 +583,73 @@ def assign_x_coordinates(self, tree, x_start, width): node_x_coord_map[u] = a + (b - a) / 2 return node_x_coord_map - def info_classes(self, focal_node): + def add_node(self, curr_svg_group, focal, dx, dy): """ - For a focal node id, return a set of classes that encode this useful information: - "nA": where A == focal node id - "pB" or "root": where B == parent id (or "root" if the focal node is a root) - "sample": a class present if the focal node is a sample - "leaf": a class present if the focal node is a leaf - "mC": where C == mutation id of all mutations above this focal node - "sD": where D == site id of the sites associated with all mutations - above this focal node + Return a list of SvgGroupInfo objects to add to the stack """ - # Add a new group for each node, and give it classes for css targetting - classes = set() - classes.add(f"node n{focal_node}") - v = self.tree.parent(focal_node) + ret = [] + dwg = self.drawing + grp = curr_svg_group + v = self.tree.parent(focal) + classes = {f"n{focal}"} + offset_x = dx + offset_y = dy if v == NULL: classes.add(f"root") + if self.mutations_over_root: + # FIXME this is pretty crappy for spacing mutations over a root. + root_branch_len = 20 + else: + root_branch_len = 0 + edge_x = 0 + edge_height = root_branch_len / (len(self.node_mutations[focal]) + 1) + offset_y = dy - root_branch_len + edge_height else: classes.add(f"p{v}") - if self.tree.is_sample(focal_node): + edge_x = offset_x + edge_height = dy / (len(self.node_mutations[focal]) + 1) + offset_y = edge_height + + # Add mutation symbols + labels + for m in reversed(self.node_mutations[focal]): + mutation_classes = {"mut", f"m{m.id}", f"s{m.site}"} + grp = grp.add( + dwg.g( + class_=" ".join(classes | mutation_classes), + transform=f"translate({offset_x} {offset_y})", + ) + ) + ret.append( + SvgGroupInfo( + g=grp, edge_dxy=(edge_x, edge_height), node=focal, mutation=m.id + ) + ) + # after the first sideways move, all further movements go downwards + offset_x = edge_x = 0 + offset_y = edge_height + + # Add a new group for each node, and give it these classes for css targetting: + # "node" + # "nA": where A == focal node id + # "pB" or "root": where B == parent id (or "root" if the focal node is a root) + # "sample": a class present if the focal node is a sample + # "leaf": a class present if the focal node is a leaf + classes.add(f"node") + if self.tree.is_sample(focal): classes.add("sample") - if self.tree.is_leaf(focal_node): + if self.tree.is_leaf(focal): classes.add("leaf") - for mutation in self.node_mutations[focal_node]: - # Adding mutations and sites above this node allows identification - # of the tree under any specific mutation - classes.add(f"m{mutation.id}") - classes.add(f"s{mutation.site}") - return sorted(classes) + grp = grp.add( + dwg.g( + class_=" ".join(classes), transform=f"translate({offset_x} {offset_y})" + ) + ) + ret.append( + SvgGroupInfo( + g=grp, edge_dxy=(edge_x, edge_height), node=focal, mutation=None + ) + ) + return ret def draw(self): dwg = self.drawing @@ -617,73 +661,59 @@ def draw(self): # Iterate over nodes, adding groups to reflect the tree heirarchy stack = [] for u in tree.roots: - grp = dwg.g( - class_=" ".join(self.info_classes(u)), - transform=f"translate({rnd(node_x_coord_map[u])} " - f"{rnd(node_y_coord_map[u])})", + stack.extend( + self.add_node( + self.root_group, u, node_x_coord_map[u], node_y_coord_map[u], + ) ) - stack.append((u, self.root_group.add(grp))) while len(stack) > 0: - u, curr_svg_group = stack.pop() - pu = node_x_coord_map[u], node_y_coord_map[u] - for focal in tree.children(u): - fx = node_x_coord_map[focal] - pu[0] - fy = node_y_coord_map[focal] - pu[1] - new_svg_group = curr_svg_group.add( - dwg.g( - class_=" ".join(self.info_classes(focal)), - transform=f"translate({rnd(fx)} {rnd(fy)})", - ) - ) - stack.append((focal, new_svg_group)) - - o = (0, 0) - v = tree.parent(u) + curr = stack.pop() + u = curr.node + if curr.mutation is None: + # This is a tree node, not a mutation group + for c in tree.children(u): + dx = node_x_coord_map[c] - node_x_coord_map[u] + dy = node_y_coord_map[c] - node_y_coord_map[u] + stack.extend(self.add_node(curr.g, c, dx, dy)) # Add edge first => below - if v != NULL: - self.add_class(self.edge_attrs[u], "edge") - pv = node_x_coord_map[v], node_y_coord_map[v] - dx = pv[0] - pu[0] - dy = pv[1] - pu[1] + dx, dy = curr.edge_dxy + if dx == dy == 0: + path = dwg.path([("M", o)], **self.edge_attrs[u]) # e.g. at root + elif dx == 0: + path = dwg.path([("M", o), ("V", -dy)], **self.edge_attrs[u]) + else: path = dwg.path( - [("M", o), ("V", rnd(dy)), ("H", rnd(dx))], **self.edge_attrs[u] + [("M", o), ("V", -dy), ("H", -dx)], **self.edge_attrs[u] ) - curr_svg_group.add(path) # Edges in parent group, so + curr.g.add(path) + + if curr.mutation is None: + # Node symbol + curr.g.add(dwg.circle(**self.node_attrs[u])) + # Labels + if not tree.is_leaf(u): + if tree.parent(u) == NULL: + if self.mutations_over_root: + self.add_class(self.node_label_attrs[u], "rgt") + else: + if u == left_child[tree.parent(u)]: + self.add_class(self.node_label_attrs[u], "lft") + else: + self.add_class(self.node_label_attrs[u], "rgt") + curr.g.add(dwg.text(**self.node_label_attrs[u])) else: - # FIXME this is pretty crappy for spacing mutations over a root. - pv = (pu[0], pu[1] - 20) - - # Add node symbol + label next (visually above the edge subtending this node) - # Symbols - curr_svg_group.add(dwg.circle(**self.node_attrs[u])) - # Labels - if not tree.is_leaf(u) and tree.parent(u) != NULL: - if u == left_child[tree.parent(u)]: - self.add_class(self.node_label_attrs[u], "rgt") - else: - self.add_class(self.node_label_attrs[u], "lft") - curr_svg_group.add(dwg.text(**self.node_label_attrs[u])) - - # Add mutation symbols + labels - delta = (pv[1] - pu[1]) / (len(self.node_mutations[u]) + 1) - for i, mutation in enumerate(reversed(self.node_mutations[u])): - # TODO get rid of these manual positioning tweaks and add them - # as offsets the user can access via a transform or something. - dy = (i + 1) * delta - mutation_class = f"mut m{mutation.id} s{mutation.site}" - mut_group = curr_svg_group.add( - dwg.g(class_=mutation_class, transform=f"translate(0 {rnd(dy)})") - ) - # Symbols - mut_group.add(dwg.rect(insert=o, **self.mutation_attrs[mutation.id])) + # Mutation symbol + curr.g.add(dwg.rect(insert=o, **self.mutation_attrs[curr.mutation])) # Labels - if mutation.node == left_child[tree.parent(mutation.node)]: - mut_label_class = "rgt" - else: + if u == left_child[tree.parent(u)]:: mut_label_class = "lft" - self.add_class(self.mutation_label_attrs[mutation.id], mut_label_class) - mut_group.add(dwg.text(**self.mutation_label_attrs[mutation.id])) + else: + mut_label_class = "rgt" + self.add_class( + self.mutation_label_attrs[curr.mutation], mut_label_class + ) + curr.g.add(dwg.text(**self.mutation_label_attrs[curr.mutation])) class TextTreeSequence: From 50e468bc0e4ea84d242f2ccff9e9631bc58d19c2 Mon Sep 17 00:00:00 2001 From: Yan Wong Date: Wed, 6 May 2020 16:58:31 +0100 Subject: [PATCH 2/6] account for different root heights --- python/tests/data/svg/mut_tree.svg | 61 ++++++++++++++++++++++++++++++ python/tests/data/svg/tree.svg | 47 +++++++++++------------ python/tests/test_drawing.py | 46 ++++++++++++++++++---- python/tskit/drawing.py | 55 +++++++++++++++++---------- 4 files changed, 157 insertions(+), 52 deletions(-) create mode 100644 python/tests/data/svg/mut_tree.svg diff --git a/python/tests/data/svg/mut_tree.svg b/python/tests/data/svg/mut_tree.svg new file mode 100644 index 0000000000..85e1fc1c41 --- /dev/null +++ b/python/tests/data/svg/mut_tree.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + 0 + + + + + 1 + + + + 4 + + + + 2 + + + + + + 2 + + + + + 3 + + + + 5 + + + + 9 + + + + 0 + + + + 1 + + + diff --git a/python/tests/data/svg/tree.svg b/python/tests/data/svg/tree.svg index 81feb6f487..22beaa89f4 100644 --- a/python/tests/data/svg/tree.svg +++ b/python/tests/data/svg/tree.svg @@ -1,47 +1,46 @@ - + - - + + - - - - - + + + + + 0 - - + + 1 - + - 4 - - - 0 - + 4 - - - + + + 2 - - + + 3 - + - 5 + 5 + - 9 + 7 diff --git a/python/tests/test_drawing.py b/python/tests/test_drawing.py index 58f7a1483a..2e7b57d97b 100644 --- a/python/tests/test_drawing.py +++ b/python/tests/test_drawing.py @@ -188,13 +188,16 @@ def get_simple_ts(self): sites = io.StringIO( """\ position ancestral_state - 0.01 A + 0.05 A + 0.06 0 """ ) mutations = io.StringIO( """\ site node derived_state parent - 0 4 T -1 + 0 9 T -1 + 0 9 G 0 + 0 4 1 -1 """ ) return tskit.load_text( @@ -1744,8 +1747,8 @@ def test_max_tree_height(self): svg1 = ts.at_index(0).draw(max_tree_height="ts") svg2 = ts.at_index(1).draw(max_tree_height="ts") - # when scaled, node 3 should be at the *same* height in both trees, so the label - # should be the same + # when scaled, node 3 should be at the *same* height in both trees, so the edge + # definition should be the same self.verify_basic_svg(svg1) self.verify_basic_svg(svg2) str_pos = svg1.find(">0<") @@ -1808,8 +1811,27 @@ def test_bad_x_scale(self): with self.assertRaises(ValueError): ts.draw_svg(x_scale=bad_x_scale) - def test_known_svg_tree(self): - tree = self.get_simple_ts().first() + def test_tree_root_branch(self): + # in the simple_ts, there are root mutations in the first tree but not the second + ts = self.get_simple_ts() + tree_with_root_mutations = ts.at_index(0) + root1 = tree_with_root_mutations.root + tree_without_root_mutations = ts.at_index(1) + root2 = tree_without_root_mutations.root + svg1 = tree_with_root_mutations.draw_svg() + svg2 = tree_without_root_mutations.draw_svg() + self.verify_basic_svg(svg1) + self.verify_basic_svg(svg2) + edge_str = ' Date: Thu, 7 May 2020 13:09:47 +0100 Subject: [PATCH 3/6] Add classes to the symbols and labels --- python/tskit/drawing.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/python/tskit/drawing.py b/python/tskit/drawing.py index 3428670d74..cb981705ec 100644 --- a/python/tskit/drawing.py +++ b/python/tskit/drawing.py @@ -473,12 +473,14 @@ def __init__( self.node_attrs[u] = {} if node_attrs is not None and u in node_attrs: self.node_attrs[u].update(node_attrs[u]) + self.add_class(self.node_attrs[u], "sym") # class 'sym' for symbol label = "" if node_labels is None: label = str(u) elif u in node_labels: label = str(node_labels[u]) self.node_label_attrs[u] = {"text": label} + self.add_class(self.node_label_attrs[u], "lab") # class 'lab' for label if node_label_attrs is not None and u in node_label_attrs: self.node_label_attrs[u].update(node_label_attrs[u]) @@ -494,6 +496,7 @@ def __init__( } if mutation_attrs is not None and m in mutation_attrs: self.mutation_attrs[m].update(mutation_attrs[m]) + self.add_class(self.mutation_attrs[m], "sym") # class 'sym' for symbol label = "" if mutation_labels is None: label = str(m) @@ -502,6 +505,7 @@ def __init__( self.mutation_label_attrs[m] = {"text": label} if mutation_label_attrs is not None and m in mutation_label_attrs: self.mutation_label_attrs[m].update(mutation_label_attrs[m]) + self.add_class(self.mutation_label_attrs[m], "lab") self.draw() @@ -622,7 +626,12 @@ def add_node(self, curr_svg_group, focal, dx, dy): edge_height = dy / (len(self.node_mutations[focal]) + 1) offset_y = edge_height - # Add mutation symbols + labels + # Add mut group for each mutation, and give it these classes for css targetting: + # "mut" + # "nA": where A == focal node id + # "pB" or "root": where B == parent id (or "root" if the focal node is a root) + # "mX": where X == mutation id + # "sY": where Y == site id for m in reversed(self.node_mutations[focal]): mutation_classes = ["mut", f"m{m.id}", f"s{m.site}"] grp = grp.add( @@ -636,7 +645,7 @@ def add_node(self, curr_svg_group, focal, dx, dy): g=grp, edge_dxy=(edge_x, edge_height), node=focal, mutation=m.id ) ) - # after the first sideways move, all further movements go downwards + # after the first sideways line of an edge all further movements go downwards offset_x = edge_x = 0 offset_y = edge_height From ebf67e9d9f074aa3ce9b8cb91995d0ad5fa41d97 Mon Sep 17 00:00:00 2001 From: Yan Wong Date: Sun, 10 May 2020 16:38:57 +0100 Subject: [PATCH 4/6] Add individual and population And group each tree in a transformed box, for easier later transformation. --- python/tests/data/svg/mut_tree.svg | 82 +++---- python/tests/data/svg/tree.svg | 56 ++--- python/tests/data/svg/ts.svg | 372 ++++++++++++++++------------- python/tests/test_drawing.py | 12 +- python/tskit/drawing.py | 62 +++-- 5 files changed, 315 insertions(+), 269 deletions(-) diff --git a/python/tests/data/svg/mut_tree.svg b/python/tests/data/svg/mut_tree.svg index 85e1fc1c41..670c4c0103 100644 --- a/python/tests/data/svg/mut_tree.svg +++ b/python/tests/data/svg/mut_tree.svg @@ -2,60 +2,60 @@ +.axis {font-weight: bold}.tree, .axis {font-size: 14px; text-anchor:middle;}.tree .lab {dominant-baseline: middle}.edge {stroke: black; fill: none}.node > .sym {r: 3px; fill: black; stroke: none}.node > .lab {transform: translateY(-0.8em)}.node.leaf > .lab {transform: translateY(1em)}.tree .lab.rgt {text-anchor: start}.tree .lab.lft {text-anchor: end}.mut > .lab.rgt {transform: translateX(0.5em);}.mut > .lab.lft {transform: translateX(-0.5em);}.node > .lab.rgt {transform: translate(0.35em, -0.5em);}.node > .lab.lft {transform: translate(-0.35em, -0.5em);}.mut > .lab {fill: red; font-style: italic}.mut > .sym {fill: red;} - - - - - - - - - 0 + + + + + + + + + 0 - - - - 1 + + + + 1 - - - 4 + + + 4 - - - 2 + + + 2 - - - - - 2 + + + + + 2 - - - - 3 + + + + 3 - - - 5 + + + 5 - - - 9 + + + 9 - - - 0 + + + 0 - - - 1 + + + 1 diff --git a/python/tests/data/svg/tree.svg b/python/tests/data/svg/tree.svg index 22beaa89f4..34e8feed99 100644 --- a/python/tests/data/svg/tree.svg +++ b/python/tests/data/svg/tree.svg @@ -2,45 +2,45 @@ +.axis {font-weight: bold}.tree, .axis {font-size: 14px; text-anchor:middle;}.tree .lab {dominant-baseline: middle}.edge {stroke: black; fill: none}.node > .sym {r: 3px; fill: black; stroke: none}.node > .lab {transform: translateY(-0.8em)}.node.leaf > .lab {transform: translateY(1em)}.tree .lab.rgt {text-anchor: start}.tree .lab.lft {text-anchor: end}.mut > .lab.rgt {transform: translateX(0.5em);}.mut > .lab.lft {transform: translateX(-0.5em);}.node > .lab.rgt {transform: translate(0.35em, -0.5em);}.node > .lab.lft {transform: translate(-0.35em, -0.5em);}.mut > .lab {fill: red; font-style: italic}.mut > .sym {fill: red;} - - - - - - 0 + + + + + + 0 - - - - 1 + + + + 1 - - - 4 + + + 4 - - - - - 2 + + + + + 2 - - - - 3 + + + + 3 - - - 5 + + + 5 - - 7 + + 7 diff --git a/python/tests/data/svg/ts.svg b/python/tests/data/svg/ts.svg index 6e68a58407..4aaa661453 100644 --- a/python/tests/data/svg/ts.svg +++ b/python/tests/data/svg/ts.svg @@ -1,8 +1,10 @@ - + - - + + @@ -11,188 +13,214 @@ - - - - - - - 0 - - - - - 1 - - - - 4 - - - 0 - + + + + + + + + + + + 0 + + + + + 1 + + + + 4 + + + + 2 + + + + + + 2 + + + + + 3 + + + + 5 + + + + 9 + + + + 0 + + + + 1 - - - - - 2 - - - - - 3 - - - - 5 - - - 9 - - - - - - - 0 - - - - - 1 - - - - 4 - - - - - - 2 - - - - - 3 - - - - 5 + + + + + + + + 0 + + + + + 1 + + + + 4 + + + + + + 2 + + + + + 3 + + + + 5 + + + + 7 - - 7 - - - - - - - 0 - - - - - 1 - - - - 4 + + + + + + + + 0 + + + + + 1 + + + + 4 + + + + + + 2 + + + + + 3 + + + + 5 + + + + 6 - - - - - 2 - - - - - 3 - - - - 5 - - - 6 - - - - - - - 0 - - - - - 1 - - - - 4 + + + + + + + + 0 + + + + + 1 + + + + 4 + + + + + + 2 + + + + + 3 + + + + 5 + + + + 7 - - - - - 2 - - - - - 3 - - - - 5 - - - 7 - - - - - - - 0 - - - - - 1 - - - - 4 - - - - - - 2 - - - - - 3 - - - - 5 + + + + + + + + 0 + + + + + 1 + + + + 4 + + + + + + 2 + + + + + 3 + + + + 5 + + + + 8 - - 8 diff --git a/python/tests/test_drawing.py b/python/tests/test_drawing.py index 2e7b57d97b..c3de6954bf 100644 --- a/python/tests/test_drawing.py +++ b/python/tests/test_drawing.py @@ -1422,12 +1422,15 @@ def verify_basic_svg(self, svg, width=200, height=200): trees = g break self.assertIsNotNone(trees) # Must have found a trees group - first_tree = trees.find(prefix + "g") + first_treebox = trees.find(prefix + "g") + self.assertIn("class", first_treebox.attrib) + self.assertRegexpMatches(first_treebox.attrib["class"], r"\btreebox\b") + first_tree = first_treebox.find(prefix + "g") self.assertIn("class", first_tree.attrib) self.assertRegexpMatches(first_tree.attrib["class"], r"\btree\b") else: first_tree = root_group - # Check that we have edges, symbols, and labels groups + # Check that the first grouping is labelled as a root groups = first_tree.findall(prefix + "g") self.assertGreater(len(groups), 0) for group in groups: @@ -1768,13 +1771,14 @@ def test_draw_simple_ts(self): self.verify_basic_svg(svg, width=200 * ts.num_trees) def test_draw_integer_breaks_ts(self): - r_map = msprime.RecombinationMap.uniform_map(1000, 0.001, num_loci=1000) r_map = msprime.RecombinationMap.uniform_map(1000, 0.005, num_loci=1000) ts = msprime.simulate(5, recombination_map=r_map, random_seed=1) + self.assertGreater(ts.num_trees, 2) svg = ts.draw_svg() self.verify_basic_svg(svg, width=200 * ts.num_trees) + axis_pos = svg.find('class="axis"') for b in ts.breakpoints(): - self.assertNotEqual(svg.find(f">{b:.0f}<"), -1) + self.assertNotEqual(svg.find(f">{b:.0f}<", axis_pos), -1) def test_draw_even_height_ts(self): ts = msprime.simulate(5, recombination_rate=1, random_seed=1) diff --git a/python/tskit/drawing.py b/python/tskit/drawing.py index cb981705ec..538ce17f53 100644 --- a/python/tskit/drawing.py +++ b/python/tskit/drawing.py @@ -314,8 +314,13 @@ def __init__( break_x = self.treebox_x_offset for svg_tree, tree in zip(svg_trees, ts.trees()): - svg_tree.root_group["transform"] = f"translate({rnd(tree_x)} {rnd(y)})" - trees.add(svg_tree.root_group) + treebox = trees.add( + dwg.g( + class_=f"treebox t{tree.index}", + transform=f"translate({rnd(tree_x)} {rnd(y)})", + ) + ) + treebox.add(svg_tree.root_group) ticks.append((tree_x, break_x, tree.interval[0])) tree_x += tree_width break_x += tree.span * drawing_scale @@ -396,17 +401,19 @@ class SvgTree: standard_style = ( ".axis {font-weight: bold}" ".tree, .axis {font-size: 14px; text-anchor:middle;}" - ".tree text {dominant-baseline: middle}" # not inherited in css 1.1 + ".tree .lab {dominant-baseline: middle}" # not inherited in css 1.1 ".edge {stroke: black; fill: none}" - ".node > .edge + * {r: 3px; fill: black; stroke: none}" - ".mut > text.rgt {transform: translateX(0.5em); text-anchor: start}" - ".mut > text.lft {transform: translateX(-0.5em); text-anchor: end}" - ".node > text {transform: translateY(-0.8em)}" # Root - ".node.leaf > text {transform: translateY(1em)}" # Leaves - ".node > text.rgt {transform: translate(0.5em, -0.5em); text-anchor: start}" - ".node > text.lft {transform: translate(-0.5em, -0.5em); text-anchor: end}" - ".mut > text {fill: red; font-style: italic}" - ".mut > .edge + * {fill: red;}" + ".node > .sym {r: 3px; fill: black; stroke: none}" + ".node > .lab {transform: translateY(-0.8em)}" # Root + ".node.leaf > .lab {transform: translateY(1em)}" # Leaves + ".tree .lab.rgt {text-anchor: start}" + ".tree .lab.lft {text-anchor: end}" + ".mut > .lab.rgt {transform: translateX(0.5em);}" + ".mut > .lab.lft {transform: translateX(-0.5em);}" + ".node > .lab.rgt {transform: translate(0.35em, -0.5em);}" + ".node > .lab.lft {transform: translate(-0.35em, -0.5em);}" + ".mut > .lab {fill: red; font-style: italic}" + ".mut > .sym {fill: red;}" ) @staticmethod @@ -621,23 +628,23 @@ def add_node(self, curr_svg_group, focal, dx, dy): edge_height = root_branch_len / (len(self.node_mutations[focal]) + 1) offset_y = dy - root_branch_len + edge_height else: - classes.append(f"p{v}") + classes.append(f"a{v}") edge_x = offset_x edge_height = dy / (len(self.node_mutations[focal]) + 1) offset_y = edge_height # Add mut group for each mutation, and give it these classes for css targetting: # "mut" - # "nA": where A == focal node id - # "pB" or "root": where B == parent id (or "root" if the focal node is a root) - # "mX": where X == mutation id - # "sY": where Y == site id + # "a" or "root": where == id of immediate ancestor (parent) node + # "n": where == focal node id + # "m": where == mutation id + # "s": where == site id for m in reversed(self.node_mutations[focal]): mutation_classes = ["mut", f"m{m.id}", f"s{m.site}"] grp = grp.add( dwg.g( class_=" ".join(classes + mutation_classes), - transform=f"translate({offset_x} {offset_y})", + transform=f"translate({rnd(offset_x)} {rnd(offset_y)})", ) ) ret.append( @@ -652,17 +659,24 @@ def add_node(self, curr_svg_group, focal, dx, dy): # Add a new group for each node, and give it these classes for css targetting: # "node" # "nA": where A == focal node id - # "pB" or "root": where B == parent id (or "root" if the focal node is a root) + # "aB" or "root": where B == parent id (or "root" if the focal node is a root) # "sample": a class present if the focal node is a sample # "leaf": a class present if the focal node is a leaf - classes.append(f"node") + classes.append("node") + ind = self.tree.tree_sequence.node(focal).individual + pop = self.tree.tree_sequence.node(focal).population + if ind != NULL: + classes.append(f"i{ind}") + if pop != NULL: + classes.append(f"p{pop}") if self.tree.is_sample(focal): classes.append("sample") if self.tree.is_leaf(focal): classes.append("leaf") grp = grp.add( dwg.g( - class_=" ".join(classes), transform=f"translate({offset_x} {offset_y})" + class_=" ".join(classes), + transform=f"translate({rnd(offset_x)} {rnd(offset_y)})", ) ) ret.append( @@ -703,10 +717,10 @@ def draw(self): if dx == dy == 0: path = dwg.path([("M", o)], **self.edge_attrs[u]) # e.g. at root elif dx == 0: - path = dwg.path([("M", o), ("V", -dy)], **self.edge_attrs[u]) + path = dwg.path([("M", o), ("V", -rnd(dy))], **self.edge_attrs[u]) else: path = dwg.path( - [("M", o), ("V", -dy), ("H", -dx)], **self.edge_attrs[u] + [("M", o), ("V", -rnd(dy)), ("H", -rnd(dx))], **self.edge_attrs[u] ) curr.g.add(path) @@ -728,7 +742,7 @@ def draw(self): # Mutation symbol curr.g.add(dwg.rect(insert=o, **self.mutation_attrs[curr.mutation])) # Labels - if u == left_child[tree.parent(u)]:: + if u == left_child[tree.parent(u)]: mut_label_class = "lft" else: mut_label_class = "rgt" From facd7136d8e8a6759394eba2b5d42655c2b905da Mon Sep 17 00:00:00 2001 From: Yan Wong Date: Tue, 26 May 2020 15:52:00 +0100 Subject: [PATCH 5/6] address review comments and set "H 0" on branches to allow nice SPR animations --- python/tests/data/svg/mut_tree.svg | 8 +++---- python/tests/data/svg/ts.svg | 16 ++++++------- python/tskit/drawing.py | 36 +++++++++++++----------------- 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/python/tests/data/svg/mut_tree.svg b/python/tests/data/svg/mut_tree.svg index 670c4c0103..ce59706a03 100644 --- a/python/tests/data/svg/mut_tree.svg +++ b/python/tests/data/svg/mut_tree.svg @@ -22,7 +22,7 @@ 1 - + 4 @@ -45,15 +45,15 @@ 5 - + 9 - + 0 - + 1 diff --git a/python/tests/data/svg/ts.svg b/python/tests/data/svg/ts.svg index 4aaa661453..5414066402 100644 --- a/python/tests/data/svg/ts.svg +++ b/python/tests/data/svg/ts.svg @@ -30,7 +30,7 @@ 1 - + 4 @@ -53,15 +53,15 @@ 5 - + 9 - + 0 - + 1 @@ -100,7 +100,7 @@ 5 - + 7 @@ -139,7 +139,7 @@ 5 - + 6 @@ -178,7 +178,7 @@ 5 - + 7 @@ -217,7 +217,7 @@ 5 - + 8 diff --git a/python/tskit/drawing.py b/python/tskit/drawing.py index 538ce17f53..e7225af683 100644 --- a/python/tskit/drawing.py +++ b/python/tskit/drawing.py @@ -260,7 +260,6 @@ def __init__( dwg = svgwrite.Drawing(size=self.image_size, debug=True, **root_svg_attributes) self.drawing = dwg dwg.defs.add(dwg.style(SvgTree.standard_style)) - self.node_labels = {u: str(u) for u in range(ts.num_nodes)} if node_labels is None: node_labels = {u: str(u) for u in range(ts.num_nodes)} if style is not None: @@ -273,7 +272,6 @@ def __init__( else: axis_top_padding = 5 tick_len = (5, 5) - self.node_labels = {u: str(u) for u in range(ts.num_nodes)} if root_branch is None: root_branch = any( any(tree.parent(mut.node) == NULL for mut in tree.mutations()) @@ -619,14 +617,11 @@ def add_node(self, curr_svg_group, focal, dx, dy): offset_y = dy if v == NULL: classes.append(f"root") - if self.root_branch: - # FIXME this is pretty crappy for spacing mutations over a root. - root_branch_len = self.root_branch_length - else: - root_branch_len = 0 edge_x = 0 - edge_height = root_branch_len / (len(self.node_mutations[focal]) + 1) - offset_y = dy - root_branch_len + edge_height + edge_height = self.root_branch_length / ( + len(self.node_mutations[focal]) + 1 + ) + offset_y = dy - self.root_branch_length + edge_height else: classes.append(f"a{v}") edge_x = offset_x @@ -653,7 +648,8 @@ def add_node(self, curr_svg_group, focal, dx, dy): ) ) # after the first sideways line of an edge all further movements go downwards - offset_x = edge_x = 0 + offset_x = 0 + edge_x = 0 offset_y = edge_height # Add a new group for each node, and give it these classes for css targetting: @@ -663,13 +659,12 @@ def add_node(self, curr_svg_group, focal, dx, dy): # "sample": a class present if the focal node is a sample # "leaf": a class present if the focal node is a leaf classes.append("node") - ind = self.tree.tree_sequence.node(focal).individual - pop = self.tree.tree_sequence.node(focal).population - if ind != NULL: - classes.append(f"i{ind}") - if pop != NULL: - classes.append(f"p{pop}") - if self.tree.is_sample(focal): + focal_node = self.tree.tree_sequence.node(focal) + if focal_node.individual != NULL: + classes.append(f"i{focal_node.individual}") + if focal_node.population != NULL: + classes.append(f"p{focal_node.population}") + if focal_node.is_sample(): classes.append("sample") if self.tree.is_leaf(focal): classes.append("leaf") @@ -714,11 +709,10 @@ def draw(self): # Add edge first => below dx, dy = curr.edge_dxy - if dx == dy == 0: + if dx == 0 and dy == 0: path = dwg.path([("M", o)], **self.edge_attrs[u]) # e.g. at root - elif dx == 0: - path = dwg.path([("M", o), ("V", -rnd(dy))], **self.edge_attrs[u]) else: + # allowing "H 0" means that animating transitions works correctly path = dwg.path( [("M", o), ("V", -rnd(dy)), ("H", -rnd(dx))], **self.edge_attrs[u] ) @@ -979,7 +973,7 @@ def __init__( # If we don't specify node_labels, default to node ID self.node_labels[u] = str(u) else: - # If we do specify node_labels, default an empty line + # If we do specify node_labels, default to an empty line self.node_labels[u] = self.default_node_label if node_labels is not None: for node, label in node_labels.items(): From f365d51574ffe70fb83b13e932b3ce1b853d8be0 Mon Sep 17 00:00:00 2001 From: Yan Wong Date: Tue, 26 May 2020 16:38:40 +0100 Subject: [PATCH 6/6] Extend lines from nodes past mutations --- python/tests/data/svg/mut_tree.svg | 4 ++-- python/tests/data/svg/ts.svg | 4 ++-- python/tskit/drawing.py | 15 ++++++--------- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/python/tests/data/svg/mut_tree.svg b/python/tests/data/svg/mut_tree.svg index ce59706a03..b237646e0d 100644 --- a/python/tests/data/svg/mut_tree.svg +++ b/python/tests/data/svg/mut_tree.svg @@ -22,7 +22,7 @@ 1 - + 4 @@ -45,7 +45,7 @@ 5 - + 9 diff --git a/python/tests/data/svg/ts.svg b/python/tests/data/svg/ts.svg index 5414066402..9e2fdb73bc 100644 --- a/python/tests/data/svg/ts.svg +++ b/python/tests/data/svg/ts.svg @@ -30,7 +30,7 @@ 1 - + 4 @@ -53,7 +53,7 @@ 5 - + 9 diff --git a/python/tskit/drawing.py b/python/tskit/drawing.py index e7225af683..95ed0724b4 100644 --- a/python/tskit/drawing.py +++ b/python/tskit/drawing.py @@ -617,11 +617,12 @@ def add_node(self, curr_svg_group, focal, dx, dy): offset_y = dy if v == NULL: classes.append(f"root") + # set the origin of the + dx = 0 + dy = self.root_branch_length edge_x = 0 - edge_height = self.root_branch_length / ( - len(self.node_mutations[focal]) + 1 - ) - offset_y = dy - self.root_branch_length + edge_height + edge_height = dy / (len(self.node_mutations[focal]) + 1) + offset_y = offset_y - self.root_branch_length + edge_height else: classes.append(f"a{v}") edge_x = offset_x @@ -674,11 +675,7 @@ def add_node(self, curr_svg_group, focal, dx, dy): transform=f"translate({rnd(offset_x)} {rnd(offset_y)})", ) ) - ret.append( - SvgGroupInfo( - g=grp, edge_dxy=(edge_x, edge_height), node=focal, mutation=None - ) - ) + ret.append(SvgGroupInfo(g=grp, edge_dxy=(dx, dy), node=focal, mutation=None)) return ret def draw(self):