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)))
Related
I am running a loop that assigns a plot to a variable. the code then pulls the variable names with the ls() pattern function
while (i > 0) {
assign(paste("fig", i, sep = ""), plot_ly(tot, y = ~gene, color = ~bx, type = "box", boxpoints = "all") %>% layout(annotations = list(x = 0 , y = 1, xanchor = "left",yanchor = "top",yshift = 20,showarrow = FALSE,font = list(size = 20), text = paste("Data", colnames(total)[i+1]))))
i = i-1
if (i == 0){
the code then pulls the variable names with the ls() pattern function and attempts to pass them as a list to subplot.
...
l <- as.list(ls(pattern = "fig"))
l <- do.call(paste, c(l,sep=", "))
fig <- subplot(l,margin = 0.06,nrows=2) %>% layout(showlegend = FALSE)
fig
}
}'
This is where i am experiencing some errors.
Error in [[: subscript out of bounds
Try the following :
library(plotly)
l <- mget(ls(pattern = "fig"))
fig <- subplot(l,margin = 0.06,nrows=2) %>% layout(showlegend = FALSE)
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 am trying to generate multiple graphs in Plotly for 30 different sales offices. Each graph would have 3 lines: sales, COGS, and inventory. I would like to keep this on one graph with 30 buttons for the different offices. This is the closest solution I could find on SO:
## Create random data. cols holds the parameter that should be switched
l <- lapply(1:100, function(i) rnorm(100))
df <- as.data.frame(l)
cols <- paste0(letters, 1:100)
colnames(df) <- cols
df[["c"]] <- 1:100
## Add trace directly here, since plotly adds a blank trace otherwise
p <- plot_ly(df,
type = "scatter",
mode = "lines",
x = ~c,
y= ~df[[cols[[1]]]],
name = cols[[1]])
## Add arbitrary number of traces
## Ignore first col as it has already been added
for (col in cols[-1]) {
p <- p %>% add_lines(x = ~c, y = df[[col]], name = col, visible = FALSE)
}
p <- p %>%
layout(
title = "Dropdown line plot",
xaxis = list(title = "x"),
yaxis = list(title = "y"),
updatemenus = list(
list(
y = 0.7,
## Add all buttons at once
buttons = lapply(cols, function(col) {
list(method="restyle",
args = list("visible", cols == col),
label = col)
})
)
)
)
print(p)
It works but only on graphs with single lines/traces. How can I modify this code to do the same thing but with graphs with 2 or more traces? or is there a better solution? Any help would be appreciated!
### EXAMPLE 2
#create fake time series data
library(plotly)
set.seed(1)
df <- data.frame(replicate(31,sample(200:500,24,rep=TRUE)))
cols <- paste0(letters, 1:31)
colnames(df) <- cols
#create time series
timeseries <- ts(df[[1]], start = c(2018,1), end = c(2019,12), frequency = 12)
fit <- auto.arima(timeseries, d=1, D=1, stepwise =FALSE, approximation = FALSE)
fore <- forecast(fit, h = 12, level = c(80, 95))
## Add trace directly here, since plotly adds a blank trace otherwise
p <- plot_ly() %>%
add_lines(x = time(timeseries), y = timeseries,
color = I("black"), name = "observed") %>%
add_ribbons(x = time(fore$mean), ymin = fore$lower[, 2], ymax = fore$upper[, 2],
color = I("gray95"), name = "95% confidence") %>%
add_ribbons(x = time(fore$mean), ymin = fore$lower[, 1], ymax = fore$upper[, 1],
color = I("gray80"), name = "80% confidence") %>%
add_lines(x = time(fore$mean), y = fore$mean, color = I("blue"), name = "prediction")
## Add arbitrary number of traces
## Ignore first col as it has already been added
for (col in cols[2:31]) {
timeseries <- ts(df[[col]], start = c(2018,1), end = c(2019,12), frequency = 12)
fit <- auto.arima(timeseries, d=1, D=1, stepwise =FALSE, approximation = FALSE)
fore <- forecast(fit, h = 12, level = c(80, 95))
p <- p %>%
add_lines(x = time(timeseries), y = timeseries,
color = I("black"), name = "observed", visible = FALSE) %>%
add_ribbons(x = time(fore$mean), ymin = fore$lower[, 2], ymax = fore$upper[, 2],
color = I("gray95"), name = "95% confidence", visible = FALSE) %>%
add_ribbons(x = time(fore$mean), ymin = fore$lower[, 1], ymax = fore$upper[, 1],
color = I("gray80"), name = "80% confidence", visible = FALSE) %>%
add_lines(x = time(fore$mean), y = fore$mean, color = I("blue"), name = "prediction", visible = FALSE)
}
p <- p %>%
layout(
title = "Dropdown line plot",
xaxis = list(title = "x"),
yaxis = list(title = "y"),
updatemenus = list(
list(
y = 0.7,
## Add all buttons at once
buttons = lapply(cols, function(col) {
list(method="restyle",
args = list("visible", cols == col),
label = col)
})
)
)
)
p
You were very close!
If for example you want graphs with 3 traces,
You only need to tweak two things:
Set visible the three first traces,
Modify buttons to show traces in groups of three.
My code:
## Create random data. cols holds the parameter that should be switched
library(plotly)
l <- lapply(1:99, function(i) rnorm(100))
df <- as.data.frame(l)
cols <- paste0(letters, 1:99)
colnames(df) <- cols
df[["c"]] <- 1:100
## Add trace directly here, since plotly adds a blank trace otherwise
p <- plot_ly(df,
type = "scatter",
mode = "lines",
x = ~c,
y= ~df[[cols[[1]]]],
name = cols[[1]])
p <- p %>% add_lines(x = ~c, y = df[[2]], name = cols[[2]], visible = T)
p <- p %>% add_lines(x = ~c, y = df[[3]], name = cols[[3]], visible = T)
## Add arbitrary number of traces
## Ignore first col as it has already been added
for (col in cols[4:99]) {
print(col)
p <- p %>% add_lines(x = ~c, y = df[[col]], name = col, visible = F)
}
p <- p %>%
layout(
title = "Dropdown line plot",
xaxis = list(title = "x"),
yaxis = list(title = "y"),
updatemenus = list(
list(
y = 0.7,
## Add all buttons at once
buttons = lapply(0:32, function(col) {
list(method="restyle",
args = list("visible", cols == c(cols[col*3+1],cols[col*3+2],cols[col*3+3])),
label = paste0(cols[col*3+1], " ",cols[col*3+2], " ",cols[col*3+3] ))
})
)
)
)
print(p)
PD: I only use 99 cols because I want 33 groups of 3 graphs
I'm trying to combine a list of R plotly plots - vertically, but since the y-axis title of each of the individual plots is horizontal the combined plot comes out messy.
Here's my example:
data:
set.seed(1)
df <- data.frame(cluster=unlist(lapply(letters[1:10],function(i) rep(paste0("cluster:",i),200))),
group=rep(c(rep("A",100),rep("B",100)),10),val=rnorm(2000))
df$group <- factor(df$group,levels=c("A","B"))
plotting a list of density plots, one per df$cluster:
library(plotly)
plot.list <- lapply(unique(df$cluster),function(i){
density.df <- do.call(rbind,lapply(c("A","B"),function(b){
dens <- density(dplyr::filter(df,cluster == i,group == b)$val,adjust=1)
return(data.frame(x=dens$x,y=dens$y,group=b,stringsAsFactors=F))
}))
density.df$group <- factor(density.df$group,levels=c("A","B"))
if(i == unique(df$cluster)[1]){
cluster.plot <- plot_ly(x=~density.df$x,y=~density.df$y,type='scatter',mode='lines',color=~density.df$group,line=list(width=3),showlegend=T) %>%
layout(xaxis=list(title="Val",zeroline=F),yaxis=list(title=i,zeroline=F,showticklabels=F),legend=list(orientation="h",xanchor="center",x=0,y=1))
} else{
cluster.plot <- plot_ly(x=~density.df$x,y=~density.df$y,type='scatter',mode='lines',color=~density.df$group,line=list(width=3),showlegend=F) %>%
layout(xaxis=list(title="Val",zeroline=F),yaxis=list(title=i,zeroline=F,showticklabels=F))
}
return(cluster.plot)
})
Then, combining them using:
subplot(plot.list,nrows=length(plot.list),shareX=T,shareY=T,titleX=T,titleY=T)
Gives:
How can I rotate the y-axis title of each plot in plot.list to be horizontal rather than vertical?
This cannot be achieved natively, if I understand correctly, as per this.
However, as the link suggests, try using annotations.
Here is how one might go about it -
set.seed(1)
df <- data.frame(cluster=unlist(lapply(letters[1:10],function(i) rep(paste0("cluster:",i),200))),
group=rep(c(rep("A",100),rep("B",100)),10),val=rnorm(2000))
df$group <- factor(df$group,levels=c("A","B"))
library(plotly)
plot.list <- lapply(unique(df$cluster),function(i){
density.df <- do.call(rbind,lapply(c("A","B"),function(b){
dens <- density(dplyr::filter(df,cluster == i,group == b)$val,adjust=1)
return(data.frame(x=dens$x,y=dens$y,group=b,stringsAsFactors=F))
}))
density.df$group <- factor(density.df$group,levels=c("A","B"))
if(i == unique(df$cluster)[1]){
cluster.plot <- plot_ly(x=~density.df$x,y=~density.df$y,type='scatter',mode='lines',color=~density.df$group,line=list(width=3),showlegend=T) %>%
layout(xaxis=list(title="Val",zeroline=F),yaxis=list(title = '', zeroline=F,showticklabels=F),legend=list(orientation="h",xanchor="center",x=0,y=1)) %>%
add_annotations(text = i, x = -0.1, xref = 'paper', y = 0.5, yref = 'paper', showarrow = FALSE)
} else {
cluster.plot <- plot_ly(x=~density.df$x,y=~density.df$y,type='scatter',mode='lines',color=~density.df$group,line=list(width=3),showlegend=F) %>%
layout(xaxis=list(title="Val",zeroline=F),yaxis=list(title = '', zeroline=F,showticklabels=F)) %>%
add_annotations(text = i, x = -0.1, xref = 'paper', y = 0.5, yref = 'paper', showarrow = FALSE)
}
return(cluster.plot)
})
subplot(plot.list,nrows=length(plot.list),shareX=T,shareY=T,titleX=T,titleY=T)
This results in -
I have a data.frame I'd like to scatter plot using R's plotly with two factors which I'd like to color and shape by.
Here's my data:
set.seed(1)
df <- data.frame(x=rnorm(12),y=rnorm(12),
group=c(rep(1,3),rep(2,3),rep(3,3),rep(4,3)),
treatment=c(rep("A",6),rep("B",6)),
stringsAsFactors=F)
df$group <- factor(df$group,levels=1:4)
df$treatment <- factor(df$treatment,levels=c("A","B"))
Here's how I'm trying to plot:
require(plotly)
plot_ly(marker=list(size=10),type='scatter',mode="markers",x=~df$x,y=~df$y,color=~df$group,symbol=~df$treatment) %>%
add_annotations(text="group,treatment",xref="paper",yref="paper",x=1.02, xanchor="left",y=1.02,yanchor="top",legendtitle=TRUE,showarrow=FALSE) %>%
layout(xaxis=list(title="x"),yaxis=list(title="y"))
which gives me:
Is it possible to get the text of group and treatment in the legend be separated by comma instead of the new line as it is now?
This means that instead of:
1
A
2
A
3
B
4
B
I'll have:
1,A
2,A
3,B
4,B
Sounds trivial but it's one of the cases where Plotly decides whats good for you.
The legend labels are composed of the categories of color and symbol which are all passed in one command. In order to get control over the output, let's add each trace separately.
for (grou in groups) {
for (treat in treatments) {
trace_data <- subset(df, group == grou & treatment == treat)
if (nrow(trace_data) > 0) {
p <- add_trace(p,
x = trace_data$x,
y = trace_data$y,
marker = list(size = 10,
color = group,
symbol = as.integer(charToRaw(treat)) - 65),
type = 'scatter',
mode = "markers",
name = paste(grou, treat, sep = ",")
)
}
}
}
We pass the color (not strictly necessary) via marker and symbol also via marker (both can be passed in the add_trace command as well but then again Plotly decides for you what do to do with it).
The legend label is passed via name.
Note: You need to convert your treatment explicitly because symbol expects either a named symbol or a number (unless your treatments are named diamond or circle)
Complete code
library(utils)
library(plotly)
set.seed(1)
df <- data.frame(x = rnorm(12),
y = rnorm(12),
group = c(rep(1, 3),
rep(2, 3),
rep(3, 3),
rep(4, 3)
),
treatment=c(rep("A", 6),
rep("B", 6)
),
stringsAsFactors = FALSE
)
groups <- unique(df$group)
treatments <- unique(df$treatment)
p <- plot_ly()
for (grou in groups) {
for (treat in treatments) {
trace_data <- subset(df, group == grou & treatment == treat)
if (nrow(trace_data) > 0) {
p <- add_trace(p,
x = trace_data$x,
y = trace_data$y,
marker = list(size = 10,
color = group,
symbol = as.integer(charToRaw(treat)) - 65),
type = 'scatter',
mode = "markers",
name = paste(grou, treat, sep = ",")
)
}
}
}
p <- add_annotations(p,
text = "group,treatment",
xref = "paper",
yref = "paper",
x = 0.96,
xanchor = "left",
y = 1.03,
yanchor = "top",
legendtitle = TRUE,
showarrow = FALSE) %>%
layout(xaxis = list(title = "x"),
yaxis = list(title = "y"))
p