# 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)}" 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" } ] } ], "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": [] } ] } 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_colors = { 'strong': '#BEBEBE', 'weak': '#FFF9C4' } attribute_colors = { 'primary_key': '#A8E6CF', 'partial_key': '#FFB3BA', 'multivalued': '#B8D4F1', 'derived': '#F0F8FF', 'composite': '#E8E8E8', 'regular': '#D4D4D4' } relationship_colors = { 'regular': '#E8E8E8', 'identifying': '#B8D4F1', 'isa': '#F0F0F0' } 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 = entity_colors.get(entity_type, entity_colors['strong']) 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_colors.get(attr_type, attribute_colors['regular']) 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 = relationship_colors.get('isa', relationship_colors['regular']) 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_colors.get(rel_type, relationship_colors['regular']) 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_colors['regular'] 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)}"