| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192 |
- const DEFAULT_LAYOUT = {
- nodeWidth: 270,
- nodeHeight: 108,
- horizontalGap: 340,
- verticalGap: 138,
- marginX: 180,
- marginY: 120,
- };
- function safeTime(node) {
- const created = node?.timestamps?.created || node?.createdAt || "";
- return Date.parse(created) || 0;
- }
- function cmpByTime(a, b) {
- const dt = safeTime(a) - safeTime(b);
- if (dt !== 0) return dt;
- return String(a.id).localeCompare(String(b.id));
- }
- function createAdjacency(nodes, edges) {
- const byId = new Map(nodes.map((node) => [node.id, node]));
- const outgoing = new Map();
- const incoming = new Map();
- const indegree = new Map();
- for (const node of nodes) {
- outgoing.set(node.id, []);
- incoming.set(node.id, []);
- indegree.set(node.id, 0);
- }
- for (const edge of edges) {
- if (!byId.has(edge.from) || !byId.has(edge.to)) continue;
- outgoing.get(edge.from).push(edge);
- incoming.get(edge.to).push(edge);
- indegree.set(edge.to, (indegree.get(edge.to) || 0) + 1);
- }
- return { byId, outgoing, incoming, indegree };
- }
- function topologicalOrder(nodes, graph) {
- const queue = nodes
- .filter((node) => (graph.indegree.get(node.id) || 0) === 0)
- .slice()
- .sort(cmpByTime);
- const indegree = new Map(graph.indegree);
- const order = [];
- while (queue.length) {
- const node = queue.shift();
- order.push(node);
- const outgoing = graph.outgoing.get(node.id) || [];
- for (const edge of outgoing) {
- const nextCount = (indegree.get(edge.to) || 0) - 1;
- indegree.set(edge.to, nextCount);
- if (nextCount === 0) {
- queue.push(graph.byId.get(edge.to));
- }
- }
- queue.sort(cmpByTime);
- }
- if (order.length < nodes.length) {
- const seen = new Set(order.map((node) => node.id));
- const fallback = nodes.filter((node) => !seen.has(node.id)).sort(cmpByTime);
- order.push(...fallback);
- }
- return order;
- }
- function computeLayers(order, graph) {
- const layerById = new Map();
- for (const node of order) {
- const incoming = graph.incoming.get(node.id) || [];
- let layer = 0;
- for (const edge of incoming) {
- const parentLayer = layerById.get(edge.from) || 0;
- layer = Math.max(layer, parentLayer + 1);
- }
- layerById.set(node.id, layer);
- }
- return layerById;
- }
- function layerBarycenter(node, graph, positions) {
- const incoming = graph.incoming.get(node.id) || [];
- if (!incoming.length) return Number.POSITIVE_INFINITY;
- let sum = 0;
- let count = 0;
- for (const edge of incoming) {
- const pos = positions.get(edge.from);
- if (!pos) continue;
- sum += pos.y;
- count += 1;
- }
- return count ? sum / count : Number.POSITIVE_INFINITY;
- }
- function computeBounds(positions, layout) {
- if (!positions.size) {
- return { minX: 0, minY: 0, maxX: 0, maxY: 0, width: 0, height: 0 };
- }
- const xs = [];
- const ys = [];
- positions.forEach((pos) => {
- xs.push(pos.x);
- ys.push(pos.y);
- });
- const minX = Math.min(...xs) - layout.nodeWidth / 2 - 60;
- const maxX = Math.max(...xs) + layout.nodeWidth / 2 + 60;
- const minY = Math.min(...ys) - layout.nodeHeight / 2 - 60;
- const maxY = Math.max(...ys) + layout.nodeHeight / 2 + 60;
- return {
- minX,
- minY,
- maxX,
- maxY,
- width: maxX - minX,
- height: maxY - minY,
- };
- }
- export function computeThoughtChainLayout(graphData, options = {}) {
- const layout = { ...DEFAULT_LAYOUT, ...options };
- const nodes = Array.isArray(graphData?.nodes) ? graphData.nodes.slice() : [];
- const edges = Array.isArray(graphData?.edges) ? graphData.edges.slice() : [];
- if (!nodes.length) {
- return {
- positions: new Map(),
- links: [],
- bounds: { minX: 0, minY: 0, maxX: 0, maxY: 0, width: 0, height: 0 },
- layout,
- };
- }
- const graph = createAdjacency(nodes, edges);
- const order = topologicalOrder(nodes, graph);
- const layerById = computeLayers(order, graph);
- const layers = new Map();
- for (const node of order) {
- const layer = layerById.get(node.id) || 0;
- if (!layers.has(layer)) layers.set(layer, []);
- layers.get(layer).push(node);
- }
- const positions = new Map();
- const layerKeys = Array.from(layers.keys()).sort((a, b) => a - b);
- for (const layer of layerKeys) {
- const layerNodes = layers.get(layer).slice().sort((a, b) => {
- const ba = layerBarycenter(a, graph, positions);
- const bb = layerBarycenter(b, graph, positions);
- if (Number.isFinite(ba) && Number.isFinite(bb) && ba !== bb) return ba - bb;
- if (Number.isFinite(ba) && !Number.isFinite(bb)) return -1;
- if (!Number.isFinite(ba) && Number.isFinite(bb)) return 1;
- return cmpByTime(a, b);
- });
- for (let i = 0; i < layerNodes.length; i += 1) {
- const node = layerNodes[i];
- positions.set(node.id, {
- x: layout.marginX + layer * layout.horizontalGap,
- y: layout.marginY + i * layout.verticalGap,
- });
- }
- }
- const links = edges
- .map((edge) => {
- const from = positions.get(edge.from);
- const to = positions.get(edge.to);
- if (!from || !to) return null;
- const targetNode = graph.byId.get(edge.to);
- return {
- ...edge,
- from,
- to,
- status: targetNode ? targetNode.status : "pending",
- };
- })
- .filter(Boolean);
- return {
- positions,
- links,
- bounds: computeBounds(positions, layout),
- layout,
- };
- }
|