15

I have a list of tuples in python containing 3-dimenstional data, where each tuple is in the form: (x, y, z, data_value), i.e., I have data values at each (x, y, z) coordinate. I would like to make a 3D discrete heatmap plot where the colors represent the value of data_values in my list of tuples. Here, I give an example of such a heatmap for a 2D dataset where I have a list of (x, y, data_value) tuples:

import matplotlib.pyplot as plt
from matplotlib import colors
import numpy as np
from random import randint

# x and y coordinates
x = np.array(range(10))
y = np.array(range(10,15))
data = np.zeros((len(y),len(x)))

# Generate some discrete data (1, 2 or 3) for each (x, y) pair
for i,yy in enumerate(y):
    for j, xx in enumerate(x):
        data[i,j] = randint(1,3)

# Map 1, 2 and 3 to 'Red', 'Green' qnd 'Blue', respectively
colormap = colors.ListedColormap(['Red', 'Green', 'Blue'])
colorbar_ticklabels = ['1', '2', '3']

# Use matshow to create a heatmap
fig, ax = plt.subplots()
ms = ax.matshow(data, cmap = colormap, vmin=data.min() - 0.5, vmax=data.max() + 0.5, origin = 'lower')

# x and y axis ticks
ax.set_xticklabels([str(xx) for xx in x])
ax.set_yticklabels([str(yy) for yy in y])
ax.xaxis.tick_bottom()

# Put the x- qnd y-axis ticks at the middle of each cell 
ax.set_xticks(np.arange(data.shape[1]), minor = False)
ax.set_yticks(np.arange(data.shape[0]), minor = False)

# Set custom ticks and ticklabels for color bar
cbar = fig.colorbar(ms,ticks = np.arange(np.min(data),np.max(data)+1))
cbar.ax.set_yticklabels(colorbar_ticklabels)

plt.show()

This generates a plot like this: enter image description here

How can I make a similar plot in 3D-space (i.e., having a z-axis), if my data have a third dimension. For example, if

# x and y and z coordinates
x = np.array(range(10))
y = np.array(range(10,15))
z = np.array(range(15,20))
data = np.zeros((len(y),len(x), len(y)))

# Generate some random discrete data (1, 2 or 3) for each (x, y, z) triplet. 
# Am I defining i, j and k correctly here?
for i,yy in enumerate(y):
    for j, xx in enumerate(x):
        for k, zz in enumerate(z):
            data[i,j, k] = randint(1,3)

I sounds like plot_surface in mplot3d should be able to do this, but z in the input of this function is essentially the value of data at (x, y) coordinate, i.e., (x, y, z = data_value), which is different from what I have, i.e., (x, y, z, data_value).

user3076813
  • 499
  • 2
  • 6
  • 13
  • You want a 3d-surface where the color of the plot is a function of the x,y, and z? – James Nov 28 '16 at 21:13
  • Correct! With a discrete colorbar though. – user3076813 Nov 28 '16 at 21:30
  • You might want to look into `mayavi`'s [`contour3d`](http://docs.enthought.com/mayavi/mayavi/auto/mlab_helper_functions.html#mayavi.mlab.contour3d), it allows you to plot iso-surfaces of a scalar field in 3d. – berna1111 Nov 28 '16 at 22:48
  • If you do not mind a language transition and license issue, Mathematica has built-in `Image3D`, `ContourPlot3D` – Kh40tiK Nov 29 '16 at 09:25
  • 1
    Image3D in Mathematica sounds very much like what i want. I just wished there was an equivalent in python/matplotlib. – user3076813 Nov 29 '16 at 17:10

2 Answers2

24

New answer:

It seems we really want to have a 3D Tetris game here ;-)

So here is a way to plot cubes of different color to fill the space given by the arrays (x,y,z).

from mpl_toolkits.mplot3d import Axes3D
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm
import matplotlib.colorbar
import matplotlib.colors

