DEV Community

Cover image for Building a React Guitar Scale Visualizer: Interactive Pentatonic Patterns
Radzion Chachura
Radzion Chachura

Posted on • Originally published at radzion.com

Building a React Guitar Scale Visualizer: Interactive Pentatonic Patterns

🐙 GitHub | 🎮 Demo

Discover how to build an interactive guitar fretboard that visualizes Major and Minor pentatonic scales using React and TypeScript. This step-by-step guide will walk you through creating an intuitive widget that helps guitarists learn and practice these essential scales. You can explore the live demo at pentafret.com or dive into the complete source code on GitHub.

5 Pentatonic Patterns

Project Overview

Our application allows users to explore guitar scales interactively by selecting a root note and scale type. The app then visualizes the corresponding notes across 15 frets on the fretboard. For a detailed walkthrough of the app's foundation and the core fretboard rendering implementation, check out this post where we cover those topics in depth.

Select Root Note and Scale Type

Understanding Pentatonic Scale Patterns

One of the most powerful aspects of pentatonic scales is their symmetrical nature - the pattern remains identical for both Major and Minor scales, with only the root note position shifting. For example, compare the G Major pentatonic with the E Minor pentatonic scales - you'll notice the same finger patterns, just starting from different positions. This means that once you master the 5 basic pentatonic patterns, you can apply them to both Major and Minor scales by simply moving the patterns to different positions on the fretboard. This versatility makes pentatonic scales an incredibly efficient learning tool for guitarists, as the same finger patterns can be used to play in any key, Major or Minor.

G Major Pentatonic

Implementing Scale Pattern Relationships

To enhance the learning experience, we want to highlight the relationship between Major and Minor pentatonic scales that share the same pattern. When a user selects either a Major or Minor pentatonic scale, we display a clickable subtitle that shows its relative counterpart (e.g., "same pattern as E Minor pentatonic" for G Major pentatonic). Clicking this subtitle instantly switches to the related scale, allowing users to visually understand how the same pattern can be used for both scales.

import { UnstyledButton } from "@lib/ui/buttons/UnstyledButton"
import { useChangeScale, useScale } from "./state/scale"
import { chromaticNotesNames } from "@product/core/note"
import { PentatonicScale, scaleNames } from "@product/core/scale"
import styled from "styled-components"
import { getColor } from "@lib/ui/theme/getters"
import { getRelativePentatonic } from "@product/core/scale/getRelativePentatonic"

const Button = styled(UnstyledButton)`
  &:hover {
    color: ${getColor("textPrimary")};
  }
`

export const PentatonicSubtitle = ({ scale }: { scale: PentatonicScale }) => {
  const { rootNote } = useScale()

  const changeScale = useChangeScale()

  const relativePentatonic = getRelativePentatonic({
    scale,
    rootNote,
  })

  return (
    <Button onClick={() => changeScale(relativePentatonic)}>
      (same pattern as {chromaticNotesNames[relativePentatonic.rootNote]}{" "}
      {scaleNames[relativePentatonic.scale]} pentatonic)
    </Button>
  )
}
Enter fullscreen mode Exit fullscreen mode

Calculating Relative Scales

The getRelativePentatonic function is the core calculation engine behind this feature. For any given scale, it determines its relative counterpart by shifting the root note by three semitones - up when converting from Minor and down for Major. For instance, this is how we determine that G Major and E Minor share the same pattern.

import { match } from "@lib/utils/match"
import { chromaticNotesNumber } from "../note"
import { getPairComplement } from "@lib/utils/pair/getPairComplement"
import { scalePatterns, pentatonicScales, PentatonicScale } from "./index"

type Pentatonic = {
  scale: PentatonicScale
  rootNote: number
}

export const getRelativePentatonic = ({
  scale,
  rootNote,
}: Pentatonic): Pentatonic => {
  const [semitones] = scalePatterns["minor-pentatonic"]
  const direction = match(scale, {
    "minor-pentatonic": () => 1,
    "major-pentatonic": () => -1,
  })

  const relativeNote =
    (rootNote + semitones * direction + chromaticNotesNumber) %
    chromaticNotesNumber

  const relativeScale = getPairComplement(pentatonicScales, scale)

  return {
    scale: relativeScale,
    rootNote: relativeNote,
  }
}
Enter fullscreen mode Exit fullscreen mode

Musical Note Representation

In our application, we represent musical notes using a zero-based numbering system. Each note is assigned a number from 0 to 11, where A is 0, A# (or Bb) is 1, B is 2, C is 3, and so forth.

import { scalePatterns } from "../scale"

