From 8816900edd1aa1973c880619e404f913f85b4bab Mon Sep 17 00:00:00 2001 From: josedario87 Date: Sat, 22 Nov 2025 02:57:12 -0600 Subject: [PATCH] Mejoras en el grafo de trazabilidad MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Corregir referencia a colorMode (agregar .value) - Optimizar renderizado de aristas usando paths SVG - Mejorar posicionamiento de nodos con agrupación por padre - Evitar solapamiento de nodos hermanos - Ajustar cálculo dinámico del ancho del SVG --- .../components/lotes/TrazabilidadGraph.vue | 168 ++++++++++++------ 1 file changed, 110 insertions(+), 58 deletions(-) diff --git a/nuxt4/app/components/lotes/TrazabilidadGraph.vue b/nuxt4/app/components/lotes/TrazabilidadGraph.vue index 70ea869..870e31e 100644 --- a/nuxt4/app/components/lotes/TrazabilidadGraph.vue +++ b/nuxt4/app/components/lotes/TrazabilidadGraph.vue @@ -30,7 +30,7 @@ - + - - + + @@ -170,6 +142,10 @@ const paddingX = 80 const paddingY = 60 const levelGap = 140 const nodeBox = { width: 180, height: 64 } +const horizontalGap = 40 +const siblingSpacing = nodeBox.width + horizontalGap +const edgePadding = 12 +const minElbowHeight = 24 const colorMode = useColorMode() const bgGradient = computed(() => colorMode.value === 'dark' ? 'from-slate-900 to-slate-950' @@ -203,23 +179,88 @@ const nodes = computed(() => { const result: GraphNode[] = [] const levels = groupedByDepth.value if (levels.size === 0) return result - const maxCount = Math.max(...Array.from(levels.values()).map((l) => l.length)) - for (const [depth, rows] of Array.from(levels.entries()).sort((a, b) => a[0] - b[0])) { - const count = rows.length - const width = Math.max(maxCount * nodeBox.width * 1.2, 400) - const spacing = width / (count + 1) - rows.forEach((row, idx) => { + const depthKeys = Array.from(levels.keys()).sort((a, b) => a - b) + const minCenterX = paddingX + nodeBox.width / 2 + const placedByDepth = new Map() + + for (const depth of depthKeys) { + const rows = [...(levels.get(depth) || [])] + + // Nivel raíz: ubicar centrado y con separación básica + if (depth === 0) { + rows.forEach((row, idx) => { + result.push({ + id: row.lote_id, + label: row.codigo || row.lote_id.slice(0, 8), + tipo: row.tipo, + cantidad_kg: row.cantidad_kg, + depth, + x: minCenterX + idx * siblingSpacing, + y: paddingY + depth * levelGap, + }) + }) + placedByDepth.set(depth, result.filter((n) => n.depth === depth)) + continue + } + + const parents = placedByDepth.get(depth - 1) || [] + const childrenByParent = new Map() + const placedIds = new Set() + + for (const row of rows) { + const key = row.parent_lote_id || '__orphan__' + const list = childrenByParent.get(key) || [] + list.push(row) + childrenByParent.set(key, list) + } + + let cursor = minCenterX + + // Ubicar hijos agrupados bajo su padre directo + for (const parent of parents) { + const children = childrenByParent.get(parent.id) + if (!children?.length) continue + + const groupWidth = (children.length - 1) * siblingSpacing + let startX = parent.x - groupWidth / 2 + + // Evitar solapamiento con grupos previos + if (startX < cursor) startX = cursor + + children.forEach((row, idx) => { + const x = startX + idx * siblingSpacing + result.push({ + id: row.lote_id, + label: row.codigo || row.lote_id.slice(0, 8), + tipo: row.tipo, + cantidad_kg: row.cantidad_kg, + depth, + x, + y: paddingY + depth * levelGap, + }) + placedIds.add(row.lote_id) + cursor = x + siblingSpacing + }) + } + + // Cualquier nodo huérfano (sin padre en el nivel superior) se coloca al final + const orphanRows = rows.filter((row) => !placedIds.has(row.lote_id)) + orphanRows.forEach((row, idx) => { + const x = cursor + idx * siblingSpacing result.push({ id: row.lote_id, label: row.codigo || row.lote_id.slice(0, 8), tipo: row.tipo, cantidad_kg: row.cantidad_kg, depth, - x: paddingX + spacing * (idx + 1), + x, y: paddingY + depth * levelGap, }) + cursor = x + siblingSpacing }) + + placedByDepth.set(depth, result.filter((n) => n.depth === depth)) } return result @@ -235,11 +276,11 @@ const nodeById = computed(() => { const edges = computed(() => { const list: { + id: string from: string to: string - fromPos: { x: number; y: number } - toPos: { x: number; y: number } - midPos: { x: number; y: number } + path: string + midY: number }[] = [] for (const row of props.historial) { @@ -247,12 +288,20 @@ const edges = computed(() => { const fromNode = nodeById.value.get(row.parent_lote_id) const toNode = nodeById.value.get(row.lote_id) if (fromNode && toNode) { + const fromY = fromNode.y + nodeBox.height / 2 + edgePadding + const toY = toNode.y - nodeBox.height / 2 - edgePadding + const rawMidY = (fromY + toY) / 2 + const midY = Math.max( + fromY + minElbowHeight, + Math.min(toY - minElbowHeight, rawMidY), + ) + list.push({ + id: `${fromNode.id}-${toNode.id}`, from: fromNode.id, to: toNode.id, - fromPos: { x: fromNode.x, y: fromNode.y + nodeBox.height / 2 }, - toPos: { x: toNode.x, y: toNode.y - nodeBox.height / 2 }, - midPos: { x: fromNode.x, y: (fromNode.y + toNode.y) / 2 }, + midY, + path: `M ${fromNode.x} ${fromY} L ${fromNode.x} ${midY} L ${toNode.x} ${midY} L ${toNode.x} ${toY}`, }) } } @@ -262,8 +311,11 @@ const edges = computed(() => { const svgWidth = computed(() => { if (!nodes.value.length) return 600 - const maxX = Math.max(...nodes.value.map((n) => n.x + nodeBox.width / 2), 600) - return maxX + paddingX + const xs = nodes.value.map((n) => n.x) + const minX = Math.min(...xs) + const maxX = Math.max(...xs) + const width = (maxX - minX) + paddingX * 2 + nodeBox.width + return Math.max(width, 600) }) const svgHeight = computed(() => {