Plotly interactive visualization of complex valued functions

In this Jupyter Notebook we generate via Plotly, an interactive plot of a complex valued function, $f:D\subset\mathbb{C}\to\mathbb{C}$. A complex function is visualized using a version of the domain coloring method.

Compared with other types of domain coloring, the Plotly interactive plot is much more informative. It displays for each point $z$ in a rectangular region of the complex plane, not only the hsv color associated to $\arg(f(z)$ (argument of $f(z)$), but also the values $\arg(f(z)$ and $\log(|f(z)|)$ (the log modulus).

First we define a Plotly hsv (hue, saturation, value) colorscale, adapted to the range of the numpy functions, np.angle, respectively, np.arctan2. We plot a Heatmap over a rectangular region in the complex plane, colored via this colorscale, according to the values of the $\arg(f(z))$. Over the Heatmap are plotted a few contour lines of the log modulus $\log(|f(z)|)$.


In [1]:
import numpy as np
import numpy.ma as ma
from numpy import pi
import matplotlib.pyplot as plt
import matplotlib.colors

In [2]:
def hsv_colorscale(S=1, V=1): 
    if S <0 or S>1 or V<0 or V>1:
        raise ValueError('Parameters S (saturation), V (value, brightness) must be in [0,1]')
   
    argument=np.array([-pi, -5*pi/6, -2*pi/3, -3*pi/6, -pi/3,  -pi/6, 0, 
                       pi/6, pi/3, 3*pi/6, 2*pi/3, 5*pi/6, pi])
    
    H=argument/(2*np.pi)+1
    H=np.mod(H,1)
    Sat=S*np.ones_like(H)
    Val=V*np.ones_like(H)
    
    HSV = np.dstack((H,Sat,Val))
    RGB = matplotlib.colors.hsv_to_rgb(HSV) 
   
    colormap = 255* np.squeeze(RGB) 
    #Define and return the Plotly hsv colorscale adapted to polar coordinates for complex valued functions
    step_size=1./(colormap.shape[0]-1)
    return [[k*step_size, 'rgb'+str((int(c[0]), int(c[1]), int(c[2])))] for k, c in enumerate(colormap)]

In [3]:
pl_hsv=hsv_colorscale(S=1,V=1)
pl_hsv


Out[3]:
[[0.0, 'rgb(0, 255, 255)'],
 [0.08333333333333333, 'rgb(0, 127, 255)'],
 [0.16666666666666666, 'rgb(0, 0, 255)'],
 [0.25, 'rgb(127, 0, 255)'],
 [0.3333333333333333, 'rgb(255, 0, 255)'],
 [0.41666666666666663, 'rgb(255, 0, 127)'],
 [0.5, 'rgb(255, 0, 0)'],
 [0.5833333333333333, 'rgb(255, 127, 0)'],
 [0.6666666666666666, 'rgb(254, 255, 0)'],
 [0.75, 'rgb(127, 255, 0)'],
 [0.8333333333333333, 'rgb(0, 255, 0)'],
 [0.9166666666666666, 'rgb(0, 255, 127)'],
 [1.0, 'rgb(0, 255, 255)']]

The following two functions compute data needed for visualization:


In [4]:
def computations(func,  re=(-1,1), im=(-1,1), N=100):
    # func is the complex function to be ploted
    #re, im are the interval ends on the real and imaginary axes, defining the rectangular region in the complex plane
    #N gives the number of points in an interval of length 1
    l=re[1]-re[0]
    h=im[1]-im[0]
    resL=int(N*l) #horizontal resolution
    resH=int(N*h) #vertical resolution
    X=np.linspace(re[0], re[1], resL)
    Y=np.linspace(im[0], im[1], resH)
    x, y=np.meshgrid(X,Y)
    z=x+1j*y
    w=func(z)
    argument=np.angle(w)
    modulus=np.absolute(w)
    log_modulus=ma.log(modulus)
    return x,y, argument, log_modulus

In [5]:
def get_levels(fmodul, nr=10):
    #define the levels for contour plot of the modulus |f(z)|
    #fmodul is the log modulus of f(z) computed on meshgrid
    #nr= the number of  contour lines
    mv=np.nanmin(fmodul)
    Mv=np.nanmax(fmodul)
    size=(Mv-mv)/float(nr)
    return [mv+k*size for k in range(nr+1)]

In [6]:
import plotly.plotly as py
from plotly.graph_objs import *

We extract the contour lines from an attribute of the matplotlib.contour.QuadContourSet object returned by the matplotlib.pyplot.contour.

The function defined in the next cell retrieves the points on the contour lines segments, and defines the corresponding Plotly traces:


In [7]:
def plotly_contour_lines(contour_data):
    #contour_data is a matplotlib.contour.QuadContourSet object returned by plt.contour
    contours=contour_data.allsegs  # 
    if len(contours)==0:
        raise ValueError('Something wrong hapend in computing contour lines')
    #contours is a list of lists; if contour is a list in contours, its elements are arrays. 
    #Each array defines a segment of contour line at some level
    xl=[]# list of x coordinates of points on a contour line
    yl=[]#         y
    #lists of coordinates for contour lines consisting in one point:
    xp=[]# 
    yp=[]

    for k,contour in enumerate(contours):
        L=len(contour)
        if L!=0: # sometimes the list of points at the level np.max(modulus) is empty
            for ar in contour:
                if ar.shape[0]==1:
                    xp+=[ar[0,0], None]
                    yp+=[ar[0,1], None]
                else: 
                    xl+=ar[:,0].tolist()
                    yl+=ar[:,1].tolist()
                    xl.append(None) 
                    yl.append(None)

    lines=Scatter(x=xl, 
                  y=yl, 
                  mode='lines',  
                  name='modulus', 
                  line=dict(width=1,
                            color='#a5bab7', 
                            shape='spline', 
                            smoothing=1),
                  hoverinfo='none'
                 )   
    if len(xp)==0:
        return lines
    else: 
        points=Scatter(x=xp, y=yp, mode='markers', marker=dict(size=4, color='#a5bab7'), hoverinfo='none')
        return lines, points

Set the layout of the plot:


In [8]:
def  set_plot_layout(title, width=500, height=500,  showticklabels=True, ticks='outside'):
    axis=dict(showgrid=False,
              showline=False,
              showticklabels=showticklabels,
              ticks=ticks,
              zeroline=False)

    return Layout(title=title,
                    width=width,
                    height=height,
                    showlegend=False,
                    xaxis=XAxis(axis, title='Re(z)'),
                    yaxis=YAxis(axis, title='Im(z)'),
                    )

Define a function that associates to each point $z$ in a meshgrid, the strings containing the values to be displayed when hovering the mouse over that point:


In [9]:
def text_to_display(x, y, argum, modulus):
    m,n=x.shape
    return [['z='+'{:.2f}'.format(x[i][j])+'+' + '{:.2f}'.format(y[i][j])+' j'+'<br>arg(f(z))='+'{:.2f}'.format(argum[i][j])+\
       '<br>log(modulus(f(z)))='+'{:.2f}'.format(modulus[i][j])  for j in range(n)] for i in range(m)]

Finally, the function plotly_plot calls all above defined functions in order to generate the phase plot of a given complex function:


In [14]:
def plotly_plot(f, re=(-1,1), im=(-1,1), N=100, nr=10, title='', width=500, height=500, **kwargs):
    x, y, argument, log_modulus=computations(f, re=re, im=im, N=N)
    
    levels=get_levels(log_modulus, nr=nr)
    cp=plt.contour(x,y, log_modulus, levels=levels)
    cl=plotly_contour_lines(cp)
    text=text_to_display(x,y, argument, log_modulus.data)
    tickvals=[-np.pi, -2*np.pi/3, -np.pi/3, 0, np.pi/3, 2*np.pi/3, np.pi]
    ticktext=['-pi', '-2pi/3', '-pi/3', '0', 'pi/3', '2pi/3', 'pi']
    dom=Heatmap(x=x[0,:], y=y[:,0], z=argument,  colorscale=pl_hsv, showscale=True, text=text, hoverinfo='text',
            colorbar=dict(thickness=20, tickvals=tickvals, 
                          ticktext=ticktext, ticks='outside', 
                          ticklen=4, titleside='top', title='arg(f(z))'))
    if len(cl)==2 and type(cl[0]) is graph_objs.Scatter:
        data=Data([dom, cl[0], cl[1]])
    else: 
        data=Data([dom, cl])
    
    layout=set_plot_layout(title=title)    
    fig=Figure(data=data, layout =layout)
    fig['layout'].update( margin=dict(t=100))
    return fig

As an example we take the function $f(z)=\sin(z)/(1-cos(z^3))$:


In [15]:
fig=plotly_plot(lambda z: np.sin(z)/(1-np.cos(z**3)), re=(-2,2), im=(-2,2), 
                nr=22, title='f(z)=sin(z)/(1-cos(z^3))')

In [17]:
py.sign_in('empet', 'my_api_key')
py.iplot(fig, filename='phase-plot1')


Out[17]:

In [1]:
from IPython.core.display import HTML
def  css_styling():
    styles = open("./custom.css", "r").read()
    return HTML(styles)
css_styling()


Out[1]: