Skip to content

Commit cf6c7fb

Browse files
committed
Added a Layer class, now each node can be assigned a layer (default=0).
1 parent d39d967 commit cf6c7fb

File tree

2 files changed

+198
-86
lines changed

2 files changed

+198
-86
lines changed

src/maxplotlib/subfigure/tikz_figure.py

Lines changed: 116 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,29 @@
1010
from maxplotlib.colors.colors import Color
1111
from maxplotlib.linestyle.linestyle import Linestyle
1212

13+
14+
class Layer:
15+
def __init__(self, label):
16+
self.label = label
17+
self.items = []
18+
def add(self, item):
19+
self.items.append(item)
20+
21+
def get_reqs(self):
22+
reqs = set()
23+
for item in self.items:
24+
if isinstance(item, Path):
25+
for node in item.nodes:
26+
reqs.add(node.layer)
27+
return reqs
28+
def generate_tikz(self):
29+
tikz_script = f"\n% Layer {self.label}\n"
30+
tikz_script += f"\\begin{{pgfonlayer}}{{{self.label}}}\n"
31+
for item in self.items:
32+
tikz_script += item.to_tikz()
33+
tikz_script += f"\\end{{pgfonlayer}}{{{self.label}}}\n"
34+
return tikz_script
35+
1336
class Node:
1437
def __init__(self, x, y, label="", content="", layer=0, **kwargs):
1538
"""
@@ -38,10 +61,10 @@ def to_tikz(self):
3861
options = ', '.join(f"{k.replace('_', ' ')}={v}" for k, v in self.options.items())
3962
if options:
4063
options = f"[{options}]"
41-
return f" \\node{options} ({self.label}) at ({self.x}, {self.y}) {{{self.content}}};\n"
64+
return f"\\node{options} ({self.label}) at ({self.x}, {self.y}) {{{self.content}}};\n"
4265

4366
class Path:
44-
def __init__(self, nodes, path_actions=[], cycle=False, layer=0, **kwargs):
67+
def __init__(self, nodes, path_actions=[], cycle=False, label="", layer=0, **kwargs):
4568
"""
4669
Represents a path (line) connecting multiple nodes.
4770
@@ -53,6 +76,7 @@ def __init__(self, nodes, path_actions=[], cycle=False, layer=0, **kwargs):
5376
self.path_actions = path_actions
5477
self.cycle = cycle
5578
self.layer = layer
79+
self.label = label
5680
self.options = kwargs
5781

5882
def to_tikz(self):
@@ -67,10 +91,10 @@ def to_tikz(self):
6791
options = ', '.join(self.path_actions) + ', ' + options
6892
if options:
6993
options = f"[{options}]"
70-
path_str = ' -- '.join(f"({node_label}.center)" for node_label in self.nodes)
94+
path_str = ' -- '.join(f"({node.label}.center)" for node in self.nodes)
7195
if self.cycle:
7296
path_str += ' -- cycle'
73-
return f" \\draw{options} {path_str};\n"
97+
return f"\\draw{options} {path_str};\n"
7498

7599
class TikzFigure:
76100
def __init__(self, **kwargs):
@@ -119,9 +143,10 @@ def add_node(self, x, y, label=None, content="", layer = 0, **kwargs):
119143
node = Node(x=x, y=y, label=label, content=content, **kwargs)
120144
self.nodes.append(node)
121145
if layer in self.layers:
122-
self.layers[layer].append(node)
146+
self.layers[layer].add(node)
123147
else:
124-
self.layers[layer] = [node]
148+
self.layers[layer] = Layer(layer)
149+
self.layers[layer].add(node)
125150
self._node_counter += 1
126151
return node
127152

@@ -141,20 +166,41 @@ def add_path(self, nodes, layer=0, **kwargs):
141166
raise ValueError("nodes parameter must be a list of node names.")
142167

143168
nodes = [
144-
node.label if isinstance(node, Node)
145-
else node if isinstance(node, str)
169+
node if isinstance(node, Node)
170+
else self.get_node(node) if isinstance(node, str)
146171
else ValueError(f"Invalid node type: {type(node)}")
147172
for node in nodes
148173
]
149174

150175
path = Path(nodes, **kwargs)
151176
self.paths.append(path)
152177
if layer in self.layers:
153-
self.layers[layer].append(path)
178+
self.layers[layer].add(path)
154179
else:
155-
self.layers[layer] = [path]
180+
self.layers[layer] = Layer(layer)
181+
self.layers[layer].add(path)
156182
return path
157-
183+
def get_node(self, node_label):
184+
for node in self.nodes:
185+
if node.label == node_label:
186+
return node
187+
def get_layer(self, item):
188+
for layer, layer_items in self.layers.items():
189+
if item in [layer_item.label for layer_item in layer_items]:
190+
return layer
191+
print(f'Item {item} not found in any layer!')
192+
193+
def add_tabs(self, tikz_script):
194+
tikz_script_new = ""
195+
tab_str = " "
196+
num_tabs = 0
197+
for line in tikz_script.split('\n'):
198+
if "\\end" in line:
199+
num_tabs = max(num_tabs - 1, 0)
200+
tikz_script_new += f"{tab_str*num_tabs}{line}\n"
201+
if "\\begin" in line:
202+
num_tabs += 1
203+
return tikz_script_new
158204
def generate_tikz(self):
159205
"""
160206
Generate the TikZ script for the figure.
@@ -163,23 +209,52 @@ def generate_tikz(self):
163209
- tikz_script (str): The TikZ script as a string.
164210
"""
165211
tikz_script = "\\begin{tikzpicture}\n"
212+
tikz_script += "% Define the layers library\n"
213+
layers = sorted([str(layer) for layer in self.layers.keys()])
214+
for layer in layers:
215+
tikz_script += f"\\pgfdeclarelayer{{{layer}}}\n"
216+
tikz_script += f"\\pgfsetlayers{{{','.join(layers)}}}\n"
166217

