Web-Based Mandelbrot Set Plotter with Pan, Zoom, and Export

Build a Mandelbrot Set Plotter in Python: Step-by-Step GuideThe Mandelbrot set is one of the most famous fractals — a simple iterative rule that produces infinitely complex, self-similar patterns. This guide walks you through building a configurable, well-documented Mandelbrot set plotter in Python. You’ll learn the math, implement the algorithm, optimize performance, add color mapping, and create interactive zooming and exporting features.


What you’ll learn

  • The mathematical definition of the Mandelbrot set
  • How to implement the iteration and escape-time algorithm in Python
  • Performance improvements (vectorization, numba, multiprocessing, GPU options)
  • Coloring techniques and smooth coloring for pleasing visuals
  • Building a simple GUI and interactive zoom/export features
  • Example code and practical tips for exploration

Prerequisites

  • Basic knowledge of Python (functions, loops, arrays)
  • Familiarity with NumPy and Matplotlib is helpful
  • Optional: numba, Pillow, PyQt or tkinter for GUI, and cupy or pyopencl for GPU acceleration

1. Mathematical background

The Mandelbrot set is the set of complex numbers c for which the sequence defined by the iteration

z_{n+1} = z_n^2 + c, with z_0 = 0

remains bounded (does not diverge to infinity). In practice we test whether |z_n| exceeds a bailout radius (commonly 2). If |z_n| > 2 at iteration n, the point is considered to escape; the iteration count gives an indication of how quickly it diverged and is used for coloring.

Key parameters:

  • Complex plane region: typically real range [-2.5, 1] and imaginary range [-1.5, 1.5]
  • Image resolution: width × height in pixels
  • Max iterations: higher values reveal finer detail but cost more computation
  • Bailout radius: commonly 2 (since |z| > 2 guarantees divergence)

2. Basic CPU implementation (simple, clear)

Start with a straightforward, easy-to-understand implementation using pure Python and NumPy for array handling.

# mandelbrot_basic.py import numpy as np import matplotlib.pyplot as plt def mandelbrot(width, height, x_min, x_max, y_min, y_max, max_iter):     # Create coordinate grid     xs = np.linspace(x_min, x_max, width)     ys = np.linspace(y_min, y_max, height)     X, Y = np.meshgrid(xs, ys)     C = X + 1j * Y     Z = np.zeros_like(C, dtype=np.complex128)     output = np.zeros(C.shape, dtype=np.int32)     for i in range(1, max_iter + 1):         mask = np.abs(Z) <= 2.0         Z[mask] = Z[mask] * Z[mask] + C[mask]         escaped = mask & (np.abs(Z) > 2.0)         output[escaped] = i     return output if __name__ == "__main__":     img = mandelbrot(800, 600, -2.5, 1.0, -1.25, 1.25, 200)     plt.imshow(img, cmap='hot', extent=[-2.5, 1.0, -1.25, 1.25])     plt.colorbar(label='Iterations to escape (0 = inside)')     plt.title('Mandelbrot Set (basic)')     plt.show() 

Notes:

  • The output array stores the iteration count at the escape moment; points that never escaped remain 0 (inside the set).
  • This simple loop is easy to understand but not the fastest.

3. Improving performance

Large images and high iteration counts need optimizations.

Options:

  • NumPy vectorization (as above) reduces Python loop overhead but still performs many array ops.
  • Numba JIT-compilation gives near-C speeds for pure-Python loops.
  • Multiprocessing splits rows/tiles across CPU cores.
  • GPU (CuPy, PyOpenCL) can dramatically accelerate for large images.

Numba example:

# mandelbrot_numba.py import numpy as np import matplotlib.pyplot as plt from numba import njit, prange @njit(parallel=True, fastmath=True) def compute_mandelbrot(width, height, x_min, x_max, y_min, y_max, max_iter):     out = np.zeros((height, width), np.int32)     for j in prange(height):         y = y_min + (y_max - y_min) * j / (height - 1)         for i in range(width):             x = x_min + (x_max - x_min) * i / (width - 1)             c_re, c_im = x, y             z_re, z_im = 0.0, 0.0             iter_count = 0             while z_re*z_re + z_im*z_im <= 4.0 and iter_count < max_iter:                 # (z_re + i z_im)^2 = (z_re^2 - z_im^2) + i(2*z_re*z_im)                 tmp = z_re*z_re - z_im*z_im + c_re                 z_im = 2.0*z_re*z_im + c_im                 z_re = tmp                 iter_count += 1             out[j, i] = iter_count     return out if __name__ == "__main__":     img = compute_mandelbrot(1920, 1080, -2.5, 1.0, -1.0, 1.0, 1000)     plt.imshow(img, cmap='viridis', extent=[-2.5, 1.0, -1.0, 1.0])     plt.title('Mandelbrot (numba)')     plt.show() 

Tips:

  • Use prange for parallel loops.
  • fastmath can speed float ops at the expense of strict IEEE behavior.
  • Warm up Numba (first run compiles).

GPU option: CuPy can replace NumPy arrays with GPU arrays and use similar code. For large resolutions, GPU offers big speedups.


4. Coloring: ugly vs. smooth

Simple coloring uses the raw iteration count mapped to a colormap. This creates banding. Smooth coloring gives continuous gradients using fractional escape time:

nu = n + 1 – log(log|z_n|)/log 2

Where n is the integer iteration at escape and z_n is the value at escape. This produces smoother transitions and more detail.

Example of smooth coloring applied to the output:

# smoothing after compute_mandelbrot import numpy as np def smooth_coloring(iter_counts, z_abs_values, max_iter):     # iter_counts: integer escape iterations (0 if inside)     # z_abs_values: |z| at escape (same shape)     h = iter_counts.astype(np.float64)     mask = (iter_counts > 0) & (iter_counts < max_iter)     h[mask] = h[mask] + 1 - np.log(np.log(z_abs_values[mask])) / np.log(2)     # Inside points keep 0 (or max value)     return h 

Combine with matplotlib’s colormaps (viridis, plasma, inferno) or create custom palettes. Perceptually-uniform maps (viridis, magma) often look better.


5. Interactive zoom and GUI

A minimal interactive viewer can be built with matplotlib’s event handling or with a GUI toolkit (tkinter, PyQt, DearPyGui).

Matplotlib example (click-and-drag to zoom):

# mandelbrot_zoom.py import numpy as np import matplotlib.pyplot as plt from mandelbrot_numba import compute_mandelbrot class MandelbrotViewer:     def __init__(self, width=800, height=600):         self.width, self.height = width, height         self.x_min, self.x_max = -2.5, 1.0         self.y_min, self.y_max = -1.25, 1.25         self.max_iter = 400         self.fig, self.ax = plt.subplots()         self.ax.set_title('Drag to zoom, right-click to reset')         self.im = None         self.rect_start = None         self.cid_press = self.fig.canvas.mpl_connect('button_press_event', self.on_press)         self.cid_release = self.fig.canvas.mpl_connect('button_release_event', self.on_release)         self.draw()     def draw(self):         img = compute_mandelbrot(self.width, self.height, self.x_min, self.x_max, self.y_min, self.y_max, self.max_iter)         if self.im is None:             self.im = self.ax.imshow(img, cmap='magma', extent=[self.x_min, self.x_max, self.y_min, self.y_max])             self.fig.colorbar(self.im, ax=self.ax)         else:             self.im.set_data(img)             self.im.set_extent([self.x_min, self.x_max, self.y_min, self.y_max])         self.fig.canvas.draw_idle()     def on_press(self, event):         if event.button == 3:  # right-click reset             self.x_min, self.x_max = -2.5, 1.0             self.y_min, self.y_max = -1.25, 1.25             self.draw()             return         self.rect_start = (event.xdata, event.ydata)     def on_release(self, event):         if self.rect_start is None:              return         x0, y0 = self.rect_start         x1, y1 = event.xdata, event.ydata         self.x_min, self.x_max = min(x0, x1), max(x0, x1)         self.y_min, self.y_max = min(y0, y1), max(y0, y1)         self.rect_start = None         self.draw() if __name__ == "__main__":     MandelbrotViewer(1000, 700)     plt.show() 

For production-grade interactivity (smooth zooming, real-time GPU rendering), use OpenGL, Vulkan, or WebGL-based viewers (e.g., in a web page with WebGL shaders).


