diff --git a/.github/workflows/atime.yaml b/.github/workflows/atime.yaml index 743886fb2..7743c7013 100644 --- a/.github/workflows/atime.yaml +++ b/.github/workflows/atime.yaml @@ -21,6 +21,8 @@ jobs: - uses: actions/checkout@v3 - uses: r-lib/actions/setup-r@v2 - uses: r-lib/actions/setup-r-dependencies@v2 + - name: install RJSONIO for testing old package versions + run: Rscript -e "install.packages('RJSONIO', repos='https://cloud.r-project.org')" - name: install package run: R CMD INSTALL . - uses: Anirban166/Autocomment-atime-results@v1.4.3 \ No newline at end of file diff --git a/DESCRIPTION b/DESCRIPTION index cdd8e0a33..2e71a75e5 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -76,7 +76,7 @@ Depends: Imports: servr, digest, - RJSONIO, + jsonlite, grid, gtable (>= 0.1.1), MASS, diff --git a/NAMESPACE b/NAMESPACE index 72f51e800..fea0f31b9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -503,7 +503,7 @@ export(xlim) export(ylab) export(ylim) export(zeroGrob) -import(RJSONIO) +import(jsonlite) import(data.table) import(grid) import(gtable) diff --git a/R/z_animint.R b/R/z_animint.R index 3beb7dd7c..10f64ba90 100644 --- a/R/z_animint.R +++ b/R/z_animint.R @@ -240,7 +240,7 @@ storeLayer <- function(meta, g, g.data.varied){ #' @param chromote_height height of chromote window in pixels, default 2000 should be sufficient for most data viz, but can be increased if your data viz screenshot appears cropped too small. #' @return invisible list of ggplots in list format. #' @export -#' @import RJSONIO +#' @import jsonlite #' @importFrom utils browseURL head packageVersion str tail #' write.table #' @example inst/examples/animint2dir.R @@ -637,7 +637,20 @@ animint2dir <- function export.data[[export.name]] <- meta[[export.name]] } } - json <- RJSONIO::toJSON(export.data) + ## Convert named vectors to lists for jsonlite compatibility (issue #193) + ## RJSONIO converts named vectors to JSON objects, jsonlite converts to arrays + ## This helper ensures jsonlite produces the same output as RJSONIO + convert_for_json <- function(x) { + if (is.list(x)) { + lapply(x, convert_for_json) + } else if (is.atomic(x) && !is.null(names(x))) { + as.list(x) + } else { + x + } + } + export.data <- convert_for_json(export.data) + json <- jsonlite::toJSON(export.data, auto_unbox = TRUE, force = TRUE, null = "null") cat(json, file = file.path(out.dir, "plot.json")) if (open.browser) { if (identical(getOption("animint.browser"),"browseURL")) { diff --git a/R/z_pages.R b/R/z_pages.R index e731a41c7..5b483491b 100644 --- a/R/z_pages.R +++ b/R/z_pages.R @@ -208,7 +208,7 @@ update_gallery <- function(gallery_path="~/R/gallery"){ } local.json <- tempfile() download.file(viz_url("plot.json"), local.json) - jlist <- RJSONIO::fromJSON(local.json) + jlist <- jsonlite::fromJSON(local.json, simplifyVector = FALSE) to.check <- c( source="URL of data viz source code", title="string describing the data viz") diff --git a/inst/examples/gps.R b/inst/examples/gps.R index a8d5fe3e7..aa8a0e2c7 100644 --- a/inst/examples/gps.R +++ b/inst/examples/gps.R @@ -61,7 +61,7 @@ if(!file.exists(ile_de_france.json)){ ile_de_france.json) download.file(u, ile_de_france.json) } -##ile_de_france.list <- RJSONIO::fromJSON(ile_de_france.json) +##ile_de_france.list <- jsonlite::fromJSON(ile_de_france.json) ile_de_france.sf <- geojsonsf::geojson_sf(ile_de_france.json) names(ile_de_france.sf) class(ile_de_france.sf) diff --git a/tests/testthat/test-compiler-json-migration.R b/tests/testthat/test-compiler-json-migration.R new file mode 100644 index 000000000..daefbab7c --- /dev/null +++ b/tests/testthat/test-compiler-json-migration.R @@ -0,0 +1,209 @@ +acontext("JSON migration validation for issue #193") +## This test validates jsonlite compatibility for RJSONIO migration. +## See https://github.com/animint/animint2/issues/193 +## RJSONIO is no longer maintained on CRAN, so we migrate to jsonlite. +## The key requirement: auto_unbox=TRUE makes jsonlite match RJSONIO behavior. +test_that("jsonlite encodes single values without array wrapping when auto_unbox=TRUE", { + data <- list(geom="point", size=3, enabled=TRUE) + json_str <- jsonlite::toJSON(data, auto_unbox=TRUE) + expect_false(grepl('\\["point"\\]', json_str)) + expect_false(grepl('\\[3\\]', json_str)) + expect_true(grepl('"geom"\\s*:\\s*"point"', json_str)) + expect_true(grepl('"size"\\s*:\\s*3', json_str)) +}) +test_that("jsonlite preserves vector arrays correctly", { + data <- list(x=c(1,2,3), labels=c("a","b","c")) + json_str <- jsonlite::toJSON(data, auto_unbox=TRUE) + parsed <- jsonlite::fromJSON(json_str, simplifyVector=FALSE) + expect_equal(length(parsed$x), 3) + expect_equal(length(parsed$labels), 3) + expect_equal(parsed$x[[1]], 1) + expect_equal(parsed$labels[[2]], "b") +}) +test_that("jsonlite handles nested list structures like plot.json geoms", { + geom_data <- list( + geom1_point_plot=list( + geom="point", + classed="geom1_point_plot", + aes=list(x="x", y="y"), + params=list(na.rm=FALSE, size=3), + chunks=1, + total=1 + ) + ) + json_str <- jsonlite::toJSON(geom_data, auto_unbox=TRUE) + parsed <- jsonlite::fromJSON(json_str, simplifyVector=FALSE) + expect_equal(parsed$geom1_point_plot$geom, "point") + expect_equal(parsed$geom1_point_plot$aes$x, "x") + expect_equal(parsed$geom1_point_plot$params$size, 3) + expect_equal(parsed$geom1_point_plot$chunks, 1) +}) +test_that("jsonlite handles plot layout data with boolean arrays", { + layout_data <- list( + PANEL=c("1","2","3","4"), + ROW=c(1,1,2,2), + COL=c(1,2,1,2), + AXIS_X=c(FALSE,FALSE,TRUE,TRUE), + AXIS_Y=c(TRUE,FALSE,TRUE,FALSE) + ) + json_str <- jsonlite::toJSON(layout_data, auto_unbox=TRUE) + parsed <- jsonlite::fromJSON(json_str, simplifyVector=FALSE) + expect_equal(length(parsed$PANEL), 4) + expect_equal(parsed$AXIS_X[[3]], TRUE) + expect_equal(parsed$AXIS_Y[[2]], FALSE) +}) +test_that("jsonlite handles axis tick data arrays", { + axis_data <- list( + x=c(2.5, 5, 7.5, 10), + xlab=c("2.5", "5.0", "7.5", "10.0"), + xrange=c(0.55, 10.45), + xline=TRUE, + xticks=TRUE + ) + json_str <- jsonlite::toJSON(axis_data, auto_unbox=TRUE) + parsed <- jsonlite::fromJSON(json_str, simplifyVector=FALSE) + expect_equal(length(parsed$x), 4) + expect_equal(parsed$xrange[[1]], 0.55) + expect_equal(parsed$xline, TRUE) +}) +test_that("jsonlite handles nested grid location arrays", { + grid_data <- list( + grid_major=list( + colour="#FFFFFF", + size=0.5, + loc=list( + x=list(c(2.5,5,7.5,10), c(2.5,5,7.5,10)), + y=list(c(2.5,5,7.5,10), c(2.5,5,7.5,10)) + ) + ) + ) + json_str <- jsonlite::toJSON(grid_data, auto_unbox=TRUE) + parsed <- jsonlite::fromJSON(json_str, simplifyVector=FALSE) + expect_equal(parsed$grid_major$colour, "#FFFFFF") + expect_equal(length(parsed$grid_major$loc$x), 2) + expect_equal(length(parsed$grid_major$loc$x[[1]]), 4) +}) +test_that("jsonlite handles selector structures for interactivity", { + selector_data <- list( + selectors=list( + year=list(selected="2000", type="single") + ), + first=list(year="2000"), + time=list(ms=2000, variable="year"), + duration=list(year=500) + ) + json_str <- jsonlite::toJSON(selector_data, auto_unbox=TRUE) + parsed <- jsonlite::fromJSON(json_str, simplifyVector=FALSE) + expect_equal(parsed$selectors$year$selected, "2000") + expect_equal(parsed$time$ms, 2000) + expect_equal(parsed$duration$year, 500) +}) +test_that("jsonlite handles empty lists and structures", { + data <- list(legend=list(), panel_border=list(), selectors=list()) + json_str <- jsonlite::toJSON(data, auto_unbox=TRUE) + parsed <- jsonlite::fromJSON(json_str, simplifyVector=FALSE) + expect_true("legend" %in% names(parsed)) + expect_true("panel_border" %in% names(parsed)) + expect_equal(length(parsed$legend), 0) +}) +test_that("jsonlite handles strip and facet data", { + strip_data <- list( + strips=list( + top=c("A","B","C","D"), + right=c(""), + n=list(top=2, right=0) + ) + ) + json_str <- jsonlite::toJSON(strip_data, auto_unbox=TRUE) + parsed <- jsonlite::fromJSON(json_str, simplifyVector=FALSE) + expect_equal(length(parsed$strips$top), 4) + expect_equal(parsed$strips$n$top, 2) +}) +test_that("jsonlite handles panel styling parameters", { + style_data <- list( + panel_background=list( + fill="#EBEBEB", + colour="transparent", + size=0.5, + linetype=1 + ) + ) + json_str <- jsonlite::toJSON(style_data, auto_unbox=TRUE) + parsed <- jsonlite::fromJSON(json_str, simplifyVector=FALSE) + expect_equal(parsed$panel_background$fill, "#EBEBEB") + expect_equal(parsed$panel_background$size, 0.5) +}) +test_that("jsonlite handles chunk_info metadata", { + chunk_data <- list( + chunk_info=list( + "geom1_point_chunk1.tsv"=list(bytes=258, rows=40) + ) + ) + json_str <- jsonlite::toJSON(chunk_data, auto_unbox=TRUE) + parsed <- jsonlite::fromJSON(json_str, simplifyVector=FALSE) + tsv_info <- parsed$chunk_info[["geom1_point_chunk1.tsv"]] + expect_equal(tsv_info$bytes, 258) + expect_equal(tsv_info$rows, 40) +}) +test_that("jsonlite round-trip preserves complete export.data structure", { + ## Mimics the export.data structure from R/z_animint.R line 631-639 + export_data <- list( + geoms=list( + geom1_point_scatter=list( + geom="point", + classed="geom1_point_scatter", + aes=list(x="x", y="y"), + params=list(na.rm=FALSE, size=3), + types=list(x="numeric", y="numeric"), + chunk_order=list(), + nest_order=c("PANEL"), + subset_order=c("PANEL"), + chunks=1, + total=1 + ) + ), + time=list(ms=2000, variable="year"), + duration=list(year=500), + selectors=list(year=list(selected="2000", type="single")), + plots=list( + scatter=list( + panel_margin_lines=0.25, + legend=list(), + xtitle="X Axis", + ytitle="Y Axis", + title="Test Plot", + options=list(width=600, height=400), + geoms=c("geom1_point_scatter") + ) + ) + ) + json_str <- jsonlite::toJSON(export_data, auto_unbox=TRUE) + parsed <- jsonlite::fromJSON(json_str, simplifyVector=FALSE) + expect_equal(parsed$geoms$geom1_point_scatter$geom, "point") + expect_equal(parsed$plots$scatter$title, "Test Plot") + expect_equal(parsed$time$ms, 2000) + expect_equal(parsed$selectors$year$selected, "2000") + expect_equal(parsed$plots$scatter$options$width, 600) +}) +test_that("jsonlite output is valid JSON parseable by JavaScript", { + ## This test ensures the JSON string format is valid + data <- list( + plot="scatter", + data=list(x=c(1,2,3), y=c(4,5,6)), + mapping=list(x="x", y="y") + ) + json_str <- jsonlite::toJSON(data, auto_unbox=TRUE) + ## Valid JSON should start with { and end with } + expect_true(grepl("^\\s*\\{", json_str)) + expect_true(grepl("\\}\\s*$", json_str)) + ## Should not have R-specific artifacts + expect_false(grepl("NA", json_str)) + expect_false(grepl("NULL", json_str)) +}) +test_that("jsonlite handles numeric precision for axis ranges", { + data <- list(xrange=c(0.55, 10.45), yrange=c(-3.14159, 2.71828)) + json_str <- jsonlite::toJSON(data, auto_unbox=TRUE) + parsed <- jsonlite::fromJSON(json_str, simplifyVector=FALSE) + expect_equal(parsed$xrange[[1]], 0.55, tolerance=1e-10) + expect_equal(parsed$yrange[[1]], -3.14159, tolerance=1e-5) +}) diff --git a/tests/testthat/test-renderer3-chunk-NA-separate-lines.R b/tests/testthat/test-renderer3-chunk-NA-separate-lines.R index 5c8f13310..133124897 100644 --- a/tests/testthat/test-renderer3-chunk-NA-separate-lines.R +++ b/tests/testthat/test-renderer3-chunk-NA-separate-lines.R @@ -76,7 +76,7 @@ test_that("geom2 common chunk with group=1 and color common", { geom1.dt <- fread(geom1.tsv) expect_equal(sum(is.na(geom1.dt)), 0) plot.json <- file.path("animint-htmltest", "plot.json") - json.list <- RJSONIO::fromJSON(plot.json) + json.list <- jsonlite::fromJSON(plot.json, simplifyVector = FALSE) group_num <- json.list$geoms$geom2_path_selected$chunks[["San Marcos"]] geom.tsv <- sprintf("animint-htmltest/geom2_path_selected_chunk%d.tsv", group_num) geom.dt <- fread(geom.tsv) @@ -125,7 +125,7 @@ test_that("geom2 common chunk with no group and color common", { geom1.dt <- fread(geom1.tsv) expect_equal(sum(is.na(geom1.dt)), 0) plot.json <- file.path("animint-htmltest", "plot.json") - json.list <- RJSONIO::fromJSON(plot.json) + json.list <- jsonlite::fromJSON(plot.json, simplifyVector = FALSE) group_num <- json.list$geoms$geom2_path_selected$chunks[["San Marcos"]] geom.tsv <- sprintf("animint-htmltest/geom2_path_selected_chunk%d.tsv", group_num) geom.dt <- fread(geom.tsv) @@ -176,7 +176,7 @@ test_that("geom2 common chunk ok with group=1 and only x common", { geom1.dt <- fread(geom1.tsv) expect_equal(sum(is.na(geom1.dt)), 0) plot.json <- file.path("animint-htmltest", "plot.json") - json.list <- RJSONIO::fromJSON(plot.json) + json.list <- jsonlite::fromJSON(plot.json, simplifyVector = FALSE) group_num <- json.list$geoms$geom2_path_selected$chunks[["San Marcos"]] geom.tsv <- sprintf("animint-htmltest/geom2_path_selected_chunk%d.tsv", group_num) geom.dt <- fread(geom.tsv)