160 lines
8.4 KiB
Python
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)
|