Related
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
I have a dataset and would like to do some exploratory data analysis before building a predictive model. All variables are categorical. I know that I can use 'dataExplorer' to do some quick EDA:
library(tidyverse)
library(dataExplorer)
dat <- data.frame(circuit = sample(c("China", "Murica", "Brazil"), 100, replace = T),
driver = sample(c("Kimi", "Seb", "Max", "Lando", "Lance"), 100, replace = T),
opinion = sample(c("Garbage", "Not.Garbage"), 100, replace = T, prob = c(0.8, 0.2)))
dat %>%
select(-opinion) %>%
plot_bar
However, I want the bars for 'circuit' and 'driver' to be filled in to represent the respective proportions of 'opinion' for each variable (see below). This is so that I can see which predictor variables are most closely associated with my outcome variable.
dat %>%
ggplot(aes(x = circuit, fill = opinion)) +
geom_histogram(stat = "count")
However, I don't want to build each plot individually and then use grid.arrange to organize them.
Thanks for any help :)
I don't think there is an easy way unless you tweak the plot_bar function, since it is currently designed to visualize univariate distribution. Run the following function and it should work with your example:
library(tidyverse)
library(data.table) ## Note: You will need to load data.table
library(DataExplorer)
## Rewrite plot_bar
plot_bar2 <- function(data, group, with = NULL, maxcat = 50, order_bar = TRUE, binary_as_factor = TRUE, title = NULL, ggtheme = theme_gray(), theme_config = list(), nrow = 3L, ncol = 3L, parallel = FALSE) {
frequency <- measure <- variable <- value <- NULL
if (!is.data.table(data)) data <- data.table(data)
split_data <- split_columns(data, binary_as_factor = binary_as_factor)
if (split_data$num_discrete == 0) stop("No discrete features found!")
discrete <- split_data$discrete
ind <- DataExplorer:::.ignoreCat(discrete, maxcat = maxcat)
if (length(ind)) {
message(length(ind), " columns ignored with more than ", maxcat, " categories.\n", paste0(names(ind), ": ", ind, " categories\n"))
drop_columns(discrete, names(ind))
if (length(discrete) == 0) stop("Note: All discrete features ignored! Nothing to plot!")
}
feature_names <- names(discrete)
if (is.null(with)) {
dt <- discrete[, list(frequency = .N), by = feature_names]
} else {
if (is.factor(data[[with]])) {
measure_var <- suppressWarnings(as.numeric(levels(data[[with]]))[data[[with]]])
} else if (is.character(data[[with]])) {
measure_var <- as.numeric(data[[with]])
} else {
measure_var <- data[[with]]
}
if (all(is.na(measure_var))) stop("Failed to convert `", with, "` to continuous!")
if (with %in% names(discrete)) drop_columns(discrete, with)
tmp_dt <- data.table(discrete, "measure" = measure_var)
dt <- tmp_dt[, list(frequency = sum(measure, na.rm = TRUE)), by = feature_names]
}
dt2 <- suppressWarnings(melt.data.table(dt, id.vars = c(group, "frequency"), measure.vars = setdiff(feature_names, group))) # This line is updated
layout <- DataExplorer:::.getPageLayout(nrow, ncol, ncol(discrete))
plot_list <- DataExplorer:::.lapply(
parallel = parallel,
X = layout,
FUN = function(x) {
if (order_bar) {
base_plot <- ggplot(dt2[variable %in% feature_names[x]], aes(x = reorder(value, frequency), y = frequency))
} else {
base_plot <- ggplot(dt2[variable %in% feature_names[x]], aes(x = value, y = frequency))
}
base_plot +
geom_bar(stat = "identity", aes_string(fill = group)) + # This line is updated
coord_flip() +
xlab("") + ylab(ifelse(is.null(with), "Frequency", toTitleCase(with)))
}
)
class(plot_list) <- c("multiple", class(plot_list))
plotDataExplorer(
plot_obj = plot_list,
page_layout = layout,
title = title,
ggtheme = ggtheme,
theme_config = theme_config,
facet_wrap_args = list(
"facet" = ~ variable,
"nrow" = nrow,
"ncol" = ncol,
"scales" = "free"
)
)
}
## Create data and plot
dat <- data.frame(
circuit = sample(c("China", "Murica", "Brazil"), 100, replace = T),
driver = sample(c("Kimi", "Seb", "Max", "Lando", "Lance"), 100, replace = T),
opinion = sample(c("Garbage", "Not.Garbage"), 100, replace = T, prob = c(0.8, 0.2))
)
plot_bar2(dat, group = "opinion")
The plot looks like this:
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 trying to reverse the value display of my leaflet legend in R. This post covers categorical data, but I am working with continuous data. Here's a toy example:
map <- leaflet() %>% addProviderTiles('Esri.WorldTopoMap')
x <- 1:100
pal <- colorNumeric(c("#d7191c","#fdae61","#ffffbf","#abd9e9", "#2c7bb6"), x)
map %>% addLegend('topright', pal=pal, values=x)
I'd like the legend to read 100 at the top and 1 on the bottom with the colors reversed. I can certainly reverse the colors in colorNumeric(), but reversing the order of the labels is harder. I have tried reversing the order of the values in x, and I even fiddled with the labelFormat() parameter for addLegend() to reference a lookup table of reversed values... nothing seems to work. Is there an easy way to do this?
Unfortunately the accepted answer to this will get the numbers out of alignment (in fact exactly reversed) from the colours they represent.
Here's the original proposed solution, which I say is incorrect:
map <- leaflet() %>% addProviderTiles('Esri.WorldTopoMap')
x <- 1:100
pal <- colorNumeric(c("#d7191c","#fdae61","#ffffbf","#abd9e9", "#2c7bb6"), x)
map %>% addLegend('topright', pal=pal, values=x)
# This solution shows 100 as red
map %>% addLegend('topright',
pal = pal,
values = x,
labFormat = labelFormat(transform = function(x) sort(x, decreasing = TRUE)))
But if you've been using the pal() function to draw anything on your map, you now have it exactly wrong.
# But 100 is blue, not red
plot(1, 1, pch = 19, cex = 3, col = pal(100))
I think the solution is to define to functions that allocate colours to numbers, one in reverse for the legend, and one for actually drawing things:
pal_rev <- colorNumeric(c("#d7191c","#fdae61","#ffffbf","#abd9e9", "#2c7bb6"), x, reverse = TRUE)
map %>% addLegend('topright',
pal = pal_rev,
values = x,
labFormat = labelFormat(transform = function(x) sort(x, decreasing = TRUE)))
This gives us a legend that matches anything we will have drawn ie 100 is now correctly shown to be blue:
Although the accepted answer does flip the legend's colors and labels, the map's colors do not adress to the legend. Here is a (stolen from here) solution. Basically mpriem89 created a new function called addLegend_decreasing which works exactly like addLegend with an extra argument: decreasing = FALSE that reverses the legend's colors and labels, correctly adressing to the map's colors. Here is the function code:
addLegend_decreasing <- function (map, position = c("topright", "bottomright", "bottomleft","topleft"),
pal, values, na.label = "NA", bins = 7, colors,
opacity = 0.5, labels = NULL, labFormat = labelFormat(),
title = NULL, className = "info legend", layerId = NULL,
group = NULL, data = getMapData(map), decreasing = FALSE) {
position <- match.arg(position)
type <- "unknown"
na.color <- NULL
extra <- NULL
if (!missing(pal)) {
if (!missing(colors))
stop("You must provide either 'pal' or 'colors' (not both)")
if (missing(title) && inherits(values, "formula"))
title <- deparse(values[[2]])
values <- evalFormula(values, data)
type <- attr(pal, "colorType", exact = TRUE)
args <- attr(pal, "colorArgs", exact = TRUE)
na.color <- args$na.color
if (!is.null(na.color) && col2rgb(na.color, alpha = TRUE)[[4]] ==
0) {
na.color <- NULL
}
if (type != "numeric" && !missing(bins))
warning("'bins' is ignored because the palette type is not numeric")
if (type == "numeric") {
cuts <- if (length(bins) == 1)
pretty(values, bins)
else bins
if (length(bins) > 2)
if (!all(abs(diff(bins, differences = 2)) <=
sqrt(.Machine$double.eps)))
stop("The vector of breaks 'bins' must be equally spaced")
n <- length(cuts)
r <- range(values, na.rm = TRUE)
cuts <- cuts[cuts >= r[1] & cuts <= r[2]]
n <- length(cuts)
p <- (cuts - r[1])/(r[2] - r[1])
extra <- list(p_1 = p[1], p_n = p[n])
p <- c("", paste0(100 * p, "%"), "")
if (decreasing == TRUE){
colors <- pal(rev(c(r[1], cuts, r[2])))
labels <- rev(labFormat(type = "numeric", cuts))
}else{
colors <- pal(c(r[1], cuts, r[2]))
labels <- rev(labFormat(type = "numeric", cuts))
}
colors <- paste(colors, p, sep = " ", collapse = ", ")
}
else if (type == "bin") {
cuts <- args$bins
n <- length(cuts)
mids <- (cuts[-1] + cuts[-n])/2
if (decreasing == TRUE){
colors <- pal(rev(mids))
labels <- rev(labFormat(type = "bin", cuts))
}else{
colors <- pal(mids)
labels <- labFormat(type = "bin", cuts)
}
}
else if (type == "quantile") {
p <- args$probs
n <- length(p)
cuts <- quantile(values, probs = p, na.rm = TRUE)
mids <- quantile(values, probs = (p[-1] + p[-n])/2, na.rm = TRUE)
if (decreasing == TRUE){
colors <- pal(rev(mids))
labels <- rev(labFormat(type = "quantile", cuts, p))
}else{
colors <- pal(mids)
labels <- labFormat(type = "quantile", cuts, p)
}
}
else if (type == "factor") {
v <- sort(unique(na.omit(values)))
colors <- pal(v)
labels <- labFormat(type = "factor", v)
if (decreasing == TRUE){
colors <- pal(rev(v))
labels <- rev(labFormat(type = "factor", v))
}else{
colors <- pal(v)
labels <- labFormat(type = "factor", v)
}
}
else stop("Palette function not supported")
if (!any(is.na(values)))
na.color <- NULL
}
else {
if (length(colors) != length(labels))
stop("'colors' and 'labels' must be of the same length")
}
legend <- list(colors = I(unname(colors)), labels = I(unname(labels)),
na_color = na.color, na_label = na.label, opacity = opacity,
position = position, type = type, title = title, extra = extra,
layerId = layerId, className = className, group = group)
invokeMethod(map, data, "addLegend", legend)
}
Once you've run it, you should replace addLegend with addLegend_decreasing and set decreasing = TRUE. Then, your code changes to:
#Default map:
map <- leaflet() %>% addProviderTiles('Esri.WorldTopoMap')
x <- 1:100
pal <- colorNumeric(c("#d7191c","#fdae61","#ffffbf","#abd9e9", "#2c7bb6"), x)
map %>% addLegend_decreasing('topright', pal = pal, values = x, decreasing = TRUE)
Here is an example for a real leaflet map:
df <- local({
n <- 300; x <- rnorm(n); y <- rnorm(n)
z <- sqrt(x ^ 2 + y ^ 2); z[sample(n, 10)] <- NA
data.frame(x, y, z)
})
pal <- colorNumeric("OrRd", df$z)
leaflet(df) %>%
addTiles() %>%
addCircleMarkers(~x, ~y, color = ~pal(z), group = "circles") %>%
addLegend(pal = pal, values = ~z, group = "circles", position = "bottomleft") %>%
addLayersControl(overlayGroups = c("circles"))
Map with default addLegend:
Same map with addLegend_decreasing and decreasing = TRUE
leaflet(df) %>%
addTiles() %>%
addCircleMarkers(~x, ~y, color = ~pal(z), group = "circles") %>%
addLegend_decreasing(pal = pal, values = ~z, group = "circles", position = "bottomleft", decreasing = TRUE) %>%
addLayersControl(overlayGroups = c("circles"))
Map with custom addLegend_decreasing:
Hope this helps, it certainly helped me.
I just found that the built-in labelFormat function has a transform parameter that takes a function. So I passed the sort function in there.
To use the same example,
map %>% addLegend('topright',
pal = pal,
values = x,
labFormat = labelFormat(transform = function(x) sort(x, decreasing = TRUE)))
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!