How to Create an Onion Graph of Power and Root Functions in Python

Introduction

An onion graph is a visual name for a plot made of several nested mathematical curves. The curves look like the layers of an onion, especially when power functions and root functions are plotted together on the same axes.

In this tutorial, we are going to create an onion graph using Python and Matplotlib. The graph will show two families of functions:

\begin{equation}
[
y = x^n
]
\end{equation}

and:

\begin{equation}
[
y = \sqrt[n]{x} = x^{1/n}
]
\end{equation}

for several values of (n).

This plot is useful because it helps visualize how exponents change the shape of a function. Power functions such as $(y=x^2)$, $(y=x^3)$, and $(y=x^{10})$ curve below the line (y=x) on the interval ([0,1]). Root functions such as $(y=\sqrt{x})$, $(y=\sqrt[3]{x})$, and $(y=\sqrt[10]{x})$ curve above the line $(y=x)$.

How to Create an Onion Graph of Power and Root Functions in Python
How to Create an Onion Graph of Power and Root Functions in Python

This kind of graph is especially useful for teaching or learning:

  • power functions
  • root functions
  • exponents
  • inverse functions
  • function families
  • curve transformations
  • mathematical visualization with Python

What is an onion graph?

An onion graph is not a strict mathematical term. It is a descriptive name for a family of curves that form a layered shape.

In this example, we plot the power functions:

\begin{equation}
[
y=x^2,\ y=x^3,\ y=x^4,\ \ldots,\ y=x^{10}
]
\end{equation}

and the root functions:

\begin{equation}
[
y=\sqrt{x},\ y=\sqrt[3]{x},\ y=\sqrt[4]{x},\ \ldots,\ y=\sqrt[10]{x}
]
\end{equation}

The line:

\begin{equation}
[
y=x
]
\end{equation}

is used as a reference line.

The power functions appear below $(y=x)$, while the root functions appear above $(y=x)$. Together, they create a symmetric onion-like shape.

Why are power functions below y = x?

For values between 0 and 1, raising a number to a power greater than 1 makes the value smaller.

For example:

\begin{equation}
[
0.5^2 = 0.25
]
\end{equation}

So:

\begin{equation}
[
x^2 < x
]
\end{equation}

when:

\begin{equation}
[
0 < x < 1
]
\end{equation}

This is why power functions such as $(y=x^2)$, $(y=x^3)$, and $(y=x^4)$ appear below the line $(y=x)$.

Why are root functions above y = x?

For values between 0 and 1, taking a root makes the value larger.

For example:

\begin{equation}
[
\sqrt{0.25}=0.5
]
\end{equation}

So:

\begin{equation}
[
\sqrt{x} > x
]
\end{equation}

when:

\begin{equation}
[
0 < x < 1
]
\end{equation}

This is why root functions appear above the line $(y=x)$.

Import Python libraries

We first import NumPy and Matplotlib.

1
2
3
4
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patheffects as pe
from matplotlib.ticker import MultipleLocator

NumPy is used to create the (x) values, and Matplotlib is used to create the plot.

Create x values

We create values between 0 and 1 using NumPy.

1
2
x = np.linspace(0, 1, 1200)
orders = range(2, 11)

The variable (x) contains 1200 evenly spaced values from 0 to 1.

The variable orders contains the exponent values from 2 to 10.

