Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions R/layers2traces.R
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
36 changes: 36 additions & 0 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,42 @@ is.discrete <- function(x) {
# standard way to specify a line break
br <- function() "<br />"

# 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")
}
Expand Down
27 changes: 27 additions & 0 deletions tests/testthat/test-ggplot-lines.R
Original file line number Diff line number Diff line change
Expand Up @@ -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])
})
60 changes: 60 additions & 0 deletions tests/testthat/test-ggplot-polygons.R
Original file line number Diff line number Diff line change
Expand Up @@ -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])
})
81 changes: 81 additions & 0 deletions tests/testthat/test-ggplot-rect.R
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
})