chrisphan.com

Plotting with TikZ, Part III: Plotting a function defined by a formula, and plotting functions that go through certain points

An analog clock reading 3:412019-05-13 / 2019-W20-1T15:41:00-05:00 / 0x5cd9d65c

Categories: TikZ ist kein Zeichenprogramm, indeed, LaTeX, math

In the last post, I described how I use TikZ to create a graph that might otherwise be created freehand:

A plot of a real-valued function on the real numbers inclusively between -5 and 5, with the exception of -2. The function is continuous except that there is a removable discontinuity, a vertical asymptote, and a jump discontinuity

In this post, I will describe one method for using TikZ to to plot a function defined by a formula, such as \[y = \left(2x^2 - x - 1\right)e^{-x}.\] Then I will show two ways to make a function go through a certain point.

Plotting from a formula

Simple example

There are a number of ways to achieve this, and PGF actually includes the functionality to perform calculations in TeX. (For example, I have used the PGF pseudorandom number generator in graphics. Someone even made TikZ code to generate a random city skyline.) However, for efficiency sake, I prefer to have the calculations done outside TeX, in a more efficient computer language.

The trick is to use the gnuplot table import format. You simply have to generate a text file with a list of coordinates in the following form:

-1.250000 11.779907
-1.240000 11.456050
-1.230000 11.138839

This file contains the coordinates \((-1.25, 11.779907)\), \((-1.24, 11.456050)\), \((-1.23, 11.138839)\).

There are a number of ways to generate such a file. Since I like Python, I used the following (simple) Python script:

Python
1
2
3
4
5
6
7
8
9
10
11
#! /usr/bin/env python3

import numpy as np

f = lambda x: (2*x**2 - x - 1)*np.exp(-x)

xvals = np.arange(-1.25, 10.25, 0.01)

with open("plot1.table", "wt") as outfile:
    for x in xvals:
        outfile.write("{:f} {:f}\n".format(x, f(x)))

Now, this curve can be imported into your TikZ illustration using \draw [. . .] plot; in this case:

LaTeX
% [...]
\draw[thick, blue, <->] plot[smooth] file {plot1.table};
% [...]

Here is a complete example (with coordinate axes and everything):

LaTeX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
\documentclass[border=0.25cm]{standalone}

\usepackage{tikz}
\usepackage{fouriernc}

\begin{document}

\begin{tikzpicture}[yscale=0.5]

      \draw[gray] (-1.25, -1.25) grid (10.25, 12.25);

      % x-axis
      \draw[thick, black, ->] (-1.25, 0) -- (10.25, 0)
        node[anchor=south west] {$x$};

      % x-axis tick marks
      \foreach \x in {-1, 10}
        \draw[thick] (\x, 0.2) -- (\x, -0.2)
          node[anchor=north] {\(\x\)};

      % y-axis
      \draw[thick, black, ->] (0, -1.25) -- (0, 12.25)
        node[anchor=south west] {$y$};

      % y-axis tick marks
      \foreach \y in {-1, 12}
        \draw[thick] (-0.1, \y) -- (0.1, \y)
          node[anchor=west] {\(\y\)};

      % graph of function
      \draw[thick, blue, <->] plot[smooth] file {plot1.table};

\end{tikzpicture}
\end{document}

And here is the output:

plot of y = (2 x^2 - x - 1) e^(-x) from -1 to 10

In my Python script above, I carefully chose the set of inputs \(x\) (np.arange(-1.25, 10.25, 0.01), i.e., every real number of the form \(-1.25 + 0.01k, \; k \in \mathbb{Z}\) in the interval \([-1.25, 10.25)\)) to keep \(f(x) < 12\). This can also be handled computationally.

Example involving asymptotes

For example, suppose I want to plot \(y = r(x)\), where \[r(x) = \frac{6x^3 + 21x^2 - 21x - 36}{3x^3 + 3x^2 - 12x - 12},\] with \(-15.5 \leq x \leq 15.5\). Let's additionally suppose I want to only see \(y\)-values with \(-15.5 \leq y \leq 15.5\). (In other words, the "viewing rectangle" will be \([-15.5, 15.5] \times [-15.5, 15.5]\).)

Factoring the numerator and denominator, we have:

