m <- crosstalk::SharedData$new(mtcars, ~vs) p <- plot_ly(m, x = ~wt, y = ~mpg) %>% add_markers() test_that("SharedData produces key/set in plot_ly", { tr <- plotly_build(p)$x$data[[1]] expect_true(all(tr$key == m$key())) expect_identical(tr$set, m$groupName()) expect_false(tr$`_isNestedKey` %||% FALSE) expect_false(tr$`_isSimpleKey` %||% FALSE) }) test_that("Warning is thrown when clickmode='select' is used with crosstalk", { expect_warning( plotly_build(layout(p, clickmode = "select")), "not designed to work well" ) }) test_that("SharedData produces key/set in ggplotly", { p <- ggplot(m, aes(x = wt, y = mpg)) + geom_point() tr <- plotly_build(p)$x$data[[1]] expect_true(all(tr$key == m$key())) expect_type(tr$set, "character") expect_length(tr$set, 1) expect_false(tr$`_isNestedKey` %||% FALSE) expect_false(tr$`_isSimpleKey` %||% FALSE) }) test_that("crosstalk keys are inherited in a layer with inherit = FALSE", { p <- txhousing %>% group_by(city) %>% crosstalk::SharedData$new(~city, "Select a city") %>% plot_ly(x = ~date, y = ~median) %>% add_lines(alpha = 0.2) %>% add_ribbons( x = c(2016, 2017), ymin = c(150000, 160000), ymax = c(200000, 190000), inherit = FALSE ) b <- plotly_build(p) # second trace should have key/set info expect_null(b$x$data[[2]][["key"]]) expect_null(b$x$data[[2]][["set"]]) # first trace should k <- unique(b$x$data[[1]]$key) expect_equal(sort(k[!is.na(k)]), sort(unique(txhousing$city))) expect_true(b$x$data[[1]][["set"]] == "Select a city") }) test_that("Simple scatterplot brushing with plot_ly() and subplot()", { p <- mtcars %>% crosstalk::SharedData$new(group = "testing") %>% plot_ly(x = ~mpg, y = ~wt) b <- subplot(p, p) %>% highlight("plotly_selected") %>% plotly_build() expect_true(all(b$x$data[[1]]$key == row.names(mtcars))) expect_true(all(b$x$data[[2]]$key == row.names(mtcars))) expect_true(b$x$data[[1]]$set == "testing") expect_true(b$x$layout$dragmode == "select") }) test_that("group_by.plotly() retains crosstalk set", { b <- mtcars %>% crosstalk::SharedData$new(group = "foo") %>% plot_ly(x = ~mpg, y = ~hp) %>% group_by(am) %>% add_markers() %>% plotly_build() expect_equal(b$x$data[[1]]$set, "foo") expect_true(all(b$x$data[[1]]$key == row.names(mtcars))) }) test_that("highlight(selectize) produces a sensible payload", { p <- plot_ly() %>% add_lines(data = mtcars, x = ~wt, y = ~mpg) %>% add_markers( data = highlight_key(mtcars, ~cyl, "Choose cylinder"), x = ~wt, y = ~mpg ) # Builds basic payload when selectize=TRUE b <- p %>% highlight(selectize = TRUE) %>% plotly_build() selectize <- list( items = data.frame(value = c(6, 4, 8), label = c(6, 4, 8)), group = "Choose cylinder" ) expect_length(b$x$selectize, 1) expect_equal(b$x$selectize[[1]], selectize) # Copies over any list() options b2 <- p %>% highlight(selectize = list(plugins = list("remove_button"))) %>% plotly_build() selectize$plugins <- list("remove_button") expect_length(b2$x$selectize, 1) expect_equal(b2$x$selectize[[1]], selectize) # Can also tack on options after building, and plotly_build() won't overwrite b2$x$selectize[[1]] <- modifyList( b2$x$selectize[[1]], list(foo = "bar") ) b2 <- plotly_build(b2) selectize$foo <- "bar" expect_equal(b2$x$selectize[[1]], selectize) }) # Ignore for now https://github.com/ggobi/ggally/issues/264 #test_that("SharedData produces key/set in ggpairs", { # p <- GGally::ggpairs(m, columns = 1:3) # l <- plotly_build(p)$x # # for (i in seq_along(l$data)) { # tr <- l$data[[i]] # if (tr$mode != "markers") next # expect_true(all(tr$key == m$key())) # expect_identical(tr$set, m$groupName()) # expect_false(tr$`_isNestedKey` %||% FALSE) # expect_false(tr$`_isSimpleKey` %||% FALSE) # } # #}) test_that("When key is equivalent to group, produce simple keys", { gg <- ggplot(m, aes(wt, mpg, color = factor(vs))) + geom_point() + geom_smooth(se = FALSE) # for interactive testing -- `highlight(gg, "plotly_click")` l <- plotly_build(gg)$x for (i in seq_along(l$data)) { tr <- l$data[[i]] expect_false(tr$`_isNestedKey` %||% FALSE) if (tr$mode == "markers") { # clicking on a single point should select the whole group in a efficient # (i.e., no trace subsetting occurs for simple keys) manner expect_true(tr$key == tr$name) expect_true(tr$`_isSimpleKey`) } else { # TODO: shouldn't key be a length 1 here? expect_true(tr$name %in% tr$key) expect_true(tr$`_isSimpleKey`) } } }) m2 <- crosstalk::SharedData$new(mtcars) test_that("When key is nested within group, produce simple key", { gg <- ggplot(m2, aes(wt, mpg, color = factor(vs))) + geom_point() + geom_smooth(se = FALSE) # for interactive testing -- `highlight(gg, "plotly_click")` l <- plotly_build(gg)$x for (i in seq_along(l$data)) { tr <- l$data[[i]] key <- m2$key()[mtcars$vs == tr$name] expect_true(all(tr$key == key)) if (tr$mode == "markers") { expect_false(tr$`_isSimpleKey` %||% FALSE) expect_false(tr$`_isNestedKey` %||% FALSE) } else { expect_true(tr$`_isSimpleKey`) expect_false(tr$`_isNestedKey` %||% FALSE) } } }) test_that("Key structure is passed along to frame data", { p <- ggplot(m2, aes(wt, mpg, color = factor(vs), frame = am)) + geom_point() + geom_smooth(se = FALSE) # TODO: why doesn't the highlight update on the second frame? # animation_opts(p, 0, redraw = T) %>% highlight("plotly_click") l <- suppressWarnings(plotly_build(p)$x) for (i in seq_along(l$data)) { tr <- l$data[[i]] key <- m2$key()[mtcars$vs == tr$name & mtcars$am == tr$frame] expect_true(all(tr$key == key)) } # the fitted line of every frame should have a simple key for (i in seq_along(l$frames)) { fr <- l$frames[[i]] for (j in seq_along(fr$data)) { tr <- fr$data[[j]] if (tr$mode != "lines") next expect_true(tr$`_isSimpleKey`) } } }) test_that("can handle inconsistent # of traces across frames & supply default colors", { d <- data.frame( y = rnorm(20), score = c(1,1,1,1,2,2,2,2,3,3,3,3,1,1,1,1,2,2,2,2), population = c(rep(1, 12), rep(2, 8)) ) p <- plot_ly(d, y = ~y, split = ~as.factor(score), frame = ~population) %>% add_boxplot() l <- plotly_build(p)$x expect_length(l$data, 3) # default colors are the plotly.js defaults cols <- sapply(l$data, function(x) x$line$color) defaultCols <- toRGB(colorway()[1:3]) expect_equivalent(cols, defaultCols) # trace names reflect the split/score (i.e., frames are removed) nms <- sapply(l$data, "[[", "name") expect_equivalent(nms, levels(as.factor(d$score))) # 2 frames: both with 3 traces expect_length(l$frames, 2) expect_length(l$frames[[1]]$data, 3) expect_length(l$frames[[2]]$data, 3) # make sure the frames are targetting the right traces expect_equivalent(l$frames[[1]]$traces, 0:2) expect_equivalent(l$frames[[2]]$traces, 0:2) # 1st frame has all 3 traces visible; 2nd frame has 2 visible expect_true( unique(sapply(l$frames[[1]]$data, "[[", "visible")) ) expect_identical( sapply(l$frames[[2]]$data, "[[", "visible"), c(TRUE, TRUE, FALSE) ) # ensure the default colors remain consistent throughout the animation cols <- sapply(l$frames[[1]]$data, function(x) x$line$color) expect_equivalent(cols, defaultCols) cols <- sapply(l$frames[[2]]$data, function(x) x$line$color) expect_equivalent(cols, defaultCols) # ensure the animation defaults are supplied buttonArgs <- l$layout$updatemenus[[1]]$buttons[[1]]$args[[2]] defaults <- animation_opts_defaults() expect_identical( buttonArgs[names(defaults)], defaults ) # step values reflect the frame values steps <- l$layout$sliders[[1]]$steps expect_equivalent( unlist(lapply(steps, function(s) s$args[[1]])), c("1", "2") ) # all the slider steps reflect the animation default res <- lapply(steps, function(s) { expect_identical(s$args[[2]], defaults) }) }) test_that("can change animation defaults", { data(mtcars) p <- plot_ly(mtcars, x = ~wt, y = ~mpg, frame = ~cyl) %>% animation_opts(frame = 1200, transition = 1000, easing = "elastic") %>% animation_button( x = 1, xanchor = "right", y = 0, yanchor = "bottom" ) %>% animation_slider( currentvalue = list(prefix = "YEAR ", font = list(color="red")) ) l <- plotly_build(p)$x expect_length(l$data, 1) expect_length(l$frames, 3) cyl <- as.character(unique(sort(mtcars$cyl))) for (i in seq_along(l$frames)) { f <- l$frames[[i]] expect_equivalent(f$name, cyl[[i]]) expect_length(f$data, 1) } # the expectation for animation option values aniOpts <- modify_list( rapply(animation_opts_defaults(), unclass, how = "list"), list( frame = list(duration = 1200), transition = list(duration = 1000, easing = "elastic") ) ) # ensure the animation options are supplied buttonArgs <- l$layout$updatemenus[[1]]$buttons[[1]]$args[[2]] expect_equivalent( buttonArgs[names(aniOpts)], aniOpts ) # step values reflect the frame values steps <- l$layout$sliders[[1]]$steps expect_equivalent( unlist(lapply(steps, function(s) s$args[[1]])), cyl ) # all the slider steps reflect the animation options res <- lapply(steps, function(s) { expect_identical( s$args[[2]], aniOpts ) }) }) test_that("simple animation targeting works", { df <- data.frame( x = c(1, 2, 2, 1, 1, 2), y = c(1, 2, 2, 1, 1, 2), z = c(1, 1, 2, 2, 3, 3) ) p <- plot_ly(df) %>% add_markers(x = 1.5, y = 1.5) %>% add_markers(x = ~x, y = ~y, frame = ~z) l <- plotly_build(p)$x expect_length(l$data, 2) for (i in seq_along(l$data)) { tr <- l$data[[i]] # trace names are empty expect_equivalent(tr$name %||% "no-name", "no-name") # color defaults are retained expect_true(tr$marker$color == toRGB(colorway()[[i]])) } # frame trace names are empty expect_length(l$frames, 3) for (i in seq_along(l$frames)) { f <- l$frames[[i]] for (j in seq_along(f$data)) { tr <- f$data[[j]] # trace names are empty expect_equivalent(tr$name %||% "no-name", "no-name") # color defaults are retained expect_true(tr$marker$color == toRGB(colorway()[[2]])) } } # since all trace types are scatter, redraw = FALSE buttonArgs <- l$layout$updatemenus[[1]]$buttons[[1]]$args expect_false(buttonArgs[[2]]$frame$redraw) steps <- l$layout$sliders[[1]]$steps res <- lapply(steps, function(s) { expect_false(s$args[[2]]$frame$redraw) }) }) test_that("animation frames are boxed up correctly", { dallas <- subset(txhousing, city == "Dallas" & month == 1) p <- ggplot(dallas, aes(x = volume, y = sales, frame = year)) + geom_point() l <- plotly_build(p)$x for (i in seq_along(l$frames)) { traces <- l$frames[[i]]$data for (j in seq_along(traces)) { x <- traces[[j]]$x y <- traces[[j]]$y expect_true(length(x) > 1 || inherits(x, "AsIs")) expect_true(length(y) > 1 || inherits(y, "AsIs")) } } }) test_that("animation button can be customized", { p <- plot_ly(mtcars, x = ~mpg, y = ~wt, frame = ~vs) %>% animation_button(label = "Custom", bgcolor = "red", font = list(color = "white")) f <- plotly_build(p)$x menu <- f$layout$updatemenus[[1]] expect_true(menu$bgcolor == "red") expect_true(menu$font$color == "white") expect_true(menu$buttons[[1]]$label == "Custom") }) test_that("sf works with crosstalk", { skip_if_not_installed("sf") skip_if_not_installed("s2") skip_if_not_installed("ggthemes") nc <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE) # shared data will make the polygons "query-able" ncsd <- crosstalk::SharedData$new(nc) p <- ggplot(ncsd) + geom_sf(aes(fill = AREA, text = paste0(NAME, "\n", "FIPS: ", FIPS))) + ggthemes::theme_map() gg <- ggplotly(p, tooltip = "text") d <- gg$x$data for (i in seq_along(d)) { if (!isTRUE(d[["_isGraticule"]])) next expect_false(is.null(d[[i]]$key)) expect_false(is.null(d[[i]]$set)) } })