I'm trying to make a horizontal legend in a Shiny app with a Leaflet map.
I can change the display to display: flex; using CSS which makes the legend horizontal but what I'm aiming at is something like:
0% - a palette of colors - 100%
edit and NOT -color- 0% -color- 10% - color- 20% etc.
I don't see a way to do that in CSS and I can't find enough info about addLegend to find a solution,
Here's a reprex:
library(leaflet)
library(RColorBrewer)
ui <- bootstrapPage(
tags$style(type = "text/css", "html, body {width:100%;height:100%}"),
leafletOutput("map", width = "100%", height = "100%"),
absolutePanel(top = 10, right = 10,
sliderInput("range", "Magnitudes", min(quakes$mag), max(quakes$mag),
value = range(quakes$mag), step = 0.1
),
selectInput("colors", "Color Scheme",
rownames(subset(brewer.pal.info, category %in% c("seq", "div")))
),
checkboxInput("legend", "Show legend", TRUE)
)
)
server <- function(input, output, session) {
# Reactive expression for the data subsetted to what the user selected
filteredData <- reactive({
quakes[quakes$mag >= input$range[1] & quakes$mag <= input$range[2],]
})
# This reactive expression represents the palette function,
# which changes as the user makes selections in UI.
colorpal <- reactive({
colorNumeric(input$colors, quakes$mag)
})
output$map <- renderLeaflet({
# Use leaflet() here, and only include aspects of the map that
# won't need to change dynamically (at least, not unless the
# entire map is being torn down and recreated).
leaflet(quakes) %>% addTiles() %>%
fitBounds(~min(long), ~min(lat), ~max(long), ~max(lat))
})
# Incremental changes to the map (in this case, replacing the
# circles when a new color is chosen) should be performed in
# an observer. Each independent set of things that can change
# should be managed in its own observer.
observe({
pal <- colorpal()
leafletProxy("map", data = filteredData()) %>%
clearShapes() %>%
addCircles(radius = ~10^mag/10, weight = 1, color = "#777777",
fillColor = ~pal(mag), fillOpacity = 0.7, popup = ~paste(mag)
)
})
# Use a separate observer to recreate the legend as needed.
observe({
proxy <- leafletProxy("map", data = quakes)
# Remove any existing legend, and only if the legend is
# enabled, create a new one.
proxy %>% clearControls()
if (input$legend) {
pal <- colorpal()
proxy %>% addLegend(position = "bottomright",
pal = pal, values = ~mag
)
}
})
}
shinyApp(ui, server)```
It does not look like it's possible to manipulate the leaflet legend as it's rendered as an <svg> element and a few other <divs>. I came up with a potential solution that involved generating a new legend using tags$ul and tags$li.
I wrote a new function called legend which generates the html markup for a legend using colorNumeric and some set of values (using quakes$mag in this example). The markup is an unordered list <ul>. All list items are generated dynamically based on the number of bins specified (the default is 7). The code used to generate a sequence of colors is adapted from the R Leaflet package: https://github.com/rstudio/leaflet/blob/master/R/legend.R#L93.
Left and right titles can be specified by using the input arguments left_label and right_label. Background colors are defined using the style attribute. All other styles are defined using tags$style.
Here's an example (some of the code is clipped for readability).
legend(
values = quakes$mag,
palette = "BrBG",
title = "Magnitude",
left_label = "0%",
right_label = "100%"
)
#
# <span class="legend-title">Magnitude</span>
# <ul class="legend">
# <li class="legend-item ..."> 0%</li>
# <li class="legend-item ..." style="background-color: #543005; ..."></li>
# ...
To render the legend into the app, you will need to create an output element in the UI. I used absolutePanel to position the legend into the bottom right corner and defined a uiOutput element.
absolutePanel(
bottom = 20, right = 10, width: "225px;",
uiOutput("map_legend")
)
In the server, I replaced the code in the if (input$colors) with:
if (inputs$colors) {
output$map_legend <- renderUI({
legend(...)
})
}
I also added a condition to render a blank element should the option be unticked. Here's a screenshot followed by the example.
The only thing I couldn't figure out is how to link the legend color scale with the circles.
Hope this helps! Let me know if you have any questions.
Screenshot
Example
library(shiny)
library(leaflet)
library(RColorBrewer)
# manually create a legend
legend <- function(values, palette, title, left_label, right_label, bins = 7) {
# validate args
stopifnot(!is.null(values))
stopifnot(!is.null(palette))
stopifnot(!is.null(title))
stopifnot(!is.null(left_label))
stopifnot(!is.null(right_label))
# generate color palette using Bins (not sure if it's the best approach)
# #reference:
# https://github.com/rstudio/leaflet/blob/c19b0fb9c60d5caf5f6116c9e30dba3f27a5288a/R/legend.R#L93
pal <- colorNumeric(palette, values)
cuts <- if (length(bins) == 1) pretty(values, n = bins) else bins
n <- length(cuts)
r <- range(values, na.rm = TRUE)
# pretty cut points may be out of the range of `values`
cuts <- cuts[cuts >= r[1] & cuts <= r[2]]
colors <- pal(c(r[1], cuts, r[2]))
# generate html list object using colors
legend <- tags$ul(class = "legend")
legend$children <- lapply(seq_len(length(colors)), function(color) {
tags$li(
class = "legend-item legend-color",
style = paste0(
"background-color:", colors[color]
),
)
})
# add labels to list
legend$children <- tagList(
tags$li(
class = "legend-item legend-label left-label",
as.character(left_label)
),
legend$children,
tags$li(
class = "legend-item legend-label right-label",
as.character(right_label)
)
)
# render legend with title
return(
tagList(
tags$span(class = "legend-title", as.character(title)),
legend
)
)
}
# ui
ui <- tagList(
tags$head(
tags$style(
"html, body {
width: 100%;
height: 100%;
}",
".legend-title {
display: block;
font-weight: bold;
}",
".legend {
list-style: none;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
}",
".legend-item {
display: inline-block;
}",
".legend-item.legend-label {
margin: 0 8px;
}",
".legend-item.legend-color {
width: 24px;
height: 16px;
}"
)
),
bootstrapPage(
leafletOutput("map", width = "100%", height = "100%"),
absolutePanel(
top = 10, right = 10,
sliderInput("range", "Magnitudes", min(quakes$mag), max(quakes$mag),
value = range(quakes$mag), step = 0.1
),
selectInput("colors", "Color Scheme",
rownames(subset(brewer.pal.info, category %in% c("seq", "div")))
),
checkboxInput("legend", "Show legend", TRUE)
),
absolutePanel(
bottom = 20,
right = 10,
width = "225px",
uiOutput("map_legend"),
)
)
)
server <- function(input, output, session) {
# Reactive expression for the data subsetted to what the user selected
filteredData <- reactive({
quakes[quakes$mag >= input$range[1] & quakes$mag <= input$range[2],]
})
# This reactive expression represents the palette function,
# which changes as the user makes selections in UI.
colorpal <- reactive({
colorNumeric(input$colors, quakes$mag)
})
output$map <- renderLeaflet({
# Use leaflet() here, and only include aspects of the map that
# won't need to change dynamically (at least, not unless the
# entire map is being torn down and recreated).
leaflet(quakes) %>%
addTiles() %>%
fitBounds(~min(long), ~min(lat), ~max(long), ~max(lat))
})
# Incremental changes to the map (in this case, replacing the
# circles when a new color is chosen) should be performed in
# an observer. Each independent set of things that can change
# should be managed in its own observer.
observe({
pal <- colorpal()
leafletProxy("map", data = filteredData()) %>%
clearShapes() %>%
addCircles(radius = ~10^mag/10, weight = 1, color = "#777777",
fillColor = ~pal(mag), fillOpacity = 0.7, popup = ~paste(mag)
)
})
# Use a separate observer to recreate the legend as needed.
observe({
if (input$legend) {
output$map_legend <- renderUI({
# build legend
legend(
values = filteredData()[["mag"]],
palette = as.character(input$colors),
title = "Mag",
left_label = "0%",
right_label = "100%"
)
})
}
if (!input$legend) {
output$map_legend <- renderUI({
tags$div("")
})
}
})
}
shinyApp(ui, server)
Related
I would like to update the colours of my pickerInput based on input from the colourInput in the below example.
This questions follows on from this question and replicating this with pickerInput instead of selectizeInput.
This works great with selectizeInput:
## load iris dataset
data(iris)
cats <- levels(iris$Species)
## colourInput ---- create list of shiny inputs for UI
ids <- paste0("col", seq(3))
cols <- c("red", "blue", "yellow")
foo <- function(x) {colourInput(ids[x], cats[x], cols[x])}
my_input <- lapply(seq(ids), foo)
## css styling for selectizeInput menu
CSS <- function(values, colors){
template <- "
.option[data-value=%s], .item[data-value=%s]{
background: %s !important;
color: white !important;
}"
paste0(
apply(cbind(values, colors), 1, function(vc){
sprintf(template, vc[1], vc[1], vc[2])
}),
collapse = "\n"
)
}
css <- CSS(cats, cols[seq(cats)])
## ------ shiny app ------
runApp(shinyApp(
ui = fluidPage(
tabsetPanel(type = "tabs",
tabPanel("Dataset", id = "data",
tags$head(
uiOutput("css")
),
selectizeInput("species", "Labels",
choices = cats,
multiple = TRUE,
selected = cats),
plotOutput("scatter")
),
tabPanel("Colour Menu", id = "colmenu",
my_input)
)
),
server = function(input, output, session) {
## get coords according to selectizeInput
mrkSel <- reactive({
lapply(input$species,
function(z) which(iris$Species == z))
})
## colours selected by user in colourPicker
cols_user <- reactive({
sapply(ids, function(z) input[[z]])
})
## update scatter colours
scattercols <- reactive({
cols_user()[sapply(input$species, function(z)
which(cats == z))]
})
## scatter plot is conditional on species selected
output$scatter <- renderPlot({
plot(iris$Petal.Length, iris$Petal.Width, pch=21)
if (!is.null(input$species)) {
for (i in 1:length(input$species)) {
points(iris$Petal.Length[mrkSel()[[i]]], iris$Petal.Width[mrkSel()[[i]]],
pch = 19, col = scattercols()[i])
}
}
})
## update colours
output$css <- renderUI({
tags$style(HTML(CSS(cats, cols_user())))
})
}
)
)
An attempt to replicate with pickerInput
## load iris dataset
data(iris)
cats <- levels(iris$Species)
## colourInput ---- create list of shiny inputs for UI
ids <- paste0("col", seq(3))
cols <- c("red", "blue", "yellow")
foo <- function(x) {colourInput(ids[x], cats[x], cols[x])}
my_input <- lapply(seq(ids), foo)
## css styling for selectizeInput menu
CSS <- function(values, colors){
template <- "
.dropdown-menu[data-value=%s] {
background: %s !important;
color: white !important;
}"
paste0(
apply(cbind(values, colors), 1, function(vc){
sprintf(template, vc[1], vc[1], vc[2])
}),
collapse = "\n"
)
}
css <- CSS(cats, cols[seq(cats)])
## ------ shiny app ------
runApp(shinyApp(
ui = fluidPage(
tabsetPanel(type = "tabs",
tabPanel("Dataset", id = "data",
tags$head(
uiOutput("css")
),
pickerInput("species", "Labels",
choices = cats,
multiple = TRUE,
selected = cats,
options = list(
`actions-box` = TRUE,
size = 10,
`selected-text-format` = "count > 3"
)),
plotOutput("scatter")
),
tabPanel("Colour Menu", id = "colmenu",
my_input)
)
),
server = function(input, output, session) {
## get coords according to selectizeInput
mrkSel <- reactive({
lapply(input$species,
function(z) which(iris$Species == z))
})
## colours selected by user in colourPicker
cols_user <- reactive({
sapply(ids, function(z) input[[z]])
})
## update scatter colours
scattercols <- reactive({
cols_user()[sapply(input$species, function(z)
which(cats == z))]
})
## scatter plot is conditional on species selected
output$scatter <- renderPlot({
plot(iris$Petal.Length, iris$Petal.Width, pch=21)
if (!is.null(input$species)) {
for (i in 1:length(input$species)) {
points(iris$Petal.Length[mrkSel()[[i]]], iris$Petal.Width[mrkSel()[[i]]],
pch = 19, col = scattercols()[i])
}
}
})
## update colours
output$css <- renderUI({
tags$style(HTML(CSS(cats, cols_user())))
})
}
)
)
I am not familiar with css styling and so I can assume my code is wrong when trying to style dropdown-menu.
Can someone tell me how to achieve colour coding of the drop down menu based on the colour selected in the Colour Menu tab? Bonus, if anyone knows of a cheatsheet they can share for css styling.
CSS <- function(colors){
template <- "
.dropdown-menu ul li:nth-child(%s) a {
background: %s !important;
color: white !important;
}"
paste0(
apply(cbind(seq_along(colors), colors), 1, function(vc){
sprintf(template, vc[1], vc[2])
}),
collapse = "\n"
)
}
and
output$css <- renderUI({
tags$style(HTML(CSS(cols_user())))
})
To deal with CSS, you should try the inspector tool (right-click on an element, then "Inspect").
The app below contains an actionButton Add data that inserts a UI element each time it is clicked. Each UI element is a box that contains one selectInput Select data and an actionButton Edit that opens a modal when clicked.
Each modal contains:
A data table with two columns: Parameter and Value (this column is editable).
An actionButton Apply, which applies any changes made to the Value
column.
When the user selects a dataset inside box x, a reactiveValue is created to store the corresponding parameters in a data.frame x_paramset (where x is the id of the box inserted via insertUI) and add a val column which has the same value as default (see list at the top of code below). I then use renderDataTable to add the Value column (which contains the numericInput) - this data table is displayed inside the modal.
To update the data.frame to apply any changes the user may have made in the modal, I use an observeEvent that listens for the Apply button and updates the val column in the data.frame x_paramset with the values inside the numericInputs in the Value column.
Here is the app (the bsModal has been commented out and replaced with a shinyWidgets::dropdownButton):
library(shiny)
library(shinydashboard)
library(shinyjs)
library(shinyWidgets)
library(DT)
library(tidyverse)
all = list(p1 = list(a = list(id = "a", default = 10)),
p2 = list(x = list(id = "x", default = 20)))
# UI ----------------------------------------------------------------------
ui<-fluidPage(shinyjs::useShinyjs(),
tags$head(
tags$script("
$(document).on('click', '.dropdown-shinyWidgets li button', function () {
$(this).blur()
Shiny.onInputChange('lastClickId',this.id)
Shiny.onInputChange('lastClick',Math.random())
});
")
),
box(title = "Add data",
column(width = 12,
fluidRow(
tags$div(id = 'add')
),
fluidRow(
actionButton("addbox", "Add data")
))
)
)
# SERVER ------------------------------------------------------------------
server <- function(input, output, session) {
rvals = reactiveValues()
getInputs <- function(pattern){
reactives <- names(reactiveValuesToList(input))
name = reactives[grep(pattern,reactives)]
}
observeEvent(input$addbox, {
lr = paste0('box', input$addbox)
insertUI(
selector = '#add',
ui = tags$div(id = lr,
box(title = lr,
selectizeInput(lr, "Choose data:", choices = names(all)),
shinyWidgets::dropdownButton(inputId = paste0(lr, "_settings"),
circle = F, status = "success", icon = icon("gear"), width = "1000px",
tooltip = tooltipOptions(title = "Click to edit"),
tags$h4(paste0("Edit settings for Learner", lr)),
hr(),
DT::dataTableOutput( paste0(lr, "_paramdt") ),
bsButton(paste0(lr, "_apply"), "Apply")
) # end dropdownButton
)
) #end tags$div
) # end inserUI
# create reactive dataset
rvals[[ paste0(lr, "_paramset") ]] <- reactive({
do.call(rbind, all[[ input[[lr]] ]]) %>%
cbind(., lr) %>%
as.data.frame %>%
mutate(val = default)
}) # end reactive
# render DT in modal
output[[ paste0(lr, "_paramdt") ]] <- renderDataTable({
DT <- rvals[[ paste0(lr, "_paramset") ]]() %>%
mutate(
Parameter = id,
Value = as.character(numericInput(paste0(lr,"value",id), label = NULL, value = default))) %>%
select(Parameter:Value)
datatable(DT,
selection = 'none',
#server = F,
escape = F,
options = list(preDrawCallback = JS('function() { Shiny.unbindAll(this.api().table().node()); }'),
drawCallback = JS('function() { Shiny.bindAll(this.api().table().node()); } ')))
}) # end renderDT
# Apply changes
observeEvent(input$lastClick, {
# replace old values with new
rvals[[ paste0(lr, "_paramset") ]](rvals[[ paste0(lr, "_paramset") ]]() %>%
mutate(
val = input$box1valuea
)
)
}) # end apply changes observeEvent
}) #end observeEvent
}
shinyApp(ui=ui, server=server)
I encounter errors when I try the following:
Add data >> Edit >> make some change to numericInput >> Apply - this
resets the numericInput inside the modal back to its default whereas
I would like the user-specified value to persist upon applying
changes or closing the modal.
The app crashes when I try:
Add data >> Edit >> Apply >> close modal >> Add data OR
Click Add data twice and then click Edit in either box.
I am not sure where my server logic is failing. I know Shiny does not support "persistent use" modals (https://github.com/rstudio/shiny/issues/1590) but I was wondering if there was a workaround? I am also not sure what inside the insertUI observeEvent is causing the app to crash in the cases described above. Any help you can offer would be greatly appreciated!
The HTML output is created by summarytool::dfSummary function.
summarytools
summarytools uses Bootstrap’s stylesheets to generate standalone HTML documents that can be displayed in a Web Browser or in RStudio’s Viewer using the generic print() function.
When the HTML gets rendered on the tabpanel, the whole UI changes. Is there a way to render the HTML on the tabpanel without changing the UI?
library(summarytools)
ui <- fluidPage(
titlePanel("dfSummary"),
sidebarLayout(
sidebarPanel(
uiOutput("dfSummaryButton")
),
mainPanel(
tabsetPanel(
tabPanel("Data Input",
dataTableOutput("dataInput"),
br(),
verbatimTextOutput("profileSTR")),
tabPanel("dfSummary Output",
htmlOutput("profileSummary")))
)
)
)
server <- function(input, output, session) {
#Read in data file
recVal <- reactiveValues()
dfdata <- iris
#First 10 records of input file
output$dataInput <- renderDataTable(head(dfdata, n = 10), options = list(scrollY = '500px',
scrollX = TRUE, searching = FALSE, paging = FALSE, info = FALSE,
ordering = FALSE, columnDefs = list(list(className = 'dt-center',
targets = '_all'))))
#str() of input file
output$profileSTR <- renderPrint({
ProStr <- str(dfdata)
return(ProStr)
})
#Create dfSummary Button
output$dfSummaryButton <- renderUI({
actionButton("dfsummarybutton", "Create dfSummary")
})
### Apply dfSummary Buttom
observeEvent(input$dfsummarybutton, {
recVal$dfdata <- dfdata
})
#dfSummary data
output$profileSummary <- renderUI({
req(recVal$dfdata)
SumProfile <- print(dfSummary(recVal$dfdata), omit.headings = TRUE, method = 'render')
SumProfile
})
}
shinyApp(ui, server)
Version 0.8.3 of summarytools has a new boolean option, bootstrap.css which will prevent this from happening. Also, graph.magnif allows adjusting the graphs' size.
SumProfile <- print(dfSummary(recVal$dfdata),
method = 'render',
omit.headings = TRUE,
footnote = NA,
bootstrap.css = FALSE,
graph.magnif = 0.8)
The latest version can be installed with devtools:
devtools::install_github("dcomtois/summarytools")
Good luck :)
I have an interactive doc in rmarkdown using shiny apps.
My YAML:
---
title: "Shiny HTML Doc"
author: "theforestecologist"
date: "Apr 13, 2017"
output: html_document
runtime: shiny
---
I generate a number of shiny apps throughout this document, but they all are too long (i.e., they all require y scrollers to view their entirety.
I know I can add options = list(height = ###,width = ###) within the shinyApp function in my code chunk to control individual rendered apps, but I want my readers to see my shiny code (sans messy option controls).
So my desired approach is to control all of the shiny app outputs at once.
Specifically, is there a way to make it so they each can have variable heights, but each one be fully pictured (i.e., not needing a vertical (y) scroller)?
Example Code:
---
title: "Shiny HTML Doc"
author: "theforestecologist"
date: "Apr 13, 2017"
output: html_document
runtime: shiny
---
I have an interactive shiny doc with app outputs of varying heights.
By default, they all have the same height, which is too small of a value
(and thus creates the need for vertical scrolling bars).
##### **Example 1**
```{r, eval=TRUE,echo=FALSE}
library(shiny)
ui <- fluidPage(
titlePanel("Ex1"),
sidebarLayout(
sidebarPanel(
checkboxGroupInput(inputId = "type", label = "Plant Type", choices = levels(CO2$Type),
selected = levels(CO2$Type))
),
mainPanel(
plotOutput(outputId = "scatter.plot")
)
)
)
server <- function(input, output) {
output$scatter.plot <- renderPlot({
plot(uptake ~ conc, data = CO2, type = "n")
points(uptake ~ conc, data = CO2[CO2$Type %in% c(input$type),])
title(main = "Plant Trends")
})
}
shinyApp(ui = ui, server = server)
```
The output of the next example is longer and therefore needs a larger height assignment to get rid of the scroll bar.
##### **Example 2**
```{r, eval=TRUE,echo=FALSE}
ui <- fluidPage(
titlePanel("Ex1"),
sidebarLayout(
sidebarPanel(
div(style = "padding:0px 0px 450px 0px;",
checkboxGroupInput(inputId = "type", label = "Plant Type", choices = levels(CO2$Type),
selected = levels(CO2$Type))
)
),
mainPanel(
plotOutput(outputId = "scatter.plot")
)
)
)
server <- function(input, output) {
output$scatter.plot <- renderPlot({
plot(uptake ~ conc, data = CO2, type = "n")
points(uptake ~ conc, data = CO2[CO2$Type %in% c(input$type),])
title(main = "Plant Trends")
})
}
shinyApp(ui = ui, server = server)
```
I'm trying to create a checkbox for which the choices are plots created through ggplot. In the result, the UI looks like the HTML code itself instead of evaluating the HTML code to show the chart. Any ideas how I can get the checkboxGroupInput to show ggplots?
Sample code below -
runApp(shinyApp(
ui = fluidPage(
headerPanel("Plot check box"),
mainPanel(
uiOutput("plotscheckboxes")
)
),
server = function(input, output, session) {
output$plot1 = renderPlot({
ggplot(mtcars)+geom_point(aes(x=mpg,y=mpg))
})
output$plot2 = renderPlot({
ggplot(mtcars)+geom_point(aes(x=mpg,y=mpg))
})
output$plotscheckboxes = renderUI({
plotlist = list(
plotOutput('plot1'),
plotOutput('plot2')
)
plotlist2 = do.call(tagList, plotlist)
# this just produces a list with 1,2, some sort of underlying value for the checkboxGroup
finaloptionlist = lapply(
seq(length(plotlist2)),
function(x) x
)
# the names of the list are what get used in the options so setting the names accordingly as the HTML code of the ggplot rendering
names(finaloptionlist) = sapply(plotlist2, function(x) paste(x, collapse = "\n"))
checkboxGroupInput("checkGroup", label = h3("Checkbox group"),
choices = finaloptionlist,
selected = 1)
})
}
))