From 6936ed59d9f58881160753df48822fb99959710c Mon Sep 17 00:00:00 2001 From: Yan Wong Date: Fri, 24 Apr 2020 16:32:59 +0100 Subject: [PATCH 1/4] switch all id labels in SVG to classes, to avoid duplicate IDs, and add classes Fixes #467 This changes class names to use simple ids, and documents the class structure As described in https://github.com/tskit-dev/tskit/issues/467#issuecomment-619048036 Placing trees in a ts using groups emoves the need for any IDs --- docs/conf.py | 1 + python/tskit/drawing.py | 80 ++++++----- python/tskit/trees.py | 296 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 336 insertions(+), 41 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 064bb8eae0..707d81dd71 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -279,6 +279,7 @@ def handle_item(fieldarg, content): intersphinx_mapping = { "https://docs.python.org/": None, "http://docs.scipy.org/doc/numpy/": None, + "https://svgwrite.readthedocs.io/en/stable/": None, } # -- Options for todo extension ---------------------------------------------- diff --git a/python/tskit/drawing.py b/python/tskit/drawing.py index 59e5e545f4..30a2ccd121 100644 --- a/python/tskit/drawing.py +++ b/python/tskit/drawing.py @@ -166,7 +166,9 @@ def remap(original_map, new_key, none_value): class SvgTreeSequence: """ - Draw a TreeSequence in SVG. + A class to draw a tree sequence in SVG format. + + See :meth:`TreeSequence.draw_svg` for a description of usage and parameters. """ def __init__( @@ -180,6 +182,8 @@ def __init__( node_attrs=None, edge_attrs=None, node_label_attrs=None, + mutation_attrs=None, + mutation_label_attrs=None, ): self.ts = ts if size is None: @@ -200,34 +204,34 @@ def __init__( SvgTree( tree, (tree_width, treebox_height), - max_tree_height="ts", node_labels=node_labels, mutation_labels=mutation_labels, tree_height_scale=tree_height_scale, + max_tree_height="ts", node_attrs=node_attrs, edge_attrs=edge_attrs, node_label_attrs=node_label_attrs, + mutation_attrs=mutation_attrs, + mutation_label_attrs=mutation_label_attrs, ) for tree in ts.trees() ] ticks = [] y = self.treebox_y_offset - defs = self.drawing.defs - for _tree, svg_tree in zip(ts.trees(), svg_trees): - defs.add(svg_tree.root_group) + dwg = self.drawing + ts_class = "tree-sequence" + root_group = dwg.add(dwg.g(class_=ts_class)) - for tree in ts.trees(): - tree_id = f"#tree_{tree.index}" - use = self.drawing.use(tree_id, (x, y)) - self.drawing.add(use) + trees = root_group.add(dwg.g(class_="trees")) + for svg_tree, tree in zip(svg_trees, ts.trees()): + svg_tree.root_group["transform"] = f"translate({x} {y})" + trees.add(svg_tree.root_group) ticks.append((x, tree.interval[0])) x += tree_width ticks.append((x, ts.sequence_length)) - dwg = self.drawing - # # Debug --- draw the tree and axes boxes # w = self.image_size[0] - 2 * self.treebox_x_offset # h = self.image_size[1] - 2 * self.treebox_y_offset @@ -241,11 +245,12 @@ def __init__( axes_left = self.treebox_x_offset axes_right = self.image_size[0] - self.treebox_x_offset y = self.image_size[1] - 2 * self.axes_y_offset - dwg.add(dwg.line((axes_left, y), (axes_right, y), stroke="black")) + axis = root_group.add(dwg.g(class_="axis")) + axis.add(dwg.line((axes_left, y), (axes_right, y), stroke="black")) for x, genome_coord in ticks: delta = 5 - dwg.add(dwg.line((x, y - delta), (x, y + delta), stroke="black")) - dwg.add( + axis.add(dwg.line((x, y - delta), (x, y + delta), stroke="black")) + axis.add( dwg.text( f"{genome_coord:.2f}", (x, y + 20), @@ -258,13 +263,9 @@ def __init__( class SvgTree: """ - An SVG representation of a single tree. - - TODO should provide much more SVG structure which we document fully - to that the SVG elements can be manipulated directly by the user. - For example, every edge should be given an SVG ID so that it can - be referred to and modified. + A class to draw a tree in SVG format. + See :meth:`Tree.draw_svg` for a description of usage and parameters. """ def __init__( @@ -339,12 +340,15 @@ def __init__( def setup_drawing(self): self.drawing = svgwrite.Drawing(size=self.image_size, debug=True) dwg = self.drawing - self.root_group = dwg.add(dwg.g(id=f"tree_{self.tree.index}")) - self.edges = self.root_group.add(dwg.g(id="edges", stroke="black", fill="none")) - self.symbols = self.root_group.add(dwg.g(id="symbols")) + tree_class = f"tree t{self.tree.index}" + self.root_group = dwg.add(dwg.g(class_=tree_class)) + self.edges = self.root_group.add( + dwg.g(class_="edges", stroke="black", fill="none") + ) + self.symbols = self.root_group.add(dwg.g(class_="symbols")) self.nodes = self.symbols.add(dwg.g(class_="nodes")) self.mutations = self.symbols.add(dwg.g(class_="mutations", fill="red")) - self.labels = self.root_group.add(dwg.g(id="labels", font_size=14)) + self.labels = self.root_group.add(dwg.g(class_="labels", font_size=14)) self.node_labels = self.labels.add(dwg.g(class_="nodes")) self.mutation_labels = self.labels.add( dwg.g(class_="mutations", font_style="italic") @@ -452,8 +456,12 @@ def draw(self): for u in tree.nodes(): pu = node_x_coord_map[u], node_y_coord_map[u] - node_id = f"node_{tree.index}_{u}" - self.nodes.add(dwg.circle(id=node_id, center=pu, **self.node_attrs[u])) + node_class = f"n{u}" + if tree.is_sample(u): + node_class += " sample" + self.nodes.add( + dwg.circle(center=pu, class_=node_class, **self.node_attrs[u]) + ) dx = 0 dy = -5 labels = self.mid_labels @@ -466,19 +474,22 @@ def draw(self): labels = self.right_labels else: labels = self.left_labels - # TODO add ID to node label text. # TODO get rid of these manual positioning tweaks and add them # as offsets the user can access via a transform or something. labels.add( - dwg.text(insert=(pu[0] + dx, pu[1] + dy), **self.node_label_attrs[u]) + dwg.text( + insert=(pu[0] + dx, pu[1] + dy), + class_=node_class, + **self.node_label_attrs[u], + ) ) v = tree.parent(u) if v != NULL: - edge_id = f"edge_{tree.index}_{u}" + edge_class = f"p{v} c{u}" pv = node_x_coord_map[v], node_y_coord_map[v] path = dwg.path( [("M", pu), ("V", pv[1]), ("H", pv[0])], - id=edge_id, + class_=edge_class, **self.edge_attrs[u], ) self.edges.add(path) @@ -490,10 +501,14 @@ def draw(self): delta = (pv[1] - pu[1]) / (num_mutations + 1) x = pu[0] y = pv[1] - delta - # TODO add mutation IDs for mutation in reversed(node_mutations[u]): + mutation_class = f"m{mutation.id} s{mutation.site} n{u}" self.mutations.add( - dwg.rect(insert=(x, y), **self.mutation_attrs[mutation.id]) + dwg.rect( + insert=(x, y), + class_=mutation_class, + **self.mutation_attrs[mutation.id], + ) ) dx = 5 if tree.left_sib(mutation.node) == NULL: @@ -507,6 +522,7 @@ def draw(self): labels.add( dwg.text( insert=(x + dx, y + dy), + class_=mutation_class, **self.mutation_label_attrs[mutation.id], ) ) diff --git a/python/tskit/trees.py b/python/tskit/trees.py index 9356d659a8..4f5bd1d991 100644 --- a/python/tskit/trees.py +++ b/python/tskit/trees.py @@ -1194,10 +1194,179 @@ def draw_text(self, orientation=None, **kwargs): ) return str(text_tree) - def draw_svg(self, path=None, **kwargs): - # Experimental drawing code. This aims to replace or at least be a more - # powerful SVG driven interface for the code below. - draw = drawing.SvgTree(self, **kwargs) + def draw_svg( + self, + path=None, + size=None, + node_labels=None, + mutation_labels=None, + *, + tree_height_scale=None, + max_tree_height=None, + node_attrs=None, + edge_attrs=None, + node_label_attrs=None, + mutation_attrs=None, + mutation_label_attrs=None, + ): + """ + Return an SVG representation of a single tree. + + When working in a Jupyter notebook, use the ``IPython.display.SVG`` function + to display the SVG output from this function inline in the notebook. + + .. note:: + + The elements in the tree are placed into + different `SVG groups `_ for + easy styling and manipulation. Both these groups and their component items + are marked with SVG classes so that they can be targetted. This allows + individual components of the drawing to be hidden, styled, or otherwise + manipulated. For example, when drawing (say) the first tree from a tree + sequence, all the SVG components will be placed in a group of class ``tree``. + The group will have the additional class ``t0``, indicating that this tree + has index 0 in the tree sequence. The general SVG structure is as follows: + + * The *tree* group (classes ``tree`` and ``tN`` where `N` is the tree index). + This contains the following three groups: + + * The *edges* group (class ``edges``), containing edges. Each edge has + classes ``pX``, and ``cY`` where `X` and `Y` are the ids of the parent + and child nodes. + * The *symbols* group (class ``symbols``), containing two subgroups: + + * The *node symbols* group (class ``nodes``) containing a + `circle `_ for + each node. Each node symbol has a class ``nX`` where ``X`` is the node + id. Symbols corresponding to sample nodes are additionally labelled + with a class of ``sample``. + * The *mutation symbols* group (class ``mutations``) containing a + `rectangle `_ for + each mutation. Each mutation symbol has classes ``mX``, ``sY``, and + ``nZ`` where `X` is the mutation id, `Y` is the site id, and `Z` is + the id of the node above which the mutation occurs. + + * The *labels* group (class ``labels``) containing two subgroups: + + * The *node labels* group (class ``nodes``) containing text for each + node. Each `text `_ + element in this group corresponds to a node, and has the same set of + classes as its equivalent node symbol (i.e. ``nX`` and potentially + ``sample``) + * The *mutation labels* group (class `mutations`) containing containing + text for each mutation. Each + `text `_ element in + this group corresponds to a mutation, and has the same set of classes + as its equivalent mutation symbol (i.e. ``mX``, ``sY``, and ``nZ``) + + The classes can be used to manipulate the element, e.g. by using + `stylesheets `_. Stylesheets can be + pasted into the SVG, or added to pages in which the SVG is embedded. For + example, the following style specification will hide all node labels apart + from the sample nodes: + + .. code-block:: css + + .tree .labels .nodes text:not(.sample) {visibility: hidden} + + You can also change the format of various items: the following styles will + display the sample node symbols as blue, and reduce the size of the symbols + on the internal nodes of the tree: + + .. code-block:: css + + .tree .symbols .nodes .sample {fill: blue} + .tree .symbols .nodes circle:not(.sample) {r: 1.5px} + + While specific nodes can be targetted by number, such as the following which + displays node 10 in red, and similarly colours the edges whose parent is node + 10: + + .. code-block:: css + + .tree .symbols .nodes .n10 {fill: red} + .tree .edges .p10 {stroke: red} + + Mutations can be targetted by id, site id, or node number. The following + style displays all mutations immediately above node 10 as yellow with a black + border: + + .. code-block:: css + + .tree .symbols .mutations .n10 {fill: yellow; stroke: black} + + :param str path: The path to the file to write the output. If None, do not + write to file. + :param size: A tuple of (width, height) giving the width and height of the + produced SVG drawing in abstract user units (usually interpreted as pixels on + initial display). If None, a default of (200, 200) is used. + :type size: tuple(int, int) + :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. + :type node_labels: dict(int, str) + :param mutation_labels: If specified, show custom labels for the + mutations (specified by ID) that are present in the map; any mutations + not present will not have a label. + :type mutation_labels: dict(int, str) + :param str tree_height_scale: Control how height values for nodes are computed. + If this is equal to ``"time"``, node heights are proportional to their time + values. If this is equal to ``"log_time"``, node heights are proportional to + their log(time) values. If it is equal to ``"rank"``, node heights are spaced + equally according to their ranked times. For SVG output the default is + 'time'-scale whereas for text output the default is 'rank'-scale. + Time scaling is not currently supported for text output. + :param str,float max_tree_height: The maximum tree height value in the current + scaling system (see ``tree_height_scale``). Can be either a string or a + numeric value. If equal to ``"tree"``, the maximum tree height is set to be + that of the oldest root in the tree. If equal to ``"ts"`` the maximum + height is set to be the height of the oldest root in the tree sequence; + this is useful when drawing trees from the same tree sequence as it ensures + that node heights are consistent. If a numeric value, this is used as the + maximum tree height by which to scale other nodes. This parameters + is not currently supported for text output. + :param node_attrs: Set custom attributes for specific node symbols. Each key in + the `node_attrs` map is a node id; the corresponding value must be a + dictionary specifying additional parameters passed to the + :meth:`svgwrite.drawing.Drawing.circle` method. + :type node_attrs: dict(int, dict) + :param edge_attrs: Set custom attributes for specific edges. Each key + in the `edge_attrs` map is the *node* id of the child node of an edge; + the corresponding value must be a dictionary specifying additional parameters + passed to the :meth:`svgwrite.drawing.Drawing.path` method. + :type edge_attrs: dict(int, dict) + :param node_label_attrs: Set custom attributes for specific node labels. Each key + in the `node_label_attrs` map is a node id; the corresponding value must be a + dictionary specifying additional parameters passed to the + :meth:`svgwrite.drawing.Drawing.text` method. + :type node_label_attrs: dict(int, dict) + :param mutation_attrs: Set custom attributes for specific mutation symbols. Each + key in the `mutation_attrs` map is a mutation id; the corresponding value + must be a dictionary specifying additional parameters passed to the + :meth:`svgwrite.drawing.Drawing.rect` method. + :type mutation_attrs: dict(int, dict) + :param mutation_label_attrs: Set custom attributes for specific mutation labels. + Each key in the `mutation_label_attrs` map is a mutation id; the + corresponding value must be a dictionary specifying additional parameters + passed to the :meth:`svgwrite.drawing.Drawing.text` method. + :type mutation_label_attrs: dict(int, dict) + + :return: An SVG representation of a tree. + :rtype: str + """ + draw = drawing.SvgTree( + self, + size, + node_labels=node_labels, + mutation_labels=mutation_labels, + tree_height_scale=tree_height_scale, + max_tree_height=max_tree_height, + node_attrs=node_attrs, + edge_attrs=edge_attrs, + node_label_attrs=node_label_attrs, + mutation_attrs=mutation_attrs, + mutation_label_attrs=mutation_label_attrs, + ) output = draw.drawing.tostring() if path is not None: # TODO: removed the pretty here when this is stable. @@ -1282,7 +1451,8 @@ def draw( present in the map take the default colour, and those mapping to ``None`` are not drawn. (Only supported in the SVG format.) :param str format: The format of the returned image. Currently supported - are 'svg', 'ascii' and 'unicode'. + are 'svg', 'ascii' and 'unicode'. Note that the :meth:`Tree.draw_svg` + method provides more comprehensive functionality for creating SVGs. :param dict edge_colours: If specified, show custom colours for the edge joining each node in the map to its parent. As for ``node_colours``, unspecified edges take the default colour, and ``None`` values result in the @@ -4062,10 +4232,118 @@ def trim(self, record_provenance=True): tables.trim(record_provenance) return tables.tree_sequence() - def draw_svg(self, path=None, **kwargs): - # TODO document this method, including semantic details of the - # returned SVG object. - draw = drawing.SvgTreeSequence(self, **kwargs) + def draw_svg( + self, + path=None, + size=None, + node_labels=None, + mutation_labels=None, + *, + tree_height_scale=None, + max_tree_height=None, + node_attrs=None, + edge_attrs=None, + mutation_attrs=None, + node_label_attrs=None, + mutation_label_attrs=None, + ): + """ + Return an SVG representation of a tree sequence. + + When working in a Jupyter notebook, use the ``IPython.display.SVG`` function + to display the SVG output from this function inline in the notebook. + + .. note:: + + The visual elements in the svg are + `grouped `_ + for easy styling and manipulation. The entire visualization with trees and X + axis is contained within a group of class ``tree-sequence``. Each tree in + the displayed tree sequence is contained in a group of class ``tree``, as + described in :meth:`Tree.draw_svg`, so that visual elements pertaining to one + or more trees targetted as documented in that method. For instance, the + following styles will change the colour of all the edges of the *initial* + tree in the sequence and hide the internal node labels in *all* the trees + + .. code-block:: css + + .tree.t0 .edges {stroke: blue} + .tree .labels .nodes text:not(.sample) {visibility: hidden} + + See :meth:`Tree.draw_svg` for further details. + + :param str path: The path to the file to write the output. If None, do not + write to file. + :param size: A tuple of (width, height) giving the width and height of the + produced SVG drawing in abstract user units (usually interpreted as pixels on + initial display). If None, a default of (200, 200) is used. + :type size: tuple(int, int) + :param str tree_height_scale: Control how height values for nodes are computed. + If this is equal to ``"time"``, node heights are proportional to their time + values. If this is equal to ``"log_time"``, node heights are proportional to + their log(time) values. If it is equal to ``"rank"``, node heights are spaced + equally according to their ranked times. For SVG output the default is + 'time'-scale whereas for text output the default is 'rank'-scale. + Time scaling is not currently supported for text output. + :param str,float max_tree_height: The maximum tree height value in the current + scaling system (see ``tree_height_scale``). Can be either a string or a + numeric value. If equal to ``"tree"``, the maximum tree height is set to be + that of the oldest root in the tree. If equal to ``"ts"`` the maximum + height is set to be the height of the oldest root in the tree sequence; + this is useful when drawing trees from the same tree sequence as it ensures + that node heights are consistent. If a numeric value, this is used as the + maximum tree height by which to scale other nodes. This parameters + is not currently supported for text output. + :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. + :type node_labels: dict(int, str) + :param mutation_labels: If specified, show custom labels for the + mutations (specified by ID) that are present in the map; any mutations + not present will not have a label. + :type mutation_labels: dict(int, str) + :param node_attrs: Set custom attributes for specific node symbols. Each key in + the `node_attrs` map is a node id; the corresponding value must be a + dictionary specifying additional parameters passed to the + :meth:`svgwrite.drawing.Drawing.circle` method. + :type node_attrs: dict(int, dict) + :param edge_attrs: Set custom attributes for specific edges. Each key + in the `edge_attrs` map is the *node* id of the child node of an edge; + the corresponding value must be a dictionary specifying additional parameters + passed to the :meth:`svgwrite.drawing.Drawing.path` method. + :type edge_attrs: dict(int, dict) + :param node_label_attrs: Set custom attributes for specific node labels. Each key + in the `node_label_attrs` map is a node id; the corresponding value must be a + dictionary specifying additional parameters passed to the + :meth:`svgwrite.drawing.Drawing.text` method. + :type node_label_attrs: dict(int, dict) + :param mutation_attrs: Set custom attributes for specific mutation symbols. Each + key in the `mutation_attrs` map is a mutation id; the corresponding value + must be a dictionary specifying additional parameters passed to the + :meth:`svgwrite.drawing.Drawing.rect` method. + :type mutation_attrs: dict(int, dict) + :param mutation_label_attrs: Set custom attributes for specific mutation labels. + Each key in the `mutation_label_attrs` map is a mutation id; the + corresponding value must be a dictionary specifying additional parameters + passed to the :meth:`svgwrite.drawing.Drawing.text` method. + :type mutation_label_attrs: dict(int, dict) + + :return: An SVG representation of a tree. + :rtype: str + """ + draw = drawing.SvgTreeSequence( + self, + size, + node_labels=node_labels, + mutation_labels=mutation_labels, + max_tree_height="ts", + tree_height_scale=tree_height_scale, + node_attrs=node_attrs, + edge_attrs=edge_attrs, + node_label_attrs=node_label_attrs, + mutation_attrs=mutation_attrs, + mutation_label_attrs=mutation_label_attrs, + ) output = draw.drawing.tostring() if path is not None: # TODO remove the 'pretty' when we are done debugging this. From 25fc12e06384b905d2845805b08e480c59c40924 Mon Sep 17 00:00:00 2001 From: Yan Wong Date: Tue, 28 Apr 2020 12:06:24 +0100 Subject: [PATCH 2/4] Add root_svg_attributes and 'style' attribute --- python/tskit/drawing.py | 38 +++++++--- python/tskit/trees.py | 154 ++++++++++++++++++++++------------------ 2 files changed, 113 insertions(+), 79 deletions(-) diff --git a/python/tskit/drawing.py b/python/tskit/drawing.py index 30a2ccd121..8db17620ff 100644 --- a/python/tskit/drawing.py +++ b/python/tskit/drawing.py @@ -180,16 +180,26 @@ def __init__( node_labels=None, mutation_labels=None, node_attrs=None, + mutation_attrs=None, edge_attrs=None, node_label_attrs=None, - mutation_attrs=None, mutation_label_attrs=None, + root_svg_attributes=None, + style=None, ): self.ts = ts if size is None: size = (200 * ts.num_trees, 200) + if root_svg_attributes is None: + root_svg_attributes = {} + if max_tree_height is None: + max_tree_height = "ts" self.image_size = size - self.drawing = svgwrite.Drawing(size=self.image_size, debug=True) + self.drawing = svgwrite.Drawing( + size=self.image_size, debug=True, **root_svg_attributes + ) + if style is not None: + self.drawing.defs.add(self.drawing.style(style)) self.node_labels = {u: str(u) for u in range(ts.num_nodes)} # TODO add general padding arguments following matplotlib's terminology. self.axes_x_offset = 15 @@ -207,7 +217,7 @@ def __init__( node_labels=node_labels, mutation_labels=mutation_labels, tree_height_scale=tree_height_scale, - max_tree_height="ts", + max_tree_height=max_tree_height, node_attrs=node_attrs, edge_attrs=edge_attrs, node_label_attrs=node_label_attrs, @@ -272,21 +282,28 @@ def __init__( self, tree, size=None, - node_labels=None, - mutation_labels=None, tree_height_scale=None, max_tree_height=None, + node_labels=None, + mutation_labels=None, node_attrs=None, + mutation_attrs=None, edge_attrs=None, node_label_attrs=None, - mutation_attrs=None, mutation_label_attrs=None, + root_svg_attributes=None, + style=None, ): self.tree = tree if size is None: size = (200, 200) self.image_size = size - self.setup_drawing() + if root_svg_attributes is None: + root_svg_attributes = {} + self.root_svg_attributes = root_svg_attributes + self.drawing = self.setup_drawing() + if style is not None: + self.drawing.defs.add(self.drawing.style(style)) self.treebox_x_offset = 10 self.treebox_y_offset = 10 self.treebox_width = size[0] - 2 * self.treebox_x_offset @@ -338,8 +355,10 @@ def __init__( self.draw() def setup_drawing(self): - self.drawing = svgwrite.Drawing(size=self.image_size, debug=True) - dwg = self.drawing + "Return an svgwrite.Drawing object for further use" + dwg = svgwrite.Drawing( + size=self.image_size, debug=True, **self.root_svg_attributes + ) tree_class = f"tree t{self.tree.index}" self.root_group = dwg.add(dwg.g(class_=tree_class)) self.edges = self.root_group.add( @@ -358,6 +377,7 @@ def setup_drawing(self): self.right_labels = self.node_labels.add(dwg.g(text_anchor="end")) self.mutation_left_labels = self.mutation_labels.add(dwg.g(text_anchor="start")) self.mutation_right_labels = self.mutation_labels.add(dwg.g(text_anchor="end")) + return dwg def assign_y_coordinates(self, tree_height_scale, max_tree_height): tree_height_scale = check_tree_height_scale(tree_height_scale) diff --git a/python/tskit/trees.py b/python/tskit/trees.py index 4f5bd1d991..2a10bb6169 100644 --- a/python/tskit/trees.py +++ b/python/tskit/trees.py @@ -1197,17 +1197,19 @@ def draw_text(self, orientation=None, **kwargs): def draw_svg( self, path=None, - size=None, - node_labels=None, - mutation_labels=None, *, + size=None, tree_height_scale=None, max_tree_height=None, + node_labels=None, + mutation_labels=None, node_attrs=None, edge_attrs=None, node_label_attrs=None, mutation_attrs=None, mutation_label_attrs=None, + root_svg_attributes=None, + style=None, ): """ Return an SVG representation of a single tree. @@ -1260,27 +1262,27 @@ def draw_svg( as its equivalent mutation symbol (i.e. ``mX``, ``sY``, and ``nZ``) The classes can be used to manipulate the element, e.g. by using - `stylesheets `_. Stylesheets can be - pasted into the SVG, or added to pages in which the SVG is embedded. For - example, the following style specification will hide all node labels apart - from the sample nodes: + `stylesheets `_. Style strings can + be embedded in the svg by using the ``style`` parameter, or added to html + pages which contain the raw SVG (e.g. within a Jupyter notebook by using the + IPython HTML() function). As a simple example, the following style string + will hide all labels: .. code-block:: css - .tree .labels .nodes text:not(.sample) {visibility: hidden} + .tree .labels {visibility: hidden} You can also change the format of various items: the following styles will - display the sample node symbols as blue, and reduce the size of the symbols - on the internal nodes of the tree: + display the symbols of the *sample* nodes only in blue, and hide the labels + of all the internal nodes: .. code-block:: css .tree .symbols .nodes .sample {fill: blue} - .tree .symbols .nodes circle:not(.sample) {r: 1.5px} + .tree .labels .nodes text:not(.sample) {visibility: hidden} - While specific nodes can be targetted by number, such as the following which - displays node 10 in red, and similarly colours the edges whose parent is node - 10: + Specific nodes can be targetted by number. The following style will display + node 10 in red, and also colour in red the edges whose parent is node 10: .. code-block:: css @@ -1289,11 +1291,14 @@ def draw_svg( Mutations can be targetted by id, site id, or node number. The following style displays all mutations immediately above node 10 as yellow with a black - border: + border, and decreases the height of all mutation symbols on the tree, + displaying them as only 2 pixels high, rather than the default of 6. .. code-block:: css .tree .symbols .mutations .n10 {fill: yellow; stroke: black} + .tree .symbols .mutations rect {width: 6px; height: 2px; + /* also must re-centre the rect */ transform: translate(-3px, -1px);} :param str path: The path to the file to write the output. If None, do not write to file. @@ -1301,6 +1306,19 @@ def draw_svg( produced SVG drawing in abstract user units (usually interpreted as pixels on initial display). If None, a default of (200, 200) is used. :type size: tuple(int, int) + :param str tree_height_scale: Control how height values for nodes are computed. + If this is equal to ``"time"`` (the default), node heights are proportional + to their time values. If this is equal to ``"log_time"``, node heights are + proportional to their log(time) values. If it is equal to ``"rank"``, node + heights are spaced equally according to their ranked times. + :param str,float max_tree_height: The maximum tree height value in the current + scaling system (see ``tree_height_scale``). Can be either a string or a + numeric value. If equal to ``"tree"`` (the default), the maximum tree height + is set to be that of the oldest root in the tree. If equal to ``"ts"`` the + maximum height is set to be the height of the oldest root in the tree + sequence; this is useful when drawing trees from the same tree sequence as it + ensures that node heights are consistent. If a numeric value, this is used as + the maximum tree height by which to scale other nodes. :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. @@ -1309,27 +1327,16 @@ def draw_svg( mutations (specified by ID) that are present in the map; any mutations not present will not have a label. :type mutation_labels: dict(int, str) - :param str tree_height_scale: Control how height values for nodes are computed. - If this is equal to ``"time"``, node heights are proportional to their time - values. If this is equal to ``"log_time"``, node heights are proportional to - their log(time) values. If it is equal to ``"rank"``, node heights are spaced - equally according to their ranked times. For SVG output the default is - 'time'-scale whereas for text output the default is 'rank'-scale. - Time scaling is not currently supported for text output. - :param str,float max_tree_height: The maximum tree height value in the current - scaling system (see ``tree_height_scale``). Can be either a string or a - numeric value. If equal to ``"tree"``, the maximum tree height is set to be - that of the oldest root in the tree. If equal to ``"ts"`` the maximum - height is set to be the height of the oldest root in the tree sequence; - this is useful when drawing trees from the same tree sequence as it ensures - that node heights are consistent. If a numeric value, this is used as the - maximum tree height by which to scale other nodes. This parameters - is not currently supported for text output. :param node_attrs: Set custom attributes for specific node symbols. Each key in the `node_attrs` map is a node id; the corresponding value must be a dictionary specifying additional parameters passed to the :meth:`svgwrite.drawing.Drawing.circle` method. :type node_attrs: dict(int, dict) + :param mutation_attrs: Set custom attributes for specific mutation symbols. Each + key in the `mutation_attrs` map is a mutation id; the corresponding value + must be a dictionary specifying additional parameters passed to the + :meth:`svgwrite.drawing.Drawing.rect` method. + :type mutation_attrs: dict(int, dict) :param edge_attrs: Set custom attributes for specific edges. Each key in the `edge_attrs` map is the *node* id of the child node of an edge; the corresponding value must be a dictionary specifying additional parameters @@ -1340,16 +1347,15 @@ def draw_svg( dictionary specifying additional parameters passed to the :meth:`svgwrite.drawing.Drawing.text` method. :type node_label_attrs: dict(int, dict) - :param mutation_attrs: Set custom attributes for specific mutation symbols. Each - key in the `mutation_attrs` map is a mutation id; the corresponding value - must be a dictionary specifying additional parameters passed to the - :meth:`svgwrite.drawing.Drawing.rect` method. - :type mutation_attrs: dict(int, dict) :param mutation_label_attrs: Set custom attributes for specific mutation labels. Each key in the `mutation_label_attrs` map is a mutation id; the corresponding value must be a dictionary specifying additional parameters passed to the :meth:`svgwrite.drawing.Drawing.text` method. :type mutation_label_attrs: dict(int, dict) + :param dict root_svg_attributes: Additional attributes, such as an id, that will + be embedded in the root ```` tag of the generated drawing. + :param str style: A `css style string `_ + that will be included in the ``