\[r(x) = \frac{6x^3 + 21x^2 - 21x - 36}{3x^3 + 3x^2 - 12x - 12}\] \[\phantom{r(x)}= \frac{\left(x + 1\right) \left(2 x - 3\right) \left(3 x + 12\right)}{\left(x + 1\right) \left(x + 2\right) \left(3 x - 6\right)}\] \[\phantom{r(x)}= \frac{\left(2 x - 3\right) \left(3 x + 12\right)}{\left(x + 2\right) \left(3 x - 6\right)}, \; x \not = -1.\]

As you can see, we will have vertical asymptotes at \(x = -2\) and \(x = 2\), and a hole at \(x = -1\). There will also be a horizontal asymptote at \(y = 2\).

To handle the asymptotes, we plot the function \[\hat r(x) = \frac{\left(2 x - 3\right) \left(3 x + 12\right)}{\left(x + 2\right) \left(3 x - 6\right)}\] along three intervals:

(Actually, for each of the above intervals \(I\), we plot something approximating \(\left\{(x, \hat r(x)): x \in I \text{ and } |\hat r(x)|\leq 16\right\}\).)

Here is the Python script:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#! /usr/bin/env python3

import numpy as np

r = lambda x: (2*x - 3)*(3*x + 12)/((x + 2)*(3*x - 6))

xvals = [
    np.arange(-15.5, -2, 0.01),
    np.arange(-2.01, 2, 0.01),
    np.arange(2.01, 15.6, 0.01)
    ]

for idx, interval in enumerate(xvals):
    with open("plot2_{}.table".format(idx), "wt") as outfile:
        for x in interval:
            if abs(r(x)) <= 16:
                outfile.write("{:f} {:f}\n".format(x, r(x)))

Here is the corresponding LaTeX code:

LaTeX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
\documentclass[border=0.25cm]{standalone}

\usepackage{tikz}
\usepackage{fouriernc}

\begin{document}

  \begin{tikzpicture}[scale=0.5] % Scale keeps the graphic small

    \draw[gray] (-15.5, -15.5) grid (15.5, 15.5);

    % x-axis
    \draw[thick, black, ->] (-15.5, 0) -- (15.5, 0)
      node[anchor=south west] {$x$};

    % x-axis tick marks
    \foreach \x in {-15, -10, -5, 5, 10, 15}
      \draw[thick] (\x, 0.5) -- (\x, -0.5)
        node[anchor=north] {\(\x\)};

    % y-axis
    \draw[thick, black, ->] (0, -15.5) -- (0, 15.5)
      node[anchor=south west] {$y$};

    % y-axis tick marks
    \foreach \y in {-15, -10, -5, 5, 10, 15}
      \draw[thick] (-0.5, \y) -- (0.5, \y)
        node[anchor=west] {\(\y\)};

    % graph of function
    \draw[thick, blue, <->] plot[smooth] file {plot2_0.table};
    \draw[thick, blue, <->] plot[smooth] file {plot2_1.table};
    \draw[thick, blue, <->] plot[smooth] file {plot2_2.table};

    % hole at (-1, 5)
    \draw[thick, blue, fill=white] (-1, 5) circle (2.5mm);

    % vertical asymptotes
    \draw[thick, dashed, red] (-2, -15.5) -- (-2, 15.5);
    \draw[thick, dashed, red] (2, -15.5) -- (2, 15.5);

    % horizontal asymptote
    \draw[thick, dashed, red] (-15.5, 2) -- (15.5, 2);

  \end{tikzpicture}
\end{document}

And here is the result:

a plot of the function. There is a horizontal asymptote at both ends at y = 2, and vertical asymptotes at x = -2 and x = 2

Plotting functions that go through certain points: two ways

Suppose I want to plot a function that go through certain points, say:

I want the function to be differentiable.

Method one: Bézier curves

Recall from the last post that a (cubic) Bézier curve is specified by four points:

The syntax is: \draw (\(x_0, y_0\)) .. controls (\(x_1, y_1\)) and (\(x_2, y_2\)) .. (\(x_3, y_3\));

bezier curve
with start, control, and end points as described

Now, we can combine multiple Bézier curves into a single \draw. For example, \draw (\(x_0, y_0\)) .. controls (\(x_1, y_1\)) and (\(x_2, y_2\)) .. (\(x_3, y_3\)) .. controls (\(x_4, y_4\)) and (\(x_5, y_5\)) .. (\(x_6, y_6\)); will produce:

two bezier curves attached at a sharp corner

