diff --git a/webapp/path_navigator.html b/webapp/path_navigator.html
index 31041d1..4a6ac39 100644
--- a/webapp/path_navigator.html
+++ b/webapp/path_navigator.html
@@ -245,10 +245,11 @@
const hs1 = pitch1.hs_array;
const hs2 = pitch2.hs_array;
+ // Check if they differ by ±1 in exactly one dimension (ignoring dim 0)
let diffCount = 0;
let diffDim = -1;
- for (let i = 1; i < hs1.length; i++) { // Skip dimension 0 (prime 2)
+ for (let i = 1; i < hs1.length; i++) {
const diff = hs2[i] - hs1[i];
if (Math.abs(diff) === 1) {
diffCount++;
@@ -261,11 +262,24 @@
// Must differ by exactly ±1 in exactly one dimension
if (diffCount !== 1) return null;
- // Check dimension 0 (prime 2) - must be same
- if (hs1[0] !== hs2[0]) return null;
+ // Calculate frequency ratio from pitch difference (hs1 - hs2)
+ // Using the formula from pitch.py:
+ // numerator *= dims[i] ** diff[i] for diff[i] >= 0
+ // denominator *= dims[i] ** (-diff[i]) for diff[i] < 0
+ const dims = [2, 3, 5, 7];
+ let numerator = 1;
+ let denominator = 1;
- // Calculate ratio
- const ratio = parseFraction(pitch1.fraction) / parseFraction(pitch2.fraction);
+ for (let i = 0; i < hs1.length; i++) {
+ const diff = hs1[i] - hs2[i]; // pitch1 - pitch2
+ if (diff > 0) {
+ numerator *= Math.pow(dims[i], diff);
+ } else if (diff < 0) {
+ denominator *= Math.pow(dims[i], -diff);
+ }
+ }
+
+ const ratio = numerator + "/" + denominator;
return {
dim: diffDim,
@@ -335,25 +349,47 @@
}
function buildGraph(chord) {
- const nodes = chord.map((pitch, i) => ({
- id: i,
- pitch: pitch,
- label: pitch.fraction,
- hs: pitch.hs_array
- }));
+ // Calculate cents for each pitch
+ const centsData = chord.map((pitch, i) => {
+ const fr = parseFraction(pitch.fraction);
+ const cents = 1200 * Math.log2(fr);
+ return { i, cents };
+ });
+
+ // Find cents for node 0 (voice 0) - use as bottom reference
+ const node0Cents = centsData.find(d => d.i === 0)?.cents || 0;
+ const maxCents = Math.max(...centsData.map(d => d.cents));
+ const range = maxCents - node0Cents || 1;
+
+ // Create nodes with y position: node 0 at bottom, others relative to it
+ const nodes = chord.map((pitch, i) => {
+ const fr = parseFraction(pitch.fraction);
+ const cents = 1200 * Math.log2(fr);
+ // Map to screen: top = high cents, bottom = node 0
+ const y = height * 0.1 + (height * 0.8) * (1 - (cents - node0Cents) / range);
+ // Initial x position: spread horizontally
+ const x = width * 0.2 + (width * 0.6) * (i / (chord.length - 1 || 1));
+ return {
+ id: i,
+ pitch: pitch,
+ label: String(i),
+ cents: cents.toFixed(0),
+ hs: pitch.hs_array,
+ x: x, // Initial x
+ y: y // Initial y (will be fixed by forceY)
+ };
+ });
const links = [];
- const labelSet = new Set();
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const edgeInfo = getEdgeInfo(nodes[i].pitch, nodes[j].pitch);
if (edgeInfo) {
- const label = formatRatio(edgeInfo.ratio);
links.push({
source: i,
target: j,
- label: label
+ label: edgeInfo.ratio
});
}
}
@@ -378,11 +414,11 @@
return;
}
- // Force simulation
+ // Force simulation with fixed y positions
simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id).distance(100))
.force("charge", d3.forceManyBody().strength(-300))
- .force("center", d3.forceCenter(width / 2, height / 2))
+ .force("y", d3.forceY(d => d.y).strength(1)) // Fix y position
.force("collision", d3.forceCollide().radius(40));
// Draw links
@@ -419,7 +455,7 @@
node.append("text")
.attr("class", "node-label")
.attr("dy", 4)
- .text(d => d.label);
+ .text(d => d.cents);
// Update positions on tick
simulation.on("tick", () => {