'interactive 3D plot with right aspect ratio using plotly

I am using matplotlib to plot 3D image (i.e 3D bin packing problem like loading containers). when plotting, the length/width/height is automatically scaled which is not proportional to its actual value, i.e. the length is 6 times bigger than the height but the picture shows almost the same scale for the three axis (see below first one pic). I understood that matplot3D has its limitations on drawing 3D plot with right aspect ratio.

enter image description here

What I need is to draw a plot in more realistic manner like the below graph. we could easily see the space of the container and the items loaded in it. Many people recommand using plotly and it supports nice interactive 3D plotting. I have never used such tool to draw 3D plot. Can someone help to provide an example of code doing so? thanks

enter image description here

below is my code:

from py3dbp import Packer, Bin, Item
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
import numpy as np
import matplotlib.pyplot as plt
import random





def cuboid_data2(o, size=(1, 1, 1)):
    X = [[[0, 1, 0], [0, 0, 0], [1, 0, 0], [1, 1, 0]],
         [[0, 0, 0], [0, 0, 1], [1, 0, 1], [1, 0, 0]],
         [[1, 0, 1], [1, 0, 0], [1, 1, 0], [1, 1, 1]],
         [[0, 0, 1], [0, 0, 0], [0, 1, 0], [0, 1, 1]],
         [[0, 1, 0], [0, 1, 1], [1, 1, 1], [1, 1, 0]],
         [[0, 1, 1], [0, 0, 1], [1, 0, 1], [1, 1, 1]]]
    X = np.array(X).astype(float)
    for i in range(3):
        X[:, :, i] *= size[i]
    X += np.array(o)
    return X


def plotCubeAt2(positions, sizes=None, colors=None, **kwargs):
    if not isinstance(colors, (list, np.ndarray)): colors = ["C0"] * len(positions)
    if not isinstance(sizes, (list, np.ndarray)): sizes = [(1, 1, 1)] * len(positions)
    g = []
    for p, s, c in zip(positions, sizes, colors):
        g.append(cuboid_data2(p, size=s))
    return Poly3DCollection(np.concatenate(g),
                            facecolors=np.repeat(colors, 6), **kwargs)
  

  containers = [
    [1203, 235, 259],
    [1203, 235, 259],
    # [1202.4, 235, 269],
    # [12.024, 2.350, 2.69],
    # [12.024, 2.350, 2.69],
    # [12.024, 2.350, 2.69],
]



packer = Packer()

containerX = 0
containerY = 0
containerZ = 0



for i, t in enumerate(range(len(containers))):
    containerX = containers[t][0]
    containerY = containers[t][1]
    containerZ = containers[t][2]
    i += 1
    packer.add_bin(Bin('40HC-' + str(i), containerX, containerY, containerZ, 18000.0))



for i in range(50):
    packer.add_item(Item('BoxA_' + str(i), 44, 39, 70, 8.20))

for i in range(35):
    packer.add_item(Item('BoxB_' + str(i), 65, 38, 40, 14))

for i in range(31):
    packer.add_item(Item('BoxC_' + str(i), 43, 52, 47, 10))

for i in range(38):
    packer.add_item(Item('BoxD_' + str(i), 60, 45, 40, 14))

for i in range(11):
    packer.add_item(Item('BoxE_' + str(i), 42, 46, 54, 9.70))

for i in range(525):
    packer.add_item(Item('BoxF_' + str(i), 62, 45, 35, 14.5))




# packer.pack()
# packer.pack(bigger_first=False)
packer.pack(bigger_first=False, distribute_items=True, number_of_decimals=3)





for b in packer.bins:
    positions = []
    sizes = []
    colors = []
    print(":::::::::::", b.string())

    print("FITTED ITEMS:")
    for item in b.items:
        print("====> ", item.string())
        x = float(item.position[0])
        y = float(item.position[1])
        z = float(item.position[2])
        positions.append((x, y, z))
        sizes.append(
            (float(item.get_dimension()[0]), float(item.get_dimension()[1]), float(item.get_dimension()[2])))

        colorList = ["crimson", "limegreen", "g", "r", "c", "m", "y", "k"]
        if item.width == 44:
            colors.append(colorList[0])
        if item.width == 65:
            colors.append(colorList[1])
        if item.width == 43:
            colors.append(colorList[2])
        if item.width == 60:
            colors.append(colorList[3])
        if item.width == 42:
            colors.append(colorList[4])
        if item.width == 62:
            colors.append(colorList[5])


    print("UNFITTED ITEMS:")
    for item in b.unfitted_items:
        print("====> ", item.string())

    print("***************************************************")
    print("***************************************************")

    # colorList = ["crimson", "limegreen", "g", "r", "c", "m", "y", "k"]
    #
    # for i in range(len(b.items)):
    #   f = random.randint(0, 7)
    #   colors.append(colorList[f])


    if len(colors) > 0:
        fig = plt.figure()
        fig.canvas.set_window_title(b.string().split("(")[0])
        ax = fig.gca(projection='3d')
        ax.set_aspect('auto')
        pc = plotCubeAt2(positions, sizes, colors=colors, edgecolor="k")
        ax.add_collection3d(pc)

        ax.set_xlim([0, float(b.string().split(",")[0].split("(")[1].split("x")[0])])
        ax.set_ylim([0, float(b.string().split(",")[0].split("(")[1].split("x")[1])])
        ax.set_zlim([0, float(b.string().split(",")[0].split("(")[1].split("x")[2])])



