tests/testthat/test-tf-ggplot-complex.R

# Tests for complex tf_ggplot use cases
# Tests complex expressions in tf aesthetics and derived quantities

test_that("tf aesthetic with function transformations works", {
  skip_if_not_installed("ggplot2")
  skip_if_no_tf_ggplot()

  # Create test data with positive and negative values
  set.seed(123)
  data <- data.frame(id = 1:3)
  arg_vals <- seq(-2, 2, length.out = 11)
  func_vals <- list(
    sin(arg_vals), # Has negative values
    cos(arg_vals), # Has negative values
    arg_vals^2 - 1 # Has negative values
  )
  data$curves <- tfd(func_vals, arg = arg_vals)

  # Test abs() transformation
  expect_no_error({
    p_abs <- tf_ggplot(data, aes(tf = abs(curves))) +
      geom_line()
  })

  # Check that plot builds correctly
  built_abs <- ggplot_build(p_abs)
  plot_data_abs <- built_abs$data[[1]]

  # All y values should be non-negative after abs()
  expect_true(all(plot_data_abs$y >= 0))

  # Should have 3 groups (one per function)
  expect_equal(length(unique(plot_data_abs$group)), 3)

  # Should have correct number of rows (3 functions × 11 points)
  expect_equal(nrow(plot_data_abs), 33)
})

test_that("tf aesthetic with mathematical operations works", {
  skip_if_not_installed("ggplot2")
  skip_if_no_tf_ggplot()

  set.seed(456)
  data <- data.frame(id = 1:2)
  data$func1 <- tf_rgp(2, arg = seq(0, 1, length.out = 6))
  data$func2 <- tf_rgp(2, arg = seq(0, 1, length.out = 6))

  # Test arithmetic operations
  expect_no_error({
    p_sum <- tf_ggplot(data, aes(tf = func1 + func2)) +
      geom_line()
    p_scaled <- tf_ggplot(data, aes(tf = 2 * func1)) +
      geom_line()
  })

  # Check that plots build correctly
  built_sum <- ggplot_build(p_sum)
  built_scaled <- ggplot_build(p_scaled)

  expect_equal(length(unique(built_sum$data[[1]]$group)), 2)
  expect_equal(length(unique(built_scaled$data[[1]]$group)), 2)
  expect_equal(nrow(built_sum$data[[1]]), 12) # 2 functions × 6 points
  expect_equal(nrow(built_scaled$data[[1]]), 12)
})

test_that("color aesthetic with tf-derived quantities works", {
  skip_if_not_installed("ggplot2")
  skip_if_no_tf_ggplot()

  set.seed(789)
  data <- data.frame(id = 1:4)
  data$curves <- tf_rgp(4, arg = seq(0, 1, length.out = 11))

  # Test color by tf_depth
  expect_no_error({
    p_depth <- tf_ggplot(data, aes(tf = curves, color = tf_depth(curves))) +
      geom_line()
  })

  built_depth <- ggplot_build(p_depth)
  plot_data_depth <- built_depth$data[[1]]

  # Should have 4 groups (one per function)
  expect_equal(length(unique(plot_data_depth$group)), 4)

  # Color should vary based on depth
  expect_true(length(unique(plot_data_depth$colour)) > 1)

  # Each group should have consistent color within group
  color_by_group <- split(plot_data_depth$colour, plot_data_depth$group)
  consistent_colors <- all(sapply(
    color_by_group,
    function(x) length(unique(x)) == 1
  ))
  expect_true(consistent_colors)
})

test_that("multiple derived aesthetics work together", {
  skip_if_not_installed("ggplot2")
  skip_if_no_tf_ggplot()

  set.seed(101112)
  data <- data.frame(id = 1:5)
  data$curves <- tf_rgp(5, arg = seq(0, 1, length.out = 11))

  # Test multiple tf-derived aesthetics
  expect_no_error({
    p_multi <- tf_ggplot(
      data,
      aes(
        tf = curves,
        color = tf_depth(curves),
        alpha = tf_fmin(curves)
      )
    ) +
      geom_line()
  })
  expect_no_error(print(p_multi))
  built_multi <- ggplot_build(p_multi)
  plot_data_multi <- built_multi$data[[1]]

  # Should have all aesthetic mappings
  expect_true("colour" %in% names(plot_data_multi))
  expect_true("alpha" %in% names(plot_data_multi))

  # Should have 5 groups
  expect_equal(length(unique(plot_data_multi$group)), 5)

  # Alpha should vary based on function range
  expect_true(length(unique(plot_data_multi$alpha)) > 1)
})

