diff --git a/R/config.R b/R/config.R index 5d43046..c113c29 100644 --- a/R/config.R +++ b/R/config.R @@ -1,3 +1,86 @@ +#' Helper function for processing coordination dictionaries +#' +#' @keywords internal +#' @param scopes The coordination scopes to process. +#' @param coordination_scopes The coordination scopes object to update. +#' @param coordination_scopes_by The coordination scopes by object to update. +#' @return A list containing updated coordination_scopes and coordination_scopes_by. +use_coordination_by_dict_helper <- function(scopes, coordination_scopes, coordination_scopes_by) { + # Recursive inner function + process_level <- function(parent_type, parent_scope, level_type, level_val) { + if(is.list(level_val) && !inherits(level_val, "VitessceConfigCoordinationScope")) { + # Check if this is a list of coordination objects + all_have_scope <- all(sapply(level_val, function(x) is.list(x) && "scope" %in% names(x))) + if(all_have_scope) { + # This is a list of coordination objects + if(is.null(coordination_scopes_by[[parent_type]])) coordination_scopes_by[[parent_type]] <- list() + if(is.null(coordination_scopes_by[[parent_type]][[level_type]])) coordination_scopes_by[[parent_type]][[level_type]] <- list() + coordination_scopes_by[[parent_type]][[level_type]][[parent_scope$c_scope]] <- sapply(level_val, function(child_val) child_val[["scope"]]$c_scope) + + for(child_val in level_val) { + if("children" %in% names(child_val)) { + # Continue recursion + for(next_level_type in names(child_val[["children"]])) { + next_level_val <- child_val[["children"]][[next_level_type]] + process_level(level_type, child_val[["scope"]], next_level_type, next_level_val) + } + } + } + } + } else { + # Single coordination object + if(is.null(coordination_scopes_by[[parent_type]])) coordination_scopes_by[[parent_type]] <- list() + if(is.null(coordination_scopes_by[[parent_type]][[level_type]])) coordination_scopes_by[[parent_type]][[level_type]] <- list() + coordination_scopes_by[[parent_type]][[level_type]][[parent_scope$c_scope]] <- level_val[["scope"]]$c_scope + + if("children" %in% names(level_val)) { + # Continue recursion + for(next_level_type in names(level_val[["children"]])) { + next_level_val <- level_val[["children"]][[next_level_type]] + process_level(level_type, level_val[["scope"]], next_level_type, next_level_val) + } + } + } + } + + # Process top-level coordination types + for(top_level_type in names(scopes)) { + top_level_val <- scopes[[top_level_type]] + + if(is.list(top_level_val) && !inherits(top_level_val, "VitessceConfigCoordinationScope")) { + # Check if this is a list of coordination objects + all_have_scope <- all(sapply(top_level_val, function(x) is.list(x) && "scope" %in% names(x))) + if(all_have_scope) { + coordination_scopes[[top_level_type]] <- sapply(top_level_val, function(level_val) level_val[["scope"]]$c_scope) + + for(level_val in top_level_val) { + if("children" %in% names(level_val)) { + # Begin recursion + for(next_level_type in names(level_val[["children"]])) { + next_level_val <- level_val[["children"]][[next_level_type]] + process_level(top_level_type, level_val[["scope"]], next_level_type, next_level_val) + } + } + } + } else { + # Single coordination object - handle the case where it's a simple list with scope + if("scope" %in% names(top_level_val)) { + coordination_scopes[[top_level_type]] <- top_level_val[["scope"]]$c_scope + if("children" %in% names(top_level_val)) { + # Begin recursion + for(next_level_type in names(top_level_val[["children"]])) { + next_level_val <- top_level_val[["children"]][[next_level_type]] + process_level(top_level_type, top_level_val[["scope"]], next_level_type, next_level_val) + } + } + } + } + } + } + + return(list(coordination_scopes = coordination_scopes, coordination_scopes_by = coordination_scopes_by)) +} + #' Get next scope name #' #' @keywords internal @@ -280,7 +363,7 @@ VitessceConfigMetaCoordinationScope <- R6::R6Class("VitessceConfigMetaCoordinati self$meta_by_scope = VitessceConfigCoordinationScope$new(CoordinationType$META_COORDINATION_SCOPES_BY, meta_by_scope) }, use_coordination = function(c_scopes) { - if(is.na(self$meta_scope4c_value)) { + if(is.na(self$meta_scope$c_value)) { self$meta_scope$set_value(obj_list()) } @@ -292,7 +375,23 @@ VitessceConfigMetaCoordinationScope <- R6::R6Class("VitessceConfigMetaCoordinati invisible(self) }, use_coordination_by_dict = function(scopes) { - # TODO + if(is.na(self$meta_scope$c_value)) { + self$meta_scope$set_value_raw(obj_list()) + } + + if(is.na(self$meta_by_scope$c_value)) { + self$meta_by_scope$set_value_raw(obj_list()) + } + + result <- use_coordination_by_dict_helper( + scopes, + self$meta_scope$c_value, + self$meta_by_scope$c_value + ) + + self$meta_scope$set_value_raw(result$coordination_scopes) + self$meta_by_scope$set_value_raw(result$coordination_scopes_by) + invisible(self) } ) @@ -384,11 +483,48 @@ VitessceConfigView <- R6::R6Class("VitessceConfigView", invisible(self) }, use_coordination_by_dict = function(scopes) { - # TODO + if(is.null(private$view$coordinationScopes)) { + private$view$coordinationScopes <- list() + } + + if(is.null(private$view$coordinationScopesBy)) { + private$view$coordinationScopesBy <- list() + } + + result <- use_coordination_by_dict_helper( + scopes, + private$view$coordinationScopes, + private$view$coordinationScopesBy + ) + + private$view$coordinationScopes <- result$coordination_scopes + private$view$coordinationScopesBy <- result$coordination_scopes_by + invisible(self) }, use_meta_coordination = function(meta_scope) { - # TODO + if(is.null(private$view$coordinationScopes)) { + private$view$coordinationScopes <- list() + } + + # Initialize as empty lists if they don't exist + if(is.null(private$view$coordinationScopes[[CoordinationType$META_COORDINATION_SCOPES]])) { + private$view$coordinationScopes[[CoordinationType$META_COORDINATION_SCOPES]] <- list() + } + if(is.null(private$view$coordinationScopes[[CoordinationType$META_COORDINATION_SCOPES_BY]])) { + private$view$coordinationScopes[[CoordinationType$META_COORDINATION_SCOPES_BY]] <- list() + } + + # Append the new meta scope values + private$view$coordinationScopes[[CoordinationType$META_COORDINATION_SCOPES]] <- c( + private$view$coordinationScopes[[CoordinationType$META_COORDINATION_SCOPES]], + meta_scope$meta_scope$c_scope + ) + private$view$coordinationScopes[[CoordinationType$META_COORDINATION_SCOPES_BY]] <- c( + private$view$coordinationScopes[[CoordinationType$META_COORDINATION_SCOPES_BY]], + meta_scope$meta_by_scope$c_scope + ) + invisible(self) }, #' @description @@ -557,13 +693,104 @@ VitessceConfig <- R6::R6Class("VitessceConfig", result }, add_meta_coordination = function() { - # TODO + prev_meta_scopes <- names(self$config$coordinationSpace[[CoordinationType$META_COORDINATION_SCOPES]]) + prev_meta_by_scopes <- names(self$config$coordinationSpace[[CoordinationType$META_COORDINATION_SCOPES_BY]]) + + if(is.null(prev_meta_scopes)) prev_meta_scopes <- character() + if(is.null(prev_meta_by_scopes)) prev_meta_by_scopes <- character() + + meta_container <- VitessceConfigMetaCoordinationScope$new( + get_next_scope(prev_meta_scopes), + get_next_scope(prev_meta_by_scopes) + ) + + if(!is.element(CoordinationType$META_COORDINATION_SCOPES, names(self$config$coordinationSpace))) { + self$config$coordinationSpace[[CoordinationType$META_COORDINATION_SCOPES]] <- list() + } + if(!is.element(CoordinationType$META_COORDINATION_SCOPES_BY, names(self$config$coordinationSpace))) { + self$config$coordinationSpace[[CoordinationType$META_COORDINATION_SCOPES_BY]] <- list() + } + + self$config$coordinationSpace[[CoordinationType$META_COORDINATION_SCOPES]][[meta_container$meta_scope$c_scope]] <- meta_container$meta_scope + self$config$coordinationSpace[[CoordinationType$META_COORDINATION_SCOPES_BY]][[meta_container$meta_by_scope$c_scope]] <- meta_container$meta_by_scope + + meta_container }, add_coordination_by_dict = function(input_val) { - # TODO + # Recursive function to process each level + process_level <- function(level) { + result <- list() + if(is.null(level)) { + return(result) + } + + for(c_type in names(level)) { + next_level_or_initial_value <- level[[c_type]] + + # Check if value is a CoordinationLevel instance + if(inherits(next_level_or_initial_value, "CoordinationLevel")) { + next_level <- next_level_or_initial_value$value + if(is.list(next_level)) { + if(next_level_or_initial_value$is_cached()) { + result[[c_type]] <- next_level_or_initial_value$get_cached() + } else { + processed_level <- lapply(next_level, function(next_el) { + dummy_scopes <- self$add_coordination(c_type) + dummy_scope <- dummy_scopes[[1]] + dummy_scope$set_value("__dummy__") + list( + scope = dummy_scope, + children = process_level(next_el) + ) + }) + next_level_or_initial_value$set_cached(processed_level) + result[[c_type]] <- processed_level + } + } else { + stop("Expected CoordinationLevel$value to be a list.") + } + } else { + # Base case + initial_value <- next_level_or_initial_value + if(inherits(initial_value, "VitessceConfigCoordinationScope")) { + result[[c_type]] <- list(scope = initial_value) + } else { + scopes <- self$add_coordination(c_type) + scope <- scopes[[1]] + if(is.list(initial_value)) { + scope$set_value_raw(initial_value) + } else { + scope$set_value(initial_value) + } + result[[c_type]] <- list(scope = scope) + } + } + } + return(result) + } + + # Begin recursion + output_val <- process_level(input_val) + return(output_val) }, - link_views_by_dict = function(views, input_val, meta = TRUE) { - # TODO + link_views_by_dict = function(views, input_val, meta = TRUE, scope_prefix = NA) { + # TODO: implement scope_prefix functionality similar to Python version + scopes <- self$add_coordination_by_dict(input_val) + + if(meta) { + meta_scope <- self$add_meta_coordination() + meta_scope$use_coordination_by_dict(scopes) + + for(view in views) { + view$use_meta_coordination(meta_scope) + } + } else { + for(view in views) { + view$use_coordination_by_dict(scopes) + } + } + + invisible(self) }, #' @description #' Define the layout of views. diff --git a/tests/testthat/test-config.R b/tests/testthat/test-config.R index c0c3e3c..cdf189c 100644 --- a/tests/testthat/test-config.R +++ b/tests/testthat/test-config.R @@ -423,3 +423,472 @@ test_that("VitessceConfig from list", { vc_list_orig[['coordinationSpace']][['spatialTargetX']][['A']] <- jsonlite::unbox(20) expect_equal(vc_list_loaded, vc_list_orig) }) + +test_that("VitessceConfig link_views_by_dict basic functionality", { + vc <- VitessceConfig$new(schema_version = "1.0.16", name = "Test config") + ds <- vc$add_dataset("Test dataset") + v1 <- vc$add_view(ds, "spatial") + v2 <- vc$add_view(ds, "scatterplot") + + # Test simple coordination with meta = FALSE + simple_input <- list() + simple_input[[CoordinationType$SPATIAL_ZOOM]] <- 2 + simple_input[[CoordinationType$SPATIAL_TARGET_X]] <- 0 + simple_input[[CoordinationType$SPATIAL_TARGET_Y]] <- 0 + + vc$link_views_by_dict(list(v1, v2), simple_input, meta = FALSE) + + vc_list <- vc$to_list() + + # Check that coordination scopes were created + expect_true("spatialZoom" %in% names(vc_list$coordinationSpace)) + expect_true("spatialTargetX" %in% names(vc_list$coordinationSpace)) + expect_true("spatialTargetY" %in% names(vc_list$coordinationSpace)) + + # Check that values were set correctly + zoom_scope_name <- names(vc_list$coordinationSpace$spatialZoom)[1] + expect_equal(vc_list$coordinationSpace$spatialZoom[[zoom_scope_name]], jsonlite::unbox(2)) + + x_scope_name <- names(vc_list$coordinationSpace$spatialTargetX)[1] + expect_equal(vc_list$coordinationSpace$spatialTargetX[[x_scope_name]], jsonlite::unbox(0)) + + y_scope_name <- names(vc_list$coordinationSpace$spatialTargetY)[1] + expect_equal(vc_list$coordinationSpace$spatialTargetY[[y_scope_name]], jsonlite::unbox(0)) + + # Check that views use the coordination scopes + expect_equal(vc_list$layout[[1]]$coordinationScopes$spatialZoom, zoom_scope_name) + expect_equal(vc_list$layout[[1]]$coordinationScopes$spatialTargetX, x_scope_name) + expect_equal(vc_list$layout[[1]]$coordinationScopes$spatialTargetY, y_scope_name) + + expect_equal(vc_list$layout[[2]]$coordinationScopes$spatialZoom, zoom_scope_name) + expect_equal(vc_list$layout[[2]]$coordinationScopes$spatialTargetX, x_scope_name) + expect_equal(vc_list$layout[[2]]$coordinationScopes$spatialTargetY, y_scope_name) +}) + +test_that("VitessceConfig link_views_by_dict with meta coordination", { + vc <- VitessceConfig$new(schema_version = "1.0.16", name = "Test config") + ds <- vc$add_dataset("Test dataset") + v1 <- vc$add_view(ds, "spatial") + v2 <- vc$add_view(ds, "scatterplot") + + # Test with meta coordination (default behavior) + simple_input <- list() + simple_input[[CoordinationType$SPATIAL_ZOOM]] <- 3 + + vc$link_views_by_dict(list(v1, v2), simple_input) # meta = TRUE by default + + vc_list <- vc$to_list() + + # Check that meta coordination scopes were created + expect_true("metaCoordinationScopes" %in% names(vc_list$coordinationSpace)) + expect_true("metaCoordinationScopesBy" %in% names(vc_list$coordinationSpace)) + + # Check that views use meta coordination + expect_true("metaCoordinationScopes" %in% names(vc_list$layout[[1]]$coordinationScopes)) + expect_true("metaCoordinationScopesBy" %in% names(vc_list$layout[[1]]$coordinationScopes)) + expect_true("metaCoordinationScopes" %in% names(vc_list$layout[[2]]$coordinationScopes)) + expect_true("metaCoordinationScopesBy" %in% names(vc_list$layout[[2]]$coordinationScopes)) +}) + +test_that("VitessceConfig add_coordination_by_dict", { + vc <- VitessceConfig$new(schema_version = "1.0.16", name = "Test config") + + # Test add_coordination_by_dict alone + input_val <- list() + input_val[[CoordinationType$SPATIAL_ZOOM]] <- 5 + input_val[[CoordinationType$SPATIAL_TARGET_X]] <- 10 + + result <- vc$add_coordination_by_dict(input_val) + + # Check structure of result + expect_true("spatialZoom" %in% names(result)) + expect_true("spatialTargetX" %in% names(result)) + expect_true("scope" %in% names(result$spatialZoom)) + expect_true("scope" %in% names(result$spatialTargetX)) + + # Check that scopes were created with correct values + expect_equal(as.numeric(result$spatialZoom$scope$c_value), 5) + expect_equal(as.numeric(result$spatialTargetX$scope$c_value), 10) + expect_equal(result$spatialZoom$scope$c_type, "spatialZoom") + expect_equal(result$spatialTargetX$scope$c_type, "spatialTargetX") +}) + +test_that("VitessceConfig add_meta_coordination", { + vc <- VitessceConfig$new(schema_version = "1.0.16", name = "Test config") + + meta_scope <- vc$add_meta_coordination() + + # Check that meta scope object was created + expect_true(inherits(meta_scope, "VitessceConfigMetaCoordinationScope")) + expect_true(inherits(meta_scope$meta_scope, "VitessceConfigCoordinationScope")) + expect_true(inherits(meta_scope$meta_by_scope, "VitessceConfigCoordinationScope")) + + # Check that coordination space was updated + vc_list <- vc$to_list() + expect_true("metaCoordinationScopes" %in% names(vc_list$coordinationSpace)) + expect_true("metaCoordinationScopesBy" %in% names(vc_list$coordinationSpace)) +}) + +test_that("VitessceConfig add_coordination_by_dict with hierarchical structure", { + vc <- VitessceConfig$new(schema_version = "1.0.16", name = "Test config") + + # Test complex hierarchical coordination using available coordination types + # Simulate the Python test structure with R-compatible types + scopes <- vc$add_coordination_by_dict(list( + spatialLayers = CL(list( + list( + geneFilter = list("BRCA1", "TP53"), + spatialZoom = 2.5, + cellFilter = CL(list( + list( + spatialTargetX = 100, + cellSetColor = list(255, 0, 0) + ), + list( + spatialTargetX = 200, + cellSetColor = list(0, 255, 0) + ) + )) + ) + )) + )) + + vc_list <- vc$to_list() + + # Check that the hierarchical structure was created + expect_true("spatialLayers" %in% names(vc_list$coordinationSpace)) + expect_true("geneFilter" %in% names(vc_list$coordinationSpace)) + expect_true("spatialZoom" %in% names(vc_list$coordinationSpace)) + expect_true("cellFilter" %in% names(vc_list$coordinationSpace)) + expect_true("spatialTargetX" %in% names(vc_list$coordinationSpace)) + expect_true("cellSetColor" %in% names(vc_list$coordinationSpace)) + + # Check that dummy values were set for list types + spatial_layers_scope <- names(vc_list$coordinationSpace$spatialLayers)[1] + expect_equal(vc_list$coordinationSpace$spatialLayers[[spatial_layers_scope]], jsonlite::unbox("__dummy__")) + + # Check that regular values were set correctly + zoom_scope <- names(vc_list$coordinationSpace$spatialZoom)[1] + expect_equal(vc_list$coordinationSpace$spatialZoom[[zoom_scope]], jsonlite::unbox(2.5)) + + # Check gene filter list + gene_scope <- names(vc_list$coordinationSpace$geneFilter)[1] + expect_equal(vc_list$coordinationSpace$geneFilter[[gene_scope]], list("BRCA1", "TP53")) + + # Check spatial target values + x_scopes <- names(vc_list$coordinationSpace$spatialTargetX) + expect_length(x_scopes, 2) + expect_equal(vc_list$coordinationSpace$spatialTargetX[[x_scopes[1]]], jsonlite::unbox(100)) + expect_equal(vc_list$coordinationSpace$spatialTargetX[[x_scopes[2]]], jsonlite::unbox(200)) + + # Check color values + color_scopes <- names(vc_list$coordinationSpace$cellSetColor) + expect_length(color_scopes, 2) + expect_equal(vc_list$coordinationSpace$cellSetColor[[color_scopes[1]]], list(255, 0, 0)) + expect_equal(vc_list$coordinationSpace$cellSetColor[[color_scopes[2]]], list(0, 255, 0)) +}) + +test_that("VitessceConfig add_and_use_coordination_by_dict", { + vc <- VitessceConfig$new(schema_version = "1.0.16", name = "My config") + dataset <- vc$add_dataset(name = "My dataset") + + # Add coordination first + color_scope_list <- vc$add_coordination("cellSetColor") + color_scope <- color_scope_list[[1]] + color_scope$set_value(list(255, 0, 0)) + + # Add hierarchical coordination using available R types + scopes <- vc$add_coordination_by_dict(list( + spatialLayers = CL(list( + list( + geneFilter = list("GENE1", "GENE2"), + spatialZoom = 1.5, + cellFilter = CL(list( + list( + spatialTargetX = 0, + cellSetColor = list(0, 255, 0) + ), + list( + spatialTargetX = 1, + cellSetColor = list(0, 0, 255) + ) + )) + ) + )), + cellHighlight = CL(list( + list( + geneSelection = list("GENE3"), + spatialTargetY = 10, + cellSetColor = color_scope + ) + )) + )) + + spatial_view <- vc$add_view("spatial", dataset = dataset) + spatial_view$use_coordination_by_dict(scopes) + + vc_list <- vc$to_list() + + # Check dataset + expect_equal(vc_list$datasets[[1]]$uid, "A") + expect_equal(vc_list$datasets[[1]]$name, "My dataset") + + # Check coordination space structure + expect_true("dataset" %in% names(vc_list$coordinationSpace)) + expect_true("spatialLayers" %in% names(vc_list$coordinationSpace)) + expect_true("geneFilter" %in% names(vc_list$coordinationSpace)) + expect_true("spatialZoom" %in% names(vc_list$coordinationSpace)) + expect_true("cellFilter" %in% names(vc_list$coordinationSpace)) + expect_true("spatialTargetX" %in% names(vc_list$coordinationSpace)) + expect_true("spatialTargetY" %in% names(vc_list$coordinationSpace)) + expect_true("cellSetColor" %in% names(vc_list$coordinationSpace)) + expect_true("cellHighlight" %in% names(vc_list$coordinationSpace)) + expect_true("geneSelection" %in% names(vc_list$coordinationSpace)) + + # Check specific values + expect_equal(vc_list$coordinationSpace$dataset[["A"]], jsonlite::unbox("A")) + + # Check that spatial layers has dummy value + spatial_layers_scope <- names(vc_list$coordinationSpace$spatialLayers)[1] + expect_equal(vc_list$coordinationSpace$spatialLayers[[spatial_layers_scope]], jsonlite::unbox("__dummy__")) + + # Check zoom value + zoom_scope <- names(vc_list$coordinationSpace$spatialZoom)[1] + expect_equal(vc_list$coordinationSpace$spatialZoom[[zoom_scope]], jsonlite::unbox(1.5)) + + # Check that color scope value is shared + color_scopes <- names(vc_list$coordinationSpace$cellSetColor) + expect_true(any(sapply(color_scopes, function(scope) { + identical(vc_list$coordinationSpace$cellSetColor[[scope]], list(255, 0, 0)) + }))) + + # Check layout - view should use the scopes + expect_equal(vc_list$layout[[1]]$component, "spatial") + expect_true("dataset" %in% names(vc_list$layout[[1]]$coordinationScopes)) + expect_equal(vc_list$layout[[1]]$coordinationScopes$dataset, "A") +}) + +test_that("VitessceConfig use_meta_complex_coordination", { + vc <- VitessceConfig$new(schema_version = "1.0.16", name = "My config") + dataset <- vc$add_dataset(name = "My dataset") + + # Create complex hierarchical coordination structure + scopes <- vc$add_coordination_by_dict(list( + spatialLayers = CL(list( + list( + geneFilter = list("GENE_A", "GENE_B"), + spatialZoom = 2.0, + cellFilter = CL(list( + list( + spatialTargetX = 0, + cellSetColor = list(255, 0, 0) + ), + list( + spatialTargetX = 1, + cellSetColor = list(0, 255, 0) + ) + )) + ) + )), + cellHighlight = CL(list( + list( + geneSelection = list("GENE_C"), + spatialTargetY = 5, + cellSetColor = list(255, 0, 0) + ) + )) + )) + + meta_coordination_scope <- vc$add_meta_coordination() + meta_coordination_scope$use_coordination_by_dict(scopes) + + spatial_view <- vc$add_view("spatial", dataset = dataset) + lc_view <- vc$add_view("layerController", dataset = dataset) + + spatial_view$use_meta_coordination(meta_coordination_scope) + lc_view$use_meta_coordination(meta_coordination_scope) + + vc_list <- vc$to_list() + + # Check basic structure + expect_equal(vc_list$version, "1.0.16") + expect_equal(vc_list$name, "My config") + expect_equal(vc_list$datasets[[1]]$uid, "A") + expect_equal(vc_list$datasets[[1]]$name, "My dataset") + + # Check coordination space + expect_true("dataset" %in% names(vc_list$coordinationSpace)) + expect_true("spatialLayers" %in% names(vc_list$coordinationSpace)) + expect_true("geneFilter" %in% names(vc_list$coordinationSpace)) + expect_true("spatialZoom" %in% names(vc_list$coordinationSpace)) + expect_true("cellFilter" %in% names(vc_list$coordinationSpace)) + expect_true("spatialTargetX" %in% names(vc_list$coordinationSpace)) + expect_true("spatialTargetY" %in% names(vc_list$coordinationSpace)) + expect_true("cellSetColor" %in% names(vc_list$coordinationSpace)) + expect_true("cellHighlight" %in% names(vc_list$coordinationSpace)) + expect_true("geneSelection" %in% names(vc_list$coordinationSpace)) + expect_true("metaCoordinationScopes" %in% names(vc_list$coordinationSpace)) + expect_true("metaCoordinationScopesBy" %in% names(vc_list$coordinationSpace)) + + # Check meta coordination structure + meta_scope_names <- names(vc_list$coordinationSpace$metaCoordinationScopes) + expect_length(meta_scope_names, 1) + meta_scope_content <- vc_list$coordinationSpace$metaCoordinationScopes[[meta_scope_names[1]]] + expect_true("spatialLayers" %in% names(meta_scope_content)) + expect_true("cellHighlight" %in% names(meta_scope_content)) + + # Check that views use meta coordination + expect_equal(vc_list$layout[[1]]$component, "spatial") + expect_true("metaCoordinationScopes" %in% names(vc_list$layout[[1]]$coordinationScopes)) + expect_true("metaCoordinationScopesBy" %in% names(vc_list$layout[[1]]$coordinationScopes)) + + expect_equal(vc_list$layout[[2]]$component, "layerController") + expect_true("metaCoordinationScopes" %in% names(vc_list$layout[[2]]$coordinationScopes)) + expect_true("metaCoordinationScopesBy" %in% names(vc_list$layout[[2]]$coordinationScopes)) +}) + +test_that("VitessceConfig link_views_by_dict complex hierarchical", { + vc <- VitessceConfig$new(schema_version = "1.0.16", name = "My config") + dataset <- vc$add_dataset(name = "My dataset") + + spatial_view <- vc$add_view("spatial", dataset = dataset) + lc_view <- vc$add_view("layerController", dataset = dataset) + + # Use complex hierarchical coordination linking + vc$link_views_by_dict(list(spatial_view, lc_view), list( + spatialLayers = CL(list( + list( + geneFilter = list("GENE_1", "GENE_2"), + spatialZoom = 3.0, + cellFilter = CL(list( + list( + spatialTargetX = 0, + cellSetColor = list(255, 0, 0) + ), + list( + spatialTargetX = 1, + cellSetColor = list(0, 255, 0) + ) + )) + ) + )), + cellHighlight = CL(list( + list( + geneSelection = list("GENE_3"), + spatialTargetY = 0, + cellSetColor = list(255, 0, 0) + ) + )) + )) + + vc_list <- vc$to_list() + + # Check structure matches expected format + expect_equal(vc_list$version, "1.0.16") + expect_equal(vc_list$name, "My config") + expect_equal(vc_list$datasets[[1]]$uid, "A") + expect_equal(vc_list$datasets[[1]]$name, "My dataset") + + # Check coordination space contains all expected types + expect_true("dataset" %in% names(vc_list$coordinationSpace)) + expect_true("spatialLayers" %in% names(vc_list$coordinationSpace)) + expect_true("geneFilter" %in% names(vc_list$coordinationSpace)) + expect_true("spatialZoom" %in% names(vc_list$coordinationSpace)) + expect_true("cellFilter" %in% names(vc_list$coordinationSpace)) + expect_true("spatialTargetX" %in% names(vc_list$coordinationSpace)) + expect_true("spatialTargetY" %in% names(vc_list$coordinationSpace)) + expect_true("cellSetColor" %in% names(vc_list$coordinationSpace)) + expect_true("cellHighlight" %in% names(vc_list$coordinationSpace)) + expect_true("geneSelection" %in% names(vc_list$coordinationSpace)) + expect_true("metaCoordinationScopes" %in% names(vc_list$coordinationSpace)) + expect_true("metaCoordinationScopesBy" %in% names(vc_list$coordinationSpace)) + + # Check specific values + zoom_scope <- names(vc_list$coordinationSpace$spatialZoom)[1] + expect_equal(vc_list$coordinationSpace$spatialZoom[[zoom_scope]], jsonlite::unbox(3.0)) + + gene_filter_scope <- names(vc_list$coordinationSpace$geneFilter)[1] + expect_equal(vc_list$coordinationSpace$geneFilter[[gene_filter_scope]], list("GENE_1", "GENE_2")) + + # Check that both views use meta coordination + expect_equal(vc_list$layout[[1]]$component, "spatial") + expect_true("metaCoordinationScopes" %in% names(vc_list$layout[[1]]$coordinationScopes)) + expect_true("metaCoordinationScopesBy" %in% names(vc_list$layout[[1]]$coordinationScopes)) + + expect_equal(vc_list$layout[[2]]$component, "layerController") + expect_true("metaCoordinationScopes" %in% names(vc_list$layout[[2]]$coordinationScopes)) + expect_true("metaCoordinationScopesBy" %in% names(vc_list$layout[[2]]$coordinationScopes)) +}) + +test_that("VitessceConfig link_views_by_dict with scope_prefix", { + vc <- VitessceConfig$new(schema_version = "1.0.16", name = "My config") + dataset <- vc$add_dataset(name = "My dataset") + + spatial_view <- vc$add_view("spatial", dataset = dataset) + lc_view <- vc$add_view("layerController", dataset = dataset) + + # Test with scope prefix like Python test + vc$link_views_by_dict(list(spatial_view, lc_view), list( + spatialLayers = CL(list( + list( + spatialZoom = 1.0, + cellFilter = CL(list( + list( + spatialTargetX = 0, + cellSetColor = list(255, 0, 0) + ), + list( + spatialTargetX = 1, + cellSetColor = list(0, 255, 0) + ) + )) + ) + )) + ), scope_prefix = "SOME_PREFIX_") + + vc_list <- vc$to_list() + + # Check structure + expect_equal(vc_list$version, "1.0.16") + expect_equal(vc_list$name, "My config") + expect_equal(vc_list$datasets[[1]]$uid, "A") + expect_equal(vc_list$datasets[[1]]$name, "My dataset") + + # Check coordination space with prefixes + expect_true("dataset" %in% names(vc_list$coordinationSpace)) + expect_true("spatialLayers" %in% names(vc_list$coordinationSpace)) + expect_true("spatialZoom" %in% names(vc_list$coordinationSpace)) + expect_true("cellFilter" %in% names(vc_list$coordinationSpace)) + expect_true("spatialTargetX" %in% names(vc_list$coordinationSpace)) + expect_true("cellSetColor" %in% names(vc_list$coordinationSpace)) + expect_true("metaCoordinationScopes" %in% names(vc_list$coordinationSpace)) + expect_true("metaCoordinationScopesBy" %in% names(vc_list$coordinationSpace)) + + # Check that scope names use the prefix + spatial_layers_scopes <- names(vc_list$coordinationSpace$spatialLayers) + expect_true(any(grepl("^SOME_PREFIX_", spatial_layers_scopes))) + + zoom_scopes <- names(vc_list$coordinationSpace$spatialZoom) + expect_true(any(grepl("^SOME_PREFIX_", zoom_scopes))) + + x_target_scopes <- names(vc_list$coordinationSpace$spatialTargetX) + expect_true(any(grepl("^SOME_PREFIX_", x_target_scopes))) + + color_scopes <- names(vc_list$coordinationSpace$cellSetColor) + expect_true(any(grepl("^SOME_PREFIX_", color_scopes))) + + # Check meta coordination scope names use prefix + meta_scopes <- names(vc_list$coordinationSpace$metaCoordinationScopes) + expect_true(any(grepl("^SOME_PREFIX_", meta_scopes))) + + # Check that views use the prefixed meta coordination + expect_equal(vc_list$layout[[1]]$component, "spatial") + expect_true("metaCoordinationScopes" %in% names(vc_list$layout[[1]]$coordinationScopes)) + meta_scope_used <- vc_list$layout[[1]]$coordinationScopes$metaCoordinationScopes[1] + expect_true(grepl("^SOME_PREFIX_", meta_scope_used)) + + expect_equal(vc_list$layout[[2]]$component, "layerController") + expect_true("metaCoordinationScopes" %in% names(vc_list$layout[[2]]$coordinationScopes)) + meta_scope_used_2 <- vc_list$layout[[2]]$coordinationScopes$metaCoordinationScopes[1] + expect_true(grepl("^SOME_PREFIX_", meta_scope_used_2)) +})