In general, this curve will not necessarily be differentiable. We can get differentiability by ensuring that \((x_2, y_2)\), \((x_3, y_3)\) and \((x_4, y_4)\) are colinear (i.e., the end of the first Bézier curve has the same derivative as the beginning of the second) with \(x_2 \not = x_3\):

two bezier curves attached so that the two curves have the same tangents at
their connecting ends

Keeping these principles in mind, we can plot a function going through the desired points with:

LaTeX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
\documentclass[border=0.25cm]{standalone}

\usepackage{tikz}
\usepackage{fouriernc}

\begin{document}

  \begin{tikzpicture}[scale=0.5] % Scale keeps the graphic small

    \draw[gray] (-5.25, 0) grid (5.25, 8.25);

    % x-axis
    \draw[thick, black, ->] (-5.25, 0) -- (5.25, 0)
      node[anchor=south west] {$x$};

    % x-axis tick marks
    \foreach \x in {-5, 5}
      \draw[thick] (\x, 0.25) -- (\x, -0.25)
        node[anchor=north] {\(\x\)};

    % y-axis
    \draw[thick, black, ->] (0, 0) -- (0, 8.25)
      node[anchor=south west] {$y$};

    % y-axis tick marks
    \foreach \y in {4, 8}
      \draw[thick] (-0.25, \y) -- (0.25, \y)
        node[anchor=west] {\(\y\)};

    % graph of function
    \draw[thick, blue, <->] (-5.25, -0.5)
      .. controls (-4, 0) and (-5, 0) .. (-4, 2)
      .. controls (-3.5, 3) and (-3, 6) .. (-1, 6)
      .. controls (1, 6) and (1.75, 4) .. (2, 3)
      .. controls (2.25, 2) and (3, 2) .. (4, 5)
      .. controls (4.5, 6.5) and (4.5, 7) .. (5.25, 8);

  \end{tikzpicture}
\end{document}

Here is the output:

a plot of a differentiable function passing through the desired

Method two: Polynomial interpolation

SciPy's interpolation functionality can find a smooth function that passes through an arbitrary list of points. Here is an example of a Python script to produce such a function and create a table in the necessary format:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#! /usr/bin/env python3

import numpy as np
import scipy.interpolate

coords = [(-10, 8), (-4, 2), (-1, 6), (2, 3), (4, 5), (10, 1)]

# Since our goal is to plot from x = -5.25 to x = 5.25,

# we need to specify some points beyond that interval on both sides.

xcoords, ycoords = zip(*coords)

# This produces a seperate list of x-coordinates and y-coordinates

f = scipy.interpolate.interp1d(xcoords, ycoords, kind="cubic")

xvals = np.arange(-5.25, 5.25, 0.01)
yvals = f(xvals)

outcoords = zip(xvals, yvals)

# This produces a list of coordinate pairs

with open("plot3.table", "wt") as outfile:
    for pair in outcoords:
        outfile.write("{:f} {:f}\n".format(pair[0], pair[1]))

Note that if we had set coords = [(-4, 2), (-1, 6), (2, 3), (4, 5)], then the resulting function would not have had a defined value outside of the interval \([-4, 4]\).

To plot, we use the following tikzpicture:

LaTeX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
\documentclass[border=0.25cm]{standalone}

\usepackage{tikz}
\usepackage{fouriernc}

\begin{document}

  \begin{tikzpicture}[scale=0.5] % Scale keeps the graphic small

    \draw[gray] (-5.25, 0) grid (5.25, 8.25);

    % x-axis
    \draw[thick, black, ->] (-5.25, 0) -- (5.25, 0)
      node[anchor=south west] {$x$};

    % x-axis tick marks
    \foreach \x in {-5, 5}
      \draw[thick] (\x, 0.25) -- (\x, -0.25)
        node[anchor=north] {\(\x\)};

    % y-axis
    \draw[thick, black, ->] (0, 0) -- (0, 8.25)
      node[anchor=south west] {$y$};

    % y-axis tick marks
    \foreach \y in {4, 8}
      \draw[thick] (-0.25, \y) -- (0.25, \y)
        node[anchor=west] {\(\y\)};

    % graph of function
    \draw[thick, blue, <->] plot[smooth] file {plot3.table};

  \end{tikzpicture}
\end{document}

Here is the result:

a plot of a polynomial function whose graph passes through the desired points

As we have seen, creating plots with TikZ allows you a deep amount of control over the final result, and can be especially powerful when combined with a programming language such as Python.