Draw Bounding Box Around Matplotlib 3D Plots in Python

In this tutorial, you’ll learn how to draw bounding boxes around 3D plots in Python using Matplotlib.

You’ll explore various methods to create, customize, and optimize bounding boxes for different types of 3D plots.

 

 

Calculate Bounding Box Coordinates

To create a bounding box, you first need to determine its coordinates.

You can do this by finding the minimum and maximum values for each axis in your data.

Here’s how you can calculate the bounding box coordinates for a simple 3D scatter plot:

import numpy as np
np.random.seed(42)
data = np.random.rand(100, 3) * 10
x_min, x_max = data[:, 0].min(), data[:, 0].max()
y_min, y_max = data[:, 1].min(), data[:, 1].max()
z_min, z_max = data[:, 2].min(), data[:, 2].max()
print(f"X range: {x_min:.2f} to {x_max:.2f}")
print(f"Y range: {y_min:.2f} to {y_max:.2f}")
print(f"Z range: {z_min:.2f} to {z_max:.2f}")

Output:

X range: 0.06 to 9.90
Y range: 0.05 to 9.86
Z range: 0.07 to 9.70

The code generates random 3D data points and calculates the minimum and maximum values for each axis.

These values define the corners of the bounding box.

For different data types, you need to adjust your method to calculate the bounding box coordinates.

Here’s an example for a surface plot:

x = np.linspace(-5, 5, 50)
y = np.linspace(-5, 5, 50)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))
x_min, x_max = X.min(), X.max()
y_min, y_max = Y.min(), Y.max()
z_min, z_max = Z.min(), Z.max()
print(f"X range: {x_min:.2f} to {x_max:.2f}")
print(f"Y range: {y_min:.2f} to {y_max:.2f}")
print(f"Z range: {z_min:.2f} to {z_max:.2f}")

Output:

X range: -5.00 to 5.00
Y range: -5.00 to 5.00
Z range: -1.00 to 1.00

For surface plots, you need to consider the entire mesh grid when calculating the bounding box coordinates.

 

Draw the Bounding Box

You can draw the bounding box using Matplotlib plot3D function:

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
np.random.seed(42)
data = np.random.rand(100, 3) * 10
x_min, x_max = data[:, 0].min(), data[:, 0].max()
y_min, y_max = data[:, 1].min(), data[:, 1].max()
z_min, z_max = data[:, 2].min(), data[:, 2].max()
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')
ax.scatter(data[:, 0], data[:, 1], data[:, 2], c='b', marker='o')
ax.plot3D([x_min, x_max], [y_min, y_min], [z_min, z_min], 'r--')
ax.plot3D([x_min, x_max], [y_max, y_max], [z_min, z_min], 'r--')
ax.plot3D([x_min, x_max], [y_min, y_min], [z_max, z_max], 'r--')
ax.plot3D([x_min, x_max], [y_max, y_max], [z_max, z_max], 'r--')
ax.plot3D([x_min, x_min], [y_min, y_max], [z_min, z_min], 'r--')
ax.plot3D([x_max, x_max], [y_min, y_max], [z_min, z_min], 'r--')
ax.plot3D([x_min, x_min], [y_min, y_max], [z_max, z_max], 'r--')
ax.plot3D([x_max, x_max], [y_min, y_max], [z_max, z_max], 'r--')
ax.plot3D([x_min, x_min], [y_min, y_min], [z_min, z_max], 'r--')
ax.plot3D([x_max, x_max], [y_min, y_min], [z_min, z_max], 'r--')
ax.plot3D([x_min, x_min], [y_max, y_max], [z_min, z_max], 'r--')
ax.plot3D([x_max, x_max], [y_max, y_max], [z_min, z_max], 'r--')
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
ax.set_title('3D Scatter Plot with Bounding Box using plot3D')
plt.show()

Output:

Bounding Box

This method uses plot3D to draw each edge of the bounding box individually. It provides more control over the appearance of each edge.

 

Handle Different Plot Types

Surface Plots

For surface plots, you can adapt the bounding box drawing method:

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from itertools import product, combinations
def plot_3d_surface_with_bbox(X, Y, Z):
    fig = plt.figure(figsize=(10, 8))
    ax = fig.add_subplot(111, projection='3d')
    surf = ax.plot_surface(X, Y, Z, cmap='viridis')
    x_min, x_max = X.min(), X.max()
    y_min, y_max = Y.min(), Y.max()
    z_min, z_max = Z.min(), Z.max()
    for s, e in combinations(np.array(list(product([x_min, x_max], [y_min, y_max], [z_min, z_max]))), 2):
        if np.sum(np.abs(s-e)) == x_max-x_min or np.sum(np.abs(s-e)) == y_max-y_min or np.sum(np.abs(s-e)) == z_max-z_min:
            ax.plot3D(*zip(s, e), color="r", linestyle="--")
    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_zlabel('Z')
    ax.set_title('3D Surface Plot with Bounding Box')
    plt.colorbar(surf)
    plt.show()
