/** * 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 => { const classes = node.image ? [node.kind, 'has-image'] : [node.kind] return { data: { id: node.id, kind: node.kind, label: node.label, image: node.image || null, entity: node.entity || null, }, classes, position: { x: node.x, y: node.y }, } }), ...edges.map(edge => ({ data: { id: `edge:${edge.from}:${edge.to}`, source: edge.from, target: edge.to, }, })), ] }