Skip to content

Loops

Repetitive structures — grids, chains, matrices of nodes — can be created in two ways: with Python loops that call add_node / draw many times, or with native TikZ \foreach loops via fig.add_loop(), which embeds the loop directly in the generated LaTeX instead of unrolling it in Python.

Benefits of TikZ \foreach loops: - the generated .tex file is much shorter and more readable - loop variable values are available as TikZ expressions (e.g. \i) at compile time - loops can be nested

This tutorial covers: - Python loop vs TikZ \foreach — side-by-side comparison - basic add_loop usage - loop variable in colour expressions - nested loops for a grid - add_variable for reusable TikZ constants - a practical chain-of-boxes diagram

from tikzfigure import TikzFigure

Python loop vs TikZ \foreach — a side-by-side comparison

Section titled “Python loop vs TikZ \foreach — a side-by-side comparison”

Both approaches produce the same visual output: a row of five circles. The key difference is in the generated LaTeX. The Python loop unrolls every node into a separate \node command, while add_loop emits a single compact \foreach block.

Python loop version:

fig = TikzFigure()
for i in range(5):
fig.add_node(
x=i * 2,
y=0,
shape="circle",
fill="blue!40",
content=str(i),
minimum_size="0.8cm",
)
fig.show()

Show Tikz code
print(fig)
% --------------------------------------------- %
% Tikzfigure generated by tikzfigure v0.2.1 %
% https://github.com/max-models/tikzfigure %
% --------------------------------------------- %
\begin{tikzpicture}
\node[shape=circle, fill=blue!40, minimum size=0.8cm] (node0) at ({0}, {0}) {0};
\node[shape=circle, fill=blue!40, minimum size=0.8cm] (node1) at ({2}, {0}) {1};
\node[shape=circle, fill=blue!40, minimum size=0.8cm] (node2) at ({4}, {0}) {2};
\node[shape=circle, fill=blue!40, minimum size=0.8cm] (node3) at ({6}, {0}) {3};
\node[shape=circle, fill=blue!40, minimum size=0.8cm] (node4) at ({8}, {0}) {4};
\end{tikzpicture}

TikZ \foreach version — same output, much shorter LaTeX:

fig = TikzFigure()
with fig.add_loop("i", range(5), comment="Row of circles") as loop:
loop.add_node(
x=r"2*\i",
y=0,
shape="circle",
fill="blue!40",
content=r"\i",
minimum_size="0.8cm",
)
fig.show()

Show Tikz code
print(fig)
% --------------------------------------------- %
% Tikzfigure generated by tikzfigure v0.2.1 %
% https://github.com/max-models/tikzfigure %
% --------------------------------------------- %
\begin{tikzpicture}
% Row of circles
\foreach \i in {0,1,2,3,4}{
\node[shape=circle, fill=blue!40, minimum size=0.8cm] () at ({2*\i}, {0}) {\i};
}
\end{tikzpicture}

Notice how \foreach \i in {0,1,2,3,4} replaces five separate \node commands.

fig.add_loop(var, iterable) returns a context manager. Inside the with block, use loop.add_node() and loop.draw(). The loop variable \i (or whatever name you choose) is available in any string passed as a coordinate or option.

fig = TikzFigure()
with fig.add_loop("i", range(6), comment="Row of circles") as loop:
loop.add_node(
x=r"2*\i",
y=0,
shape="circle",
fill="blue!40",
content=r"\i",
minimum_size="0.8cm",
)
fig.show()

Show Tikz code
print(fig)
% --------------------------------------------- %
% Tikzfigure generated by tikzfigure v0.2.1 %
% https://github.com/max-models/tikzfigure %
% --------------------------------------------- %
\begin{tikzpicture}
% Row of circles
\foreach \i in {0,1,2,3,4,5}{
\node[shape=circle, fill=blue!40, minimum size=0.8cm] () at ({2*\i}, {0}) {\i};
}
\end{tikzpicture}

TikZ colour mixing expressions like blue!\i0!red interpolate between two colours. Here \i ranges from 1 to 9, so \i0 gives 10, 20, … 90 — a smooth gradient from blue to red.

fig = TikzFigure()
with fig.add_loop("i", range(1, 10), comment="Colour gradient") as loop:
loop.add_node(
x=r"2*\i",
y=0,
shape="circle",
fill=r"blue!\i0!red",
content=r"\i",
minimum_size="1cm",
color="white",
font=r"\bfseries",
)
fig.show()

