2

Consider a 3D bar plot with custom grid lines:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from matplotlib.ticker import MultipleLocator
# This import registers the 3D projection, but is otherwise unused.
from mpl_toolkits.mplot3d import Axes3D  # noqa: F401 unused import

fig = plt.figure(figsize=(20, 10))
ax = fig.add_subplot(111, projection='3d')

ax.xaxis.set_major_locator(MultipleLocator(1))
ax.yaxis.set_major_locator(MultipleLocator(1))
ax.zaxis.set_major_locator(MultipleLocator(2))

nx = 10
ny = 10

colors = cm.tab20(np.linspace(0, 1, nx))
width = depth = 0.1

for x in np.arange(nx):
    for y in np.arange(ny):
        ax.bar3d(x, y, 0, width, depth, x+y, shade=False, color = colors[x], edgecolor = 'black')

plt.show()

3D bar plot example

How can I place the bars so that the bars are centered where the grid lines cross each other in the xy plane?

I'm thinking about something like

ax.bar3d(x+0.5*depth, y+0.5*width, ...)

only it is not clear to me what the offset is that matplotlib uses. It should work for all depth and width values.

For 2D bar plots there is an argument for this, align = 'center', but it doesn't seem to work for 3D.

denis
  • 21,378
  • 10
  • 65
  • 88
C. E.
  • 10,297
  • 10
  • 53
  • 77

1 Answers1

3

What looks to you as a shift in coordinates is really just the projection in combination with the margins of the axes. Hence even if the bars are correctly positionned in their center they look offset and that offset is dependent on the axes size, viewing angle etc.

The solution to this is in principle given in this Q&A: Removing axes margins in 3D plot

You would center the bars by subtracting half of their width and add a patch to remove the margin of the zaxis. Then setting the lower z limit to 0 pins the bars to the grid and makes them look centered for any viewing angle.

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from matplotlib.ticker import MultipleLocator
from mpl_toolkits.mplot3d import Axes3D 
from mpl_toolkits.mplot3d.axis3d import Axis

def _get_coord_info_new(self, renderer):
    mins, maxs, cs, deltas, tc, highs = self._get_coord_info_old(renderer)
    correction = deltas * [0,0,1.0/4]
    mins += correction
    maxs -= correction
    return mins, maxs, cs, deltas, tc, highs
if not hasattr(Axis, "_get_coord_info_old"):
    Axis._get_coord_info_old = Axis._get_coord_info  
Axis._get_coord_info = _get_coord_info_new


fig = plt.figure(figsize=(20, 10))
ax = fig.add_subplot(111, projection='3d')

ax.xaxis.set_major_locator(MultipleLocator(1))
ax.yaxis.set_major_locator(MultipleLocator(1))
ax.zaxis.set_major_locator(MultipleLocator(2))

nx = 10
ny = 10

colors = cm.tab20(np.linspace(0, 1, nx))
width = depth = 0.1

for x in np.arange(nx):
    for y in np.arange(ny):
        ax.bar3d(x-width/2., y-depth/2., 0, width, depth, x+y, shade=False, 
                 color = colors[x], edgecolor = 'black')

ax.set_zlim(0,None)

plt.show()

enter image description here

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • Thank you. This solution is more complicated than I had expected, but I guess it is what it is. Does this make sense to you, i.e. do you understand why they chose to not let the bars start at z = 0? – C. E. Sep 19 '18 at 11:40
  • I can only guess here, but it seems the automatic margin of the axes was introduced without having cases like barplots, which should stick to the planes in mind. – ImportanceOfBeingErnest Sep 19 '18 at 11:44