x = np.linspace(-5, 5, 50)
y = np.linspace(-5, 5, 50)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))
plot_3d_surface_with_bbox(X, Y, Z)

Output:

Surface Plot bounding box

Wireframe Plots

For wireframe plots, you can use a similar approach:

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from itertools import product, combinations
def plot_3d_wireframe_with_bbox(X, Y, Z):
    fig = plt.figure(figsize=(10, 8))
    ax = fig.add_subplot(111, projection='3d')
    ax.plot_wireframe(X, Y, Z, color='b')
    x_min, x_max = X.min(), X.max()
    y_min, y_max = Y.min(), Y.max()
    z_min, z_max = Z.min(), Z.max()
    for s, e in combinations(np.array(list(product([x_min, x_max], [y_min, y_max], [z_min, z_max]))), 2):
        if np.sum(np.abs(s-e)) == x_max-x_min or np.sum(np.abs(s-e)) == y_max-y_min or np.sum(np.abs(s-e)) == z_max-z_min:
            ax.plot3D(*zip(s, e), color="r", linestyle="--")
    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_zlabel('Z')
    ax.set_title('3D Wireframe Plot with Bounding Box')
    plt.show()
x = np.linspace(-5, 5, 20)
y = np.linspace(-5, 5, 20)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))
plot_3d_wireframe_with_bbox(X, Y, Z)

Output:

Wireframe Plot bounding box

This function creates a 3D wireframe plot with a bounding box, using the same bounding box method as before.

Volumetric Data

For volumetric data, you can use the voxels function to create a 3D plot:

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from itertools import product, combinations
def plot_3d_voxels_with_bbox(voxels):
  fig = plt.figure(figsize=(10, 8))
  ax = fig.add_subplot(111, projection='3d')  
  ax.voxels(voxels, edgecolor='k')  
  x_min, x_max = 0, voxels.shape[0] - 1
  y_min, y_max = 0, voxels.shape[1] - 1
  z_min, z_max = 0, voxels.shape[2] - 1  
  for s, e in combinations(np.array(list(product([x_min, x_max], [y_min, y_max], [z_min, z_max]))), 2):
      if np.sum(np.abs(s-e)) == x_max-x_min or np.sum(np.abs(s-e)) == y_max-y_min or np.sum(np.abs(s-e)) == z_max-z_min:
          ax.plot3D(*zip(s, e), color="r", linestyle="--")  
  ax.set_xlabel('X')
  ax.set_ylabel('Y')
  ax.set_zlabel('Z')
  ax.set_title('3D Voxel Plot with Bounding Box')  
  plt.show()
x, y, z = np.indices((10, 10, 10))
voxels = (x == y) | (y == z) | (x == z)
plot_3d_voxels_with_bbox(voxels)

Output:

voxel plot box

This function creates a 3D voxel plot with a bounding box. The voxel data is represented as a boolean 3D array, where True values are shown as filled cubes.

 

Dynamically Adjust the Bounding Box

To create an animated plot with a dynamically adjusting bounding box, you can use Matplotlib animation functionality:

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.animation import FuncAnimation
from itertools import product, combinations
def update_plot(frame, data, scatter, bbox_lines):
    # Update data
    new_data = data + np.random.randn(100, 3) * 0.2
    scatter._offsets3d = (new_data[:, 0], new_data[:, 1], new_data[:, 2])

    # Update bounding box
    x_min, x_max = new_data[:, 0].min(), new_data[:, 0].max()
    y_min, y_max = new_data[:, 1].min(), new_data[:, 1].max()
    z_min, z_max = new_data[:, 2].min(), new_data[:, 2].max()
    bbox_coords = list(product([x_min, x_max], [y_min, y_max], [z_min, z_max]))
    line_index = 0
    for s, e in combinations(bbox_coords, 2):
        if np.sum(np.abs(np.array(s) - np.array(e))) in [x_max-x_min, y_max-y_min, z_max-z_min]:
            bbox_lines[line_index].set_data_3d(*zip(s, e))
            line_index += 1
    return scatter, *bbox_lines
