import sys
import json
import numpy as np
import plotly.graph_objects as go
import plotly.subplots
import plotly.express.colors
def main(data: dict):
containers = data["containers"]
box_types = {bt["id"]: bt for bt in data["box_types"]}
max_dims = calc_max_dims(containers)
n = len(containers)
cols = min(4, n) rows = (n + cols - 1) // cols
infos = []
for c in containers:
info = f"Container {c['type']['id']}<br>"
info += f"<sub>Volume Rate: {c['volume_rate']:.2%}</sub>"
if "weight_rate" in c:
info += f"<sub>, Weight Rate: {c['weight_rate']:.2%}</sub>"
infos.append(info)
subplot_titles = infos
fig = plotly.subplots.make_subplots(
rows=rows, cols=cols,
specs=[[{"type": "scatter3d"} for _ in range(cols)] for _ in range(rows)],
subplot_titles=subplot_titles,
)
for i, container in enumerate(containers):
row = i // cols + 1
col = i % cols + 1
draw(container, fig, row, col, max_dims, box_types)
add_view_selector(fig, rows, cols)
fig.show()
def draw(container: dict, fig: go.Figure, row: int, col: int, max_dims: tuple[int, int, int], box_types: dict, shown_types=set()):
draw_container(fig, container, row, col)
for box in container["boxes"]:
draw_box(fig, box, row, col, shown_types, box_types)
max_dim = max(max_dims)
scene_config = dict(
xaxis=dict(range=[0, max_dims[0]], title="X"),
yaxis=dict(range=[0, max_dims[1]], title="Y"),
zaxis=dict(range=[0, max_dims[2]], title="Z"),
aspectratio=dict(
x=max_dims[0] / max_dim,
y=max_dims[1] / max_dim,
z=max_dims[2] / max_dim
)
)
scene = f"scene{(row-1)*4 + col}" if (row-1)*4 + col > 1 else "scene"
fig.layout[scene].update(scene_config)
def draw_container(fig: go.Figure, container: dict, row: int, col: int):
t = container["type"]
l, w, h = t["lx"], t["ly"], t["lz"]
vertices = np.array([
[0, 0, 0], [l, 0, 0], [l, w, 0], [0, w, 0], [0, 0, h], [l, 0, h], [l, w, h], [0, w, h] ])
line = dict(color='gray', width=2)
draw_edges(fig, vertices, line, row, col)
def draw_box(fig: go.Figure, box: dict, row: int, col: int, shown_types: set, box_types: dict):
x, y, z = box["x"], box["y"], box["z"]
box_type = box_types[box["type"]]
l, w, h = get_oriented_dim(box_type["lx"], box_type["ly"], box_type["lz"], box["orient"])
color = get_color(box["type"])
vertices = np.array([
[x, y, z], [x + l, y, z], [x + l, y + w, z], [x, y + w, z],
[x, y, z + h], [x + l, y, z + h], [x + l, y + w, z + h], [x, y + w, z + h],
])
faces = [
[0, 1, 2], [0, 2, 3],
[4, 5, 6], [4, 6, 7],
[0, 1, 5], [0, 5, 4],
[3, 2, 6], [3, 6, 7],
[0, 3, 7], [0, 7, 4],
[1, 2, 6], [1, 6, 5]
]
fig.add_trace(
go.Mesh3d(
x=vertices[:, 0],
y=vertices[:, 1],
z=vertices[:, 2],
i=[face[0] for face in faces],
j=[face[1] for face in faces],
k=[face[2] for face in faces],
color=color,
name=box["type"],
legendgroup=box["type"],
showlegend=box["type"] not in shown_types,
text=get_text(box, box_type),
hoverinfo='text',
),
row=row, col=col
)
shown_types.add(box["type"])
line = dict(color='black', width=1)
draw_edges(fig, vertices, line, row, col)
def draw_edges(fig: go.Figure, vertices: np.ndarray, line: dict, row: int, col: int):
edges = [
(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7) ]
for a, b in edges:
fig.add_trace(
go.Scatter3d(
x=[vertices[a, 0], vertices[b, 0]],
y=[vertices[a, 1], vertices[b, 1]],
z=[vertices[a, 2], vertices[b, 2]],
mode='lines',
line=line,
showlegend=False,
hoverinfo='skip',
),
row=row, col=col
)
def get_text(box: dict, box_type: dict) -> str:
text = f"box: {box["id"]}<br>"
text += f"type: {box["type"]}<br>"
text += f"size: ({box_type["lx"]}x{box_type["ly"]}x{box_type["lz"]})<br>"
text += f"pos: ({box["x"]}, {box["y"]}, {box["z"]})<br>"
text += f"orient: {box["orient"]}<br>"
return text
def get_oriented_dim(l: int, w: int, h: int, orient: str) -> tuple[int, int, int]:
orient_map = {
"XYZ": (l, w, h),
"YXZ": (w, l, h),
"XZY": (l, h, w),
"ZXY": (h, l, w),
"YZX": (w, h, l),
"ZYX": (h, w, l),
}
return orient_map[orient]
def get_color(type_id: str, colors={}):
if type_id not in colors:
base = plotly.express.colors.qualitative.Plotly
colors[type_id] = base[len(colors) % len(base)]
return colors[type_id]
def calc_max_dims(containers: list[dict]) -> tuple[int, int, int]:
max_l, max_w, max_h = 0, 0, 0
for container in containers:
max_l = max(max_l, container["type"]["lx"])
max_w = max(max_w, container["type"]["ly"])
max_h = max(max_h, container["type"]["lz"])
return (max_l, max_w, max_h)
def add_view_selector(fig: go.Figure, rows: int, cols: int):
views = {
"Default": None, "Front": {"eye": {"x": 0, "y": -2, "z": 0}, "up": {"x": 0, "y": 0, "z": 1}},
"Back": {"eye": {"x": 0, "y": 2, "z": 0}, "up": {"x": 0, "y": 0, "z": 1}},
"Left": {"eye": {"x": -2, "y": 0, "z": 0}, "up": {"x": 0, "y": 0, "z": 1}},
"Right": {"eye": {"x": 2, "y": 0, "z": 0}, "up": {"x": 0, "y": 0, "z": 1}},
"Top": {"eye": {"x": 0, "y": 0, "z": 2}, "up": {"x": 0, "y": 1, "z": 0}},
"Bottom": {"eye": {"x": 0, "y": 0, "z": -2}, "up": {"x": 0, "y": -1, "z": 0}},
}
scenes = ["scene"] + [f"scene{i}" for i in range(2, rows * cols + 1)]
buttons = []
for view_name, camera in views.items():
buttons.append(
dict(
label=view_name,
method="relayout",
args=[{f"{scene}.camera": camera for scene in scenes}],
)
)
fig.update_layout(
updatemenus=[
dict(
buttons=buttons,
direction="down",
showactive=True,
active=0,
xanchor="left",
yanchor="top",
)
]
)
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: py draw.py <result_json>")
sys.exit(1)
with open(sys.argv[1], "r", encoding="utf-8") as f:
data = json.load(f)
main(data)