Sometimes you need the simplicity, this pen draw an ERD with only a few parameters, clean and easy to use, was developed prompting ChatGPT
A Pen by Ronald Avendaño on CodePen.
Sometimes you need the simplicity, this pen draw an ERD with only a few parameters, clean and easy to use, was developed prompting ChatGPT
A Pen by Ronald Avendaño on CodePen.
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Entity Relationship Diagram ERD</title> | |
| </head> | |
| <body> | |
| <p>This is a single ERD using canvas and javascript aided with ChatGPT, no libraries needed</p> | |
| <canvas id="canvas" width="1000" height="1000"></canvas> | |
| </body> | |
| </html> |
| const canvas = document.getElementById('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| let selectedTable = null; | |
| let offsetX = 0; | |
| let offsetY = 0; | |
| // Definición de tablas y relaciones | |
| const entities = { | |
| projects: { | |
| x: 50, | |
| y: 100, | |
| title: "Projects", | |
| fields: ["ProjectID (PK)", "ProjectName", "StartDate", "EndDate"], | |
| headerBackgroundColor: 'lightblue', | |
| headerBorderColor: 'black', | |
| headerFont: '16px Arial', | |
| headerPadding: 10, | |
| fieldsBackgroundColor: 'white', | |
| fieldsBorderColor: 'black', | |
| fieldsFont: '12px Arial' | |
| }, | |
| tasks: { | |
| x: 400, | |
| y: 100, | |
| title: "Tasks", | |
| fields: ["TaskID (PK)", "ProjectID (FK)", "TaskName", "StartDate", "EndDate"], | |
| headerBackgroundColor: 'lightgreen', | |
| headerBorderColor: 'black', | |
| headerFont: '16px Arial', | |
| headerPadding: 10, | |
| fieldsBackgroundColor: 'white', | |
| fieldsBorderColor: 'black', | |
| fieldsFont: '12px Arial' | |
| }, | |
| employees: { | |
| x: 50, | |
| y: 300, | |
| title: "Employees", | |
| fields: ["EmployeeID (PK)", "FirstName", "LastName", "Position"], | |
| headerBackgroundColor: 'lightyellow', | |
| headerBorderColor: 'black', | |
| headerFont: '16px Arial', | |
| headerPadding: 10, | |
| fieldsBackgroundColor: 'white', | |
| fieldsBorderColor: 'black', | |
| fieldsFont: '12px Arial' | |
| }, | |
| assignments: { | |
| x: 750, | |
| y: 300, | |
| title: "Assignments", | |
| fields: ["AssignmentID (PK)", "TaskID (FK)", "EmployeeID (FK)"], | |
| headerBackgroundColor: 'lightblue', | |
| headerBorderColor: 'black', | |
| headerFont: '16px Arial', | |
| headerPadding: 10, | |
| fieldsBackgroundColor: 'white', | |
| fieldsBorderColor: 'black', | |
| fieldsFont: '12px Arial' | |
| } | |
| }; | |
| const relationships = [ | |
| { from: entities.projects, to: entities.tasks, label: "0 or 1" }, | |
| { from: entities.employees, to: entities.assignments, label: "1 :. 1" }, | |
| { from: entities.tasks, to: entities.assignments, label: "1 - N" } | |
| ]; | |
| // Dibuja las tablas en su posición actual | |
| function drawTables() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Dibuja relaciones | |
| ctx.strokeStyle = 'gray'; | |
| ctx.lineWidth = 2; | |
| relationships.forEach(relationship => { | |
| drawRelationship(relationship); | |
| }); | |
| // Dibuja tablas | |
| Object.values(entities).forEach(entity => { | |
| drawTable(entity); | |
| }); | |
| } | |
| // Dibuja una tabla con campos | |
| function drawTable(table) { | |
| const tableWidth = 200; | |
| const cellHeight = 20; | |
| const tableHeight = 100 + table.fields.length * cellHeight; | |
| // Dibuja el rectángulo de la tabla | |
| ctx.fillStyle = table.fieldsBackgroundColor; | |
| ctx.fillRect(table.x, table.y, tableWidth, tableHeight); | |
| ctx.strokeStyle = table.fieldsBorderColor; | |
| ctx.strokeRect(table.x, table.y, tableWidth, tableHeight); | |
| // Dibuja el encabezado de la tabla | |
| ctx.fillStyle = table.headerBackgroundColor; | |
| ctx.strokeStyle = table.headerBorderColor; | |
| ctx.lineWidth = 2; | |
| ctx.fillRect(table.x, table.y, tableWidth, table.headerPadding * 2); | |
| ctx.strokeRect(table.x, table.y, tableWidth, table.headerPadding * 2); | |
| // Dibuja el título de la tabla | |
| ctx.fillStyle = 'black'; | |
| ctx.font = table.headerFont; | |
| ctx.fillText(table.title, table.x + table.headerPadding, table.y + table.headerPadding * 1.5); | |
| // Dibuja los campos de la tabla | |
| ctx.font = table.fieldsFont; | |
| for (let i = 0; i < table.fields.length; i++) { | |
| ctx.fillText(table.fields[i], table.x + table.headerPadding, table.y + table.headerPadding * 3.5 + i * cellHeight); | |
| } | |
| } | |
| // Dibuja una relación entre dos tablas con líneas angulares redondeadas y notación de cardinalidad | |
| function drawRelationship(relationship) { | |
| const startX = relationship.from.x + 200; | |
| const startY = relationship.from.y + 50; | |
| const endX = relationship.to.x; | |
| const endY = relationship.to.y + 50; | |
| // Puntos de control para curvas | |
| const controlPointX1 = startX + 100; | |
| const controlPointY1 = startY; | |
| const controlPointX2 = endX - 100; | |
| const controlPointY2 = endY; | |
| // Dibuja la línea de relación | |
| ctx.beginPath(); | |
| ctx.moveTo(startX, startY); | |
| ctx.lineTo(controlPointX1, controlPointY1); | |
| ctx.quadraticCurveTo(startX, startY, controlPointX1, controlPointY1); | |
| ctx.lineTo(controlPointX2, controlPointY2); | |
| ctx.quadraticCurveTo(endX, endY, controlPointX2, controlPointY2); | |
| ctx.lineTo(endX, endY); | |
| ctx.stroke(); | |
| // Dibuja la notación de cardinalidad | |
| const labelX = (startX + endX) / 2; | |
| const labelY = (startY + endY) / 2; | |
| ctx.fillStyle = 'black'; | |
| ctx.font = '12px Arial'; | |
| ctx.fillText(relationship.label, labelX, labelY); | |
| // Dibuja la terminación de la relación | |
| if (relationship.label === "1 - N") { | |
| ctx.beginPath(); | |
| ctx.moveTo(startX + 10, startY - 8); | |
| ctx.lineTo(startX + 10, startY + 8); | |
| ctx.moveTo(endX - 10, endY); | |
| ctx.lineTo(endX, endY + 5); | |
| ctx.moveTo(endX - 10, endY); | |
| ctx.lineTo(endX, endY - 5); | |
| ctx.stroke(); | |
| } else if(relationship.label === "1 - 1") { | |
| ctx.beginPath(); | |
| ctx.moveTo(startX + 10, startY - 8); | |
| ctx.lineTo(startX + 10, startY + 8); | |
| ctx.moveTo(endX - 10, endY - 8); | |
| ctx.lineTo(endX - 10, endY + 8); | |
| ctx.stroke(); | |
| } else if(relationship.label === "N - N") { | |
| ctx.beginPath(); | |
| ctx.moveTo(startX + 10, startY); | |
| ctx.lineTo(startX, startY - 5); | |
| ctx.moveTo(startX + 10, startY); | |
| ctx.lineTo(startX, startY + 5); | |
| ctx.moveTo(endX - 10, endY); | |
| ctx.lineTo(endX, endY + 5); | |
| ctx.moveTo(endX - 10, endY); | |
| ctx.lineTo(endX, endY - 5); | |
| ctx.stroke(); | |
| } else if(relationship.label === "1 or N"){ | |
| ctx.beginPath(); | |
| ctx.moveTo(startX + 10, startY - 8); | |
| ctx.lineTo(startX + 10, startY + 8); | |
| ctx.moveTo(endX - 10, endY - 8); | |
| ctx.lineTo(endX - 10, endY + 8); | |
| ctx.moveTo(endX - 10, endY); | |
| ctx.lineTo(endX, endY + 5); | |
| ctx.moveTo(endX - 10, endY); | |
| ctx.lineTo(endX, endY - 5); | |
| ctx.stroke(); | |
| } else if(relationship.label === "0 or 1"){ | |
| ctx.beginPath(); | |
| ctx.moveTo(startX + 10, startY - 8); | |
| ctx.lineTo(startX + 10, startY + 8); | |
| ctx.moveTo(endX - 6, endY - 8); | |
| ctx.lineTo(endX - 6, endY + 8); | |
| ctx.stroke(); | |
| ctx.beginPath(); | |
| ctx.arc(endX - 15, endY, 5, 0, 2 * Math.PI); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| } else if(relationship.label === "0 or N"){ | |
| ctx.beginPath(); | |
| ctx.moveTo(startX + 10, startY - 8); | |
| ctx.lineTo(startX + 10, startY + 8); | |
| ctx.moveTo(endX - 10, endY); | |
| ctx.moveTo(endX - 10, endY); | |
| ctx.lineTo(endX, endY + 5); | |
| ctx.moveTo(endX - 10, endY); | |
| ctx.lineTo(endX, endY - 5); | |
| ctx.stroke(); | |
| //ctx.fillStyle = 'white'; | |
| ctx.beginPath(); | |
| ctx.arc(endX - 15, endY, 5, 0, 2 * Math.PI); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| } else if(relationship.label === "1 :. 1"){ | |
| ctx.beginPath(); | |
| ctx.moveTo(endX - 10, endY - 8); | |
| ctx.lineTo(endX - 10, endY + 8); | |
| ctx.moveTo(endX - 14, endY - 8); | |
| ctx.lineTo(endX - 14, endY + 8); | |
| ctx.stroke(); | |
| } | |
| } | |
| // Evento de clic del ratón para seleccionar y mover tablas | |
| canvas.addEventListener('mousedown', (e) => { | |
| const mouseX = e.clientX - canvas.getBoundingClientRect().left; | |
| const mouseY = e.clientY - canvas.getBoundingClientRect().top; | |
| // Verifica si se hizo clic en una tabla | |
| selectedTable = Object.values(entities).find(entity => { | |
| return ( | |
| mouseX >= entity.x && | |
| mouseX <= entity.x + 200 && | |
| mouseY >= entity.y && | |
| mouseY <= entity.y + (100 + entity.fields.length * 20) | |
| ); | |
| }); | |
| if (selectedTable) { | |
| offsetX = mouseX - selectedTable.x; | |
| offsetY = mouseY - selectedTable.y; | |
| } | |
| }); | |
| // Evento de movimiento del ratón para arrastrar la tabla seleccionada | |
| canvas.addEventListener('mousemove', (e) => { | |
| if (selectedTable) { | |
| const mouseX = e.clientX - canvas.getBoundingClientRect().left; | |
| const mouseY = e.clientY - canvas.getBoundingClientRect().top; | |
| selectedTable.x = mouseX - offsetX; | |
| selectedTable.y = mouseY - offsetY; | |
| drawTables(); | |
| } | |
| }); | |
| // Evento de liberación del botón del ratón para soltar la tabla seleccionada | |
| canvas.addEventListener('mouseup', () => { | |
| selectedTable = null; | |
| }); | |
| // Dibuja las tablas iniciales | |
| drawTables(); |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| } |