diff --git a/R/layers2traces.R b/R/layers2traces.R index 181ef72909..3251c8c4f6 100644 --- a/R/layers2traces.R +++ b/R/layers2traces.R @@ -97,6 +97,9 @@ layers2traces <- function(data, prestats_data, layout, p) { d <- to_basic(data[[i]], prestats_data[[i]], layout, params[[i]], p) d <- structure(d, set = set) if (is.data.frame(d)) d <- list(d) + # Replace Inf values with panel limits for all coordinate columns (fixes #2364) + # JSON doesn't support Inf, so they become null and shapes won't render + d <- lapply(d, replace_inf_in_data, layout = layout) for (j in seq_along(d)) { datz <- c(datz, d[j]) paramz <- c(paramz, params[i]) diff --git a/R/utils.R b/R/utils.R index b558e12b03..5e672e131b 100644 --- a/R/utils.R +++ b/R/utils.R @@ -53,6 +53,42 @@ is.discrete <- function(x) { # standard way to specify a line break br <- function() "
" +# Replace Inf values in all coordinate columns of a data frame (fixes #2364) +# JSON doesn't support Inf, so they become null and shapes won't render. +# This handles x, y, xmin, xmax, xend, ymin, ymax, yend columns generically. +# Called after to_basic() returns, when panel limits are available. +replace_inf_in_data <- function(data, layout) { + if (!is.data.frame(data) || nrow(data) == 0) return(data) + if (is.null(data$PANEL)) return(data) + + # Use match() for robustness in case PANEL values aren't consecutive integers + panel_idx <- match(data$PANEL, layout$layout$PANEL) + x_cols <- intersect(names(data), c("x", "xmin", "xmax", "xend")) + y_cols <- intersect(names(data), c("y", "ymin", "ymax", "yend")) + + replace_inf_vec <- function(vals, min_vals, max_vals) { + neg_inf <- is.infinite(vals) & vals < 0 + pos_inf <- is.infinite(vals) & vals > 0 + vals[neg_inf] <- min_vals[panel_idx[neg_inf]] + vals[pos_inf] <- max_vals[panel_idx[pos_inf]] + vals + } + + for (col in x_cols) { + if (is.numeric(data[[col]]) && any(is.infinite(data[[col]]))) { + data[[col]] <- replace_inf_vec(data[[col]], + layout$layout$x_min, layout$layout$x_max) + } + } + for (col in y_cols) { + if (is.numeric(data[[col]]) && any(is.infinite(data[[col]]))) { + data[[col]] <- replace_inf_vec(data[[col]], + layout$layout$y_min, layout$layout$y_max) + } + } + data +} + is.default <- function(x) { inherits(x, "plotly_default") } diff --git a/tests/testthat/test-ggplot-lines.R b/tests/testthat/test-ggplot-lines.R index e70179612a..6cae239bcd 100644 --- a/tests/testthat/test-ggplot-lines.R +++ b/tests/testthat/test-ggplot-lines.R @@ -139,3 +139,30 @@ test_that("NA values do not cause a lot of warnings when ploting (#1299)", { expect_warning(plotly_build(p), "Ignoring") expect_failure(expect_warning(plotly_build(p), "structure")) }) + +test_that('geom_line handles Inf values correctly (#2364)', { + # This is the original issue: geom_line with Inf y values + df <- data.frame(x = 1:10, y = 1:10) + line_df <- data.frame(x = c(3, 6), y = c(-Inf, Inf)) + + p <- ggplot(df, aes(x, y)) + + geom_point() + + geom_line(data = line_df, aes(x = x, y = y), color = "blue") + + L <- plotly_build(p) + + # Find the line trace + line_traces <- Filter(function(tr) identical(tr$mode, "lines"), L$x$data) + expect_length(line_traces, 1) + + line_trace <- line_traces[[1]] + + # Inf values should be replaced with finite panel limits + expect_false(any(is.infinite(line_trace$y), na.rm = TRUE)) + expect_false(any(is.infinite(line_trace$x), na.rm = TRUE)) + + # Verify the replaced values match the panel limits + y_range <- L$x$layout$yaxis$range + expect_equal(min(line_trace$y), y_range[1]) + expect_equal(max(line_trace$y), y_range[2]) +}) diff --git a/tests/testthat/test-ggplot-polygons.R b/tests/testthat/test-ggplot-polygons.R index 82bd1728e9..77a854156d 100644 --- a/tests/testthat/test-ggplot-polygons.R +++ b/tests/testthat/test-ggplot-polygons.R @@ -210,3 +210,63 @@ test_that("geom_polygon(aes(group, fill), color) -> 2 trace", { expect_equivalent(traces.by.name[[1]]$x, c(0, -1, 2, -2, 1, 0)) expect_equivalent(traces.by.name[[2]]$x, c(10, 9, 12, 8, 11, 10)) }) + +test_that('geom_polygon handles Inf values correctly (#2364)', { + df <- data.frame(x = 1:10, y = 1:10) + + # Polygon with Inf y values (like a vertical band) + poly_df <- data.frame( + x = c(3, 3, 6, 6), + y = c(-Inf, Inf, Inf, -Inf) + ) + + p <- ggplot(df, aes(x, y)) + + geom_point() + + geom_polygon( + data = poly_df, + aes(x = x, y = y), + fill = "blue", alpha = 0.2, inherit.aes = FALSE + ) + + L <- plotly_build(p) + + # Find the polygon trace + poly_traces <- Filter(function(tr) identical(tr$fill, "toself"), L$x$data) + expect_length(poly_traces, 1) + + poly_trace <- poly_traces[[1]] + + # Inf values should be replaced with finite panel limits + expect_false(any(is.infinite(poly_trace$y), na.rm = TRUE)) + expect_false(any(is.infinite(poly_trace$x), na.rm = TRUE)) + + # Verify the replaced values match the panel limits + y_range <- L$x$layout$yaxis$range + expect_equal(min(poly_trace$y, na.rm = TRUE), y_range[1]) + expect_equal(max(poly_trace$y, na.rm = TRUE), y_range[2]) + + # Test with x Inf values as well + poly_df2 <- data.frame( + x = c(-Inf, -Inf, Inf, Inf), + y = c(4, 6, 6, 4) + ) + + p2 <- ggplot(df, aes(x, y)) + + geom_point() + + geom_polygon( + data = poly_df2, + aes(x = x, y = y), + fill = "red", alpha = 0.2, inherit.aes = FALSE + ) + + L2 <- plotly_build(p2) + poly_trace2 <- Filter(function(tr) identical(tr$fill, "toself"), L2$x$data)[[1]] + + expect_false(any(is.infinite(poly_trace2$x), na.rm = TRUE)) + expect_false(any(is.infinite(poly_trace2$y), na.rm = TRUE)) + + # Verify the replaced x values match the panel limits + x_range <- L2$x$layout$xaxis$range + expect_equal(min(poly_trace2$x, na.rm = TRUE), x_range[1]) + expect_equal(max(poly_trace2$x, na.rm = TRUE), x_range[2]) +}) diff --git a/tests/testthat/test-ggplot-rect.R b/tests/testthat/test-ggplot-rect.R index 6a3b69ec1d..42ee72efd2 100644 --- a/tests/testthat/test-ggplot-rect.R +++ b/tests/testthat/test-ggplot-rect.R @@ -138,3 +138,84 @@ test_that('Specifying alpha in hex color code works', { expect_match(info$data[[1]]$fillcolor, "rgba\\(0,0,0,0\\.0[6]+") }) +test_that('geom_rect handles Inf values correctly (#2364)', { + df <- data.frame(x = 1:10, y = 1:10) + rect_df <- data.frame(xmin = 3, xmax = 6, ymin = -Inf, ymax = Inf) + + p <- ggplot(df, aes(x, y)) + + geom_point() + + geom_rect( + data = rect_df, + aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax), + fill = "blue", alpha = 0.2, inherit.aes = FALSE + ) + + L <- plotly_build(p) + + # Find the rect trace (polygon with fill="toself") + rect_traces <- Filter(function(tr) identical(tr$fill, "toself"), L$x$data) + expect_length(rect_traces, 1) + + rect_trace <- rect_traces[[1]] + + # Inf values should be replaced with finite panel limits + expect_false(any(is.infinite(rect_trace$y), na.rm = TRUE)) + expect_false(any(is.infinite(rect_trace$x), na.rm = TRUE)) + + # Verify the replaced values match the panel limits + y_range <- L$x$layout$yaxis$range + expect_equal(min(rect_trace$y, na.rm = TRUE), y_range[1]) + expect_equal(max(rect_trace$y, na.rm = TRUE), y_range[2]) + + # Test with x Inf values as well + rect_df2 <- data.frame(xmin = -Inf, xmax = Inf, ymin = 4, ymax = 6) + + p2 <- ggplot(df, aes(x, y)) + + geom_point() + + geom_rect( + data = rect_df2, + aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax), + fill = "red", alpha = 0.2, inherit.aes = FALSE + ) + + L2 <- plotly_build(p2) + rect_trace2 <- Filter(function(tr) identical(tr$fill, "toself"), L2$x$data)[[1]] + + expect_false(any(is.infinite(rect_trace2$x), na.rm = TRUE)) + expect_false(any(is.infinite(rect_trace2$y), na.rm = TRUE)) + + # Verify the replaced x values match the panel limits + x_range <- L2$x$layout$xaxis$range + expect_equal(min(rect_trace2$x, na.rm = TRUE), x_range[1]) + expect_equal(max(rect_trace2$x, na.rm = TRUE), x_range[2]) +}) + +test_that('geom_rect handles Inf values correctly with facets (#2364)', { + df <- data.frame( + x = c(1:10, 11:20), + y = c(1:10, 21:30), + facet = rep(c("A", "B"), each = 10) + ) + rect_df <- data.frame(xmin = 3, xmax = 6, ymin = -Inf, ymax = Inf) + + p <- ggplot(df, aes(x, y)) + + geom_point() + + geom_rect( + data = rect_df, + aes(xmin = xmin, xmax = xmax, ymin = ymin, ymax = ymax), + fill = "blue", alpha = 0.2, inherit.aes = FALSE + ) + + facet_wrap(~facet, scales = "free_y") + + L <- plotly_build(p) + + # Find rect traces (one per facet panel) + rect_traces <- Filter(function(tr) identical(tr$fill, "toself"), L$x$data) + + # All traces should have finite coordinates + for (tr in rect_traces) { + expect_false(any(is.infinite(tr$y), na.rm = TRUE)) + expect_false(any(is.infinite(tr$x), na.rm = TRUE)) + } +}) +