test_that("complex tf expressions in constructor vs geom are equivalent", {
  skip_if_not_installed("ggplot2")
  skip_if_no_tf_ggplot()

  set.seed(131415)
  data <- data.frame(id = 1:3)
  data$curves <- tf_rgp(3, arg = seq(-1, 1, length.out = 9))

  # Method 1: Complex expression in constructor
  p1 <- tf_ggplot(data, aes(tf = abs(curves), color = tf_depth(curves))) +
    geom_line()

  # Method 2: Complex expression in geom
  p2 <- tf_ggplot(data) +
    geom_line(aes(tf = abs(curves), color = tf_depth(curves)))

  # Both should build successfully
  built1 <- ggplot_build(p1)
  built2 <- ggplot_build(p2)

  # Should have same dimensions
  expect_equal(dim(built1$data[[1]]), dim(built2$data[[1]]))

  # Should have same number of groups
  expect_equal(
    length(unique(built1$data[[1]]$group)),
    length(unique(built2$data[[1]]$group))
  )

  # Both should have only non-negative y values (due to abs)
  expect_true(all(built1$data[[1]]$y >= 0))
  expect_true(all(built2$data[[1]]$y >= 0))

  # Colors should vary in both (due to tf_depth)
  expect_true(length(unique(built1$data[[1]]$colour)) > 1)
  expect_true(length(unique(built2$data[[1]]$colour)) > 1)
})

test_that("nested function calls in tf aesthetics work", {
  skip_if_not_installed("ggplot2")
  skip_if_no_tf_ggplot()

  set.seed(161718)
  data <- data.frame(id = 1:2)
  data$curves <- tf_rgp(2, arg = seq(-1, 1, length.out = 7))

  # Test nested function calls
  expect_no_error({
    p_nested <- tf_ggplot(data, aes(tf = sqrt(abs(curves)))) +
      geom_line()
  })

  built_nested <- ggplot_build(p_nested)
  plot_data_nested <- built_nested$data[[1]]

  # All y values should be non-negative (due to sqrt(abs()))
  expect_true(all(plot_data_nested$y >= 0))
  expect_true(all(is.finite(plot_data_nested$y)))

  # Should have 2 groups
  expect_equal(length(unique(plot_data_nested$group)), 2)
})

test_that("tf aesthetics with scalars and derived quantities", {
  skip_if_not_installed("ggplot2")
  skip_if_no_tf_ggplot()

  set.seed(192021)
  data <- data.frame(
    id = 1:4,
    treatment = factor(c("A", "A", "B", "B")),
    baseline = rnorm(4)
  )
  data$curves <- tf_rgp(4, arg = seq(0, 1, length.out = 8))

  # Mix tf aesthetics with scalar summaries and regular variables
  expect_no_error({
    p_mixed <- tf_ggplot(
      data,
      aes(
        tf = curves,
        color = treatment, # Regular factor variable
        linewidth = tf_fmean(curves), # Scalar summary of tf
        alpha = abs(baseline) # Transformation of scalar
      )
    ) +
      geom_line()
  })

  built_mixed <- ggplot_build(p_mixed)
  plot_data_mixed <- built_mixed$data[[1]]

  # Should have all aesthetics
  expect_true("colour" %in% names(plot_data_mixed))
  expect_true("linewidth" %in% names(plot_data_mixed))
  expect_true("alpha" %in% names(plot_data_mixed))

  # Should have 4 groups (one per function)
  expect_equal(length(unique(plot_data_mixed$group)), 4)

  # Linewidth should vary based on function means
  expect_true(length(unique(plot_data_mixed$linewidth)) > 1)

  # Alpha should vary based on baseline
  expect_true(length(unique(plot_data_mixed$alpha)) > 1)

  # Color should reflect treatment levels (2 levels)
  expect_equal(length(unique(plot_data_mixed$colour)), 2)
})

test_that("tf transformations preserve grouping correctly", {
  skip_if_not_installed("ggplot2")
  skip_if_no_tf_ggplot()

  set.seed(222324)
  data <- data.frame(
    id = 1:6,
    group = factor(rep(c("X", "Y", "Z"), each = 2))
  )
  data$curves <- tf_rgp(6, arg = seq(0, 1, length.out = 5))

  # Test transformation with grouping variable
  p_transform <- tf_ggplot(
    data,
    aes(
      tf = abs(curves - tf_fmean(curves)), # Center around mean then abs
      color = group
    )
  ) +
    geom_line()

  built_transform <- ggplot_build(p_transform)
  plot_data_transform <- built_transform$data[[1]]

  # Should have 6 groups (one per function)
  expect_equal(length(unique(plot_data_transform$group)), 6)

  # Should have 3 colors (for groups X, Y, Z)
  expect_equal(length(unique(plot_data_transform$colour)), 3)

  # All y values should be non-negative (due to abs)
  expect_true(all(plot_data_transform$y >= 0))

  # Should have 30 rows (6 functions × 5 points)
  expect_equal(nrow(plot_data_transform), 30)
})

