From e2211ad262931f1e6f4bf849bb093a33855e7d05 Mon Sep 17 00:00:00 2001 From: Gauarv Chaudhary Date: Sun, 4 Jan 2026 21:43:54 +0530 Subject: [PATCH 01/10] test: add regression test for issue #279 facet_wrap spacing Adds comprehensive test cases to verify that facet_wrap spacing scales proportionally with custom height parameter. Tests cover: - Vertical facet layout (ncol=1) with 2x and 3x custom heights - 2x2 grid layout with 1.5x custom height - Multiple height values to verify proportionality This ensures issue #279 (excessive spacing below facets) does not regress. All tests pass (16 assertions). Refs #279 --- ...sue-279-facet-wrap-custom-height-spacing.R | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R diff --git a/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R b/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R new file mode 100644 index 000000000..4394cefaf --- /dev/null +++ b/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R @@ -0,0 +1,171 @@ +context("Issue #279: facet_wrap spacing with custom height") + +test_that("facet_wrap vertical layout height scales proportionally with custom height parameter (#279)", { + skip_on_cran() + + # Create simple test data with 2 facets for vertical layout + # This setup is most likely to show excessive spacing issues + test_data <- data.frame( + x = c(1, 5, 10, 1, 5, 10), + y = c(1, 5, 10, 1, 5, 10), + category = c("A", "A", "A", "B", "B", "B") + ) + + # Test Case 1: Default height (baseline for comparison) + viz_default <- ggplot() + + geom_point(aes(x, y), data = test_data, size = 3) + + facet_wrap(~category, ncol = 1) + # Vertical layout + ggtitle("Default height") + + # Test Case 2: Custom height (2x default) + viz_custom_800 <- ggplot() + + geom_point(aes(x, y), data = test_data, size = 3) + + facet_wrap(~category, ncol = 1) + + theme_animint(height = 800) + # 2x default 400 + ggtitle("Custom height 800") + + # Test Case 3: Custom height (3x default) + viz_custom_1200 <- ggplot() + + geom_point(aes(x, y), data = test_data, size = 3) + + facet_wrap(~category, ncol = 1) + + theme_animint(height = 1200) + # 3x default 400 + ggtitle("Custom height 1200") + + # Render all three visualizations + dir_default <- tempfile() + dir_800 <- tempfile() + dir_1200 <- tempfile() + + info_default <- animint2dir( + list(plot = viz_default), + out.dir = dir_default, + open.browser = FALSE + ) + + info_800 <- animint2dir( + list(plot = viz_custom_800), + out.dir = dir_800, + open.browser = FALSE + ) + + info_1200 <- animint2dir( + list(plot = viz_custom_1200), + out.dir = dir_1200, + open.browser = FALSE + ) + + # Read the generated plot.json files to verify height settings + json_default <- jsonlite::fromJSON(file.path(dir_default, "plot.json")) + json_800 <- jsonlite::fromJSON(file.path(dir_800, "plot.json")) + json_1200 <- jsonlite::fromJSON(file.path(dir_1200, "plot.json")) + + # Verify that custom heights are correctly stored in plot.json + expect_equal(json_800$plots$plot$options$height, 800) + expect_equal(json_1200$plots$plot$options$height, 1200) + + # The bug in issue #279 was: excessive vertical spacing below facets + # when using custom height != 400 (default) + # Expected behavior: spacing should scale proportionally, not excessively + + # We verify this indirectly by checking that the layout information + # is consistent across different height values + + # Check that panel layout is identical (same number of rows/cols) + expect_equal(json_default$plots$plot$layout$ROW, json_800$plots$plot$layout$ROW) + expect_equal(json_default$plots$plot$layout$ROW, json_1200$plots$plot$layout$ROW) + expect_equal(json_default$plots$plot$layout$COL, json_800$plots$plot$layout$COL) + expect_equal(json_default$plots$plot$layout$COL, json_1200$plots$plot$layout$COL) + + # Check that height_proportion values are identical + # (these control relative spacing, not absolute) + expect_equal(json_default$plots$plot$layout$height_proportion, + json_800$plots$plot$layout$height_proportion) + expect_equal(json_default$plots$plot$layout$height_proportion, + json_1200$plots$plot$layout$height_proportion) +}) + +test_that("facet_wrap grid layout (2x2) with custom height has proportional spacing (#279)", { + skip_on_cran() + + # Test with a 2x2 grid layout (similar to ROC example in issue) + test_data_grid <- data.frame( + x = rep(1:10, 4), + y = rep(1:10, 4), + category = rep(c("A", "B", "C", "D"), each = 10) + ) + + # Default height + viz_default <- ggplot() + + geom_point(aes(x, y), data = test_data_grid) + + facet_wrap(~category, ncol = 2) + + # Custom height (1.5x default) + viz_custom <- ggplot() + + geom_point(aes(x, y), data = test_data_grid) + + facet_wrap(~category, ncol = 2) + + theme_animint(height = 600) + + dir_default <- tempfile() + dir_custom <- tempfile() + + info_default <- animint2dir(list(plot = viz_default), out.dir = dir_default, open.browser = FALSE) + info_custom <- animint2dir(list(plot = viz_custom), out.dir = dir_custom, open.browser = FALSE) + + # Verify custom height is applied + json_custom <- jsonlite::fromJSON(file.path(dir_custom, "plot.json")) + expect_equal(json_custom$plots$plot$options$height, 600) + + # Check that layout proportions are consistent + json_default <- jsonlite::fromJSON(file.path(dir_default, "plot.json")) + expect_equal(json_default$plots$plot$layout$height_proportion, + json_custom$plots$plot$layout$height_proportion) + + # Verify we have 2 rows and 2 columns as expected + expect_equal(max(json_custom$plots$plot$layout$ROW), 2) + expect_equal(max(json_custom$plots$plot$layout$COL), 2) +}) + +test_that("facet_wrap with multiple custom height values maintains proportional spacing (#279)", { + skip_on_cran() + + # Test that spacing proportions are maintained across various custom heights + test_data <- data.frame( + x = c(1, 5, 10, 1, 5, 10), + y = c(1, 5, 10, 1, 5, 10), + category = c("X", "X", "X", "Y", "Y", "Y") + ) + + heights_to_test <- c(400, 600, 800, 1000, 1200) + layout_proportions <- list() + + for (i in seq_along(heights_to_test)) { + h <- heights_to_test[i] + + if (h == 400) { + # Default height + viz <- ggplot() + + geom_point(aes(x, y), data = test_data) + + facet_wrap(~category, ncol = 1) + } else { + # Custom height + viz <- ggplot() + + geom_point(aes(x, y), data = test_data) + + facet_wrap(~category, ncol = 1) + + theme_animint(height = h) + } + + test_dir <- tempfile() + info <- animint2dir(list(plot = viz), out.dir = test_dir, open.browser = FALSE) + + # Extract layout proportions + json <- jsonlite::fromJSON(file.path(test_dir, "plot.json")) + layout_proportions[[i]] <- json$plots$plot$layout$height_proportion + } + + # All layout proportions should be identical + # (height should scale, but proportions should remain constant) + for (i in 2:length(layout_proportions)) { + expect_equal(layout_proportions[[1]], layout_proportions[[i]], + info = sprintf("Height %d proportions differ from default (400)", heights_to_test[i])) + } +}) From a6599292fa9624951b5a2a64a6fea838ea672034 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Tue, 10 Mar 2026 21:00:18 +0530 Subject: [PATCH 02/10] fix #279: set SVG display:block to eliminate inline whitespace below facets; replace JSON-only test with browser-based regression test - Add .style("display","block") to SVG creation in animint.js so browsers do not add text-descender whitespace below inline SVG elements (fixes visible gap below facet_wrap/facet_grid at any custom height). - Replace the three animint2dir+JSON tests with a single animint2HTML test that renders in a real browser and asserts SVG height stays proportional (not height*num_facets) when using theme_animint(height=600) on a facet_grid(task_id ~ .) plot with sonar/spam/vowel/waveform/zip data, matching the ch20 examples that originally exposed the bug. --- inst/htmljs/animint.js | 3 +- ...sue-279-facet-wrap-custom-height-spacing.R | 197 +++--------------- 2 files changed, 36 insertions(+), 164 deletions(-) diff --git a/inst/htmljs/animint.js b/inst/htmljs/animint.js index bfcc4b07f..f49c74908 100644 --- a/inst/htmljs/animint.js +++ b/inst/htmljs/animint.js @@ -283,7 +283,8 @@ var animint = function (to_select, json_file) { var svg = tdLeft.append("svg") .attr("id", p_info.plot_id) .attr("height", p_info.options.height) - .attr("width", p_info.options.width); + .attr("width", p_info.options.width) + .style("display", "block"); // divvy up width/height based on the panel layout var nrows = Math.max.apply(null, p_info.layout.ROW); diff --git a/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R b/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R index 4394cefaf..08b91efbe 100644 --- a/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R +++ b/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R @@ -1,171 +1,42 @@ -context("Issue #279: facet_wrap spacing with custom height") +acontext("Issue #279: facet_grid spacing with custom height") -test_that("facet_wrap vertical layout height scales proportionally with custom height parameter (#279)", { +test_that("facet_grid SVG height is proportional to theme_animint height, no excess space (#279)", { skip_on_cran() - - # Create simple test data with 2 facets for vertical layout - # This setup is most likely to show excessive spacing issues - test_data <- data.frame( - x = c(1, 5, 10, 1, 5, 10), - y = c(1, 5, 10, 1, 5, 10), - category = c("A", "A", "A", "B", "B", "B") - ) - - # Test Case 1: Default height (baseline for comparison) - viz_default <- ggplot() + - geom_point(aes(x, y), data = test_data, size = 3) + - facet_wrap(~category, ncol = 1) + # Vertical layout - ggtitle("Default height") - - # Test Case 2: Custom height (2x default) - viz_custom_800 <- ggplot() + - geom_point(aes(x, y), data = test_data, size = 3) + - facet_wrap(~category, ncol = 1) + - theme_animint(height = 800) + # 2x default 400 - ggtitle("Custom height 800") - - # Test Case 3: Custom height (3x default) - viz_custom_1200 <- ggplot() + - geom_point(aes(x, y), data = test_data, size = 3) + - facet_wrap(~category, ncol = 1) + - theme_animint(height = 1200) + # 3x default 400 - ggtitle("Custom height 1200") - - # Render all three visualizations - dir_default <- tempfile() - dir_800 <- tempfile() - dir_1200 <- tempfile() - - info_default <- animint2dir( - list(plot = viz_default), - out.dir = dir_default, - open.browser = FALSE - ) - - info_800 <- animint2dir( - list(plot = viz_custom_800), - out.dir = dir_800, - open.browser = FALSE - ) - - info_1200 <- animint2dir( - list(plot = viz_custom_1200), - out.dir = dir_1200, - open.browser = FALSE + + task_data <- data.frame( + x = rep(1:5, 5), + y = rep(1:5, 5), + task_id = rep(c("sonar", "spam", "vowel", "waveform", "zip"), each = 5) ) - - # Read the generated plot.json files to verify height settings - json_default <- jsonlite::fromJSON(file.path(dir_default, "plot.json")) - json_800 <- jsonlite::fromJSON(file.path(dir_800, "plot.json")) - json_1200 <- jsonlite::fromJSON(file.path(dir_1200, "plot.json")) - - # Verify that custom heights are correctly stored in plot.json - expect_equal(json_800$plots$plot$options$height, 800) - expect_equal(json_1200$plots$plot$options$height, 1200) - - # The bug in issue #279 was: excessive vertical spacing below facets - # when using custom height != 400 (default) - # Expected behavior: spacing should scale proportionally, not excessively - - # We verify this indirectly by checking that the layout information - # is consistent across different height values - - # Check that panel layout is identical (same number of rows/cols) - expect_equal(json_default$plots$plot$layout$ROW, json_800$plots$plot$layout$ROW) - expect_equal(json_default$plots$plot$layout$ROW, json_1200$plots$plot$layout$ROW) - expect_equal(json_default$plots$plot$layout$COL, json_800$plots$plot$layout$COL) - expect_equal(json_default$plots$plot$layout$COL, json_1200$plots$plot$layout$COL) - - # Check that height_proportion values are identical - # (these control relative spacing, not absolute) - expect_equal(json_default$plots$plot$layout$height_proportion, - json_800$plots$plot$layout$height_proportion) - expect_equal(json_default$plots$plot$layout$height_proportion, - json_1200$plots$plot$layout$height_proportion) -}) -test_that("facet_wrap grid layout (2x2) with custom height has proportional spacing (#279)", { - skip_on_cran() - - # Test with a 2x2 grid layout (similar to ROC example in issue) - test_data_grid <- data.frame( - x = rep(1:10, 4), - y = rep(1:10, 4), - category = rep(c("A", "B", "C", "D"), each = 10) + viz_default <- list( + plot = ggplot() + + geom_point(aes(x, y), data = task_data) + + facet_grid(task_id ~ .) + + theme_bw() ) - - # Default height - viz_default <- ggplot() + - geom_point(aes(x, y), data = test_data_grid) + - facet_wrap(~category, ncol = 2) - - # Custom height (1.5x default) - viz_custom <- ggplot() + - geom_point(aes(x, y), data = test_data_grid) + - facet_wrap(~category, ncol = 2) + - theme_animint(height = 600) - - dir_default <- tempfile() - dir_custom <- tempfile() - - info_default <- animint2dir(list(plot = viz_default), out.dir = dir_default, open.browser = FALSE) - info_custom <- animint2dir(list(plot = viz_custom), out.dir = dir_custom, open.browser = FALSE) - - # Verify custom height is applied - json_custom <- jsonlite::fromJSON(file.path(dir_custom, "plot.json")) - expect_equal(json_custom$plots$plot$options$height, 600) - - # Check that layout proportions are consistent - json_default <- jsonlite::fromJSON(file.path(dir_default, "plot.json")) - expect_equal(json_default$plots$plot$layout$height_proportion, - json_custom$plots$plot$layout$height_proportion) - - # Verify we have 2 rows and 2 columns as expected - expect_equal(max(json_custom$plots$plot$layout$ROW), 2) - expect_equal(max(json_custom$plots$plot$layout$COL), 2) -}) -test_that("facet_wrap with multiple custom height values maintains proportional spacing (#279)", { - skip_on_cran() - - # Test that spacing proportions are maintained across various custom heights - test_data <- data.frame( - x = c(1, 5, 10, 1, 5, 10), - y = c(1, 5, 10, 1, 5, 10), - category = c("X", "X", "X", "Y", "Y", "Y") + viz_custom <- list( + plot = ggplot() + + geom_point(aes(x, y), data = task_data) + + facet_grid(task_id ~ .) + + theme_bw() + + theme_animint(height = 600) ) - - heights_to_test <- c(400, 600, 800, 1000, 1200) - layout_proportions <- list() - - for (i in seq_along(heights_to_test)) { - h <- heights_to_test[i] - - if (h == 400) { - # Default height - viz <- ggplot() + - geom_point(aes(x, y), data = test_data) + - facet_wrap(~category, ncol = 1) - } else { - # Custom height - viz <- ggplot() + - geom_point(aes(x, y), data = test_data) + - facet_wrap(~category, ncol = 1) + - theme_animint(height = h) - } - - test_dir <- tempfile() - info <- animint2dir(list(plot = viz), out.dir = test_dir, open.browser = FALSE) - - # Extract layout proportions - json <- jsonlite::fromJSON(file.path(test_dir, "plot.json")) - layout_proportions[[i]] <- json$plots$plot$layout$height_proportion - } - - # All layout proportions should be identical - # (height should scale, but proportions should remain constant) - for (i in 2:length(layout_proportions)) { - expect_equal(layout_proportions[[1]], layout_proportions[[i]], - info = sprintf("Height %d proportions differ from default (400)", heights_to_test[i])) - } + + info_default <- animint2HTML(viz_default) + info_custom <- animint2HTML(viz_custom) + + svg_default <- XML::getNodeSet(info_default$html, "//svg[contains(@id,'plot_plot')]") + expect_equal(length(svg_default), 1L) + h_default <- as.numeric(XML::xmlAttrs(svg_default[[1]])[["height"]]) + expect_lt(h_default, 400 * 2) + + svg_custom <- XML::getNodeSet(info_custom$html, "//svg[contains(@id,'plot_plot')]") + expect_equal(length(svg_custom), 1L) + h_custom <- as.numeric(XML::xmlAttrs(svg_custom[[1]])[["height"]]) + + expect_lt(h_custom, 600 * 2, + label = "SVG height should not be 600*num_facets — regression from issue #279") + expect_gt(h_custom, h_default) }) From 98f7efa622ea3397641964903b193310efdd84aa Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Thu, 19 Mar 2026 06:30:25 +0530 Subject: [PATCH 03/10] fix CI: use robust SVG id extraction to avoid matrix dimension error The display:block style added to SVGs causes sapply(svg.list, xmlAttrs) to return a list instead of a matrix when SVGs have inconsistent attribute counts. Use lapply/sapply with a function to extract id attributes safely regardless of attribute structure. --- tests/testthat/test-renderer3-knit-print.R | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/testthat/test-renderer3-knit-print.R b/tests/testthat/test-renderer3-knit-print.R index e732b42c8..ba69fb115 100644 --- a/tests/testthat/test-renderer3-knit-print.R +++ b/tests/testthat/test-renderer3-knit-print.R @@ -45,8 +45,9 @@ test_that("segments and breakpoints are rendered", { test_that("svg id property is unique", { svg.list <- getNodeSet(html, "//svg") - attr.mat <- sapply(svg.list, xmlAttrs) - id.counts <- table(attr.mat["id",]) + attr.list <- lapply(svg.list, xmlAttrs) + id.vec <- sapply(attr.list, function(a) a[["id"]]) + id.counts <- table(id.vec) expect_true(all(id.counts==1)) }) From e19b3b57884e381ace2bfd7764c9373c442b251e Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Tue, 26 May 2026 16:20:31 +0530 Subject: [PATCH 04/10] test: addresses Sir tdhock review comments for issue #279 - use facet_wrap instead of facet_grid (matches original bug report) - remove acontext() call as requested - remove empty lines inside test_that block - introduce n_facets variable and use 400*n_facets / 600*n_facets bounds to make the regression guard self-explanatory - refactor with viz_list + lapply to eliminate code duplication --- ...sue-279-facet-wrap-custom-height-spacing.R | 54 +++++++------------ 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R b/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R index 08b91efbe..e669096f9 100644 --- a/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R +++ b/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R @@ -1,42 +1,28 @@ -acontext("Issue #279: facet_grid spacing with custom height") - -test_that("facet_grid SVG height is proportional to theme_animint height, no excess space (#279)", { +test_that("facet_wrap SVG height is proportional to theme_animint height, no excess space (#279)", { skip_on_cran() - task_data <- data.frame( x = rep(1:5, 5), y = rep(1:5, 5), task_id = rep(c("sonar", "spam", "vowel", "waveform", "zip"), each = 5) ) - - viz_default <- list( - plot = ggplot() + - geom_point(aes(x, y), data = task_data) + - facet_grid(task_id ~ .) + - theme_bw() + n_facets <- length(unique(task_data$task_id)) + base_plot <- ggplot() + + geom_point(aes(x, y), data = task_data) + + facet_wrap(~ task_id, ncol = 1) + + theme_bw() + viz_list <- list( + default = list(plot = base_plot), + custom = list(plot = base_plot + theme_animint(height = 600)) ) - - viz_custom <- list( - plot = ggplot() + - geom_point(aes(x, y), data = task_data) + - facet_grid(task_id ~ .) + - theme_bw() + - theme_animint(height = 600) - ) - - info_default <- animint2HTML(viz_default) - info_custom <- animint2HTML(viz_custom) - - svg_default <- XML::getNodeSet(info_default$html, "//svg[contains(@id,'plot_plot')]") - expect_equal(length(svg_default), 1L) - h_default <- as.numeric(XML::xmlAttrs(svg_default[[1]])[["height"]]) - expect_lt(h_default, 400 * 2) - - svg_custom <- XML::getNodeSet(info_custom$html, "//svg[contains(@id,'plot_plot')]") - expect_equal(length(svg_custom), 1L) - h_custom <- as.numeric(XML::xmlAttrs(svg_custom[[1]])[["height"]]) - - expect_lt(h_custom, 600 * 2, + info_list <- lapply(viz_list, animint2HTML) + h_list <- lapply(info_list, function(info) { + svg_node <- XML::getNodeSet(info$html, "//svg[contains(@id,'plot_plot')]") + expect_equal(length(svg_node), 1L) + as.numeric(XML::xmlAttrs(svg_node[[1]])[["height"]]) + }) + # SVG height must not scale as height * n_facets (regression from issue #279) + expect_lt(h_list$default, 400 * n_facets) + expect_lt(h_list$custom, 600 * n_facets, label = "SVG height should not be 600*num_facets — regression from issue #279") - expect_gt(h_custom, h_default) -}) + expect_gt(h_list$custom, h_list$default) +}) \ No newline at end of file From c48f242bc49e160c1a5c0e6a8a9adb16dab3db10 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Tue, 26 May 2026 16:28:45 +0530 Subject: [PATCH 05/10] ci: trigger GitHub Actions From 411279698c9d706f87e6a740ea4f55976d362a2b Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Tue, 21 Jul 2026 23:25:50 +0530 Subject: [PATCH 06/10] test: address review feedback and guard display:block fix for #279 Restore test-renderer3-knit-print.R to master, remove skip_on_cran(), and assert plot SVG uses display:block via getComputedStyle. --- ...sue-279-facet-wrap-custom-height-spacing.R | 26 ++++++++++++++----- tests/testthat/test-renderer3-knit-print.R | 5 ++-- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R b/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R index e669096f9..629c4d7b3 100644 --- a/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R +++ b/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R @@ -1,5 +1,4 @@ test_that("facet_wrap SVG height is proportional to theme_animint height, no excess space (#279)", { - skip_on_cran() task_data <- data.frame( x = rep(1:5, 5), y = rep(1:5, 5), @@ -14,15 +13,28 @@ test_that("facet_wrap SVG height is proportional to theme_animint height, no exc default = list(plot = base_plot), custom = list(plot = base_plot + theme_animint(height = 600)) ) - info_list <- lapply(viz_list, animint2HTML) - h_list <- lapply(info_list, function(info) { + results <- lapply(viz_list, function(viz) { + info <- animint2HTML(viz) svg_node <- XML::getNodeSet(info$html, "//svg[contains(@id,'plot_plot')]") expect_equal(length(svg_node), 1L) - as.numeric(XML::xmlAttrs(svg_node[[1]])[["height"]]) + svg_attrs <- XML::xmlAttrs(svg_node[[1]]) + svg_id <- svg_attrs[["id"]] + list( + height = as.numeric(svg_attrs[["height"]]), + display = runtime_evaluate(sprintf( + "window.getComputedStyle(document.getElementById('%s')).display", svg_id)) + ) }) - # SVG height must not scale as height * n_facets (regression from issue #279) + expect_equal(results$default$display, "block", + label = "plot SVG should use display:block to prevent whitespace below it") + expect_equal(results$custom$display, "block", + label = "plot SVG should use display:block to prevent whitespace below it") + h_list <- lapply(results, `[[`, "height") expect_lt(h_list$default, 400 * n_facets) expect_lt(h_list$custom, 600 * n_facets, - label = "SVG height should not be 600*num_facets — regression from issue #279") + label = "SVG height should not be 600*num_facets — regression from issue #279") expect_gt(h_list$custom, h_list$default) -}) \ No newline at end of file + bottom_gap <- runtime_evaluate("(() => { const svg = document.querySelector(\"svg[id*='plot_plot']\"); const parent = svg.parentElement; return parent.getBoundingClientRect().bottom - svg.getBoundingClientRect().bottom; })()") + expect_lte(bottom_gap, 1, + label = "no excess rendered gap below plot SVG (issue #279)") +}) diff --git a/tests/testthat/test-renderer3-knit-print.R b/tests/testthat/test-renderer3-knit-print.R index ba69fb115..e732b42c8 100644 --- a/tests/testthat/test-renderer3-knit-print.R +++ b/tests/testthat/test-renderer3-knit-print.R @@ -45,9 +45,8 @@ test_that("segments and breakpoints are rendered", { test_that("svg id property is unique", { svg.list <- getNodeSet(html, "//svg") - attr.list <- lapply(svg.list, xmlAttrs) - id.vec <- sapply(attr.list, function(a) a[["id"]]) - id.counts <- table(id.vec) + attr.mat <- sapply(svg.list, xmlAttrs) + id.counts <- table(attr.mat["id",]) expect_true(all(id.counts==1)) }) From bdc834feb2cdd693db02eb8307e4eb6d4e9b3596 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Sat, 25 Jul 2026 10:47:24 +0530 Subject: [PATCH 07/10] fix: robust SVG id extraction after display:block attrs change sapply(xmlAttrs) no longer simplifies to a matrix when plot SVGs have style attributes from the issue #279 fix. Use getPropertyValue(). --- tests/testthat/test-renderer3-knit-print.R | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/testthat/test-renderer3-knit-print.R b/tests/testthat/test-renderer3-knit-print.R index e732b42c8..8d236a226 100644 --- a/tests/testthat/test-renderer3-knit-print.R +++ b/tests/testthat/test-renderer3-knit-print.R @@ -44,10 +44,9 @@ test_that("segments and breakpoints are rendered", { }) test_that("svg id property is unique", { - svg.list <- getNodeSet(html, "//svg") - attr.mat <- sapply(svg.list, xmlAttrs) - id.counts <- table(attr.mat["id",]) - expect_true(all(id.counts==1)) + id.vec <- getPropertyValue(html, "//svg", "id") + id.counts <- table(id.vec) + expect_equal(max(id.counts), 1L) }) all.list <- getNodeSet(html, "//*") From 0d1a83df01631eddfdb69df83bbc6207fce2bfc6 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Thu, 30 Jul 2026 13:34:30 +0530 Subject: [PATCH 08/10] test: use getStyleValue and get_element_bbox for issue #279 Replace custom runtime_evaluate JS with existing R helpers per review. Assert display:block and bottom_gap for both default and custom heights. --- ...sue-279-facet-wrap-custom-height-spacing.R | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R b/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R index 629c4d7b3..b88ce6b0c 100644 --- a/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R +++ b/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R @@ -5,36 +5,35 @@ test_that("facet_wrap SVG height is proportional to theme_animint height, no exc task_id = rep(c("sonar", "spam", "vowel", "waveform", "zip"), each = 5) ) n_facets <- length(unique(task_data$task_id)) + svg_xpath <- "//svg[@id='plot_plot']" + svg_sel <- "svg#plot_plot" + parent_sel <- "td:has(> svg#plot_plot)" base_plot <- ggplot() + geom_point(aes(x, y), data = task_data) + facet_wrap(~ task_id, ncol = 1) + theme_bw() viz_list <- list( default = list(plot = base_plot), - custom = list(plot = base_plot + theme_animint(height = 600)) + custom = list(plot = base_plot + theme_animint(height = 600)) ) results <- lapply(viz_list, function(viz) { info <- animint2HTML(viz) - svg_node <- XML::getNodeSet(info$html, "//svg[contains(@id,'plot_plot')]") + svg_node <- getNodeSet(info$html, svg_xpath) expect_equal(length(svg_node), 1L) - svg_attrs <- XML::xmlAttrs(svg_node[[1]]) - svg_id <- svg_attrs[["id"]] - list( - height = as.numeric(svg_attrs[["height"]]), - display = runtime_evaluate(sprintf( - "window.getComputedStyle(document.getElementById('%s')).display", svg_id)) - ) + svg_attrs <- xmlAttrs(svg_node[[1]]) + display <- getStyleValue(info$html, svg_xpath, "display") + expect_equal(display, "block", + label = "plot SVG should have inline display:block to prevent whitespace below it") + svg_box <- get_element_bbox(svg_sel) + parent_box <- get_element_bbox(parent_sel) + bottom_gap <- (parent_box$top + parent_box$height) - (svg_box$top + svg_box$height) + expect_lte(bottom_gap, 1, + label = "no excess rendered gap below plot SVG (issue #279)") + list(height = as.numeric(svg_attrs[["height"]])) }) - expect_equal(results$default$display, "block", - label = "plot SVG should use display:block to prevent whitespace below it") - expect_equal(results$custom$display, "block", - label = "plot SVG should use display:block to prevent whitespace below it") h_list <- lapply(results, `[[`, "height") expect_lt(h_list$default, 400 * n_facets) expect_lt(h_list$custom, 600 * n_facets, label = "SVG height should not be 600*num_facets — regression from issue #279") expect_gt(h_list$custom, h_list$default) - bottom_gap <- runtime_evaluate("(() => { const svg = document.querySelector(\"svg[id*='plot_plot']\"); const parent = svg.parentElement; return parent.getBoundingClientRect().bottom - svg.getBoundingClientRect().bottom; })()") - expect_lte(bottom_gap, 1, - label = "no excess rendered gap below plot SVG (issue #279)") }) From ab5483f70a6383b74b5212c30af12b86552b955c Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Fri, 21 Aug 2026 23:31:20 +0530 Subject: [PATCH 09/10] test: address tdhock review for issue #279 Use animint(), for loop, and named theme height constants. --- ...sue-279-facet-wrap-custom-height-spacing.R | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R b/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R index b88ce6b0c..86d8b217a 100644 --- a/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R +++ b/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R @@ -5,6 +5,8 @@ test_that("facet_wrap SVG height is proportional to theme_animint height, no exc task_id = rep(c("sonar", "spam", "vowel", "waveform", "zip"), each = 5) ) n_facets <- length(unique(task_data$task_id)) + default_theme_height <- 400 + custom_theme_height <- 600 svg_xpath <- "//svg[@id='plot_plot']" svg_sel <- "svg#plot_plot" parent_sel <- "td:has(> svg#plot_plot)" @@ -13,11 +15,12 @@ test_that("facet_wrap SVG height is proportional to theme_animint height, no exc facet_wrap(~ task_id, ncol = 1) + theme_bw() viz_list <- list( - default = list(plot = base_plot), - custom = list(plot = base_plot + theme_animint(height = 600)) + default = animint(plot = base_plot), + custom = animint(plot = base_plot + theme_animint(height = custom_theme_height)) ) - results <- lapply(viz_list, function(viz) { - info <- animint2HTML(viz) + heights <- list() + for (viz_name in names(viz_list)) { + info <- animint2HTML(viz_list[[viz_name]]) svg_node <- getNodeSet(info$html, svg_xpath) expect_equal(length(svg_node), 1L) svg_attrs <- xmlAttrs(svg_node[[1]]) @@ -29,11 +32,12 @@ test_that("facet_wrap SVG height is proportional to theme_animint height, no exc bottom_gap <- (parent_box$top + parent_box$height) - (svg_box$top + svg_box$height) expect_lte(bottom_gap, 1, label = "no excess rendered gap below plot SVG (issue #279)") - list(height = as.numeric(svg_attrs[["height"]])) - }) - h_list <- lapply(results, `[[`, "height") - expect_lt(h_list$default, 400 * n_facets) - expect_lt(h_list$custom, 600 * n_facets, - label = "SVG height should not be 600*num_facets — regression from issue #279") - expect_gt(h_list$custom, h_list$default) + heights[[viz_name]] <- as.numeric(svg_attrs[["height"]]) + } + expect_lt(heights$default, default_theme_height * n_facets) + expect_lt(heights$custom, custom_theme_height * n_facets, + label = sprintf( + "SVG height should not be %d*num_facets — regression from issue #279", + custom_theme_height)) + expect_gt(heights$custom, heights$default) }) From 15eb57ad3807b9aafd5129051fd742fe7ba67bb5 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Fri, 4 Sep 2026 23:13:27 +0530 Subject: [PATCH 10/10] fix #279: document SVG display:block and bump version Keep display:block on plot SVGs to remove inline baseline whitespace. Add NEWS entry and DESCRIPTION version for PR #288. --- DESCRIPTION | 2 +- NEWS.md | 4 ++++ inst/htmljs/animint.js | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index b8c2df633..3bc1148bf 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: animint2 Title: Animated Interactive Grammar of Graphics -Version: 2026.8.23 +Version: 2026.9.4 URL: https://animint.github.io/animint2 BugReports: https://github.com/animint/animint2/issues Authors@R: c( diff --git a/NEWS.md b/NEWS.md index b2d504529..95942701b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +# Changes in version 2026.9.4 (PR#288) + +- Plot SVGs use `display: block` so browsers do not leave baseline whitespace below the plot (issue #279). Thanks @ANAMASGARD. + # Changes in development (PR#339) - Multi-line facet strip labels: `facet_grid()` and `facet_wrap()` now render each facet variable on its own line, matching ggplot2 (issue #262). diff --git a/inst/htmljs/animint.js b/inst/htmljs/animint.js index 27b1e424f..1e6193cfa 100644 --- a/inst/htmljs/animint.js +++ b/inst/htmljs/animint.js @@ -402,6 +402,7 @@ var setMultilineText = function(textElement, text) { .attr("id", p_info.plot_id) .attr("height", p_info.options.height) .attr("width", p_info.options.width) + // block avoids inline SVG baseline/descender gap below the plot (issue #279) .style("display", "block"); // divvy up width/height based on the panel layout