Show Tikz code
print(fig)
% --------------------------------------------- %
% Tikzfigure generated by tikzfigure v0.2.1 %
% https://github.com/max-models/tikzfigure %
% --------------------------------------------- %
\begin{tikzpicture}
% Colour gradient
\foreach \i in {1,2,3,4,5,6,7,8,9}{
\node[shape=circle, color=white, fill=blue!\i0!red, minimum size=1cm, font=\bfseries] () at ({2*\i}, {0}) {\i};
}
\end{tikzpicture}

Use a second loop.add_loop() inside the first to create a 2-D grid. The inner loop variable \j is independent of the outer \i.

fig = TikzFigure()
with fig.add_loop("i", range(5), comment="Outer loop (columns)") as loop_i:
with loop_i.add_loop("j", range(4), comment="Inner loop (rows)") as loop_j:
loop_j.add_node(
x=r"2*\i",
y=r"2*\j",
shape="rectangle",
fill="orange!30",
content=r"(\i,\j)",
minimum_size="0.9cm",
)
fig.show()

Show Tikz code
print(fig)
% --------------------------------------------- %
% Tikzfigure generated by tikzfigure v0.2.1 %
% https://github.com/max-models/tikzfigure %
% --------------------------------------------- %
\begin{tikzpicture}
% Outer loop (columns)
\foreach \i in {0,1,2,3,4}{
% Inner loop (rows)
\foreach \j in {0,1,2,3}{
\node[shape=rectangle, fill=orange!30, minimum size=0.9cm] () at ({2*\i}, {2*\j}) {(\i,\j)};
}
}
\end{tikzpicture}

The generated TikZ is a compact nested \foreach — compare that with what 20 separate \node commands would look like.

fig.add_variable(name, value) emits a \pgfmathsetmacro declaration at the top of the picture. Use it to store a radius, spacing, or any numeric constant that you want to be able to change in one place.

Here 12 nodes are placed on a circle of radius \radius. Python computes the trigonometric coefficients; TikZ multiplies them by \radius at compile time.

import math
fig = TikzFigure()
fig.add_variable("radius", 3)
N = 12
for i in range(N):
frac = round(i / N, 4)
angle = frac * 2 * math.pi
fig.add_node(
x=f"{{\\radius*{math.cos(angle):.4f}}}",
y=f"{{\\radius*{math.sin(angle):.4f}}}",
shape="circle",
fill="teal!50",
content=str(i + 1),
minimum_size="0.7cm",
color="white",
font=r"\small",
)
fig.show()

Show Tikz code
print(fig)
% --------------------------------------------- %
% Tikzfigure generated by tikzfigure v0.2.1 %
% https://github.com/max-models/tikzfigure %
% --------------------------------------------- %
\begin{tikzpicture}
\pgfmathsetmacro{\radius}{3}
\node[shape=circle, color=white, fill=teal!50, minimum size=0.7cm, font=\small] (node0) at ({{\radius*1.0000}}, {{\radius*0.0000}}) {1};
\node[shape=circle, color=white, fill=teal!50, minimum size=0.7cm, font=\small] (node1) at ({{\radius*0.8661}}, {{\radius*0.4998}}) {2};
\node[shape=circle, color=white, fill=teal!50, minimum size=0.7cm, font=\small] (node2) at ({{\radius*0.4998}}, {{\radius*0.8661}}) {3};
\node[shape=circle, color=white, fill=teal!50, minimum size=0.7cm, font=\small] (node3) at ({{\radius*0.0000}}, {{\radius*1.0000}}) {4};
\node[shape=circle, color=white, fill=teal!50, minimum size=0.7cm, font=\small] (node4) at ({{\radius*-0.4998}}, {{\radius*0.8661}}) {5};
\node[shape=circle, color=white, fill=teal!50, minimum size=0.7cm, font=\small] (node5) at ({{\radius*-0.8661}}, {{\radius*0.4998}}) {6};
\node[shape=circle, color=white, fill=teal!50, minimum size=0.7cm, font=\small] (node6) at ({{\radius*-1.0000}}, {{\radius*0.0000}}) {7};
\node[shape=circle, color=white, fill=teal!50, minimum size=0.7cm, font=\small] (node7) at ({{\radius*-0.8661}}, {{\radius*-0.4998}}) {8};
\node[shape=circle, color=white, fill=teal!50, minimum size=0.7cm, font=\small] (node8) at ({{\radius*-0.4998}}, {{\radius*-0.8661}}) {9};
\node[shape=circle, color=white, fill=teal!50, minimum size=0.7cm, font=\small] (node9) at ({{\radius*-0.0000}}, {{\radius*-1.0000}}) {10};
\node[shape=circle, color=white, fill=teal!50, minimum size=0.7cm, font=\small] (node10) at ({{\radius*0.4998}}, {{\radius*-0.8661}}) {11};
\node[shape=circle, color=white, fill=teal!50, minimum size=0.7cm, font=\small] (node11) at ({{\radius*0.8661}}, {{\radius*-0.4998}}) {12};
\end{tikzpicture}

