Skip to content

Commit 7dd224f

Browse files
benjefferymergify[bot]
authored andcommitted
Add mini svgwrite inline
1 parent b99f2f6 commit 7dd224f

File tree

6 files changed

+183
-8
lines changed

6 files changed

+183
-8
lines changed

python/pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ requires-python = ">=3.9"
4343
dependencies = [
4444
"jsonschema>=3.0.0",
4545
"numpy>=1.23.5",
46-
"svgwrite>=1.1.10",
4746
]
4847

4948
[project.urls]

python/requirements/CI-docs/requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ breathe==4.35.0
33
sphinx-autodoc-typehints==2.3.0
44
sphinx-issues==5.0.0
55
sphinx-argparse==0.5.2
6-
svgwrite==1.4.3
76
msprime==1.3.3
87
sphinx-book-theme
98
pandas==2.2.3

python/requirements/benchmark.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ tqdm
44
matplotlib
55
si-prefix
66
jsonschema
7-
svgwrite
87
msprime
98
PyYAML
109
numpy<2

python/requirements/development.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,4 @@ sphinx-issues
3535
tqdm
3636
sphinx-book-theme
3737
tszip
38-
svgwrite>=1.1.10
3938
xmlunittest

python/requirements/development.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ dependencies:
4141
- sphinxcontrib-prettyspecialmethods
4242
- sphinx-book-theme
4343
- pydata_sphinx_theme>=0.7.2
44-
- svgwrite>=1.1.10
4544
- tqdm
4645
- tszip
4746
- pip:

python/tskit/drawing.py

Lines changed: 183 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# MIT License
22
#
3-
# Copyright (c) 2018-2024 Tskit Developers
3+
# Copyright (c) 2018-2025 Tskit Developers
44
# Copyright (c) 2015-2017 University of Oxford
55
#
66
# Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -30,13 +30,13 @@
3030
import numbers
3131
import operator
3232
import warnings
33+
import xml
3334
from dataclasses import dataclass
3435
from typing import List
3536
from typing import Mapping
3637
from typing import Union
3738

3839
import numpy as np
39-
import svgwrite
4040

4141
import tskit
4242
import tskit.util as util
@@ -55,6 +55,186 @@
5555
OMIT_MIDDLE = 8
5656

5757

