Background

GuitaR is a simple Shiny app to facilitate guitar soloing learning process, based on scales and intervals. GuitaR generates selected type of scale (e.g. minor pentatonic) in a given key (e.g. key of A) and displays it on the fretboard. Coloring can be used to highlight root notes and intervals.

URL: https://arturmatysik.shinyapps.io/guitaR_app/

In this tutorial I will walk through the design of GuitaR Shiny app with tidyverse meta package.

Topics include:

Generate fretboard

Libraries

The code is dependent on the following packages:

  • ggplot2
  • dplyr
  • stringr

They are all part of tidyverse meta package:

library(tidyverse)

Guitar setup

Start with guitar setup. Create list guitar_setup and store:

  • tuning: vector of string names, e.g.: c("E", "A", "D", "G", "B", "E") will be the standard guitar tuning.
  • frets: number of frets that will be later used for fretboard display.
# setup
guitar_setup <- list(
  tuning = c("E", "A", "D", "G", "B", "E"),
  frets = 17
)

Although we can provide separately number of strings that guitar uses, it will be redundant, because string can’t exist without tuning! If necessary, number of strings can be easily calculated (e.g. length(guitar_setup$tuning)).

Fretboard layout

Simple fretboard

From the setup we know the number of frets as well as number and names of strings. That it all we need to draw an empty fretboard with ggplot().

  • Use geom_vline() to draw frets and nut (with significantly thicker line)
  • Use geom_hline() to draw strings
    • thickness of the strings is provided as a vector generated with seq() function. In this case it will create vector of length(guitar_setup$tuning) elements, ranging between 1.5 to 0.5 (seq(1.5, 0.5, length.out = length(guitar_setup$tuning)))
  • Use scale_y_continuous() to label strings
    • set breaks to 1:length(guitar_setup$tuning)
    • set labels to guitar_setup$tuning
  • Use scale_x_continuous() to label strings
    • breaks and labels equal to 1:guitar_setup$frets
  • Remove unwanted margin with expand()
  • Use theme_minimal() for minimalist design
  • Use theme() attributes to
    • remove grid panel.grid.minor = element_blank()
    • remove axis labels axis.title=element_blank()

Note, that (for now) we will skip aesthetics (aes()) and provide data (xintercept, yintercept) directly.

library(tidyverse)
ggplot(NULL) +
  # plot frets
  geom_vline(xintercept = 1:guitar_setup$frets, color = "gray60") +
  # plot nut
  geom_vline(xintercept = 0, color = "gray50", size = 2) +
  # plot strings
  geom_hline(
    yintercept = 1:length(guitar_setup$tuning),
    # string thickness
    size = seq(1.5, 0.5, length.out = length(guitar_setup$tuning))
  ) +
  # label strings
  scale_y_continuous(
    breaks = 1:length(guitar_setup$tuning),
    labels = guitar_setup$tuning,
    expand = c(0.07, 0.07)
  ) +
  # label frets
  scale_x_continuous(
    breaks = 1:guitar_setup$frets,
    labels = 1:guitar_setup$frets,
    expand = c(.005, .005)
  ) +
  theme_minimal() +
  theme(# remove unwanted grids
    panel.grid.minor = element_blank(),
    # remove axis labels
    axis.title = element_blank())

That looks good already! But lets make it even better.

Fret distance

Real guitar frets are not evenly spread. Their distance is bigger close to the guitar neck, and smaller towards the bridge. Without going into details, the following formula can help in calculating fret relative position: 1 - exp(-k * n) where n is the fret number and k is a constant of k = 5.71584144995393e-2.

Use the formula to generate function, that takes fret number and returns its relative position:

calc_fret_distance <- function(n, s = 1, k = 5.71584144995393e-2) {
  s * (1 - exp(-k * n))
}

# position of nut
calc_fret_distance(0)
## [1] 0
# position of 1st fret
calc_fret_distance(1)
## [1] 0.05555556
# position of 10th fret
calc_fret_distance(10)
## [1] 0.4353697

Having this convenient way of fret position calculation, we can create simple data frame fretboard, to hold fret number (fret_num) and fret position fret_pos):

