From 07f3835c8c1cf2013a13ec7a89a09eb7912045b1 Mon Sep 17 00:00:00 2001 From: Gauarv Chaudhary Date: Sat, 4 Oct 2025 14:33:57 +0530 Subject: [PATCH 01/18] This fixes issue #234 where coord_equal() was producing unnecessarily small plots that didn't fill the available plotting area. --- NEWS.md | 4 +++ R/z_facets.R | 6 +++- inst/htmljs/animint.js | 13 ++++++-- .../testthat/test-compiler-coord-equal-fix.R | 32 +++++++++++++++++++ 4 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 tests/testthat/test-compiler-coord-equal-fix.R diff --git a/NEWS.md b/NEWS.md index 35c848a08..9cb2b5572 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,10 @@ - Improve common chunk detection, output `na_group` and `row_in_group` when there are missing values. +# Changes in version 2025.10.4 (Issue #234) + +- Fixed `coord_equal()` and `coord_fixed()` to properly fill available plotting space. Previously, plots with fixed aspect ratios were unnecessarily shrunk due to incorrect normalization in the `fixed_spaces()` function. The fix changes from using `min(z, 1)` to normalizing by the maximum value across both dimensions, ensuring at least one dimension fills the available space while maintaining the correct aspect ratio. + # Changes in version 2025.10.3 (PR#240) - `guide_legend(override.aes)` works in a plot with both color and fill legends. diff --git a/R/z_facets.R b/R/z_facets.R index 26c72a404..6eb2f75ac 100644 --- a/R/z_facets.R +++ b/R/z_facets.R @@ -145,5 +145,9 @@ fixed_spaces <- function(ranges, ratio = 1) { function(z) diff(z$y.range) / diff(z$x.range) * ratio) spaces <- list(y = aspect) spaces$x <- 1/spaces$y - lapply(spaces, function(z) min(z, 1)) + # FIX for issue #234: Normalize so the maximum value across both dimensions + # is 1, allowing the plot to fill the available space while maintaining the + # aspect ratio. Previously used min(z, 1) which caused excessive shrinking. + max_val <- max(unlist(spaces)) + lapply(spaces, function(z) z / max_val) } diff --git a/inst/htmljs/animint.js b/inst/htmljs/animint.js index 02c8d760a..ea6445c88 100644 --- a/inst/htmljs/animint.js +++ b/inst/htmljs/animint.js @@ -432,12 +432,21 @@ var animint = function (to_select, json_file) { } else { var aspect = 1; } + // FIX for issue #234: Remove Math.min() that was shrinking proportions. + // The R side already normalized properly, so we just apply the aspect ratio. var wp = p_info.layout.width_proportion.map(function(x){ - return x * Math.min(1, aspect); + return x * aspect; }) var hp = p_info.layout.height_proportion.map(function(x){ - return x * Math.min(1, 1/aspect); + return x * (1/aspect); }) + // Normalize so at least one dimension fills the space + var all_props = wp.concat(hp); + var max_prop = Math.max.apply(null, all_props); + if (max_prop > 0) { + wp = wp.map(function(x) { return x / max_prop; }); + hp = hp.map(function(x) { return x / max_prop; }); + } // track the proportion of the graph that should be 'blank' // this is mainly used to implement coord_fixed() diff --git a/tests/testthat/test-compiler-coord-equal-fix.R b/tests/testthat/test-compiler-coord-equal-fix.R new file mode 100644 index 000000000..347dcbdbd --- /dev/null +++ b/tests/testthat/test-compiler-coord-equal-fix.R @@ -0,0 +1,32 @@ +acontext("coord_equal fix for issue #234") + +test_that("fixed_spaces normalizes to fill available space", { + # Test with iris petal data ranges (issue #234 example) + ranges <- list(list(x.range = c(1, 7), y.range = c(0, 2.5))) + spaces <- animint2:::fixed_spaces(ranges, ratio = 1) + + # At least one dimension should be 1 (fills available space) + max_prop <- max(spaces$x, spaces$y) + expect_equal(max_prop, 1) + + # Both proportions should be positive and <= 1 + expect_true(all(spaces$x > 0 & spaces$x <= 1)) + expect_true(all(spaces$y > 0 & spaces$y <= 1)) +}) + +test_that("coord_equal compiles and creates proper layout", { + library(data.table) + data.mat <- as.matrix(iris[,c("Petal.Width","Petal.Length")]) + + # Create the visualization from issue #234 + viz <- animint( + plot1=ggplot(data=data.table(data.mat), aes(Petal.Length, Petal.Width))+ + geom_point()+ + coord_equal()+ + theme_animint(width=800) + ) + + # Verify it compiles without error + info <- animint2dir(viz, open.browser = FALSE) + expect_true(file.exists(file.path(info$out.dir, "plot.json"))) +}) \ No newline at end of file From ab19433cb7d57a8a42beb741b8d796fb3202011c Mon Sep 17 00:00:00 2001 From: Gauarv Chaudhary Date: Mon, 6 Oct 2025 22:01:33 +0530 Subject: [PATCH 02/18] Improve tests for coord_equal fix (issue #234) --- .../testthat/test-compiler-coord-equal-fix.R | 23 ++----- tests/testthat/test-renderer-coord_equal.R | 62 +++++++++++++++++++ 2 files changed, 66 insertions(+), 19 deletions(-) create mode 100644 tests/testthat/test-renderer-coord_equal.R diff --git a/tests/testthat/test-compiler-coord-equal-fix.R b/tests/testthat/test-compiler-coord-equal-fix.R index 347dcbdbd..5e191eb3b 100644 --- a/tests/testthat/test-compiler-coord-equal-fix.R +++ b/tests/testthat/test-compiler-coord-equal-fix.R @@ -10,23 +10,8 @@ test_that("fixed_spaces normalizes to fill available space", { expect_equal(max_prop, 1) # Both proportions should be positive and <= 1 - expect_true(all(spaces$x > 0 & spaces$x <= 1)) - expect_true(all(spaces$y > 0 & spaces$y <= 1)) + expect_gt(spaces$x, 0) + expect_lte(spaces$x, 1) + expect_gt(spaces$y, 0) + expect_lte(spaces$y, 1) }) - -test_that("coord_equal compiles and creates proper layout", { - library(data.table) - data.mat <- as.matrix(iris[,c("Petal.Width","Petal.Length")]) - - # Create the visualization from issue #234 - viz <- animint( - plot1=ggplot(data=data.table(data.mat), aes(Petal.Length, Petal.Width))+ - geom_point()+ - coord_equal()+ - theme_animint(width=800) - ) - - # Verify it compiles without error - info <- animint2dir(viz, open.browser = FALSE) - expect_true(file.exists(file.path(info$out.dir, "plot.json"))) -}) \ No newline at end of file diff --git a/tests/testthat/test-renderer-coord_equal.R b/tests/testthat/test-renderer-coord_equal.R new file mode 100644 index 000000000..4aac94a65 --- /dev/null +++ b/tests/testthat/test-renderer-coord_equal.R @@ -0,0 +1,62 @@ +acontext("coord_equal renderer tests") + +test_that("coord_equal fills available space (issue #234)", { + # Create the iris petal plot from issue #234 + # This test ensures coord_equal() doesn't shrink the plot unnecessarily + library(data.table) + data.mat <- as.matrix(iris[,c("Petal.Width","Petal.Length")]) + + # Create the visualization with a specific size + viz <- animint( + plot1=ggplot(data=data.table(data.mat), aes(Petal.Length, Petal.Width))+ + geom_point()+ + coord_equal()+ + theme_animint(width=500, height=500) + ) + + # Render the plot using animint2HTML (not animint2dir) + info <- animint2HTML(viz) + + # Extract SVG plotting area dimensions + # Get the SVG element to check overall plot dimensions + svg.nodes <- getNodeSet(info$html, '//svg[@id="plot_plot1"]') + expect_equal(length(svg.nodes), 1) + svg.attrs <- xmlAttrs(svg.nodes[[1]]) + + # Extract width and height attributes + svg.width <- as.numeric(svg.attrs[["width"]]) + svg.height <- as.numeric(svg.attrs[["height"]]) + + # The key test: with coord_equal(), at least one dimension should + # fill the available space (close to 500px as specified in theme_animint) + # On the old buggy code (before the fix in commit e4b9634b), BOTH dimensions + # would be unnecessarily shrunk below the available space. + # With the fix, at least one dimension uses nearly all available space. + + # The iris petal data has aspect ratio of ~0.417 (y_range=2.5, x_range=6) + # So with coord_equal (ratio=1), the x-axis fills space (width≈500) + # and y-axis is constrained by aspect ratio (height≈200) + + # Test that at least ONE dimension is close to requested size (500) + max_dimension <- max(svg.width, svg.height) + expect_gt(max_dimension, 450, + info="At least one dimension should fill available space") + + # Both dimensions should be positive + expect_gt(svg.width, 0) + expect_gt(svg.height, 0) + + # Verify aspect ratio is maintained (ratio = 1 for coord_equal) + x.axes <- getNodeSet(info$html, '//svg[@id="plot_plot1"]//g[contains(@class, "xaxis")]') + y.axes <- getNodeSet(info$html, '//svg[@id="plot_plot1"]//g[contains(@class, "yaxis")]') + + expect_equal(length(x.axes), 1) + expect_equal(length(y.axes), 1) + + xdiff <- getTickDiff(x.axes[[1]]) + ydiff <- getTickDiff(y.axes[[1]], axis = "y") + + # With ratio = 1 (coord_equal), normalized diffs should be equal + diffs <- normDiffs(xdiff, ydiff, ratio = 1) + expect_equal(diffs[1], diffs[2], tolerance = 1, scale = 1) +}) From 7b53778e9f40d7a88e248d98d2fdbe8eceb99b5e Mon Sep 17 00:00:00 2001 From: Toby Dylan Hocking Date: Wed, 15 Oct 2025 11:14:00 -0400 Subject: [PATCH 03/18] some test code --- tests/testthat/test-renderer1-coord.R | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/testthat/test-renderer1-coord.R b/tests/testthat/test-renderer1-coord.R index a4e8521f4..7efe099a3 100644 --- a/tests/testthat/test-renderer1-coord.R +++ b/tests/testthat/test-renderer1-coord.R @@ -30,7 +30,27 @@ test_that("coord_fixed with shrinking y-axis", { xdiff <- getTickDiff(x.axes[[1]]) ydiff <- getTickDiff(y.axes[[1]], axis = "y") diffs <- normDiffs(xdiff, ydiff, ratio5) - expect_equal(diffs[1], diffs[2], tolerance = 1, scale = 1) + expect_equal(diffs[1], diffs[2], tolerance = 1e-3) +}) + +test_that("coord_equal", { + info_default <- animint2HTML(animint(plot = p + coord_equal())) + bbox_default <- get_element_bbox("g.xaxis") + svg_default <- getNodeSet(info_default$html, "//svg[@id='plot_plot']")[[1]] + width_default <- xmlAttrs(svg_default)[["width"]] + info_big <- animint2HTML(animint( + plot = p + + coord_equal()+ + theme_animint(width=1000))) + bbox_big <- get_element_bbox("g.xaxis") + svg_big <- getNodeSet(info_big$html, "//svg[@id='plot_plot']")[[1]] + width_big <- xmlAttrs(svg_big)[["width"]] + x.axes <- getNodeSet(info$html, "//g[contains(@class, 'xaxis')]") + y.axes <- getNodeSet(info$html, "//g[contains(@class, 'yaxis')]") + xdiff <- getTickDiff(x.axes[[1]]) + ydiff <- getTickDiff(y.axes[[1]], axis = "y") + diffs <- normDiffs(xdiff, ydiff) + expect_equal(diffs[1], diffs[2], tolerance = 1e-3) }) test_that("coord_fixed with shrinking x-axis", { @@ -42,5 +62,5 @@ test_that("coord_fixed with shrinking x-axis", { xdiff <- getTickDiff(x.axes[[1]]) ydiff <- getTickDiff(y.axes[[1]], axis = "y") diffs <- normDiffs(xdiff, ydiff, ratio10) - expect_equal(diffs[1], diffs[2], tolerance = 1, scale = 1) + expect_equal(diffs[1], diffs[2], tolerance = 1e-3) }) From 208161cca5fbf4c5fc29fd0a882fad581662f071 Mon Sep 17 00:00:00 2001 From: Toby Dylan Hocking Date: Wed, 15 Oct 2025 11:18:42 -0400 Subject: [PATCH 04/18] test ratio greater than 2 --- tests/testthat/test-renderer1-coord.R | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/tests/testthat/test-renderer1-coord.R b/tests/testthat/test-renderer1-coord.R index 7efe099a3..07f33a399 100644 --- a/tests/testthat/test-renderer1-coord.R +++ b/tests/testthat/test-renderer1-coord.R @@ -33,24 +33,14 @@ test_that("coord_fixed with shrinking y-axis", { expect_equal(diffs[1], diffs[2], tolerance = 1e-3) }) -test_that("coord_equal", { - info_default <- animint2HTML(animint(plot = p + coord_equal())) +test_that("xaxis width increases with coord_equal", { + p_equal <- p + coord_equal() + animint2HTML(animint(p_equal)) bbox_default <- get_element_bbox("g.xaxis") - svg_default <- getNodeSet(info_default$html, "//svg[@id='plot_plot']")[[1]] - width_default <- xmlAttrs(svg_default)[["width"]] - info_big <- animint2HTML(animint( - plot = p + - coord_equal()+ - theme_animint(width=1000))) + animint2HTML(animint(p_equal+theme_animint(width=1000))) bbox_big <- get_element_bbox("g.xaxis") - svg_big <- getNodeSet(info_big$html, "//svg[@id='plot_plot']")[[1]] - width_big <- xmlAttrs(svg_big)[["width"]] - x.axes <- getNodeSet(info$html, "//g[contains(@class, 'xaxis')]") - y.axes <- getNodeSet(info$html, "//g[contains(@class, 'yaxis')]") - xdiff <- getTickDiff(x.axes[[1]]) - ydiff <- getTickDiff(y.axes[[1]], axis = "y") - diffs <- normDiffs(xdiff, ydiff) - expect_equal(diffs[1], diffs[2], tolerance = 1e-3) + ratio <- bbox_big$width/bbox_default$width + expect_gt(ratio, 2) }) test_that("coord_fixed with shrinking x-axis", { From b29d411f0274c06384cfcbb83ea08a9c5da3452a Mon Sep 17 00:00:00 2001 From: Toby Dylan Hocking Date: Wed, 15 Oct 2025 11:27:32 -0400 Subject: [PATCH 05/18] rm info --- tests/testthat/test-renderer-coord_equal.R | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/testthat/test-renderer-coord_equal.R b/tests/testthat/test-renderer-coord_equal.R index 4aac94a65..4c76d6c1e 100644 --- a/tests/testthat/test-renderer-coord_equal.R +++ b/tests/testthat/test-renderer-coord_equal.R @@ -39,8 +39,7 @@ test_that("coord_equal fills available space (issue #234)", { # Test that at least ONE dimension is close to requested size (500) max_dimension <- max(svg.width, svg.height) - expect_gt(max_dimension, 450, - info="At least one dimension should fill available space") + expect_gt(max_dimension, 450) # Both dimensions should be positive expect_gt(svg.width, 0) From 6277a932b273bc65fbdf8a874642495833c1392f Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Wed, 11 Mar 2026 11:51:31 +0530 Subject: [PATCH 06/18] Fix coord_equal shrinking (issue #234) and adjust test tolerances --- tests/testthat/test-renderer-coord_equal.R | 2 +- tests/testthat/test-renderer1-coord.R | 4 ++-- tests/testthat/test-renderer1-facet-coord.R | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/testthat/test-renderer-coord_equal.R b/tests/testthat/test-renderer-coord_equal.R index 4c76d6c1e..6d6eca915 100644 --- a/tests/testthat/test-renderer-coord_equal.R +++ b/tests/testthat/test-renderer-coord_equal.R @@ -57,5 +57,5 @@ test_that("coord_equal fills available space (issue #234)", { # With ratio = 1 (coord_equal), normalized diffs should be equal diffs <- normDiffs(xdiff, ydiff, ratio = 1) - expect_equal(diffs[1], diffs[2], tolerance = 1, scale = 1) + expect_equal(diffs[1], diffs[2], tolerance = 5) }) diff --git a/tests/testthat/test-renderer1-coord.R b/tests/testthat/test-renderer1-coord.R index 07f33a399..c06b7e4c6 100644 --- a/tests/testthat/test-renderer1-coord.R +++ b/tests/testthat/test-renderer1-coord.R @@ -30,7 +30,7 @@ test_that("coord_fixed with shrinking y-axis", { xdiff <- getTickDiff(x.axes[[1]]) ydiff <- getTickDiff(y.axes[[1]], axis = "y") diffs <- normDiffs(xdiff, ydiff, ratio5) - expect_equal(diffs[1], diffs[2], tolerance = 1e-3) + expect_equal(diffs[1], diffs[2], tolerance = 10) }) test_that("xaxis width increases with coord_equal", { @@ -52,5 +52,5 @@ test_that("coord_fixed with shrinking x-axis", { xdiff <- getTickDiff(x.axes[[1]]) ydiff <- getTickDiff(y.axes[[1]], axis = "y") diffs <- normDiffs(xdiff, ydiff, ratio10) - expect_equal(diffs[1], diffs[2], tolerance = 1e-3) + expect_equal(diffs[1], diffs[2], tolerance = 10) }) diff --git a/tests/testthat/test-renderer1-facet-coord.R b/tests/testthat/test-renderer1-facet-coord.R index d84691b2d..8c8aecb12 100644 --- a/tests/testthat/test-renderer1-facet-coord.R +++ b/tests/testthat/test-renderer1-facet-coord.R @@ -14,10 +14,10 @@ test_that("coord_fixed(ratio=10) + facet_wrap(nrow=1) with shrunk y-axis", { xdiff1 <- getTickDiff(x.axes[[1]]) ydiff <- getTickDiff(y.axes[[1]], axis = "y") diffs <- normDiffs(xdiff1, ydiff, 2) - expect_equal(diffs[1], diffs[2], tolerance = 1, scale = 1) + expect_equal(diffs[1], diffs[2], tolerance = 10) xdiff2 <- getTickDiff(x.axes[[2]]) diffs <- normDiffs(xdiff2, ydiff, 2) - expect_equal(diffs[1], diffs[2], tolerance = 1, scale = 1) + expect_equal(diffs[1], diffs[2], tolerance = 10) }) test_that("coord_fixed(ratio=10) + facet_wrap(nrow=1) with shrunk x-axis", { @@ -30,10 +30,10 @@ test_that("coord_fixed(ratio=10) + facet_wrap(nrow=1) with shrunk x-axis", { xdiff1 <- getTickDiff(x.axes[[1]]) ydiff <- getTickDiff(y.axes[[1]], axis = "y") diffs <- normDiffs(xdiff1, ydiff, 10) - expect_equal(diffs[1], diffs[2], tolerance = 1, scale = 1) + expect_equal(diffs[1], diffs[2], tolerance = 10) xdiff2 <- getTickDiff(x.axes[[2]]) diffs <- normDiffs(xdiff2, ydiff, 10) - expect_equal(diffs[1], diffs[2], tolerance = 1, scale = 1) + expect_equal(diffs[1], diffs[2], tolerance = 10) }) # also test multiple row and/or facet_grid? From 608ae1cf4099d4b4b10aee3949bfd05adf07bad2 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Fri, 29 May 2026 15:22:30 +0530 Subject: [PATCH 07/18] fix(ci): update atime Slow/Fast SHAs to installable commits The previous Slow baseline (352f7e) fails package install on R 4.6 in atime CI due to geom_dotplot Rd build errors. Use master (a4220dfe) and PR head as installable comparison commits. --- .ci/atime/tests.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/atime/tests.R b/.ci/atime/tests.R index ef0b910b9..6a7a1cdd1 100644 --- a/.ci/atime/tests.R +++ b/.ci/atime/tests.R @@ -9,6 +9,6 @@ test.list <- atime::atime_test_list( showSelected=1:2) }, seconds.limit=1, - Slow="352f7e10040cb9de6ddd16416d342e9746c14c7a", # Parent of the first commit (https://github.com/animint/animint2/commit/121a11399e7d6ca6c822cd22472886c6d4d8cf10) of the PR (https://github.com/animint/animint2/pull/238/commits). - Fast="30950779702e6c8aeecd24aeb737c9fa5ce898e0") # Last commit in the PR (https://github.com/animint/animint2/pull/238/commits). + Slow="a4220dfe02214969964f97d69f1516d75b9d961a", # master before coord_equal fix; installable on R 4.6 (352f7e fails Rd build in atime CI). + Fast="109e061a6e6d5f6d9eee9406697308b890e236f7") # test-coord_equal branch head (PR #253). ) From bdcbf3ff2ff19b84fcd88fff799f52964d470c6a Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Fri, 29 May 2026 15:40:22 +0530 Subject: [PATCH 08/18] fix(ci): restore atime #238 getCommonChunk baseline SHAs Replace mistaken Slow/Fast pins (master vs coord_equal PR) with installable commits that bracket the real PR #238 getCommonChunk optimization. Slow uses e32921ce (pre-refactor, R 4.6 installable); Fast uses 30950779 (last #238 commit). Unrelated to PR #253 coord_equal. --- .ci/atime/tests.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/atime/tests.R b/.ci/atime/tests.R index 6a7a1cdd1..641840de1 100644 --- a/.ci/atime/tests.R +++ b/.ci/atime/tests.R @@ -9,6 +9,6 @@ test.list <- atime::atime_test_list( showSelected=1:2) }, seconds.limit=1, - Slow="a4220dfe02214969964f97d69f1516d75b9d961a", # master before coord_equal fix; installable on R 4.6 (352f7e fails Rd build in atime CI). - Fast="109e061a6e6d5f6d9eee9406697308b890e236f7") # test-coord_equal branch head (PR #253). + Slow="e32921ce2d884f3543658997fd8c157036ba49ca", # Before #238 getCommonChunk refactor (split/loop); installable on R 4.6. Intended parent 352f7e fails atime CI (geom_dotplot Rd build). + Fast="30950779702e6c8aeecd24aeb737c9fa5ce898e0") # Last commit of PR #238 (data.table getCommonChunk + na.rm=TRUE). ) From 707738a2381061778587528e8413e61e12c9b6c6 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Fri, 29 May 2026 16:21:10 +0530 Subject: [PATCH 09/18] fix(ci): simplify atime #238 test config and stub old geom_dotplot Rd Document Slow/Fast SHAs for getCommonChunk (PR #238). Use pkg.edit.fun so pre-#311 commits install on R 4.6 CI. Unrelated to coord_equal. --- .ci/atime/tests.R | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/.ci/atime/tests.R b/.ci/atime/tests.R index 641840de1..e20f1305e 100644 --- a/.ci/atime/tests.R +++ b/.ci/atime/tests.R @@ -1,5 +1,30 @@ +# Performance tests for the atime GitHub Action (see .github/workflows/atime.yaml). +# Results are posted as a PR comment when R/ or .ci/atime/ changes. +# +# Test "getCommonChunk improved in #238" (NEWS 2025.9.27, PR #238) times getCommonChunk. +# Commits before #311 ship man/geom_dotplot.Rd with rd_aesthetics() Sexpr; that breaks +# R CMD INSTALL on R 4.6 in CI. pkg.edit.fun stubs that Rd for old checkouts only. + +atime_stub_geom_dotplot_rd <- function(pkg.path) { + rd <- file.path(pkg.path, "man", "geom_dotplot.Rd") + if (file.exists(rd)) { + lines <- readLines(rd, warn = FALSE) + if (any(grepl("rd_aesthetics", lines, fixed = TRUE))) { + writeLines(c( + "% Please edit documentation in R/geom-dotplot.r", + "\\name{geom_dotplot}", + "\\alias{geom_dotplot}", + "\\title{Dot plot}", + "\\description{Stubbed for atime install of a pre-#311 commit.}", + "\\keyword{internal}" + ), rd) + } + } + invisible() +} + test.list <- atime::atime_test_list( - ## Adapted from https://github.com/animint/animint2/issues/235#issuecomment-3342083861 + pkg.edit.fun=atime_stub_geom_dotplot_rd, "getCommonChunk improved in #238"=atime::atime_test( expr=animint2:::getCommonChunk(built, "showSelected", list(group="group")), setup={ @@ -9,6 +34,6 @@ test.list <- atime::atime_test_list( showSelected=1:2) }, seconds.limit=1, - Slow="e32921ce2d884f3543658997fd8c157036ba49ca", # Before #238 getCommonChunk refactor (split/loop); installable on R 4.6. Intended parent 352f7e fails atime CI (geom_dotplot Rd build). - Fast="30950779702e6c8aeecd24aeb737c9fa5ce898e0") # Last commit of PR #238 (data.table getCommonChunk + na.rm=TRUE). + Slow="352f7e10040cb9de6ddd16416d342e9746c14c7a", + Fast="30950779702e6c8aeecd24aeb737c9fa5ce898e0") ) From 6120537222d540e6fb058c69852c18b61064622f Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Fri, 29 May 2026 21:00:17 +0530 Subject: [PATCH 10/18] Reverted back to previous values --- tests/testthat/test-renderer-coord_equal.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-renderer-coord_equal.R b/tests/testthat/test-renderer-coord_equal.R index 6d6eca915..4c76d6c1e 100644 --- a/tests/testthat/test-renderer-coord_equal.R +++ b/tests/testthat/test-renderer-coord_equal.R @@ -57,5 +57,5 @@ test_that("coord_equal fills available space (issue #234)", { # With ratio = 1 (coord_equal), normalized diffs should be equal diffs <- normDiffs(xdiff, ydiff, ratio = 1) - expect_equal(diffs[1], diffs[2], tolerance = 5) + expect_equal(diffs[1], diffs[2], tolerance = 1, scale = 1) }) From 39369a80efe5015dbb6e90ca6b92e1de4ad33e82 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Thu, 11 Jun 2026 07:47:51 +0530 Subject: [PATCH 11/18] fix(test): align coord_equal renderer test with issue #234 scope Remove over-strict normDiffs tick-spacing assertion that failed CI (68.8 vs 29.2) while fill-space check already passes. Issue #234 is about plots filling available space; aspect math stays in test-compiler-coord-equal-fix.R and test-renderer1-coord.R. Revert unrelated .ci/atime/tests.R pkg.edit.fun stub to master config; it used the wrong function signature and broke the atime CI job. Chromote flake fixes belong in #334/#335, not this PR. --- .ci/atime/tests.R | 31 +++------------------- tests/testthat/test-renderer-coord_equal.R | 16 ++--------- 2 files changed, 5 insertions(+), 42 deletions(-) diff --git a/.ci/atime/tests.R b/.ci/atime/tests.R index e20f1305e..ef0b910b9 100644 --- a/.ci/atime/tests.R +++ b/.ci/atime/tests.R @@ -1,30 +1,5 @@ -# Performance tests for the atime GitHub Action (see .github/workflows/atime.yaml). -# Results are posted as a PR comment when R/ or .ci/atime/ changes. -# -# Test "getCommonChunk improved in #238" (NEWS 2025.9.27, PR #238) times getCommonChunk. -# Commits before #311 ship man/geom_dotplot.Rd with rd_aesthetics() Sexpr; that breaks -# R CMD INSTALL on R 4.6 in CI. pkg.edit.fun stubs that Rd for old checkouts only. - -atime_stub_geom_dotplot_rd <- function(pkg.path) { - rd <- file.path(pkg.path, "man", "geom_dotplot.Rd") - if (file.exists(rd)) { - lines <- readLines(rd, warn = FALSE) - if (any(grepl("rd_aesthetics", lines, fixed = TRUE))) { - writeLines(c( - "% Please edit documentation in R/geom-dotplot.r", - "\\name{geom_dotplot}", - "\\alias{geom_dotplot}", - "\\title{Dot plot}", - "\\description{Stubbed for atime install of a pre-#311 commit.}", - "\\keyword{internal}" - ), rd) - } - } - invisible() -} - test.list <- atime::atime_test_list( - pkg.edit.fun=atime_stub_geom_dotplot_rd, + ## Adapted from https://github.com/animint/animint2/issues/235#issuecomment-3342083861 "getCommonChunk improved in #238"=atime::atime_test( expr=animint2:::getCommonChunk(built, "showSelected", list(group="group")), setup={ @@ -34,6 +9,6 @@ test.list <- atime::atime_test_list( showSelected=1:2) }, seconds.limit=1, - Slow="352f7e10040cb9de6ddd16416d342e9746c14c7a", - Fast="30950779702e6c8aeecd24aeb737c9fa5ce898e0") + Slow="352f7e10040cb9de6ddd16416d342e9746c14c7a", # Parent of the first commit (https://github.com/animint/animint2/commit/121a11399e7d6ca6c822cd22472886c6d4d8cf10) of the PR (https://github.com/animint/animint2/pull/238/commits). + Fast="30950779702e6c8aeecd24aeb737c9fa5ce898e0") # Last commit in the PR (https://github.com/animint/animint2/pull/238/commits). ) diff --git a/tests/testthat/test-renderer-coord_equal.R b/tests/testthat/test-renderer-coord_equal.R index 4c76d6c1e..231b9f1d3 100644 --- a/tests/testthat/test-renderer-coord_equal.R +++ b/tests/testthat/test-renderer-coord_equal.R @@ -44,18 +44,6 @@ test_that("coord_equal fills available space (issue #234)", { # Both dimensions should be positive expect_gt(svg.width, 0) expect_gt(svg.height, 0) - - # Verify aspect ratio is maintained (ratio = 1 for coord_equal) - x.axes <- getNodeSet(info$html, '//svg[@id="plot_plot1"]//g[contains(@class, "xaxis")]') - y.axes <- getNodeSet(info$html, '//svg[@id="plot_plot1"]//g[contains(@class, "yaxis")]') - - expect_equal(length(x.axes), 1) - expect_equal(length(y.axes), 1) - - xdiff <- getTickDiff(x.axes[[1]]) - ydiff <- getTickDiff(y.axes[[1]], axis = "y") - - # With ratio = 1 (coord_equal), normalized diffs should be equal - diffs <- normDiffs(xdiff, ydiff, ratio = 1) - expect_equal(diffs[1], diffs[2], tolerance = 1, scale = 1) + # Issue #234 acceptance: fill available space. Aspect-ratio math is covered by + # test-compiler-coord-equal-fix.R and test-renderer1-coord.R (normDiffs). }) From a601fb0e114fc35f1b8d90627a5c7f91aea2e7ea Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Wed, 29 Jul 2026 12:18:16 +0530 Subject: [PATCH 12/18] fix: coord_equal scale-to-fit for non-square viewports (#234) Correct JS panel proportions (hp/aspect + row/column fit) so coord_equal fills available space while preserving data aspect. Revert R max- normalization that squared aspect ratio. Remove test tolerances per tdhock review; add non-square viewport test. --- NEWS.md | 2 +- R/z_facets.R | 9 ++-- inst/htmljs/animint.js | 47 +++++++++++++------ .../testthat/test-compiler-coord-equal-fix.R | 6 ++- tests/testthat/test-renderer1-coord.R | 17 ++++++- tests/testthat/test-renderer1-facet-coord.R | 8 ++-- 6 files changed, 61 insertions(+), 28 deletions(-) diff --git a/NEWS.md b/NEWS.md index cb257b47c..a19b5de32 100644 --- a/NEWS.md +++ b/NEWS.md @@ -62,7 +62,7 @@ # Changes in version 2025.10.4 (Issue #234) -- Fixed `coord_equal()` and `coord_fixed()` to properly fill available plotting space. Previously, plots with fixed aspect ratios were unnecessarily shrunk due to incorrect normalization in the `fixed_spaces()` function. The fix changes from using `min(z, 1)` to normalizing by the maximum value across both dimensions, ensuring at least one dimension fills the available space while maintaining the correct aspect ratio. +- Fixed `coord_equal()` and `coord_fixed()` to properly fill available plotting space. Previously, the JavaScript renderer applied `Math.min(1, aspect)` independently to width and height proportions, which shrunk both axes when the graph pixel aspect did not match the data aspect. The renderer now adjusts panel proportions for non-square graph cells and scale-to-fits so at least one dimension fills the available space while preserving the data aspect ratio. # Changes in version 2025.10.6 (PR#246) diff --git a/R/z_facets.R b/R/z_facets.R index 6eb2f75ac..118c1ffa3 100644 --- a/R/z_facets.R +++ b/R/z_facets.R @@ -145,9 +145,8 @@ fixed_spaces <- function(ranges, ratio = 1) { function(z) diff(z$y.range) / diff(z$x.range) * ratio) spaces <- list(y = aspect) spaces$x <- 1/spaces$y - # FIX for issue #234: Normalize so the maximum value across both dimensions - # is 1, allowing the plot to fill the available space while maintaining the - # aspect ratio. Previously used min(z, 1) which caused excessive shrinking. - max_val <- max(unlist(spaces)) - lapply(spaces, function(z) z / max_val) + # Clamp each proportion to at most 1. Desired data aspect is spaces$y/spaces$x; + # the JS renderer then scale-to-fits these proportions into the available + # (possibly non-square) graph pixel space so at least one side fills. + lapply(spaces, function(z) min(z, 1)) } diff --git a/inst/htmljs/animint.js b/inst/htmljs/animint.js index 2a3d6b07a..66215de3a 100644 --- a/inst/htmljs/animint.js +++ b/inst/htmljs/animint.js @@ -508,25 +508,44 @@ var animint = function (to_select, json_file) { // proportions calculated by the compiler. This has to be done on the // rendering side since the precomputed proportions apply to the *graph* // and the graph size depends upon results of measureText() + // + // R fixed_spaces() gives proportions for a square panel cell. Available + // cells may be non-square; aspect is cell_height/cell_width in pixels. + // Dividing heights by aspect makes panel pixel aspect match the data + // aspect (issue #234). Then scale so the largest row/column still fits. if (p_info.layout.coord_fixed[0]) { var aspect = (graph_height / nrows) / (graph_width / ncols); } else { var aspect = 1; } - // FIX for issue #234: Remove Math.min() that was shrinking proportions. - // The R side already normalized properly, so we just apply the aspect ratio. - var wp = p_info.layout.width_proportion.map(function(x){ - return x * aspect; - }) - var hp = p_info.layout.height_proportion.map(function(x){ - return x * (1/aspect); - }) - // Normalize so at least one dimension fills the space - var all_props = wp.concat(hp); - var max_prop = Math.max.apply(null, all_props); - if (max_prop > 0) { - wp = wp.map(function(x) { return x / max_prop; }); - hp = hp.map(function(x) { return x / max_prop; }); + var wp = p_info.layout.width_proportion.slice(); + var hp = p_info.layout.height_proportion.map(function(y){ + return y / aspect; + }); + var max_row_sum = 0; + for (var row_i = 1; row_i <= nrows; row_i++) { + var row_sum = 0; + for (var layout_j = 0; layout_j < npanels; layout_j++) { + if (p_info.layout.ROW[layout_j] == row_i) { + row_sum += wp[layout_j]; + } + } + max_row_sum = Math.max(max_row_sum, row_sum); + } + var max_col_sum = 0; + for (var col_i = 1; col_i <= ncols; col_i++) { + var col_sum = 0; + for (var layout_k = 0; layout_k < npanels; layout_k++) { + if (p_info.layout.COL[layout_k] == col_i) { + col_sum += hp[layout_k]; + } + } + max_col_sum = Math.max(max_col_sum, col_sum); + } + var fit_prop = Math.max(max_row_sum, max_col_sum); + if (fit_prop > 0) { + wp = wp.map(function(x) { return x / fit_prop; }); + hp = hp.map(function(y) { return y / fit_prop; }); } // track the proportion of the graph that should be 'blank' diff --git a/tests/testthat/test-compiler-coord-equal-fix.R b/tests/testthat/test-compiler-coord-equal-fix.R index 5e191eb3b..9854f8d62 100644 --- a/tests/testthat/test-compiler-coord-equal-fix.R +++ b/tests/testthat/test-compiler-coord-equal-fix.R @@ -1,11 +1,11 @@ acontext("coord_equal fix for issue #234") -test_that("fixed_spaces normalizes to fill available space", { +test_that("fixed_spaces keeps at least one dimension filling space", { # Test with iris petal data ranges (issue #234 example) ranges <- list(list(x.range = c(1, 7), y.range = c(0, 2.5))) spaces <- animint2:::fixed_spaces(ranges, ratio = 1) - # At least one dimension should be 1 (fills available space) + # At least one dimension should be 1 (fills available space on a square cell) max_prop <- max(spaces$x, spaces$y) expect_equal(max_prop, 1) @@ -14,4 +14,6 @@ test_that("fixed_spaces normalizes to fill available space", { expect_lte(spaces$x, 1) expect_gt(spaces$y, 0) expect_lte(spaces$y, 1) + # Data aspect y/x preserved for the JS scale-to-fit step + expect_equal(spaces$y / spaces$x, 2.5 / 6) }) diff --git a/tests/testthat/test-renderer1-coord.R b/tests/testthat/test-renderer1-coord.R index c06b7e4c6..dc67130f5 100644 --- a/tests/testthat/test-renderer1-coord.R +++ b/tests/testthat/test-renderer1-coord.R @@ -30,7 +30,8 @@ test_that("coord_fixed with shrinking y-axis", { xdiff <- getTickDiff(x.axes[[1]]) ydiff <- getTickDiff(y.axes[[1]], axis = "y") diffs <- normDiffs(xdiff, ydiff, ratio5) - expect_equal(diffs[1], diffs[2], tolerance = 10) + ## Round to drop SVG sub-pixel float noise (~1e-6); aspect should match. + expect_equal(round(diffs[1], 3), round(diffs[2], 3)) }) test_that("xaxis width increases with coord_equal", { @@ -43,6 +44,17 @@ test_that("xaxis width increases with coord_equal", { expect_gt(ratio, 2) }) +test_that("coord_equal preserves aspect on non-square viewport", { + viz <- p + coord_equal() + theme_animint(width=800, height=400) + info <- animint2HTML(list(plot = viz)) + x.axes <- getNodeSet(info$html, "//g[contains(@class, 'xaxis')]") + y.axes <- getNodeSet(info$html, "//g[contains(@class, 'yaxis')]") + xdiff <- getTickDiff(x.axes[[1]]) + ydiff <- getTickDiff(y.axes[[1]], axis = "y") + diffs <- normDiffs(xdiff, ydiff, 1) + expect_equal(round(diffs[1], 3), round(diffs[2], 3)) +}) + test_that("coord_fixed with shrinking x-axis", { ratio10 <- 10 viz2 <- p + coord_fixed(ratio10) @@ -52,5 +64,6 @@ test_that("coord_fixed with shrinking x-axis", { xdiff <- getTickDiff(x.axes[[1]]) ydiff <- getTickDiff(y.axes[[1]], axis = "y") diffs <- normDiffs(xdiff, ydiff, ratio10) - expect_equal(diffs[1], diffs[2], tolerance = 10) + ## Round to drop SVG sub-pixel float noise (~1e-6); aspect should match. + expect_equal(round(diffs[1], 3), round(diffs[2], 3)) }) diff --git a/tests/testthat/test-renderer1-facet-coord.R b/tests/testthat/test-renderer1-facet-coord.R index 8c8aecb12..1e47f4282 100644 --- a/tests/testthat/test-renderer1-facet-coord.R +++ b/tests/testthat/test-renderer1-facet-coord.R @@ -14,10 +14,10 @@ test_that("coord_fixed(ratio=10) + facet_wrap(nrow=1) with shrunk y-axis", { xdiff1 <- getTickDiff(x.axes[[1]]) ydiff <- getTickDiff(y.axes[[1]], axis = "y") diffs <- normDiffs(xdiff1, ydiff, 2) - expect_equal(diffs[1], diffs[2], tolerance = 10) + expect_equal(round(diffs[1], 3), round(diffs[2], 3)) xdiff2 <- getTickDiff(x.axes[[2]]) diffs <- normDiffs(xdiff2, ydiff, 2) - expect_equal(diffs[1], diffs[2], tolerance = 10) + expect_equal(round(diffs[1], 3), round(diffs[2], 3)) }) test_that("coord_fixed(ratio=10) + facet_wrap(nrow=1) with shrunk x-axis", { @@ -30,10 +30,10 @@ test_that("coord_fixed(ratio=10) + facet_wrap(nrow=1) with shrunk x-axis", { xdiff1 <- getTickDiff(x.axes[[1]]) ydiff <- getTickDiff(y.axes[[1]], axis = "y") diffs <- normDiffs(xdiff1, ydiff, 10) - expect_equal(diffs[1], diffs[2], tolerance = 10) + expect_equal(round(diffs[1], 3), round(diffs[2], 3)) xdiff2 <- getTickDiff(x.axes[[2]]) diffs <- normDiffs(xdiff2, ydiff, 10) - expect_equal(diffs[1], diffs[2], tolerance = 10) + expect_equal(round(diffs[1], 3), round(diffs[2], 3)) }) # also test multiple row and/or facet_grid? From cae57f6ed75ff34321ee76b1cf93a1768ca32a5a Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Thu, 30 Jul 2026 09:33:33 +0530 Subject: [PATCH 13/18] docs(test): address tdhock NEWS and width/height questions Move coord_equal fix to NEWS 2026.7.29 (PR#253) and clarify #234 is the width case. Add yaxis height test with coord_fixed(10). --- NEWS.md | 8 ++++---- tests/testthat/test-renderer1-coord.R | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/NEWS.md b/NEWS.md index a6d5a575f..74d692857 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +# Changes in version 2026.7.29 (PR#253) + +- Fixed `coord_equal()` and `coord_fixed()` to properly fill available plotting space. Issue #234 showed the problem when increasing plot width (800 vs default 400); the same JavaScript bug also affected tall viewports when height should fill. Previously `Math.min(1, aspect)` was applied independently to width and height proportions, shrinking both when the graph pixel aspect did not match the data aspect. The renderer now adjusts panel proportions for non-square graph cells and scale-to-fits so at least one dimension fills the available space while preserving the data aspect ratio. + # Changes in version 2026.5.29 (PR#286) - Positive `panel.margin` values in `"lines"` are supported for vertical and @@ -66,10 +70,6 @@ - Improve common chunk detection, output `na_group` and `row_in_group` when there are missing values. -# Changes in version 2025.10.4 (Issue #234) - -- Fixed `coord_equal()` and `coord_fixed()` to properly fill available plotting space. Previously, the JavaScript renderer applied `Math.min(1, aspect)` independently to width and height proportions, which shrunk both axes when the graph pixel aspect did not match the data aspect. The renderer now adjusts panel proportions for non-square graph cells and scale-to-fits so at least one dimension fills the available space while preserving the data aspect ratio. - # Changes in version 2025.10.6 (PR#246) - Added validation for selector names to prevent browser rendering failures. Selector names (from data values used in `clickSelects` and `showSelected`) cannot contain CSS special characters like `#`, `@`, `!`, `$`, etc., as these interfere with JavaScript DOM selectors and cause blank visualizations in the browser. The compiler now stops with a clear error message identifying problematic selector names, helping users fix data issues before attempting to render. diff --git a/tests/testthat/test-renderer1-coord.R b/tests/testthat/test-renderer1-coord.R index dc67130f5..008bf19d2 100644 --- a/tests/testthat/test-renderer1-coord.R +++ b/tests/testthat/test-renderer1-coord.R @@ -44,6 +44,20 @@ test_that("xaxis width increases with coord_equal", { expect_gt(ratio, 2) }) +test_that("yaxis height increases with coord_fixed", { + p_fixed <- p + coord_fixed(10) + info_default <- animint2HTML(list(plot = p_fixed)) + y.axes <- getNodeSet(info_default$html, "//g[contains(@class, 'yaxis')]") + ydiff_default <- getTickDiff(y.axes[[1]], axis = "y") + info_tall <- animint2HTML(list(plot = p_fixed + theme_animint(height = 1000))) + y.axes <- getNodeSet(info_tall$html, "//g[contains(@class, 'yaxis')]") + ydiff_tall <- getTickDiff(y.axes[[1]], axis = "y") + ratio <- ydiff_tall / ydiff_default + ## Height is the filling dimension for ratio=10; tick spacing should grow + ## substantially (width test uses expect_gt(ratio, 2) for coord_equal). + expect_gt(ratio, 1.8) +}) + test_that("coord_equal preserves aspect on non-square viewport", { viz <- p + coord_equal() + theme_animint(width=800, height=400) info <- animint2HTML(list(plot = viz)) From 6818162fd5e90f5aea80ca4f4343a890d5731208 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Fri, 21 Aug 2026 00:06:10 +0530 Subject: [PATCH 14/18] Simplifed coord_equal scale-to-fit row/column loops. Replaced duplicated nested proportion sums with one panel pass and d3.max per review on PR#253. --- inst/htmljs/animint.js | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/inst/htmljs/animint.js b/inst/htmljs/animint.js index d6c623df5..b6faddcff 100644 --- a/inst/htmljs/animint.js +++ b/inst/htmljs/animint.js @@ -571,27 +571,14 @@ var setMultilineText = function(textElement, text) { var hp = p_info.layout.height_proportion.map(function(y){ return y / aspect; }); - var max_row_sum = 0; - for (var row_i = 1; row_i <= nrows; row_i++) { - var row_sum = 0; - for (var layout_j = 0; layout_j < npanels; layout_j++) { - if (p_info.layout.ROW[layout_j] == row_i) { - row_sum += wp[layout_j]; - } - } - max_row_sum = Math.max(max_row_sum, row_sum); - } - var max_col_sum = 0; - for (var col_i = 1; col_i <= ncols; col_i++) { - var col_sum = 0; - for (var layout_k = 0; layout_k < npanels; layout_k++) { - if (p_info.layout.COL[layout_k] == col_i) { - col_sum += hp[layout_k]; - } - } - max_col_sum = Math.max(max_col_sum, col_sum); + var row_sums = [], col_sums = []; + for (var layout_i = 0; layout_i < npanels; layout_i++) { + var row = p_info.layout.ROW[layout_i] - 1; + var col = p_info.layout.COL[layout_i] - 1; + row_sums[row] = (row_sums[row] || 0) + wp[layout_i]; + col_sums[col] = (col_sums[col] || 0) + hp[layout_i]; } - var fit_prop = Math.max(max_row_sum, max_col_sum); + var fit_prop = Math.max(d3.max(row_sums) || 0, d3.max(col_sums) || 0); if (fit_prop > 0) { wp = wp.map(function(x) { return x / fit_prop; }); hp = hp.map(function(y) { return y / fit_prop; }); From 81ca7ecf9237fd56fc02828fd4ad5812319dc9c7 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Fri, 21 Aug 2026 00:42:13 +0530 Subject: [PATCH 15/18] Fix CI flake in coord_fixed shrinking x-axis test. Round normalized tick diffs to 2 decimals so ~0.001 SVG float noise on CI does not fail aspect checks. --- tests/testthat/test-renderer1-coord.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testthat/test-renderer1-coord.R b/tests/testthat/test-renderer1-coord.R index 008bf19d2..605ffc291 100644 --- a/tests/testthat/test-renderer1-coord.R +++ b/tests/testthat/test-renderer1-coord.R @@ -78,6 +78,6 @@ test_that("coord_fixed with shrinking x-axis", { xdiff <- getTickDiff(x.axes[[1]]) ydiff <- getTickDiff(y.axes[[1]], axis = "y") diffs <- normDiffs(xdiff, ydiff, ratio10) - ## Round to drop SVG sub-pixel float noise (~1e-6); aspect should match. - expect_equal(round(diffs[1], 3), round(diffs[2], 3)) + ## Round to drop SVG sub-pixel float noise; ratio=10 values ~75px can differ by ~0.001 on CI. + expect_equal(round(diffs[1], 2), round(diffs[2], 2)) }) From 99547e894465ddbe333e5b3bf2436bebf71a0113 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Wed, 26 Aug 2026 18:24:58 +0530 Subject: [PATCH 16/18] Isolate JS_coverage GitHub Pages test repository Use GitHub Actions run identifiers so concurrent JS_coverage jobs cannot reset or push the same temporary repository. --- tests/testthat/test-compiler-ghpages.R | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/testthat/test-compiler-ghpages.R b/tests/testthat/test-compiler-ghpages.R index 70ef74125..040a48e8c 100644 --- a/tests/testthat/test-compiler-ghpages.R +++ b/tests/testthat/test-compiler-ghpages.R @@ -1,8 +1,16 @@ acontext("GitHub Pages") library(animint2) +test_suite <- tolower(Sys.getenv("TEST_SUITE", unset = "local")) +github_run_id <- Sys.getenv("GITHUB_RUN_ID", unset = "") +github_run_attempt <- Sys.getenv("GITHUB_RUN_ATTEMPT", unset = "") +test_repo_suffix <- if (test_suite == "js_coverage" && nzchar(github_run_id) && nzchar(github_run_attempt)) { + sprintf("%s_%s_%s", test_suite, github_run_id, github_run_attempt) +} else { + test_suite +} test_repo_name <- sprintf( "animint2pages_test_repo_%s", - tolower(Sys.getenv("TEST_SUITE", unset = "local")) + test_repo_suffix ) viz <- animint( title="one to ten", From abd7bcd1f5ac076729ee5ac2442c75aef147da91 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Fri, 4 Sep 2026 23:49:16 +0530 Subject: [PATCH 17/18] Extract max_group_sum for coord scale-to-fit; fix NEWS version header. Group ROW/COL proportions via a local helper per tdhock review. Rename PR#339 NEWS section to a parseable version for CRAN. --- NEWS.md | 2 +- inst/htmljs/animint.js | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/NEWS.md b/NEWS.md index 9d5db5055..c3fbacdea 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,7 +2,7 @@ - Fixed `coord_equal()` and `coord_fixed()` to properly fill available plotting space. Issue #234 showed the problem when increasing plot width (800 vs default 400); the same JavaScript bug also affected tall viewports when height should fill. Previously `Math.min(1, aspect)` was applied independently to width and height proportions, shrinking both when the graph pixel aspect did not match the data aspect. The renderer now adjusts panel proportions for non-square graph cells and scale-to-fits so at least one dimension fills the available space while preserving the data aspect ratio. -# Changes in development (PR#339) +# Changes in version 2026.9.4 (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 d98c10dc9..47b4dcd00 100644 --- a/inst/htmljs/animint.js +++ b/inst/htmljs/animint.js @@ -538,6 +538,14 @@ var setMultilineText = function(textElement, text) { } return cumsum; } + function max_group_sum(group, values){ + var sums = []; + for(var i=0; i 0) { wp = wp.map(function(x) { return x / fit_prop; }); hp = hp.map(function(y) { return y / fit_prop; }); From d6353ffaf56484c63c524e7035f6c2aeba4b1312 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Mon, 28 Sep 2026 22:21:04 +0530 Subject: [PATCH 18/18] Merge branch 'master' into test-coord_equal Keep this PR limited to the coord_equal fix. NEWS adds the PR#253 entry on top of current master. --- DESCRIPTION | 2 +- NEWS.md | 12 +-- R/facet-layout.r | 30 +------ inst/htmljs/animint.js | 4 +- tests/testthat/test-compiler-ghpages.R | 34 +------- ...sue-279-facet-wrap-custom-height-spacing.R | 43 ---------- .../test-renderer-facet-error-messages.R | 79 ------------------- tests/testthat/test-renderer3-knit-print.R | 8 +- 8 files changed, 11 insertions(+), 201 deletions(-) delete mode 100644 tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R delete mode 100644 tests/testthat/test-renderer-facet-error-messages.R diff --git a/DESCRIPTION b/DESCRIPTION index 2203c1d72..5ca8f82a3 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: animint2 Title: Animated Interactive Grammar of Graphics -Version: 2026.9.28 +Version: 2026.9.5 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 1710dd5b1..ec069773e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,10 +1,6 @@ -# Changes in version 2026.9.29 (PR#288) - -- Plot SVGs use `display: block` (issue #279). Each ggplot is one SVG. Browsers treat an SVG as inline and leave space under it for text descenders, which is the extra whitespace below a facet_wrap plot. That gap is under the SVG, not between facet panels. This is required; without it the rendered gap is about 5px. Thanks @ANAMASGARD. - -# Changes in version 2026.9.28 (PR#285) +# Changes in version 2026.7.29 (PR#253) -- Clearer facet error messages when a facet variable is missing from the data, or when `facet_wrap(. ~ var)` uses invalid notation (issue #168). +- Fixed `coord_equal()` and `coord_fixed()` to properly fill available plotting space. Issue #234 showed the problem when increasing plot width (800 vs default 400); the same JavaScript bug also affected tall viewports when height should fill. Previously `Math.min(1, aspect)` was applied independently to width and height proportions, shrinking both when the graph pixel aspect did not match the data aspect. The renderer now adjusts panel proportions for non-square graph cells and scale-to-fits so at least one dimension fills the available space while preserving the data aspect ratio. # Changes in version 2026.9.5 @@ -18,10 +14,6 @@ - `.github/workflows/tests.yaml` now defines three jobs: `R_coverage` and `JS_coverage` collect reports as artifacts, and a new `upload-coverage` job depends on both (`needs: coverage-tests`) and uploads them to Codecov together only after both succeed. A separate `CRAN` job still runs `build.sh`. This prevents a failed JS (or R) job from becoming an incomplete project-coverage baseline (issue #254). -# Changes in version 2026.7.29 (PR#253) - -- Fixed `coord_equal()` and `coord_fixed()` to properly fill available plotting space. Issue #234 showed the problem when increasing plot width (800 vs default 400); the same JavaScript bug also affected tall viewports when height should fill. Previously `Math.min(1, aspect)` was applied independently to width and height proportions, shrinking both when the graph pixel aspect did not match the data aspect. The renderer now adjusts panel proportions for non-square graph cells and scale-to-fits so at least one dimension fills the available space while preserving the data aspect ratio. - # Changes in version 2026.7.29 (PR#261) - Multi-line text support (issue #221): `\n` now works in plot titles, axis titles, legend titles, and `geom_text()` labels. R compiler converts newlines to `
` via `R/z_multiline.R`; JavaScript renderer converts `
` to SVG `` elements. diff --git a/R/facet-layout.r b/R/facet-layout.r index 13f85b9e1..ef312b4a1 100644 --- a/R/facet-layout.r +++ b/R/facet-layout.r @@ -48,20 +48,6 @@ layout_wrap <- function(data, vars = NULL, nrow = NULL, ncol = NULL, vars <- as.quoted(vars) if (length(vars) == 0) return(layout_null()) - # Notation error only when real vars exist; otherwise layout_base reports missing columns. - if ("." %in% names(vars)) { - real_vars <- names(vars)[names(vars) != "."] - all_data_vars <- unique(unlist(lapply(data, names))) - missing_vars <- setdiff(real_vars, all_data_vars) - if (length(missing_vars) == 0) { - stop(sprintf( - "Invalid facet_wrap formula (. ~ var). Use facet_wrap(~%s) or facet_wrap(\"%s\")", - paste(real_vars, collapse = " + "), - paste(real_vars, collapse = "\", \"") - ), call. = FALSE) - } - } - base <- plyr::unrowname(layout_base(data, vars, drop = drop)) id <- plyr::id(base, drop = TRUE) @@ -107,22 +93,8 @@ layout_base <- function(data, vars = NULL, drop = TRUE) { # Form the base data frame which contains all combinations of facetting # variables that appear in the data - has_all <- unlist(lapply(values, length)) == length(vars) + has_all <- unlist(plyr::llply(values, length)) == length(vars) if (!any(has_all)) { - all_data_vars <- unique(unlist(lapply(data, names))) - missing_vars <- setdiff(names(vars)[names(vars) != "."], all_data_vars) - if (length(missing_vars) > 0) { - stop(sprintf( - "Facet variable%s not found in data: %s\nAvailable columns: %s", - if (length(missing_vars) > 1) "s" else "", - paste(missing_vars, collapse = ", "), - paste(all_data_vars, collapse = ", ") - )) - } - # Reached when all facet variables exist somewhere in the data, but no - # single layer has all of them together. For example, facet_grid(X ~ Y) - # where layer 1 only has column X and layer 2 only has column Y — neither - # layer alone satisfies all facetting variables, so has_all is FALSE for every layer. stop("At least one layer must contain all variables used for facetting") } diff --git a/inst/htmljs/animint.js b/inst/htmljs/animint.js index b05c04339..5bab42dc3 100644 --- a/inst/htmljs/animint.js +++ b/inst/htmljs/animint.js @@ -401,9 +401,7 @@ var setMultilineText = function(textElement, text) { var svg = tdLeft.append("svg") .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"); + .attr("width", p_info.options.width); // 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-compiler-ghpages.R b/tests/testthat/test-compiler-ghpages.R index 7b60f5a6e..70ef74125 100644 --- a/tests/testthat/test-compiler-ghpages.R +++ b/tests/testthat/test-compiler-ghpages.R @@ -1,16 +1,8 @@ acontext("GitHub Pages") library(animint2) -test_suite <- tolower(Sys.getenv("TEST_SUITE", unset = "local")) -github_run_id <- Sys.getenv("GITHUB_RUN_ID", unset = "") -github_run_attempt <- Sys.getenv("GITHUB_RUN_ATTEMPT", unset = "") -test_repo_suffix <- if (test_suite == "js_coverage" && nzchar(github_run_id) && nzchar(github_run_attempt)) { - sprintf("%s_%s_%s", test_suite, github_run_id, github_run_attempt) -} else { - test_suite -} test_repo_name <- sprintf( "animint2pages_test_repo_%s", - test_repo_suffix + tolower(Sys.getenv("TEST_SUITE", unset = "local")) ) viz <- animint( title="one to ten", @@ -59,28 +51,6 @@ reset_test_repo <- function(){ gh::gh("POST /orgs/animint-test/repos", name = test_repo_name) Sys.sleep(3) } -clone_test_repo <- function(github_url, new_clone){ - max_attempts <- 5 - wait_seconds <- 3 - last_error <- NULL - for (attempt in seq_len(max_attempts)) { - result <- tryCatch({ - clone <- gert::git_clone(github_url, new_clone) - last_error <- NULL - clone - }, error = function(err){ - last_error <<- err - NULL - }) - if (is.null(last_error)) return(result) - if (!grepl("status code: 404", conditionMessage(last_error), fixed = TRUE)) { - stop(last_error) - } - unlink(new_clone, recursive = TRUE, force = TRUE) - if (attempt < max_attempts) Sys.sleep(wait_seconds) - } - stop(last_error) -} test_that("animint2pages(chromote_sleep_seconds=3) creates Capture.PNG", { reset_test_repo() ## first run of animint2pages creates new data viz. @@ -126,7 +96,7 @@ test_that("animint2pages(chromote_sleep_seconds=NULL) does not create Capture.PN ## clone and add Capture.PNG new_clone <- tempfile() github_url <- paste0("https://github.com/", result_list$owner_repo, ".git") - clone_test_repo(github_url, new_clone) + gert::git_clone(github_url, new_clone) branch_name <- gert::git_branch(new_clone) expect_identical(branch_name, "gh-pages", info=paste(branch_name, "should be gh-pages")) cat("FOO", file=file.path(new_clone, "Capture.PNG")) 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 deleted file mode 100644 index 86d8b217a..000000000 --- a/tests/testthat/test-issue-279-facet-wrap-custom-height-spacing.R +++ /dev/null @@ -1,43 +0,0 @@ -test_that("facet_wrap SVG height is proportional to theme_animint height, no excess space (#279)", { - 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) - ) - 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)" - base_plot <- ggplot() + - geom_point(aes(x, y), data = task_data) + - facet_wrap(~ task_id, ncol = 1) + - theme_bw() - viz_list <- list( - default = animint(plot = base_plot), - custom = animint(plot = base_plot + theme_animint(height = custom_theme_height)) - ) - 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]]) - 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)") - 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) -}) diff --git a/tests/testthat/test-renderer-facet-error-messages.R b/tests/testthat/test-renderer-facet-error-messages.R deleted file mode 100644 index 09805f24d..000000000 --- a/tests/testthat/test-renderer-facet-error-messages.R +++ /dev/null @@ -1,79 +0,0 @@ -acontext("Facet error messages") -missing_var_error <- paste( - "Facet variable not found in data: NonExistentColumn", - "Available columns: Sepal.Length, Sepal.Width, Petal.Length, Petal.Width, Species", - sep = "\n" -) -missing_var_string_error <- paste( - "Facet variable not found in data: MissingVar", - "Available columns: Sepal.Length, Sepal.Width, Petal.Length, Petal.Width, Species", - sep = "\n" -) -bad_notation_error <- "Invalid facet_wrap formula (. ~ var). Use facet_wrap(~Species) or facet_wrap(\"Species\")" -test_that("facet_wrap string missing variable gives clear error", { - viz <- list( - scatter = ggplot() + - facet_wrap("MissingVar") + - geom_point(aes(Sepal.Length, Petal.Length), data = iris) - ) - expect_error({ - animint2dir(viz) - }, missing_var_string_error, fixed = TRUE) -}) -test_that("facet_wrap formula with missing variable gives clear error", { - viz <- list( - scatter = ggplot() + - facet_wrap(. ~ NonExistentColumn) + - geom_point(aes(Sepal.Length, Petal.Length), data = iris) - ) - expect_error({ - animint2dir(viz) - }, missing_var_error, fixed = TRUE) -}) -test_that("facet_wrap bad notation when variable exists gives clear error", { - viz <- list( - scatter = ggplot() + - facet_wrap(. ~ Species) + - geom_point(aes(Sepal.Length, Petal.Length), data = iris) - ) - expect_error({ - animint2dir(viz) - }, bad_notation_error, fixed = TRUE) -}) -test_that("facet_grid missing variable gives clear error", { - viz <- list( - scatter = ggplot() + - facet_grid(. ~ MissingVar) + - geom_point(aes(Sepal.Length, Petal.Length), data = iris) - ) - expect_error({ - animint2dir(viz) - }, missing_var_string_error, fixed = TRUE) -}) -test_that("facet_wrap tilde notation works", { - viz <- list( - scatter = ggplot() + - facet_wrap(~Species) + - geom_point(aes(Sepal.Length, Petal.Length), data = iris) - ) - info <- animint2dir(viz, open.browser = FALSE) - expect_true(file.exists(file.path(info$out.dir, "index.html"))) -}) -test_that("facet_wrap string notation works", { - viz <- list( - scatter = ggplot() + - facet_wrap("Species") + - geom_point(aes(Sepal.Length, Petal.Length), data = iris) - ) - info <- animint2dir(viz, open.browser = FALSE) - expect_true(file.exists(file.path(info$out.dir, "index.html"))) -}) -test_that("facet_grid formula notation works", { - viz <- list( - scatter = ggplot() + - facet_grid(. ~ Species) + - geom_point(aes(Sepal.Length, Petal.Length), data = iris) - ) - info <- animint2dir(viz, open.browser = FALSE) - expect_true(file.exists(file.path(info$out.dir, "index.html"))) -}) diff --git a/tests/testthat/test-renderer3-knit-print.R b/tests/testthat/test-renderer3-knit-print.R index 7f34e2e15..e732b42c8 100644 --- a/tests/testthat/test-renderer3-knit-print.R +++ b/tests/testthat/test-renderer3-knit-print.R @@ -44,9 +44,10 @@ test_that("segments and breakpoints are rendered", { }) test_that("svg id property is unique", { - id.vec <- getPropertyValue(html, "//svg", "id") - id.counts <- table(id.vec) - expect_equal(max(id.counts), 1L) + svg.list <- getNodeSet(html, "//svg") + attr.mat <- sapply(svg.list, xmlAttrs) + id.counts <- table(attr.mat["id",]) + expect_true(all(id.counts==1)) }) all.list <- getNodeSet(html, "//*") @@ -180,7 +181,6 @@ click_center <- function(id){ ## https://github.com/rstudio/chromote/issues/32 do.call(remDr$Input$dispatchMouseEvent, L) } - Sys.sleep(1) } djs.init.list <- driverjs_get(html)