plt.show()

The 3D bin packing calculation output from above code looks like, where "pos" should be the 3D position data:

====>  BoxC_16(64.000x37.000x52.000, weight: 0.000) pos([Decimal('1024.000'), 0, 0]) rt(0) vol(123136.000)
====>  BoxC_17(64.000x37.000x52.000, weight: 0.000) pos([Decimal('1088.000'), 0, 0]) rt(0) vol(123136.000)
====>  BoxC_18(64.000x37.000x52.000, weight: 0.000) pos([Decimal('1152.000'), 0, 0]) rt(0) vol(123136.000)
====>  BoxC_19(64.000x37.000x52.000, weight: 0.000) pos([Decimal('1216.000'), 0, 0]) rt(0) vol(123136.000)
====>  BoxC_20(64.000x37.000x52.000, weight: 0.000) pos([Decimal('1280.000'), 0, 0]) rt(0) vol(123136.000)
====>  BoxC_21(64.000x37.000x52.000, weight: 0.000) pos([0, Decimal('37.000'), 0]) rt(0) vol(123136.000)
====>  BoxC_22(64.000x37.000x52.000, weight: 0.000) pos([Decimal('64.000'), Decimal('37.000'), 0]) rt(0) vol(123136.000)
====>  BoxC_23(64.000x37.000x52.000, weight: 0.000) pos([Decimal('128.000'), Decimal('37.000'), 0]) rt(0) vol(123136.000)
====>  BoxC_24(64.000x37.000x52.000, weight: 0.000) pos([Decimal('192.000'), Decimal('37.000'), 0]) rt(0) vol(123136.000)
====>  BoxC_25(64.000x37.000x52.000, weight: 0.000) pos([Decimal('256.000'), Decimal('37.000'), 0]) rt(0) vol(123136.000)
====>  BoxC_26(64.000x37.000x52.000, weight: 0.000) pos([Decimal('320.000'), Decimal('37.000'), 0]) rt(0) vol(123136.000)

UPADTE: (drawing outer container frame)

def parallelipipedic_frame(xm, xM, ym, yM, zm, zM):
    # defines the coords of each segment followed by None, if the line is
    # discontinuous
    x = [xm, xM, xM, xm, xm, None, xm, xM, xM, xm, xm, None, xm, xm, None, xM, xM,
         None, xM, xM, None, xm, xm]
    y = [ym, ym, yM, yM, ym, None, ym, ym, yM, yM, ym, None, ym, ym, None, ym, ym,
         None, yM, yM, None, yM, yM]
    z = [zm, zm, zm, zm, zm, None, zM, zM, zM, zM, zM, None, zm, zM, None, zm, zM,
         None, zm, zM, None, zm, zM]
    return x, y, z

x, y, z = parallelipipedic_frame(0, 1202.4, 0, 235, 0, 269.7)
# fig = go.Figure(go.Scatter3d(x=x, y=y, z=z, mode="lines", line_width=4))

fig.add_trace(
    go.Scatter3d(
        x=x,
        y=y,
        z=z,
        mode="lines",
        line_color="blue",
        line_width=2,
        hoverinfo="skip",
    )
)

ar = 4
xr = max(d["x"].max()) - min(d["x"].min())
fig.update_layout(
    title={"text": pbin, "y": 0.9, "x": 0.5, "xanchor": "center", "yanchor": "top"},
    margin={"l": 0, "r": 0, "t": 0, "b": 0},
    # autosize=False,
    scene=dict(
        camera=dict(eye=dict(x=2, y=2, z=2)),
        aspectratio={
            **{"x": ar},
            **{
                c: ((max(d[c].max()) - min(d[c].min())) / xr) * ar
                for c in list("yz")
            },
        },
        aspectmode="manual",
    ),
)

enter image description here



Solution 1:[1]

from py3dbp import Packer, Bin, Item
import plotly.graph_objects as go
from plotly.subplots import make_subplots

containers = [
    [1203, 235, 259],
    [1203, 235, 259],
]

packer = Packer()

for i, t in enumerate(containers):
    packer.add_bin(Bin("40HC-" + str(i + 1), *t, 18000.0))

pbins = {
    "BoxA": {"n": 50, "s": [44, 39, 70, 8.20]},
    "BoxB": {"n": 35, "s": [65, 38, 40, 14]},
    "BoxC": {"n": 31, "s": [43, 52, 47, 10]},
    "BoxD": {"n": 38, "s": [60, 45, 40, 14]},
    "BoxE": {"n": 11, "s": [65, 38, 40, 14]},
    "BoxF": {"n": 525, "s": [62, 45, 35, 14.5]},
}

