DEV Community

Cover image for Building an Interactive Blues Scale Visualizer with React and TypeScript
Radzion Chachura
Radzion Chachura

Posted on โ€ข Originally published at radzion.com

1

Building an Interactive Blues Scale Visualizer with React and TypeScript

๐Ÿ™ GitHub | ๐ŸŽฎ Demo

In this post, we'll explore how to create an interactive visualization of the blues scale on a guitar fretboard using React and TypeScript. You can explore the complete source code in the GitHub repository and try out the live demo here. The project is built on RadzionKit, a robust boilerplate that provides essential components and utilities for a streamlined development experience.

A Minor Blues Scale on the Guitar

Core Data Structures

Let's start by defining our core data structure. A musical scale in our application is represented by a TypeScript interface with three essential properties: type, tonality and rootNote, which defines the starting note of the scale.

import { Tonality } from "../tonality"
import { ScaleType } from "./ScaleType"

export type Scale = {
  type: ScaleType
  tonality: Tonality
  rootNote: number
}
Enter fullscreen mode Exit fullscreen mode

Scale Types and Patterns

The scale's tonality determines its fundamental character, being either "minor" or "major".

export const tonalities = ["minor", "major"] as const
export type Tonality = (typeof tonalities)[number]
Enter fullscreen mode Exit fullscreen mode

Each scale comes in three types: full, pentatonic, and blues. The scale pattern represents the sequence of intervals (measured in semitones) between consecutive notes. For example, in a minor blues scale, we move up 3 semitones, then 2, then 1, and so on, creating its distinctive bluesy sound.

import { Tonality } from "../tonality"
import { ScalePattern } from "./ScalePattern"

export const scaleTypes = ["full", "pentatonic", "blues"] as const
export type ScaleType = (typeof scaleTypes)[number]

export const scalePatterns: Record<
  ScaleType,
  Record<Tonality, ScalePattern>
> = {
  full: {
    major: [2, 2, 1, 2, 2, 2, 1],
    minor: [2, 1, 2, 2, 1, 2, 2],
  },
  pentatonic: {
    major: [2, 2, 3, 2, 3],
    minor: [3, 2, 2, 3, 2],
  },
  blues: {
    major: [2, 1, 1, 3, 2, 3],
    minor: [3, 2, 1, 1, 3, 2],
  },
}

export const scalePatternsNumber = 5
Enter fullscreen mode Exit fullscreen mode

State Management

We implement URL-based state management to handle scale changes, encoding the scale parameters directly in the URL (e.g., /scale/A/blues/minor)

import { getValueProviderSetup } from "@lib/ui/state/getValueProviderSetup"
import { useRouter } from "next/router"
import { useCallback } from "react"
import { toUriNote } from "@product/core/note/uriNote"
import { Scale } from "@product/core/scale/Scale"

export const makeScalePath = ({ type, tonality, rootNote }: Scale) =>
  `/scale/${toUriNote(rootNote)}/${type}/${tonality}`

export const { useValue: useScale, provider: ScaleProvider } =
  getValueProviderSetup<Scale>("Scale")

export const useChangeScale = () => {
  const value = useScale()

  const { push } = useRouter()

  return useCallback(
    (params: Partial<Scale>) => {
      push(makeScalePath({ ...value, ...params }))
    },
    [push, value],
  )
}
Enter fullscreen mode Exit fullscreen mode

Component Architecture

The scale page architecture is thoughtfully organized into four essential components, each serving a distinct purpose in our interactive learning environment. At its core, we have the ScaleManager for dynamically changing scales, the ScalePageTitle for clear scale identification, the ScaleNotes for interactive fretboard visualization, and the ScalePatterns for mastering the five fundamental scale positions. While we covered the fretboard rendering mechanics in our previous article here, this post delves into the specialized aspects of blues scale visualization.

import { VStack } from "@lib/ui/css/stack"
import { ScalePageTitle } from "./ScalePageTitle"
import { ScaleProvider } from "./state/scale"
import { ScaleNotes } from "./ScaleNotes"
import { PageContainer } from "../layout/PageContainer"
import { ScaleManager } from "./manage/ScaleManager"
import { ValueProp } from "@lib/ui/props"
import { Scale } from "@product/core/scale/Scale"
import { ScalePatterns } from "./patterns/ScalePatterns"

export const ScalePage = ({ value }: ValueProp<Scale>) => {
  return (
    <ScaleProvider value={value}>
      <PageContainer>
        <VStack gap={120}>
          <VStack gap={60}>
            <ScaleManager />
            <ScalePageTitle />
            <ScaleNotes />
          </VStack>
          <ScalePatterns />
        </VStack>
      </PageContainer>
    </ScaleProvider>
  )
}
Enter fullscreen mode Exit fullscreen mode

Intuitive Scale Visualization

To help users grasp the blues scale intuitively, we leverage their likely familiarity with the pentatonic scale. Within the ScalePageTitle component, we display a descriptive subtitle that presents the blues scale as a natural extension of its pentatonic counterpart. For instance, when viewing an A minor blues scale, the subtitle would show "A minor pentatonic + D# (blue note)", making it clear that a blues scale is essentially a pentatonic scale with one additional noteโ€”the characteristic blue note that gives the scale its distinctive sound.

import { useScale } from "./state/scale"
import { getScaleName } from "@product/core/scale/getScaleName"
import { getBlueNote } from "@product/core/scale/blues/getBlueNote"
import { chromaticNotesNames } from "@product/core/note"

export const BluesScaleSubtitle = () => {
  const scale = useScale()

  const text = `${getScaleName({ ...scale, type: "pentatonic" })} + ${chromaticNotesNames[getBlueNote(scale)]} (blue note)`

  return text
}
Enter fullscreen mode Exit fullscreen mode

Visual Note Differentiation

To enhance the visual distinction between different types of notes on the fretboard, we implement the ScaleNote component. This component determines how each note should be displayed based on its role in the scale. Root notes are rendered with primary emphasis, while the characteristic blue note receives special styling when playing a blues scale.

import { useMemo } from "react"
import { Note, NoteKind, NoteProps } from "../guitar/fretboard/Note"
import { useScale } from "./state/scale"
import { getBlueNote } from "@product/core/scale/blues/getBlueNote"
import { getNoteFromPosition } from "@product/core/note/getNoteFromPosition"
import { tuning } from "../guitar/config"

type ScaleNoteProps = Omit<NoteProps, "kind">

export const ScaleNote = (props: ScaleNoteProps) => {
  const { rootNote, type, tonality } = useScale()
  const note = getNoteFromPosition({ tuning, position: props })

  const kind: NoteKind = useMemo(() => {
    if (rootNote === note) {
      return "primary"
    }

    if (type === "blues" && note === getBlueNote({ rootNote, tonality })) {
      return "blue"
    }

    return "regular"
  }, [note, rootNote, tonality, type])

  return <Note {...props} kind={kind} />
}
Enter fullscreen mode Exit fullscreen mode

Finding the Blue Note

The getBlueNote function uses set theory to identify the blue note of a scale. Rather than hardcoding note positions, it calculates the note by finding the difference between the blues scale and its pentatonic counterpart. This approach leverages the fact that a blues scale consists of a pentatonic scale plus one additional note. By generating both scale patterns and comparing their note sets, the function can determine the blue note programmatically.

import { difference } from "@lib/utils/array/difference"
import { getScaleNotes } from "../getScaleNotes"
import { Scale } from "../Scale"
import { scalePatterns } from "../ScaleType"

export const getBlueNote = (scale: Omit<Scale, "type">) => {
  const pentatonicNotes = getScaleNotes({
    rootNote: scale.rootNote,
    pattern: scalePatterns.pentatonic[scale.tonality],
  })

  const bluesNotes = getScaleNotes({
    rootNote: scale.rootNote,
    pattern: scalePatterns.blues[scale.tonality],
  })

  const [blueNote] = difference(pentatonicNotes, bluesNotes)

  return blueNote
}
Enter fullscreen mode Exit fullscreen mode

Scale Pattern Implementation

While visualizing individual notes is valuable, guitarists typically learn scales through repeatable patterns across the fretboard. These patterns break down the scale into five manageable positions, making it easier to navigate the fretboard during improvisation. The ScalePatterns component implements this approach by displaying these essential positions:

import { useMemo } from "react"
import { useScale } from "../state/scale"
import { match } from "@lib/utils/match"
import { scalePatternsNumber } from "@product/core/scale/ScaleType"
import { range } from "@lib/utils/array/range"
import { getPentatonicPattern } from "@product/core/scale/pentatonic/getPentatonicPattern"
import { getBluesScalePattern } from "@product/core/scale/blues/getBluesScalePattern"
import { stringsCount, tuning } from "../../guitar/config"
import { VStack } from "@lib/ui/css/stack"
import { Text } from "@lib/ui/text"
import { ScalePattern } from "./ScalePattern"
import { getScaleName } from "@product/core/scale/getScaleName"

