graph TB subgraph launcher app_options --> json_file; json_file --> app_graphics; json_file --> app_details; end
This is an initial concept of a framework for multiwindow shiny apps. Shiny is a great R package for building an interactive, web-based, graphical UI using R. It does, however, suffer from some limitations. One of these is the fact that a Shiny app runs in a single browser window. To make a more complex interface we can extend the browser window to full screen and add tab panels or scrollbars. Another concept is to break parts of the app into separate apps, each with their own window, separate but linked through a single data source (a single source of truth).
In this prototype, one app pushes data to a json file and two others read the data from this file using a reactiveFileReader
. The reactiveFileReader
ensures that any changes made to the data updates the apps that are dependent upon it. One additional shiny app (launcher
) is responsible for opening and closing all other apps.
Folder Structure
The folder structure is as follows, under the multiwindow
project folder:
multiwindow
├── launcher
│ ├── app.R
│ └── www
│ └── script.js
├── app_options
│ └── app.R
├── app_graphics
│ └── app.R
└── app_details
└── app.R
There are four apps along with a javascript file which handles opening windows: - launcher/app.R - an app that manages opening and closing the other shiny apps as well as defining the json file location. - launcher/www/script.js - a javascript file that opens shiny apps in minimal browser windows of specified size and location. - app_options/app.R - a simple app that allows the user to select a number of options and writes them to a json file. - app_graphics/app.R - a simple app that reads in the json file and plots a chart. - app_details/app.R - a simple app that reads in the json file and outputs the file contents.
Concept
launcher controls opening and closing of the other three apps (app_options, app_graphics and app_details). Communication is handled using a json file.
Code
launcher/app.R
## App lauuncher
## This app manages all others
library(shiny)
library(shinyWidgets)
<- function(input, output, session) {
server
## create a temp folder with access permissions
<- tempfile(fileext = ".json")
jsonfile
## replace this with the shiny server URL (subfolder = mutiwindow)
<- ###
url_base
## define app windows
<- data.frame(
app_windows name = c("app_options", "app_graphics", "app_details"),
app = c("app_options", "app_graphics", "app_details"),
height = c(0.25, 0.4, 0.25),
width = c(0.095, 0.2, 0.095),
left = c(0.02, 0.02, 0.125),
top = c(0.02, 0.33, 0.02),
closable = c(TRUE, TRUE, TRUE),
stringsAsFactors = FALSE
)$url <- paste0(url_base, app_windows$app, "/?file=", jsonfile)
app_windows
## launch all apps
for (i in 1:nrow(app_windows)) {
$sendCustomMessage("launch_app", app_windows[i, ])
session
}
observe({
req(!is.null(input$txt_com_file))
$sendCustomMessage("disable", "txt_com_file")
session
})
$ui_communications <- renderUI({
outputcolumn(10, offset = 1, textInput("txt_com_file", label = "Communications File", width = "100%", value = jsonfile))
})
# app_options ------------------------------------------------------------------
## options UI
$ui_app_options <- renderUI({
outputfluidRow(
column(1, offset = 1, style = "margin-top: 8px;", prettySwitch("swt_app_options", status = "success", label = NULL, inline = TRUE, bigger = TRUE, value = TRUE)),
column(5, h4("shiny app: setting options")),
column(2, offset = 1, actionBttn("but_close_all", label = "Close All", style = "simple", color = "danger", size = "sm"))
)
})
## switch enable
observe({
req(!is.null(input$swt_app_options))
if (!app_windows[1, ]$closable) {
$sendCustomMessage("disable", "swt_app_options")
session
}
})
## close app_options
observeEvent(input$swt_app_options, {
if (input$swt_app_options == TRUE) {
$sendCustomMessage("launch_app", app_windows[1, ])
sessionelse {
} $sendCustomMessage("close_app", app_windows[1, ])
session
}
})
# app_graphics ------------------------------------------------------------------
## graphics UI
$ui_app_graphics <- renderUI({
outputfluidRow(
column(1, offset = 1, style = "margin-top: 8px;", prettySwitch("swt_app_graphics", status = "success", label = NULL, inline = TRUE, bigger = TRUE, value = TRUE)),
column(5, h4("shiny app: plotting graphics"))
)
})
## switch enable
observe({
req(!is.null(input$swt_app_graphics))
if (!app_windows[2, ]$closable) {
$sendCustomMessage("disable", "swt_app_graphics")
session
}
})
## close app_options
observeEvent(input$swt_app_graphics, {
if (input$swt_app_graphics == TRUE) {
$sendCustomMessage("launch_app", app_windows[2, ])
sessionelse {
} $sendCustomMessage("close_app", app_windows[2, ])
session
}
})
# app_details ------------------------------------------------------------------
## details UI
$ui_app_details <- renderUI({
outputfluidRow(
column(1, offset = 1, style = "margin-top: 8px;", prettySwitch("swt_app_details", status = "success", label = NULL, inline = TRUE, bigger = TRUE, value = TRUE)),
column(5, h4("shiny app: setting details"))
)
})
## switch enable
observe({
req(!is.null(input$swt_app_details))
if (!app_windows[3, ]$closable) {
$sendCustomMessage("disable", "swt_app_details")
session
}
})
## close app_options
observeEvent(input$swt_app_details, {
if (input$swt_app_details == TRUE) {
$sendCustomMessage("launch_app", app_windows[3, ])
sessionelse {
} $sendCustomMessage("close_app", app_windows[3, ])
session
}
})
## close all apps
observeEvent(input$but_close_all, {
for (i in 1:nrow(app_windows)) {
$sendCustomMessage("close_app", app_windows[i, ])
session
}
})
}
<- fluidPage(
ui $head(
tags$script(type = "text/javascript", src = "script.js")
tags
),br(),
br(),
fluidRow(column(10, offset = 1,
panel(status = "primary", heading = "App Launcher",
panel(status = "danger", heading = "Communications",
uiOutput("ui_communications")
),br(),
panel(status = "danger", heading = "App Windows",
fluidRow(uiOutput("ui_app_options")),
fluidRow(uiOutput("ui_app_graphics")),
fluidRow(uiOutput("ui_app_details"))
)
)
))
)
shinyApp(ui = ui, server = server)
launcher/www/script.js
var shiny_app_options = "";
var shiny_app_graphics = "";
var shiny_app_details = "";
// launch a shiny app in a minimal window
// window opens with a specified size at a specified screen location (based on fraction of total screen width and height)
.addCustomMessageHandler('launch_app', function(x) {
Shiny= window.screen.height;
scr_height = window.screen.width;
scr_width = scr_height * x.height;
window_height = scr_width * x.width;
window_width = scr_width * x.left;
window_left = scr_height * x.top;
window_top = "height=" + window_height + ", width=" + window_width + ", left=" + window_left + ", top=" + window_top;
window_options
if (x.name == "app_options") {
= window.open(x.url, x.name, window_options);
shiny_app_options else if (x.name == "app_graphics") {
} = window.open(x.url, x.name, window_options);
shiny_app_graphics else if (x.name == "app_details") {
} = window.open(x.url, x.name, window_options);
shiny_app_details
};
})
// close a shiny app
.addCustomMessageHandler('close_app', function(x) {
Shinyconsole.log(x.name);
// can't pass window name as variable to close so have to hardcode :(
if (x.name == "app_options") {
console.log('close app_options');
.close();
shiny_app_optionselse if (x.name == "app_graphics") {
} console.log('close app_graphics');
.close();
shiny_app_graphicselse if (x.name == "app_details") {
} console.log('close app_details');
.close();
shiny_app_details
};
})
// disable a shiny input
.addCustomMessageHandler('disable', function(id) {
Shinyvar input_type = $("#" + id).prop("type");
if (input_type.startsWith("select")) {
$("#" + id)[0].selectize.disable();
else {
} $("#" + id).prop("disabled", true);
}; })
app_options/app.R
## app_options
## A simple app that offers a series of options
library(shiny)
library(shinyWidgets)
library(jsonlite)
<- function(input, output, session) {
server
<- reactive({
filename <- parseQueryString(session$clientData$url_search)
query if (!is.null(query$file)) {
$file
queryelse {
} ""
}
})
$ui_axes <- renderUI({
outputreq(input$sel_data)
## select x and y parameters from numeric columns
<- get(input$sel_data)
df <- names(df)[sapply(df, class) == "numeric"]
columns tagList(
selectizeInput("sel_x", "X parameter", choices = columns),
selectizeInput("sel_y", "Y parameter", choices = columns)
)
})
## write file when updating parameter
observeEvent(input$sel_data, {
write_file()
})
observeEvent(input$sel_x, {
write_file()
})
observeEvent(input$sel_y, {
write_file()
})
## write a file
<- function() {
write_file if (filename() != "") {
<- list(
output data = input$sel_data,
x = input$sel_x,
y = input$sel_y
)<- toJSON(output, auto_unbox = TRUE, null = "null")
json_out <- file(filename(), open = "wt")
con writeLines(json_out, con)
close(con)
}
}
}
<- fluidPage(
ui br(),
br(),
panel(heading = "Options", status = "primary",
selectizeInput("sel_data", "dataset", choices = c("iris", "mtcars")),
uiOutput("ui_axes")
)
)
shinyApp(ui, server)
app_graphics/app.R
## app_graphics
## A simple app that draws a ggplot
library(shiny)
library(shinyWidgets)
library(ggplot2)
library(jsonlite)
<- function(input, output, session) {
server
<- reactive({
filename <- parseQueryString(session$clientData$url_search)
query if (!is.null(query$file)) {
$file
queryelse {
} ""
}
})
## retrieve data from file
<- reactiveFileReader(2000, session, filePath = filename, readFunc = function(filePath) {
json_data tryCatch({
<- file(filePath)
con <- readLines(con)
file_contents close(con)
file_contents
},error = function(e) NA
)
})
$plt <- renderPlot({
outputreq(json_data())
## check all plotting parameters are present
<- c("data", "x", "y")
params <- fromJSON(json_data())
data if (all(sapply(params, function(x) !is.null(data[[x]])))) {
ggplot(get(data[["data"]]), aes(x = .data[[data[["x"]]]], y = .data[[data[["y"]]]])) +
geom_point() +
labs(title = paste0("Plot data = ", data[["data"]]),
x = data[["x"]],
y = data[["y"]])
}
})
}
<- fluidPage(
ui br(),
br(),
panel(heading = "Graphics", status = "primary",
plotOutput("plt")
)
)
shinyApp(ui, server)
app_details/app.R
## app_details
## A simple app that lists some details
library(shiny)
library(shinyWidgets)
library(jsonlite)
<- function(input, output, session) {
server
<- reactive({
filename <- parseQueryString(session$clientData$url_search)
query if (!is.null(query$file)) {
$file
queryelse {
} ""
}
})
## retrieve data from file
<- reactiveFileReader(2000, session, filePath = filename, readFunc = function(filePath) {
json_data tryCatch({
<- file(filePath)
con <- readLines(con)
file_contents close(con)
file_contents
},error = function(e) NA
)
})
$txt_details <- renderPrint({
outputprettify(json_data())
})
}
<- fluidPage(
ui br(),
br(),
panel(heading = "Output", status = "primary",
verbatimTextOutput("txt_details")
)
)
shinyApp(ui, server)
Output
When running the launcher
app the following window first opens:
This automatically triggers opening of the other three shiny apps (app_options, app_graphics and app_details). The windows for these apps are sized and located according to the app_windows
data frame in launcher/app.R as follows:
## define app windows
<- data.frame(
app_windows name = c("app_options", "app_graphics", "app_details"),
app = c("app_options", "app_graphics", "app_details"),
height = c(0.25, 0.4, 0.25),
width = c(0.095, 0.2, 0.095),
left = c(0.02, 0.02, 0.125),
top = c(0.02, 0.33, 0.02),
closable = c(TRUE, TRUE, TRUE),
stringsAsFactors = FALSE
)$url <- paste0(url_base, app_windows$app, "/?file=", jsonfile) app_windows
Here, the sizes and dimensions are based on screen fractions so app_options is 25% of the screen height and 9.5% of the screen width, and is placed 2% from the left of the screen and 2% from the top of the screen.
The resulting screen capture of app_options, app_graphics and app_details looks like this:
When an option in app_options is changed, the json file is update and the reactiveFileReader
in app_graphics and app_details triggers an update. All three apps are independent but connected through the json file.
Here’s a screenshot after updating: