Interactive Mandelbrot set and Julia Set

This notebook provides an interactive view of both Mandelbrot and Julia set fractals using Jupyter's ipywidgets. Widgets must be enabled in Jupyter to run. After executing the next cell, if a slider widget does not show up in the output, run the following command in your terminal and restart Jupyter:

jupyter nbextension enable --py widgetsnbextension

Using the plot controls to zoom or pan the view will call for a re-render of the fractal with the current view.

In [1]:
import ipywidgets as widgets

import numpy as np
import matplotlib.pyplot as plt
%matplotlib notebook

np.seterr(over='ignore', invalid='ignore');  # Ignore floating point overflows

widgets.IntSlider()
In [2]:
class Mandel(object):
    ''' Interactive Mandelbrot Object '''
    def __init__(self, xlim, ylim, maxiters=50, escape=2, cmap='jet'):
        ''' Initialize Mandelbrot Set.
        
            xlim: tuple of (xlow, xhigh)
            ylim: tuple of (ylow, yhigh)
            maxiters: maximum iterations
            escape: escape threshold
            cmap: colormap name
        '''
        self.xlim = xlim
        self.ylim = ylim
        self.maxiters = maxiters
        self.escape = escape
        self.cmap = cmap
    
        self.fig, self.ax = plt.subplots()
        self.ax.set_xlim(self.xlim)
        self.ax.set_ylim(self.ylim)
        self.ax.set_autoscale_on(False)
        self.ax.callbacks.connect('xlim_changed', self.limchange)
        self.ax.callbacks.connect('ylim_changed', self.limchange)        
        self.fig.canvas.mpl_connect('button_release_event', self.buttonevent)
        
        self.widgetiter = widgets.IntSlider(maxiters, 1, 1000, description='Iterations', continuous_update=False)
        self.widgetiter.observe(self.iterchange, names='value')
        self.widgetcmap = widgets.Dropdown(options=plt.colormaps(), value='viridis_r', description='Colormap')
        self.widgetcmap.observe(self.cmapchange, names='value')
        self.widgetlabel = widgets.HTML('Ready')

        display(self.widgetlabel)
        display(self.widgetiter)
        display(self.widgetcmap)
        self.render()
        self.redraw()

    def iterchange(self, change):
        ''' Iterations slider changed '''
        self.maxiters = change.new
        self.render()
        self.redraw()
        
    def cmapchange(self, change):
        ''' Colormap selection changed '''
        self.cmap = change.new
        self.redraw()  # Redraw with new color, but don't need to regenerate
    
    def limchange(self, ax):
        ''' Window Limits changed (zooming) '''
        if self.fig.canvas.toolbar.mode == 'zoom rect':
            xstart, ystart, xdelta, ydelta = ax.viewLim.bounds
            self.xlim = (xstart, xstart + xdelta)
            self.ylim = (ystart, ystart + ydelta)
            self.render()
            self.redraw()
        
    def buttonevent(self, event):
        ''' Button release event (panning) '''
        if event.canvas.toolbar.mode == 'pan/zoom':
            self.xlim = self.ax.get_xlim()
            self.ylim = self.ax.get_ylim()
            self.render()
            self.redraw()

    def render(self):
        ''' Generate mandelbrot set in self.image '''
        self.widgetlabel.value = '<font color="red">RENDERING...</font>'
        xrng = np.linspace(self.xlim[0], self.xlim[1], 1000)
        yrng = np.linspace(self.ylim[0], self.ylim[1], 1000).reshape(-1, 1)
        z0 = xrng + 1.0j * yrng
        self.buildimage(z0)
        self.widgetlabel.value = 'Ready'
    
    def buildimage(self, z):
        ''' Build self.image from z. Subclass this for other fractal types. '''
        z0 = z.copy()
        self.image = np.zeros(z.shape, dtype='int')  # The image data, how long each point takes to escape
        mask = np.full(self.image.shape, True)       # True/False mask indicating whether each pixel has escaped yet        
        for i in range(self.maxiters):
            z[mask] = z[mask]**2 + z0[mask]
            mask = abs(z) < self.escape
            self.image += mask
    
    def redraw(self):
        ''' Draw the image '''
        if len(self.ax.images) > 0:
            self.ax.images.pop()
            
        self.ax.imshow(self.image, cmap=self.cmap,
                extent=(self.xlim[0], self.xlim[1], self.ylim[0], self.ylim[1]),
                origin='lower', aspect='equal',
                interpolation='bicubic')
In [3]:
m = Mandel(xlim=(-2.0,1.2),ylim=(-1.4,1.4))
In [4]:
class Julia(Mandel):
    ''' Julia Set Fractal '''
    def __init__(self, xlim, ylim, c, maxiters=50, escape=2, cmap='jet'):
        ''' Initialize Julia Set.
        
            xlim: tuple of (xlow, xhigh)
            ylim: tuple of (ylow, yhigh)
            c: complex
            maxiters: maximum iterations
            escape: escape threshold
            cmap: colormap name
        '''        
        self.c = c
        super(Julia, self).__init__(xlim, ylim, maxiters=maxiters, escape=escape, cmap=cmap)
        self.widgetreal = widgets.FloatSlider(c.real, min=-1, max=1, step=.01, description='Re(c)', continuous_update=False)
        self.widgetimag = widgets.FloatSlider(c.imag, min=-1, max=1, step=.01, description='Im(c)', continuous_update=False)
        self.widgetreal.observe(self.cchange, names='value')
        self.widgetimag.observe(self.cchange, names='value')
        display(self.widgetreal)
        display(self.widgetimag)
    
    def cchange(self, value):
        ''' Real/Imaginary C value changed '''
        self.c = self.widgetreal.value + 1j * self.widgetimag.value
        self.render()
        self.redraw()
    
    def buildimage(self, z):
        ''' Override Mandelbrot's buildimage function '''
        self.image = np.zeros(z.shape, dtype='int')  # The image data, how long each point takes to escape
        mask = np.full(self.image.shape, True)       # True/False mask indicating whether each pixel has escaped yet        
        for i in range(self.maxiters):
            z[mask] = z[mask]**2 + self.c
            mask = abs(z) < self.escape
            self.image += mask
In [5]:
j = Julia(xlim=(-2,2), ylim=(-2,2), c=.35-.43j, cmap='PRGn')