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:
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()
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.