167218

168219
# Add grid if enabled
169220
if self._grid:
170221
tikz_script += " \\draw[step=1cm, gray, very thin] (-10,-10) grid (10,10);\n"
171-
172-
for key, layer_items in self.layers.items():
173-
tikz_script += f"\n % Layer {key}\n"
174-
for item in layer_items:
175-
tikz_script += item.to_tikz()
176-
# # Add nodes
177-
# for node in self.nodes:
178-
# tikz_script += node.to_tikz()
179-
180-
# # Add paths
181-
# for path in self.paths:
182-
# tikz_script += path.to_tikz()
222+
# def update_layer_order(layer, layer_order, buffered_layers):
223+
# reqs = layer.get_reqs()
224+
# if len(reqs) == 0:
225+
# layer_order.append(key)
226+
# buffered_layers.pop(key)
227+
# elif all([r in layer_order for r in reqs]):
228+
# layer_order.append(key)
229+
# buffered_layers.pop(key)
230+
# else:
231+
# buffered_layers.append(key)
232+
# return layer, layer_order, buffered_layers
233+
# Determine the order to print the layers
234+
ordered_layers = []
235+
buffered_layers = set()
236+
237+
for key, layer in self.layers.items():
238+
#layer_order, buffered_layers = update_layer_order(layer, layer_order, buffered_layers)
239+
reqs = layer.get_reqs()
240+
if all([r == layer.label for r in reqs]):
241+
ordered_layers.append(layer)
242+
elif all([r in [l.label for l in ordered_layers] for r in reqs]):
243+
ordered_layers.append(layer)
244+
else:
245+
buffered_layers.add(layer)
246+
247+
for buffered_layer in buffered_layers:
248+
buff_reqs = buffered_layer.get_reqs()
249+
print(buff_reqs)
250+
print([r in [l.label for l in ordered_layers] for r in buff_reqs], [l.label for l in ordered_layers])
251+
if all([r in [l.label for l in ordered_layers] for r in buff_reqs]):
252+
print('Move layer from buffer')
253+
ordered_layers.append(key)
254+
buffered_layers.remove(key)
255+
assert len(buffered_layers) == 0, f"Layer order is impossible for layer {[layer.label for layer in buffered_layers]}"
256+
for layer in ordered_layers:
257+
tikz_script += layer.generate_tikz()
183258

184259
tikz_script += "\\end{tikzpicture}"
185260

@@ -192,19 +267,10 @@ def generate_tikz(self):
192267
figure_env += f" \\label{{{self._label}}}\n"
193268
figure_env += "\\end{figure}"
194269
tikz_script = figure_env
195-
270+
tikz_script = self.add_tabs(tikz_script)
196271
return tikz_script
197272

198-
def compile_pdf(self, filename='output.pdf'):
199-
"""
200-
Compile the TikZ script into a PDF using pdflatex.
201-
202-
Parameters:
203-
- filename (str): The name of the output PDF file (default is 'output.pdf').
204-
205-
Notes:
206-
- Requires 'pdflatex' to be installed and accessible from the command line.
207-
"""
273+
def generate_standalone(self):
208274
tikz_code = self.generate_tikz()
209275

210276
# Create a minimal LaTeX document
@@ -215,6 +281,19 @@ def compile_pdf(self, filename='output.pdf'):
215281
f"{tikz_code}\n"
216282
"\\end{document}"
217283
)
284+
return latex_document
285+
286+
def compile_pdf(self, filename='output.pdf'):
287+
"""
288+
Compile the TikZ script into a PDF using pdflatex.
289+
290+
Parameters:
291+
- filename (str): The name of the output PDF file (default is 'output.pdf').
292+
293+
Notes:
294+
- Requires 'pdflatex' to be installed and accessible from the command line.
295+
"""
296+
latex_document = self.generate_standalone()
218297

219298
# Use a temporary directory to store the LaTeX files
220299
with tempfile.TemporaryDirectory() as tempdir:
@@ -254,9 +333,9 @@ def plot_matplotlib(self, ax):
254333

255334
# Plot paths first so they appear behind nodes
256335
for path in self.paths:
257-
x_coords = [next(node.x for node in self.nodes if node.label == label) for label in path.nodes]
258-
y_coords = [next(node.y for node in self.nodes if node.label == label) for label in path.nodes]
259-
336+
x_coords = [node.x for node in path.nodes]
337+
y_coords = [node.y for node in path.nodes]
338+
260339
# Parse path color
261340
path_color_spec = path.options.get('color', 'black')
262341
try:

tutorials/tutorial_02.ipynb

Lines changed: 82 additions & 49 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)