for name, cfg in pbins.items():
    for i in range(cfg["n"]): 
        packer.add_item(Item(f"{name}_{i}", *cfg["s"]))

# packer.pack()
# packer.pack(bigger_first=False)
print("about to pack")
packer.pack(bigger_first=False, distribute_items=True, number_of_decimals=3)
print("packed")


### PLOTLY ###
# https://plotly.com/python/3d-mesh/#mesh-cube
def vertices(xmin=0, ymin=0, zmin=0, xmax=1, ymax=1, zmax=1):
    return {
        "x": [xmin, xmin, xmax, xmax, xmin, xmin, xmax, xmax],
        "y": [ymin, ymax, ymax, ymin, ymin, ymax, ymax, ymin],
        "z": [zmin, zmin, zmin, zmin, zmax, zmax, zmax, zmax],
        "i": [7, 0, 0, 0, 4, 4, 6, 1, 4, 0, 3, 6],
        "j": [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3],
        "k": [0, 7, 2, 3, 6, 7, 1, 6, 5, 5, 7, 2],
    }

# take a packer item and build parameters to a plotly mesh3d cube
def packer_to_plotly(item):
    colors = ["crimson", "limegreen", "green", "red", "cyan", "magenta", "yellow"]

    ret = vertices(
        *item.position, *[sum(x) for x in zip(item.position, item.get_dimension())]
    )
    ret["name"] = item.name
    ret["color"] = colors[ord(item.name.split("_")[0][-1]) - ord("A")]
    return ret

# create a multi-plot figure for each bin
fig = make_subplots(rows=len(packer.bins), cols=1, specs=[[{"type":"mesh3d"}], [{"type":"mesh3d"}]])

# add a trace for each packer item
for row, pbin in enumerate(packer.bins):
    for item in pbin.items:
        fig.add_trace(go.Mesh3d(packer_to_plotly(item)), row=row+1, col=1)

# some first attempts at sorting out layout, prmarily aspect ratio
fig.update_layout(
    margin={"l": 0, "r": 0, "t": 0, "b": 0},
    autosize=False,
    scene=dict(
        camera=dict(
            # eye=dict(x=0.1, y=0.1, z=1.5)
        ),
        aspectratio=dict(x=1, y=.2, z=0.2),
        aspectmode="manual",
    ),
)

enter image description here

additional requirements

  1. how to make individual plot (not merge subplot in the same graph as they too small to see);

    • simple create a figure per bin
  2. how to solve the problem of aspect ratio for non-full container? if not full, I want it to display the empty space for user to see;

    • change to using dataframe to work out aspect ratios from data
  3. how to make a different edge color in order to distinguish from each individual cube

    • this is the more complex part. use Scatter3d()` as well build co-ordinates of vertices of cube
import pandas as pd

# push data into a data frame to enable more types of analysis
df = pd.DataFrame(
    [
        {
            "bin_name": b.name,
            "bin_index": i,
            **packer_to_plotly(item),
            **{d: v for v, d in zip(item.get_dimension(), list("hwl"))},
            **{d + d: v for v, d in zip(item.position, list("xyz"))},
        }
        for i, b in enumerate(packer.bins)
        for item in b.items
    ]
)

# create a figure for each container (bin)
for pbin, d in df.groupby("bin_name"):
    fig = go.Figure()
    xx = []
    yy = []
    zz = []

    # create a trace for each box (bin)
    for _, r in d.iterrows():
        fig.add_trace(
            go.Mesh3d(r[["x", "y", "z", "i", "j", "k", "name", "color"]].to_dict())
        )
        xx += [r.xx, r.xx + r.h, r.xx + r.h, r.xx, r.xx, None] * 2 + [r.xx] * 5 + [None]
        yy += [r.yy, r.yy, r.yy + r.w, r.yy + r.w, r.yy, None] * 2 + [
            r.yy,
            r.yy + r.w,
            r.yy + r.w,
            r.yy,
            r.yy,
            None,
        ]
        zz += (
            [r.zz] * 5
            + [None]
            + [r.zz + r.l] * 5
            + [None]
            + [r.zz, r.zz, r.zz + r.l, r.zz + r.l, r.zz, None]
        )

    fig.add_trace(
        go.Scatter3d(
            x=xx,
            y=yy,
            z=zz,
            mode="lines",
            line_color="black",
            line_width=2,
            hoverinfo="skip",
        )
    )
    ar = 4
    xr = max(d["x"].max()) - min(d["x"].min())
    fig.update_layout(
        title={"text": pbin, "y": 0.9, "x": 0.5, "xanchor": "center", "yanchor": "top"},
        margin={"l": 0, "r": 0, "t": 0, "b": 0},
        # autosize=False,
        scene=dict(
            camera=dict(eye=dict(x=2, y=2, z=2)),
            aspectratio={
                **{"x": ar},
                **{
                    c: ((max(d[c].max()) - min(d[c].min())) / xr) * ar
                    for c in list("yz")
                },
            },
            aspectmode="manual",
        ),
    )

    fig.show()

enter image description here

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1