Spaces:
Running
Running
| 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. | |
| Expected JSON Format Example: | |
| { | |
| "entities": [ | |
| { | |
| "name": "User", | |
| "type": "strong", | |
| "attributes": [ | |
| { | |
| "name": "user_id", | |
| "type": "primary_key" | |
| }, | |
| { | |
| "name": "username", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "email", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "password_hash", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "full_name", | |
| "type": "composite" | |
| }, | |
| { | |
| "name": "phone_numbers", | |
| "type": "multivalued" | |
| }, | |
| { | |
| "name": "age", | |
| "type": "derived" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "Product", | |
| "type": "strong", | |
| "attributes": [ | |
| { | |
| "name": "product_id", | |
| "type": "primary_key" | |
| }, | |
| { | |
| "name": "name", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "description", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "price", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "stock_quantity", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "tags", | |
| "type": "multivalued" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "Category", | |
| "type": "strong", | |
| "attributes": [ | |
| { | |
| "name": "category_id", | |
| "type": "primary_key" | |
| }, | |
| { | |
| "name": "name", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "description", | |
| "type": "regular" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "Order", | |
| "type": "strong", | |
| "attributes": [ | |
| { | |
| "name": "order_id", | |
| "type": "primary_key" | |
| }, | |
| { | |
| "name": "order_date", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "status", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "total_amount", | |
| "type": "derived" | |
| }, | |
| { | |
| "name": "shipping_address", | |
| "type": "composite" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "OrderItem", | |
| "type": "weak", | |
| "attributes": [ | |
| { | |
| "name": "line_number", | |
| "type": "partial_key" | |
| }, | |
| { | |
| "name": "quantity", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "unit_price", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "subtotal", | |
| "type": "derived" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "Payment", | |
| "type": "strong", | |
| "attributes": [ | |
| { | |
| "name": "payment_id", | |
| "type": "primary_key" | |
| }, | |
| { | |
| "name": "amount", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "payment_method", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "payment_date", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "status", | |
| "type": "regular" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "Review", | |
| "type": "strong", | |
| "attributes": [ | |
| { | |
| "name": "review_id", | |
| "type": "primary_key" | |
| }, | |
| { | |
| "name": "rating", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "comment", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "review_date", | |
| "type": "regular" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "Vendor", | |
| "type": "strong", | |
| "attributes": [ | |
| { | |
| "name": "vendor_id", | |
| "type": "primary_key" | |
| }, | |
| { | |
| "name": "company_name", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "contact_person", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "contact_emails", | |
| "type": "multivalued" | |
| }, | |
| { | |
| "name": "business_address", | |
| "type": "composite" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "ShoppingCart", | |
| "type": "strong", | |
| "attributes": [ | |
| { | |
| "name": "cart_id", | |
| "type": "primary_key" | |
| }, | |
| { | |
| "name": "created_date", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "last_updated", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "total_items", | |
| "type": "derived" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "CartItem", | |
| "type": "weak", | |
| "attributes": [ | |
| { | |
| "name": "item_position", | |
| "type": "partial_key" | |
| }, | |
| { | |
| "name": "quantity", | |
| "type": "regular" | |
| }, | |
| { | |
| "name": "added_date", | |
| "type": "regular" | |
| } | |
| ] | |
| } | |
| ], | |
| "relationships": [ | |
| { | |
| "name": "PlacesOrder", | |
| "type": "regular", | |
| "entities": ["User", "Order"], | |
| "cardinalities": { | |
| "User": "1", | |
| "Order": "M" | |
| }, | |
| "attributes": [] | |
| }, | |
| { | |
| "name": "Contains", | |
| "type": "identifying", | |
| "entities": ["Order", "OrderItem"], | |
| "cardinalities": { | |
| "Order": "1", | |
| "OrderItem": "M" | |
| }, | |
| "attributes": [] | |
| }, | |
| { | |
| "name": "OrdersProduct", | |
| "type": "regular", | |
| "entities": ["OrderItem", "Product"], | |
| "cardinalities": { | |
| "OrderItem": "M", | |
| "Product": "1" | |
| }, | |
| "attributes": [] | |
| }, | |
| { | |
| "name": "BelongsTo", | |
| "type": "regular", | |
| "entities": ["Product", "Category"], | |
| "cardinalities": { | |
| "Product": "M", | |
| "Category": "1" | |
| }, | |
| "attributes": [] | |
| }, | |
| { | |
| "name": "ProcessesPayment", | |
| "type": "regular", | |
| "entities": ["Order", "Payment"], | |
| "cardinalities": { | |
| "Order": "1", | |
| "Payment": "M" | |
| }, | |
| "attributes": [] | |
| }, | |
| { | |
| "name": "WritesReview", | |
| "type": "regular", | |
| "entities": ["User", "Review"], | |
| "cardinalities": { | |
| "User": "1", | |
| "Review": "M" | |
| }, | |
| "attributes": [] | |
| }, | |
| { | |
| "name": "ReviewsProduct", | |
| "type": "regular", | |
| "entities": ["Review", "Product"], | |
| "cardinalities": { | |
| "Review": "M", | |
| "Product": "1" | |
| }, | |
| "attributes": [] | |
| }, | |
| { | |
| "name": "Supplies", | |
| "type": "regular", | |
| "entities": ["Vendor", "Product"], | |
| "cardinalities": { | |
| "Vendor": "M", | |
| "Product": "M" | |
| }, | |
| "attributes": [ | |
| { | |
| "name": "supply_price" | |
| }, | |
| { | |
| "name": "lead_time" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "HasCart", | |
| "type": "regular", | |
| "entities": ["User", "ShoppingCart"], | |
| "cardinalities": { | |
| "User": "1", | |
| "ShoppingCart": "1" | |
| }, | |
| "attributes": [] | |
| }, | |
| { | |
| "name": "CartContains", | |
| "type": "identifying", | |
| "entities": ["ShoppingCart", "CartItem"], | |
| "cardinalities": { | |
| "ShoppingCart": "1", | |
| "CartItem": "M" | |
| }, | |
| "attributes": [] | |
| }, | |
| { | |
| "name": "CartHasProduct", | |
| "type": "regular", | |
| "entities": ["CartItem", "Product"], | |
| "cardinalities": { | |
| "CartItem": "M", | |
| "Product": "1" | |
| }, | |
| "attributes": [] | |
| } | |
| ] | |
| } | |
| Returns: | |
| str: The filepath to the generated PNG 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 = '#D4D4D4' | |
| relationship_color = '#E8E8E8' | |
| isa_color = '#F0F0F0' | |
| font_color = 'black' | |
| entities = data.get('entities', []) | |
| relationships = data.get('relationships', []) | |
| # Process entities with new styling | |
| for entity in entities: | |
| entity_name = entity.get('name') | |
| entity_type = entity.get('type', 'strong') | |
| attributes = entity.get('attributes', []) | |
| if not entity_name: | |
| continue | |
| 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}" | |
| if attr_type == 'primary_key': | |
| dot.node( | |
| attr_id, | |
| f'{attr_name} (PK)', | |
| shape='ellipse', | |
| style='filled,rounded', | |
| fillcolor=attribute_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=attribute_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=attribute_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=attribute_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=attribute_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=attribute_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}" | |
| 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: | |
| if rel_type == 'identifying': | |
| dot.node( | |
| rel_name, | |
| rel_name, | |
| shape='diamond', | |
| style='filled,rounded', | |
| fillcolor=relationship_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=relationship_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}" | |
| dot.node( | |
| attr_id, | |
| attr_name, | |
| shape='ellipse', | |
| style='filled,rounded', | |
| fillcolor=attribute_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)}" |