fretboard <- data.frame(
  fret_num = 0:guitar_setup$frets,
  fret_pos = calc_fret_distance(0:guitar_setup$frets)
)
head(fretboard)
##   fret_num   fret_pos
## 1        0 0.00000000
## 2        1 0.05555556
## 3        2 0.10802469
## 4        3 0.15757888
## 5        4 0.20438005
## 6        5 0.24858116

Fretboard markers

Another missing things are the markers. Lets add them to the fretboard_layout data frame. For standard guitar markers are located at frets 3, 5, 7, 9, 12, 15, 17, 19, 21, 24.

Typically, there are two types:

  • single markers (marker = "single"), located at frets 3, 5, 7, 9, 15, 17, 19, 21 (fret_num = c(3, 5, 7, 9, 15, 17, 19, 21))
  • double marker (marker = "double") at frets 12, 24 (fret_num = c(12, 24))

Their position however is not exactly where the fret is, but rather between two frets. For example: marker at 5th fret will lie between 4th and 5th fret.

First, create function calc_fret_marker_position, that will take fret number and vector of positions (fretboard$fret_pos) to calculate mean position between two frets.

calc_fret_marker_position <- function(n, pos) {
  mean(c(pos[n], pos[n + 1]))
}

Use Vectorize() to enable vector input:

calc_fret_marker_position_v <-
  Vectorize(calc_fret_marker_position, "n")

Then, use vectorized function to calculate fretboard_markers data frame, holding marker X positions (marker_posX).

As for the Y position (marker_posY):

  • Single markers are place in the middle (max(length(guitar_setup$tuning) + 1) / 2)
  • For double markers calculate middle point as for single, then double and separate +1 up or down by adding + c(-1, 1) to the middle position. Note, that by multiplying c(-1, 1) by some value we can control distance between the two markers!
fretboard <- fretboard %>%
  mutate(
    marker_type = case_when(
      fret_num %in% c(12, 24) ~ "double",
      fret_num %in% c(3, 5, 7, 9, 12, 15, 17, 19, 21, 24) ~ "single"
    ),
    marker_posY = max(length(guitar_setup$tuning) + 1) / 2,
    marker_posX = calc_fret_marker_position_v(fret_num, fret_pos)
  )

head(fretboard)
##   fret_num   fret_pos marker_type marker_posY marker_posX
## 1        0 0.00000000        <NA>         3.5  0.00000000
## 2        1 0.05555556        <NA>         3.5  0.02777778
## 3        2 0.10802469        <NA>         3.5  0.08179012
## 4        3 0.15757888      single         3.5  0.13280178
## 5        4 0.20438005        <NA>         3.5  0.18097946
## 6        5 0.24858116      single         3.5  0.22648061

Fancy fretboard

We can combine all above functions into one calc_fretboard(), that will take guitar_setup and return the fretboard:

# design fretboard
calc_fretboard <- function(guitar_setup) {
  # calculate layout
  calc_fret_distance <- function(n, s = 1, k = 5.71584144995393e-2) {
    s * (1 - exp(-k * n))
  }
  # calculate markers
  calc_fret_marker_position <- function(n, pos) {
    mean(c(pos[n], pos[n+1]))
  }
  calc_fret_marker_position_v <- Vectorize(calc_fret_marker_position, "n")
  
  data.frame(
    fret_num = 0:guitar_setup$frets,
    fret_pos = calc_fret_distance(0:guitar_setup$frets)
  ) %>%
    mutate(
      marker_type = case_when(
        fret_num %in% c(12, 24) ~ "double",
        fret_num %in% c(3, 5, 7, 9, 12, 15, 17, 19, 21, 24) ~ "single"
      ),
      marker_posY = max(length(guitar_setup$tuning) + 1) / 2,
      marker_posX = calc_fret_marker_position_v(fret_num, fret_pos)
    )
}

fretboard <- calc_fretboard(guitar_setup)

Fancy fretboard - plot

Use fretboard for plotting the fretboard:

