codefig 0.17.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codefig/__init__.py +210 -0
- codefig/_validate.py +12 -0
- codefig/autowrap.py +277 -0
- codefig/charts.py +1806 -0
- codefig/code.py +602 -0
- codefig/compose.py +374 -0
- codefig/constraints.py +1571 -0
- codefig/dim.py +214 -0
- codefig/edge_align.py +231 -0
- codefig/edges.py +2824 -0
- codefig/export.py +204 -0
- codefig/figure.py +1923 -0
- codefig/fills.py +146 -0
- codefig/flows.py +102 -0
- codefig/glyphs.py +395 -0
- codefig/icon.py +288 -0
- codefig/icons/LICENSE-tabler-icons.txt +21 -0
- codefig/icons/__init__.py +101 -0
- codefig/icons/tabler.json +1 -0
- codefig/legend.py +483 -0
- codefig/lint.py +1737 -0
- codefig/lint_geom.py +528 -0
- codefig/node.py +864 -0
- codefig/notices.py +34 -0
- codefig/overlaps.py +392 -0
- codefig/policy.py +137 -0
- codefig/primitives.py +2721 -0
- codefig/py.typed +0 -0
- codefig/regions.py +727 -0
- codefig/render_svg.py +512 -0
- codefig/routing.py +932 -0
- codefig/shapes.py +642 -0
- codefig/solids.py +514 -0
- codefig/steps.py +203 -0
- codefig/styles.py +893 -0
- codefig/textutil.py +889 -0
- codefig/timeline.py +1288 -0
- codefig/tree.py +277 -0
- codefig/venues.py +93 -0
- codefig-0.17.0.dist-info/METADATA +542 -0
- codefig-0.17.0.dist-info/RECORD +44 -0
- codefig-0.17.0.dist-info/WHEEL +5 -0
- codefig-0.17.0.dist-info/licenses/LICENSE +21 -0
- codefig-0.17.0.dist-info/top_level.txt +1 -0
codefig/tree.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Tidy trees (v0.4.0): ``Tree(root, child, Tree(sub, leaf, leaf), …)``.
|
|
2
|
+
|
|
3
|
+
Nodes are any primitives (Box, Circle, Icon, Image, Group …). The layout is a
|
|
4
|
+
Reingold–Tilford style tidy tree: subtrees are packed as close as their *contours*
|
|
5
|
+
allow (not their bounding boxes), parents are centred over their children, and each
|
|
6
|
+
depth level gets one row (``direction="down"``) or column (``"right"``, also "up" /
|
|
7
|
+
"left"). Edges are real :class:`~codefig.edges.Fork` buses (``edges="ortho"``, the
|
|
8
|
+
TikZ / draw.io tree look) or straight :class:`~codefig.edges.Edge` lines between the
|
|
9
|
+
true outlines (``edges="straight"``), or none. A Tree is a :class:`Diagram`, so it nests
|
|
10
|
+
anywhere and other edges can reach its nodes.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
|
16
|
+
|
|
17
|
+
from .edges import Diagram, Edge, Fork
|
|
18
|
+
from .node import Node
|
|
19
|
+
|
|
20
|
+
_DIRS = ("down", "up", "right", "left")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _Spec:
|
|
24
|
+
__slots__ = ("node", "kids", "edge_kw")
|
|
25
|
+
|
|
26
|
+
def __init__(self, node: Node, kids: List["_Spec"], edge_kw: Optional[Dict[str, Any]] = None):
|
|
27
|
+
self.node = node
|
|
28
|
+
self.kids = kids
|
|
29
|
+
self.edge_kw = edge_kw or {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _as_spec(item: Any) -> _Spec:
|
|
33
|
+
if isinstance(item, Tree):
|
|
34
|
+
return item._spec
|
|
35
|
+
if isinstance(item, (tuple, list)):
|
|
36
|
+
if not item:
|
|
37
|
+
raise ValueError("empty subtree spec")
|
|
38
|
+
return _Spec(item[0], [_as_spec(k) for k in item[1:]])
|
|
39
|
+
if isinstance(item, Node):
|
|
40
|
+
return _Spec(item, [])
|
|
41
|
+
raise TypeError(f"Tree children must be nodes, Trees or (node, *children) tuples, got {type(item).__name__}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _TreeLayout(Node):
|
|
45
|
+
"""Layout-only content of a Tree: places every node; draws them (no frame)."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, tree: "Tree", theme: Any = None) -> None:
|
|
48
|
+
super().__init__(theme=theme)
|
|
49
|
+
self.tree = tree
|
|
50
|
+
self._nodes: List[Node] = []
|
|
51
|
+
for s, _p in tree._walk():
|
|
52
|
+
self._nodes.append(s.node)
|
|
53
|
+
s.node.parent = self
|
|
54
|
+
self._pos: Dict[int, Tuple[float, float]] = {} # node id → (breadth centre, depth start)
|
|
55
|
+
self._sizes: Dict[int, Tuple[float, float]] = {}
|
|
56
|
+
|
|
57
|
+
def children(self) -> Sequence[Node]:
|
|
58
|
+
return self._nodes
|
|
59
|
+
|
|
60
|
+
def invalidate(self) -> None:
|
|
61
|
+
super().invalidate()
|
|
62
|
+
for n in self._nodes:
|
|
63
|
+
n.invalidate()
|
|
64
|
+
|
|
65
|
+
def _bd(self, n: Node) -> Tuple[float, float]:
|
|
66
|
+
w, h = n.measure()
|
|
67
|
+
return (w, h) if self.tree.direction in ("down", "up") else (h, w)
|
|
68
|
+
|
|
69
|
+
def _measure(self) -> Tuple[float, float]:
|
|
70
|
+
t = self.tree
|
|
71
|
+
gap, lgap = t.gap, t.level_gap
|
|
72
|
+
# depth thickness per level
|
|
73
|
+
levels: Dict[int, float] = {}
|
|
74
|
+
|
|
75
|
+
def walk(s: _Spec, d: int) -> None:
|
|
76
|
+
b, dd = self._bd(s.node)
|
|
77
|
+
levels[d] = max(levels.get(d, 0.0), dd)
|
|
78
|
+
for k in s.kids:
|
|
79
|
+
walk(k, d + 1)
|
|
80
|
+
walk(t._spec, 0)
|
|
81
|
+
|
|
82
|
+
offs: Dict[int, float] = {}
|
|
83
|
+
|
|
84
|
+
def lay(s: _Spec, d: int) -> Tuple[Dict[int, float], Dict[int, Tuple[float, float]]]:
|
|
85
|
+
b, _ = self._bd(s.node)
|
|
86
|
+
pos: Dict[int, float] = {id(s.node): 0.0}
|
|
87
|
+
cont: Dict[int, Tuple[float, float]] = {d: (-b / 2, b / 2)}
|
|
88
|
+
if not s.kids:
|
|
89
|
+
return pos, cont
|
|
90
|
+
merged: Dict[int, Tuple[float, float]] = {}
|
|
91
|
+
placed: List[Tuple[float, Dict[int, float]]] = []
|
|
92
|
+
for k in s.kids:
|
|
93
|
+
kp, kc = lay(k, d + 1)
|
|
94
|
+
if not merged:
|
|
95
|
+
o = 0.0
|
|
96
|
+
else:
|
|
97
|
+
o = max(merged[dd][1] - kc[dd][0] + gap for dd in kc if dd in merged) \
|
|
98
|
+
if any(dd in merged for dd in kc) else 0.0
|
|
99
|
+
for dd, (l, r) in kc.items():
|
|
100
|
+
if dd in merged:
|
|
101
|
+
merged[dd] = (min(merged[dd][0], l + o), max(merged[dd][1], r + o))
|
|
102
|
+
else:
|
|
103
|
+
merged[dd] = (l + o, r + o)
|
|
104
|
+
placed.append((o, kp))
|
|
105
|
+
first, last = placed[0][0], placed[-1][0]
|
|
106
|
+
if t.align == "first":
|
|
107
|
+
c = first
|
|
108
|
+
else:
|
|
109
|
+
c = (first + last) / 2
|
|
110
|
+
for o, kp in placed:
|
|
111
|
+
for nid, v in kp.items():
|
|
112
|
+
pos[nid] = v + o - c
|
|
113
|
+
for dd, (l, r) in merged.items():
|
|
114
|
+
cont[dd] = (l - c, r - c) if dd != d else cont[dd]
|
|
115
|
+
return pos, cont
|
|
116
|
+
|
|
117
|
+
pos, cont = lay(t._spec, 0)
|
|
118
|
+
lo = min(l for l, _ in cont.values())
|
|
119
|
+
hi = max(r for _, r in cont.values())
|
|
120
|
+
depth_start: Dict[int, float] = {}
|
|
121
|
+
acc = 0.0
|
|
122
|
+
for d in sorted(levels):
|
|
123
|
+
depth_start[d] = acc
|
|
124
|
+
acc += levels[d] + lgap
|
|
125
|
+
total_depth = acc - lgap
|
|
126
|
+
|
|
127
|
+
self._nodes = []
|
|
128
|
+
self._pos = {}
|
|
129
|
+
|
|
130
|
+
def collect(s: _Spec, d: int) -> None:
|
|
131
|
+
self._nodes.append(s.node)
|
|
132
|
+
s.node.parent = self
|
|
133
|
+
b, dd = self._bd(s.node)
|
|
134
|
+
self._pos[id(s.node)] = (pos[id(s.node)] - lo, depth_start[d] + (levels[d] - dd) / 2)
|
|
135
|
+
for k in s.kids:
|
|
136
|
+
collect(k, d + 1)
|
|
137
|
+
collect(t._spec, 0)
|
|
138
|
+
self._extent = (hi - lo, total_depth)
|
|
139
|
+
bw, dw = hi - lo, total_depth
|
|
140
|
+
return (bw, dw) if t.direction in ("down", "up") else (dw, bw)
|
|
141
|
+
|
|
142
|
+
def _layout_children(self) -> None:
|
|
143
|
+
g = self.geom
|
|
144
|
+
t = self.tree
|
|
145
|
+
bw, dw = self._extent
|
|
146
|
+
for n in self._nodes:
|
|
147
|
+
w, h = n.measure()
|
|
148
|
+
bc, ds = self._pos[id(n)]
|
|
149
|
+
b, dd = self._bd(n)
|
|
150
|
+
if t.direction == "down":
|
|
151
|
+
n.layout(g.x + bc - w / 2, g.y + ds, w, h)
|
|
152
|
+
elif t.direction == "up":
|
|
153
|
+
n.layout(g.x + bc - w / 2, g.y + dw - ds - h, w, h)
|
|
154
|
+
elif t.direction == "right":
|
|
155
|
+
n.layout(g.x + ds, g.y + bc - h / 2, w, h)
|
|
156
|
+
else:
|
|
157
|
+
n.layout(g.x + dw - ds - w, g.y + bc - h / 2, w, h)
|
|
158
|
+
|
|
159
|
+
def draw(self, buf: List[str], ox: float = 0.0, oy: float = 0.0) -> None:
|
|
160
|
+
for n in self._nodes:
|
|
161
|
+
n.draw(buf, ox, oy)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class Tree(Diagram):
|
|
165
|
+
"""Tidy tree of arbitrary nodes.
|
|
166
|
+
|
|
167
|
+
``Tree(root, a, Tree(b, c, d), (e, f))`` — children are nodes, nested Trees or
|
|
168
|
+
``(node, *children)`` tuples. direction: down | up | right | left. gap: space
|
|
169
|
+
between neighbouring subtrees (contour to contour); level_gap: space between depth
|
|
170
|
+
levels. edges: "ortho" (one Fork bus per parent), "straight" (outline-to-outline
|
|
171
|
+
lines), "none". edge_style: Edge / Fork keyword dict (colour, width, style, head …;
|
|
172
|
+
trees default to no arrowheads). ``tree.style_edge(child, label=…, style="dashed")``
|
|
173
|
+
gives the edge into one child its own style / label. align: "center" (parent over the
|
|
174
|
+
middle of its children) or "first" (over the first child).
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
def __init__(self, root: Node, *children: Any, direction: str = "down",
|
|
178
|
+
gap: Optional[float] = None, level_gap: Optional[float] = None,
|
|
179
|
+
edges: str = "ortho", edge_style: Optional[Dict[str, Any]] = None,
|
|
180
|
+
align: str = "center", **kwargs: Any) -> None:
|
|
181
|
+
if direction not in _DIRS:
|
|
182
|
+
raise ValueError(f"direction must be one of {_DIRS}")
|
|
183
|
+
if edges not in ("ortho", "straight", "none"):
|
|
184
|
+
raise ValueError("edges must be ortho|straight|none")
|
|
185
|
+
if align not in ("center", "first"):
|
|
186
|
+
raise ValueError("align must be center|first")
|
|
187
|
+
self._spec = _Spec(root, [_as_spec(c) for c in children])
|
|
188
|
+
self.direction = direction
|
|
189
|
+
self.align = align
|
|
190
|
+
self.edge_mode = edges
|
|
191
|
+
self.edge_style = dict(edge_style or {})
|
|
192
|
+
theme = kwargs.get("theme")
|
|
193
|
+
self._gap_raw = None if gap is None else float(gap)
|
|
194
|
+
self._level_gap_raw = None if level_gap is None else float(level_gap)
|
|
195
|
+
super().__init__(_TreeLayout(self, theme), [], **kwargs)
|
|
196
|
+
self._rebuild_edges()
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def gap(self) -> float:
|
|
200
|
+
"""Space between neighbouring subtrees (default ``theme.gap``)."""
|
|
201
|
+
return self._gap_raw if self._gap_raw is not None else float(self.theme.gap)
|
|
202
|
+
|
|
203
|
+
@gap.setter
|
|
204
|
+
def gap(self, v: Optional[float]) -> None:
|
|
205
|
+
self._gap_raw = None if v is None else float(v)
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def level_gap(self) -> float:
|
|
209
|
+
"""Space between depth levels (default ``2.2 × theme.gap``)."""
|
|
210
|
+
return (self._level_gap_raw if self._level_gap_raw is not None
|
|
211
|
+
else float(self.theme.gap) * 2.2)
|
|
212
|
+
|
|
213
|
+
@level_gap.setter
|
|
214
|
+
def level_gap(self, v: Optional[float]) -> None:
|
|
215
|
+
self._level_gap_raw = None if v is None else float(v)
|
|
216
|
+
|
|
217
|
+
# ---- structure -------------------------------------------------------------
|
|
218
|
+
def _walk(self, s: Optional[_Spec] = None, parent: Optional[_Spec] = None):
|
|
219
|
+
s = s or self._spec
|
|
220
|
+
yield s, parent
|
|
221
|
+
for k in s.kids:
|
|
222
|
+
yield from self._walk(k, s)
|
|
223
|
+
|
|
224
|
+
def nodes(self) -> List[Node]:
|
|
225
|
+
return [s.node for s, _p in self._walk()]
|
|
226
|
+
|
|
227
|
+
def parent_of(self, node: Node) -> Optional[Node]:
|
|
228
|
+
for s, p in self._walk():
|
|
229
|
+
if s.node is node:
|
|
230
|
+
return p.node if p is not None else None
|
|
231
|
+
raise KeyError("node is not in this tree")
|
|
232
|
+
|
|
233
|
+
def style_edge(self, child: Node, **kw: Any) -> "Tree":
|
|
234
|
+
"""Own style / label for the edge into ``child`` (e.g. ``style="dashed", label="evicted"``)."""
|
|
235
|
+
for s, p in self._walk():
|
|
236
|
+
if s.node is child:
|
|
237
|
+
if p is None:
|
|
238
|
+
raise ValueError("the root has no incoming edge")
|
|
239
|
+
s.edge_kw.update(kw)
|
|
240
|
+
self._rebuild_edges()
|
|
241
|
+
return self
|
|
242
|
+
raise KeyError("node is not in this tree")
|
|
243
|
+
|
|
244
|
+
def _sides(self) -> Tuple[str, str]:
|
|
245
|
+
return {"down": ("bottom", "top"), "up": ("top", "bottom"),
|
|
246
|
+
"right": ("right", "left"), "left": ("left", "right")}[self.direction]
|
|
247
|
+
|
|
248
|
+
def _rebuild_edges(self) -> None:
|
|
249
|
+
out: List[Any] = []
|
|
250
|
+
if self.edge_mode == "none":
|
|
251
|
+
self.edges = out
|
|
252
|
+
return
|
|
253
|
+
base = {"head": "none"}
|
|
254
|
+
base.update(self.edge_style)
|
|
255
|
+
ps, cs = self._sides()
|
|
256
|
+
for s, _p in self._walk():
|
|
257
|
+
if not s.kids:
|
|
258
|
+
continue
|
|
259
|
+
if self.edge_mode == "straight":
|
|
260
|
+
for k in s.kids:
|
|
261
|
+
kw = dict(base, **k.edge_kw)
|
|
262
|
+
out.append(Edge(s.node.port("auto"), k.node.port("auto"), route="straight",
|
|
263
|
+
stub=0, **kw))
|
|
264
|
+
continue
|
|
265
|
+
labels = [k.edge_kw.get("label") for k in s.kids]
|
|
266
|
+
styles = [{x: v for x, v in k.edge_kw.items() if x != "label"} or None
|
|
267
|
+
for k in s.kids]
|
|
268
|
+
for st_, lab in zip(styles, labels): # branch labels sit beside the branch
|
|
269
|
+
if lab is not None and "label_side" not in st_:
|
|
270
|
+
st_["label_side"] = "auto"
|
|
271
|
+
out.append(Fork(s.node.port(ps), [k.node.port(cs) for k in s.kids],
|
|
272
|
+
labels=labels if any(l is not None for l in labels) else None,
|
|
273
|
+
leaf_styles=styles if any(styles) else None, **base))
|
|
274
|
+
self.edges = out
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
__all__ = ["Tree"]
|
codefig/venues.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Venue presets: final printed widths (inches) and figure rules of common templates.
|
|
2
|
+
|
|
3
|
+
Numbers come from the official LaTeX templates (see ``docs/concepts/themes-venues.md``
|
|
4
|
+
and the research notes ``figure_style_rules.md`` §4). Widths are in inches; codefig
|
|
5
|
+
lays out in points (1 in = 72 pt), so ``Figure(venue="icml", width="column")`` is
|
|
6
|
+
234 pt wide and every font size you see in the theme is the printed size.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Dict, Optional, Tuple, Union
|
|
13
|
+
|
|
14
|
+
PT_PER_IN = 72.0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class Venue:
|
|
19
|
+
"""A paper template: text / column width (inches) and figure constraints."""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
text: float
|
|
23
|
+
column: Optional[float] = None
|
|
24
|
+
body_pt: float = 10.0
|
|
25
|
+
font_rule: str = "type1|truetype"
|
|
26
|
+
min_font_pt: float = 6.0
|
|
27
|
+
min_stroke_pt: float = 0.5
|
|
28
|
+
strict_stroke: bool = False
|
|
29
|
+
no_title: bool = False
|
|
30
|
+
no_gray_text_bg: bool = False
|
|
31
|
+
bw_legible: bool = True
|
|
32
|
+
alt_text: bool = False
|
|
33
|
+
aliases: Tuple[str, ...] = field(default=())
|
|
34
|
+
|
|
35
|
+
def width_in(self, width: Union[str, float, None] = "text") -> float:
|
|
36
|
+
"""Width in inches for ``"text"`` / ``"column"`` / a fraction ≤ 1 of the
|
|
37
|
+
text width / an absolute number of inches (> 1)."""
|
|
38
|
+
if width is None or width == "text":
|
|
39
|
+
return self.text
|
|
40
|
+
if width == "column":
|
|
41
|
+
if self.column is None:
|
|
42
|
+
raise ValueError(f"venue {self.name!r} is single-column; use width='text' "
|
|
43
|
+
"or a fraction such as width=0.5")
|
|
44
|
+
return self.column
|
|
45
|
+
w = float(width)
|
|
46
|
+
if w <= 0:
|
|
47
|
+
raise ValueError("width must be positive")
|
|
48
|
+
return self.text * w if w <= 1.0 else w
|
|
49
|
+
|
|
50
|
+
def width_pt(self, width: Union[str, float, None] = "text") -> float:
|
|
51
|
+
return self.width_in(width) * PT_PER_IN
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
VENUES: Dict[str, Venue] = {
|
|
55
|
+
"neurips": Venue("neurips", 5.5, None, 10),
|
|
56
|
+
"iclr": Venue("iclr", 5.5, None, 10),
|
|
57
|
+
"icml": Venue("icml", 6.75, 3.25, 10, font_rule="type1", strict_stroke=True,
|
|
58
|
+
no_title=True, no_gray_text_bg=True),
|
|
59
|
+
"mlsys": Venue("mlsys", 6.75, 3.25, 10, font_rule="type1", strict_stroke=True,
|
|
60
|
+
no_title=True, no_gray_text_bg=True),
|
|
61
|
+
"vldb": Venue("vldb", 7.0, 3.33, 9, alt_text=True),
|
|
62
|
+
"sigmod": Venue("sigmod", 7.0, 3.33, 9, alt_text=True, aliases=("acm",)),
|
|
63
|
+
"osdi": Venue("osdi", 7.0, 3.335, 10, aliases=("usenix", "nsdi", "fast", "atc")),
|
|
64
|
+
"sosp": Venue("sosp", 7.0, 3.33, 10, alt_text=True),
|
|
65
|
+
}
|
|
66
|
+
_ALIASES = {a: v.name for v in VENUES.values() for a in v.aliases}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_venue(venue: Union[str, Venue, None]) -> Optional[Venue]:
|
|
70
|
+
"""``"icml"`` / ``"acm"`` / ``"usenix"`` / a :class:`Venue` → Venue (None → None)."""
|
|
71
|
+
if venue is None or isinstance(venue, Venue):
|
|
72
|
+
return venue
|
|
73
|
+
key = str(venue).lower()
|
|
74
|
+
key = _ALIASES.get(key, key)
|
|
75
|
+
if key not in VENUES:
|
|
76
|
+
names = sorted(set(VENUES) | set(_ALIASES))
|
|
77
|
+
raise ValueError(f"unknown venue {venue!r}; choose one of {names}")
|
|
78
|
+
return VENUES[key]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def resolve_width_pt(venue: Union[str, Venue, None], width: Union[str, float, None]) -> Optional[float]:
|
|
82
|
+
"""Final figure width in pt from ``venue`` + ``width`` (None if neither is given).
|
|
83
|
+
|
|
84
|
+
Without a venue, ``width`` must be a number of inches.
|
|
85
|
+
"""
|
|
86
|
+
v = get_venue(venue)
|
|
87
|
+
if v is not None:
|
|
88
|
+
return v.width_pt(width)
|
|
89
|
+
if width is None:
|
|
90
|
+
return None
|
|
91
|
+
if isinstance(width, str):
|
|
92
|
+
raise ValueError(f"width={width!r} needs venue=; or pass inches, e.g. width=3.3")
|
|
93
|
+
return float(width) * PT_PER_IN
|