Skip to content

Commit

Permalink
Improve functionality, update tests and documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
leonawicz committed Jun 29, 2024
1 parent 0d38ad1 commit 5ed2d77
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 114 deletions.
200 changes: 133 additions & 67 deletions R/plot.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,46 @@
#'
#' Create a fretboard diagram for a single chord or a general progression.
#'
#' These functions are under development and subject to change. They each
#' return a ggplot object.
#' These functions are under development and subject to change. They each return
#' a ggplot object.
#'
#' Use `plot_chord()` to create a fretboard diagram of a specific chord.
#' `plot_chord()` accepts a character string in simple fretboard format,
#' e.g., `chord = "xo221o"`.
#' Zero is allowed in place of `"o"`. This only works when no spaces or
#' semicolons are detected. The function checks for spaces first, then
#' semicolons, to split fret numbers.
#' Do not mix formats. For example, you can use `chord = "xo221o"`,
#' `chord = "x 8 10 10 9 8"` or `chord = "x;8;10;10;9;8"`.
#' Trailing delimiters are ignored (LilyPond format: `"x;8;10;10;9;8;"`).
#' If there are fewer fret values than there are strings on the instrument, as
#' inferred from `tuning`, then muted strings, `x`, are inferred for
#' the remaining lower-pitch strings.
#' `plot_chord()` accepts a character string in simple fretboard format, e.g.,
#' `chord = "xo221o"`. Zero is allowed in place of `"o"`. This only works when
#' no spaces or semicolons are detected. The function checks for spaces first,
#' then semicolons, to split fret numbers. Do not mix formats. For example, you
#' can use `chord = "xo221o"`, `chord = "x 8 10 10 9 8"` or
#' `chord = "x;8;10;10;9;8"`. Trailing delimiters are ignored (LilyPond format:
#' `"x;8;10;10;9;8;"`). If there are fewer fret values than there are strings on
#' the instrument, as inferred from `tuning`, then muted strings, `x`, are
#' inferred for the remaining lower-pitch strings.
#'
#' `plot_fretboard()` produces a more general fretboard diagram plot. It is
#' intended for scales, arpeggios and other patterns along the fretboard. For
#' this function, provide vectors of string and fret numbers. `mute` is
#' available but not as applicable for this function. For single chord diagrams,
#' use `plot_chord()`.
#' available but not as applicable for this function; it is a pass-through from
#' `plot_chord()`. For single chord diagrams, use `plot_chord()`. The letter
#' `"o"` is also allowed in `fret` for open strings and will display below the
#' lowest fret plotted. The number `0` is treated with the intent of displaying
#' the corresponding position on the instrument neck.
#'
#' Number of strings is derived from `tuning`. See [tunings()]
#' for pre-defined tunings and examples of explicit tunings.
#' `tuning` affects point labels when `labels = "notes"`.
#' Number of strings is derived from `tuning`. See [tunings()] for pre-defined
#' tunings and examples of explicit tunings. `tuning` affects point labels when
#' `labels = "notes"`.
#'
#' Providing `fret_labels` overrides the default (minimal) fret numbering
#' behavior for the fret axis. These are only intended to be integers. The
#' vector of integers given is sorted and subset if needed to the range of frets
#' that appear in the plot. See example.
#'
#' @param string integer or as a space-delimited character string; instrument
#' string numbers.
#' @param fret integer or as a space-delimited character string; fret numbers.
#' @param chord character, a single chord given in fret notation. See details.
#' @param labels character, optional text labels, must be one for every point.
#' @param labels `NULL` or character, optional vector of text labels, must be
#' one for every point; or just the special value `"notes"`.
#' @param mute logical vector or specific integer indices, which notes to mute.
#' See details.
#' @param label_size numeric, size of fretted note labels.
#' @param label_color character, label color.
#' @param point_size numeric, size of fretted note points.
Expand All @@ -44,11 +52,16 @@
#' @param left_handed logical, handedness orientation.
#' @param fret_range fret limits, if not `NULL`, overrides limits derived
#' from `fret`.
#' @param fret_labels integer, vector of fret number labels for fret axis. See
#' details.
#' @param fret_offset logical set to `TRUE` to shift the fret axis number labels
#' (if present) from being directly next to the fret to being aligned with the
#' circles behind the fret.
#' @param accidentals character, when `labels = "notes"` represent accidentals:
#' `"flat"` or `"sharp"`.
#' @param tuning explicit tuning, e.g., `"e, a, d g b e'"`, or a pre-defined
#' tuning. See details.
#' @param show_tuning logical, show tuning of each string.
#' @param show_tuning logical, show tuning of each string on string axis.
#' @param asp numeric, aspect ratio, overrides default aspect ratio derived
#' from number of strings and frets.
#' @param base_size base size for `ggplot2::theme_void()`.
Expand All @@ -60,7 +73,12 @@
#' # General patterns: scale shifting exercise
#' string <- c(6, 6, 6, 5, 5, 5, 4, 4, 4, 4, 4, 3, 3, 3, 2, 2, 2, 1, 1, 1)
#' fret <- "2 4 5 2 4 5 2 4 6 7 9 6 7 9 7 9 10 7 9 10" # string input accepted
#' plot_fretboard(string, fret, labels = "notes")
#' plot_fretboard(string, fret, labels = "notes", fret_offset = TRUE)
#' plot_fretboard(string, fret, fret_labels = c(3, 5, 7, 9, 12), show_tuning = TRUE)
#'
#' # open and muted strings on shifted general fretboard layout
#' # try to use plot_chord() if more suitable
#' plot_fretboard("6 5 4 3", "o 9 10 12", mute = 2, show_tuning = TRUE)
#'
#' # Single chord diagrams
#' # open chord
Expand All @@ -72,46 +90,87 @@
#' # moveable chord
#' plot_chord("355433", horizontal = TRUE, show_tuning = TRUE)
#'
#' plot_chord("0231") # leading x inferred; same as plot_chord("xxo321")
#'
#' plot_chord("10 12 13 11", fret_range = c(10, 14))
#' # leading x inferred; same as plot_chord("xxo321")
#' plot_chord("o231", fret_labels = 3)
#' plot_chord("10 12 13 11", show_tuning = TRUE)
#' plot_chord("o x 10 12 13 11", fret_range = c(9, 14), fret_labels = c(9, 12))
plot_fretboard <- function(string, fret, labels = NULL, mute = FALSE,
label_size = 10, label_color = "white",
point_size = 10, point_color = "black",
point_fill = "black", group = NULL,
horizontal = FALSE, left_handed = FALSE,
fret_range = NULL, accidentals = c("flat", "sharp"),
fret_range = NULL, fret_labels = NULL,
fret_offset = FALSE, accidentals = c("flat", "sharp"),
tuning = "standard", show_tuning = FALSE,
asp = NULL, base_size = 20){
accidentals <- match.arg(accidentals)
label_size <- label_size / 2.5
key <- if(accidentals == "flat") "f" else "g"
string <- as.integer(.uncollapse(string))
fret <- as.integer(.uncollapse(fret))
if(length(string) != length(fret))
stop("`string` and `fret` must have equal number of entries.",
call. = FALSE)
o <- .split_chords(.map_tuning(tuning))

if(is.logical(mute) & length(mute) == 1) mute <- rep(mute, length(string))
if(is.numeric(mute)){
mute0 <- mute
mute <- rep(FALSE, length(string))
mute[mute0] <- TRUE
}

fret <- .uncollapse(fret)
open_string <- fret == "o"
nut <- 0

if(!is.null(fret_range)){
idx <- fret == 0 & fret < fret_range[1] & mute
if(any(idx)) fret[idx] <- fret_range[1] - 1
if(!fret_range[1] == 0) fret_range <- fret_range - c(1, 0)
if(length(fret_range) != 2)
stop("Explicit fret range must be a vector of two numbers.", call. = FALSE)
}

if(any(open_string)){
fret[open_string] <- min(as.integer(fret[!open_string & !mute])) - 1
fret <- as.integer(fret)
if(!is.null(fret_range)){
if(fret_range[1] < fret[open_string][1])
stop("Explicit fret range must be greater than implicit fret number of explicit open strings.", call. = FALSE)
}
nut <- fret[open_string][1]
} else {
fret_range <- range(fret, na.rm = TRUE)
if(!any(fret == 0, na.rm = TRUE) & !fret_range[1] == 0)
fret_range <- fret_range - c(1, 0)
fret <- as.integer(fret)
}

if(any(mute)){
if(nut > 0) fret[mute] <- nut else nut <- fret[mute]
}

if(length(string) != length(fret))
stop("`string` and `fret` must have equal number of entries.", call. = FALSE)

if(any(string < 1)) stop("String numbers must be >= 1.", call. = FALSE)

if(!is.null(fret_range)){
if(any(fret < fret_range[1]) | any(fret > fret_range[2]))
stop("Explicit fret range will cut off notes.", call. = FALSE)
}
if(is.null(fret_range)) fret_range <- range(fret, na.rm = TRUE)
fr2 <- fret_range - c(1, 0)
if(any(fret_range < 0) | any(fret < 0, na.rm = TRUE))
stop("Fret numbers must be >= 0.", call. = FALSE)
if(any(string < 1)) stop("String numbers must be >= 1.", call. = FALSE)

fret_pos <- NULL
if(is.null(fret_labels)){
if(fret_range[1] > 1) fret_pos <- fret_labels <- fret_range[1] + any(open_string | mute)
} else {
if(!is.numeric(fret_labels))
stop("Fret labels must be integers.", call. = FALSE)
fret_labels <- as.integer(fret_labels)
fret_labels <- fret_labels[fret_labels %in% seq(max(fret_range[1] - 1, 1), fret_range[2], by = 1)]
fret_pos <- fret_labels
}
if(is.numeric(fret_labels) && fret_offset){
fret_pos <- fret_labels - 0.5
}

o <- .split_chords(.map_tuning(tuning))
n <- length(o)

d <- data.frame(x = string, y = fret - 0.5, label_color = label_color,
point_color = point_color, point_fill = point_fill)
if(!is.null(group)) d$group <- group
Expand All @@ -128,9 +187,9 @@ plot_fretboard <- function(string, fret, labels = NULL, mute = FALSE,
d$lab <- labels
}
}
d2 <- data.frame(string_1 = 1, string_n = n,
fret = fret_range[1]:fret_range[2])
d3 <- data.frame(string = 1:n, fret_1 = fret_range[1], fret_n = fret_range[2])