58+
# Minimal SVG generation module to replace svgwrite for tskit visualization.
59+
# This implementation provides only the functionality needed for the visualization
60+
# code while maintaining the same API as svgwrite.
61+
62+
63+
class Element:
64+
def __init__(self, tag, **kwargs):
65+
self.tag = tag
66+
self.attrs = {}
67+
self.children = []
68+
69+
# Process kwargs in alphabetical order
70+
for key in sorted(kwargs.keys()):
71+
value = kwargs[key]
72+
# Handle class_ special case for class attribute
73+
if key.endswith("_"):
74+
key = key[:-1]
75+
key = key.replace("_", "-")
76+
self.attrs[key] = value
77+
78+
def __getitem__(self, key):
79+
return self.attrs.get(key, "")
80+
81+
def __setitem__(self, key, value):
82+
self.attrs[key] = value
83+
84+
def add(self, child):
85+
self.children.append(child)
86+
return child
87+
88+
def set_desc(self, **kwargs):
89+
if "title" in kwargs:
90+
title_elem = Element("title")
91+
title_elem.children.append(kwargs["title"])
92+
self.children.append(title_elem)
93+
return self
94+
95+
def _attr_str(self):
96+
result = []
97+
for key, value in self.attrs.items():
98+
if isinstance(value, (list, tuple)):
99+
# Handle points lists (for polygon/polyline)
100+
if key == "points":
101+
points_str = " ".join(f"{x},{y}" for x, y in value)
102+
result.append(f'{key}="{points_str}"')
103+
else:
104+
result.append(f'{key}="{" ".join(map(str, value))}"')
105+
else:
106+
result.append(f'{key}="{value}"')
107+
return " ".join(result)
108+
109+
def tostring(self):
110+
attr_str = self._attr_str()
111+
start = f"<{self.tag}"
112+
if attr_str:
113+
start += f" {attr_str}"
114+
115+
if not self.children:
116+
return f"{start}/>"
117+
118+
result = [f"{start}>"]
119+
for child in self.children:
120+
if isinstance(child, Element):
121+
result.append(child.tostring())
122+
else:
123+
# Convert any non-Element to string
124+
result.append(str(child))
125+
result.append(f"</{self.tag}>")
126+
return "".join(result)
127+
128+
129+
class Drawing:
130+
def __init__(self, size=None, debug=False, **kwargs):
131+
kwargs = {
132+
"version": "1.1",
133+
"xmlns": "http://www.w3.org/2000/svg",
134+
"xmlns:ev": "http://www.w3.org/2001/xml-events",
135+
"xmlns:xlink": "http://www.w3.org/1999/xlink",
136+
"baseProfile": "full",
137+
**kwargs,
138+
}
139+
if size is not None:
140+
kwargs["width"] = size[0]
141+
kwargs["height"] = size[1]
142+
self.root = Element("svg", **kwargs)
143+
self.defs = Element("defs")
144+
self.root.add(self.defs)
145+
146+
def add(self, element):
147+
return self.root.add(element)
148+
149+
def g(self, **kwargs):
150+
return Element("g", **kwargs)
151+
152+
def rect(self, insert=None, size=None, **kwargs):
153+
if insert:
154+
kwargs["x"] = insert[0]
155+
kwargs["y"] = insert[1]
156+
if size:
157+
kwargs["width"] = size[0]
158+
kwargs["height"] = size[1]
159+
return Element("rect", **kwargs)
160+
161+
def circle(self, center=None, r=None, **kwargs):
162+
if center:
163+
kwargs["cx"] = center[0]
164+
kwargs["cy"] = center[1]
165+
if r:
166+
kwargs["r"] = r
167+
return Element("circle", **kwargs)
168+
169+
def line(self, start=None, end=None, **kwargs):
170+
if start:
171+
kwargs["x1"] = start[0]
172+
kwargs["y1"] = start[1]
173+
else:
174+
kwargs["x1"] = 0
175+
kwargs["y1"] = 0
176+
if end:
177+
kwargs["x2"] = end[0]
178+
kwargs["y2"] = end[1]
179+
else:
180+
kwargs["x2"] = 0 # pragma: not covered
181+
kwargs["y2"] = 0 # pragma: not covered
182+
return Element("line", **kwargs)
183+
184+
def polyline(self, points=None, **kwargs):
185+
if points:
186+
kwargs["points"] = points
187+
return Element("polyline", **kwargs)
188+
189+
def polygon(self, points=None, **kwargs):
190+
if points:
191+
kwargs["points"] = points
192+
return Element("polygon", **kwargs)
193+
194+
def path(self, d=None, **kwargs):
195+
if isinstance(d, list):
196+
# Convert path commands from tuples to string
197+
path_str = ""
198+
for cmd in d:
199+
if isinstance(cmd, tuple) and len(cmd) >= 2:
200+
cmd_letter = cmd[0]
201+
# Handle nested tuples by flattening
202+
params = []
203+
for param in cmd[1:]:
204+
if isinstance(param, tuple):
205+
# Flatten tuple coordinates
206+
params.extend(str(p) for p in param)
207+
else:
208+
params.append(str(param))
209+
path_str += f"{cmd_letter} {' '.join(params)} "
210+
kwargs["d"] = path_str.strip()
211+
elif d:
212+
kwargs["d"] = d
213+
return Element("path", **kwargs)
214+
215+
def text(self, text=None, **kwargs):
216+
elem = Element("text", **kwargs)
217+
if text:
218+
elem.children.append(text)
219+
return elem
220+
221+
def style(self, content):
222+
elem = Element("style", type="text/css")
223+
if content:
224+
# Use CDATA to avoid having to escape special characters in CSS
225+
elem.children.append(f"<![CDATA[{content}]]>")
226+
return elem
227+
228+
def tostring(self, pretty=False):
229+
if pretty:
230+
return xml.dom.minidom.parseString(self.root.tostring()).toprettyxml()
231+
return self.root.tostring()
232+
233+
def saveas(self, path, pretty=False):
234+
with open(path, "w", encoding="utf-8") as f:
235+
f.write(self.tostring(pretty=pretty))
236+
237+
58238
@dataclass
59239
class Offsets:
60240
"Used when x_lim set, and displayed ts has been cut down by keep_intervals"
@@ -688,7 +868,7 @@ def __init__(
688868
root_svg_attributes = {}
689869
if canvas_size is None:
690870
canvas_size = size
691-
dwg = svgwrite.Drawing(size=canvas_size, debug=True, **root_svg_attributes)
871+
dwg = Drawing(size=canvas_size, debug=True, **root_svg_attributes)
692872

693873
self.image_size = size
694874
self.plotbox = Plotbox(size)

0 commit comments

Comments
 (0)