# -*- coding: utf-8 -*-
"""
办公室平面图生成器 - 左上角为原点版本
功能:基于 seat.xlsx,在 a-r 行 × 每行 22 座位的固定布局中,
绘制完整工位图,已分配高亮,未分配灰显,且 a 行在最上方。
"""
import os
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['axes.unicode_minus'] = False
# -------------------------------
# 1. 检查输入文件是否存在
# -------------------------------
if not os.path.exists("seat.xlsx"):
raise FileNotFoundError("请确保当前目录下有 'seat.xlsx' 文件!")
# -------------------------------
# 2. 读取 Excel 数据并构建映射
# -------------------------------
try:
df = pd.read_excel("seat.xlsx")
if len(df.columns) < 3:
raise ValueError("Excel 文件必须至少包含三列。")
df = df.iloc[:, :3]
df.columns = ['seat', 'emp_id', 'dept']
except Exception as e:
raise RuntimeError(f"无法读取 seat.xlsx: {e}")
# 清理数据
df.dropna(subset=['seat'], inplace=True)
df['seat'] = df['seat'].astype(str).str.upper().str.strip()
df['dept'] = df['dept'].astype(str).str.upper().str.strip()
assigned_seats = df.set_index('seat').T.to_dict()
pillar_color = '#555555' # 柱子颜色(深灰)
w, h = 0.9, 0.8 # 座位矩形宽高
zorder_base = 10 # 层级顺序基础值
# -------------------------------
# 3. 定义完整布局:a - r 行,每行 1 - 22 号
# -------------------------------
rows = [chr(i) for i in range(ord('c'), ord('r') + 1)] # c 到 r 共 16 行
cols = list(range(1, 23)) # 1 到 22
# 映射字母到 y 坐标:a -> 0, b -> 1, ..., r -> 17
row_map = {letter: idx for idx, letter in enumerate(rows)}
col_map = {num: num - 1 for num in cols} # 数字→索引(等距排列)
use_numeric_x = False # 推荐 False:等间距更整齐
# -------------------------------
# 4. 预生成所有座位的位置
# -------------------------------
seat_positions = {}
for letter in rows:
for num in cols:
seat_id = f"{letter.upper()}{num:02d}"
y = row_map[letter] # a=0, b=1, ..., r=17 → 将通过 invert_yaxis() 变成顶部到底部
x = num if use_numeric_x else col_map[num]
seat_positions[seat_id] = (x, y)
x_vals = [pos[0] for pos in seat_positions.values()]
y_vals = [pos[1] for pos in seat_positions.values()]
x_min, x_max = min(x_vals), max(x_vals)
y_min, y_max = min(y_vals), max(y_vals)
# -------------------------------
# 5. 颜色设置
# -------------------------------
dept_colors = {
'S19': '#FF6B6B',
'S13': '#4ECDC4',
'HR': '#E57777',
'S17': '#77C7E5',
'S11': '#E5BC77',
'QM': '#E5DF77',
'RD': '#C2E577',
'先进制程技术': '#7DE577',
'IT': '#77E5B3',
'资材': '#77E5D6',
'厂务': '#E59977',
'总经理室': '#77A4E5',
'环安': '#A977E5',
'外包': '#E577D6',
'客戶服務課': '#E577B3',
'财务': '#E57790',
'外派(非AUS)': '#E57777'
}
pillars = ['G02', 'G06', 'G11', 'G14', 'G18', 'G22', 'M02', 'M06', 'M11', 'M14', 'M18', 'M22']
valid_pillars = [p for p in pillars if p in seat_positions]
# -------------------------------
# 6. 创建绘图
# -------------------------------
fig_width = (x_max - x_min + 2) * 0.6
fig_height = (y_max + 2) * 0.5
fig, ax = plt.subplots(1, figsize=(fig_width, fig_height))
# -------------------------------
# 7. 遍历所有座位绘制
# -------------------------------
for seat_id, (x, y) in seat_positions.items():
rect_center_x = x + 0.5 * w if use_numeric_x else x + 0.5
rect_y = y + 0.1 # 矩形底部 y 坐标
# 判断是否为柱子
is_pillar = seat_id in pillars
is_occupied = seat_id in assigned_seats and not is_pillar
if is_pillar:
color = pillar_color
hatch = 'xxxx'
zorder = zorder_base + 3
elif is_occupied:
dept = assigned_seats[seat_id]['dept']
color = dept_colors.get(dept, '#CCCCCC')
hatch = None
zorder = zorder_base + 2
else:
color = '#FFFFFF'
hatch = None
zorder = zorder_base + 1
# 绘制矩形
rect = Rectangle((x, y), w, h, facecolor=color,
edgecolor='black', lw=0.8, hatch=hatch,
zorder=zorder)
ax.add_patch(rect)
# 添加座位编号(所有位置都显示)
ax.text(
rect_center_x, rect_y + h * 0.4,
seat_id,
ha='center', va='center',
fontsize=7, color='black', style='italic',
alpha=0.9,
zorder=zorder + 1
)
# 如果已分配且不是柱子,才显示员工信息
if is_occupied:
person = assigned_seats[seat_id]
raw_id = str(person['emp_id']).strip()
if raw_id and raw_id[0].isalpha():
formatted_emp_id = raw_id[0].upper() + raw_id[1:]
else:
formatted_emp_id = raw_id
ax.text(
rect_center_x, rect_y + h * 0.75,
f"{formatted_emp_id}",
ha='center', va='center',
fontsize=8, weight='bold', color='black',
zorder=zorder + 1
)
# -------------------------------
# 8. 设置坐标轴范围与标签
# -------------------------------
ax.set_xlim(x_min, x_max + 1)
ax.set_ylim(y_min - 1, y_max + 1)
# Y 轴标签:a 在顶部
ax.set_yticks([i + 0.5 for i in range(len(rows))])
ax.set_yticklabels([r.upper() for r in rows])
# X 轴标签
if use_numeric_x:
ax.set_xticks(range(1, 23))
else:
ax.set_xticks([col_map[n] + 0.5 for n in cols])
ax.set_xticklabels(cols)
# ax.set_xlabel("Seat Number", fontsize=12, labelpad=10)
# ax.set_title(
# "🏢 Office Floor Plan\nOrigin: Top-Left | Rows a–r (a at top), Seats 01–22",
# fontsize=16, pad=25, weight='bold'
# )
# -------------------------------
# 翻转 Y 轴,使 a 行在最上方
# -------------------------------
ax.invert_yaxis()
# -------------------------------
# 9. 图例
# -------------------------------
used_depts = set(assigned_seats[s]['dept'] for s in assigned_seats if 'dept' in assigned_seats[s])
legend_handles = [plt.Rectangle((0,0),1,1, color=dept_colors[d]) for d in used_depts if d in dept_colors]
legend_labels = [d.upper() for d in used_depts if d in dept_colors]
if legend_handles:
ax.legend(
legend_handles, legend_labels,
loc='upper right',
ncol=15,
fontsize=10,
title_fontsize=11
)
# -------------------------------
# 10. 框
# -------------------------------
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.tick_params(left=False, bottom=False, labelleft=True, labelbottom=True)
# -------------------------------
# 11. 调整布局
# -------------------------------
plt.subplots_adjust(left=0, right=1, top=1, bottom=0)
# -------------------------------
# 12. 保存输出
# -------------------------------
output_pdf = "T1.pdf"
# output_png = "office_floor_plan_top_left.png"
plt.savefig(output_pdf, dpi=200, bbox_inches='tight')
# plt.savefig(output_png, dpi=200, bbox_inches='tight')
print(f"{output_pdf}")
# -------------------------------
# 13. 显示图像
# -------------------------------
# plt.show()
某几个位置为空白不填充任何内容