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.
Table of contents
- Introduction
- What is an onion graph?
- Why are power functions below y = x?
- Why are root functions above y = x?
- Import Python libraries
- Create x values
- Complete Python code
- Explanation
- Plotting the power functions
- Plotting the root functions
- Adding the reference line y = x
- Making the labels easier to read
- Adding mathematical text with Matplotlib
- Saving the figure
- Conclusion
- References
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)$.

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
Table of contents
- Introduction
- What is an onion graph?
- Why are power functions below y = x?
- Why are root functions above y = x?
- Import Python libraries
- Create x values
- Complete Python code
- Explanation
- Plotting the power functions
- Plotting the root functions
- Adding the reference line y = x
- Making the labels easier to read
- Adding mathematical text with Matplotlib
- Saving the figure
- Conclusion
- References
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
| Links | Site |
|---|---|
| Matplotlib documentation | Matplotlib |
| Writing mathematical expressions in Matplotlib | Matplotlib |
| matplotlib.pyplot.savefig | Matplotlib |
| numpy.linspace | NumPy |