test_that("error handling for invalid tf expressions", {
  skip_if_not_installed("ggplot2")
  skip_if_no_tf_ggplot()

  set.seed(252627)
  data <- data.frame(id = 1:2)
  data$curves <- tf_rgp(2, arg = seq(0, 1, length.out = 5))
  data$not_tf <- rnorm(2)

  # Should error when applying tf functions to non-tf objects
  expect_error(
    {
      p <- tf_ggplot(data, aes(tf = abs(not_tf))) +
        geom_line()
      ggplot_build(p) # Trigger validation
    },
    "tf.*object|must be.*tf"
  )

  # Should error when tf aesthetic references non-existent column
  expect_error(
    {
      p <- tf_ggplot(data, aes(tf = nonexistent_func)) +
        geom_line()
      ggplot_build(p) # Trigger validation
    },
    "not found|object.*not found"
  )
})

test_that("performance with complex tf expressions", {
  skip_if_not_installed("ggplot2")
  skip_if_no_tf_ggplot()

  # Test with moderately large dataset that triggers warning
  set.seed(282930)
  data <- data.frame(id = 1:100) # 100 functions
  data$curves <- tf_rgp(100, arg = seq(0, 1, length.out = 101)) # 101 points each (100 * 101 = 10100 > 10000)

  # Create large plot (warning logic needs to be re-added to finalization process)
  p_large <- tf_ggplot(
    data,
    aes(
      tf = abs(curves),
      color = tf_depth(curves)
    )
  ) +
    geom_line()

  # But should still build correctly
  built_large <- ggplot_build(p_large)
  plot_data_large <- built_large$data[[1]]

  # Should have 100 groups
  expect_equal(length(unique(plot_data_large$group)), 100)

  # Should have 10100 rows (100 functions × 101 points)
  expect_equal(nrow(plot_data_large), 10100)

  # All y values should be non-negative
  expect_true(all(plot_data_large$y >= 0))
})

test_that("complex expressions work with faceting", {
  skip_if_not_installed("ggplot2")
  skip_if_no_tf_ggplot()

  set.seed(313233)
  data <- data.frame(
    id = 1:8,
    condition = factor(rep(c("Control", "Treatment"), each = 4)),
    replicate = factor(rep(1:4, 2))
  )
  data$curves <- tf_rgp(8, arg = seq(0, 1, length.out = 6))

  # Test complex expressions with faceting
  expect_no_error({
    p_facet <- tf_ggplot(
      data,
      aes(
        tf = abs(curves),
        color = tf_depth(curves)
      )
    ) +
      geom_line() +
      facet_wrap(~condition)
  })

  built_facet <- ggplot_build(p_facet)

  # Should have 2 panels (Control, Treatment)
  expect_equal(length(unique(built_facet$layout$layout$PANEL)), 2)

  # Should have 8 groups total
  plot_data_facet <- built_facet$data[[1]]
  expect_equal(length(unique(plot_data_facet$group)), 8)

  # All y values should be non-negative
  expect_true(all(plot_data_facet$y >= 0))
})

test_that("tf expressions work with custom evaluation grids", {
  skip_if_not_installed("ggplot2")
  skip_if_no_tf_ggplot()

  set.seed(343536)
  data <- data.frame(id = 1:3)
  data$curves <- tf_rgp(3, arg = seq(0, 1, length.out = 21)) # Dense original grid

  # Test with custom coarser grid
  custom_arg <- seq(0, 1, length.out = 6) # Coarser grid

  expect_no_error({
    p_custom <- tf_ggplot(data, aes(tf = abs(curves)), arg = custom_arg) +
      geom_line()
  })

  built_custom <- ggplot_build(p_custom)
  plot_data_custom <- built_custom$data[[1]]

  # Should use custom grid (6 points)
  expect_equal(length(unique(plot_data_custom$x)), 6)
  expect_equal(sort(unique(plot_data_custom$x)), custom_arg)

  # Should have 18 rows (3 functions × 6 points)
  expect_equal(nrow(plot_data_custom), 18)

  # All y values should be non-negative
  expect_true(all(plot_data_custom$y >= 0))
})

Try the tidyfun package in your browser

Any scripts or data that you put into this service are public.

tidyfun documentation built on April 24, 2026, 5:06 p.m.