|
import gradio as gr |
|
import pandas as pd |
|
import os |
|
import logging |
|
from weasyprint import HTML |
|
from pdf2image import convert_from_path |
|
|
|
logging.basicConfig(level=logging.INFO) |
|
|
|
|
|
DEFAULT_CSV = "default.csv" |
|
|
|
def load_csv(file): |
|
""" |
|
Load a CSV file from the given file path. |
|
If the file is missing, empty, or is a directory, load the default CSV. |
|
""" |
|
cwd = os.getcwd() |
|
logging.info(f"load_csv received: {file}") |
|
|
|
|
|
if not file or file.strip() == "" or os.path.isdir(file) or os.path.abspath(file) == cwd: |
|
logging.info("No valid file provided; using default CSV.") |
|
if os.path.isfile(DEFAULT_CSV): |
|
return pd.read_csv(DEFAULT_CSV) |
|
else: |
|
raise FileNotFoundError("Default CSV not found.") |
|
|
|
|
|
if os.path.isfile(file): |
|
logging.info(f"Loading uploaded CSV: {file}") |
|
return pd.read_csv(file) |
|
|
|
logging.warning(f"Provided file path '{file}' is not valid. Using default CSV.") |
|
return pd.read_csv(DEFAULT_CSV) |
|
|
|
def build_tree(df): |
|
employees = {} |
|
children = {} |
|
all_emps = set() |
|
all_managers = set() |
|
|
|
for _, row in df.iterrows(): |
|
name = row["Name"].strip() |
|
role = row["Role"].strip() |
|
label = f"{name}<br>({role})" |
|
employees[name] = label |
|
all_emps.add(name) |
|
children.setdefault(name, []) |
|
|
|
for _, row in df.iterrows(): |
|
subordinate = row["Name"].strip() |
|
manager = str(row["Reporting To"]).strip() |
|
if manager and manager.lower() != "nan": |
|
children.setdefault(manager, []).append(subordinate) |
|
all_managers.add(manager) |
|
|
|
roots = [emp for emp in all_emps if emp not in all_managers] |
|
if not roots: |
|
roots = [df.iloc[0]["Name"].strip()] |
|
return employees, children, roots |
|
|
|
def generate_node_html(node, employees, children, visited=None): |
|
if visited is None: |
|
visited = set() |
|
if node in visited: |
|
return f"<li><div class='node'>{employees.get(node, node)} (cycle)</div></li>" |
|
visited.add(node) |
|
label = employees.get(node, node) |
|
html = f"<li><div class='node'>{label}</div>" |
|
if node in children and children[node]: |
|
html += "<ul>" |
|
for child in children[node]: |
|
html += generate_node_html(child, employees, children, visited) |
|
html += "</ul>" |
|
html += "</li>" |
|
visited.remove(node) |
|
return html |
|
|
|
def generate_org_chart_html(df, title): |
|
employees, children, roots = build_tree(df) |
|
tree_html = "" |
|
for root in roots: |
|
tree_html += generate_node_html(root, employees, children) |
|
html_content = f""" |
|
<html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<title>{title}</title> |
|
<style> |
|
body {{ |
|
font-family: Arial, sans-serif; |
|
}} |
|
.org-chart {{ |
|
text-align: center; |
|
margin: 20px; |
|
}} |
|
.org-chart ul {{ |
|
padding-top: 20px; |
|
position: relative; |
|
display: inline-block; |
|
}} |
|
.org-chart li {{ |
|
list-style-type: none; |
|
position: relative; |
|
padding: 20px 5px 0 5px; |
|
text-align: center; |
|
}} |
|
.org-chart li::before, .org-chart li::after {{ |
|
content: ''; |
|
position: absolute; |
|
top: 0; |
|
border-top: 2px solid #ccc; |
|
width: 50%; |
|
height: 20px; |
|
}} |
|
.org-chart li::before {{ |
|
right: 50%; |
|
border-right: 2px solid #ccc; |
|
}} |
|
.org-chart li::after {{ |
|
left: 50%; |
|
border-left: 2px solid #ccc; |
|
}} |
|
.org-chart li:only-child::after, .org-chart li:only-child::before {{ |
|
display: none; |
|
}} |
|
.org-chart li:only-child {{ |
|
padding-top: 0; |
|
}} |
|
.org-chart .node {{ |
|
display: inline-block; |
|
padding: 5px 10px; |
|
border: 1px solid #ccc; |
|
border-radius: 5px; |
|
background: #e5e5e5; |
|
white-space: nowrap; |
|
}} |
|
</style> |
|
</head> |
|
<body> |
|
<h1 style="text-align:center;">{title}</h1> |
|
<div class="org-chart"> |
|
<ul> |
|
{tree_html} |
|
</ul> |
|
</div> |
|
</body> |
|
</html> |
|
""" |
|
return html_content |
|
|
|
def generate_chart(file, title): |
|
try: |
|
df = load_csv(file) |
|
except Exception as e: |
|
logging.error(f"Error loading CSV: {e}") |
|
return None, f"Error loading CSV: {e}" |
|
|
|
|
|
df.columns = df.columns.str.strip() |
|
logging.info("CSV columns: " + ", ".join(df.columns)) |
|
logging.info(f"CSV read successfully with {df.shape[0]} rows.") |
|
|
|
expected_columns = {"Name", "Role", "Reporting To"} |
|
if not expected_columns.issubset(set(df.columns)): |
|
return None, "CSV must contain Name, Role, and Reporting To columns." |
|
|
|
html_content = generate_org_chart_html(df, title) |
|
|
|
pdf_path = "/tmp/chart.pdf" |
|
try: |
|
HTML(string=html_content).write_pdf(pdf_path) |
|
logging.info("PDF generated successfully.") |
|
except Exception as e: |
|
logging.error(f"Error generating PDF: {e}") |
|
return None, f"Error generating PDF: {e}" |
|
|
|
try: |
|
images = convert_from_path(pdf_path, dpi=150) |
|
if images: |
|
image_path = "/tmp/chart.png" |
|
images[0].save(image_path, 'PNG') |
|
else: |
|
image_path = "" |
|
except Exception as e: |
|
logging.error(f"Error converting PDF to image: {e}") |
|
image_path = "" |
|
|
|
return image_path, pdf_path |
|
|
|
with gr.Blocks() as demo: |
|
gr.Markdown("## Organization Chart Generator") |
|
gr.Markdown("Upload a CSV file (optional). If no file is uploaded or an invalid file is provided, the default CSV (default.csv) will be used.") |
|
|
|
file_input = gr.File(label="Upload CSV File (optional)", type="filepath") |
|
title_input = gr.Textbox(label="Enter PDF Title", placeholder="Company Org Chart") |
|
submit_button = gr.Button("Generate Chart") |
|
image_output = gr.Image(label="Generated Chart (PNG)") |
|
pdf_output = gr.File(label="Download PDF") |
|
|
|
submit_button.click(generate_chart, inputs=[file_input, title_input], outputs=[image_output, pdf_output]) |
|
|
|
demo.launch() |
|
|