export const naturalNotesNames = ["A", "B", "C", "D", "E", "F", "G"]

export const chromaticNotesNames = scalePatterns.minor.reduce(
  (acc, step, index) => {
    const note = naturalNotesNames[index]

    acc.push(note)

    if (step === 2) {
      acc.push(`${note}#`)
    }

    return acc
  },
  [] as string[],
)

export const chromaticNotesNumber = chromaticNotesNames.length

export const isNaturalNote = (note: number) =>
  chromaticNotesNames[note].length === 1
Enter fullscreen mode Exit fullscreen mode

Visualizing Scale Patterns

To present the scale patterns in an organized and visually appealing way, we create a PentatonicPatterns component. This component displays a collection of essential scale shapes, each representing a different position on the fretboard. It shows a title with the current scale name (e.g., "G Major Pentatonic Patterns") and renders multiple PentatonicPattern components, one for each shape in the selected scale:

import { range } from "@lib/utils/array/range"
import { scaleNames, PentatonicScale, scalePatterns } from "@product/core/scale"
import { Text } from "@lib/ui/text"
import { chromaticNotesNames } from "@product/core/note"
import { useScale } from "../state/scale"
import { VStack } from "@lib/ui/css/stack"
import { PentatonicPattern } from "./PentatonicPattern"

export const PentatonicPatterns = ({ scale }: { scale: PentatonicScale }) => {
  const { rootNote } = useScale()

  const noteName = chromaticNotesNames[rootNote]
  const scaleName = scaleNames[scale]

  const title = `${noteName} ${scaleName} Pentatonic Patterns`

  return (
    <VStack gap={60}>
      <VStack gap={8}>
        <Text
          centerHorizontally
          weight={800}
          size={32}
          color="contrast"
          as="h2"
        >
          {title}
        </Text>
        <Text
          centerHorizontally
          weight={700}
          size={20}
          color="supporting"
          as="h4"
        >
          {scalePatterns[scale].length} Essential Shapes for Guitar Solos
        </Text>
      </VStack>
      {range(scalePatterns[scale].length).map((index) => (
        <PentatonicPattern key={index} index={index} scale={scale} />
      ))}
    </VStack>
  )
}
Enter fullscreen mode Exit fullscreen mode

Rendering Individual Patterns

Each pattern is rendered using the PentatonicPattern component, which creates an interactive fretboard visualization for a specific position. The component takes a pattern index and scale type as props, calculates the note positions using getPentatonicPattern, and renders them on a fretboard. Root notes are highlighted to help guitarists identify key reference points while practicing:

import { IndexProp } from "@lib/ui/props"
import { useScale } from "../state/scale"
import { PentatonicScale, scaleNames } from "@product/core/scale"
import { chromaticNotesNames } from "@product/core/note"
import { stringsCount, tuning } from "../../guitar/config"
import { Fretboard } from "../../guitar/fretboard/Fretboard"
import { Note } from "../../guitar/fretboard/Note"
import { VStack } from "@lib/ui/css/stack"
import { Text } from "@lib/ui/text"
import { getNoteFromPosition } from "@product/core/note/getNoteFromPosition"
import { getPentatonicPattern } from "./getPentatonicPattern"

export const PentatonicPattern = ({
  index: patternIndex,
  scale,
}: IndexProp & { scale: PentatonicScale }) => {
  const { rootNote } = useScale()

  const notes = getPentatonicPattern({
    index: patternIndex,
    scale,
    rootNote,
    stringsCount,
    tuning,
  })

  const noteName = chromaticNotesNames[rootNote]
  const scaleName = scaleNames[scale]

  const title = `${noteName} ${scaleName} Pentatonic Pattern #${patternIndex + 1}`

  return (
    <VStack gap={40}>
      <Text centerHorizontally color="contrast" as="h3" weight="700" size={18}>
        {title}
      </Text>
      <Fretboard>
        {notes.map((position) => {
          const note = getNoteFromPosition({ tuning, position })

          return (
            <Note
              key={`${position.string}-${position.fret}`}
              {...position}
              kind={rootNote === note ? "primary" : "regular"}
            />
          )
        })}
      </Fretboard>
    </VStack>
  )
}
Enter fullscreen mode Exit fullscreen mode

Fretboard Position System

To reference a note's position on the fretboard, we need two pieces of information: the string number (zero-based, from top to bottom) and the fret number (where -1 represents an open string, and 0 represents the first fret):

export type NotePosition = {
  // 0-based index of the string
  string: number
  // -1 if the note is open
  // 0 if the note is on the 1st fret
  fret: number
}
Enter fullscreen mode Exit fullscreen mode

