diff --git a/webapp/path_navigator.html b/webapp/path_navigator.html index 5424406..1fb5a03 100644 --- a/webapp/path_navigator.html +++ b/webapp/path_navigator.html @@ -46,18 +46,23 @@ }); }); - const edgeList = edges.map(e => ({ s: e.source().id(), t: e.target().id() })); + const edgeList = edges.map(e => ({ + s: e.source().id(), + t: e.target().id(), + isCrossChord: e.data('isCrossChord') + })); const iter = (i) => { state.forEach(s => s.fx = 0); - edgeList.forEach(({s,t})=>{ + edgeList.forEach(({s, t, isCrossChord})=>{ const a = state.get(s), b = state.get(t); if(!a || !b) return; const dx = b.x - a.x; const dist = Math.abs(dx) || 0.0001; const dir = dx / dist; - const spring = opts.linkStrength * (dist - opts.linkDistance); + const strength = isCrossChord ? (opts.crossChordStrength || 0.001) : opts.linkStrength; + const spring = strength * (dist - opts.linkDistance); a.fx += spring * dir; b.fx -= spring * dir; }); @@ -350,8 +355,8 @@ { selector: 'edge', style: { - 'width': 1.5, - 'line-color': '#555555', + 'width': 2.5, + 'line-color': '#ffffff', 'curve-style': 'straight', 'target-arrow-shape': 'none', 'label': 'data(ratio)', @@ -364,6 +369,17 @@ 'text-background-padding': '2px', } }, + { + selector: 'edge[isCrossChord = "true"]', + style: { + 'width': 1, + 'line-color': '#aaaaaa', + 'line-style': 'dashed', + 'curve-style': 'straight', + 'target-arrow-shape': 'none', + 'label': '', + } + }, { selector: ':selected', style: { @@ -479,6 +495,33 @@ // Build elements array for all chords let elements = []; + // Collect cross-chord edges (same hs_array between adjacent chords) + const crossChordEdges = []; + for (let chordIdx = 1; chordIdx < allGraphsData.graphs.length; chordIdx++) { + const prevGraph = allGraphsData.graphs[chordIdx - 1]; + const currGraph = allGraphsData.graphs[chordIdx]; + if (!prevGraph || !prevGraph.nodes || !currGraph || !currGraph.nodes) continue; + + currGraph.nodes.forEach(n => { + const prevNode = prevGraph.nodes.find(pn => + JSON.stringify(pn.hs_array) === JSON.stringify(n.hs_array) + ); + if (prevNode) { + const prevNodeId = `c${chordIdx - 1}_${prevNode.id}`; + const currNodeId = `c${chordIdx}_${n.id}`; + crossChordEdges.push({ + group: 'edges', + data: { + source: prevNodeId, + target: currNodeId, + ratio: "1/1", + isCrossChord: "true" + }, + }); + } + }); + } + allGraphsData.graphs.forEach((graph, chordIdx) => { if (!graph || !graph.nodes) return; @@ -527,6 +570,9 @@ if (elements.length === 0) return; + // Add cross-chord edges to elements BEFORE layout (so xforce considers them) + elements.push(...crossChordEdges); + // Add all elements cy.add(elements); console.log('Added', elements.length, 'elements'); @@ -544,16 +590,19 @@ }); // Run xforce layout to optimize x positions while keeping y fixed - cy.layout({ + const layout = cy.layout({ name: 'xforce', linkDistance: 60, linkStrength: 0.1, + crossChordStrength: 0.00005, charge: -60, collisionDistance: 35, damping: 0.7, iterations: 250, bounds: bounds, - }).run(); + }); + + layout.run(); // Set canvas size cy.style().json()[0].value = graphWidth;