Tikz Template for Mathematical Figures: Example with Double Number Lines

Tags: Tikz

(Español: Plantilla de Tikz para crear diagramas matemáticos: ejemplo de rectas numéricas dobles)

In this post I explain how I built a system using TikZ (a computer language for drawing mathematical figures rather than dragging shapes around) to produce figures efficiently and in a way that is easy to maintain. The system is a template – a bit of reusable structure – for making double number line diagrams, which are a visual tool used in many modern mathematics resources to aid in learning proportional reasoning (think of percentages, ratios, or recipes).

The goal of this post is to show how these diagrams can be produced “automatically”, using code that is short, flexible, and produces figures which are visually consistent, so that the focus can stay on the mathematics being illustrated and learned. Consistent visual structure supports learners and mathematical thinking.

To illustrate what such a diagram achieves, consider the following example:

What is 75% if 100% calcium is 1200mg?

The idea is that the diagram helps understand what the answer is visually by matching the 75% mark on one line with the correct value on the other.

And here is what the code for that figure looks like, using the template:

\begin{tikzpicture}

% Parameters
\def\lineWide{6.3cm} % width of line (not including label)
\def\numMarks{5}
\def\topMarkLabels{0,300,...,1200} 
\def\topLineLabel{calcium (mg)}
\def\bottomMarkLabels{0\%, 25\%, 50\%,75\%, 100\%} 
\def\bottomLineLabel{}

% Draw double number line
% creates several coordinates like (top-leftEnd) and (bot-mark-2). See details in template.
\doubleNumberLine{\lineWide}{\numMarks}{\topMarkLabels}{\topLineLabel}{\bottomMarkLabels}{\bottomLineLabel}

% box around some top and bottom marks
\node[fit=(top-mark-4) (bot-mark-4), draw, rectangle, rounded corners=1.5ex, inner sep=2.7ex, densely dashed] {};

\end{tikzpicture}

You do not need to understand all of this code to follow the rest of the post. The main point is that once the template is defined, producing diagrams like this becomes fast and easy: just change a few lines, and a new figure appears, properly spaced, with all texts and tick marks properly placed.

Before going on, give this a try! No coding skills required!

  • How would you adjust the code above to change calcium into magnesium in the figure?
  • What would you change to get the top tick marks to be 0,500,1000,1500,2000?

A well-constructed template effectively separates what varies (the content of the diagram) from what stays the same (the structure and layout). The diagrams all follow a shared design, so they look coherent, and only the content changes.

Who This Is For

This post is meant for anyone interested in how mathematical diagrams are produced at scale. I hope it is an interesting read even if you just know that TikZ is a computer language for drawing mathematical figures (and if interested, you can read more about it here).

In this post I try to show the logic behind the template, by showing the step-by-step decisions involved in building it. Even if you never write TikZ code yourself, you may still find it helpful to see how diagrams can be “templated” in a principled way. If you are already familiar with TikZ, you may find ideas here for improving reusability and consistency.

The emphasis is not just on drawing one figure, but on producing a system for drawing many related figures, in a way that keeps them consistent, understandable, and easy to adapt. This is particularly useful when materials are developed collaboratively or reused over time.

Here are two examples that are sized identically (length of the number lines), even though their content differs:


Step-by-Step: Building the Template

This is a walkthrough of how the template is constructed. Each step illustrates a decision that allows one to separate the structure and visual identity from the content.

What the Template Aims to Solve

When making double number line diagrams, one often encounters several problems:

  • Long labels make the diagrams wider than the available width, so one needs to shorten the number lines to prevent the entire figure from being scaled down (which produces inconsistent font sizes and line widths).
  • These line length adjustments snowball into adjusting tick mark placements and placement of their labels, which can become time consuming (and repetitive).
  • If the number of tick marks needs to be changed, the positions of all the tick marks and their labels need to be adjusted.
  • If using code, it gets messy if reused or copied. If using graphical software, all these individual tweaks become cumbersome, repetitive, and time consuming.

The template addresses all of these issues by automating them all, while at the same time enforcing a few design principles:

  • Text appears at the same size across diagrams by allowing the number line length (excluding labels) to be explicitly controllable, so that figures can be adjusted to fit on a page.
  • Number line labels sit at a fixed, consistent distance to the left.
  • Tick marks are evenly spaced.
  • Tick labels are centered above or below ticks, at consistent vertical spacing.
  • Code is clean and readable, so it is easy to adapt.

