How to organize the position of the legend, colorbar and image in the plot?

I'm trying to plot some data, and I don't like the organization of the items. For example, I would like to have a bigger image and a smaller colorbar. When I modify the size of the figure still not proportional. And I also would like to tag each borehole to the legend, so I can identify it.

This is the image I have now: enter image description here

and this is the code:

# Create data
l = [2, 3, 4, 5,6]
n = 20
labels = [item for item in l for i in range(n)]
random.shuffle(labels,random.random)
labels =np.array(labels)
label_unique = np.unique(labels)

n = 100
x = np.linspace(613000, 615000, num=n) + np.random.uniform(-5, 5, size=n)
y = np.linspace(7763800, 7765800, num=n) + np.random.uniform(-5, 5, size=n)
z = np.linspace(1230, 1260, num=n) + np.random.uniform(-5, 5, size=n)
cpt1 = pd.DataFrame(list(zip(x, y, z,labels)),
              columns=['x','y', 'z','labels'])

l = [2, 3, 4, 5,6]
n = 60
labels = [item for item in l for i in range(n)]
random.shuffle(labels,random.random)
labels =np.array(labels)
label_unique = np.unique(labels)


cpt2 = pd.DataFrame(list(zip(x, y, z,labels)),
              columns=['x','y', 'z','labels'])

n = 400
x = np.linspace(613000, 615000, num=n) + np.random.uniform(-7, 7, size=n)
y = np.linspace(7763800, 7765800, num=n) + np.random.uniform(-7, 7, size=n)
z = np.linspace(1230, 1260, num=n) + np.random.uniform(-7, 7, size=n)
l = [2, 3, 4, 5,6]
n = 80
labels = [item for item in l for i in range(n)]
random.shuffle(labels,random.random)
labels =np.array(labels)
label_unique = np.unique(labels)

cpt3 = pd.DataFrame(list(zip(x, y, z,labels)),
              columns=['x','y', 'z','labels'])

cpt = [cpt1,cpt2,cpt3]

legend = cpt1.columns.values.tolist()


fig = plt.figure(figsize = (20, 9))
ax = plt.axes(projection ="3d")

# Add x, y gridlines
ax.grid(b = True, color ='grey',
        linestyle ='-.', linewidth = 0.3,
        alpha = 0.2)


# Creating color map
my_cmap = plt.get_cmap('hsv')
for  count, c in enumerate(cpt):
    x = c.x
    y = c.y
    z = c.z
    colorz = c.labels



    # Creating plot
    sctt = ax.scatter3D(x, y, z,
                        alpha = 0.8,
                        c = colorz,
                        cmap = my_cmap,
                        marker ='^',label = legend[count])

ax.set_xlabel('X-axis', fontweight ='bold')
ax.set_ylabel('Y-axis', fontweight ='bold')
ax.set_zlabel('Z-axis', fontweight ='bold')
fig.colorbar(sctt, ax = ax, shrink = 0.3, aspect = 5,orientation="horizontal")
plt.legend(bbox_to_anchor=(1.5,1), loc="upper left")
plt.show()


Solution 1:

Two parts to the question, and at least three parts to this answer.

Setting up the imports and the synthetic data. Whenever I find myself retyping or copy/pasting a complicated line with different parameters, I put in a function instead:



from matplotlib import pyplot as plt
import pandas as pd
import numpy as np

# Fake data of about the right shape
def syncoords(pars, n):
    '''pars: tuple or list of min, max, abs_variance'''
    return np.linspace(pars[0], pars[1], num=n) + \
        np.random.uniform(-1*pars[2], pars[2], size=n)

def synbore(n, xparams, yparams, zparams, zonevalues):
    '''create n entries for x,y,z, and zone from parameter tuples
       xyzparams: tuple of min, max, abs_variance
       zonevalues: list of zone values'''
    return pd.DataFrame({'x': syncoords(xparams, n),
                         'y': syncoords(yparams, n),
                         'z': syncoords(zparams, n),
                         'Zone': np.random.choice(zonevalues, size=n)})


boreparams = [['melaza', 10,
               (61300, 61500, 5), (77638, 77658, 5), (5023, 5400, .5),
               [2,3,4,5,6]],
              ['miel',   23,
               (45000, 45555, 5), (69712, 68800, 5), (4701, 5100, .7),
               [2,3,4,5,6]],
              ['jalea',  50,
               (50432, 50000, 6), (38200, 38600, 6), (5050, 5600, .9),
               [4,5,6,7,8]]] 

I didn't stay with the list of dataframes because I always want my data to "travel with" its ID strings. When I have two lists, I have to verify that edits and updates always still match. Making it a dictionary didn't make the rest of the code any longer, so a dictionary of our data sets:


# I like my data to travel with its ID, which dictionaries are great for. 
# boredict entries: {"ID": xyzZone_dataframe}
# easy to make a dict from a list of (k, v) pairs, 
# so a lambda function to do that:
boredict = dict(map(lambda l:(l[0],
                              synbore(l[1],l[2],l[3],l[4],l[5])),
                    boreparams))

# Get ready to plot
fig = plt.figure(figsize=(11, 8.5)) # Or A? papersize
ax = plt.axes(projection ="3d")
ax.set_xlabel('X-axis', fontweight ='bold')
ax.set_ylabel('Y-axis', fontweight ='bold')
ax.set_zlabel('Z-axis', fontweight ='bold')
ax.grid(b = True, color ='grey',
        linestyle ='-.', linewidth = 0.3,
        alpha = 0.2)

# TODO: collect the max-min of all the Zones so one colormap works for all
# see https://matplotlib.org/stable/tutorials/colors/colormapnorms.html
# and https://matplotlib.org/stable/tutorials/colors/colorbar_only.html

for bname in boredict:
    # plot the actual bore data in 3D+colormap
    bdata = boredict[bname]
    sctt = ax.scatter3D(bdata.x, bdata.y, bdata.z,
                        alpha = 0.8,
                        c = bdata.Zone,
                        cmap = plt.get_cmap('hsv'),
                        marker ='^')
    # and a different marker to match the bore with the legend 
    ax.scatter3D(bdata.x[-1:], bdata.y[-1:], bdata.z[-1:] + 25,
                 marker = 'v',
                 s = 80, 
                 label = bname) 

Finally the plot layout management. 3D plots need a lot of whitespace for the corners to rotate in, but you can trim padding off the colorbar (pad = 0) and off the figure itself, using subplots_adjust. I liked a bigger but skinnier colorbar, too.


fig.colorbar(sctt, ax = ax,
             shrink = 0.4, aspect = 16, pad = 0, 
             orientation="horizontal")

plt.legend(bbox_to_anchor=(1.1, .8), loc="upper left")
fig.subplots_adjust(left=0, right=1,bottom=0,top=1) #reduce whitespace around the fig
plt.show()

3D scatterplot of color-coded borehole data, with stars at the top of each borehole to match legend entries to.

There's one more thing this plot is going to need -- Here we create a colorbar based on the last dataframe to be plotted in the loop, and only that dataframe. But maybe the dataframes have different Zone data ranges! We want a colorbar that applies accurately to all the data at once. That means looking at all the data twice, once to figure out what the colorbar ranges will be and then again to plot them all with the overall colorbar. I put a #TODO comment in the code where you'd do this, with links to existing questions/answers/examples.