Building a React Guitar Scale Visualizer: Interactive Pentatonic Patterns

Building a React Guitar Scale Visualizer: Interactive Pentatonic Patterns

February 16, 2025

9 min read

Building a React Guitar Scale Visualizer: Interactive Pentatonic Patterns

Building an Interactive Pentatonic Scale Visualizer

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
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
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
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>
  )
}

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,
  }
}

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

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>
  )
}

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>
  )
}

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
}

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
}

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
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.