#49 feat: servicio genérico de layout por columnas y adaptador cytoscape

This commit is contained in:
2026-08-15 17:13:37 -05:00
parent 1200951b51
commit 21376151f0
2 changed files with 163 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
/**
* Servicio genérico de grafos en capas (columnas).
*
* Provee un layout determinista por columnas (un tipo por columna) y un
* adaptador al formato `elements` de cytoscape.js. No conoce el dominio
* (provenance, organigrama, etc.), solo el modelo de grafo:
* node: { id, kind, label, image?, entity? }
* edge: { from, to } (ids de los nodos)
*/
const DEFAULTS = {
columnWidth: 220,
rowHeight: 64,
padding: 24,
}
function groupByColumn (nodes, columnOf) {
const columns = {}
for (const node of nodes) {
const column = columnOf(node) ?? 0
if (!columns[column]) columns[column] = []
columns[column].push(node)
}
return columns
}
export function computeColumnLayout (nodes, edges, options = {}) {
const { columnOf, columnWidth = DEFAULTS.columnWidth, rowHeight = DEFAULTS.rowHeight, padding = DEFAULTS.padding } = options
const columns = groupByColumn(nodes, columnOf)
const columnKeys = Object.keys(columns).map(Number)
const maxColumn = columnKeys.length > 0 ? Math.max(...columnKeys) : 0
const maxRows = columnKeys.length > 0 ? Math.max(...columnKeys.map(c => columns[c].length)) : 0
const positionedNodes = nodes.map(node => {
const column = columnOf(node) ?? 0
const index = columns[column].indexOf(node)
return {
...node,
x: padding + column * columnWidth + columnWidth / 2,
y: padding + index * rowHeight + rowHeight / 2,
}
})
return {
nodes: positionedNodes,
edges,
width: padding * 2 + (maxColumn + 1) * columnWidth,
height: padding * 2 + maxRows * rowHeight,
}
}
export function toCytoscapeElements ({ nodes, edges }) {
return [
...nodes.map(node => ({
data: {
id: node.id,
kind: node.kind,
label: node.label,
image: node.image || null,
entity: node.entity || null,
},
classes: [node.kind],
position: { x: node.x, y: node.y },
})),
...edges.map(edge => ({
data: {
id: `edge:${edge.from}:${edge.to}`,
source: edge.from,
target: edge.to,
},
})),
]
}