# plot empty fretboard
fr <- ggplot(NULL) +
  # plot frets
  geom_vline(data = fretboard, aes(xintercept = fret_pos), color = "gray60") +
  # plot nut
  geom_vline(xintercept = 0, color = "gray50", size = 2) +
  # plot single markers
  geom_point(
    data = fretboard %>% filter(marker_type == "single"),
    aes(x = marker_posX, y = marker_posY),
    size = 5, color = "gray80"
  ) +
  # plot double markers
  geom_point(
    data = fretboard %>% filter(marker_type == "double"),
    aes(x = marker_posX, y = marker_posY + 1),
    size = 5, color = "gray80"
  ) +
    geom_point(
    data = fretboard %>% filter(marker_type == "double"),
    aes(x = marker_posX, y = marker_posY - 1),
    size = 5, color = "gray80"
  ) +
  # plot strings
  geom_hline(
    yintercept = 1:length(guitar_setup$tuning),
    # string thickness
    size = seq(1.5, 0.5, length.out = length(guitar_setup$tuning))
  ) +
  # label strings
  scale_y_continuous(
    breaks = 1:length(guitar_setup$tuning),
    labels = guitar_setup$tuning,
    expand = c(0.07, 0.07)
  ) +
  # label frets
  scale_x_continuous(
    breaks = fretboard$fret_pos,
    labels = fretboard$fret_num,
    expand = c(.005, .005)
  ) +
  theme_minimal() +
  theme(panel.grid.minor = element_blank(),
        axis.title=element_blank()
        )

fr

Fretboard notes

Ok, we have a fretboard with labeled frets, markers and strings, each string is in tune. Now we can ask, what are the notes on each fret for each string?

In western music, there are 12 notes in an octave: C, C#/Db, D, D#/Eb, E, F, F#/Gb, G, G#/Ab, A, A#/Bb and B.

Lets store them as a vector notes in this exact order (which corresponds to C chromatic scale). For simplicity, only # notation will be used here.

notes = c("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B")

Having in mind, that with each fret note increases by one step, lets ask what is the note “E” string at 3rd fret? Step by step answer can be:

string_tune <- "E"
fret_num <- 3

# is note equal to string tune? 
notes == string_tune
##  [1] FALSE FALSE FALSE FALSE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
# what is the index of note equal to string tune?
which(notes == string_tune)
## [1] 5
# what is the note with index of note equal to string tune?
notes[which(notes == string_tune) + fret_num]
## [1] "G"

Note, that above method will not work if we use e.g. string “B” at 2nd fret, because which(notes == string_tune) + fret_num is equal to 15, which is more than notes vector length (12). Simple trick of doubling the note vector (using rep(c(....), 2)) will partially deal with that problem. Since each note is now doubled, select only the first one ([1]):

notes <- rep(c("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"), 2)
string_tune <- "B"
fret_num <- 3
notes[which(notes == string_tune) + fret_num][1]
## [1] "D"

Unfortunately, this will still not work for large higher frets (to be exact if which(notes == string_tune) + fret_num > length(notes)). But we know that note pattern is repeating every 12 steps so that can be easily solved as well:

string_tune <- "D"
fret_num <- 15

if (fret_num >= 12) {
  fret_num = fret_num - (12 * floor(fret_num / 12))
}
note_idx <- (which(notes == string_tune) + fret_num)[1]
if (note_idx > 12) {
  note_idx <- note_idx - 12
}
notes[note_idx]
## [1] "F"

Finally, calculations can be wrapped into single function that takes fret number (fret_num) and string tune (string_tune) and returns the corresponding note:

calc_note <- function(fret_num, string_tune) {
  notes <-
    rep(c("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"),
        2)
  if (fret_num >= 12) {
    fret_num = fret_num - (12 * floor(fret_num / 12))
  }
  note_idx <- note_idx <- (which(notes == string_tune) + fret_num)[1]
  if (note_idx > 12) {
    note_idx <- note_idx - 12
  }
  notes[note_idx]
}

calc_note(15, "D")
## [1] "F"

Vectorize the function to enable vector input and see the notes of “A” string at frets 0 to 5:

calc_note_v <- Vectorize(calc_note)

calc_note_v(0:5, "A")
## [1] "A"  "A#" "B"  "C"  "C#" "D"

Now, lets create new data frame, containing the following variables:

  • fret number
  • string idx
  • string tune
  • fret note
  • fret position

This structure is an example of tidy data, in which variables are in columns and observations in rows (more info here).

fretboard_notes <- data.frame(
    fret_num = rep(0:guitar_setup$frets, each = length(guitar_setup$tuning)),
    string_idx = rep(1:length(guitar_setup$tuning), guitar_setup$frets + 1)
  ) %>% mutate(
    string_tune = guitar_setup$tuning[string_idx],
    fret_note = calc_note_v(fret_num, string_tune),
    interval_idx = names(calc_note_v(fret_num, string_tune))
  ) %>%
    left_join(fretboard %>% select(fret_num, fret_pos), by = "fret_num") %>%
    distinct()