export const ScalePatterns = () => {
  const scale = useScale()

  const scaleName = getScaleName(scale)

  const patterns = useMemo(() => {
    const { type } = scale
    if (type === "full") return undefined

    const generate = match(type, {
      pentatonic: () => getPentatonicPattern,
      blues: () => getBluesScalePattern,
    })

    return range(scalePatternsNumber).map((index) =>
      generate({ index, scale, stringsCount, tuning }),
    )
  }, [scale])

  if (!patterns) return null

  return (
    <VStack gap={60}>
      <VStack gap={8}>
        <Text
          centerHorizontally
          weight={800}
          size={32}
          color="contrast"
          as="h2"
        >
          {scaleName} Patterns
        </Text>
        <Text
          centerHorizontally
          weight={700}
          size={20}
          color="supporting"
          as="h4"
        >
          {scalePatternsNumber} Essential Shapes for Guitar Solos
        </Text>
      </VStack>
      {patterns.map((pattern, index) => (
        <ScalePattern key={index} value={pattern} index={index} />
      ))}
    </VStack>
  )
}
Enter fullscreen mode Exit fullscreen mode

Building Blues Patterns

We've already covered pentatonic patterns in the previous article. Building on this foundation, our blues scale patterns extend the pentatonic shapes by strategically inserting the blue note. The getBluesScalePattern function takes a pentatonic pattern and examines each note position, adding the blue note either one fret below the root or one fret above specific pentatonic notes.

import { getPentatonicPattern } from "../pentatonic/getPentatonicPattern"
import { Scale } from "../Scale"
import { NotePosition } from "../../note/NotePosition"
import { getNoteFromPosition } from "../../note/getNoteFromPosition"
import { normalizeFretPositions } from "../../note/normalizeFretPositions"
import { getBlueNote } from "./getBlueNote"

type Input = {
  index: number
  scale: Omit<Scale, "type">
  stringsCount: number
  tuning: number[]
}

export const getBluesScalePattern = (input: Input) => {
  const pentatonicPattern = getPentatonicPattern(input)

  const { scale, tuning } = input

  const blueNote = getBlueNote(scale)

  const result: NotePosition[] = []

  pentatonicPattern.forEach((position, noteIndex) => {
    result.push(position)

    const note = getNoteFromPosition({ position, tuning })
    if (noteIndex === 0 && note - 1 === blueNote) {
      result.push({
        string: position.string,
        fret: position.fret - 1,
      })
    } else if (
      noteIndex < pentatonicPattern.length - 1 &&
      note + 1 === blueNote
    ) {
      result.push({
        string: position.string,
        fret: position.fret + 1,
      })
    }
  })

  return normalizeFretPositions(result)
}
Enter fullscreen mode Exit fullscreen mode

This implementation ensures that each pattern position remains intuitive to play while incorporating the essential blue note. By placing the blue note adjacent to existing pentatonic notes, guitarists can seamlessly transition between pentatonic and blues phrases, enriching their improvisational vocabulary.

Playwright CLI Flags Tutorial

5 Playwright CLI Flags That Will Transform Your Testing Workflow

  • 0:56 --last-failed: Zero in on just the tests that failed in your previous run
  • 2:34 --only-changed: Test only the spec files you've modified in git
  • 4:27 --repeat-each: Run tests multiple times to catch flaky behavior before it reaches production
  • 5:15 --forbid-only: Prevent accidental test.only commits from breaking your CI pipeline
  • 5:51 --ui --headed --workers 1: Debug visually with browser windows and sequential test execution

Learn how these powerful command-line options can save you time, strengthen your test suite, and streamline your Playwright testing experience. Click on any timestamp above to jump directly to that section in the tutorial!

Watch Full Video ๐Ÿ“น๏ธ

Top comments (0)

Image of Timescale

๐Ÿ“Š Benchmarking Databases for Real-Time Analytics Applications

Benchmarking Timescale, Clickhouse, Postgres, MySQL, MongoDB, and DuckDB for real-time analytics. Introducing RTABench ๐Ÿš€

Read full post โ†’

๐Ÿ‘‹ Kindness is contagious

If this article connected with you, consider tapping โค๏ธ or leaving a brief comment to share your thoughts!

Okay