Pattern Generation Algorithm

The getPentatonicPattern function builds each scale shape using a systematic approach. First, it determines the starting note by adding up the scale intervals based on the pattern index. Then, it follows a specific traversal strategy: starting from the lowest string (6th), it places two notes per string, working its way up to the highest string (1st). For each new note, it calculates the fret position by either using the first note of the pattern or by adding the appropriate interval to the previous note's position. When moving to a new string, the algorithm applies a shift of 4 or 5 frets (depending on the string) to maintain the pattern's shape:

import { scalePatterns } from "@product/core/scale"
import { sum } from "@lib/utils/array/sum"
import { match } from "@lib/utils/match"
import { getRelativePentatonic } from "@product/core/scale/pentatonic/getRelativePentatonic"
import { PentatonicScale } from "@product/core/scale"
import { getLastItem } from "@lib/utils/array/getLastItem"
import { range } from "@lib/utils/array/range"
import { getNoteFret } from "@product/core/guitar/getNoteFret"
import { chromaticNotesNumber } from "@product/core/note"
import { NotePosition } from "@product/core/note/NotePosition"

type Input = {
  index: number
  scale: PentatonicScale
  rootNote: number
  stringsCount: number
  tuning: number[]
}

export const getPentatonicPattern = ({
  index,
  scale,
  rootNote,
  stringsCount,
  tuning,
}: Input) => {
  const pattern = scalePatterns["minor-pentatonic"]

  const minorRootNote = match(scale, {
    "minor-pentatonic": () => rootNote,
    "major-pentatonic": () =>
      getRelativePentatonic({ scale, rootNote }).rootNote,
  })

  const firstNote =
    (minorRootNote + sum(pattern.slice(0, index))) % chromaticNotesNumber

  const result: NotePosition[] = []

  range(stringsCount * 2).forEach((index) => {
    const string = stringsCount - Math.floor(index / 2) - 1

    const openNote = tuning[string]

    const previousPosition = getLastItem(result)

    const getFret = () => {
      if (!previousPosition) {
        return getNoteFret({ openNote, note: firstNote })
      }

      const step = pattern[(index + index - 1) % pattern.length]

      const fret = previousPosition.fret + step

      if (index % 2 === 0) {
        const shift = string === 1 ? 4 : 5

        return fret - shift
      }

      return fret
    }

    result.push({
      string,
      fret: getFret(),
    })
  })

  if (result.some((position) => position.fret < -1)) {
    return result.map((position) => ({
      ...position,
      fret: position.fret + chromaticNotesNumber,
    }))
  }

  return result
}
Enter fullscreen mode Exit fullscreen mode

Handling Edge Cases

Sometimes, our pattern calculation might result in negative fret positions (less than -1), which aren't playable on a guitar. In such cases, we shift the entire pattern up by an octave (12 frets) to keep it in a playable range. For example, in the G Minor pentatonic pattern shown below, we move all notes 12 frets higher to maintain the same pattern shape in a more practical position:

Moving Pattern an octave higher

Conclusion

With these components and algorithms in place, we've created an interactive learning tool that helps guitarists visualize and practice pentatonic scales across the fretboard. The application handles the complexities of musical theory and fretboard geometry, allowing players to focus on mastering these essential patterns in any key they choose.

Top comments (6)

Collapse
 
highcenburg profile image
Vicente G. Reyes

I'm on B Standard/Drop A on my 6 string guitar. Would love to have an option to choose a tuning to show all notes of the fretboard 😬

Collapse
 
dan_campbell_8a72ca0ca1d2 profile image
Dan Campbell

Look at songperformer.net in the theory section, it has lots of different tunings, even a sequencer, you can also overlay scales/modes/chord tones across the fretboard, it's free to use, I wrote it.

Collapse
 
highcenburg profile image
Vicente G. Reyes

Interesting. I'll take a look at this

Collapse
 
radzion profile image
Radzion Chachura

Hello! I've noted your request and added the tuning customization to the backlog. :)

Collapse
 
highcenburg profile image
Vicente G. Reyes

Can't wait!

Collapse
 
dan_campbell_8a72ca0ca1d2 profile image
Dan Campbell

There are quite a few fretboard visualisers out there, I wrote the one in songperformer.net, using JavaScript, no frameworks. For fun you should add audio, colour coding, left and right handed, allow selection of number of frets, different tunings and instruments, show on keyboards at the same time and allow multiple scales/ modes to overlay, circle of fifths and AI integration for music theory help. I did this embedded in a WASM app so no server trips required. Use SVG, then you will have scalable graphics too.
Image description