A few weeks ago I begrudgingly decided that my Chord project needs a web-based front-end. After weighing various options, I decided to implement the heart of the front-end as a React-based ASCII chord chart renderer.
After some initial code sketching, I had a working prototype, and a few revisions later I found myself happy with the final code. Let’s dig into it!
What’s the Goal?
Before we start diving into code, let’s take a look at what we’ll be building.
Our Chord back-end treats chords as either a list of optional numbers representing frets played on specific strings, or a list of optional two-tuples of numbers representing the fret played and the finger used to play that fret. For example, on the back-end we’d represent a classic open C major chord with the following list:
[nil, 3, 2, 0, 1, nil]
And with a common fingering:
[nil, {3, 3}, {2, 2}, {0, nil}, {1, 1}, nil]
Unfortunately, Javascript doesn’t have a “tuple” type, so we’re forced to represent our chords as either one or two dimensional arrays of numbers. In our front-end, those same chords would be represented like so:
[null, 3, 2, 0, 1, null]
[null, [3, 3], [2, 2], [0, null], [1, 1], null]
Our goal is to transform that representation into the following chord chart:
C major chord chart.
Let’s get to it!
Building Our Chart
We’ll start by creating a new React component to render a chord passed in through a given chord
prop, and rendering a styled pre
element to hold our soon-to-be chord chart:
const Chart = styled.pre`
font-family: "Source Code Pro";
text-align: center;
`;
export default ({ chord }) => {
return (
<Chart/>
);
};
Before we render our chord, we’ll need to calculate some basic metrics which we’ll use throughout the process, and lay out our plan of attack:
export default ({ chord, name }) => {
let { min, max } = getMinAndMax(chord)
return (
<Chart>
{_.chain()
.thru(buildFretRange)
.thru(buildFretRows)
.thru(intersperseFretWire)
.thru(appendFingering)
.thru(attachLeftGutter)
.thru(joinRows)
.value()}
</Chart>
);
};
The getMinAndMax
helper is defined globally inside our module and simply filters out unplayed frets and returns an object consisting of the minimum fret used in the chord (min
), and the maximum fret used in the chord (max
):
const getMinAndMax = chord =>
_.chain(chord)
.map(string => (_.isArray(string) ? string[0] : string))
.reject(_.isNull)
.thru(frets => ({
min: _.min(frets),
max: _.max(frets)
}))
.value();
Once we’re armed with these metrics, we can see that our game plan is to build our range of frets (buildFretRange
), build each of our fret rows (buildFretRows
), intersperse our fret wire between those fret rows (intersperseFretWire
), append any fingering instructions that were passed in with our chord
(appendFingering
), attach the left gutter (attachLeftGutter
), and join everything together (joinRows
).
Now we need to build out each of these component pieces.
Divide and Conquer
With min
and max
in scope, we can easily build a helper function to build our fret range:
const buildFretRange = () => _.range(min, Math.max(max + 1, min + 5));
Notice that we’re enforcing a minimum height on our chord chart. If the range of our chord is less than five frets, we’ll render enough empty frets at the bottom of the chart to fill the remaining space.
Our resulting range is a range of numbers, one for each fret used in the span of our chord.
Once we have our chord’s range, we can transform each of the frets in that range into a renderable representation of a fret row:
const buildFretRows = frets =>
_.map(frets, fret =>
_.chain(_.range(chord.length))
.map(
string =>
(_.isArray(chord[string]) ? chord[string][0] : chord[string]) ==
fret ? (
<Finger>{fret == 0 ? "○" : "●"}</Finger>
) : (
<Wire>{fret == 0 ? "┬" : "│"}</Wire>
)
)
.value()
);
We start by mapping over each fret
in our list of frets
. For each fret
, We map over each of the strings in our chord (_.range(chord.length)
). Next, we check if each string
and fret
combination is being played in our current chord. If it is, we render either a ●
symbol, if the fret is being fingered, or a ○
symbol if we’re playing an open string.
If we’re not playing the string
/fret
combination, we render a fret wire with either the ┬
symbol used to represent the nut of the guitar, or the │
symbol used to represent an unfretted string.
Both Finger
and Wire
are simply styled span
elements:
const Finger = styled.span`
font-weight: bold;
`;
const Wire = styled.span``;
At this point, our chord chart is starting to take shape, but without any horizontal fret wire or fret markers, it’s a bit disorienting to look at:
C major chord chart without fret wire.
Let’s clear things up a bit by interspersing fret wire between each of our fret rows:
const intersperseFretWire = rows =>
_.flatMap(rows, row => [
row,
<Wire>{`├${_.repeat("┼", chord.length - 2)}┤`{:.language-javascript}}</Wire>
]);
We use Lodash’s flatMap
to append a Wire
component after each of our fret rows. This leaves us with an array of alternating fret rows and fret wires.
Some chords come complete with fingering suggestions. We’ll place those suggestions below our chord chart:
const appendFingering = rows => [
...rows,
<Fingering>
{_.chain(chord)
.map(fret => (_.isArray(fret) ? fret[1] : " "))
.value()}
</Fingering>
];
Note that the Fingering
component is just a (un-)styled span
:
const Fingering = styled.span``;
We’re almost finished. Some chords are played further up the neck than others. Without indicating where the nut of our guitar is, a player has no way of orienting themselves.
Let’s give the readers of our charts some grounding by labeling the lowest fret of our chart in a left gutter:
const attachLeftGutter = rows =>
_.map(rows, (row, i) => (
<Fragment>
<Label>{i == 0 && min != 0 ? _.pad(min, 2) : " "}</Label>
{row}
</Fragment>
));
React’s new Fragment
syntax gives us a nice way of combining multiple rendered components without introducing extra DOM cruft.
Notice that we’re not rendering fret labels for open chords. Because we’re rendering the nut using special symbols (┬
), we don’t need to indicate that the chord starts on fret zero.
Final Thoughts
That’s all there is to it. We can use our new component to render a wide variety of chords:
<Chord chord={[null, 10, 10, 9, 12, null]} />
<Chord chord={[null, 8, 10, 9, 10, null]} />
<Chord chord={[null, 3, 8, 6, 9, null]} />
<Chord chord={[null, [3, 3], [2, 2], [0, null], [1, 1], null]} />
<Chord chord={[null, [10, 2], [10, 3], [9, 1], [12, 4], null]} />
All of which look beautiful when rendered in glorious ASCII!
Our chords.
Be sure to check out the entire project on Github, and while you’re at it, check out the refactor of my original solution done by Giorgio Torres. Giorgio swooped in after I complained that my first iteration was some of the ugliest React I’ve ever written and contributed his highly-polished solution. Thanks Giorgio!