Complete Python code

  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
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
import numpy as np
    import matplotlib.pyplot as plt
    import matplotlib.patheffects as pe
    from matplotlib.ticker import MultipleLocator

    # ------------------------------------------------------------
    # Data
    # ------------------------------------------------------------
    x = np.linspace(0, 1, 1200)
    orders = range(2, 11)

    # ------------------------------------------------------------
    # Figure setup
    # ------------------------------------------------------------
    fig, ax = plt.subplots(figsize=(9, 9), dpi=160)

    fig.patch.set_facecolor("#f8fafc")
    ax.set_facecolor("white")

    # Colors for both families of functions
    power_colors = plt.cm.plasma(np.linspace(0.12, 0.85, len(list(orders))))
    root_colors = plt.cm.viridis(np.linspace(0.12, 0.85, len(list(orders))))

    # ------------------------------------------------------------
    # Plot root functions: y = x^(1/n)
    # ------------------------------------------------------------
    for n, color in zip(orders, root_colors):
        ax.plot(
            x,
            x ** (1 / n),
            color=color,
            lw=2.2,
            alpha=0.95,
            solid_capstyle="round"
        )

    # ------------------------------------------------------------
    # Plot power functions: y = x^n
    # ------------------------------------------------------------
    for n, color in zip(orders, power_colors):
        ax.plot(
            x,
            x ** n,
            color=color,
            lw=2.2,
            alpha=0.95,
            solid_capstyle="round"
        )

    # ------------------------------------------------------------
    # Plot the reference line: y = x
    # ------------------------------------------------------------
    ax.plot(
        x,
        x,
        color="#111827",
        lw=3.0,
        solid_capstyle="round"
    )

    # ------------------------------------------------------------
    # Helper function to rotate labels along the curves
    # ------------------------------------------------------------
    def curve_angle(ax, func, x0, dx=0.01):
        """
        Estimate the local angle of a curve in display coordinates.
        This helps text labels follow the visual slope of the curve.
        """
        x1 = max(0, x0 - dx)
        x2 = min(1, x0 + dx)

        y1 = func(x1)
        y2 = func(x2)

        p1 = ax.transData.transform((x1, y1))
        p2 = ax.transData.transform((x2, y2))

        angle = np.degrees(
            np.arctan2(p2[1] - p1[1], p2[0] - p1[0])
        )

        return angle


    def add_curve_label(ax, func, x0, text, color, fontsize=11):
        """
        Add a readable label directly on a curve.
        """
        y0 = func(x0)
        angle = curve_angle(ax, func, x0)

        ax.text(
            x0,
            y0,
            text,
            color=color,
            fontsize=fontsize,
            fontweight="semibold",
            rotation=angle,
            rotation_mode="anchor",
            ha="center",
            va="center",
            bbox=dict(
                boxstyle="round,pad=0.22",
                facecolor="white",
                edgecolor="none",
                alpha=0.82
            ),
            path_effects=[
                pe.withStroke(linewidth=3.5, foreground="white")
            ]
        )

    # Set axis limits before computing text rotation
    ax.set_xlim(-0.02, 1.02)
    ax.set_ylim(-0.02, 1.02)
    ax.set_aspect("equal", adjustable="box")

    fig.canvas.draw()

    # ------------------------------------------------------------
    # Add selected curve labels
    # ------------------------------------------------------------
    # To keep the plot readable, only selected curves are labeled.

    key_roots = [2, 3, 5, 10]
    root_label_y = {
        2: 0.58,
        3: 0.68,
        5: 0.78,
        10: 0.88,
    }

    for n in key_roots:
        color = root_colors[n - 2]
        y_target = root_label_y[n]
        x_label = y_target ** n

        add_curve_label(
            ax,
            lambda t, n=n: t ** (1 / n),
            x_label,
            rf"$y=\sqrt[{n}]{{x}}$",
            color,
            fontsize=11
        )

    key_powers = [2, 3, 5, 10]
    power_label_y = {
        2: 0.34,
        3: 0.25,
        5: 0.16,
        10: 0.10,
    }

    for n in key_powers:
        color = power_colors[n - 2]
        y_target = power_label_y[n]
        x_label = y_target ** (1 / n)

        add_curve_label(
            ax,
            lambda t, n=n: t ** n,
            x_label,
            rf"$y=x^{{{n}}}$",
            color,
            fontsize=11
        )

    # Label the reference line
    add_curve_label(
        ax,
        lambda t: t,
        0.55,
        r"$y=x$",
        "#111827",
        fontsize=12
    )

    # ------------------------------------------------------------
    # Family annotations
    # ------------------------------------------------------------
    ax.text(
        0.06,
        0.96,
        "Root functions",
        fontsize=14,
        fontweight="bold",
        color="#065f46"
    )

    ax.text(
        0.75,
        0.03,
        "Power functions",
        fontsize=14,
        fontweight="bold",
        color="#7e22ce"
    )

    ax.text(
        0.06,
        0.91,
        r"$y = x^{1/n}, \quad n = 2,\ldots,10$",
        fontsize=11,
        color="#334155"
    )

    ax.text(
        0.75,
        0.01,
        r"$y = x^n, \quad n = 2,\ldots,10$",
        fontsize=11,
        color="#334155"
    )

    # ------------------------------------------------------------
    # Styling
    # ------------------------------------------------------------
    ax.set_title(
        "Onion Graph of Power and Root Functions",
        fontsize=23,
        fontweight="bold",
        pad=22,
        color="#0f172a"
    )

    ax.set_xlabel("x", fontsize=13, fontweight="bold", color="#334155")
    ax.set_ylabel("y", fontsize=13, fontweight="bold", color="#334155")

    ax.xaxis.set_major_locator(MultipleLocator(0.1))
    ax.yaxis.set_major_locator(MultipleLocator(0.1))

    ax.grid(
        True,
        color="#e5e7eb",
        linewidth=0.8,
        alpha=0.75
    )

    ax.tick_params(axis="both", labelsize=10, colors="#475569")

    # Clean axes
    ax.spines["top"].set_visible(False)
    ax.spines["right"].set_visible(False)

    ax.spines["left"].set_color("#334155")
    ax.spines["bottom"].set_color("#334155")
    ax.spines["left"].set_linewidth(1.2)
    ax.spines["bottom"].set_linewidth(1.2)

    # Mark the two common points
    ax.scatter([0, 1], [0, 1], color="#111827", s=35, zorder=5)

    # Add a short explanatory note
    ax.text(
        0.5,
        -0.11,
        "Root functions curve above y = x; power functions curve below it.",
        transform=ax.transAxes,
        ha="center",
        fontsize=10,
        color="#475569"
    )

    plt.tight_layout()

    # Save the figure
    plt.savefig(
        "onion_graph_power_root_functions.png",
        dpi=300,
        bbox_inches="tight"
    )

    plt.show()