head(fretboard_notes, 10)
##    fret_num string_idx string_tune fret_note   fret_pos
## 1         0          1           E         E 0.00000000
## 2         0          2           A         A 0.00000000
## 3         0          3           D         D 0.00000000
## 4         0          4           G         G 0.00000000
## 5         0          5           B         B 0.00000000
## 6         0          6           E         E 0.00000000
## 7         1          1           E         F 0.05555556
## 8         1          2           A        A# 0.05555556
## 9         1          3           D        D# 0.05555556
## 10        1          4           G        G# 0.05555556

As before, we can wrap it into single function calc_notes:

# calculate notes on the fretboard
calc_notes <- function(fretboard, guitar_setup) {
  calc_note <- function(fret_num, string_tune) {
    notes <-
      rep(c("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"),
          2)
    if (fret_num >= 12) {
      fret_num = fret_num - (12 * floor(fret_num / 12))
    }
    note_idx <-
      note_idx <- (which(notes == string_tune) + fret_num)[1]
    if (note_idx > 12) {
      note_idx <- note_idx - 12
    }
    notes[note_idx]
  }
  
  calc_note_v <- Vectorize(calc_note)
  
  data.frame(
    fret_num = rep(0:guitar_setup$frets, each = length(guitar_setup$tuning)),
    string_idx = rep(1:length(guitar_setup$tuning), guitar_setup$frets + 1)
  ) %>% mutate(
    string_tune = guitar_setup$tuning[string_idx],
    fret_note = calc_note_v(fret_num, string_tune),
    interval_idx = names(calc_note_v(fret_num, string_tune))
  ) %>%
    left_join(fretboard %>% select(fret_num, fret_pos), by = "fret_num") %>%
    distinct()
  
}

fretboard_notes <- calc_notes(fretboard, guitar_setup)

How easy it is now to plot notes on the fretboard:

fr +
  geom_point(
    data = fretboard_notes,
    mapping = aes(x = fret_pos, y = string_idx),
    shape = 16,
    size = 5,
    color = "white"
  ) +
  geom_text(
    data = fretboard_notes,
    mapping = aes(x = fret_pos, y = string_idx, label = fret_note),
    size = 3
  )

Scale

Harmonic scale

To generate the desired scale, we will start from constructing chromatic scale of a given key. Here, harmonic scale is composed of consecutive 12 notes, starting from desired root note:

# set root
root <- "A"

# data frame of all notes (two octaves)
all_notes = data.frame(idx = 1:24,
                       note = rep(c(
                         "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
                       ), 2))

# find indexes of 12 consecutive notes starting from the root note
idx <- which(all_notes$note==root)[1] + 0:11

# display the scele
all_notes[idx,"note"]
##  [1] "A"  "A#" "B"  "C"  "C#" "D"  "D#" "E"  "F"  "F#" "G"  "G#"

Intervals

