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>
|
</defs>
|
||||||
|
|
||||||
<!-- Grid horizontal y vertical -->
|
<!-- 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
|
<line
|
||||||
v-for="depth in depthLines"
|
v-for="depth in depthLines"
|
||||||
:key="`h-${depth}`"
|
:key="`h-${depth}`"
|
||||||
@@ -52,48 +52,20 @@
|
|||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Aristas con codo (vertical + horizontal) -->
|
<!-- Aristas con codo (vertical + horizontal) -->
|
||||||
<g stroke="url(#edgeGradient)" stroke-width="2.5" stroke-linecap="round">
|
<g
|
||||||
<template v-for="edge in edges" :key="`${edge.from}-${edge.to}`">
|
stroke="url(#edgeGradient)"
|
||||||
<!-- tramo vertical -->
|
stroke-width="2.5"
|
||||||
<line
|
stroke-linecap="round"
|
||||||
:x1="edge.fromPos.x"
|
stroke-linejoin="round"
|
||||||
:y1="edge.fromPos.y"
|
fill="none"
|
||||||
:x2="edge.fromPos.x"
|
>
|
||||||
:y2="edge.midPos.y"
|
<path
|
||||||
|
v-for="edge in edges"
|
||||||
|
:key="edge.id"
|
||||||
|
:d="edge.path"
|
||||||
class="transition-opacity duration-200"
|
class="transition-opacity duration-200"
|
||||||
:opacity="0.9"
|
: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>
|
</g>
|
||||||
|
|
||||||
<!-- Nodos -->
|
<!-- Nodos -->
|
||||||
@@ -170,6 +142,10 @@ const paddingX = 80
|
|||||||
const paddingY = 60
|
const paddingY = 60
|
||||||
const levelGap = 140
|
const levelGap = 140
|
||||||
const nodeBox = { width: 180, height: 64 }
|
const nodeBox = { width: 180, height: 64 }
|
||||||
|
const horizontalGap = 40
|
||||||
|
const siblingSpacing = nodeBox.width + horizontalGap
|
||||||
|
const edgePadding = 12
|
||||||
|
const minElbowHeight = 24
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
const bgGradient = computed(() => colorMode.value === 'dark'
|
const bgGradient = computed(() => colorMode.value === 'dark'
|
||||||
? 'from-slate-900 to-slate-950'
|
? 'from-slate-900 to-slate-950'
|
||||||
@@ -203,12 +179,16 @@ const nodes = computed<GraphNode[]>(() => {
|
|||||||
const result: GraphNode[] = []
|
const result: GraphNode[] = []
|
||||||
const levels = groupedByDepth.value
|
const levels = groupedByDepth.value
|
||||||
if (levels.size === 0) return result
|
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 depthKeys = Array.from(levels.keys()).sort((a, b) => a - b)
|
||||||
const count = rows.length
|
const minCenterX = paddingX + nodeBox.width / 2
|
||||||
const width = Math.max(maxCount * nodeBox.width * 1.2, 400)
|
const placedByDepth = new Map<number, GraphNode[]>()
|
||||||
const spacing = width / (count + 1)
|
|
||||||
|
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) => {
|
rows.forEach((row, idx) => {
|
||||||
result.push({
|
result.push({
|
||||||
id: row.lote_id,
|
id: row.lote_id,
|
||||||
@@ -216,10 +196,71 @@ const nodes = computed<GraphNode[]>(() => {
|
|||||||
tipo: row.tipo,
|
tipo: row.tipo,
|
||||||
cantidad_kg: row.cantidad_kg,
|
cantidad_kg: row.cantidad_kg,
|
||||||
depth,
|
depth,
|
||||||
x: paddingX + spacing * (idx + 1),
|
x: minCenterX + idx * siblingSpacing,
|
||||||
y: paddingY + depth * levelGap,
|
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,
|
||||||
|
y: paddingY + depth * levelGap,
|
||||||
|
})
|
||||||
|
cursor = x + siblingSpacing
|
||||||
|
})
|
||||||
|
|
||||||
|
placedByDepth.set(depth, result.filter((n) => n.depth === depth))
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -235,11 +276,11 @@ const nodeById = computed(() => {
|
|||||||
|
|
||||||
const edges = computed(() => {
|
const edges = computed(() => {
|
||||||
const list: {
|
const list: {
|
||||||
|
id: string
|
||||||
from: string
|
from: string
|
||||||
to: string
|
to: string
|
||||||
fromPos: { x: number; y: number }
|
path: string
|
||||||
toPos: { x: number; y: number }
|
midY: number
|
||||||
midPos: { x: number; y: number }
|
|
||||||
}[] = []
|
}[] = []
|
||||||
|
|
||||||
for (const row of props.historial) {
|
for (const row of props.historial) {
|
||||||
@@ -247,12 +288,20 @@ const edges = computed(() => {
|
|||||||
const fromNode = nodeById.value.get(row.parent_lote_id)
|
const fromNode = nodeById.value.get(row.parent_lote_id)
|
||||||
const toNode = nodeById.value.get(row.lote_id)
|
const toNode = nodeById.value.get(row.lote_id)
|
||||||
if (fromNode && toNode) {
|
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({
|
list.push({
|
||||||
|
id: `${fromNode.id}-${toNode.id}`,
|
||||||
from: fromNode.id,
|
from: fromNode.id,
|
||||||
to: toNode.id,
|
to: toNode.id,
|
||||||
fromPos: { x: fromNode.x, y: fromNode.y + nodeBox.height / 2 },
|
midY,
|
||||||
toPos: { x: toNode.x, y: toNode.y - nodeBox.height / 2 },
|
path: `M ${fromNode.x} ${fromY} L ${fromNode.x} ${midY} L ${toNode.x} ${midY} L ${toNode.x} ${toY}`,
|
||||||
midPos: { x: fromNode.x, y: (fromNode.y + toNode.y) / 2 },
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,8 +311,11 @@ const edges = computed(() => {
|
|||||||
|
|
||||||
const svgWidth = computed(() => {
|
const svgWidth = computed(() => {
|
||||||
if (!nodes.value.length) return 600
|
if (!nodes.value.length) return 600
|
||||||
const maxX = Math.max(...nodes.value.map((n) => n.x + nodeBox.width / 2), 600)
|
const xs = nodes.value.map((n) => n.x)
|
||||||
return maxX + paddingX
|
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(() => {
|
const svgHeight = computed(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user