6. Advanced features

  • Continuous zoom sequences: store camera (x_min/x_max/y_min/y_max) per frame and render frames for an animation (FFmpeg to combine).
  • Anti-aliasing: supersample by rendering at 2× or 4× resolution and downsample.
  • Periodicity checking: detect bulbs that are inside earlier to stop iteration earlier.
  • Distance estimation: compute distance from point to set boundary for high-quality ray-marching and smooth shading.
  • Arbitrary-precision arithmetic (mpmath, gmpy) lets you zoom far beyond double precision; required for extreme deep zooms.

7. Example: Full script with optional numba and smooth coloring

# mandelbrot_full.py import numpy as np import matplotlib.pyplot as plt try:     from numba import njit, prange     numba_available = True except ImportError:     numba_available = False def mandelbrot_numpy(width, height, x_min, x_max, y_min, y_max, max_iter):     xs = np.linspace(x_min, x_max, width)     ys = np.linspace(y_min, y_max, height)     X, Y = np.meshgrid(xs, ys)     C = X + 1j * Y     Z = np.zeros_like(C)     iters = np.zeros(C.shape, dtype=np.int32)     absZ = np.zeros(C.shape, dtype=np.float64)     for n in range(1, max_iter + 1):         mask = np.abs(Z) <= 2.0         Z[mask] = Z[mask]*Z[mask] + C[mask]         escaped = mask & (np.abs(Z) > 2.0)         iters[escaped] = n         absZ[escaped] = np.abs(Z[escaped])     return iters, absZ if numba_available:     @njit(parallel=True, fastmath=True)     def mandelbrot_numba(width, height, x_min, x_max, y_min, y_max, max_iter):         out = np.zeros((height, width), np.int32)         abs_out = np.zeros((height, width), np.float64)         for j in prange(height):             y = y_min + (y_max - y_min) * j / (height - 1)             for i in range(width):                 x = x_min + (x_max - x_min) * i / (width - 1)                 c_re, c_im = x, y                 z_re, z_im = 0.0, 0.0                 iter_count = 0                 while z_re*z_re + z_im*z_im <= 4.0 and iter_count < max_iter:                     tmp = z_re*z_re - z_im*z_im + c_re                     z_im = 2.0*z_re*z_im + c_im                     z_re = tmp                     iter_count += 1                 out[j, i] = iter_count                 abs_out[j, i] = np.sqrt(z_re*z_re + z_im*z_im)         return out, abs_out def smooth(iter_counts, absZ, max_iter):     h = iter_counts.astype(np.float64)     mask = (iter_counts > 0) & (iter_counts < max_iter)     h[mask] = h[mask] + 1 - np.log(np.log(absZ[mask])) / np.log(2)     h[iter_counts == 0] = max_iter     return h if __name__ == "__main__":     width, height = 1200, 800     bounds = (-2.5, 1.0, -1.25, 1.25)     max_iter = 500     if numba_available:         iters, absZ = mandelbrot_numba(width, height, *bounds, max_iter)     else:         iters, absZ = mandelbrot_numpy(width, height, *bounds, max_iter)     img = smooth(iters, absZ, max_iter)     plt.figure(figsize=(10, 7))     plt.imshow(img, cmap='twilight', extent=[bounds[0], bounds[1], bounds[2], bounds[3]])     plt.axis('off')     plt.title('Mandelbrot Set (smooth)')     plt.show() 

8. Practical tips and exploration ideas

  • Start with modest resolution (800×600) and low iterations (200) to test parameters.
  • Increase max_iter gradually when zooming deeper; use arbitrary precision for extremely deep zooms.
  • Try different color maps and log/linear scaling to highlight structures.
  • Render tiles in parallel and stitch them for very large images.
  • Share sequences as animated zooms or interactive viewers using web frameworks.

9. Troubleshooting common issues

  • Slow rendering: try numba or GPU, or reduce resolution/iterations.
  • Banding in colors: use smooth coloring and continuous colormaps.
  • Artifacts at edges: ensure correct aspect ratio and extent in imshow.
  • Crashes with deep zooms: use arbitrary-precision arithmetic libraries.

10. Further reading and experiments

  • Explore Julia sets (same iteration formula but z_0 = point, c constant).
  • Implement distance estimation to create 3D-like shading.
  • Study period-doubling, cardioid and bulb structures, and external angles for deeper math.

This guide gives you a practical pipeline from math to a polished Mandelbrot plotter: starting with a clear CPU implementation, moving to performance and coloring improvements, and finishing with interactivity and advanced features.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *