1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835
|
#' @title Plot likert scales as centered stacked bars
#' @name plot_likert
#'
#' @description Plot likert scales as centered stacked bars.
#'
#' @note Note that only even numbers of categories are possible to plot, so the "positive"
#' and "negative" values can be splitted into two halfs. A neutral category (like "don't know")
#' can be used, but must be indicated by \code{cat.neutral}. \cr \cr
#' The \code{catcount}-argument indicates how many item categories are in the
#' Likert scale. Normally, this argument can be ignored because the amount of
#' valid categories is retrieved automatically. However, sometimes (for instance,
#' if a certain category is missing in all items), auto-detection of the amount
#' of categories fails. In such cases, specify the amount of categories
#' with the \code{catcount}-argument.
#'
#' @param catcount optional, amount of categories of \code{items} (e.g. \emph{"strongly disagree",
#' "disagree", "agree"} and \emph{"strongly agree"} would be \code{catcount = 4}).
#' Note that this argument only applies to "valid" answers, i.e. if you
#' have an additional neutral category (see \code{cat.neutral}) like \emph{"don't know"},
#' this won't count for \code{catcount} (e.g. "strongly disagree",
#' "disagree", "agree", "strongly agree" and neutral category "don't know"
#' would still mean that \code{catcount = 4}). See 'Note'.
#' @param cat.neutral If there's a neutral category (like "don't know" etc.), specify
#' the index number (value) for this category. Else, set \code{cat.neutral = NULL} (default).
#' The proportions of neutral category answers are plotted as grey bars on the left side of
#' the figure.
#' @param sort.frq Indicates whether the items of \code{items} should be ordered by
#' total sum of positive or negative answers.
#' \describe{
#' \item{\code{"pos.asc"}}{to order ascending by sum of positive answers}
#' \item{\code{"pos.desc"}}{to order descending by sum of positive answers}
#' \item{\code{"neg.asc"}}{for sorting ascending negative answers}
#' \item{\code{"neg.desc"}}{for sorting descending negative answers}
#' \item{\code{NULL}}{(default) for no sorting}
#' }
#' @param reverse.colors logical, if \code{TRUE}, the color scale from \code{geom.colors} will be reversed,
#' so positive and negative values switch colors.
#' @param cat.neutral.color Color of the neutral category, if plotted (see \code{cat.neutral}).
#' @param intercept.line.color Color of the vertical intercept line that divides positive and negative values.
#' @param values Determines style and position of percentage value labels on the bars:
#' \describe{
#' \item{\code{"show"}}{(default) shows percentage value labels in the middle of each category bar}
#' \item{\code{"hide"}}{hides the value labels, so no percentage values on the bars are printed}
#' \item{\code{"sum.inside"}}{shows the sums of percentage values for both negative and positive values and prints them inside the end of each bar}
#' \item{\code{"sum.outside"}}{shows the sums of percentage values for both negative and positive values and prints them outside the end of each bar}
#' }
#' @param show.prc.sign logical, if \code{TRUE}, \%-signs for value labels are shown.
#' @param grid.range Numeric, limits of the x-axis-range, as proportion of 100.
#' Default is 1, so the x-scale ranges from zero to 100\% on both sides from the center.
#' Can alternatively be supplied as a vector of 2 positive numbers (e.g. \code{grid.range = c(1, .8)})
#' to set the left and right limit separately. You can use values beyond 1 (100\%) in case bar labels are not printed because
#' they exceed the axis range. E.g. \code{grid.range = 1.4} will set the axis from -140 to +140\%, however, only
#' (valid) axis labels from -100 to +100\% are printed. Neutral categories are adjusted to the most left limit.
#' @param reverse.scale logical, if \code{TRUE}, the ordering of the categories is reversed, so positive and negative values switch position.
#' @param groups (optional) Must be a vector of same length as \code{ncol(items)},
#' where each item in this vector represents the group number
#' of the related columns of \code{items}. See 'Examples'.
#' @param groups.titles (optional, only used if groups are supplied) Titles for each factor group that will be used as table caption for each
#' component-table. Must be a character vector of same length as \code{length(unique(groups))}.
#' Default is \code{"auto"}, which means that each table has a standard caption \emph{Component x}.
#' Use \code{NULL} to use names as supplied to \code{groups} and use \code{FALSE} to suppress table captions.
#' @param sort.groups (optional, only used if groups are supplied) logical, if groups should be sorted according to the values supplied to \code{groups}. Defaults to \code{TRUE}.
#' @param legend.pos (optional, only used if groups are supplied) Defines the legend position. Possible values are \code{c("bottom", "top", "both", "all", "none")}.
#' If the is only one group or this option is set to \code{"all"} legends will be printed as defined with \code{\link{set_theme}}.
#' @param rel_heights (optional, only used if groups are supplied) This option can be used to adjust the height of the subplots. The bars in subplots can have different heights due to a differing number of items
#' or due to legend placement. This can be adjusted here. Takes a vector of numbers, one
#' for each plot. Values are evaluated relative to each other.
#' @param group.legend.options (optional, only used if groups are supplied) List of options to be passed to \code{\link[ggplot2]{guide_legend}}.
#' The most notable options are \code{byrow=T} (default), this will order the categories row wise.
#' And with \code{group.legend.options = list(nrow = 1)} all categories can be forced to be on a single row.
#' @param cowplot.options (optional, only used if groups are supplied) List of label options to be passed to \code{\link[cowplot]{plot_grid}}.
#'
#' @inheritParams plot_grpfrq
#' @inheritParams plot_stackfrq
#' @inheritParams plot_model
#'
#' @return A ggplot-object.
#'
#' @examples
#' if (requireNamespace("ggrepel") && requireNamespace("sjmisc")) {
#' library(sjmisc)
#' data(efc)
#' # find all variables from COPE-Index, which all have a "cop" in their
#' # variable name, and then plot that subset as likert-plot
#' mydf <- find_var(efc, pattern = "cop", out = "df")
#'
#' plot_likert(mydf)
#'
#' plot_likert(
#' mydf,
#' grid.range = c(1.2, 1.4),
#' expand.grid = FALSE,
#' values = "sum.outside",
#' show.prc.sign = TRUE
#' )
#'
#' # Plot in groups
#'
#' plot_likert(mydf, c(2,1,1,1,1,2,2,2,1))
#'
#' if (require("parameters") && require("nFactors")) {
#' groups <- parameters::principal_components(mydf)
#' plot_likert(mydf, groups = parameters::closest_component(groups))
#' }
#'
#' plot_likert(mydf,
#' c(rep("B", 4), rep("A", 5)),
#' sort.groups = FALSE,
#' grid.range = c(0.9, 1.1),
#' geom.colors = "RdBu",
#' rel_heights = c(6, 8),
#' wrap.labels = 40,
#' reverse.scale = TRUE)
#'
#' # control legend items
#' six_cat_example = data.frame(
#' matrix(sample(1:6, 600, replace = TRUE),
#' ncol = 6)
#' )
#'
#' \dontrun{
#' six_cat_example <-
#' six_cat_example %>%
#' dplyr::mutate_all(~ordered(.,labels = c("+++","++","+","-","--","---")))
#'
#' # Old default
#' plot_likert(
#' six_cat_example,
#' groups = c(1, 1, 1, 2, 2, 2),
#' group.legend.options = list(nrow = 2, byrow = FALSE)
#' )
#'
#' # New default
#' plot_likert(six_cat_example, groups = c(1, 1, 1, 2, 2, 2))
#'
#' # Single row
#' plot_likert(
#' six_cat_example,
#' groups = c(1, 1, 1, 2, 2, 2),
#' group.legend.options = list(nrow = 1)
#' )}
#' }
#' @import ggplot2
#' @export
plot_likert <- function(items,
groups = NULL,
groups.titles = "auto",
title = NULL,
legend.title = NULL, # Options to be passed directly to .plot_likert()
legend.labels = NULL,
axis.titles = NULL,
axis.labels = NULL,
catcount = NULL,
cat.neutral = NULL,
sort.frq = NULL,
weight.by = NULL,
title.wtd.suffix = NULL,
wrap.title = 50,
wrap.labels = 30,
wrap.legend.title = 30,
wrap.legend.labels = 28,
geom.size = .6,
geom.colors = "BrBG",
cat.neutral.color = "grey70",
intercept.line.color = "grey50",
reverse.colors = FALSE,
values = "show",
show.n = TRUE,
show.legend = TRUE,
show.prc.sign = FALSE,
grid.range = 1,
grid.breaks = 0.2,
expand.grid = TRUE,
digits = 1,
reverse.scale = FALSE,
coord.flip = TRUE,
sort.groups = TRUE, # Group Options
legend.pos = "bottom",
rel_heights = 1,
group.legend.options = list(nrow = NULL, byrow = TRUE), # Add rowwise order of levels and option to force a single rowed legend for 6 or more categories
cowplot.options = list(label_x = 0.01, hjust = 0, align = "v") # Fix for label position depending on label length bug in cowplot
) {
# Select options to be passed to .plot_likert()
.likert_options <- as.list(environment())[5:32]
## If now no groups are supplied only 1 group will be assumed. Check for cowplot is only performed if there are groups supplied.
if (is.null(groups)) {
groups <- rep(1, length.out = ncol(items))
} else {
if (!requireNamespace("cowplot", quietly = T))
stop("Package 'cowplot' required for this function wor work. Please install it.", call. = F)
}
if (ncol(items) != length(groups))
stop("Length of groups has to equal the number of items: ncol(items) != length(groups).", call. = F)
# retrieve unique factor / group index values
findex <- unique(groups)
if (sort.groups) findex <- sort(findex)
# Add empty title to plots, to create space for the group.labels
if (is.null(title) && length(findex) != 1) title <- rep("", length(findex))
.plot_list <- list()
# iterate all sub-plots (groups)
for (i in seq_along(findex)) {
index <- which(groups == findex[i])
.pl <- do.call(".plot_likert", args = c(list(items[, index], title = title[i]), .likert_options))
# If there are 2 or more groups, the legend will be plotted according to legend.pos.
if (length(findex) != 1) {
if (legend.pos %in% c("top", "both") && i == 1)
.pl <- .pl + theme(legend.position = "top") + guides(fill = do.call(guide_legend, group.legend.options))
else if (legend.pos %in% c("bottom", "both") && i == length(findex))
.pl <- .pl + theme(legend.position = "bottom") + guides(fill = do.call(guide_legend, group.legend.options))
else if (legend.pos != "all")
.pl <- .pl + theme(legend.position = "none")
}
.plot_list[i] <- list(.pl)
}
# Options to turn off or overwrite cowplot group.labels.
if (.is_false(groups.titles)) {
groups.titles <- rep("", length(findex))
} else if (!is.null(groups.titles) && (groups.titles[1] == "auto" || length(groups.titles) != length(findex)) && (is.numeric(groups))) {
groups.titles <- sprintf("Component %i", seq_along(findex)) # For tab_itemscale compatibility
} else if (length(groups.titles) != length(findex)) {
groups.titles <- findex
}
# If groups were supplied, combine the subplots with cowplot::plot_grid()
if (length(findex) == 1) {
.out <- .plot_list[[1]]
} else {
.out <- do.call(get("plot_grid", asNamespace("cowplot")),
args = c(
list(
"plotlist" = .plot_list,
"labels" = groups.titles,
"rel_heights" = rel_heights,
"ncol" = 1
),
cowplot.options
))
}
.out
}
.plot_likert <- function(items,
title = NULL,
legend.title = NULL,
legend.labels = NULL,
axis.titles = NULL,
axis.labels = NULL,
catcount = NULL,
cat.neutral = NULL,
sort.frq = NULL,
weight.by = NULL,
title.wtd.suffix = NULL,
wrap.title = 50,
wrap.labels = 30,
wrap.legend.title = 30,
wrap.legend.labels = 28,
geom.size = .6,
geom.colors = "BrBG",
cat.neutral.color = "grey70",
intercept.line.color = "grey50",
reverse.colors = FALSE,
values = "show",
show.n = TRUE,
show.legend = TRUE,
show.prc.sign = FALSE,
grid.range = 1,
grid.breaks = 0.2,
expand.grid = TRUE,
digits = 1,
reverse.scale = FALSE,
coord.flip = TRUE) {
# check param. if we have a single vector instead of
# a data frame with several items, convert vector to data frame
if (!is.data.frame(items) && !is.matrix(items)) items <- as.data.frame(items)
# if grid.range is supplied as 1 value, it is duplicated for symmetric results. This is for compatibillity with older versions.
if (length(grid.range) == 1) grid.range <- c(grid.range, grid.range)
# copy titles
if (is.null(axis.titles)) {
axisTitle.x <- NULL
axisTitle.y <- NULL
} else {
axisTitle.x <- axis.titles[1]
if (length(axis.titles) > 1)
axisTitle.y <- axis.titles[2]
else
axisTitle.y <- NULL
}
# check sorting
if (!is.null(sort.frq)) {
if (sort.frq == "pos.asc") {
sort.frq <- "pos"
reverseOrder <- FALSE
}
if (sort.frq == "pos.desc") {
sort.frq <- "pos"
reverseOrder <- TRUE
}
if (sort.frq == "neg.asc") {
sort.frq <- "neg"
reverseOrder <- FALSE
}
if (sort.frq == "neg.desc") {
sort.frq <- "neg"
reverseOrder <- TRUE
}
} else {
reverseOrder <- FALSE
}
# try to automatically set labels is not passed as argument
if (is.null(legend.labels)) {
legend.labels <- sjlabelled::get_labels(
items[[1]],
attr.only = F,
values = NULL,
non.labelled = T
)
}
if (is.null(axis.labels)) {
# retrieve variable name attribute
axis.labels <- unname(sjlabelled::get_label(items, def.value = colnames(items)))
}
# unname labels, if necessary, so we have a simple character vector
if (!is.null(names(axis.labels))) axis.labels <- as.vector(axis.labels)
if (!is.null(legend.labels)) {
if (!is.null(names(legend.labels))) legend.labels <- as.vector(legend.labels)
}
# determine catcount
adding <- ifelse(is.null(cat.neutral), 0, 1)
if (is.null(catcount)) {
# add new unique item values to catcount, so catcount
# finally contains all unique values of items
catcount <- items %>%
purrr::map(~ stats::na.omit(unique(.x))) %>%
purrr::flatten_dbl() %>%
unique() %>%
sort()
neutral.between <- FALSE
# remove neutral category
if (!is.null(cat.neutral)) {
# find neutral cat value in catcount
ncv_pos <- which(catcount == cat.neutral)
# if not empty, remove
if (!sjmisc::is_empty(ncv_pos)) {
catcount <- catcount[-ncv_pos]
neutral.between <- dplyr::between(cat.neutral, min(catcount), max(catcount))
}
}
# detect range of valid categories, which
# then equals catcount
catcount <- max(catcount) - min(catcount) + 1
# check if category count matches category label count
if (!is.null(legend.labels)) {
# how many labels do we have?
# substract 1, if we have neutral category
lll <- length(legend.labels) - adding
# catcount and legend label count equal?
if (catcount < lll) {
# warn user that detected amount of categories and supplied legend labels
# are different.
warning("Length of labels for item categories `legend.labels` differs from detected amount of categories. Use `catcount` argument to define amount of item categories, if plotting does not work.", call. = F)
# adjust catcount to length of legend labels, because
# we assume that labels represent the valid range of
# item categories
catcount <- lll
}
}
# is catcount odd or even? make catcount even
if (sjmisc::is_odd(catcount)) {
# warn user about uneven category count, but only if
# neutral category is not inside valid categories
if (!neutral.between)
warning("Detected uneven category count in items. Dropping last category.", call. = F)
catcount <- catcount - 1
}
}
# set legend labels, if we have none yet
if (is.null(legend.labels)) legend.labels <- seq_len(catcount + adding)
# prepare data frames
mydat.pos <- data.frame()
mydat.neg <- data.frame()
mydat.dk <- data.frame()
freq.df <- data.frame()
# If we have neutral category in between and not as last
# category, recode neutral category to last category
if (!is.null(cat.neutral) && cat.neutral <= catcount) {
# first, each other category has to be moved down one position
# therefore, we create a pattern with values from neutral
# category to category count
downvote <- seq(cat.neutral, catcount + 1, by = 1)
# now we "shift" this value pattern and make a
# string out of it
recode.pattern <- paste0(
paste0(sprintf("%i=%i", c(downvote[-1], downvote[1]), downvote),
collapse = ";"), ";else=copy"
)
# all factors with char labels need to be numeric,
# else, recode won't work
items <- purrr::modify_if(
items,
is_labelled_factor,
sjlabelled::as_numeric,
keep.labels = FALSE
)
# finally, recode data
items <- sjmisc::rec(items, rec = recode.pattern, append = FALSE)
# re-order legend labels as well
ll.order <- c(seq_len(catcount + adding)[-cat.neutral], cat.neutral)
legend.labels <- legend.labels[ll.order]
}
# loop through all likert-items
for (i in seq_len(ncol(items))) {
# convert to numeric values
if (!is.numeric(items[[i]])) {
items[[i]] <- sjlabelled::as_numeric(items[[i]], keep.labels = F)
}
# If we don't plot neutral category, but item still contains
# that category, replace it with NA
if (is.null(cat.neutral) && max(items[[i]], na.rm = T) > catcount)
items[[i]] <- sjmisc::set_na(items[[i]], na = catcount + 1, as.tag = F)
# create proportional frequency table
if (is.null(weight.by)) {
tab <- round(prop.table(table(items[[i]])), digits + 3)
} else {
tab <- round(prop.table(stats::xtabs(weight.by ~ items[[i]])), digits + 3)
}
# retrieve category number and related frequencies
counts <- as.numeric(tab)
valid <- as.numeric(names(tab))
# create frequency vector, so zero-categories are cared for
freq <- rep(0, catcount + adding)
freq[valid] <- counts
# append to data frame
if (ncol(freq.df) == 0)
freq.df <- as.data.frame(freq)
else {
# check for valid rows. if we hav missing categories
# in all items, argument "catcount" must be set, because
# automatic detection of amount of categories does not
# work then.
if (length(freq) != nrow(freq.df))
stop("Could not determine amount of item categories. Please use argument `catcount`.", call. = F)
else
freq.df <- as.data.frame(cbind(freq.df, freq))
}
}
# Check whether N of each item should be included into axis labels
if (show.n) {
for (i in seq_len(length(axis.labels))) {
axis.labels[i] <- paste(
axis.labels[i],
sprintf(" (n=%i)", length(stats::na.omit(items[[i]]))),
sep = ""
)
}
}
# determine split between pos and neg values
# lower.half <- rev(seq(catcount / 2))
lower.half <- rev(seq(ceiling(catcount / 2)))
# upper.half <- 1 + catcount - lower.half
upper.half <- setdiff(seq.int(catcount), lower.half)
# sum up values to total, so we can sort items
sums.lower <- unname(apply(freq.df[lower.half, , drop = FALSE], 2, sum))
sums.upper <- unname(apply(freq.df[upper.half, , drop = FALSE], 2, sum))
# sort items
if (is.null(sort.frq))
sort.freq <- seq_len(ncol(freq.df))
else if (sort.frq == "pos")
sort.freq <- order(sums.lower)
else if (sort.frq == "neg")
sort.freq <- order(sums.upper)
else
sort.freq <- seq_len(ncol(freq.df))
# reverse item order?
if (!reverseOrder) sort.freq <- rev(sort.freq)
# save summed up y-values, for label positioning and annotation
ypos.sum.pos <- c()
ypos.sum.neg <- c()
ypos.sum.dk <- c()
# iterate all frequencies of the items. we have the simple
# data rows in this data frame and now need to "split"
# positive and negative values
for (i in seq_len(ncol(freq.df))) {
# sort
fr <- freq.df[, sort.freq[i]]
# positive values. we need an x-position for each item,
# a group indicator, the frequencies (as percent value),
# and the y position for labels.
mydat.pos <- as.data.frame(
rbind(mydat.pos,
cbind(x = i,
grp = lower.half,
frq = fr[lower.half],
ypos = cumsum(fr[lower.half]) - 0.5 * (fr[lower.half]),
ypos2 = sum(fr[lower.half])
)))
# summed y-position for plotting the summed up frequency labels
ypos.sum.pos <- c(ypos.sum.pos, sum(fr[lower.half]))
# same as above for negative values
mydat.neg <- as.data.frame(
rbind(mydat.neg,
cbind(x = i,
grp = upper.half,
frq = -fr[upper.half],
ypos = -1 * (cumsum(fr[upper.half]) - 0.5 * (fr[upper.half])),
ypos2 = -1 * sum(fr[upper.half])
)))
# summed up (cumulative) percs
ypos.sum.neg <- c(ypos.sum.neg, -1 * sum(fr[upper.half]))
# same as above for neutral category, if we have any
if (!is.null(cat.neutral)) {
mydat.dk <- as.data.frame(
rbind(mydat.dk,
cbind(x = i,
grp = catcount + adding,
frq = -1 + fr[catcount + adding],
ypos = -1 + (fr[catcount + adding] / 2),
ypos2 = -1 + fr[catcount + adding],
offset = -1 * grid.range[1])
))
# cumulative neutral cat
ypos.sum.dk <- c(ypos.sum.dk, -1 + fr[catcount + adding])
}
}
# x-positions for cumulative percentages
xpos.sum.dk <- xpos.sum.neg <- xpos.sum.pos <- seq_len(length(ypos.sum.pos))
# grp as factor
mydat.pos$grp <- as.factor(mydat.pos$grp)
mydat.neg$grp <- as.factor(mydat.neg$grp)
# same for neutral
if (!is.null(cat.neutral)) {
mydat.dk$grp <- as.factor("neutral")
mydat.dk$geom.size <- geom.size
mydat.dk$digits <- digits
}
# label digits needed
mydat.neg$digits <- digits
mydat.pos$digits <- digits
# Prepare and trim legend labels to appropriate size
legend.labels <- sjmisc::word_wrap(legend.labels, wrap.legend.labels)
if (!is.null(legend.title)) {
legend.title <- sjmisc::word_wrap(legend.title, wrap.legend.title)
}
if (!is.null(title)) {
if (!is.null(title.wtd.suffix)) {
title <- paste(title, title.wtd.suffix, sep = "")
}
title <- sjmisc::word_wrap(title, wrap.title)
}
# check length of x-axis-labels and split longer strings at into new lines
# every 10 chars, so labels don't overlap
axis.labels <- sjmisc::word_wrap(axis.labels, wrap.labels)
axis.labels <- axis.labels[sort.freq]
# set diagram margins
if (expand.grid) {
expgrid <- waiver()
} else {
expgrid <- c(0, 0)
}
# Set up grid breaks. Calculate grid breaks starting at the center (0). Negative sequence is reversed. Positive sequence is skipping the 0 to avoid doubeling.
gridbreaks <- round(c(rev(seq(0, -grid.range[1], by = -grid.breaks)), seq(grid.breaks, grid.range[2], by = grid.breaks)), 2)
gridlabs <- ifelse(abs(gridbreaks) > 1, "", paste0(abs(round(100 * gridbreaks)), "%"))
# start plot here
gp <- ggplot() +
# positive value bars
geom_col(
data = mydat.pos,
aes_string(x = "x", y = "frq", fill = "grp"),
width = geom.size
) +
# negative value bars
geom_col(
data = mydat.neg,
aes_string(x = "x", y = "frq", fill = "grp"),
width = geom.size,
position = position_stack(reverse = T)
)
# print bar for neutral category. this is a "fake" bar created
# with geom_rect. to make this work, we need to set the x-axis
# to a continuous scale...
if (!is.null(cat.neutral)) {
gp <- gp +
geom_rect(
data = mydat.dk,
aes(
xmin = .data$x - (geom.size / 2),
xmax = .data$x + (geom.size / 2),
ymin = .data$offset,
ymax = .data$frq + (.data$offset + 1),
fill = "neutral")
)
}
# if we have neutral colors, we need to add the geom-color
# to the color values.
if (!is.null(cat.neutral)) geom.colors <- c(geom.colors, cat.neutral.color)
# should percentage value labels be printed?
percsign <- mydat.pos$percsign <- mydat.neg$percsign <- ifelse(isTRUE(show.prc.sign), "%", "")
if (nrow(mydat.dk) > 0) mydat.dk$percsign <- percsign
# creating value labels for cumulative percentages, so
# zero-percentages are not printed
ypos.sum.pos.lab <- ifelse(ypos.sum.pos > 0, sprintf("%.*f%s", digits, 100 * ypos.sum.pos, percsign), "")
ypos.sum.neg.lab <- ifelse(ypos.sum.neg < 0, sprintf("%.*f%s", digits, 100 * abs(ypos.sum.neg), percsign), "")
ypos.sum.dk.lab <- ifelse(ypos.sum.dk > -1, sprintf("%.*f%s", digits, 100 * (1 + ypos.sum.dk), percsign), "")
if (values == "show") {
if (!requireNamespace("ggrepel", quietly = TRUE)) {
stop("Package `ggrepel` needed to plot labels. Please install it.", call. = FALSE)
}
# show them in middle of bar
gp <- gp +
ggrepel::geom_text_repel(
data = dplyr::filter(mydat.pos, .data$frq > 0),
aes(
x = .data$x,
y = .data$frq,
label = sprintf("%.*f%s", digits, 100 * .data$frq, percsign)
),
direction = "y",
position = position_stack(vjust = 0.5, reverse = TRUE),
force = .5,
point.padding = NA
) +
ggrepel::geom_text_repel(
data = dplyr::filter(mydat.neg, .data$frq < 0),
aes(
x = .data$x,
y = .data$frq,
label = sprintf("%.*f%s", digits, 100 * abs(.data$frq), percsign)
),
direction = "y",
position = position_stack(vjust = 0.5, reverse = TRUE),
force = .5,
point.padding = NA
)
if (!is.null(cat.neutral)) {
gp <- gp +
geom_text(
data = dplyr::filter(mydat.dk, .data$frq > -1),
aes(
x = .data$x,
y = .data$ypos + .data$offset + 1,
label = sprintf("%.*f%s", digits, 100 * (1 + .data$frq), percsign)
)
)
}
} else if (values == "sum.inside" || values == "sum.outside") {
# choose label offsets for summed proportions
move_pos_labels_left = dplyr::case_when(
values == "sum.outside" & !reverse.scale ~ T,
values == "sum.inside" & !reverse.scale ~ F,
values == "sum.outside" & reverse.scale ~ F,
values == "sum.inside" & reverse.scale ~ T
)
# show cumulative outside bar
if (move_pos_labels_left) {
hort.pos <- -0.15
hort.neg <- 1.15
hort.dk <- -0.15
# show cumulative inside bar
} else {
hort.pos <- 1.15
hort.neg <- -0.15
hort.dk <- 1.15
}
gp <- gp +
annotate("text", x = xpos.sum.pos, y = ypos.sum.pos, hjust = hort.pos, label = ypos.sum.pos.lab) +
annotate("text", x = xpos.sum.neg, y = ypos.sum.neg, hjust = hort.neg, label = ypos.sum.neg.lab)
if (!is.null(cat.neutral)) {
gp <- gp +
annotate("text", x = xpos.sum.dk, y = ypos.sum.dk + 1 - grid.range[1], hjust = hort.dk, label = ypos.sum.dk.lab)
}
}
# continues with plot
gp <- gp +
labs(title = title, x = axisTitle.x, y = axisTitle.y, fill = legend.title) +
# scale x is continuous to make plotting the bar annotation
# for neutral category work...
scale_x_continuous(breaks = seq_len(ncol(freq.df)), labels = axis.labels) +
geom_hline(yintercept = 0, color = intercept.line.color)
# check wether percentage scale (y-axis) should be reversed
if (!reverse.scale) {
gp <- gp + scale_y_continuous(breaks = gridbreaks, limits = c(-grid.range[1], grid.range[2]), expand = expgrid, labels = gridlabs)
} else {
gp <- gp + scale_y_reverse(breaks = gridbreaks, limits = c(grid.range[2], -grid.range[1]), expand = expgrid, labels = gridlabs)
}
# check whether coordinates should be flipped, i.e.
# swap x and y axis
if (coord.flip) gp <- gp + coord_flip()
# set geom colors
sj.setGeomColors(
gp,
geom.colors,
(catcount + adding),
show.legend,
legend.labels,
reverse.colors
)
}
# is factor with char levels?
#' @importFrom sjmisc is_num_fac
is_labelled_factor <- function(x) is.factor(x) && !sjmisc::is_num_fac(x)
|