Explanation

The main idea is to plot two families of functions.

The first family contains power functions:

\begin{equation}
[
y = x^n
]
\end{equation}

In Python, this is written as:

1
y = x ** n

The second family contains root functions:

\begin{equation}
[
y = x^{1/n}
]
\end{equation}

In Python, this is written as:

1
y = x ** (1 / n)

For example, when (n=2), the power function is:

\begin{equation}
[
y=x^2
]
\end{equation}

and the root function is:

\begin{equation}
[
y=\sqrt{x}
]
\end{equation}

When (n=3), the power function is:

\begin{equation}
[
y=x^3
]
\end{equation}

and the root function is:

\begin{equation}
[
y=\sqrt[3]{x}
]
\end{equation}

Plotting the power functions

The power functions are plotted using a loop:

1
2
3
4
5
6
7
8
9
for n, color in zip(orders, power_colors):
    ax.plot(
        x,
        x ** n,
        color=color,
        lw=2.2,
        alpha=0.95,
        solid_capstyle="round"
    )

This loop creates the curves:

[
y=x^2,\ y=x^3,\ y=x^4,\ \ldots,\ y=x^{10}
]

Plotting the root functions

The root functions are also plotted using a loop:

1
2
3
4
5
6
7
8
9
for n, color in zip(orders, root_colors):
    ax.plot(
        x,
        x ** (1 / n),
        color=color,
        lw=2.2,
        alpha=0.95,
        solid_capstyle="round"
    )

This loop creates the curves:

\begin{equation}
[
y=\sqrt{x},\ y=\sqrt[3]{x},\ y=\sqrt[4]{x},\ \ldots,\ y=\sqrt[10]{x}
]
\end{equation}

Adding the reference line y = x

The line (y=x) is added as a reference:

1
2
3
4
5
6
7
ax.plot(
    x,
    x,
    color="#111827",
    lw=3.0,
    solid_capstyle="round"
)

This line separates the power functions from the root functions.

Making the labels easier to read

If every curve is labeled, the graph can quickly become crowded. To make the plot easier to read, only a few important curves are labeled:

1
2
key_roots = [2, 3, 5, 10]
key_powers = [2, 3, 5, 10]

This keeps the figure clean while still showing the behavior of the full family of functions.

Adding mathematical text with Matplotlib

Matplotlib can display mathematical expressions using mathtext.

For example:

1
r"$y=x^n$"

or:

1
r"$y = \sqrt[n]{x}$"

The letter r before the string creates a raw string. This is useful because mathematical expressions often contain backslashes.

In the figure, the two family annotations are added using:

1
2
3
4
5
6
7
8
ax.text(
    0.06,
    0.87,
    r"$y = \sqrt[n]{x}, \quad n = 2,\ldots,10$",
    transform=ax.transAxes,
    fontsize=11,
    color="#334155"
)

and:

1
2
3
4
5
6
7
8
ax.text(
    0.63,
    0.04,
    r"$y = x^n, \quad n = 2,\ldots,10$",
    transform=ax.transAxes,
    fontsize=11,
    color="#334155"
)

The option:

1
transform=ax.transAxes

places the text using relative axes coordinates instead of data coordinates. This makes the annotation position more stable if the axis limits change.

Saving the figure

The figure is saved using:

1
2
3
4
5
plt.savefig(
    "onion_graph_power_root_functions.png",
    dpi=300,
    bbox_inches="tight"
)

The option:

1
dpi=300

exports a high-resolution image.

The option:

1
bbox_inches="tight"

removes unnecessary white space around the figure.

Conclusion

An onion graph is a simple but powerful way to visualize power and root functions together. It clearly shows how changing the exponent affects the shape of a curve on the interval ([0,1]).

Power functions (y=x^n) curve below the line (y=x), while root functions (y=x^{1/n}) curve above it. Plotting both families together creates a beautiful layered structure.

This type of graph is useful for teaching mathematical concepts such as exponents, roots, inverse functions, and transformations. With Python and Matplotlib, it is also easy to customize the colors, labels, annotations, and output resolution.

References

Image

of