Files
JH/exports/create_erd_unified_lanes.py

160 lines
8.4 KiB
Python

from PIL import Image, ImageDraw, ImageFont
from pathlib import Path
OUT = Path('/home/hyein/jh-mh/장헌산업/exports')
PNG = OUT / 'work_data_erd_unified_lanes.png'
W, H = 1900, 1980
img = Image.new('RGB', (W, H), '#151718')
d = ImageDraw.Draw(img)
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 18)
font_b = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 21)
font_t = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 34)
font_s = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 15)
font_badge = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 16)
BG = '#151718'
PANEL = '#181b1c'
CARD = '#202527'
HEADER = '#293033'
BORDER = '#667179'
TEXT = '#eef3f5'
MUTED = '#a8b1b7'
PK = '#f5d76e'
FK = '#87c8ff'
PEOPLE = '#55d8f4'
PROJECT = '#81e681'
ERP = '#b99bff'
CAL = '#a6b0b6'
BADGE = '#263036'
entities = {
# left source column
'member': (80, 210, 330, ['PK MemberNo', 'korName', 'teamName', 'rankName', 'groupCode', 'rankCode', 'isRetired']),
'dallyproject': (80, 585, 360, ['PK id', 'FK MemberNo', 'WorkDate', 'EntryPCode', 'EntryTime / LeaveTime', 'OverTime', 'TotalHours', 'RegularHours', 'OvertimeHours']),
'project_alias': (80, 1035, 360, ['PK projectCode', 'shortName', 'bridge/project name']),
# shared center columns
'site_worksheet_worker_cache': (640, 170, 430, ['PK projectCode + workDate', 'PK korName + jobType', 'workText', 'note', 'personCount', 'syncedAt']),
'site_worksheet_record': (640, 570, 430, ['PK projectCode + workDate', 'PK memberNo + korName', 'FK memberNo', 'jobType', 'workText', 'note', 'personCount']),
'site_worksheet_day_sync': (640, 960, 410, ['PK projectCode + workDate', 'syncedAt']),
'site_worksheet_menu_sync': (640, 1140, 410, ['PK projectCode + workDate', 'PK selMenu', '2 = normal worksheet', '3 = tension/temp works', 'syncedAt']),
# right output column
'work_calendar_detail': (1350, 330, 430, ['PK id', 'source: sql / site', 'FK memberNo', 'workDate', 'projectCode', 'projectName', 'workText', 'jobType', 'hours', 'regularHours', 'overtimeHours', 'personCount']),
'work_calendar_day': (1350, 920, 430, ['PK memberNo + workDate', 'korName / teamName / rankName', 'sqlHours', 'sqlProjectCodes', 'siteCount', 'siteProjectCodes', 'siteWorkTexts', 'hasSql / hasSite']),
'project_yearly_summary': (1350, 1285, 430, ['VIEW project tab output', 'projectCode + yearMonth', 'memberNo / korName', 'intranetRegularHours', 'siteHours = personCount * 8', 'commonHours', 'total = intranet + site - common']),
}
heights = {k: 54 + len(v[3]) * 27 + 20 for k, v in entities.items()}
relations = [
('1', 'member -> dallyproject', PEOPLE),
('2', 'member -> site_worksheet_record', PEOPLE),
('3', 'project_alias -> dallyproject', PROJECT),
('4', 'project_alias -> worker_cache', PROJECT),
('5', 'day_sync: fetched project/date', CAL),
('6', 'menu_sync: fetched menu 2/3', CAL),
('7', 'worker_cache -> record', ERP),
('8', 'dallyproject -> calendar_detail', PEOPLE),
('9', 'record -> calendar_detail', ERP),
('10', 'project_alias -> projectName', PROJECT),
('11', 'calendar_detail -> calendar_day', CAL),
('12', 'dallyproject -> project summary', PEOPLE),
('13', 'record -> project summary', ERP),
]
def panel(x1, y1, x2, y2, title, color):
d.rounded_rectangle((x1, y1, x2, y2), radius=16, fill=PANEL, outline='#2e3437', width=2)
d.text((x1 + 24, y1 + 20), title, font=font_b, fill=color)
def card(name):
x, y, w, fields = entities[name]
h = heights[name]
d.rounded_rectangle((x, y, x + w, y + h), radius=8, fill=CARD, outline=BORDER, width=2)
d.rectangle((x, y, x + w, y + 46), fill=HEADER)
d.line((x, y + 46, x + w, y + 46), fill=BORDER, width=1)
d.text((x + 18, y + 12), name, font=font_b, fill=TEXT)
yy = y + 64
for f in fields:
color = PK if f.startswith('PK') else FK if f.startswith('FK') else TEXT
d.text((x + 20, yy), f, font=font_s, fill=color)
yy += 27
def anchor(name, side, off=0):
x, y, w, _ = entities[name]
h = heights[name]
if side == 'r': return (x + w, y + h // 2 + off)
if side == 'l': return (x, y + h // 2 + off)
if side == 't': return (x + w // 2 + off, y)
if side == 'b': return (x + w // 2 + off, y + h)
def badge(num, x, y, color):
d.ellipse((x - 14, y - 14, x + 14, y + 14), fill=BADGE, outline=color, width=2)
tw = d.textlength(num, font=font_badge)
d.text((x - tw / 2, y - 10), num, font=font_badge, fill=color)
def arrow(points, color, num=None, badge_at=None):
d.line(points, fill=color, width=4, joint='curve')
x1, y1 = points[-2]
x2, y2 = points[-1]
if abs(x2 - x1) >= abs(y2 - y1):
tri = [(x2, y2), (x2 - 12 if x2 >= x1 else x2 + 12, y2 - 7), (x2 - 12 if x2 >= x1 else x2 + 12, y2 + 7)]
else:
tri = [(x2, y2), (x2 - 7, y2 - 12 if y2 >= y1 else y2 + 12), (x2 + 7, y2 - 12 if y2 >= y1 else y2 + 12)]
d.polygon(tri, fill=color)
if num:
bx, by = badge_at if badge_at else points[len(points)//2]
badge(num, bx, by, color)
# title
d.text((70, 40), 'Unified Work Data ERD', font=font_t, fill=TEXT)
d.text((72, 82), 'One diagram, separated into people flow and project flow. Numbered connectors avoid line-label overlap.', font=font_s, fill=MUTED)
# panels
panel(45, 130, 500, 1535, 'SOURCE TABLES', MUTED)
panel(585, 130, 1130, 1535, 'SHARED ERP CACHE / MATCH TABLES', MUTED)
panel(1300, 130, 1860, 1535, 'OUTPUT / VIEW TABLES', MUTED)
# connectors without text labels
arrow([anchor('member','b'), (245, 545), anchor('dallyproject','t')], PEOPLE, '1', (245, 545))
arrow([anchor('member','r',10), (555, anchor('member','r',10)[1]), (555, anchor('site_worksheet_record','l',-40)[1]), anchor('site_worksheet_record','l',-40)], PEOPLE, '2', (555, 470))
arrow([anchor('project_alias','t'), (260, 925), anchor('dallyproject','b')], PROJECT, '3', (260, 925))
arrow([anchor('project_alias','r',-20), (560, anchor('project_alias','r',-20)[1]), (560, anchor('site_worksheet_worker_cache','l',-35)[1]), anchor('site_worksheet_worker_cache','l',-35)], PROJECT, '4', (560, 825))
arrow([anchor('site_worksheet_day_sync','t'), (1110, 910), (1110, 340), anchor('site_worksheet_worker_cache','r',45)], CAL, '5', (1110, 910))
arrow([anchor('site_worksheet_menu_sync','t'), (1160, 1120), (1160, 370), anchor('site_worksheet_worker_cache','r',90)], CAL, '6', (1160, 1120))
arrow([anchor('site_worksheet_worker_cache','b'), (855, 520), anchor('site_worksheet_record','t')], ERP, '7', (855, 520))
arrow([anchor('dallyproject','r',-30), (1235, anchor('dallyproject','r',-30)[1]), (1235, anchor('work_calendar_detail','l',-80)[1]), anchor('work_calendar_detail','l',-80)], PEOPLE, '8', (1235, 610))
arrow([anchor('site_worksheet_record','r'), (1240, anchor('site_worksheet_record','r')[1]), (1240, anchor('work_calendar_detail','l',40)[1]), anchor('work_calendar_detail','l',40)], ERP, '9', (1240, 690))
arrow([anchor('project_alias','r',35), (1200, anchor('project_alias','r',35)[1]), (1200, anchor('work_calendar_detail','l',95)[1]), anchor('work_calendar_detail','l',95)], PROJECT, '10', (1200, 1030))
arrow([anchor('work_calendar_detail','b'), (1565, 880), anchor('work_calendar_day','t')], CAL, '11', (1565, 880))
arrow([anchor('dallyproject','r',35), (1280, anchor('dallyproject','r',35)[1]), (1280, anchor('project_yearly_summary','l',-45)[1]), anchor('project_yearly_summary','l',-45)], PEOPLE, '12', (1280, 800))
arrow([anchor('site_worksheet_record','r',55), (1265, anchor('site_worksheet_record','r',55)[1]), (1265, anchor('project_yearly_summary','l',30)[1]), anchor('project_yearly_summary','l',30)], ERP, '13', (1265, 1185))
# cards
for name in entities:
card(name)
# connector legend bottom
legend_x, legend_y = 70, 1590
d.rounded_rectangle((legend_x, legend_y, 1830, 1775), radius=10, fill='#1d2123', outline='#3a4247')
d.text((legend_x + 20, legend_y + 16), 'CONNECTORS', font=font_b, fill=TEXT)
col_w = 570
for idx, (num, text, color) in enumerate(relations):
col = idx // 5
row = idx % 5
x = legend_x + 28 + col * col_w
y = legend_y + 55 + row * 34
badge(num, x, y + 8, color)
d.text((x + 28, y), text, font=font_s, fill=TEXT)
# mini flow labels
d.rounded_rectangle((70, 1810, 1830, 1850), radius=8, fill='#1d2123', outline='#3a4247')
d.text((90, 1820), 'People flow: member + dallyproject + matched ERP rows -> calendar | Project flow: dallyproject + matched ERP rows -> project summary', font=font_s, fill=MUTED)
img.save(PNG)
print(PNG)