Mejoras en el grafo de trazabilidad
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m6s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m6s
- 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
This commit is contained in:
@@ -30,7 +30,7 @@
|
||||
</defs>
|
||||
|
||||
<!-- Grid horizontal y vertical -->
|
||||
<g stroke="#e2e8f0" stroke-width="1" :stroke-opacity="colorMode === 'dark' ? 0.1 : 0.35">
|
||||
<g stroke="#e2e8f0" stroke-width="1" :stroke-opacity="colorMode.value === 'dark' ? 0.1 : 0.35">
|
||||
<line
|
||||
v-for="depth in depthLines"
|
||||
:key="`h-${depth}`"
|
||||
@@ -52,48 +52,20 @@
|
||||
</g>
|
||||
|
||||
<!-- Aristas con codo (vertical + horizontal) -->
|
||||
<g stroke="url(#edgeGradient)" stroke-width="2.5" stroke-linecap="round">
|
||||
<template v-for="edge in edges" :key="`${edge.from}-${edge.to}`">
|
||||
<!-- tramo vertical -->
|
||||
<line
|
||||
:x1="edge.fromPos.x"
|
||||
:y1="edge.fromPos.y"
|
||||
:x2="edge.fromPos.x"
|
||||
:y2="edge.midPos.y"
|
||||
class="transition-opacity duration-200"
|
||||
:opacity="0.9"
|
||||
/>
|
||||
<!-- tramo horizontal -->
|
||||
<line
|
||||
:x1="edge.fromPos.x"
|
||||
:y1="edge.midPos.y"
|
||||
:x2="edge.toPos.x"
|
||||
:y2="edge.midPos.y"
|
||||
class="transition-opacity duration-200"
|
||||
:opacity="0.9"
|
||||
/>
|
||||
<!-- tramo final vertical -->
|
||||
<line
|
||||
:x1="edge.toPos.x"
|
||||
:y1="edge.midPos.y"
|
||||
:x2="edge.toPos.x"
|
||||
:y2="edge.toPos.y"
|
||||
class="transition-opacity duration-200"
|
||||
:opacity="0.9"
|
||||
/>
|
||||
|
||||
<!-- tramo diagonal suave (opcional) -->
|
||||
<line
|
||||
v-if="edge.toPos.x !== edge.fromPos.x"
|
||||
:x1="edge.fromPos.x"
|
||||
:y1="edge.midPos.y"
|
||||
:x2="edge.toPos.x"
|
||||
:y2="edge.toPos.y"
|
||||
class="transition-opacity duration-200"
|
||||
:opacity="0.4"
|
||||
stroke-dasharray="4 6"
|
||||
/>
|
||||
</template>
|
||||
<g
|
||||
stroke="url(#edgeGradient)"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
v-for="edge in edges"
|
||||
:key="edge.id"
|
||||
:d="edge.path"
|
||||
class="transition-opacity duration-200"
|
||||
:opacity="0.9"
|
||||
/>
|
||||
</g>
|
||||
|
||||
<!-- Nodos -->
|
||||
@@ -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<GraphNode[]>(() => {
|
||||
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<number, GraphNode[]>()
|
||||
|
||||
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<string, TrazabilidadRow[]>()
|
||||
const placedIds = new Set<string>()
|
||||
|
||||
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(() => {
|
||||
|
||||
Reference in New Issue
Block a user