In music theory, an interval is a difference in pitch between two sounds (more info here.

Lets define their index (number: 1st interval, 2nd interval, etc), abbreviated name (“P1” for Perfet unison, m2 for minor second, etc.), and type (major, minor, minor harmonic). For interval type, use logical (TRUE/FALSE) assignment (interval_isMajor, interval_isMinor, interval_isMinorHarmonic) - that will be helpful in constructing the scale later on:

all_notes = data.frame(
  idx = 1:24,
  note = rep(
    c("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"), 2)
  )

# generate chromatic scale
root <-  "A"

idx <- which(all_notes$note == root)[1] + 0:11

scale_chromatic <- all_notes[idx, ] %>%
  mutate(
    interval_num = c(1, 2, 2, 3, 3, 4, 4, 5, 6, 6, 7, 7),
    interval_name = c(
      "P1",
      "m2",
      "M2",
      "m3",
      "M3",
      "P4",
      "A4",
      "P5",
      "m6",
      "M6",
      "m7",
      "M7"
    ),
    interval_isMajor = c(T, F, T, F, T, T, F, T, F, T, F, T),
    interval_isMinor = c(T, F, T, T, F, T, F, T, T, F, T, F),
    interval_isMinorHarmonic = c(T, F, T, T, F, T, F, T, T, F, F, T)
  )

glimpse(scale_chromatic)
## Rows: 12
## Columns: 7
## $ idx                      <int> 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21
## $ note                     <chr> "A", "A#", "B", "C", "C#", "D", "D#", "E", "F~
## $ interval_num             <dbl> 1, 2, 2, 3, 3, 4, 4, 5, 6, 6, 7, 7
## $ interval_name            <chr> "P1", "m2", "M2", "m3", "M3", "P4", "A4", "P5~
## $ interval_isMajor         <lgl> TRUE, FALSE, TRUE, FALSE, TRUE, TRUE, FALSE, ~
## $ interval_isMinor         <lgl> TRUE, FALSE, TRUE, TRUE, FALSE, TRUE, FALSE, ~
## $ interval_isMinorHarmonic <lgl> TRUE, FALSE, TRUE, TRUE, FALSE, TRUE, FALSE, ~

To construct diatonic scales (major, minor natural, minor harmonic) simply select respective intervals using case_when(). If the scale is pentatonic, use only 1st, 3rd, 4th, 5th and 7th intervals. Detect type using either str_detect() or logical operator ==:

# set scale type
type <- "minor_pentatonic"

# generate A minor pentatonic scale
scale_chromatic %>% filter(
    case_when(
      str_detect(type, "major") ~ interval_isMajor,
      type == "minor_harmonic" ~ interval_isMinorHarmonic,
      str_detect(type, "minor") ~ interval_isMinor
    )
  ) %>%
    arrange(interval_num) %>%
    filter(
      case_when(str_detect(type, "pentatonic") ~ interval_num %in% c(1, 3, 4, 5, 7),
                TRUE ~ TRUE)
    ) %>%
    select(note, interval_num, interval_name) %>%
  glimpse()
## Rows: 5
## Columns: 3
## $ note          <chr> "A", "C", "D", "E", "G"
## $ interval_num  <dbl> 1, 3, 4, 5, 7
## $ interval_name <chr> "P1", "m3", "P4", "P5", "m7"

As before, wrap the scale calculation into once function calc_scale(), that will take the root note and scale type as inputs:

calc_scale <- function(root, type) {
  all_notes = data.frame(
    idx = 1:24,
    note = rep(c("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"),2)
    )

  idx <- which(all_notes$note==root)[1] + 0:11

  scale_chromatic <- all_notes[idx,] %>%
    mutate(
      interval_num = c(1, 2, 2, 3, 3, 4, 4, 5, 6, 6, 7, 7),
      interval_name = c("P1", "m2", "M2", "m3", "M3", "P4", "A4", "P5", "m6", "M6", "m7", "M7"),
      interval_isMajor = c(T, F, T, F, T, T, F, T, F, T, F, T),
      interval_isMinor = c(T, F, T, T, F, T, F, T, T, F, T, F),
      interval_isMinorHarmonic =c(T, F, T, T, F, T, F, T, T, F, F, T)
    )

  scale_chromatic %>% filter(
    case_when(
      str_detect(type, "major") ~ interval_isMajor,
      type == "minor_harmonic" ~ interval_isMinorHarmonic,
      str_detect(type, "minor") ~ interval_isMinor
    )
  ) %>%
    arrange(interval_num) %>%
    filter(
      case_when(str_detect(type, "pentatonic") ~ interval_num %in% c(1, 3, 4, 5, 7),
                TRUE ~ TRUE)
    ) %>%
    select(note, interval_num, interval_name)
}

calc_scale("C", "major") %>% glimpse()
## Rows: 7
## Columns: 3
## $ note          <chr> "C", "D", "E", "F", "G", "A", "B"
## $ interval_num  <dbl> 1, 2, 3, 4, 5, 6, 7
## $ interval_name <chr> "P1", "M2", "M3", "P4", "P5", "M6", "M7"

Notes to show on the freatboard

In the previous section we plotted all notes on the fretboard. But What if we want to show only notes of desired scale? Or maybe only selected intervals or scale root?

Lets define set of additional logical variables (show_xxx) and construct the data frame indicating which notes (and their labels) to display while plotting:

root = "A"
type = "minor_pentatonic"
show_all_notes <- FALSE
show_scale_notes <- TRUE
show_root <- TRUE
show_3 <- TRUE
show_5 <- TRUE
show_scale <- TRUE

fretboard_notes_show <- fretboard_notes %>%
  left_join(calc_scale(root, type), by = c("fret_note" = "note")) %>%
  mutate(
    show_label = case_when(
      show_scale_notes & !show_all_notes ~ !is.na(interval_num),
      show_all_notes ~ TRUE,
      TRUE ~ FALSE
    ),
    show_scale = case_when(
      show_scale ~ !is.na(interval_num),
      TRUE ~ FALSE
    ),
    show_root = case_when(
      show_root ~  interval_num == 1,
      TRUE ~ FALSE
    ),
    show_3 = case_when(
      show_3 ~ interval_num == 3,
      TRUE ~ FALSE
    ),
    show_5 = case_when(
      show_5 ~ interval_num == 5,
      TRUE ~ FALSE
    )
  )

glimpse(fretboard_notes_show)
## Rows: 108
## Columns: 12
## $ fret_num      <int> 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3,~
## $ string_idx    <int> 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1,~
## $ string_tune   <chr> "E", "A", "D", "G", "B", "E", "E", "A", "D", "G", "B", "~
## $ fret_note     <chr> "E", "A", "D", "G", "B", "E", "F", "A#", "D#", "G#", "C"~
## $ fret_pos      <dbl> 0.00000000, 0.00000000, 0.00000000, 0.00000000, 0.000000~
## $ interval_num  <dbl> 5, 1, 4, 7, NA, 5, NA, NA, NA, NA, 3, NA, NA, NA, 5, 1, ~
## $ interval_name <chr> "P5", "P1", "P4", "m7", NA, "P5", NA, NA, NA, NA, "m3", ~
## $ show_label    <lgl> TRUE, TRUE, TRUE, TRUE, FALSE, TRUE, FALSE, FALSE, FALSE~
## $ show_scale    <lgl> TRUE, TRUE, TRUE, TRUE, FALSE, TRUE, FALSE, FALSE, FALSE~
## $ show_root     <lgl> FALSE, TRUE, FALSE, FALSE, NA, FALSE, NA, NA, NA, NA, FA~
## $ show_3        <lgl> FALSE, FALSE, FALSE, FALSE, NA, FALSE, NA, NA, NA, NA, T~
## $ show_5        <lgl> TRUE, FALSE, FALSE, FALSE, NA, TRUE, NA, NA, NA, NA, FAL~

Plot fretboard with scale notes

Everything is ready to finally plot the fretboard with the scale notes (or all notes!). It is also easy to color code specific intervals or root notes. It can be done multiple ways. Colors can be either encoded in fretboard_notes_show data frame or in ggplot call. We will do with the former.

NOTE: Thinking of Shiny UI, it is worth to extract all the adjustable parameters. It will be easier later to provide them as input$....

# setup point and label size
note_label_size <- 4
note_point_size <- 8

# plot fretboard with notes
fr + geom_point(data = fretboard_notes_show %>% filter(show_label & !show_scale),
           mapping = aes(x = fret_pos, y = string_idx),
           shape = 16, size = note_point_size,
           color = "white") +
  # show scale
  geom_point(data = fretboard_notes_show %>% filter(show_scale & !show_root),
             mapping = aes(x = fret_pos, y = string_idx),
             shape = 21, size = note_point_size,
             fill = "white", color = "black") +
  # show root
  geom_point(data = fretboard_notes_show %>% filter(show_scale & show_root),
             mapping = aes(x = fret_pos, y = string_idx),
             shape = 21, size = note_point_size,
             fill = "#FF0000", color = "black") +
  # show 3
  geom_point(data = fretboard_notes_show %>% filter(show_scale & show_3),
             mapping = aes(x = fret_pos, y = string_idx),
             shape = 21, size = note_point_size,
             fill = "#FF7070", color = "black") +
  # show 5
  geom_point(data = fretboard_notes_show %>% filter(show_scale & show_5),
             mapping = aes(x = fret_pos, y = string_idx),
             shape = 21, size = note_point_size,
             fill = "#FCEDED", color = "black") +
  # show note labels
  geom_text(data = fretboard_notes_show %>% filter(show_label),
            mapping = aes(x = fret_pos, y = string_idx, label = fret_note), size = note_label_size) +
  labs(title = paste(root, str_replace(type, "_", " ")))