import graphviz import json from tempfile import NamedTemporaryFile import os def generate_entity_relationship_diagram(json_input: str, output_format: str) -> str: """ Generates an Entity Relationship (ER) diagram from JSON input. Args: json_input (str): A JSON string describing the ER diagram structure. It must follow the Expected JSON Format Example below. output_format (str): The output format for the generated diagram. Supported formats: "png" or "svg" Expected JSON Format Example: { "entities": [ { "name": "Person", "type": "strong", "attributes": [ { "name": "person_id", "type": "primary_key" }, { "name": "first_name", "type": "regular" }, { "name": "last_name", "type": "regular" }, { "name": "birth_date", "type": "regular" }, { "name": "age", "type": "derived" }, { "name": "phone_numbers", "type": "multivalued" }, { "name": "full_address", "type": "composite" } ] }, { "name": "Student", "type": "strong", "attributes": [ { "name": "student_number", "type": "regular" }, { "name": "enrollment_date", "type": "regular" }, { "name": "gpa", "type": "derived" } ] }, { "name": "UndergraduateStudent", "type": "strong", "attributes": [ { "name": "major", "type": "regular" }, { "name": "expected_graduation", "type": "regular" }, { "name": "credits_completed", "type": "regular" } ] }, { "name": "GraduateStudent", "type": "strong", "attributes": [ { "name": "thesis_topic", "type": "regular" }, { "name": "advisor_id", "type": "regular" }, { "name": "degree_type", "type": "regular" } ] }, { "name": "Faculty", "type": "strong", "attributes": [ { "name": "employee_number", "type": "regular" }, { "name": "hire_date", "type": "regular" }, { "name": "office_number", "type": "regular" }, { "name": "years_of_service", "type": "derived" } ] }, { "name": "Professor", "type": "strong", "attributes": [ { "name": "rank", "type": "regular" }, { "name": "tenure_status", "type": "regular" }, { "name": "research_areas", "type": "multivalued" } ] }, { "name": "Lecturer", "type": "strong", "attributes": [ { "name": "contract_type", "type": "regular" }, { "name": "courses_per_semester", "type": "regular" } ] }, { "name": "Staff", "type": "strong", "attributes": [ { "name": "position_title", "type": "regular" }, { "name": "department_assigned", "type": "regular" }, { "name": "salary_grade", "type": "regular" } ] }, { "name": "AdministrativeStaff", "type": "strong", "attributes": [ { "name": "access_level", "type": "regular" }, { "name": "responsibilities", "type": "multivalued" } ] }, { "name": "TechnicalStaff", "type": "strong", "attributes": [ { "name": "certifications", "type": "multivalued" }, { "name": "equipment_assigned", "type": "multivalued" } ] }, { "name": "Vehicle", "type": "strong", "attributes": [ { "name": "vehicle_id", "type": "primary_key" }, { "name": "license_plate", "type": "regular" }, { "name": "year", "type": "regular" }, { "name": "current_value", "type": "derived" } ] }, { "name": "Car", "type": "strong", "attributes": [ { "name": "doors", "type": "regular" }, { "name": "fuel_type", "type": "regular" } ] }, { "name": "Bus", "type": "strong", "attributes": [ { "name": "capacity", "type": "regular" }, { "name": "route_assigned", "type": "regular" } ] }, { "name": "MaintenanceVehicle", "type": "strong", "attributes": [ { "name": "equipment_type", "type": "regular" }, { "name": "specialized_tools", "type": "multivalued" } ] }, { "name": "Course", "type": "strong", "attributes": [ { "name": "course_id", "type": "primary_key" }, { "name": "course_name", "type": "regular" }, { "name": "credits", "type": "regular" } ] }, { "name": "Department", "type": "strong", "attributes": [ { "name": "dept_id", "type": "primary_key" }, { "name": "dept_name", "type": "regular" }, { "name": "budget", "type": "regular" } ] } ], "relationships": [ { "name": "PersonISA", "type": "isa", "parent": "Person", "children": ["Student", "Faculty", "Staff"] }, { "name": "StudentISA", "type": "isa", "parent": "Student", "children": ["UndergraduateStudent", "GraduateStudent"] }, { "name": "FacultyISA", "type": "isa", "parent": "Faculty", "children": ["Professor", "Lecturer"] }, { "name": "StaffISA", "type": "isa", "parent": "Staff", "children": ["AdministrativeStaff", "TechnicalStaff"] }, { "name": "VehicleISA", "type": "isa", "parent": "Vehicle", "children": ["Car", "Bus", "MaintenanceVehicle"] }, { "name": "Enrolls", "type": "regular", "entities": ["Student", "Course"], "cardinalities": { "Student": "M", "Course": "M" }, "attributes": [ { "name": "semester" }, { "name": "year" }, { "name": "grade" } ] }, { "name": "Teaches", "type": "regular", "entities": ["Faculty", "Course"], "cardinalities": { "Faculty": "M", "Course": "M" }, "attributes": [ { "name": "semester" }, { "name": "classroom" } ] }, { "name": "WorksIn", "type": "regular", "entities": ["Faculty", "Department"], "cardinalities": { "Faculty": "M", "Department": "1" }, "attributes": [ { "name": "start_date" } ] }, { "name": "Manages", "type": "regular", "entities": ["Staff", "Department"], "cardinalities": { "Staff": "M", "Department": "M" }, "attributes": [ { "name": "role" } ] }, { "name": "Uses", "type": "regular", "entities": ["Staff", "Vehicle"], "cardinalities": { "Staff": "M", "Vehicle": "M" }, "attributes": [ { "name": "usage_date" }, { "name": "purpose" } ] } ] } Returns: str: The filepath to the generated image file. """ try: if not json_input.strip(): return "Error: Empty input" data = json.loads(json_input) if 'entities' not in data: raise ValueError("Missing required field: entities") dot = graphviz.Graph(comment='ER Diagram', engine='neato') dot.attr( bgcolor='white', pad='1.5', overlap='false', splines='true', sep='+25', esep='+15' ) dot.attr('node', fontname='Arial', fontsize='10', color='#404040') dot.attr('edge', fontname='Arial', fontsize='9', color='#4a4a4a') entity_color = '#BEBEBE' attribute_color = '#B8D4F1' relationship_color = '#FFF9C4' isa_color = '#A8E6CF' font_color = 'black' entities = data.get('entities', []) relationships = data.get('relationships', []) for entity in entities: entity_name = entity.get('name') entity_type = entity.get('type', 'strong') attributes = entity.get('attributes', []) if not entity_name: continue entity_color = '#BEBEBE' if entity_type == 'weak': dot.node( entity_name, entity_name, shape='box', style='filled,rounded', fillcolor=entity_color, fontcolor=font_color, color='#404040', penwidth='3', width='1.8', height='0.8', fontsize='12' ) else: dot.node( entity_name, entity_name, shape='box', style='filled,rounded', fillcolor=entity_color, fontcolor=font_color, color='#404040', penwidth='1', width='1.8', height='0.8', fontsize='12' ) for i, attr in enumerate(attributes): attr_name = attr.get('name', '') attr_type = attr.get('type', 'regular') attr_id = f"{entity_name}_attr_{i}" attr_color = attribute_color if attr_type == 'primary_key': dot.node( attr_id, f'{attr_name} (PK)', shape='ellipse', style='filled,rounded', fillcolor=attr_color, fontcolor=font_color, color='#404040', width='1.2', height='0.6', fontsize='10' ) elif attr_type == 'partial_key': dot.node( attr_id, f'{attr_name} (Partial)', shape='ellipse', style='filled,rounded,dashed', fillcolor=attr_color, fontcolor=font_color, color='#404040', width='1.2', height='0.6', fontsize='10' ) elif attr_type == 'multivalued': dot.node( attr_id, attr_name, shape='ellipse', style='filled,rounded', fillcolor=attr_color, fontcolor=font_color, color='#404040', penwidth='3', width='1.2', height='0.6', fontsize='10' ) elif attr_type == 'derived': dot.node( attr_id, f'/{attr_name}/', shape='ellipse', style='filled,rounded,dashed', fillcolor=attr_color, fontcolor=font_color, color='#404040', width='1.2', height='0.6', fontsize='10' ) elif attr_type == 'composite': dot.node( attr_id, attr_name, shape='ellipse', style='filled,rounded', fillcolor=attr_color, fontcolor=font_color, color='#404040', width='1.2', height='0.6', fontsize='10' ) else: dot.node( attr_id, attr_name, shape='ellipse', style='filled,rounded', fillcolor=attr_color, fontcolor=font_color, color='#404040', width='1.2', height='0.6', fontsize='10' ) dot.edge(entity_name, attr_id, color='#4a4a4a', len='1.5') for relationship in relationships: rel_name = relationship.get('name') rel_type = relationship.get('type', 'regular') entities_involved = relationship.get('entities', []) cardinalities = relationship.get('cardinalities', {}) rel_attributes = relationship.get('attributes', []) if not rel_name: continue if rel_type == 'isa': parent = relationship.get('parent') children = relationship.get('children', []) if parent and children: isa_id = f"isa_{rel_name}" isa_color = isa_color dot.node( isa_id, 'ISA', shape='triangle', style='filled,rounded', fillcolor=isa_color, fontcolor=font_color, color='#404040', penwidth='2', width='1.0', height='0.8', fontsize='10' ) dot.edge(parent, isa_id, color='#4a4a4a', len='2.0') for child in children: dot.edge(isa_id, child, color='#4a4a4a', len='2.0') elif len(entities_involved) >= 2: rel_color = relationship_color if rel_type == 'identifying': dot.node( rel_name, rel_name, shape='diamond', style='filled,rounded', fillcolor=rel_color, fontcolor=font_color, color='#404040', penwidth='3', width='1.8', height='1.0', fontsize='11' ) else: dot.node( rel_name, rel_name, shape='diamond', style='filled,rounded', fillcolor=rel_color, fontcolor=font_color, color='#404040', penwidth='1', width='1.8', height='1.0', fontsize='11' ) for j, attr in enumerate(rel_attributes): attr_name = attr.get('name', '') attr_id = f"{rel_name}_attr_{j}" attr_color = attribute_color dot.node( attr_id, attr_name, shape='ellipse', style='filled,rounded', fillcolor=attr_color, fontcolor=font_color, color='#404040', width='1.0', height='0.5', fontsize='9' ) dot.edge(rel_name, attr_id, color='#4a4a4a', len='1.0') for entity in entities_involved: cardinality = cardinalities.get(entity, '1') dot.edge( entity, rel_name, label=f' {cardinality} ', color='#4a4a4a', len='2.5', fontcolor='#4a4a4a', fontsize='10' ) with NamedTemporaryFile(delete=False, suffix=f'.{output_format}') as tmp: dot.render(tmp.name, format=output_format, cleanup=True) return f"{tmp.name}.{output_format}" except json.JSONDecodeError: return "Error: Invalid JSON format" except Exception as e: return f"Error: {str(e)}"