I need to loop through i iteration of factors, and each factor needs to be plotted as one plot in a subplot. What I would like to do is hiding the legend for every iteration bar the first one, and use legendgroup to tie all the legends together. This is what I have done so far:
library(plotly)
library(dplyr)
mtcars %>%
mutate(vs = as.factor(vs)) %>%
group_split(cyl) %>%
lapply(function(i) {
#show.legend <- ifelse(i == 1, TRUE, FALSE)
show.legend <- if(i == 1) {TRUE} else {FALSE}
plot_ly(
data = i
,x = ~gear
,y = ~mpg
,color = ~vs
,type = "bar"
,legendgroup = ~vs
) %>%
layout(
barmode = "stack"
,showlegend = show.legend
)
}) %>%
subplot(
nrows = NROW(.)
,shareX = TRUE
,shareY = TRUE
,titleX = TRUE
,titleY = TRUE
,margin = 0.05
)
However this produces an error and no legend:
Warning messages:
1: In if (i == 1) { :
the condition has length > 1 and only the first element will be used
If I use show.legend <- ifelse(i == 1, TRUE, FALSE) (commented out above), I get multiple legends instead of just once.
I am aware I could do the below, but I need to this in a loop.
p1 <- plot_ly(blah, showlegend = TRUE)
p2 <- plot_ly(blah, showlegend = FALSE)
P3 <- plot_ly(blah, showlegend = FALSE)
subplot(p1,p2,p3)
I believe I am not calling the i iteration properly. As another option I tried case_when:
show.legend <- case_when(
i == 1 ~ TRUE
,i != 1 ~ FALSE
)
However this produces the same result as ifelse.
There are two issues in your code:
i is not 1:3 but your current tibble you are iterating through via lapply (see seq_along below).
That is why you get the warning:
In if (i == 1) { : the condition has length > 1 and only the first
element will be used
showlegend needs to be an argument to plot_ly not to layout because subplot always adopts the layout from one of its plots. see ?subplot and its argument which_layout.
layout options found later in the sequence of plots will override
options found earlier in the sequence
Here is what I think you are after:
library(plotly)
library(dplyr)
tibble_list <- mtcars %>%
mutate(vs = as.factor(vs)) %>%
group_split(cyl)
lapply(seq_along(tibble_list), function(i) {
show_legend <- if (i == 1) {TRUE} else {FALSE}
plot_ly(
data = tibble_list[[i]],
x = ~ gear,
y = ~ mpg,
color = ~ vs,
type = "bar",
legendgroup = ~ vs,
showlegend = show_legend
) %>% layout(barmode = "stack")
}) %>% subplot(
nrows = NROW(.),
shareX = TRUE,
shareY = TRUE,
titleX = TRUE,
titleY = TRUE,
margin = 0.05,
which_layout = 1
)
Please find an offical example here.
library(plotly)
library(dplyr)
## store plot as variable p
p <- mtcars %>%
mutate(vs = as.factor(vs)) %>%
group_split(cyl) %>%
lapply(function(i) {
plot_ly(
data = i
,x = ~gear
,y = ~mpg
,color = ~vs
,type = "bar"
,showlegend = TRUE ## include all legends in stored variable
) %>%
layout(
barmode = "stack"
)
}) %>%
subplot(
nrows = NROW(.)
,shareX = TRUE
,shareY = TRUE
,titleX = TRUE
,titleY = TRUE
,margin = 0.05
)
## remove unwanted legends from plot
for (i in seq(3, length(p[["x"]][["data"]]))) {
p[["x"]][["data"]][[i]][["showlegend"]] <- FALSE
}
## show plot
p
Related
I am trying to develop a Business Cycle Clock similar to https://kosis.kr/visual/bcc/index/index.do?language=eng.
I've already achieved most of the things I wanted to replicate, but I can't figure it out how to add these traces (for example, in the link above set speed to 10 and trace length to 5 and then click on 'Apply' to understand what I mean).
Does anyone have any idea how to implement it? It would make the "clock" much easier to read. Thanks in advance.
Reprocible example:
library(plotly)
library(dplyr)
library(magrittr)
variable <- rep('A',10)
above_trend <- rnorm(10)
mom_increase <- rnorm(10)
ref_date <- seq.Date('2010-01-01' %>% as.Date,
length.out = 10,by='m')
full_clock_db <- cbind.data.frame(variable, above_trend, mom_increase, ref_date)
freq_aux = 'm'
ct = 'Brazil'
main_title = paste0('Business Cycle Clock para: ', ct)
m <- list(l=60, r=170, b=50, t=70, pad=4)
y_max_abs = 2
x_max_abs = 5
fig = plot_ly(
data = full_clock_db,
x = ~mom_increase,
y = ~above_trend,
color = ~variable,
frame = ~ref_date,
text = ~variable,
hoverinfo = "text",
type = 'scatter',
mode = 'markers'
) %>%
animation_opts( frame = 800,
transition = 500,
easing = "circle",
redraw = TRUE,
mode = "immediate") %>%
animation_slider(
currentvalue = list(prefix = "PerĂodo", font = list(color="red"))
)
fig
Another more elegant solution would be to rely on ggplot2 + gganimate:
library(ggplot2)
library(gganimate)
ggplot(full_clock_db, aes(x = mom_increase, y = above_trend)) +
geom_point(aes(group = 1L)) +
transition_time(ref_date) +
shadow_wake(wake_length = 0.1, alpha = .6)
You cna play with different shadow_* functions to find the one to your liking.
One way would be to use a line plot and repeat points as necessary. Here's an example as POC:
library(dplyr)
library(plotly)
e <- tibble(x = seq(-3, 3, 0.01)) %>%
mutate(y = dnorm(x)) %>%
mutate(iter = 1:n())
accumulate <- function(data, by, trace_length = 5L) {
data_traf <- data %>%
arrange({{ by }}) %>%
mutate(pos_end = 1:n(),
pos_start = pmax(pos_end - trace_length + 1L, 1L))
data_traf %>%
rowwise() %>%
group_map(~ data_traf %>% slice(seq(.x$pos_start, .x$pos_end, 1L)) %>%
mutate("..{{by}}.new" := .x %>% pull({{by}}))) %>%
bind_rows()
}
enew <- e %>%
accumulate(iter, 100)
plot_ly(x = ~ x, y = ~ y) %>%
add_trace(data = e, type = "scatter", mode = "lines",
line = list(color = "lightgray", width = 10)) %>%
add_trace(data = enew, frame = ~ ..iter.new,
type = "scatter", mode = "lines") %>%
animation_opts(frame = 20, 10)
The idea is that for each step, you keep the trace_length previous steps and assign them to the same frame counter (here ..iter.new). Then you plot lines instead of points and you have a sort of trace..
Using the code snippet from the help page here, I'm trying to create a unique main label for each subplot but am not entirely successful. Any suggestions on how to do this?
library(plotly)
vars <- setdiff(names(economics), "date")
plots <- lapply(vars, function(var) {
plot_ly(economics, x = ~date, y = as.formula(paste0("~", var))) %>%
add_lines(name = var) %>% layout(title = paste("Title for", var, sep=' ') )
})
subplot(plots, nrows = length(plots), shareX = TRUE, titleX = FALSE)
I wish I had noticed this question sooner! Here is one way that you can make this happen.
I took the titles out of the plot build. Otherwise, you'll get an error.
library(plotly)
vars <- setdiff(names(economics), "date")
plots <- lapply(vars, function(var) {
plot_ly(economics, x = ~date, y = as.formula(paste0("~", var))) %>%
add_lines(name = var) #%>% layout(title = paste("Title for", var, sep=' '))
})
Then I created a vector of the titles.
nms <- invisible(lapply(vars, function(v){paste0("Title for ", v)}) %>% unlist())
Here I used annotations to create title objects to add to the subplot.
annots = lapply(
1:length(plots),
function(j){
list(x = .5,
y = 1 - (j - 1) * .205,
xanchor = "center",
yanchor = "center",
xref = "paper",
yref = "paper",
showarrow = F,
text = nms[j])
}
)
subplot(plots, nrows = length(plots), shareX = TRUE, titleX = FALSE) %>%
layout(annotations = annots)
I would like to use a plotly dropdown event to show different grouped boxplots, however I have not been able to achieve this as yet:
The first plot shows expected output for plotly with dropdown = "4" (obtained using ggplot). The second plot is what I get...
library(tidyverse)
library(plotly)
dat <- mtcars %>%
filter(cyl == 4 | carb == 4) %>%
group_by(cyl, carb, am) %>%
summarise(boxplot= list( setNames(boxplot.stats(disp)$stats,
c('lower_whisker','lower_hinge','median','upper_hinge','upper_whisker')) )) %>%
unnest_wider(boxplot) %>%
arrange(cyl, carb, am) %>%
ungroup() %>%
mutate_at(vars(cyl, carb, am), as.character)
cylinders <- unique(dat$cyl)
dat %>%
filter(cyl == 4) %>%
ggplot(aes(
x = carb,
lower = lower_hinge,
upper = upper_hinge,
middle = median,
ymin = lower_whisker,
ymax = upper_whisker,
colour = am)) +
geom_boxplot(stat = "identity")
p <- plot_ly(type = "box")
for(icyl in cylinders){
dataFilt <- filter(dat, cyl == icyl)
p <- add_trace(p,
visible = TRUE,
q1 = dataFilt$lower_hinge,
median = dataFilt$median,
q3 = dataFilt$upper_hinge,
lowerfence = dataFilt$lower_whisker,
upperfence = dataFilt$upper_whisker,
x = dataFilt$carb,
color = dataFilt$am,
name=icyl
)
}
p %>%
layout(boxmode = "group",
updatemenus = list(
list(
y = 0.8,
buttons = list(
list(label = cylinders[1],
method = "update",
args = list(list(visible = c(TRUE, FALSE, FALSE)))),
list(label = cylinders[2],
method = "update",
args = list(list(visible = c(FALSE, TRUE, FALSE)))),
list(label = cylinders[3],
method = "update",
args = list(list(visible = c(FALSE, FALSE, TRUE))))
))))
I wasn't able to get it to work with the loop, but I did get it to work as you had expected. Instead of using the variation where you did all the manual work to create the hinges, whiskers, etc., I used add_boxplot.
In layout you'll see that there are 6 T or F. That's because plotly doesn't save grouping as a function, it transforms the data into two separate traces. For example, the first two are cyl == "4" & am == "0" and cyl == "4" & am == "1"
cylinders <- unique(mtcars$cyl) # kept similar from original work
# added to simplify build
mtcars <- mtcars %>% mutate_at(vars(cyl, carb, am), as.character)
# made all cylinder options subplots
plot_ly() %>%
add_boxplot(x = ~carb, y = ~disp, color = ~am, colors = "Set2",
data = mtcars[mtcars$cyl == cylinders[1], ],
visible = T, inherit = F) %>% # visible
add_boxplot(x = ~carb, y = ~disp, color = ~am, colors = "Set2",
data = mtcars[mtcars$cyl == cylinders[2], ],
visible = F, inherit = F) %>% # invisible
add_boxplot(x = ~carb, y = ~disp, color = ~am, colors = "Set2",
data = mtcars[mtcars$cyl == cylinders[3], ],
visible = F, inherit = F) %>% # invisible
layout(updatemenus = list(
list(
y = 0.8,
buttons = list(
list(label = cylinders[1], # four cyl
method = "restyle",
args = list("visible", list(T, T, F, F, F, F))),
list(label = cylinders[2], # six cyl
method = "restyle",
args = list("visible", list(F, F, T, T, F, F))),
list(label = cylinders[3], # eight cyl
method = "restyle",
args = list("visible", list(F, F, F, F, T, T)))
) # end buttons
)) # end updatedmenus list list
) # end layout
Here's the 4 cyl plotly and your original 4 cyl ggplot
Pb: when I click on the geom_bar bar, the bars switch positions even though I properly set the levels in the aes call.
Please try below the simplest example I could come up with.
All it does is add alpha to the bars below the clicked one.
Problem: click bars and see them switching position.
The alpha is added with the 'type' variable that is updated in dat() on click event.
If I deactivate the aes call in geom_bar the problem doesn't occur. Nor does it happen if I place the alpha in the main aes() rather than geom_bar's one.
The reactiveVal dat()'s type is unchanged, so even though the bars switch position, for the click logic they do not (you can test this by clicking on the same spot twice: on the first bar will switch position, not in the second).
library(shiny); library(tidyverse)
ui <- function() {
plotOutput(outputId = "bar",click = "click")
}
server <- function(input, output, session) {
dat <- reactiveVal(
tibble(value = 1:4,
name = c("a", "b", "a", "b"),
type = c("small", "small", "big", "big"),
cut_off = TRUE )
)
last_click <- reactiveVal(NULL)
observeEvent(input$click, {
if (!is.null(input$click)) last_click(input$click)
})
clicked_sample <- eventReactive(last_click(), {
if (is.null(last_click())) return(NULL)
click_x <- last_click()$x
splits <- seq(1/4, 1 - 1/4, 1/2)
sample_lvls <- dat()$name %>%
as_factor() %>%
levels()
clicked_sample_name <- sample_lvls[round(click_x)]
types <- dat()$type %>% unique() %>% sort()
x <- click_x - round(click_x) + 1/2
clicked_type <- types[which.min(abs(splits - x))]
dat() %>%
filter(type == clicked_type & name == clicked_sample_name)
}, ignoreNULL = FALSE)
observeEvent(clicked_sample(), {
dat(
dat() %>%
mutate(cut_off = if_else(
value >= clicked_sample()$value,
TRUE,
FALSE,
missing = FALSE)
)
)
})
output$bar <- renderPlot({
g <- ggplot(dat()) +
aes(x = name, y = value,
fill = factor(type,
levels = type %>%
as.character() %>%
unique() %>%
sort())) +
geom_bar(
aes(alpha = cut_off %>% factor(levels = c(FALSE, TRUE))),
position = "dodge",
stat = "identity"
) +
scale_alpha_discrete(guide = "none", drop = FALSE)
if (!is.null(clicked_sample()$value)) {
g + geom_hline(yintercept = clicked_sample()$value)
} else {
g
}
})
}
shinyApp(ui, server)
The issues appears to be that as it is the bars start off being ordered by value within the groups a and b, however as you click the bars the values of your cutoff variable change from all TRUE to being a mixture of TRUE and FALSE. This then causes the plot to try to sort the bars within the groups by the cutoff value since it is a factor (the bars with a TRUE value are always switched to the right of any bar with a FALSE, while the FALSE bars go back to being sorted by value, all within the groups a and b). To avoid this from happening, you can include all of your aes within the geom_bar, so your plot function would be like this:
g <- ggplot(dat()) +
geom_bar(
aes(x = name, y = value,
fill = factor(type,
levels = type %>%
as.character() %>%
unique() %>%
sort()),
alpha = cut_off %>% factor(levels = c(FALSE, TRUE))),
position = "dodge",
stat = "identity"
) +
scale_alpha_discrete(guide = "none", drop = FALSE)
I am having problem with the following example ggvis code which is meant to make a plot that highlights an entire group of points when you hover over any member of that group. I would then like the highlighting to vanish as soon as you hover off. What is happening is that the highlighting initially works but then when you hover off, the highlighting stays, and only vanishes when you hover over another set of points and then hover off them again.
library(magrittr)
library(dplyr)
library(ggvis)
library(shiny)
dat <- iris %>% select(-Species) %>% dist %>% cmdscale %>% data.frame %>% tbl_df %>% mutate(Species = iris$Species) %>%
data.frame
Props <- reactiveValues(Size = rep(50, length.out = nrow(dat)), Stroke = rep("white", length.out = nrow(dat)))
hoveron <- function(data, ...) {
Props$Size[dat$Species == data$Species] <- 150
print("hoveron!")
Props$Stroke[dat$Species == data$Species] <- "black"
}
hoveroff <- function(...) {
Props$Size <- rep(50, length.out = nrow(dat))
print("hoveroff!")
Props$Stroke <- rep("white", length.out = nrow(dat))
}
dat %>%
ggvis(~X1, ~X2, fill = ~Species) %>% layer_points(size = reactive(Props$Size), stroke = reactive(Props$Stroke)) %>%
scale_numeric("size", range = c(80, 180)) %>% scale_numeric("x", label = "MDS Axis 1") %>%
scale_numeric("y", label = "MDS Axis 2") %>% scale_ordinal("stroke", sort = TRUE, domain = c("black", "white"), range = c("black", "white")) %>%
add_legend(scales = "size", properties = legend_props(title = list(fontSize = 0), labels = list(fontSize = 0), symbols = list(size = 0))) %>%
add_legend(scales = "stroke", properties = legend_props(title = list(fontSize = 0), labels = list(fontSize = 0), symbols = list(size = 0))) %>%
set_options(duration = 0) %>% handle_hover(hoveron, hoveroff)
You can view the results as a shinyapp here: https://ecologician.shinyapps.io/ggvis_grouping_wrong/. Note: The print statements are for debugging. hoveroff seems to fire when you mover off the first set of points but then hoveron fires immediately afterwards, with data$Species equal to what was just hovered off. I can't quite explain why. I am hoping it is just a simple mistake which I just can't see at the moment. Can anyone here see what is wrong?
More Details:
The above code was an attempt to make a less verbose / simpler version of the code below, which does work as I expect it to:
library(magrittr)
library(dplyr)
library(ggvis)
library(shiny)
hoverset <- reactiveValues(setosa = 0, versicolor = 0, virginica = 0)
hoveron <- function(data, ...) {
hoverset[[data$Species]] <- 1
}
hoveroff <- function(data, ...) {
hoverset$setosa <- 0
hoverset$versicolor <- 0
hoverset$virginica <- 0
}
dat <- iris %>% select(-Species) %>% dist %>% cmdscale %>% data.frame %>% tbl_df %>% mutate(Species = iris$Species) %>%
mutate(Size = 50, Stroke = "white") %>% data.frame
dat2 <- reactive({
if (hoverset$setosa == 1){
dat[dat[,"Species"] == "setosa","Size"] <<- 150
dat[dat[,"Species"] == "setosa","Stroke"] <<- "black"
} else {
dat[dat[,"Species"] == "setosa","Size"] <<- 50
dat[dat[,"Species"] == "setosa","Stroke"] <<- "white"
}
if (hoverset$versicolor == 1){
dat[dat[,"Species"] == "versicolor","Size"] <<- 150
dat[dat[,"Species"] == "versicolor","Stroke"] <<- "black"
} else {
dat[dat[,"Species"] == "versicolor","Size"] <<- 50
dat[dat[,"Species"] == "versicolor","Stroke"] <<- "white"
}
if (hoverset$virginica == 1){
dat[dat[,"Species"] == "virginica","Size"] <<- 150
dat[dat[,"Species"] == "virginica","Stroke"] <<- "black"
} else {
dat[dat[,"Species"] == "virginica","Size"] <<- 50
dat[dat[,"Species"] == "virginica","Stroke"] <<- "white"
}
dat
})
dat2 %>%
ggvis(~X1, ~X2, fill = ~Species) %>% layer_points(size = ~Size, stroke = ~Stroke) %>%
scale_numeric("size", range = c(80, 180)) %>% scale_numeric("x", label = "MDS Axis 1") %>%
scale_numeric("y", label = "MDS Axis 2") %>% scale_ordinal("stroke", sort = TRUE, domain = c("black", "white"), range = c("black", "white")) %>%
add_legend(scales = "size", properties = legend_props(title = list(fontSize = 0), labels = list(fontSize = 0), symbols = list(size = 0))) %>%
add_legend(scales = "stroke", properties = legend_props(title = list(fontSize = 0), labels = list(fontSize = 0), symbols = list(size = 0))) %>%
set_options(duration = 0) %>% handle_hover(hoveron, hoveroff)
See this app here: https://ecologician.shinyapps.io/ggvis_grouping/
Thanks!