215 lines
8.6 KiB
Python
215 lines
8.6 KiB
Python
import matplotlib.pyplot as plt
|
||
import matplotlib.patches as mpatches
|
||
from matplotlib.patches import FancyBboxPatch
|
||
import matplotlib.patheffects as pe
|
||
|
||
# ── colour palette ──────────────────────────────────────────────────────────
|
||
C = {
|
||
"admin": "#4A90D9",
|
||
"user": "#2ECC71",
|
||
"village": "#E74C3C",
|
||
"announce": "#F39C12",
|
||
"project": "#9B59B6",
|
||
"division": "#1ABC9C",
|
||
"discuss": "#E67E22",
|
||
"other": "#95A5A6",
|
||
"new": "#E74C3C",
|
||
}
|
||
|
||
GROUPS = {
|
||
"Admin": (["AdminRole","Admin"], C["admin"]),
|
||
"User": (["UserRole","User","TokenDeviceUser","UserLog"],C["user"]),
|
||
"Village": (["Village","ColorTheme","BannerImage"], C["village"]),
|
||
"Announcement": (["Announcement","AnnouncementMember",
|
||
"AnnouncementFile"], C["announce"]),
|
||
"Project": (["Project","ProjectMember","ProjectFile",
|
||
"ProjectLink","ProjectTask","ProjectTaskFile",
|
||
"ProjectTaskDetail"], C["project"]),
|
||
"Division": (["Division","DivisionMember","DivisionProject",
|
||
"DivisionProjectMember","DivisionProjectFile",
|
||
"DivisionProjectLink",
|
||
"DivisionProjectTask","DivisionProjectTaskFile",
|
||
"DivisionProjectTaskDetail",
|
||
"DivisionDisscussion","DivisionDisscussionComment",
|
||
"DivisionDiscussionFile",
|
||
"DivisionDocumentFolderFile","DivisionDocumentShare",
|
||
"DivisionCalendar","DivisionCalendarReminder",
|
||
"DivisionCalendarMember",
|
||
"ContainerFileDivision"], C["division"]),
|
||
"Discussion": (["Discussion","DiscussionMember",
|
||
"DiscussionComment","DiscussionFile"], C["discuss"]),
|
||
"Other": (["Group","Position","Notifications",
|
||
"Subscribe","Setting","ContainerImage"], C["other"]),
|
||
}
|
||
|
||
NEW_MODELS = {"ProjectTaskFile", "DivisionProjectTaskFile"}
|
||
|
||
# ── relations (src, dst, label) ─────────────────────────────────────────────
|
||
RELATIONS = [
|
||
# Admin
|
||
("AdminRole","Admin","1-N"),
|
||
# User
|
||
("UserRole","User","1-N"),
|
||
("Village","User","1-N"),
|
||
("Group","User","1-N"),
|
||
("Position","User","0..1-N"),
|
||
# Village
|
||
("Village","Group","1-N"),
|
||
("Village","Announcement","1-N"),
|
||
("Village","Project","1-N"),
|
||
("Village","Division","1-N"),
|
||
("Village","Discussion","1-N"),
|
||
("Village","ColorTheme","1-N"),
|
||
("Village","BannerImage","1-N"),
|
||
# Group
|
||
("Group","Position","1-N"),
|
||
("Group","Project","1-N"),
|
||
("Group","Division","1-N"),
|
||
("Group","AnnouncementMember","1-N"),
|
||
("Group","Discussion","1-N"),
|
||
# Announcement
|
||
("Announcement","AnnouncementMember","1-N"),
|
||
("Announcement","AnnouncementFile","1-N"),
|
||
("Division","AnnouncementMember","1-N"),
|
||
# Project
|
||
("Project","ProjectMember","1-N"),
|
||
("Project","ProjectFile","1-N"),
|
||
("Project","ProjectLink","1-N"),
|
||
("Project","ProjectTask","1-N"),
|
||
("ProjectTask","ProjectTaskDetail","1-N"),
|
||
("ProjectTask","ProjectTaskFile","1-N"),
|
||
("ProjectFile","ProjectTaskFile","1-N"),
|
||
# Division
|
||
("Division","DivisionMember","1-N"),
|
||
("Division","DivisionProject","1-N"),
|
||
("DivisionProject","DivisionProjectMember","1-N"),
|
||
("DivisionProject","DivisionProjectFile","1-N"),
|
||
("DivisionProject","DivisionProjectLink","1-N"),
|
||
("DivisionProject","DivisionProjectTask","1-N"),
|
||
("DivisionProjectTask","DivisionProjectTaskDetail","1-N"),
|
||
("DivisionProjectTask","DivisionProjectTaskFile","1-N"),
|
||
("DivisionProjectFile","DivisionProjectTaskFile","1-N"),
|
||
("ContainerFileDivision","DivisionProjectFile","1-N"),
|
||
("Division","DivisionDisscussion","1-N"),
|
||
("DivisionDisscussion","DivisionDisscussionComment","1-N"),
|
||
("DivisionDisscussion","DivisionDiscussionFile","1-N"),
|
||
("Division","DivisionDocumentFolderFile","1-N"),
|
||
("DivisionDocumentFolderFile","DivisionDocumentShare","1-N"),
|
||
("Division","DivisionCalendar","1-N"),
|
||
("DivisionCalendar","DivisionCalendarReminder","1-N"),
|
||
("DivisionCalendar","DivisionCalendarMember","1-N"),
|
||
# Discussion
|
||
("Discussion","DiscussionMember","1-N"),
|
||
("Discussion","DiscussionComment","1-N"),
|
||
("Discussion","DiscussionFile","1-N"),
|
||
# Other
|
||
("User","Notifications","1-N"),
|
||
("User","Subscribe","1-1"),
|
||
("User","TokenDeviceUser","1-N"),
|
||
("User","UserLog","1-N"),
|
||
]
|
||
|
||
# ── layout: group boxes ──────────────────────────────────────────────────────
|
||
# (x, y, w, h) in data coordinates (canvas = 0..100 x 0..100)
|
||
LAYOUT = {
|
||
"Admin": ( 1, 88, 18, 10),
|
||
"User": ( 1, 68, 22, 18),
|
||
"Village": (26, 88, 22, 10),
|
||
"Other": (51, 88, 22, 10),
|
||
"Announcement": (76, 80, 22, 18),
|
||
"Project": ( 1, 2, 38, 48),
|
||
"Division": (41, 2, 38, 64),
|
||
"Discussion": (81, 2, 17, 30),
|
||
}
|
||
|
||
def group_center(gname):
|
||
x,y,w,h = LAYOUT[gname]
|
||
return x+w/2, y+h/2
|
||
|
||
def model_pos(model):
|
||
for gname,(models,_) in GROUPS.items():
|
||
if model in models:
|
||
x,y,w,h = LAYOUT[gname]
|
||
idx = models.index(model)
|
||
n = len(models)
|
||
cols = max(1, min(3, n))
|
||
rows = (n + cols - 1) // cols
|
||
col = idx % cols
|
||
row = idx // cols
|
||
mx = x + 1.5 + col * (w-2) / cols
|
||
my = y + h - 2.5 - row * (h-1.5) / rows
|
||
return mx, my
|
||
return 50, 50
|
||
|
||
# ── draw ─────────────────────────────────────────────────────────────────────
|
||
fig, ax = plt.subplots(figsize=(28, 22))
|
||
ax.set_xlim(0, 100)
|
||
ax.set_ylim(0, 100)
|
||
ax.axis("off")
|
||
fig.patch.set_facecolor("#F0F4F8")
|
||
ax.set_facecolor("#F0F4F8")
|
||
|
||
ax.set_title("ERD – Sistem Desa Mandiri", fontsize=20, fontweight="bold",
|
||
color="#2C3E50", pad=14)
|
||
|
||
# group boxes
|
||
for gname, (models, color) in GROUPS.items():
|
||
x,y,w,h = LAYOUT[gname]
|
||
rect = FancyBboxPatch((x,y), w, h,
|
||
boxstyle="round,pad=0.3",
|
||
linewidth=2, edgecolor=color,
|
||
facecolor=color+"22")
|
||
ax.add_patch(rect)
|
||
ax.text(x+w/2, y+h-0.6, gname, ha="center", va="top",
|
||
fontsize=9, fontweight="bold", color=color)
|
||
|
||
# model nodes
|
||
for gname, (models, color) in GROUPS.items():
|
||
for m in models:
|
||
mx, my = model_pos(m)
|
||
is_new = m in NEW_MODELS
|
||
fc = "#FFECEC" if is_new else "white"
|
||
ec = C["new"] if is_new else color
|
||
lw = 2.5 if is_new else 1.5
|
||
node = FancyBboxPatch((mx-3.2, my-0.85), 6.4, 1.7,
|
||
boxstyle="round,pad=0.2",
|
||
linewidth=lw, edgecolor=ec, facecolor=fc)
|
||
ax.add_patch(node)
|
||
fw = "bold" if is_new else "normal"
|
||
ax.text(mx, my, m, ha="center", va="center",
|
||
fontsize=6.2, color="#2C3E50", fontweight=fw)
|
||
|
||
# relations
|
||
drawn = set()
|
||
for src, dst, lbl in RELATIONS:
|
||
key = tuple(sorted([src,dst]))
|
||
sx, sy = model_pos(src)
|
||
dx, dy = model_pos(dst)
|
||
color = "#BDC3C7"
|
||
is_new_rel = src in NEW_MODELS or dst in NEW_MODELS
|
||
if is_new_rel:
|
||
color = C["new"]
|
||
ax.annotate("", xy=(dx,dy), xytext=(sx,sy),
|
||
arrowprops=dict(arrowstyle="-|>", color=color,
|
||
lw=1.5 if is_new_rel else 0.8,
|
||
connectionstyle="arc3,rad=0.05"))
|
||
if key not in drawn:
|
||
mx2, my2 = (sx+dx)/2, (sy+dy)/2
|
||
ax.text(mx2, my2+0.4, lbl, ha="center", va="bottom",
|
||
fontsize=4.5, color=color, alpha=0.85)
|
||
drawn.add(key)
|
||
|
||
# legend
|
||
leg_items = [
|
||
mpatches.Patch(facecolor="#FFECEC", edgecolor=C["new"], linewidth=2,
|
||
label="Model Baru"),
|
||
mpatches.Patch(facecolor="white", edgecolor="#BDC3C7", label="Model Lama"),
|
||
]
|
||
ax.legend(handles=leg_items, loc="lower right", fontsize=9,
|
||
framealpha=0.9, edgecolor="#BDC3C7")
|
||
|
||
out = "/Users/wibu04/Documents/Projects/sistem-desa-mandiri/erd.png"
|
||
plt.savefig(out, dpi=150, bbox_inches="tight", facecolor=fig.get_facecolor())
|
||
plt.close()
|
||
print("Saved:", out)
|