The main goals of this post are:
- Provide bullet points about Matplotlib’s architecture and provide documentation for more in depth exploration.
- Build two examples plotting multidimensional data with basic interactive capabilities.
1. Matplotlib architecture bullets
In order to enable Matplotlib’s interactive capabilities, it doesn’t hurt to understand how it is structured. The current matplotlib architecture is comprised of three layers: the scripting layer, the artist layer and backend layer, which interact in the following way:
- The data is either created or loaded in the scripting layer, this layer basically supports the programmatic interaction and provides users with the ability to manipulate figures with a syntax that is somewhat intuitive.
- The data is transformed into various objects in the artist layer; it is adjusted as scripted. This layer is responsible for the abstraction of each visual component that you see in a figure.
- These objects are then rendered by the backend. This last layer enables the users to create, render, and update the figure objects. Figures can be displayed and interacted with via common user interface events such as the keyboard and mouse inputs.
Matplotlib has extensive documentation; however, the best way to learn about backends is exploring the sourcecode. Some of the documents that I also found helpful, aside from the wonderful stackoverflow, are:
Raman, Kirthi. Mastering Python Data Visualization. Packt Publishing Limited, 2015.
Root, Benjamin V. “Interactive Applications Using Matplotlib.” (2015).
McGreggor, Duncan M. “Mastering matplotlib.” (2015).
2. Example: Fun click
Here’s a simple example that connects a 5D scatter plot to a pick and a mouse click event. The goal is to show information about the point being clicked. In this case, the row index of the data matrix and the corresponding values are shown in the python terminal as shown in the following figure:This could be useful when plotting a multidimensional Pareto Front. If you see a solution of interest in your scatter plot, you can directly access its exact values and its index by a simple mouse click. This index may be useful to track the corresponding decision vector in your decision matrix. Also, note that even if I am only plotting a 5D scatter plot, I used a 6 D data matrix, hence, I can see what the sixth value is on the terminal. The following code was used to generate the previous figure. Parts 2.1. through 2.5 were adapted from the Visualization strategies for multidimensional data post, refer to this link for a detailed explanation of these fragments.
2.1. Required Libraries
import pylab as plt mpl_toolkits.mplot3d.axes3d as p3 import numpy as np import seaborn # not required
2.2. Loading or generating data
#data= np.loadtxt('your_data.txt’) data= X =np.random.random((50,6)) # setting a random matrix
2.3. Accessing object oriented Matplotlib’s objective oriented plotting
fig, ax = plt.subplots(1, 1) ax = fig.add_subplot(111, projection = '3d') im= ax.scatter(data[:,0], data[:,1],data[:,2], c=data[:,3], s= data[:,4]*scale, alpha=1, cmap=plt.cm.spectral, picker=True)
2.4. Setting the main axis labels
ax.set_xlabel('OBJECTIVE 1') ax.set_ylabel('OBJECTIVE 2') ax.set_zlabel('OBJECTIVE 3')
2.5. Setting colorbar and its label vertically
cbar= fig.colorbar(im) cbar.ax.set_ylabel('OBJECTIVE 4')
2.6. Setting size legend
objs=data[:,4] max_size=np.amax(objs)*scale min_size=np.amin(objs)*scale handles, labels = ax.get_legend_handles_labels() display = (0,1,2) size_max = plt.Line2D((0,1),(0,0), color='k', markersize=max_size,linestyle='') size_min = plt.Line2D((0,1),(0,0), color='k', markersize=min_size,linestyle='') legend1= ax.legend([handle for i,handle in enumerate(handles) if i in display]+[size_max,size_min], [label for i,label in enumerate(labels) if i in display]+["%.2f"%(np.amax(objs)), "%.2f"%(np.amin(objs))], labelspacing=1.5, title='OBJECTIVE 5', loc=1, frameon=True, numpoints=1, markerscale=1)
2.7. Setting the picker function
This part defines and connects the events. The mpl_connect() method connects the event to the figure. It accepts two arguments, the name of the event and the callable object (such as a function).
def onpick(event):ind = event.ind print ('index: %d\nobjective 1: %0.2f\nobjective 2: %0.2f\nobjective 3: %0.2f\nobjective 4: %0.2f\nobjective 5: %0.2f\nobjective 6: %0.2f' % (event.ind[0],data[ind,0],data[ind,1],data[ind,2],data[ind,3],data[ind,4],data[ind,5])) fig.canvas.mpl_connect('pick_event', onpick) plt.show
For a list of events that Matplotlib supports, please refer to: Matplotlib event options. To download the previous code go to the following link: https://github.com/JazminZatarain/Basic-interactive-plotting/blob/master/fun_click.py
3. Example: Fun Annotation
Transitioning to a slightly more sophisticated interaction, we use annotations to link the event to a figure. That is, the information is shown directly in the figure as opposed to the terminal as in the previous example. In the following snippet, the row index of the data matrix is shown directly in the figure canvas by simply clicking on the point of interest.
The following code was adapted from Matplotlib’s documentation and this stackoverflow post to execute the previous figure. Sections 3.1. and 3.2. define our scatter plot with its corresponding labels as we saw in the previous example.
3.1. Required Libraries
import matplotlib.pyplot as plt, numpy as np from mpl_toolkits.mplot3d import proj3d
3.2. Visualizing data in 3d plot with popover next to mouse position
def visualize3DData (X,scale,cmap): fig = plt.figure(figsize = (16,10)) ax = fig.add_subplot(111, projection = '3d') im= ax.scatter(X[:, 0], X[:, 1], X[:, 2], c= X[:, 3], s= X[:, 4]*scale, cmap=cmap,     alpha=1, picker = True) ax.set_xlabel('OBJECTIVE 1') ax.set_ylabel('OBJECTIVE 2') ax.set_zlabel('OBJECTIVE 3') cbar= fig.colorbar(im) cbar.ax.set_ylabel('OBJECTIVE 4') objs=X[:,4] max_size=np.amax(objs)*scale min_size=np.amin(objs)*scale handles, labels = ax.get_legend_handles_labels() display = (0,1,2) size_max = plt.Line2D((0,1),(0,0), color='k', marker='o', markersize=max_size,linestyle='') size_min = plt.Line2D((0,1),(0,0), color='k', marker='o', markersize=min_size,linestyle='') legend1= ax.legend([handle for i,handle in enumerate(handles) if i in display]+[size_max,size_min], [label for i,label in enumerate(labels) if i in display]+["%.2f"%(np.amax(objs)), "%.2f"%(np.amin(objs))], labelspacing=1.5, title='OBJECTIVE 5', loc=1, frameon=True, numpoints=1, markerscale=1)
3.3. Return distance between mouse position and given data point
def distance(point, event): assert point.shape == (3,), "distance: point.shape is wrong: %s, must be (3,)" % point.shape
3.4. Project 3d data space to 2d data space
x2, y2, _ = proj3d.proj_transform(point[0], point[1], point[2], plt.gca().get_proj()) # Convert 2d data space to 2d screen space x3, y3 = ax.transData.transform((x2, y2)) return np.sqrt ((x3 - event.x)**2 + (y3 - event.y)**2)
3.5. Calculate which data point is closest to the mouse position
def calcClosestDatapoint(X, event): distances = [distance (X[i, 0:3], event) for i in range(X.shape[0])] return np.argmin(distances) def annotatePlot(X, index): # If we have previously displayed another label, remove it first if hasattr(annotatePlot, 'label'): annotatePlot.label.remove() Get data point from array of points X, at position index: x2, y2,_ = proj3d.proj_transform(X[index, 0], X[index, 1], X[index, 2], ax.get_proj())
3.6. Specify the information to be plotted in the annotation label
annotatePlot.label = plt.annotate("index: %d" % index, xy = (x2, y2), xytext = (-20,20), textcoords = 'offset points', ha = 'right', va = 'bottom', bbox = dict(boxstyle = 'round,pad=0.5', fc = 'yellow', alpha = 0.5), arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3,rad=0')) fig.canvas.draw()
3.7. Defining the event
This last part defines the event that is triggered when the mouse is moved. It also shows the text annotation over the data point closest to the mouse.
def onMouseMotion(event): closestIndex = calcClosestDatapoint(X, event) annotatePlot (X, closestIndex) fig.canvas.mpl_connect('motion_notify_event', onMouseMotion)  # on mouse motion plt.show()
3.8. You can forget the previous code and simply insert your data here
if __name__ == '__main__': import seaborn #X=np.loadtxt('your_data.txt’) # your data goes here X = np.random.random((50,6)) #this is the randomly generated data for this example scale=1000 # scale of the size objective cmap=plt.cm.spectral # choose your colormap visualize3DData (X,scale,cmap)
To download the previous code go to the following link: https://github.com/JazminZatarain/Basic-interactive-plotting/blob/master/annotacious.py