np.random.seed(42)
data = np.random.rand(100, 3) * 10
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')
scatter = ax.scatter(data[:, 0], data[:, 1], data[:, 2], c='b', marker='o')
x_min, x_max = data[:, 0].min(), data[:, 0].max()
y_min, y_max = data[:, 1].min(), data[:, 1].max()
z_min, z_max = data[:, 2].min(), data[:, 2].max()
bbox_lines = []
for s, e in combinations(np.array(list(product([x_min, x_max], [y_min, y_max], [z_min, z_max]))), 2):
    if np.sum(np.abs(np.array(s) - np.array(e))) in [x_max-x_min, y_max-y_min, z_max-z_min]:
        line, = ax.plot3D(*zip(s, e), color="r", linestyle="--")
        bbox_lines.append(line)
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
ax.set_title('Animated 3D Scatter Plot with Dynamic Bounding Box')
anim = FuncAnimation(fig, update_plot, frames=100, fargs=(data, scatter, bbox_lines),
                     interval=50, blit=False)
plt.show()

Output:

dynamically adjust bounding box

This code creates an animated 3D scatter plot where both the data points and the bounding box are updated in each frame.

 

Highlight Bounding Box

You can adjust the bounding box color, line style, and width:

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from itertools import product, combinations
def plot_3d_scatter_with_highlighted_bbox(data):
  fig = plt.figure(figsize=(10, 8))
  ax = fig.add_subplot(111, projection='3d')  
  scatter = ax.scatter(data[:, 0], data[:, 1], data[:, 2], c='b', marker='o', alpha=0.6)
  
  x_min, x_max = data[:, 0].min(), data[:, 0].max()
  y_min, y_max = data[:, 1].min(), data[:, 1].max()
  z_min, z_max = data[:, 2].min(), data[:, 2].max()  
  for s, e in combinations(np.array(list(product([x_min, x_max], [y_min, y_max], [z_min, z_max]))), 2):
      if np.sum(np.abs(s-e)) == x_max-x_min or np.sum(np.abs(s-e)) == y_max-y_min or np.sum(np.abs(s-e)) == z_max-z_min:
          ax.plot3D(*zip(s, e), color="r", linestyle="-", linewidth=2.5)  
  ax.set_xlabel('X')
  ax.set_ylabel('Y')
  ax.set_zlabel('Z')
  ax.set_title('3D Scatter Plot with Highlighted Bounding Box')  
  plt.show()
np.random.seed(42)
data = np.random.rand(100, 3) * 10
plot_3d_scatter_with_highlighted_bbox(data)

Output:

Highlight Bounding Box

This function creates a 3D scatter plot with a bounding box using solid red lines with increased line width.

 

Bounding Boxes for Subplots

To create bounding boxes for multiple subplots, you can use a loop to generate multiple 3D axes:

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from itertools import product, combinations
def plot_multiple_3d_scatter_with_bbox(data_list):
  fig = plt.figure(figsize=(15, 5))  
  for i, data in enumerate(data_list, 1):
      ax = fig.add_subplot(1, 3, i, projection='3d')      
      scatter = ax.scatter(data[:, 0], data[:, 1], data[:, 2], c='b', marker='o')      
      x_min, x_max = data[:, 0].min(), data[:, 0].max()
      y_min, y_max = data[:, 1].min(), data[:, 1].max()
      z_min, z_max = data[:, 2].min(), data[:, 2].max()      
      for s, e in combinations(np.array(list(product([x_min, x_max], [y_min, y_max], [z_min, z_max]))), 2):
          if np.sum(np.abs(s-e)) == x_max-x_min or np.sum(np.abs(s-e)) == y_max-y_min or np.sum(np.abs(s-e)) == z_max-z_min:
              ax.plot3D(*zip(s, e), color="r", linestyle="--")      
      ax.set_xlabel('X')
      ax.set_ylabel('Y')
      ax.set_zlabel('Z')
      ax.set_title(f'Subplot {i}')  
  plt.tight_layout()
  plt.show()
np.random.seed(42)
data1 = np.random.rand(100, 3) * 10
data2 = np.random.rand(100, 3) * 5 + 5
data3 = np.random.randn(100, 3) * 2
plot_multiple_3d_scatter_with_bbox([data1, data2, data3])

Output:

Bounding Boxes for Subplots

This function creates three 3D scatter plots with bounding boxes in a single figure.

Leave a Reply

Your email address will not be published. Required fields are marked *