diff --git a/DESCRIPTION b/DESCRIPTION index 9a8d4e2..51dacdc 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: autospc Title: Automatically Partitioned SPC Charts -Version: 0.0.0.9040 +Version: 0.0.0.9041 Authors@R: c( person("Thomas", "Woodcock", , "woodcock.thomas@gmail.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-4735-4856")), @@ -14,6 +14,7 @@ Imports: DiagrammeR, dplyr, fpCompare, + ggnewscale, ggplot2, lifecycle, magrittr, diff --git a/R/add_annotation_data.R b/R/add_annotation_data.R index db505c9..b9d7410 100644 --- a/R/add_annotation_data.R +++ b/R/add_annotation_data.R @@ -128,12 +128,17 @@ add_annotations_to_plot_pp <- function(p, if(annotation_arrows) { p_annotated <- p + ggrepel::geom_text_repel( + data = . %>% dplyr::filter(series %in% c("cl"), + !is.na(annotation_level)), ggplot2::aes(x = x, - y = cl, + y = value, label = cl_label), - position = ggpp::position_nudge_to(y = df %>% - dplyr::filter(!is.na(y)) %>% - dplyr::pull(annotation_level)), + position = ggpp::position_nudge_to( + y = df %>% + dplyr::filter(series %in% c("cl"), + !is.na(value), + !is.na(annotation_level)) %>% + dplyr::pull(annotation_level)), color = "grey40", size = annotation_size, fontface = "bold", @@ -143,7 +148,9 @@ add_annotations_to_plot_pp <- function(p, hjust = 0, segment.size = 0.75, segment.curvature = df %>% - dplyr::filter(!is.na(y)) %>% + dplyr::filter(series %in% c("cl"), + !is.na(value), + !is.na(annotation_level)) %>% dplyr::pull(annotation_curvature), segment.ncp = 4, segment.inflect = FALSE, @@ -153,12 +160,17 @@ add_annotations_to_plot_pp <- function(p, max.overlaps = Inf) } else { p_annotated <- p + ggrepel::geom_text_repel( + data = . %>% dplyr::filter(series %in% c("cl"), + !is.na(annotation_level)), ggplot2::aes(x = x, - y = cl, + y = value, label = cl_label), - position = ggpp::position_nudge_to(y = df %>% - dplyr::filter(!is.na(y)) %>% - dplyr::pull(annotation_level)), + position = ggpp::position_nudge_to( + y = df %>% + dplyr::filter(series %in% c("cl"), + !is.na(value), + !is.na(annotation_level)) %>% + dplyr::pull(annotation_level)), color = "grey40", size = annotation_size, fontface = "bold", @@ -184,14 +196,17 @@ add_annotations_to_plot_basic <- function(p, x_nudge <- x_range/25 p_annotated <- p + - ggplot2::geom_text(mapping = ggplot2::aes(x = x, - y = annotation_level, - label = cl_label), - nudge_x = x_nudge, - na.rm = TRUE, - color = "grey40", - size = annotation_size, - fontface = "bold") + ggplot2::geom_text( + data = . %>% dplyr::filter(series %in% c("cl"), + !is.na(annotation_level)), + mapping = ggplot2::aes(x = x, + y = annotation_level, + label = cl_label), + nudge_x = x_nudge, + na.rm = TRUE, + color = "grey40", + size = annotation_size, + fontface = "bold") return(p_annotated) diff --git a/R/add_floating_median.R b/R/add_floating_median.R new file mode 100644 index 0000000..a61923b --- /dev/null +++ b/R/add_floating_median.R @@ -0,0 +1,38 @@ +# Add floating median line to the plot p +add_floating_median <- function(df, + p, + floating_median_n) { + + p <- p + + ggplot2::geom_line(data = df %>% + dplyr::filter(series == "cl"), + ggplot2::aes(x, median), + linetype = "75551555", + colour = "gray50", + linewidth = 0.5, + show.legend = TRUE, + na.rm = TRUE) + + ggplot2::annotate( + "text", + x = df %>% + dplyr::filter(series == "cl") %>% + dplyr::filter(dplyr::row_number() == + nrow(df %>% + dplyr::filter(series == "cl")) - + floating_median_n + 1L) %>% + dplyr::pull(x), + y = df %>% + dplyr::filter(series == "cl") %>% + dplyr::filter(dplyr::row_number() == + nrow(df %>% + dplyr::filter(series == "cl")) - + floating_median_n + 1L) %>% + dplyr::pull(median)*0.95, + label = "Median", + size = 3, + colour = "gray50", + na.rm = TRUE) + + return(p) + +} \ No newline at end of file diff --git a/R/algorithm_helpers.R b/R/algorithm_helpers.R index 3b392f2..86212e5 100644 --- a/R/algorithm_helpers.R +++ b/R/algorithm_helpers.R @@ -481,39 +481,6 @@ floating_median_column <- function(df, } -# Add floating median line to the plot p -add_floating_median <- function(df, - p, - floating_median_n) { - - p <- p + - ggplot2::geom_line(data = df, - ggplot2::aes(x, median), - linetype = "75551555", - colour = "gray50", - linewidth = 0.5, - show.legend = TRUE, - na.rm = TRUE) + - ggplot2::annotate( - "text", - x = df %>% - dplyr::filter(dplyr::row_number() == nrow(df) - - floating_median_n + 1L) %>% - dplyr::pull(x), - y = df %>% - dplyr::filter(dplyr::row_number() == nrow(df) - - floating_median_n + 1L) %>% - dplyr::pull(median)*0.95, - label = "Median", - size = 3, - colour = "gray50", - na.rm = TRUE) - - return(p) - -} - - sign_chr <- function(x) { y <- dplyr::case_when( x < 0 ~ "01", diff --git a/R/visualisation.R b/R/visualisation.R index f5161b3..65aad81 100644 --- a/R/visualisation.R +++ b/R/visualisation.R @@ -29,9 +29,22 @@ create_spc_plot <- function(df, x_date_format = "%Y-%m-%d", split_rows = NULL) { + df_long <- df %>% + tidyr::pivot_longer(cols = c(y, cl, ucl, lcl), + names_to = "series", + values_to = "value") + + df_long <- df_long %>% + dplyr::select(x, + series, + value, + everything()) + # Create initial plot object without formatting - pct <- ggplot2::ggplot(df %>% dplyr::filter(!is.na(y)), - ggplot2::aes(x,y)) + pct <- ggplot2::ggplot(df_long %>% + dplyr::filter(!is.na(value)), + ggplot2::aes(x = x, + y = value)) if(use_caption) { caption <- paste(chart_type, @@ -46,8 +59,9 @@ create_spc_plot <- function(df, rule_title <- "Rule triggered" } + # Apply autospc formatting p <- format_SPC(pct, - df = df, + df_long = df_long, r1_col = r1_col, r2_col = r2_col, point_size = point_size, @@ -62,16 +76,18 @@ create_spc_plot <- function(df, breaks = scales::breaks_pretty(), labels = scales::label_number(big.mark = ",")) + # Add floating median to chart if needed if("median" %in% colnames(df)) { p <- add_floating_median(p = p, - df = df, + df = df_long, floating_median_n = floating_median_n) } + # Add annotations to chart if needed if(include_annotations == TRUE){ p <- add_annotations_to_plot(p = p, - df = df, + df = df_long, basic_annotations = basic_annotations, annotation_size = annotation_size, annotation_arrows = annotation_arrows, @@ -79,62 +95,14 @@ create_spc_plot <- function(df, } # Format x-axis depending on x type - if(any(xType == "Date")) { - if(is.null(x_break)) { - p <- p + - ggplot2::scale_x_date(labels = scales::date_format(x_date_format), - breaks = scales::breaks_pretty(), - limits = c(as.Date(start_x), - as.Date(end_x))) - } else { - p <- p + - ggplot2::scale_x_date(labels = scales::date_format(x_date_format), - breaks = seq(as.Date(start_x), - as.Date(end_x), - x_break), - limits = c(as.Date(start_x), - as.Date(end_x))) - } - } else if(any(xType == "integer")) { - if(is.null(x_break)) { - p <- p + - ggplot2::scale_x_continuous(breaks = scales::breaks_extended(), - limits = c(start_x, - end_x)) - } else { - p <- p + - ggplot2::scale_x_continuous(breaks = seq(start_x, - end_x, - x_break), - limits = c(start_x, - end_x)) - } - } else if(any(xType == "POSIXct")) { - if(is.null(x_break)) { - p <- p + - ggplot2::scale_x_datetime(breaks = scales::breaks_pretty(), - limits = c(start_x, end_x)) - } else { - if(any(class(x_break) != "difftime")) { - rlang::abort(paste("Please specify x_break as a difftime object when", - "x is POSIXct.")) - } - p <- p + - ggplot2::scale_x_datetime(breaks = seq(start_x, end_x, x_break), - limits = c(start_x, end_x)) - } - } else { - if(is.null(x_break)) { - p <- p + - ggplot2::scale_x_continuous(breaks = scales::breaks_extended(), - limits = c(start_x, end_x)) - } else { - p <- p + - ggplot2::scale_x_continuous(breaks = seq(start_x, end_x, x_break), - limits = c(start_x, end_x)) - } - } + p <- format_x_axis(p = p, + xType = xType, + x_break = x_break, + x_date_format = x_date_format, + start_x = start_x, + end_x = end_x) + # Facet by stages if needed if(!is.null(split_rows)) { p <- p + ggplot2::facet_wrap(facets = ggplot2::vars(stage), @@ -142,6 +110,7 @@ create_spc_plot <- function(df, } + # Combine X and MR charts if needed if((chart_type == "XMR") & show_mr) { p <- p + ggplot2::labs(caption = NULL, @@ -190,25 +159,7 @@ create_timeseries_plot <- function(df, ggplot2::geom_line(colour = "black", linewidth = 0.5*line_width_sf) + ggplot2::geom_point(colour = "black", size = point_size) + - ggplot2::theme(panel.grid.major.y = ggplot2::element_blank(), - panel.grid.major.x = ggplot2::element_line( - colour = "grey80" - ), - panel.grid.minor = ggplot2::element_blank(), - panel.background = ggplot2::element_blank(), - axis.text.x = ggplot2::element_text(angle = 45, - hjust = 1, - vjust = 1.0, - size = 14), - axis.text.y = ggplot2::element_text(size = 14), - axis.title = ggplot2::element_text(size = 14), - plot.title = ggplot2::element_text(size = 20, - hjust = 0), - plot.subtitle = ggplot2::element_text(size = 16, - face = "italic"), - axis.line = ggplot2::element_line(colour = "grey60"), - plot.caption = ggplot2::element_text(size = 10, - hjust = 0.5)) + + theme_autospc() + ggplot2::ggtitle(title, subtitle = subtitle) + ggplot2::labs(x = override_x_title, @@ -223,7 +174,7 @@ create_timeseries_plot <- function(df, format_SPC <- function(cht, - df, + df_long, r1_col, r2_col, point_size, @@ -236,91 +187,161 @@ format_SPC <- function(cht, "None" = "black", "Excluded from limits calculation" = "grey") - #get exemplar calculation and display periods - plot_periods <- df$plotPeriod + line_colours <- c("Calculation" = "black", + "Display" = "grey50") + + # Prepare information on plot periods + plot_periods <- df_long$plotPeriod first_display_period <- plot_periods[grep("display", plot_periods)[1]] first_calc_period <- plot_periods[1] - suppressWarnings( # to avoid the warning about using alpha for discrete vars - cht + - ggplot2::geom_line(colour = "black", - linewidth = 0.5*line_width_sf, - na.rm = TRUE) + - ggplot2::geom_line(data = df, - ggplot2::aes(x,cl, - alpha = plotPeriod), - linetype = "solid", - linewidth = 0.75*line_width_sf, - na.rm = TRUE) + - ggplot2::geom_line(data = df, - ggplot2::aes(x,lcl, - alpha = plotPeriod), - linetype = "42", - linewidth = 0.5*line_width_sf, - show.legend = FALSE, - na.rm = TRUE) + - ggplot2::geom_line(data = df, - ggplot2::aes(x,ucl, - alpha = plotPeriod), - linetype = "42", - linewidth = 0.5*line_width_sf, - show.legend = FALSE, - na.rm = TRUE) + - ggplot2::geom_point(ggplot2::aes(colour = highlight), - size = point_size, - na.rm = TRUE) + - ggplot2::scale_color_manual(rule_title, - values = point_colours) + - ggplot2::scale_alpha_discrete("Period Type", - labels = if(!is.na(first_display_period)) { - c("Calculation", "Display") - } else { - c("Calculation") - }, - range = if(!is.na(first_display_period)) { - c(1, 0.4) - } else { - c(1, 1) - }, - breaks = if(!is.na(first_display_period)) { - c(first_calc_period, - first_display_period) - } else { - c(first_calc_period) - }, - guide = ggplot2::guide_legend( - override.aes = list( - alpha = if( - !is.na(first_display_period) - ) { - c(1, 0.4) - } else { - c(1) - } - ) - ) - ) + - ggplot2::theme(panel.grid.major.y = ggplot2::element_blank(), - panel.grid.major.x = ggplot2::element_line( - colour = "grey80" - ), - panel.grid.minor = ggplot2::element_blank(), - panel.background = ggplot2::element_blank(), - axis.text.x = ggplot2::element_text(angle = 45, - hjust = 1, - vjust = 1.0, - size = 14), - axis.text.y = ggplot2::element_text(size = 14), - axis.title = ggplot2::element_text(size = 14), - plot.title = ggplot2::element_text(size = 20, - hjust = 0), - plot.subtitle = ggplot2::element_text(size = 16, - face = "italic"), - axis.line = ggplot2::element_line(colour = "grey60"), - plot.caption = ggplot2::element_text(size = 10, - hjust = 0.5)) - ) + list_of_plot_periods <- unique(plot_periods) + + linecolour_scale <- grepl("calculation", + list_of_plot_periods) %>% + ifelse(line_colours["Calculation"], + line_colours["Display"]) + + names(linecolour_scale) <- list_of_plot_periods + + # Create spc plot components + cht <- cht + + ggplot2::geom_line(data = . %>% dplyr::filter( + series %in% c("cl", "ucl", "lcl")), + ggplot2::aes(colour = plotPeriod, + linetype = series, + linewidth = series), + na.rm = TRUE) + + ggplot2::geom_line(data = . %>% dplyr::filter(series %in% c("y")), + ggplot2::aes(linetype = series, + linewidth = series), + show.legend = FALSE, + na.rm = TRUE) + + ggplot2::scale_colour_manual( + "Period Type", + values = linecolour_scale, + breaks = if(!is.na(first_display_period)) { + c(first_calc_period, + first_display_period) + } else { + c(first_calc_period) + }, + labels = if(!is.na(first_display_period)) { + c("Calculation", "Display") + } else { + c("Calculation") + } + ) + + ggplot2::scale_linetype_manual(values = c("solid", "42", "42", "solid"), + guide = "none") + + ggplot2::scale_linewidth_manual(values = + c(0.75, 0.5, 0.5, 0.5)*line_width_sf, + guide = "none") + + ggnewscale::new_scale_colour() + + ggplot2::geom_point(data = . %>% dplyr::filter(series == "y"), + ggplot2::aes(colour = highlight), + size = point_size, + na.rm = TRUE) + + ggplot2::scale_color_manual(rule_title, + values = point_colours) + + theme_autospc() + + return(cht) +} + + +theme_autospc <- function(){ + + thm_aspc <- ggplot2::theme(panel.grid.major.y = ggplot2::element_blank(), + panel.grid.major.x = ggplot2::element_line( + colour = "grey80" + ), + panel.grid.minor = ggplot2::element_blank(), + panel.background = ggplot2::element_blank(), + axis.text.x = ggplot2::element_text(angle = 45, + hjust = 1, + vjust = 1.0, + size = 14), + axis.text.y = ggplot2::element_text(size = 14), + axis.title = ggplot2::element_text(size = 14), + plot.title = ggplot2::element_text(size = 20, + hjust = 0), + plot.subtitle = ggplot2::element_text(size = 16, + face = "italic"), + axis.line = ggplot2::element_line(colour = "grey60"), + plot.caption = ggplot2::element_text(size = 10, + hjust = 0.5)) + + return(thm_aspc) + } + +format_x_axis <- function(p, + xType, + x_break, + x_date_format, + start_x, + end_x) { + + if(any(xType == "Date")) { + if(is.null(x_break)) { + p <- p + + ggplot2::scale_x_date(labels = scales::date_format(x_date_format), + breaks = scales::breaks_pretty(), + limits = c(as.Date(start_x), + as.Date(end_x))) + } else { + p <- p + + ggplot2::scale_x_date(labels = scales::date_format(x_date_format), + breaks = seq(as.Date(start_x), + as.Date(end_x), + x_break), + limits = c(as.Date(start_x), + as.Date(end_x))) + } + } else if(any(xType == "integer")) { + if(is.null(x_break)) { + p <- p + + ggplot2::scale_x_continuous(breaks = scales::breaks_extended(), + limits = c(start_x, + end_x)) + } else { + p <- p + + ggplot2::scale_x_continuous(breaks = seq(start_x, + end_x, + x_break), + limits = c(start_x, + end_x)) + } + } else if(any(xType == "POSIXct")) { + if(is.null(x_break)) { + p <- p + + ggplot2::scale_x_datetime(breaks = scales::breaks_pretty(), + limits = c(start_x, end_x)) + } else { + if(any(class(x_break) != "difftime")) { + rlang::abort(paste("Please specify x_break as a difftime object when", + "x is POSIXct.")) + } + p <- p + + ggplot2::scale_x_datetime(breaks = seq(start_x, end_x, x_break), + limits = c(start_x, end_x)) + } + } else { + if(is.null(x_break)) { + p <- p + + ggplot2::scale_x_continuous(breaks = scales::breaks_extended(), + limits = c(start_x, end_x)) + } else { + p <- p + + ggplot2::scale_x_continuous(breaks = seq(start_x, end_x, x_break), + limits = c(start_x, end_x)) + } + } + + return(p) + +} diff --git a/renv.lock b/renv.lock index 9068339..03c2b05 100644 --- a/renv.lock +++ b/renv.lock @@ -1225,6 +1225,32 @@ "Maintainer": "Hadley Wickham ", "Repository": "CRAN" }, + "ggnewscale": { + "Package": "ggnewscale", + "Version": "0.5.2", + "Source": "Repository", + "Language": "en-GB", + "Title": "Multiple Fill and Colour Scales in 'ggplot2'", + "Authors@R": "person(given = \"Elio\", family = \"Campitelli\", role = c(\"cre\", \"aut\"), email = \"eliocampitelli@gmail.com\", comment = c(ORCID = \"0000-0002-7742-9230\"))", + "Description": "Use multiple fill and colour scales in 'ggplot2'.", + "License": "GPL-3", + "URL": "https://eliocamp.github.io/ggnewscale/, https://github.com/eliocamp/ggnewscale", + "BugReports": "https://github.com/eliocamp/ggnewscale/issues", + "Encoding": "UTF-8", + "Imports": [ + "ggplot2 (>= 3.5.0)" + ], + "RoxygenNote": "7.3.2", + "Suggests": [ + "testthat", + "vdiffr", + "covr" + ], + "NeedsCompilation": "no", + "Author": "Elio Campitelli [cre, aut] (ORCID: )", + "Maintainer": "Elio Campitelli ", + "Repository": "CRAN" + }, "ggplot2": { "Package": "ggplot2", "Version": "3.5.2", diff --git a/tests/testthat/test_linetypes.R b/tests/testthat/test_linetypes.R index 0260223..e351ace 100644 --- a/tests/testthat/test_linetypes.R +++ b/tests/testthat/test_linetypes.R @@ -1,4 +1,3 @@ -#load in test data test_data <- readRDS("testdata/test_data.rds") test_that("Linetypes are formed correctly", { @@ -6,18 +5,29 @@ test_that("Linetypes are formed correctly", { test_plt <- autospc(test_data, chart_type = "C'") + # layer 1 holds the centre line and control limits + layer_1 <- ggplot2::layer_data(test_plt, 1) %>% dplyr::arrange(x) + # layer 2 holds the data series layer_2 <- ggplot2::layer_data(test_plt, 2) %>% dplyr::arrange(x) - layer_4 <- ggplot2::layer_data(test_plt, 4) %>% dplyr::arrange(x) + # layer 1 has 3n rows, as it holds three series + expect_equal(nrow(layer_1), 450L) + # layer 2 has n rows, as it holds one series + expect_equal(nrow(layer_2), 150L) + + rle_layer_1 <- rle(layer_1$linetype) rle_layer_2 <- rle(layer_2$linetype) - rle_layer_4 <- rle(layer_4$linetype) - correct_answer_2 <- structure(list(lengths = 150L, values = "solid"), + correct_answer_1 <- structure(list(lengths = rep(c(1L, 2L), + 150), + values = rep(c("solid", "42"), + 150)), class = "rle") - correct_answer_4 <- structure(list(lengths = 150L, values = "42"), + correct_answer_2 <- structure(list(lengths = 150L, + values = "solid"), class = "rle") + expect_identical(rle_layer_1, correct_answer_1) expect_identical(rle_layer_2, correct_answer_2) - expect_identical(rle_layer_4, correct_answer_4) }) diff --git a/tests/testthat/test_median_plot.R b/tests/testthat/test_median_plot.R index 996bd05..4f9c4fb 100644 --- a/tests/testthat/test_median_plot.R +++ b/tests/testthat/test_median_plot.R @@ -16,12 +16,13 @@ test_that("the series of medians being plotted are correctly calculated when flo chart_result_data <- chart_result$data # Extract all median values from the result data - result_median <- chart_result_data %>% + result_median <- chart_result_data %>% + dplyr::filter(series == "y") %>% dplyr::filter(!is.na(median)) %>% dplyr::pull(median) # Test that the length of the test_median__n points matches the length of the median points calculated - expect_equal(length(result_median),test_median_n) + expect_equal(length(result_median), test_median_n) # Identify how many distinct values for the median are being calculated unique_result_median <- unique(result_median) @@ -31,14 +32,17 @@ test_that("the series of medians being plotted are correctly calculated when flo # Summarises the column of medians into a singular median value result_median <- chart_result_data %>% + dplyr::filter(series == "y") %>% dplyr::filter(!is.na(median)) %>% dplyr::summarise(medi = median(median)) %>% dplyr::pull(medi) # Calculates the correct median from the data correct_median <- chart_result_data %>% + dplyr::filter(series == "y") %>% dplyr::slice((dplyr::n() - test_median_n + 1):dplyr::n()) %>% - dplyr::summarise(med = median(y, na.rm = TRUE)) %>% + dplyr::summarise(med = median(value, + na.rm = TRUE)) %>% dplyr::pull(med) # Test that the median displayed is calculated correctly @@ -80,6 +84,7 @@ test_that("the series of medians being plotted are correctly calculated when flo # Extract all median values from the result data auto_result_median <- auto_median_result_data %>% + dplyr::filter(series == "y") %>% dplyr::filter(!is.na(median)) %>% dplyr::pull(median) @@ -94,14 +99,16 @@ test_that("the series of medians being plotted are correctly calculated when flo # Summarises the column of medians into a singular median value auto_result_median <- auto_median_result_data %>% + dplyr::filter(series == "y") %>% dplyr::filter(!is.na(median)) %>% dplyr::summarise(med = median(median)) %>% dplyr::pull(med) # Calculates the correct median from the data correct_median_auto <- auto_median_result_data %>% + dplyr::filter(series == "y") %>% dplyr::slice((dplyr::n() - test_median_n + 1L):dplyr::n()) %>% - dplyr::summarise(med = median(y, na.rm = TRUE)) %>% + dplyr::summarise(med = median(value, na.rm = TRUE)) %>% dplyr::pull(med) # Test that the median displayed is calculated correctly @@ -153,6 +160,7 @@ test_that("NAs do not prevent median from being plotted",{ # Test it is not NA and has the correct value result_median <- chart_result_data %>% + dplyr::filter(series == "y") %>% dplyr::filter(!is.na(median)) %>% dplyr::summarise(medi = median(median)) %>% dplyr::pull(medi)