Add vertical node positioning by frequency in Path Navigator

This commit is contained in:
Michael Winter 2026-03-30 22:09:07 +02:00
parent 27d34fdafc
commit 6924c85d19

View file

@ -245,10 +245,11 @@
const hs1 = pitch1.hs_array; const hs1 = pitch1.hs_array;
const hs2 = pitch2.hs_array; const hs2 = pitch2.hs_array;
// Check if they differ by ±1 in exactly one dimension (ignoring dim 0)
let diffCount = 0; let diffCount = 0;
let diffDim = -1; 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]; const diff = hs2[i] - hs1[i];
if (Math.abs(diff) === 1) { if (Math.abs(diff) === 1) {
diffCount++; diffCount++;
@ -261,11 +262,24 @@
// Must differ by exactly ±1 in exactly one dimension // Must differ by exactly ±1 in exactly one dimension
if (diffCount !== 1) return null; if (diffCount !== 1) return null;
// Check dimension 0 (prime 2) - must be same // Calculate frequency ratio from pitch difference (hs1 - hs2)
if (hs1[0] !== hs2[0]) return null; // 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 for (let i = 0; i < hs1.length; i++) {
const ratio = parseFraction(pitch1.fraction) / parseFraction(pitch2.fraction); 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 { return {
dim: diffDim, dim: diffDim,
@ -335,25 +349,47 @@
} }
function buildGraph(chord) { function buildGraph(chord) {
const nodes = chord.map((pitch, i) => ({ // 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, id: i,
pitch: pitch, pitch: pitch,
label: pitch.fraction, label: String(i),
hs: pitch.hs_array cents: cents.toFixed(0),
})); hs: pitch.hs_array,
x: x, // Initial x
y: y // Initial y (will be fixed by forceY)
};
});
const links = []; const links = [];
const labelSet = new Set();
for (let i = 0; i < nodes.length; i++) { for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) { for (let j = i + 1; j < nodes.length; j++) {
const edgeInfo = getEdgeInfo(nodes[i].pitch, nodes[j].pitch); const edgeInfo = getEdgeInfo(nodes[i].pitch, nodes[j].pitch);
if (edgeInfo) { if (edgeInfo) {
const label = formatRatio(edgeInfo.ratio);
links.push({ links.push({
source: i, source: i,
target: j, target: j,
label: label label: edgeInfo.ratio
}); });
} }
} }
@ -378,11 +414,11 @@
return; return;
} }
// Force simulation // Force simulation with fixed y positions
simulation = d3.forceSimulation(nodes) simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id).distance(100)) .force("link", d3.forceLink(links).id(d => d.id).distance(100))
.force("charge", d3.forceManyBody().strength(-300)) .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)); .force("collision", d3.forceCollide().radius(40));
// Draw links // Draw links
@ -419,7 +455,7 @@
node.append("text") node.append("text")
.attr("class", "node-label") .attr("class", "node-label")
.attr("dy", 4) .attr("dy", 4)
.text(d => d.label); .text(d => d.cents);
// Update positions on tick // Update positions on tick
simulation.on("tick", () => { simulation.on("tick", () => {