def cuboid_data(center, size=(1,1,1)):
    # code taken from
    # http://stackoverflow.com/questions/30715083/python-plotting-a-wireframe-3d-cuboid?noredirect=1&lq=1
    # suppose axis direction: x: to left; y: to inside; z: to upper
    # get the (left, outside, bottom) point
    o = [a - b / 2 for a, b in zip(center, size)]
    # get the length, width, and height
    l, w, h = size
    x = [[o[0], o[0] + l, o[0] + l, o[0], o[0]],  # x coordinate of points in bottom surface
         [o[0], o[0] + l, o[0] + l, o[0], o[0]],  # x coordinate of points in upper surface
         [o[0], o[0] + l, o[0] + l, o[0], o[0]],  # x coordinate of points in outside surface
         [o[0], o[0] + l, o[0] + l, o[0], o[0]]]  # x coordinate of points in inside surface
    y = [[o[1], o[1], o[1] + w, o[1] + w, o[1]],  # y coordinate of points in bottom surface
         [o[1], o[1], o[1] + w, o[1] + w, o[1]],  # y coordinate of points in upper surface
         [o[1], o[1], o[1], o[1], o[1]],          # y coordinate of points in outside surface
         [o[1] + w, o[1] + w, o[1] + w, o[1] + w, o[1] + w]]    # y coordinate of points in inside surface
    z = [[o[2], o[2], o[2], o[2], o[2]],                        # z coordinate of points in bottom surface
         [o[2] + h, o[2] + h, o[2] + h, o[2] + h, o[2] + h],    # z coordinate of points in upper surface
         [o[2], o[2], o[2] + h, o[2] + h, o[2]],                # z coordinate of points in outside surface
         [o[2], o[2], o[2] + h, o[2] + h, o[2]]]                # z coordinate of points in inside surface
    return x, y, z

def plotCubeAt(pos=(0,0,0), c="b", alpha=0.1, ax=None):
    # Plotting N cube elements at position pos
    if ax !=None:
        X, Y, Z = cuboid_data( (pos[0],pos[1],pos[2]) )
        ax.plot_surface(X, Y, Z, color=c, rstride=1, cstride=1, alpha=0.1)

def plotMatrix(ax, x, y, z, data, cmap="jet", cax=None, alpha=0.1):
    # plot a Matrix 
    norm = matplotlib.colors.Normalize(vmin=data.min(), vmax=data.max())
    colors = lambda i,j,k : matplotlib.cm.ScalarMappable(norm=norm,cmap = cmap).to_rgba(data[i,j,k]) 
    for i, xi in enumerate(x):
            for j, yi in enumerate(y):
                for k, zi, in enumerate(z):
                    plotCubeAt(pos=(xi, yi, zi), c=colors(i,j,k), alpha=alpha,  ax=ax)



    if cax !=None:
        cbar = matplotlib.colorbar.ColorbarBase(cax, cmap=cmap,
                                norm=norm,
                                orientation='vertical')  
        cbar.set_ticks(np.unique(data))
        # set the colorbar transparent as well
        cbar.solids.set(alpha=alpha)              



if __name__ == '__main__':

    # x and y and z coordinates
    x = np.array(range(10))
    y = np.array(range(10,15))
    z = np.array(range(15,20))
    data_value = np.random.randint(1,4, size=(len(x), len(y), len(z)) )
    print data_value.shape

    fig = plt.figure(figsize=(10,4))
    ax = fig.add_axes([0.1, 0.1, 0.7, 0.8], projection='3d')
    ax_cb = fig.add_axes([0.8, 0.3, 0.05, 0.45])
    ax.set_aspect('equal')

    plotMatrix(ax, x, y, z, data_value, cmap="jet", cax = ax_cb)

    plt.savefig(__file__+".png")
    plt.show()

enter image description here I find it really hard to see anything here, but that may be a question of taste and now hopefully also answers the question.


Original Answer:

It seems I misunderstood the question. Therefore the following does not answer the question. For the moment I leave it here, to keep the comments below available for others.

I think plot_surface is fine for the specified task.

Essentially you would plot a surface with the shape given by your points X,Y,Z in 3D and colorize it using the values from data_values as shown in the code below.

from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import matplotlib.pyplot as plt
import numpy as np

fig = plt.figure()
ax = fig.gca(projection='3d')

# as plot_surface needs 2D arrays as input
x = np.arange(10)
y = np.array(range(10,15))
# we make a meshgrid from the x,y data
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))

# data_value shall be represented by color
data_value = np.random.rand(len(y), len(x))
# map the data to rgba values from a colormap
colors = cm.ScalarMappable(cmap = "viridis").to_rgba(data_value)


# plot_surface with points X,Y,Z and data_value as colors
surf = ax.plot_surface(X, Y, Z, rstride=1, cstride=1, facecolors=colors,
                       linewidth=0, antialiased=True)

plt.show()

enter image description here

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • 1
    This is not exactly what I want. In your example, you have only ONE value for z for each (x,y) pair. In my case, z is an independent coordinate, i.e., for each (x,y) pair, I'll have all possible values for z (not just a single value). I case, I am missing something, that would greatly help if you modify your example using the 3D data I gave in my example code. – user3076813 Nov 29 '16 at 15:34
  • 1
    @user3076813 I might have completely misunderstood your question. But in that case I think you first need to define your mapping. I'm indeed mapping from R x R -> R (2D -> 1D mapping). In case you really want a R x R x R -> R mapping, how should the resulting plot look like? A big pile of semitransparent cubes? Once you have defined that, we can have a look if it's possible with matplotlib. – ImportanceOfBeingErnest Nov 29 '16 at 16:32
  • 1
    I actually want a R x R x R --> Z+ mapping (where Z+ is the set of non-negative integers). For the 2D example I gave above I have a colored square for each (x, y) point. For the 3D case, I expect to have a (semitransparent, if possible) colored cube for each (x,y, z) point. – user3076813 Nov 29 '16 at 17:08
  • 1
    Ok, so what is rather confusing is that you answered the question in the comments "You want a 3d-surface [...]" with "Correct". In a way I highly doubt that what you want is actually giving an informative plot. – ImportanceOfBeingErnest Nov 29 '16 at 17:42
  • 1
    Sorry for the confusion. I guess I had misunderstood that question. So, there is no way of doing something like this in python? Any suggestions for making a more informative plot? – user3076813 Nov 29 '16 at 18:09
  • So, I tinkered a new solution to the question with a lot of cubes. Concerning a more informative plot, I have no idea if the data is really randomly distributed. If however it has some structure, there might be good ways by using isosurfaces or isolines. You can of course provide some more info on your real data or see if you find some image on how others have plotted similar data. – ImportanceOfBeingErnest Nov 29 '16 at 18:26
  • This looks very much like what I wanted. You are calling plot_surface for each cube separately though. How can we add the colorbar to the entire graph then, like the one in the 2D example? Also, where in your code specify the transparency level for the colors/cubes (if you do it at all)? – user3076813 Nov 29 '16 at 19:46
  • I added a colorbar to the solution. Transparency is set via `alpha=0.1` in the `plotCubeAt()` method (alpha ranges between 0 (transparent) to 1 (opaque)). And yes, I'm calling plot_surface for each cube. Is this a problem? How would you do it otherwise? – ImportanceOfBeingErnest Nov 29 '16 at 20:22
  • No problem with using calling plot_surface for each cube. I just didn't know how to add the colorbar with that. One thing about your colorbar: Here we have only three data values 1, 2 or 3. So, I expect to get a colorbar with three distinct colors (e.g., Red, Green, Blue, like the one for the 2D example). Yours shows a continuum of colors, instead. – user3076813 Nov 29 '16 at 21:00
  • Disregarding what OP wants, the original answer is a quite nifty idea to visualize basically 4D data sets. nice! – mxmlnkn Aug 26 '17 at 14:02
  • This is broken on newer matplot lib. I tried fixing a couple of errors, but gave up. eg. `AttributeError: 'list' object has no attribute 'ndim'` – Navin May 06 '21 at 19:06
1

I've update the code above to be compatible with newer version of matplot lib.

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colorbar
from matplotlib import cm

viridis = cm.get_cmap('plasma', 8) #Our color map

def cuboid_data(center, size=(1,1,1)):
    # code taken from
    # http://stackoverflow.com/questions/30715083/python-plotting-a-wireframe-3d-cuboid?noredirect=1&lq=1
    # suppose axis direction: x: to left; y: to inside; z: to upper
    # get the (left, outside, bottom) point
    o = [a - b / 2 for a, b in zip(center, size)]
    # get the length, width, and height
    l, w, h = size
    x =  np.array([[o[0], o[0] + l, o[0] + l, o[0], o[0]],      # x coordinate of points in bottom surface
         [o[0], o[0] + l, o[0] + l, o[0], o[0]],                # x coordinate of points in upper surface
         [o[0], o[0] + l, o[0] + l, o[0], o[0]],                # x coordinate of points in outside surface
         [o[0], o[0] + l, o[0] + l, o[0], o[0]]])               # x coordinate of points in inside surface
    y =  np.array([[o[1], o[1], o[1] + w, o[1] + w, o[1]],      # y coordinate of points in bottom surface
         [o[1], o[1], o[1] + w, o[1] + w, o[1]],                # y coordinate of points in upper surface
         [o[1], o[1], o[1], o[1], o[1]],                        # y coordinate of points in outside surface
         [o[1] + w, o[1] + w, o[1] + w, o[1] + w, o[1] + w]])   # y coordinate of points in inside surface
    z =  np.array([[o[2], o[2], o[2], o[2], o[2]],              # z coordinate of points in bottom surface
         [o[2] + h, o[2] + h, o[2] + h, o[2] + h, o[2] + h],    # z coordinate of points in upper surface
         [o[2], o[2], o[2] + h, o[2] + h, o[2]],                # z coordinate of points in outside surface
         [o[2], o[2], o[2] + h, o[2] + h, o[2]]])               # z coordinate of points in inside surface
    return x, y, z

def plotCubeAt(pos=(0,0,0), c="b", alpha=0.1, ax=None):
    # Plotting N cube elements at position pos
    if ax !=None:
        X, Y, Z = cuboid_data( (pos[0],pos[1],pos[2]) )
        ax.plot_surface(X, Y, Z, color=c, rstride=1, cstride=1, alpha=0.1)

def plotMatrix(ax, x, y, z, data, cmap=viridis, cax=None, alpha=0.1):
    # plot a Matrix 
    norm = matplotlib.colors.Normalize(vmin=data.min(), vmax=data.max())
    colors = lambda i,j,k : matplotlib.cm.ScalarMappable(norm=norm,cmap = cmap).to_rgba(data[i,j,k]) 
    for i, xi in enumerate(x):
            for j, yi in enumerate(y):
                for k, zi, in enumerate(z):
                    plotCubeAt(pos=(xi, yi, zi), c=colors(i,j,k), alpha=alpha,  ax=ax)



    if cax !=None:
        cbar = matplotlib.colorbar.ColorbarBase(cax, cmap=cmap,
                                norm=norm,
                                orientation='vertical')  
        cbar.set_ticks(np.unique(data))
        # set the colorbar transparent as well
        cbar.solids.set(alpha=alpha)              



if __name__ == '__main__':

    # x and y and z coordinates
    x = np.array(range(10))
    y = np.array(range(10,15))
    z = np.array(range(15,20))
    data_value = np.random.randint(1,4, size=(len(x), len(y), len(z)) )
    print(data_value.shape)

    fig = plt.figure(figsize=(10,4))
    ax = fig.add_axes([0.1, 0.1, 0.7, 0.8], projection='3d')
    ax_cb = fig.add_axes([0.8, 0.3, 0.05, 0.45])
    ax.set_aspect('auto')

    plotMatrix(ax, x, y, z, data_value, cmap=viridis, cax = ax_cb)

    plt.savefig(__file__+".png")
    plt.show()
Lebonq
  • 11
  • 2