d2 <- data.frame(string_1 = 1, string_n = n, fret = max(fr2[1], nut):fret_range[2])
d3 <- data.frame(string = 1:n, fret_1 = max(fr2[1], nut), fret_n = fret_range[2])
g <- ggplot2::ggplot(d, ggplot2::aes(.data[["x"]], .data[["y"]])) +
ggplot2::geom_segment(
data = d2,
Expand All @@ -142,41 +201,40 @@ plot_fretboard <- function(string, fret, labels = NULL, mute = FALSE,
ggplot2::aes(x = .data[["string"]], xend = .data[["string"]],
y = .data[["fret_1"]], yend = .data[["fret_n"]]),
linewidth = 1)

y <- x <- ggplot2::element_text(size = 18, face = 2)
if(fret_range[1] < 2){
g <- g + ggplot2::geom_segment(
data = data.frame(string_1 = 1, string_n = n, fret = nut),
ggplot2::aes(
x = .data[["string_1"]], xend = .data[["string_n"]],
y = .data[["fret"]], yend = .data[["fret"]]
), linewidth = 4
)
}

if(0 %in% fret_range){
fr2 <- if(any(fret == 0)) fret_range - c(1, 0) else fret_range
if(is.null(fret_labels)){
if(horizontal){
x <- ggplot2::element_blank()
} else {
y <- ggplot2::element_blank()
}
d4 <- data.frame(string_1 = 1, string_n = n, fret = 0)
g <- g + ggplot2::geom_segment(
data = d4,
ggplot2::aes(
x = .data[["string_1"]], xend = .data[["string_n"]],
y = .data[["fret"]], yend = .data[["fret"]]), linewidth = 4) +
if(horizontal & !left_handed){
ggplot2::scale_y_continuous(limits = fr2)
} else {
ggplot2::scale_y_reverse(limits = rev(fr2))
}
} else {
if(any(mute | fret == 0)){
fr2 <- fret_range - c(1, 0)
bump <- 1
}

if(fret_range[1] == 0){
lims <- if(all(fret > 0)) fret_range else fr2
if(horizontal & !left_handed){
g <- g + ggplot2::scale_y_continuous(limits = lims, labels = fret_labels, breaks = fret_pos)
} else {
fr2 <- fret_range
bump <- 0
g <- g + ggplot2::scale_y_reverse(limits = rev(lims), labels = fret_labels, breaks = fret_pos)
}
} else {
if(horizontal & !left_handed){
g <- g + ggplot2::scale_y_continuous(
limits = fr2, labels = fr2[1] + bump + 1, breaks = fr2[1] + bump + 0.5)
limits = fr2, labels = fret_labels, breaks = fret_pos)
} else {
g <- g + ggplot2::scale_y_reverse(
limits = rev(fr2), labels = fr2[1] + bump + 1,
breaks = fr2[1] + bump + 0.5)
limits = rev(fr2), labels = fret_labels, breaks = fret_pos)
}
}

Expand Down Expand Up @@ -230,9 +288,10 @@ plot_fretboard <- function(string, fret, labels = NULL, mute = FALSE,
#' @rdname plot_fretboard
plot_chord <- function(chord, labels = NULL, label_size = 10,
label_color = "white", point_size = 10,
point_color = "black", point_fill = "black",
group = NULL, horizontal = FALSE, left_handed = FALSE,
fret_range = NULL, accidentals = c("flat", "sharp"),
point_color = "black", point_fill = "black", group = NULL,
horizontal = FALSE, left_handed = FALSE,
fret_range = NULL, fret_labels = NULL,
fret_offset = FALSE, accidentals = c("flat", "sharp"),
tuning = "standard", show_tuning = FALSE, asp = NULL,
base_size = 20){
accidentals <- match.arg(accidentals)
Expand All @@ -251,13 +310,20 @@ plot_chord <- function(chord, labels = NULL, label_size = 10,
stop("Cannot have more fret values than number of instrument strings.",
call. = FALSE)
if(n < n_strings) chord <- c(rep("x", n_strings - n), chord)
explicit_open_string <- chord == "o"
chord <- gsub("o", "0", chord)
mute <- chord == "x"

if(any(mute)){
chord[mute] <- 0
fret <- as.integer(chord)
zero_fret <- max(min(fret[!mute]) - 1, 0)
chord[mute] <- zero_fret
}
if(any(explicit_open_string)) chord[explicit_open_string] <- "o"

plot_fretboard(n_strings:1, chord, labels, mute, label_size, label_color,
point_size, point_color, point_fill, group, horizontal,
left_handed, fret_range, accidentals, tuning, show_tuning,
asp, base_size)
left_handed, fret_range, fret_labels, fret_offset, accidentals,
tuning, show_tuning, asp, base_size)
}
Loading

0 comments on commit 5ed2d77

Please sign in to comment.