← banjotuna

Fix chord diagram display and fingering

7a9aaebd0645a1c4a20865e677ff52bd47a379b4 · Claude · Feb 1, 2026 18:59

Major fixes:
- Exclude 5th drone string for low-position chords (only show 4 main strings DGBD)
- Fix string ordering: left to right = strings 4, 3, 2, 1 (low to high)
- Fix finger assignment: finger 1 on lowest fret, then 2, 3 for higher frets
- Add string number labels (4, 3, 2, 1) at top of diagram

C Major in Open G now correctly shows:
- String 4: fret 2 (E/3rd) - finger 2
- String 3: open (G/5th)
- String 2: fret 1 (C/root) - finger 1
- String 1: fret 2 (E/3rd) - finger 3
Degrees displayed: 3 - 5 - 1 - 3

https://claude.ai/code/session_017ri8N4m6j39ajt5xUaz65n

+114 −151 · 1 file changed

  • index.html +114 −151
diff --git a/index.html b/index.html
index 338d19c..c25076f 100644
--- a/index.html
+++ b/index.html
@@ -1372,39 +1372,48 @@
             return pitchClasses.map(pc => CHROMATIC_SCALE[pc]);
         }
 
-        // Recursive backtracking algorithm to find all valid chord voicings
-        function findAllVoicings(tuningData, rootNote, chordType, options = {}) {
-            const { maxFret = 12, maxReach = 4, maxFingers = 4 } = options;
-
+        // Find chord voicing for 4-string banjo (excludes 5th drone string for low positions)
+        // For 5-string banjo, we only use the 4 main strings for chords (DGBD in Open G)
+        function findChordVoicing(tuningData, rootNote, chordType, maxFret = 12) {
             const chordPitchClasses = getChordPitchClasses(rootNote, chordType);
-            if (chordPitchClasses.length === 0) return [];
+            if (chordPitchClasses.length === 0) return null;
 
-            const strings = tuningData.strings;
-            const numStrings = strings.length;
+            const type = CHORD_TYPES[chordType];
+            const rootPitch = noteToPitchClass(rootNote);
+
+            // Get the 4 main strings (exclude 5th/drone string for 5-string banjo)
+            const allStrings = tuningData.strings;
             const shortStringIdx = tuningData.shortStringIndex;
-            const shortStringStart = tuningData.shortStringStartFret || 6;
 
-            // Convert string notes to pitch classes
-            const stringPitchClasses = strings.map(note => noteToPitchClass(note));
+            // For 5-string banjo, use strings 1-4 (indices 1-4), exclude index 0 (5th string)
+            // For 4-string banjo, use all strings
+            let mainStrings;
+            if (shortStringIdx === 0 && allStrings.length === 5) {
+                // 5-string banjo: use strings at indices 1,2,3,4 (4th,3rd,2nd,1st strings)
+                mainStrings = allStrings.slice(1);
+            } else {
+                mainStrings = allStrings;
+            }
+
+            const numStrings = mainStrings.length;
+            const stringPitchClasses = mainStrings.map(note => noteToPitchClass(note));
 
+            // Find all valid voicings using recursive backtracking
             const results = [];
+            const maxReach = 4;
+            const maxFingers = 4;
 
-            // Recursive function to explore voicings
             function explore(stringIdx, currentVoicing, usedTones) {
-                // Count non-open fretted positions (fingers needed)
                 const fingersUsed = currentVoicing.filter(v => v && v.fret > 0).length;
                 if (fingersUsed > maxFingers) return;
 
-                // Check reach constraint
                 const frettedFrets = currentVoicing.filter(v => v && v.fret > 0).map(v => v.fret);
                 if (frettedFrets.length > 1) {
                     const reach = Math.max(...frettedFrets) - Math.min(...frettedFrets);
                     if (reach > maxReach) return;
                 }
 
-                // Base case: all strings processed
                 if (stringIdx === numStrings) {
-                    // Validate: all chord tones must be present
                     if (usedTones.size === chordPitchClasses.length) {
                         results.push([...currentVoicing]);
                     }
@@ -1413,39 +1422,14 @@
 
                 const stringPitch = stringPitchClasses[stringIdx];
 
-                // Option 1: Mute this string (don't play it)
+                // Option 1: Mute this string
                 explore(stringIdx + 1, [...currentVoicing, null], new Set(usedTones));
 
-                // Determine fret range for this string
-                let startFret = 0;
-                let endFret = maxFret;
-
-                // Handle 5th string (short string) - can only play open or from startFret onward
-                if (stringIdx === shortStringIdx && shortStringStart !== null) {
-                    // For simplicity, treat the 5th string as only playable open at its tuned pitch
-                    // (since it starts at fret 5/6, low fret voicings can't use it)
-                    const openPitch = stringPitch;
-                    const toneIndex = chordPitchClasses.indexOf(openPitch);
-                    if (toneIndex !== -1) {
-                        const newUsedTones = new Set(usedTones);
-                        newUsedTones.add(openPitch);
-                        explore(stringIdx + 1, [...currentVoicing, {
-                            fret: 0,
-                            pitchClass: openPitch,
-                            toneIndex: toneIndex,
-                            note: CHROMATIC_SCALE[openPitch]
-                        }], newUsedTones);
-                    }
-                    // Also try muting (already handled above)
-                    return;
-                }
-
-                // Try each fret position
-                for (let fret = startFret; fret <= endFret; fret++) {
+                // Try each fret position (0 = open, up to maxFret)
+                for (let fret = 0; fret <= maxFret; fret++) {
                     const pitchAtFret = (stringPitch + fret) % 12;
                     const toneIndex = chordPitchClasses.indexOf(pitchAtFret);
 
-                    // Only use this fret if it produces a chord tone
                     if (toneIndex !== -1) {
                         const newUsedTones = new Set(usedTones);
                         newUsedTones.add(pitchAtFret);
@@ -1462,53 +1446,31 @@
 
             explore(0, [], new Set());
 
-            return results;
-        }
-
-        // Score and sort voicings by playability
-        function scoreVoicing(voicing) {
-            const sounding = voicing.filter(v => v !== null);
-            const fretted = sounding.filter(v => v.fret > 0);
-
-            // Prefer voicings with:
-            // 1. Lower minimum fret (more accessible)
-            // 2. More open strings (easier to play)
-            // 3. More strings sounding (fuller sound)
-            // 4. Smaller fret span (easier reach)
-
-            const minFret = fretted.length > 0 ? Math.min(...fretted.map(v => v.fret)) : 0;
-            const maxFret = fretted.length > 0 ? Math.max(...fretted.map(v => v.fret)) : 0;
-            const span = maxFret - minFret;
-            const openCount = sounding.filter(v => v.fret === 0).length;
-            const soundingCount = sounding.length;
-
-            // Lower score is better
-            return minFret * 100 + span * 50 - openCount * 30 - soundingCount * 10;
-        }
-
-        // Find the best chord voicing for a given tuning
-        function findChordVoicing(tuningData, rootNote, chordType, maxFret = 12) {
-            const allVoicings = findAllVoicings(tuningData, rootNote, chordType, {
-                maxFret: maxFret,
-                maxReach: 4,
-                maxFingers: 4
-            });
-
-            if (allVoicings.length === 0) return null;
-
-            // Sort by score and pick the best
-            allVoicings.sort((a, b) => scoreVoicing(a) - scoreVoicing(b));
+            if (results.length === 0) return null;
+
+            // Score and sort voicings
+            function scoreVoicing(voicing) {
+                const sounding = voicing.filter(v => v !== null);
+                const fretted = sounding.filter(v => v.fret > 0);
+                const minFret = fretted.length > 0 ? Math.min(...fretted.map(v => v.fret)) : 0;
+                const maxFretUsed = fretted.length > 0 ? Math.max(...fretted.map(v => v.fret)) : 0;
+                const span = maxFretUsed - minFret;
+                const openCount = sounding.filter(v => v.fret === 0).length;
+                const soundingCount = sounding.length;
+                return minFret * 100 + span * 50 - openCount * 30 - soundingCount * 10;
+            }
 
-            const bestVoicing = allVoicings[0];
-            const numStrings = bestVoicing.length;
-            const type = CHORD_TYPES[chordType];
-            const rootPitch = noteToPitchClass(rootNote);
+            results.sort((a, b) => scoreVoicing(a) - scoreVoicing(b));
+            const bestVoicing = results[0];
 
             // Convert to position format for rendering
+            // Positions are ordered: string 4 (index 0), string 3, string 2, string 1 (index 3)
             const positions = bestVoicing.map((pos, idx) => {
+                const stringNum = 4 - idx; // 4, 3, 2, 1
+
                 if (pos === null) {
                     return {
-                        string: numStrings - idx,
+                        stringNum: stringNum,
                         fret: null,
                         muted: true,
                         note: null,
@@ -1521,7 +1483,7 @@
                 const isRoot = pos.pitchClass === rootPitch;
 
                 return {
-                    string: numStrings - idx,
+                    stringNum: stringNum,
                     fret: pos.fret,
                     muted: false,
                     note: pos.note,
@@ -1530,7 +1492,8 @@
                 };
             });
 
-            // Assign finger numbers
+            // Assign finger numbers based on fret position
+            // Finger 1 = lowest fret, then 2, 3, 4 in ascending order
             assignFingers(positions);
 
             // Determine base fret for display
@@ -1548,61 +1511,56 @@
                 positions,
                 baseFret,
                 barre,
-                chordNotes: chordNotes.map(n => displayNote(n))
+                chordNotes: chordNotes.map(n => displayNote(n)),
+                numStrings: numStrings
             };
         }
 
         function assignFingers(positions) {
-            // Assign finger numbers based on fret positions
-            const fretted = positions.filter(p => p.fret > 0).sort((a, b) => a.fret - b.fret);
+            // Get fretted positions sorted by fret, then by string number (high to low)
+            const fretted = positions
+                .filter(p => p.fret > 0)
+                .sort((a, b) => {
+                    if (a.fret !== b.fret) return a.fret - b.fret;
+                    return a.stringNum - b.stringNum; // Lower string number = higher pitch
+                });
 
             if (fretted.length === 0) return;
 
-            // Group by fret (for barres)
-            const fretGroups = {};
-            fretted.forEach(p => {
-                if (!fretGroups[p.fret]) fretGroups[p.fret] = [];
-                fretGroups[p.fret].push(p);
-            });
-
             let fingerNum = 1;
-            const sortedFrets = Object.keys(fretGroups).map(Number).sort((a, b) => a - b);
+            let lastFret = -1;
 
-            sortedFrets.forEach(fret => {
-                const group = fretGroups[fret];
-                if (group.length > 1) {
-                    // Potential barre - use same finger for all
-                    group.forEach(p => p.finger = fingerNum);
-                } else {
-                    group[0].finger = fingerNum;
+            fretted.forEach(p => {
+                if (p.fret !== lastFret) {
+                    // New fret position, increment finger
+                    if (lastFret !== -1) fingerNum++;
+                    lastFret = p.fret;
                 }
-                fingerNum++;
-                if (fingerNum > 4) fingerNum = 4; // Max 4 fingers
+                p.finger = Math.min(fingerNum, 4);
             });
         }
 
         function detectBarre(positions) {
-            // Detect if there's a barre (same fret on multiple strings)
             const fretGroups = {};
             positions.forEach((p, idx) => {
                 if (p.fret > 0) {
                     if (!fretGroups[p.fret]) fretGroups[p.fret] = [];
-                    fretGroups[p.fret].push(idx);
+                    fretGroups[p.fret].push({ pos: p, idx: idx });
                 }
             });
 
-            for (const [fret, indices] of Object.entries(fretGroups)) {
-                if (indices.length >= 2) {
-                    const sortedIndices = indices.sort((a, b) => a - b);
-                    const isConsecutive = sortedIndices.every((val, idx) =>
-                        idx === 0 || val - sortedIndices[idx - 1] <= 2
-                    );
+            for (const [fret, group] of Object.entries(fretGroups)) {
+                if (group.length >= 2) {
+                    // Check if they're on adjacent strings
+                    const stringNums = group.map(g => g.pos.stringNum).sort((a, b) => b - a);
+                    const minStr = Math.min(...stringNums);
+                    const maxStr = Math.max(...stringNums);
 
-                    if (isConsecutive) {
+                    if (maxStr - minStr <= 3) {
                         return {
                             fret: parseInt(fret),
-                            fromString: Math.min(...sortedIndices),
-                            toString: Math.max(...sortedIndices)
+                            fromString: minStr,
+                            toString: maxStr
                         };
                     }
                 }
@@ -1611,17 +1569,17 @@
             return null;
         }
 
-        // Generate SVG chord diagram
+        // Generate SVG chord diagram for 4 main strings
         function generateChordDiagramSVG(voicing, chordName, tuningName, fingerNotation = 'numbers') {
-            const width = 160;
-            const height = 200;
-            const marginLeft = 30;
-            const marginTop = 40;
-            const marginRight = 20;
-            const marginBottom = 30;
+            const width = 140;
+            const height = 180;
+            const marginLeft = 25;
+            const marginTop = 35;
+            const marginRight = 15;
+            const marginBottom = 25;
 
             const positions = voicing.positions;
-            const numStrings = positions.length;
+            const numStrings = 4; // Always 4 main strings for chord diagrams
             const numFrets = 5;
             const baseFret = voicing.baseFret;
 
@@ -1635,10 +1593,10 @@
 
             // Nut (thick line at top) - only show if baseFret is 1
             if (baseFret === 1) {
-                svg += `<rect x="${marginLeft - 5}" y="${marginTop - 3}" width="${(numStrings - 1) * stringSpacing + 10}" height="6" fill="#1a1a1a"/>`;
+                svg += `<rect x="${marginLeft - 3}" y="${marginTop - 3}" width="${(numStrings - 1) * stringSpacing + 6}" height="5" fill="#1a1a1a"/>`;
             } else {
                 // Show fret number for higher positions
-                svg += `<text x="${marginLeft - 20}" y="${marginTop + fretSpacing / 2}" font-size="12" font-weight="bold" text-anchor="middle">${baseFret}fr</text>`;
+                svg += `<text x="${marginLeft - 15}" y="${marginTop + fretSpacing / 2 + 4}" font-size="11" font-weight="bold" text-anchor="middle">${baseFret}fr</text>`;
             }
 
             // Fret lines
@@ -1647,74 +1605,79 @@
                 svg += `<line x1="${marginLeft}" y1="${y}" x2="${marginLeft + (numStrings - 1) * stringSpacing}" y2="${y}" stroke="#333" stroke-width="${i === 0 ? 2 : 1}"/>`;
             }
 
-            // String lines
+            // String lines (left to right: string 4, 3, 2, 1)
             for (let i = 0; i < numStrings; i++) {
                 const x = marginLeft + i * stringSpacing;
                 svg += `<line x1="${x}" y1="${marginTop}" x2="${x}" y2="${marginTop + numFrets * fretSpacing}" stroke="#333" stroke-width="1.5"/>`;
             }
 
-            // Open/muted string indicators and finger positions
+            // String labels at top (4, 3, 2, 1 from left to right)
+            for (let i = 0; i < numStrings; i++) {
+                const x = marginLeft + i * stringSpacing;
+                const stringNum = 4 - i;
+                svg += `<text x="${x}" y="${marginTop - 20}" font-size="9" text-anchor="middle" fill="#666">${stringNum}</text>`;
+            }
+
+            // Finger positions and open/muted indicators
             positions.forEach((pos, idx) => {
-                // Reverse index for display (5th string on left)
-                const displayIdx = numStrings - 1 - idx;
-                const x = marginLeft + displayIdx * stringSpacing;
+                // idx 0 = string 4 (leftmost), idx 3 = string 1 (rightmost)
+                const x = marginLeft + idx * stringSpacing;
 
                 if (pos.muted) {
                     // X for muted string
-                    svg += `<text x="${x}" y="${marginTop - 12}" font-size="14" font-weight="bold" text-anchor="middle" fill="#666">✕</text>`;
+                    svg += `<text x="${x}" y="${marginTop - 8}" font-size="12" font-weight="bold" text-anchor="middle" fill="#666">✕</text>`;
                 } else if (pos.fret === 0) {
                     // Open string circle
-                    svg += `<circle cx="${x}" cy="${marginTop - 15}" r="6" fill="none" stroke="#333" stroke-width="2"/>`;
+                    svg += `<circle cx="${x}" cy="${marginTop - 10}" r="5" fill="none" stroke="#333" stroke-width="1.5"/>`;
                 } else {
-                    // Fretted position - adjust for base fret
+                    // Fretted position
                     const displayFret = pos.fret - baseFret + 1;
                     if (displayFret >= 1 && displayFret <= numFrets) {
                         const y = marginTop + (displayFret - 0.5) * fretSpacing;
 
                         // Root notes get filled circle
                         if (pos.isRoot) {
-                            svg += `<circle cx="${x}" cy="${y}" r="10" fill="#1a1a1a" stroke="#1a1a1a" stroke-width="2"/>`;
-                            // Finger number in white
+                            svg += `<circle cx="${x}" cy="${y}" r="9" fill="#1a1a1a" stroke="#1a1a1a" stroke-width="1.5"/>`;
                             if (pos.finger) {
                                 const fingerLabel = FINGER_NOTATION[fingerNotation][pos.finger] || pos.finger;
-                                svg += `<text x="${x}" y="${y + 4}" font-size="11" font-weight="bold" text-anchor="middle" fill="white">${fingerLabel}</text>`;
+                                svg += `<text x="${x}" y="${y + 3}" font-size="10" font-weight="bold" text-anchor="middle" fill="white">${fingerLabel}</text>`;
                             }
                         } else {
-                            svg += `<circle cx="${x}" cy="${y}" r="10" fill="white" stroke="#1a1a1a" stroke-width="2"/>`;
-                            // Finger number
+                            svg += `<circle cx="${x}" cy="${y}" r="9" fill="white" stroke="#1a1a1a" stroke-width="1.5"/>`;
                             if (pos.finger) {
                                 const fingerLabel = FINGER_NOTATION[fingerNotation][pos.finger] || pos.finger;
-                                svg += `<text x="${x}" y="${y + 4}" font-size="11" font-weight="bold" text-anchor="middle">${fingerLabel}</text>`;
+                                svg += `<text x="${x}" y="${y + 3}" font-size="10" font-weight="bold" text-anchor="middle">${fingerLabel}</text>`;
                             }
                         }
                     }
                 }
             });
 
-            // Barre indicator (arc connecting notes on same fret)
+            // Barre indicator
             if (voicing.barre) {
                 const barre = voicing.barre;
                 const displayFret = barre.fret - baseFret + 1;
                 if (displayFret >= 1 && displayFret <= numFrets) {
                     const y = marginTop + (displayFret - 0.5) * fretSpacing;
-                    const x1 = marginLeft + (numStrings - 1 - barre.toString) * stringSpacing;
-                    const x2 = marginLeft + (numStrings - 1 - barre.fromString) * stringSpacing;
+                    // Find string positions for barre
+                    const fromIdx = 4 - barre.toString; // Convert string num to index
+                    const toIdx = 4 - barre.fromString;
+                    const x1 = marginLeft + fromIdx * stringSpacing;
+                    const x2 = marginLeft + toIdx * stringSpacing;
 
-                    // Draw barre as a rounded rectangle
-                    svg += `<rect x="${Math.min(x1, x2) - 10}" y="${y - 8}" width="${Math.abs(x2 - x1) + 20}" height="16" rx="8" fill="#1a1a1a" opacity="0.8"/>`;
+                    svg += `<rect x="${Math.min(x1, x2) - 9}" y="${y - 6}" width="${Math.abs(x2 - x1) + 18}" height="12" rx="6" fill="#1a1a1a" opacity="0.85"/>`;
                 }
             }
 
             // Degree labels below strings
             positions.forEach((pos, idx) => {
-                const displayIdx = numStrings - 1 - idx;
-                const x = marginLeft + displayIdx * stringSpacing;
-                const y = marginTop + numFrets * fretSpacing + 18;
+                const x = marginLeft + idx * stringSpacing;
+                const y = marginTop + numFrets * fretSpacing + 15;
 
                 if (pos.degree && !pos.muted) {
-                    svg += `<text x="${x}" y="${y}" font-size="10" text-anchor="middle" fill="#666">${pos.degree}</text>`;
+                    svg += `<text x="${x}" y="${y}" font-size="9" text-anchor="middle" fill="#666">${pos.degree}</text>`;
                 } else if (pos.muted) {
-                    svg += `<text x="${x}" y="${y}" font-size="10" text-anchor="middle" fill="#999">-</text>`;
+                    svg += `<text x="${x}" y="${y}" font-size="9" text-anchor="middle" fill="#999">-</text>`;
                 }
             });