diff --git a/.codeocean/app-panel.json b/.codeocean/app-panel.json index 269c50d..0c25463 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,3)", @@ -107,6 +104,15 @@ "value_type": "string", "default_value": "1,2,3" }, + { + "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": "point_size_id", "category": "FvI4Z2eb9sjL47Jt", @@ -132,7 +138,8 @@ "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" @@ -153,4 +160,4 @@ "file_name": "figures/pca/pca_3D.html" } ] -} \ No newline at end of file +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e7b785..053213a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Code Ocean capsule - MOSuite - plot 3D PCA +## Development version + +- Align the PCA color and point size 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 3D capsule Code Ocean panel and CLI contract (#1, @phoman14). +- Improved the Code Ocean parameter UI for the plot pca 3D capsule (#1, @phoman14). + ## v2.0 - Use MOSuite v0.3.0. @@ -9,4 +16,4 @@ Initial release - \ No newline at end of file + diff --git a/code/main.R b/code/main.R index a290cd4..8831391 100644 --- a/code/main.R +++ b/code/main.R @@ -13,18 +13,73 @@ 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,3", help="Principal components to plot (comma-separated)") -parser$add_argument("--point_size", type="integer", default=8, help="Size of points in plot") -parser$add_argument("--label_font_size", type="integer", default=24, help="Font size for labels") -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("--plot_title", type="character", default="PCA 3D", help="Title for the plot") +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,3", + help = "Principal components to plot (comma-separated)" +) +parser$add_argument( + "--point_size", + type = "integer", + default = 8, + help = "Size of points in plot" +) +parser$add_argument( + "--label_font_size", + type = "integer", + default = 24, + help = "Font size for labels" +) +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." +) +parser$add_argument( + "--plot_title", + type = "character", + default = "PCA 3D", + help = "Title for the plot" +) args <- parser$parse_args() @@ -33,17 +88,19 @@ moo <- load_moo_from_data_dir() # run MOSuite plot_pca( - 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, - point_size = args$point_size, - label_font_size = args$label_font_size, - color_values = parse_optional_vector(args$color_values), - plot_title = args$plot_title + 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, + point_size = args$point_size, + label_font_size = args$label_font_size, + color_values = parse_optional_vector(args$color_values), + plot_title = args$plot_title ) 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..9ce5907 --- /dev/null +++ b/tests/testthat/test-main.R @@ -0,0 +1,57 @@ +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("3D 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" + ) + three_dimensional_args <- "plot_title" + + expect_same_values( + extract_main_arguments(main_lines), + c(shared_pca_args, three_dimensional_args), + info = "3D PCA main.R should expose shared PCA args plus 3D-specific controls" + ) + expect_match(paste(main_lines, collapse = "\n"), "plot_pca\\(") + expect_equal( + extract_panel_default(panel_lines, "principal_components"), + "1,2,3" + ) + expect_equal(extract_panel_default(panel_lines, "point_size"), "8") + expect_equal(extract_panel_default(panel_lines, "label_font_size"), "24") + expect_equal(extract_panel_default(panel_lines, "plot_title"), "PCA 3D") +}) + +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 "\\$@"') +})