Changing fig.add_variable("radius", 5) would scale the entire circle without touching any node code.

Remember the heart shape from the styling tutorial? It can be drawn with a single \draw command

width, height = 1.75, 2.0
fig = TikzFigure()
fig.colorlet("lightred", "red!40!white")
fig.add_variable("width", 1.75)
fig.add_variable("height", 2.0)
A = fig.add_node(r"-\width", r"\height")
B = fig.add_node(0, 0)
C = fig.add_node(r"\width", r"\height")
D = fig.add_node(0, r"\height")
fig.draw(
A.to(B, options=["out=-90, in=135"])
.to(C, options=["out=45, in=-90"])
.to(D, options=["in=80, out=100"])
.to(A, options=["in=80, out=100"]),
color="red",
line_width=4,
cycle=True,
center=True,
fill="lightred",
)
fig.show()

Practical example: a chain of labelled boxes

Section titled “Practical example: a chain of labelled boxes”

A common diagram pattern is a horizontal chain of labelled boxes connected by arrows. Using a \foreach loop keeps the LaTeX concise even for long chains.

steps = ["Load", "Clean", "Transform", "Model", "Evaluate"]
fig = TikzFigure()
# Draw boxes via a TikZ foreach over a comma-separated list of indices
with fig.add_loop("i", range(len(steps)), comment="Pipeline steps") as loop:
loop.add_node(
x=r"3*\i",
y=0,
shape="rectangle",
fill="blue!20",
draw="blue!60",
minimum_width="2.2cm",
minimum_height="0.8cm",
rounded_corners="4pt",
line_width=1.5,
)
# Add step labels in Python (strings can't live inside \foreach easily)
for i, step in enumerate(steps):
fig.add_node(
x=i * 3,
y=0,
content=step,
draw="none",
font=r"\small\bfseries",
)
# Draw arrows between boxes
for i in range(len(steps) - 1):
fig.draw(
[(i * 3 + 1.1, 0), (i * 3 + 1.9, 0)],
arrows="->",
line_width=1.5,
color="blue!60",
)
fig.show()

Show Tikz code
print(fig)
% --------------------------------------------- %
% Tikzfigure generated by tikzfigure v0.2.1 %
% https://github.com/max-models/tikzfigure %
% --------------------------------------------- %
\begin{tikzpicture}
% Pipeline steps
\foreach \i in {0,1,2,3,4}{
\node[shape=rectangle, fill=blue!20, draw=blue!60, minimum width=2.2cm, minimum height=0.8cm, line width=1.5, rounded corners=4pt] () at ({3*\i}, {0}) {};
}
\node[draw=none, font=\small\bfseries] (node0) at ({0}, {0}) {Load};
\node[draw=none, font=\small\bfseries] (node1) at ({3}, {0}) {Clean};
\node[draw=none, font=\small\bfseries] (node2) at ({6}, {0}) {Transform};
\node[draw=none, font=\small\bfseries] (node3) at ({9}, {0}) {Model};
\node[draw=none, font=\small\bfseries] (node4) at ({12}, {0}) {Evaluate};
\draw[color=blue!60, line width=1.5, arrows=->] (1.1, 0) to (1.9, 0);
\draw[color=blue!60, line width=1.5, arrows=->] (4.1, 0) to (4.9, 0);
\draw[color=blue!60, line width=1.5, arrows=->] (7.1, 0) to (7.9, 0);
\draw[color=blue!60, line width=1.5, arrows=->] (10.1, 0) to (10.9, 0);
\end{tikzpicture}