/** * The trading-network layout, as a pure function of nodes and edges. * * Kept apart from the page so it can run in a Web Worker (see * network-layout-worker.js). fcose runs synchronously: on the ~1200-profile * production graph it blocks ~4.5s on a desktop and several times that on a * phone, where iOS stops a script that runs too long in one go - the graph * then stayed on "Arranging N profiles..." for good. In a worker the page's * own thread never blocks, whatever the device. * * Profiled on the real graph: ~70% of the time is fcose's repulsion and * overlap maths, and numIter makes no difference (250 or 2500, same time, * same quality) - so moving it off the main thread is the fix, not tuning. */ /** * @param {Function} cytoscape The cytoscape factory, with fcose registered. * @param {object} input {nodes: [{id, size}], edges: [{id, source, target}], options} * @returns {Promise>} positions by node id */ export function layoutPositions(cytoscape, { nodes, edges, options }) { return new Promise((resolve, reject) => { let cy = null; try { cy = cytoscape({ headless: true, // Needed headless for node sizes to count: fcose keeps nodes // apart by their real dimensions, so a layout computed on // default-sized nodes would not match the drawing. styleEnabled: true, style: [{ selector: 'node', style: { width: 'data(size)', height: 'data(size)' } }], elements: [ ...nodes.map((n) => ({ data: { id: n.id, size: n.size || 8 } })), ...edges.map((e) => ({ data: { id: e.id, source: e.source, target: e.target } })), ], }); const layout = cy.layout({ ...options, animate: false }); layout.one('layoutstop', () => { const positions = {}; cy.nodes().forEach((node) => { const p = node.position(); positions[node.id()] = { x: p.x, y: p.y }; }); cy.destroy(); resolve(positions); }); layout.run(); } catch (error) { if (cy && !cy.destroyed()) { cy.destroy(); } reject(error); } }); }