diff --git a/.codeocean/app-panel.json b/.codeocean/app-panel.json index 9939872..097ca34 100644 --- a/.codeocean/app-panel.json +++ b/.codeocean/app-panel.json @@ -12,56 +12,73 @@ } ], "categories": [ - { - "id": "7Rr6IxOMDucKMImp", - "name": "Input Data Parameters", - "description": "Options for defining input data", - "icon": "📁" - }, { "id": "EzTg1ivlFDHEy9PI", "name": "Basic", - "icon": "📂" + "icon": "\ud83d\udcc2" }, { "id": "FvI4Z2eb9sjL47Jt", "name": "Visualization", "description": "Visualization and plotting options", - "icon": "📊" + "icon": "\ud83d\udcca" + }, + { + "id": "7Rr6IxOMDucKMImp", + "name": "Advanced", + "description": "Advanced input data options", + "icon": "\ud83d\udcc1" } ], "parameters": [ { "id": "count_type_id", - "category": "FvI4Z2eb9sjL47Jt", + "category": "EzTg1ivlFDHEy9PI", "name": "Count type", "param_name": "count_type", "description": "Type of counts to use (e.g., filt, norm)", + "type": "list", + "value_type": "string", + "default_value": "filt", + "extra_data": [ + "raw", + "clean", + "filt", + "norm" + ] + }, + { + "id": "group_colname_id", + "category": "EzTg1ivlFDHEy9PI", + "name": "Group column name", + "param_name": "group_colname", + "description": "Column name for sample groups", + "type": "text", + "value_type": "string", + "default_value": "Group" + }, + { + "id": "label_colname_id", + "category": "EzTg1ivlFDHEy9PI", + "name": "Label column name", + "param_name": "label_colname", + "description": "Column name for sample labels", "type": "text", "value_type": "string", - "default_value": "filt" + "default_value": "Label" }, { "id": "sub_count_type_id", - "category": "FvI4Z2eb9sjL47Jt", + "category": "7Rr6IxOMDucKMImp", "name": "Sub count type", "param_name": "sub_count_type", "description": "Sub count type if count_type is a list", "type": "text", "value_type": "string" }, - { - "id": "feature_id_colname_id", - "category": "FvI4Z2eb9sjL47Jt", - "name": "Feature ID column name", - "param_name": "feature_id_colname", - "description": "Column name for feature IDs", - "type": "text", - "value_type": "string" - }, { "id": "sample_id_colname_id", - "category": "FvI4Z2eb9sjL47Jt", + "category": "EzTg1ivlFDHEy9PI", "name": "Sample ID column name", "param_name": "sample_id_colname", "description": "Column name for sample IDs", @@ -77,29 +94,9 @@ "type": "text", "value_type": "string" }, - { - "id": "group_colname_id", - "category": "7Rr6IxOMDucKMImp", - "name": "Group column name", - "param_name": "group_colname", - "description": "Column name for sample groups", - "type": "text", - "value_type": "string", - "default_value": "Group" - }, - { - "id": "label_colname_id", - "category": "7Rr6IxOMDucKMImp", - "name": "Label column name", - "param_name": "label_colname", - "description": "Column name for sample labels", - "type": "text", - "value_type": "string", - "default_value": "Label" - }, { "id": "principal_components_id", - "category": "EzTg1ivlFDHEy9PI", + "category": "7Rr6IxOMDucKMImp", "name": "Principal components", "param_name": "principal_components", "description": "Principal components to plot (comma-separated, e.g., 1,2)", @@ -107,15 +104,30 @@ "value_type": "string", "default_value": "1,2" }, + { + "id": "feature_id_colname_id", + "category": "7Rr6IxOMDucKMImp", + "name": "Feature ID column name", + "param_name": "feature_id_colname", + "description": "Column name for feature IDs", + "type": "text", + "value_type": "string" + }, { "id": "legend_position_id", "category": "FvI4Z2eb9sjL47Jt", "name": "Legend position", "param_name": "legend_position", "description": "Position of the legend", - "type": "text", + "type": "list", "value_type": "string", - "default_value": "top" + "default_value": "top", + "extra_data": [ + "top", + "bottom", + "left", + "right" + ] }, { "id": "point_size_id", @@ -125,7 +137,7 @@ "description": "Size of points in plot", "type": "text", "value_type": "string", - "default_value": "1" + "default_value": "3" }, { "id": "add_label_id", @@ -133,9 +145,13 @@ "name": "Add labels", "param_name": "add_label", "description": "Whether to add labels to points", - "type": "text", + "type": "list", "value_type": "string", - "default_value": "TRUE" + "default_value": "TRUE", + "extra_data": [ + "TRUE", + "FALSE" + ] }, { "id": "label_font_size_id", @@ -173,16 +189,21 @@ "name": "Interactive plots", "param_name": "interactive_plots", "description": "Whether to create interactive plots", - "type": "text", + "type": "list", "value_type": "string", - "default_value": "FALSE" + "default_value": "FALSE", + "extra_data": [ + "TRUE", + "FALSE" + ] }, { "id": "color_values_id", "category": "FvI4Z2eb9sjL47Jt", "name": "Color values", "param_name": "color_values", - "description": "Comma-separated color values for groups", + "description": "Comma-separated group colors. Defaults to the MOSuite palette.", + "help_text": "Use color names or hex codes. Supplied colors are used first; if there are more groups than colors, additional colors are generated from the MOSuite palette, with random colors only if that palette is too short.", "type": "text", "value_type": "string", "default_value": "#5954d6,#e1562c,#b80058,#00c6f8,#d163e6,#00a76c,#ff9287,#008cf9,#006e00,#796880,#FFA500,#878500" @@ -193,4 +214,4 @@ "file_name": "figures/pca/pca_2D.png" } ] -} \ No newline at end of file +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 033542a..d008cad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Code Ocean capsule - MOSuite - plot 2D PCA +## Development version + +- Align the PCA color defaults with MOSuite package defaults. +- Document that color palettes fall back to random colors only when too few colors are provided. +- Added tests for the plot pca 2D capsule Code Ocean panel and CLI contract (#1, @phoman14). +- Improved the Code Ocean parameter UI for the plot pca 2D capsule (#1, @phoman14). + ## v2.0 - Use MOSuite v0.3.0 @@ -9,4 +16,4 @@ Initial release - `bd6ca75` \ No newline at end of file + `bd6ca75` diff --git a/code/main.R b/code/main.R index 64d8eb1..89642f2 100644 --- a/code/main.R +++ b/code/main.R @@ -13,22 +13,97 @@ setup_capsule_environment() # parse CLI arguments parser <- ArgumentParser() -parser$add_argument("--count_type", type="character", default="filt") -parser$add_argument("--sub_count_type", type="character", default=NULL, help="Sub count type if count_type is a list") -parser$add_argument("--feature_id_colname", type="character", default=NULL, help="Column name for feature IDs") -parser$add_argument("--sample_id_colname", type="character", default=NULL, help="Column name for sample IDs") -parser$add_argument("--samples_to_rename", type="character", default="", help="Samples to rename in format old:new,old2:new2") -parser$add_argument("--group_colname", type="character", default="Group", help="Column name for sample groups") -parser$add_argument("--label_colname", type="character", default="Label", help="Column name for sample labels") -parser$add_argument("--principal_components", type="character", default="1,2", help="Principal components to plot (comma-separated)") -parser$add_argument("--legend_position", type="character", default="top", help="Position of the legend") -parser$add_argument("--point_size", type="double", default=1, help="Size of points in plot") -parser$add_argument("--add_label", type="logical", default=TRUE, help="Whether to add labels to points") -parser$add_argument("--label_font_size", type="double", default=3, help="Font size for labels") -parser$add_argument("--label_offset_x_", type="double", default=2, help="X-axis offset for labels") -parser$add_argument("--label_offset_y_", type="double", default=2, help="Y-axis offset for labels") -parser$add_argument("--interactive_plots", type="logical", default=FALSE, help="Whether to create interactive plots") -parser$add_argument("--color_values", type="character", default="#5954d6,#e1562c,#b80058,#00c6f8,#d163e6,#00a76c,#ff9287,#008cf9,#006e00,#796880,#FFA500,#878500", help="Comma-separated color values") +parser$add_argument("--count_type", type = "character", default = "filt") +parser$add_argument( + "--sub_count_type", + type = "character", + default = NULL, + help = "Sub count type if count_type is a list" +) +parser$add_argument( + "--feature_id_colname", + type = "character", + default = NULL, + help = "Column name for feature IDs" +) +parser$add_argument( + "--sample_id_colname", + type = "character", + default = NULL, + help = "Column name for sample IDs" +) +parser$add_argument( + "--samples_to_rename", + type = "character", + default = "", + help = "Samples to rename in format old:new,old2:new2" +) +parser$add_argument( + "--group_colname", + type = "character", + default = "Group", + help = "Column name for sample groups" +) +parser$add_argument( + "--label_colname", + type = "character", + default = "Label", + help = "Column name for sample labels" +) +parser$add_argument( + "--principal_components", + type = "character", + default = "1,2", + help = "Principal components to plot (comma-separated)" +) +parser$add_argument( + "--legend_position", + type = "character", + default = "top", + help = "Position of the legend" +) +parser$add_argument( + "--point_size", + type = "double", + default = 3, + help = "Size of points in plot" +) +parser$add_argument( + "--add_label", + type = "logical", + default = TRUE, + help = "Whether to add labels to points" +) +parser$add_argument( + "--label_font_size", + type = "double", + default = 3, + help = "Font size for labels" +) +parser$add_argument( + "--label_offset_x_", + type = "double", + default = 2, + help = "X-axis offset for labels" +) +parser$add_argument( + "--label_offset_y_", + type = "double", + default = 2, + help = "Y-axis offset for labels" +) +parser$add_argument( + "--interactive_plots", + type = "logical", + default = FALSE, + help = "Whether to create interactive plots" +) +parser$add_argument( + "--color_values", + type = "character", + default = "#5954d6,#e1562c,#b80058,#00c6f8,#d163e6,#00a76c,#ff9287,#008cf9,#006e00,#796880,#FFA500,#878500", + help = "Comma-separated group colors. Defaults to the MOSuite palette. Extra group colors are generated when needed." +) args <- parser$parse_args() @@ -37,21 +112,23 @@ moo <- load_moo_from_data_dir() # run MOSuite plot_pca_2d( - moo, - count_type = args$count_type, - sub_count_type = args$sub_count_type, - principal_components = as.integer(parse_optional_vector(args$principal_components)), - feature_id_colname = args$feature_id_colname, - sample_id_colname = args$sample_id_colname, - samples_to_rename = parse_samples_to_rename(args$samples_to_rename), - group_colname = args$group_colname, - label_colname = args$label_colname, - legend_position = args$legend_position, - point_size = args$point_size, - add_label = args$add_label, - label_font_size = args$label_font_size, - label_offset_x_ = args$label_offset_x_, - label_offset_y_ = args$label_offset_y_, - interactive_plots = args$interactive_plots, - color_values = parse_optional_vector(args$color_values) + moo, + count_type = args$count_type, + sub_count_type = args$sub_count_type, + principal_components = as.integer(parse_optional_vector( + args$principal_components + )), + feature_id_colname = args$feature_id_colname, + sample_id_colname = args$sample_id_colname, + samples_to_rename = parse_samples_to_rename(args$samples_to_rename), + group_colname = args$group_colname, + label_colname = args$label_colname, + legend_position = args$legend_position, + point_size = args$point_size, + add_label = args$add_label, + label_font_size = args$label_font_size, + label_offset_x_ = args$label_offset_x_, + label_offset_y_ = args$label_offset_y_, + interactive_plots = args$interactive_plots, + color_values = parse_optional_vector(args$color_values) ) diff --git a/tests/test-setup.R b/tests/test-setup.R new file mode 100644 index 0000000..dc1ce41 --- /dev/null +++ b/tests/test-setup.R @@ -0,0 +1,11 @@ +testthat::test_that("test setup has expected capsule files", { + repo_root <- normalizePath(file.path(testthat::test_path(), "..", "..")) + + testthat::expect_true(file.exists(file.path(repo_root, "code", "main.R"))) + testthat::expect_true(file.exists(file.path(repo_root, "code", "run"))) + testthat::expect_true(file.exists(file.path( + repo_root, + ".codeocean", + "app-panel.json" + ))) +}) diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..fa13c48 --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,3 @@ +library(testthat) + +test_dir(file.path("tests", "testthat")) diff --git a/tests/testthat/helper-panel.R b/tests/testthat/helper-panel.R new file mode 100644 index 0000000..fbf5a2d --- /dev/null +++ b/tests/testthat/helper-panel.R @@ -0,0 +1,76 @@ +repo_path <- function(...) { + file.path(normalizePath(file.path(testthat::test_path(), "..", "..")), ...) +} + +read_repo_file <- function(...) { + readLines(repo_path(...), warn = FALSE) +} + +extract_main_arguments <- function(main_lines) { + main_text <- paste(main_lines, collapse = "\n") + matches <- regmatches( + main_text, + gregexpr( + 'parser\\$add_argument\\(\\s*"--([[:alnum:]_]+)"', + main_text, + perl = TRUE + ) + )[[1]] + + sub('.*"--([[:alnum:]_]+)".*', "\\1", matches) +} + +extract_panel_param_names <- function(panel_lines) { + matches <- regmatches( + panel_lines, + gregexpr( + '"param_name"[[:space:]]*:[[:space:]]*"([[:alnum:]_]+)"', + panel_lines + ) + ) + matches <- unlist(matches, use.names = FALSE) + + sub( + '.*"param_name"[[:space:]]*:[[:space:]]*"([[:alnum:]_]+)".*', + "\\1", + matches + ) +} + +extract_panel_default <- function(panel_lines, param_name) { + param_line <- grep( + sprintf('"param_name"[[:space:]]*:[[:space:]]*"%s"', param_name), + panel_lines + ) + if (length(param_line) != 1) { + return(NA_character_) + } + + next_param <- grep('"param_name"[[:space:]]*:', panel_lines) + next_param <- next_param[next_param > param_line] + end_line <- if (length(next_param) > 0) { + next_param[[1]] - 1 + } else { + length(panel_lines) + } + block <- panel_lines[param_line:end_line] + default_line <- grep('"default_value"[[:space:]]*:', block, value = TRUE) + if (length(default_line) != 1) { + return(NA_character_) + } + + sub( + '.*"default_value"[[:space:]]*:[[:space:]]*"([^"]*)".*', + "\\1", + default_line + ) +} + +expect_same_values <- function(actual, expected, info = NULL) { + testthat::expect_setequal(actual, expected) + testthat::expect_equal( + length(actual), + length(unique(actual)), + info = "Values should not be duplicated" + ) +} diff --git a/tests/testthat/test-main.R b/tests/testthat/test-main.R new file mode 100644 index 0000000..b0c0746 --- /dev/null +++ b/tests/testthat/test-main.R @@ -0,0 +1,62 @@ +test_that("Code Ocean panel uses named parameters accepted by main.R", { + main_args <- extract_main_arguments(read_repo_file("code", "main.R")) + panel_lines <- read_repo_file(".codeocean", "app-panel.json") + panel_args <- extract_panel_param_names(panel_lines) + + expect_true( + any(grepl('"named_parameters"[[:space:]]*:[[:space:]]*true', panel_lines)), + info = "Code Ocean should pass parameters by name to main.R" + ) + expect_same_values( + panel_args, + main_args, + info = "Every app-panel param_name should match a main.R CLI argument" + ) +}) + +test_that("2D PCA capsule keeps expected PCA parameter contract", { + main_lines <- read_repo_file("code", "main.R") + panel_lines <- read_repo_file(".codeocean", "app-panel.json") + + shared_pca_args <- c( + "count_type", + "sub_count_type", + "feature_id_colname", + "sample_id_colname", + "samples_to_rename", + "group_colname", + "label_colname", + "principal_components", + "point_size", + "label_font_size", + "color_values" + ) + two_dimensional_args <- c( + "legend_position", + "add_label", + "label_offset_x_", + "label_offset_y_", + "interactive_plots" + ) + + expect_same_values( + extract_main_arguments(main_lines), + c(shared_pca_args, two_dimensional_args), + info = "2D PCA main.R should expose shared PCA args plus 2D-specific controls" + ) + expect_match(paste(main_lines, collapse = "\n"), "plot_pca_2d\\(") + expect_equal( + extract_panel_default(panel_lines, "principal_components"), + "1,2" + ) + expect_equal(extract_panel_default(panel_lines, "add_label"), "TRUE") + expect_equal(extract_panel_default(panel_lines, "interactive_plots"), "FALSE") +}) + +test_that("run wrapper prepares result directories and forwards CLI arguments", { + run_lines <- read_repo_file("code", "run") + run_text <- paste(run_lines, collapse = "\n") + + expect_match(run_text, "mkdir -p \\.\\./results/figures \\.\\./results/moo") + expect_match(run_text, 'Rscript main\\.R "\\$@"') +})