Step 1: Draw the Line

Assuming we have a target width, say \def\lineWide{6.3cm}, we can draw the line as:

\def\lineWide{6.3cm}
\coordinate (leftEnd) at (0, 0);
\coordinate (rightEnd) at (\lineWide, 0);
\draw[->, >=stealth] (leftEnd) -- (rightEnd);

Here:

  • The \coordinate commands name the endpoints of the number line.
  • The \draw command draws the line (leftEnd) -- (rightEnd) between the two points.
  • The [->, >=stealth] are draw options:
    • -> says to draw an arrowhead at the end
    • >=stealth says to use as arrowhead, the “stealth” style.

The result is the following figure:

Step 2: Add the Line Label

The label to the left of the line should be consistently placed, regardless of text length. The following works well:

\node[anchor=east, xshift=-0.15cm] at (leftEnd) {label goes here};

The anchor=east aligns the right edge of the label with (leftEnd), and the xshift moves it slightly to the left. This keeps the label visually aligned, even if the text changes. It looks like this:

To separate the piece of code that adds the label and specifies its placement from the actual text of the label (separate content from style and structure), one can define \def\lineLabel with the label text and then use it. Like this:

\def\lineLabel{label goes here}
\node[anchor=east, xshift=-0.15cm] at (leftEnd) {\lineLabel};

Step 3: Add Tickmarks

To avoid crowding the endpoints, we leave some space on each side. The marks are placed using a loop.

This is an example for 5 ticks:

\def\numLineExtra{0.3cm}

% draw ticks as nodes (mark-n). 1st mark is (mark-1).
\foreach \x in {1,...,5}{%
    \node[draw] (mark-\x) at ({\numLineExtra+(\x-1)*(\lineWide-2.5\numLineExtra)/(5-1)},0) {};
}

It looks like this:

The code places \nodes with empty text {} at the equally spaced points (mark-1),…,(mark-5) leaving \numLineExtra space to the left and right in the number line (actually, a bit more to the right). The node commands have the optional [draw] which draws the edges of the node. Naming the nodes as (mark-1),…,(mark-5) is useful to later be able to place things around them, like tickmark labels.

Step 4: Style the Tickmarks

Now we need to style those nodes so that they actually look like tickmarks and not like little squares! This is done by defining a TikZ style, and then declaring those nodes to have that style.

This is the style definition:

% Define the "numLineTick" style
numLineTick/.style={
  draw,               % Draw tickmark node
  minimum width=0pt,  % No width for the node box
  minimum height=7pt, % Height of the tickmark
  line cap=round,     % Rounded ends for ticks
  inner sep=0pt,      % No inner padding
  outer sep=2pt,      % Space around the tick
  line width=0.35pt,  % Thickness of the tick line
},

To apply it, use the style instead of [draw] for the nodes:

% draw ticks as nodes (mark-n). 1st mark is (mark-1).
\foreach \x in {1,...,5}{%
    \node[numLineTick] (mark-\x) at ({\numLineExtra+(\x-1)*(\lineWide-2.5\numLineExtra)/(5-1)},0) {};
}

Result:

Step 5: Generalize the Tick Count

But of course, part of the point of the template is that it should be able to create any number of ticks. By how this has been coded already, this is not hard: the 5 can be replaced with a parameter \numMarks, and the code will do everything automatically!

% draw ticks as nodes (mark-n). 1st mark is (mark-1).
\foreach \x in {1,...,\numMarks}{%
    \node[numLineTick] (mark-\x) at ({\numLineExtra+(\x-1)*(\lineWide-2.5\numLineExtra)/(\numMarks-1)},0) {};
}

For example, setting \def\numMarks{9}, the code produces 9 equally spaced tickmarks:

Step 6: Add Tick Labels

With named tick nodes, labeling is simple. For example:

\node[above,font=\small] at (mark-4) {above};
\node[below,font=\small] at (mark-4) {(mark-4)};

Or with a loop, and adjusting the distance for the above:

\foreach \num [count=\i] in {0,20,...,100} {
    \node[above=3pt,font=\scriptsize] at (mark-\i) {\num\%};
}

Even better (thinking about a template), one can define the labels as a separate macro and then loop over the labels:

\def\labels{0\%,20\%, , 60\% , ,100\%}
\foreach \label [count=\i] in \labels {
    \node[above=3pt,font=\scriptsize] at (mark-\i) {\label};
}

Notice how easy it was to get empty labels with the comma-separated the list. It is useful to be able to leave empty labels in these sorts of diagrams with learning resources in mind.

Step 7: Define Styles for Labels

Again, trying to remove unnecessary repetition and making this easy to maintain/adjust in the future, the style and placement of the labels should be factored out into a TikZ style. Two different styles, for labels above or below the tickmarks:

tickLabelAbove/.style={
    font=\scriptsize,   % Font size for tick labels
    anchor=mid,         % Baseline align text vertically across nodes
    yshift=2.0ex,       % Vertical offset for labels above the line
},
tickLabelBelow/.style={
    font=\scriptsize,   % Font size for tick labels
    anchor=mid,         % Baseline align text vertically across nodes
    yshift=-2.3ex,      % Slightly larger offset below the line to account 
                        % for the label's baseline alignment (ensures visual balance).
},

Then, the code simplifies to:

\def\labels{0\%,20\%, , 60\% , ,100\%}
\foreach \label [count=\i] in \labels {
    \node[tickLabelAbove] at (mark-\i) {\label};
}

Final Template: \doubleNumberLine

What the above has accomplished is to separate all structure from content. We can declare the parameters for a specific figure, as:

\def\lineWide{6.3cm} % width of line (not including label)
\def\numMarks{5}
\def\topMarkLabels{0,300,...,1200} 
\def\topLineLabel{calcium (mg)}
\def\bottomMarkLabels{0\%, 25\%, 50\%,75\%, 100\%} 
\def\bottomLineLabel{}

and define the template code elsewhere (e.g., preamble or a separate file). The following code defines the command \doubleNumberLine which will take care of all the drawing:

\newcommand{\doubleNumberLine}[6]{%
% Macro to draw double number line with labeled ticks and labels at the left
% Command creates several coordinates:
% - Endpoints for top and bottom line: (top-leftEnd), (top-rightEnd), (bot-leftEnd), (bot-rightEnd)
% - Nodes of top marks: (top-mark-1), ...,(top-mark-\numMarks)
% - Nodes of bottom marks: (bot-mark-1), ...,(bot-mark-\numMarks)
% 
%   Parameters:
%   #1: Number line lengths (without the label)
%   #2: Number of marks
%   #3: Top mark labels (comma-separated)
%   #4: Top line label (placed at the left)
%   #5: Bottom mark labels (comma-separated)
%   #6: Bottom line label (placed at the left)

   % Internal constants
  \def\numLineExtra{0.3cm} % Hardcoded extra space at the ends
  
  %%% TOP number line
  \begin{scope}
    % Coordinates of endpoints
    \coordinate (top-leftEnd) at (0, 0);
    \coordinate (top-rightEnd) at (#1, 0);

    % Draw the number line and add label to the left
    \draw[->, >=stealth] (top-leftEnd) -- (top-rightEnd);
    \node[anchor=east, xshift=-1.5ex] at (top-leftEnd) {#4};

    % draw ticks as nodes (top-mark-n). 1st mark is (top-mark-1).
    \foreach \x in {1,...,#2}{%
        \node[numLineTick] (top-mark-\x) at ({\numLineExtra+(\x-1)*(#1-2.5*\numLineExtra)/(#2-1)},0) {};
    }

    % add mark labels
    \foreach \num [count=\i] in #3 {
        \node[tickLabelAbove] at (top-mark-\i) {\num};
    }
  \end{scope}

  %%% Bottom number line
  \begin{scope}[yshift=-6ex]
    % Coordinates of endpoints
    \coordinate (bot-leftEnd) at (0, 0);
    \coordinate (bot-rightEnd) at (#1, 0);

    % Draw the number line and add label to the left
    \draw[->, >=stealth] (bot-leftEnd) -- (bot-rightEnd);
    \node[anchor=east, xshift=-1.5ex] at (bot-leftEnd) {#6};

    % draw ticks as nodes (bot-mark-n). 1st mark is (bot-mark-1).
    \foreach \x in {1,...,#2}{%
        \node[numLineTick] (bot-mark-\x) at ({\numLineExtra+(\x-1)*(#1-2.5*\numLineExtra)/(#2-1)},0) {};
    }

    % add mark labels
    \foreach \num [count=\i] in #5 {
        \node[tickLabelBelow] at (bot-mark-\i) {\num};
    }
  \end{scope}
}

and the following code has all the factored-out styles which are used by the \doubleNumberLine:

%  TikZ Styles for Number Lines
\tikzset{
  tickLabelAbove/.style={
      font=\small,        % Font size for tick labels
      anchor=mid,         % Baseline align text vertically across nodes
      yshift=2.0ex,       % Vertical offset for labels above the line
  },
  tickLabelBelow/.style={
      font=\small,        % Font size for tick labels
      anchor=mid,         % Baseline align text vertically across nodes
      yshift=-2.3ex,      % Slightly larger offset below the line to account 
                          % for the label's baseline alignment (ensures visual balance).
  },
  numLineTick/.style={
      draw,               % Draw tickmark node
      minimum width=0pt,  % No width for the node box
      minimum height=7pt, % Height of the tickmark
      line cap=round,     % Rounded ends for ticks
      inner sep=0pt,      % No inner padding
      outer sep=2pt,      % Space around the tick
      line width=0.35pt,  % Thickness of the tick line
  },
}

With this, tiny codes can produce consistently styled figures.

Examples

The single line

% \begin{tikzpicture}
\doubleNumberLine{6.3cm}{7}{0,1,...,6}{number of books}{\$0, \$15, \$30, \$45, \$60, \$75, \$90}{total cost}
% \end{tikzpicture}

produces the following figure:

The single line

% \begin{tikzpicture}
\doubleNumberLine{5cm}{5}{0,50,,,200}{number of cards}{0\%, 10\%, , , ? }{percentage}
% \end{tikzpicture}

gives

Making the Template Easier to Use

It is nice to specify double number lines with a single line of code. However, brevity is not the same as usability. In fact, it is not easy for people to remember what all the 6 {}{}{}{}{}{} in \doubleNumberLine mean, so it is better to use the following template code, which invokes \doubleNumberLine and makes transparent the meaning of each {}{}{}{}{}{}. More lines of code, but much easier to use.

\begin{tikzpicture}
% Parameters
\def\lineWide{3in} % width of line (not including label)
\def\numMarks{7}
\def\topMarkLabels{0,1,...,6} 
\def\topLineLabel{number of books}
\def\bottomMarkLabels{\$0, \$15, \$30, \$45, \$60, \$75, \$90} 
\def\bottomLineLabel{total cost}

% Draw double number line
% creates several coordinates like (top-leftEnd) and (bot-mark-2). See details in template.
\doubleNumberLine{\lineWide}{\numMarks}{\topMarkLabels}{\topLineLabel}{\bottomMarkLabels}{\bottomLineLabel}
\end{tikzpicture}

Full example, with code

Here is a full self-contained example, including all the code in a single file (link to the TikZ gallery). It works out of the box. To use as a template, all the styles and the definition of the macro \doubleNumberLine should be moved into a .sty file, or a separate latex file included in the preamble with the include command (like in this Overleaf link).

(Figure created with TikZ. Source code here.)

Further work

Number of tickmarks

The macro \doubleNumberLine could infer \numMarks from the length of \topMarkLabels. This is not trivial because of how \LaTeX expands things, but it can be done.

For example, one could add the following code close to the start of the code of doubleNumberLine:

% Figure out the number of marks from #3 and store in \numMarks
\pgfmathsetmacro{\numMarks}{0}% 
\foreach \i in #3{%
  \pgfmathtruncatemacro{\numMarks}{\numMarks+1}% 
  \global\let\numMarks\numMarks%
}%

This would eliminate one input from \doubleNumberLine, but I am still undecided if removing the explicitness is worth the more complex LaTeX code (due to the use of \global and \pgfmathtruncatemacro). Finding even one reliable version of this code was not trivial. Many trials failed, which may imply that this code may be harder to maintain through changes in TikZ and LaTeX.

Streamlining the parameters

To address the six positional arguments, a key-value interface is friendlier, and can probably be developed with pgfkeys. So, calling the template may look like:

\doubleNumberLine[
  length=7.6cm,
  topmarks={0,50,...,200},
  toplabel={calcium (mg)},
  bottommarks={},
  bottomlabels={0\%,25\%,50\%,75\%,100\%}
]

Subscribe

Want to get an email when a new post is added? If so, subscribe here.