library(shiny)
library(shinyFiles)
library(fs)
library(haven)
library(tablet)
library(dplyr)
library(magrittr)
library(yamlet)
library(yaml)
library(sortable)
library(kableExtra)
library(knitr)
library(latexpdf)
library(tools)
library(csv)
library(shinyAce)
library(spork)
library(reactable)
ui <- shinyUI(
navbarPage(
'Mesa',
tabPanel(
'Input',
sidebarLayout(
sidebarPanel(
width = 12,
shinyFilesButton(
id = 'source',
label = 'data',
title = 'choose data or metadata (*.yaml) file:',
multiple = FALSE
),
textOutput('filepath'),
shinyFilesButton(
id = 'config',
label = 'configuration',
title = 'choose configuration file:',
multiple = FALSE
),
textOutput('confpath'),
uiOutput('saveconfig'),
uiOutput('splice'),
uiOutput('keep'),
uiOutput('buckets')
),
mainPanel(
width = 0
) # end main panel
) # end sidebar layout
),
tabPanel(
'Data',
sidebarLayout(
sidebarPanel(
width = 0
),
mainPanel(
width = 12,
reactable::reactableOutput("data"),
)
)
),
tabPanel(
'Variables',
sidebarLayout(
sidebarPanel(
width = 2,
uiOutput('saveMeta'),
textOutput('metapath')
),
mainPanel(
width = 12,
uiOutput('meta')
)
)
),
tabPanel(
'Shell',
sidebarLayout(
sidebarPanel(
width = 12,
actionButton('submit', 'Save'),
uiOutput('outputid'),
uiOutput('caption'),
uiOutput('footnotes'),
uiOutput('lhead1'),
uiOutput('lhead2'),
uiOutput('rhead1'),
uiOutput('rhead2'),
uiOutput('cont')
),
mainPanel(width = 0) #end main panel
)
),
tabPanel(
'Preview',
sidebarLayout(
sidebarPanel(
width = 2,
uiOutput('labelhtml'),
uiOutput('savecsv')
),
mainPanel(width = 10,
htmlOutput('preview')
)
)
),
tabPanel(
'PDF',
sidebarLayout(
sidebarPanel(
width = 2,
uiOutput('repeatheader'),
uiOutput('repeatfootnote'),
#uiOutput('spork'),
# uiOutput('na_string'),
uiOutput('labeltex'),
uiOutput('savepdf')
),
mainPanel(
width = 10,
uiOutput('pdfview')
)
)
)
) # end page
) # end ui
server <- shinyServer(function(input, output, session) {
# declare the objects that control the application
conf <- reactiveValues(
filepath = character(0),
metapath = character(0),
confpath = character(0),
selected = character(0),
filter_by = character(0),
keep = list(), # a named list of filter_by levels to keep
group_by = character(0),
sequential = FALSE,
title = 'Title',
outputid = 'T-00-00',
lhead1 = 'Company',
lhead2 = 'Project',
rhead1 = 'Confidential',
rhead2 = 'Draft',
cont = '(continued)',
footnotes = '(footnotes here)',
# na_string = 'NA',
x = data.frame(),
mv = 0,
editor = NULL,
labelhtml = 'no',
labeltex = 'no',
repeathead = 'no',
repeatfoot = 'no',
tablet = as.character(packageVersion('tablet'))
)
reset_conf <- function(){
printer('reset_conf')
conf$filepath <- character(0)
conf$metapath <- character(0)
conf$confpath <- character(0)
conf$selected <- character(0)
conf$filter_by <- character(0)
conf$keep <- list() # a named list of filter_by levels to keep
conf$group_by <- character(0)
conf$sequential <- FALSE
conf$title <- 'Title'
conf$outputid <- 'T-00-00'
conf$lhead1 <- 'Company'
conf$lhead2 <- 'Project'
conf$rhead1 <- 'Confidential'
conf$rhead2 <- 'Draft'
conf$cont <- '(continued)'
conf$footnotes <- '(footnotes here)'
# conf$na_string <- 'NA'
conf$x <- data.frame()
conf$imputed <- character()
conf$mv <- 0
conf$editor <- NULL
labelhtml <- 'no'
labeltex <- 'no'
repeathead <- 'no'
repeatfoot <- 'no'
tablet <- as.character(packageVersion('tablet'))
}
file_ok <- function(x){
if(!length(x))return(FALSE)
if(!file.exists(x))return(FALSE)
return(TRUE)
}
# https://github.com/thomasp85/shinyFiles/issues/85
volumes <- getVolumes()
moreVolumes <- function()c(
volumes(),
examples = system.file('shiny-examples/mesa/data', package = 'tablet'),
home = fs::path_home(),
R = R.home()
)
ui_volumes <- reactive({
printer('reactive ui_volumes')
volumes <- moreVolumes()
if(length(conf$filepath) & !any(is.na(conf$filepath))){
sel_path <- dirname(conf$filepath)
if(!sel_path %in% volumes){
vnames <- c(basename(sel_path), names(volumes))
volumes <- setNames(c(sel_path, volumes), vnames)
}
}
if(length(conf$confpath) & !any(is.na(conf$confpath))){
sel_path <- dirname(conf$confpath)
if(!sel_path %in% volumes){
vnames <- c(basename(sel_path), names(volumes))
volumes <- setNames(c(sel_path, volumes), vnames)
}
}
volumes
})
# set up the file choosers
# https://stackoverflow.com/questions/53641749/how-to-use-shinyfilechoose-to-create-an-reactive-object-to-load-a-data-frame
# file_selected <- reactive({
# shinyFileChoose(input, "file", roots = volumes, session = session)
# req(input$file)
# if (is.null(input$file))
# return(NULL)
# return(parseFilePaths(volumes, input$file)$datapath)
# })
# choose data (or metadata)
shinyFileChoose(
input,
'source',
roots = ui_volumes,
session = session,
filetypes = c('sas7bdat', 'csv', 'xpt', 'yaml')
)
observeEvent(input$source, {
printer('observeEvent:input$source')
req(input$source)
if(is.null(input$source)) return(NULL)
newsource <- parseFilePaths(ui_volumes, input$source)$datapath
if(is.character(newsource)){
if(length(newsource)){
if(file.exists(newsource)){
reset_conf()
conf$filepath <- newsource
}
}
}
})
# choose config
shinyFileChoose(
input,
'config',
roots = ui_volumes,
session = session,
filetypes = c('conf')
)
observeEvent(input$config,{
printer('observeEvent:input$config')
req(input$config)
if(is.null(input$config)) return(NULL)
newconfig <- parseFilePaths(ui_volumes, input$config)$datapath
if(is.character(newconfig)){
if(length(newconfig)){
if(file.exists(newconfig)) {
conf$confpath <- newconfig
}
}
}
})
# https://stackoverflow.com/questions/39517199/how-to-specify-file-and-path-to-save-a-file-with-r-shiny-and-shinyfiles
output$saveconfig <- renderUI({
printer('output$saveconfig')
shinySaveButton(
id = 'saveconf',
label = 'save configuration',
title = 'save configuration as:',
filetype = list(conf = 'conf'),
filename = paste0(conf$outputid, '.conf')
)
})
# save the current config
observeEvent(input$saveconf, {
printer('observeEvent:input$saveconf')
req(input$saveconf)
shinyFileSave(input, 'saveconf', roots = ui_volumes, session = session)
fileinfo <- parseSavePath(ui_volumes, input$saveconf)
if (nrow(fileinfo) > 0) {
path <- as.character(fileinfo$datapath)
vals <- isolate(
reactiveValuesToList(conf)[
!names(conf) %in% c(
'x',
'confpath',
'editor',
'mv',
'imputed'
)
]
)
# dictate storage order!
vals <- vals[c(
'filepath',
'metapath',
'selected',
'group_by',
'filter_by',
'keep',
'sequential',
'outputid',
'title',
'lhead1',
'lhead2',
'rhead1',
'rhead2',
'footnotes',
'repeathead',
'repeatfoot',
'cont',
'labelhtml',
'labeltex',
'tablet'
)]
# note: below is the only place in the application where the configuration is written to storage.
# filepath and metapath, like confpath, are stored internally as absolute paths.
# but on write they are expressed relative to confpath directory,
# and on read they are understood relative to confpath directory (and converted to absolute).
if(length(vals$filepath))vals$filepath <- relativizePath(vals$filepath, dirname(path))
if(length(vals$metapath))vals$metapath <- relativizePath(vals$metapath, dirname(path))
res <- try(write_yaml(vals, path)) # only reads on save
res <- !inherits(res, 'try-error')
dur <- 10
if(res) dur <- 5
showNotification(
duration = dur,
type = ifelse(res, 'default', 'error'),
ui = paste(
ifelse(res, 'wrote', 'did not write'),
path
)
)
if(res){
conf$confpath <- path
}
}
})
output$savecsv <- renderUI({
printer('output$savecsv')
shinySaveButton(
id = 'savetable',
label = 'save table',
title = 'save table as:',
filetype = list(csv = 'csv'),
filename = paste0(conf$outputid, '.csv')
)
})
# save the preview table
observeEvent(input$savetable, {
printer('observeEvent: input$savetable')
req(input$savetable)
shinyFileSave(input, 'savetable', roots = ui_volumes, session = session)
fileinfo <- parseSavePath(ui_volumes, input$savetable)
if (nrow(fileinfo) > 0) {
path <- as.character(fileinfo$datapath)
data <- isolate(summarized())
res <- try(as.csv(data, path))
res <- !inherits(res, 'try-error')
dur <- 10
if(res) dur <- 5
showNotification(
duration = dur,
type = ifelse(res, 'default', 'error'),
ui = paste(
ifelse(res, 'wrote', 'did not write'),
path
)
)
}
})
output$savepdf <- renderUI({
printer('output$savepdf')
shinySaveButton(
id = 'savepdf',
label = 'save pdf',
title = 'save pdf as:',
filetype = list(pdf = 'pdf'),
filename = paste0(conf$outputid, '.pdf')
)
})
# save the pdf as
observeEvent(input$savepdf, {
printer('observeEvent:input$savepdf')
req(input$savepdf)
shinyFileSave(input, 'savepdf', roots = ui_volumes, session = session)
fileinfo <- parseSavePath(ui_volumes, input$savepdf)
if (nrow(fileinfo) > 0) {
path <- as.character(fileinfo$datapath)
from <- isolate(pdf_location())
from <- file.path('www', from)
res <- file.copy(from, path, overwrite = TRUE)
dur <- 10
if(res) dur <- 5
showNotification(
duration = dur,
type = ifelse(res, 'default', 'error'),
ui = paste(
ifelse(res, 'wrote', 'did not write'),
path
)
)
}
})
#https://stackoverflow.com/questions/40547786/shiny-can-dynamically-generated-buttons-act-as-trigger-for-an-event
observers <- list()
observeEvent(input$selected,{
printer('observeEvent: input$selected')
conf$selected <- input$selected
})
observeEvent(input$filter_by,{
printer('observeEvent:input$filter_by')
conf$filter_by <- input$filter_by
})
observeEvent(input$group_by,{
printer('observeEvent:input$group_by')
conf$group_by <- input$group_by
})
observeEvent(input$splice,{
printer('observeEvent:Input$splice')
conf$sequential <- ifelse(input$splice == 'sequential', TRUE, FALSE)
})
observeEvent(input$submit,{
printer('observeEvent:input$submit(conf$title)')
conf$title <- input$caption
})
# observeEvent(input$csv,{
# as.csv(summarized(), paste0(conf$outputid,'.csv'))
# })
observeEvent(input$submit,{
printer('observeEvent:input$submit(conf$outputid)')
conf$outputid <- input$outputid
})
observeEvent(input$submit,{
printer('observeEvent:input$submit(conf$lhead1)')
conf$lhead1 <- input$lhead1
})
observeEvent(input$submit,{
printer('observeEvent:input$submit(conf$lhead2)')
conf$lhead2 <- input$lhead2
})
observeEvent(input$submit,{
printer('observeEvent:input$submit(conf$rhead1)')
conf$rhead1 <- input$rhead1
})
observeEvent(input$submit,{
printer('observeEvent:input$submit(conf$rhead2)')
conf$rhead2 <- input$rhead2
})
observeEvent(input$submit,{
printer('observeEvent:input$submit(conf$cont)')
conf$cont <- input$cont
})
observeEvent(input$submit,{
printer('observeEvent:input$submit(conf$footnotes)')
conf$footnotes <- input$footnotes
})
# observeEvent(input$na_string,{
# conf$na_string <- input$na_string
# })
observeEvent(input$repeathead,{
printer('observeEvent:input$repeathead')
conf$repeathead <- input$repeathead
})
observeEvent(input$repeatfoot,{
printer('observeEvent:input$repeatfoot')
conf$repeatfoot <- input$repeatfoot
})
observeEvent(input$labelhtml,{
printer('observeEvent:input$labelhtml')
conf$labelhtml <- input$labelhtml
})
observeEvent(input$labeltex,{
printer('observeEvent:input$labeltex')
printer(paste0(conf$labeltex,'<-', input$labeltex))
conf$labeltex <- input$labeltex
})
observeEvent(conf$confpath,{
printer('observeEvent:conf$confpath')
if(!length(conf$confpath)){
return()
}
if(!file.exists(conf$confpath)){
return()
}
saved <- list()
tryCatch(
saved <- read_yaml(conf$confpath),
error = function(e) showNotification(duration = NULL, type = 'error', as.character(e))
)
if(!length(saved)){
showNotification(duration = NULL, type = 'message', 'resetting configuration')
reset_conf()
return()
}
# items not saved should be re-initialized
if(!('filepath' %in% names(saved))) {
showNotification(duration = NULL, type = 'error', 'configuration does not specify file path')
reset_conf()
return()
}
if(
!file.exists(
absolutizePath(
saved$filepath,
dirname(conf$confpath)
)
)
){
showNotification(duration = NULL, type = 'error', 'configured file path not found')
reset_conf()
return()
}
# at this point:
# * confpath has changed
# * confpath is readable/parseable
# * configuration has a filepath
# * filepath exists
# update internal configuration from saved configuration
# note: below is the only place in the application where the configuration is read from storage.
# filepath and metapath, like confpath, are stored internally as absolute paths.
# but on write they are expressed relative to confpath directory,
# and on read they are understood relative to confpath directory (and converted to absolute).
if(!is.null(saved$filepath))conf$filepath <- absolutizePath(saved$filepath, dirname(conf$confpath))
if(!is.null(saved$metapath))conf$metapath <- absolutizePath(saved$metapath, dirname(conf$confpath))
if(!is.null(saved$selected))conf$selected <- saved$selected
if(!is.null(saved$filter_by))conf$filter_by <- saved$filter_by
if(!is.null(saved$keep))conf$keep <- saved$keep
if(!is.null(saved$group_by))conf$group_by <- saved$group_by
if(!is.null(saved$sequential))conf$sequential<- saved$sequential
if(!is.null(saved$title))conf$title <- saved$title
if(!is.null(saved$lhead1))conf$lhead1 <- saved$lhead1
if(!is.null(saved$lhead2))conf$lhead2 <- saved$lhead2
if(!is.null(saved$rhead1))conf$rhead1 <- saved$rhead1
if(!is.null(saved$rhead2))conf$rhead2 <- saved$rhead2
if(!is.null(saved$cont))conf$cont <- saved$cont
if(!is.null(saved$footnotes))conf$footnotes <- saved$footnotes
# if(!is.null(saved$na_string))conf$na_string <- saved$na_string
if(!is.null(saved$outputid))conf$outputid <- saved$outputid
if(!is.null(saved$repeathead))conf$repeathead <- saved$repeathead
if(!is.null(saved$repeatfoot))conf$repeatfoot <- saved$repeatfoot
if(!is.null(saved$labelhtml))conf$labelhtml <- saved$labelhtml
if(!is.null(saved$labeltex))conf$labeltex <- saved$labeltex
# version control
if(!is.null(saved$tablet)){
if(!identical(saved$tablet, conf$tablet)){
showNotification(
duration = NULL,
type = 'warning',
paste(
'configuration was last saved by tablet version', saved$tablet,
'but currently using', conf$tablet
)
)
}
}
#conf$x = data.frame()
# if filepath has changed, data will be re-read: see observeEvent(conf$filepath)
})
# when conf$filepath changes, we rebuild the data
# also need to trigger when metadata changes on disk
# i.e. when we have saved it.
# https://stackoverflow.com/questions/34731975/how-to-listen-for-more-than-one-event-expression-within-a-shiny-eventreactive-ha
printer <- function(x)writeLines(as.character(x))
observeEvent({
conf$filepath # new data selected
conf$mv # metadata re-written
1 # prevents NULL from squelching the observation
},
{
printer('observeEvent:conf$filepath')
# invalidate the keep/filter observers if data changes
observers <<- list()
# invalidate configuration if an attempt is made to supplant data
# conf$confpath <- character(0)
# this does not work!
if(!length(conf$filepath))return()
theFile <- conf$filepath
is_data <- grepl('\\.sas7bdat|xpt|csv$', theFile)
is_meta <- grepl('\\.yaml$', theFile)
datafile <- theFile
if(is_meta) {
datafile <- sub('yaml$','sas7bdat',theFile)
if(!file.exists(datafile)) datafile <- sub('yaml$','xpt',theFile)
if(!file.exists(datafile)) datafile <- sub('yaml$','csv',theFile)
} # try everything
metafile <- theFile
if(is_data) metafile <- sub('sas7bdat|xpt|csv$', 'yaml', theFile)
has_data <- file.exists(datafile)
has_meta <- file.exists(metafile)
# d <- data.frame() # 2022/04/13 make html() responsive to coerced columns
d <- conf$x
if(has_data){
if(grepl('sas7bdat$', datafile)) d <- data.frame(read_sas(datafile))
if(grepl('xpt$', datafile)) d <- data.frame(read.xport(datafile))
if(grepl('csv$', datafile)) d <- data.frame(as.csv(datafile))
}
# at this point, best data has been defined. Define default metadata.
m <- decorations(d)
# Either read the metadata or write it.
if(has_meta){
# try for better meta.
tryCatch(
m <- read_yamlet(metafile),
error = function(e) showNotification(duration = NULL, type = 'error', as.character(e))
)
} else {
write_yamlet(m, metafile)
}
# now we have best available metadata
has_meta <- TRUE
conf$metapath <- metafile # make visible
# make data look like metadata (which may be superset)
have <- names(d)
need <- names(m)
make <- setdiff(need, have)
#browser()
for(col in make) d[[col]] <- rep(NA_real_, nrow(d))
# ensure positive nrow # removed at 0.5.4
# if(nrow(d) == 0) d <- d['',,drop = FALSE]
# drop unspecified
d %<>% select(!!!names(m))
# apply meta
d <- redecorate(d, m)
# # Promote NA to a level of the factor
# d %<>% resolve(exclude = NULL)
d %<>% resolve()
# store on the session
conf$x <- d
conf$imputed <- sapply(select(d, !!!make), attr, 'label')
})
output$filepath <- renderPrint({
#printer('observeEvent:output$filepath')
if (!length(conf$filepath)) {
cat('No input data selected.')
} else {
cat(conf$filepath)
}
})
filtered <- reactive({
printer('filtered')
x <- conf$x
cols <- conf$filter_by
for(filter in cols){
scope <- input[[paste0('mesa_filter_', filter)]]
if(length(scope)){ # only filter if at least one choice was made!
# save these for drawing the UI
conf$keep[[filter]] <- scope
index <- x[[filter]] %in% scope
x <- x[index,,drop = FALSE]
}
}
x
})
factorized <- reactive({
printer('factorized')
x <- filtered()
x %<>% mutate_if(is.character, classified)
suppressWarnings(x %<>% modify(title = label))
#browser()
hasUnits <- sapply(x, function(col)'units' %in% names(attributes(col)))
hasUnits <- names(hasUnits[hasUnits])
suppressWarnings(x %<>% modify(!!!hasUnits, title = paste0(label, ' (', .data$units, ')')))
# conditionally creating scripted labels has the
# unintended effect of making the pdf and preview displays
# co-dependent, since the output of factorized() changes
# when either flag changes.
# Meanwhile, it is easy and cheap to calculate html/tex labels
# unconditionally, but use them conditionally.
# Accordingly, we unconditionalize the following code.
# if(length(input$labelhtml) == 1){
# printer('factorized - labelhtml')
# if(input$labelhtml == TRUE){
suppressWarnings(x %<>% modify(original = name))
suppressWarnings(x %<>% modify(html = as_html(as_spork(.data$name)))) # default
suppressWarnings(x %<>% modify(html = as_html(as_spork(.data$label))))
hasUnits <- sapply(x, function(col)'units' %in% names(attributes(col)))
hasUnits <- names(hasUnits[hasUnits])
suppressWarnings(x %<>% modify(
!!!hasUnits,
html = concatenate(as_html(as_spork(c(.data$label, ' (', .data$units,')'))))
))
# }
# }else{printer('factorized - no labelhtml')}
# if(length(input$labeltex) == 1){
# printer('factorized - labeltex')
#
# if(input$labeltex == TRUE){
# browser()
# we need default 'latex' tex attributes for all columns ...
suppressWarnings(x %<>% modify(tex = as_latex(as_spork(.data$name))))
suppressWarnings(x %<>% modify(tex = as_latex(as_spork(.data$label))))
hasUnits <- sapply(x, function(col)'units' %in% names(attributes(col)))
hasUnits <- names(hasUnits[hasUnits])
suppressWarnings(x %<>% modify(
!!!hasUnits,
# should retain class 'latex'
# currently pre-doubled by escape_latex.latex
tex = concatenate( as_latex(as_spork(c(.data$label, ' (', .data$units,')'))))
))
# }
# }else{printer('factorized - no labeltex')}
x
})
selected <- reactive({
printer('selected')
x <- factorized()
if(length(conf$group_by)) x %<>% group_by(!!!syms(conf$group_by))
x %<>% select(!!!syms(conf$selected))
x
})
args <- reactive({
printer('args')
x <- list(x = selected())
extra <- list(
all_levels = TRUE,
# all = 'All',
fun = list(
sum ~ sum(x, na.rm = TRUE),
pct ~ signif(digits = 3, sum / n * 100 ),
ave ~ signif(digits = 3, mean(x, na.rm = TRUE)),
std ~ signif(digits = 3, sd(x, na.rm = TRUE)),
med ~ signif(digits = 3, median(x, na.rm = TRUE)),
min ~ signif(digits = 3, min(x, na.rm = TRUE)),
max ~ signif(digits = 3, max(x, na.rm = TRUE)),
smn ~ sum(!is.na(x))
),
num = list(
n ~ smn,
`Mean (SD)` ~ ave + ' (' + std + ')',
Median ~ paste(med),
`Min, Max` ~ min + ', ' + max
),
fac = list(
` ` ~ ifelse(sum == 0, '0', sum + ' (' + pct + '%' + ')')
)
)
bundle <- c(x, extra)
bundle
})
summarized <- reactive({
printer('summarized')
fun <- tablet
if(conf$sequential) fun <- splice
args <- args()
do.call(fun,args)
})
html <- reactive({
printer('html')
# options(knitr.kable.NA = conf$na_string)
options(knitr.kable.NA = 0)
# browser()
fun <- tablet
if(conf$sequential) fun <- splice
args <- args()
if(!is.null(input$labelhtml)){
if(input$labelhtml == 'yes'){
args$x %<>% modify(title = .data$html)
}
} else {
printer('no labelhtml yet')
return()
}
x <- do.call(fun, args)
x %<>% tablette # 0.6.0 revert to old format
# browser()
# remove NA groups
na <- which(names(x) == 'NA')
for(i in rev(na))x[[na]] <- NULL
# strikethru imputed columns for visual clarity
codelist <- attr(x$`_tablet_name`, 'codelist')
x$`_tablet_original` <- unlist(codelist[x$`_tablet_name`])
# very elegant, but blows away attributes
# x %<>% mutate(
# across(
# .cols = -starts_with('_tablet_'),
# .fns = ~ ifelse(`_tablet_original` %in% names(conf$imputed), '-', .x)
# )
# )
nms <- names(x)
nontargets <- grepl('^_tablet_', nms)
targets <- !nontargets
#targets <- x %>% select(-starts_with('_tablet_')) %>% names
imputed <- x$`_tablet_original` %in% names(conf$imputed)
if(length(imputed) & length(targets)) x[imputed, targets] <- '-'
x$`_tablet_original` <- NULL
x %<>% tablet # 0.6.0 new format
x %<>% as_kable(caption = conf$title)
x %<>% kable_classic(full_width = F, html_font = "Cambria")
x %<>% kable_styling(fixed_thead = T)
x
})
tex <- reactive({
printer('tex')
#browser()
old <- opts_knit$get('out.format')
opts_knit$set(out.format = 'latex')
# options(knitr.kable.NA = escape_latex(conf$na_string))
options(knitr.kable.NA = 0)
fun <- tablet
if(conf$sequential) fun <- splice
args <- args()
# browser()
if(!is.null(input$labeltex)){
# browser()
if(input$labeltex == 'yes'){
printer('using spork')
args$x %<>% modify(title = .data$tex) # should have class 'latex', unescaped
#args$x %<>% modify(codelist = lapply(codelist, kableExtra:::escape_latex2))
} else {
# args$x %<>% modify(title = kableExtra:::escape_latex(title))
# otherwise trap specials and pre-double secondary backslash
args$x %<>% modify(title = tablet::escape_latex(title))
#args$x %<>% modify(codelist = lapply(codelist, kableExtra:::escape_latex2))
}
} else {
printer('no labeltex yet')
return()
# next maybe unnecessary if as_kable auto-escapes names(index) in >= 0.4.2
# args$x %<>% modify(title = kableExtra:::escape_latex(title))
}
# call tablet
x <- do.call(fun, args)
x %<>% tablette # 0.10.21 revert to old format
# remove NA groups
na <- which(names(x) == 'NA')
for(i in rev(na))x[[na]] <- NULL
# strikethru imputed columns for visual clarity
codelist <- attr(x$`_tablet_name`, 'codelist')
x$`_tablet_original` <- unlist(codelist[x$`_tablet_name`])
# very elegant, but blows away attributes
# x %<>% mutate(
# across(
# .cols = -starts_with('_tablet_'),
# .fns = ~ ifelse(`_tablet_original` %in% names(conf$imputed), '-', .x)
# )
# )
nms <- names(x)
nontargets <- grepl('^_tablet_', nms)
targets <- !nontargets
#targets <- x %>% select(-starts_with('_tablet_')) %>% names
imputed <- x$`_tablet_original` %in% names(conf$imputed)
if(length(imputed) & length(targets)) x[imputed, targets] <- '-'
x$`_tablet_original` <- NULL
if(!nrow(x)){
showNotification(duration = 5, type = 'error', 'nothing selected')
return(character(0))
}
# browser()
# _tablet_name has been thoroughly pre-escaped for all cases.
# however, it is created as factor.
# we flag it as latex to invoke the right method in as_kable(escape_latex = tablet::escape_latex)
x$`_tablet_name` %<>% as_latex
x %<>% tablet # 0.10.21 new format
x %<>% as_kable(format = 'latex', caption = escape_latex(conf$title), longtable = TRUE)
if(length(input$repeatheader) == 1){
if(input$repeatheader == 'yes'){
x %<>% kable_styling(latex_options = 'repeat_header', repeat_header_text = '')
}
}
feet <- unlist(strsplit(conf$footnotes, '\n'))
if(length(feet)){
x %<>% footnote(general = ,fixed_small_size = TRUE, general_title = " ",threeparttable = TRUE)
}
x %<>% as.character
# insert footnote on every page
cont <- input$cont
mycont <- NULL
if(!is.null(cont)){
if(nchar(cont) > 0){
mycont <- c(
paste0('\\multicolumn{1}{r}{\\emph{', cont, '}}\\\\'),
'\\midrule'
)
}
}
insertion <- c(
'\\endhead',
'\\midrule',
mycont,
'\\insertTableNotes'
)
insertion <- paste(insertion, collapse = '\n')
if(length(input$repeatfootnote) == 1){
if(input$repeatfootnote == 'yes'){
x %<>% sub('\\endhead', insertion, ., fixed = TRUE)
}
}
x %<>% as.document(
thispagestyle = '',
pagestyle = '',
preamble = c(
'\\documentclass{article}',
'\\usepackage[utf8]{inputenc}',
'\\usepackage[T1]{fontenc}',
'\\usepackage[showseconds=false]{datetime2}',
'\\usepackage[landscape]{geometry}',
'\\usepackage{fancyhdr}',
'\\fancyhf{}',
'\\renewcommand{\\headrulewidth}{0pt}',
'\\pagestyle{fancy}',
paste0('\\lhead{', escape_latex(conf$lhead1),' \\\\ ',escape_latex(conf$lhead2), '}'),
'%\\chead{Table 0.0.0.xxx}',
paste0('\\rhead{', escape_latex(conf$rhead1),' \\\\ ',escape_latex(conf$rhead2), '}'),
#paste0('\\lfoot{\\textit{',file_path_sans_ext(conf$filepath),'}}'),
paste0('\\lfoot{\\textit{~', sub(getwd(),'',conf$confpath, fixed = TRUE),'}}'),
'\\rfoot{\\today{~at~\\DTMcurrenttime}}',
'\\usepackage{booktabs}',
'\\usepackage{longtable}',
'\\usepackage{array}',
'\\usepackage{multirow}',
'\\usepackage{wrapfig}',
'\\usepackage{float}',
'\\usepackage{colortbl}',
'\\usepackage{pdflscape}',
'\\usepackage{tabu}',
'\\usepackage{threeparttable}',
'\\usepackage{threeparttablex}',
'\\usepackage[normalem]{ulem}',
'\\usepackage{xcolor}',
'\\usepackage[labelformat=empty]{caption}',
'\\usepackage{makecell}'
)
)
opts_knit$set(out.format = old)
x
})
output$buckets <- renderUI({
printer('output$buckets')
if(!length(conf$x))return()
nms <- names(conf$x)
selected <- intersect(conf$selected, nms)
group_by <- intersect(conf$group_by, nms)
filter_by <-intersect(conf$filter_by, nms)
used <- union(selected, group_by)
used <- union(used, filter_by)
available <- sort(setdiff(nms, used)) # definitive set
suggested <- union(input$available, available) # user's sort order
available <- intersect(suggested, available) # defer to user where possible
bucket_list(
header = 'Data Item Roles',
group_name = 'bucket_list_group',
orientation = 'horizontal',
add_rank_list(
text = 'Available',
labels = available,
input_id = 'available'
),
add_rank_list(
text = 'Summarize',
labels = selected,
input_id = 'selected'
),
add_rank_list(
text = 'Group By',
labels = group_by,
input_id = 'group_by'
),
add_rank_list(
text = 'Filter By',
labels = filter_by,
input_id = 'filter_by'
)
)
})
output$splice <- renderUI({
printer('output$splice')
radioButtons(
'splice',
'Grouping Style',
inline = TRUE,
choices = c('nested','sequential'),
selected = ifelse(conf$sequential,'sequential','nested')
)
})
output$repeatheader <- renderUI({
printer('output$repeatheader')
radioButtons(
'repeatheader',
'repeat header on each page',
inline = TRUE,
choices = c('no','yes'),
selected = conf$repeathead
)
})
output$repeatfootnote <- renderUI({
printer('output$repeatfootnote')
radioButtons(
'repeatfootnote',
'repeat footnote on each page',
inline = TRUE,
choices = c('no','yes'),
selected = conf$repeatfoot
)
})
output$labelhtml <- renderUI({
printer('output$labelhtml')
radioButtons(
'labelhtml',
'scripted labels',
inline = TRUE,
choices = c('no','yes'),
selected = conf$labelhtml
)
})
output$labeltex <- renderUI({
printer('output$labeltex')
radioButtons(
'labeltex',
'scripted labels',
inline = TRUE,
choices = c('no','yes'),
selected = conf$labeltex
)
})
output$caption <- renderUI({
printer('output$caption')
textAreaInput('caption','Title', value = conf$title, resize = 'both')
})
output$outputid <- renderUI({
printer('output$outputid')
textInput('outputid','Output Identifier', value = conf$outputid)
})
output$lhead1 <- renderUI({
printer('output$lhead1')
textInput('lhead1','Left Header 1', value = conf$lhead1)
})
output$lhead2 <- renderUI({
printer('output$lhead2')
textInput('lhead2','Left Header 2', value = conf$lhead2)
})
output$rhead1 <- renderUI({
printer('output$rhead1')
textInput('rhead1','Right Header 1', value = conf$rhead1)
})
output$rhead2 <- renderUI({
printer('output$rhead2')
textInput('rhead2','Right Header 2', value = conf$rhead2)
})
output$cont <- renderUI({
printer('output$cont')
textInput('cont','Continued', value = conf$cont)
})
output$footnotes <- renderUI({
printer('output$footnotes')
textAreaInput('footnotes','Footnotes', value = conf$footnotes, resize = 'both')
})
# output$na_string <- renderUI({
# textInput('na_string','text substitute for NA', value = conf$na_string)
# })
output$keep <- renderUI({
printer('output$keep')
if(!length(input$filter_by))return()
myFilter <- function(var, dat){
nms <- as.character(sort(unique(dat[[var]])))
lbl <- attr(dat[[var]], 'label')
checkboxGroupInput(
inline = TRUE,
inputId = paste0('mesa_filter_',var),
label = lbl,
choices = nms,
selected = conf$keep[[var]]
)
}
myObserver <- function(var){
observers[[var]] <<- observeEvent(input[[paste0('mesa_filter_',var)]], {
conf$keep[[var]] <- input[[paste0('mesa_filter_',var)]]
})
}
# pre-assign an observer if not already
lapply(input$filter_by, myObserver)
lapply(input$filter_by, myFilter, dat = conf$x)
})
output$data <- reactable::renderReactable({
printer('output$data')
if(!ncol(conf$x))return(structure(data.frame(` `='data goes here.', check.names = F), row.names = ' '))
out <- conf$x
#out %<>% resolve # already done
#browser()
out %<>% modify(name = paste(name, label, sep = ': '))
hasUnits <- sapply(out, function(col)'units' %in% names(attributes(col)))
hasUnits <- names(hasUnits[hasUnits])
#browser()
out %<>% modify(!!!hasUnits, name = paste0(name, ' (', .data$units, ')'))
reactable(out)
})
output$preview <- renderText({
#printer('output$preview')
if(!length(input$selected))return('Output goes here.')
# ensure html
opts_knit$set(out.format = 'html')
x <- suppressMessages(html())
x
})
pdf_location <- reactive({
printer('pdf_location')
# browser()
x <- suppressWarnings(tex())
if(!length(x))return('1x1.png')
stem <- isolate(conf$outputid) # basename(tempfile())
# backup
writeLines(x, con = 'www/cache.tex')
# clean slate
tex <- file.path('www', paste0(stem, '.tex'))
pdf <- file.path('www', paste0(stem, '.pdf'))
unlink(tex)
unlink(pdf)
# # some tables need to be run twice! Not sure why!
# # particularly for repeat headers with nesting.
# #browser()
# path <- try(
# as.pdf(
# x,
# dir = 'www',
# stem = stem,
# clean = FALSE,
# ignore.stdout = TRUE
# )
# )
#
# # ignore incomplete pdf
# unlink(file.path('www', paste0(stem, '.pdf')))
#
# path <- try(
# as.pdf(
# x,
# dir = 'www',
# stem = stem,
# clean = TRUE,
# ignore.stdout = TRUE
# )
# )
# 0.6.0: trying tinytex instead of system command in latexpdf::as.pdf.document
# 0.6.0: must now write the tex file manually.
writeLines(x, tex)
path <- try(tinytex::pdflatex(tex))
if(inherits(path, 'try-error')){
showNotification(as.character(path), type = 'error', duration = 5)
}
if(!file.exists(path)) return('1x1.png')
basename(path)
})
# https://stackoverflow.com/questions/19469978/displaying-a-pdf-from-a-local-drive-in-shiny
output$pdfview <- renderUI({
printer('output$pdfview')
if(!ncol(conf$x))return('PDF displays here.')
loc <- pdf_location()
printer('directory')
printer(getwd())
printer('file')
printer(loc)
tags$iframe(
style="height:600px; width:100%; scrolling:yes",
src = paste0('/', loc)
)
})
output$confpath <- renderPrint({
#printer('output$confpath')
req(conf$confpath)
cat(conf$confpath)
# if(!length(path)){
# cat('No configuration selected.')
# } else {
# if(is.na(path)){
# cat('No configuration selected.')
# } else {
# cat(path)
# }
# }
})
output$metapath <- renderPrint({
#printer('output$metapath')
if (!length(conf$metapath)) {
cat('No data selected.')
} else {
cat(conf$metapath)
}
})
# https://stackoverflow.com/questions/54304518/how-to-edit-a-yml-file-in-shiny
output$saveMeta <- renderUI({
printer('output$saveMeta')
actionButton("saveMeta", label = "Save")
})
output$meta <- renderUI({
printer("output$meta")
current <- conf$editor
if(!length(conf$metapath))return(current)
if(is.na(conf$metapath))return(current)
val <- NULL
tryCatch(
val <- readLines(conf$metapath),
error = function(e) showNotification(
duration = NULL,
type = 'error',
as.character(e)
)
)
if(!is.null(val)){
val <- aceEditor(
outputId = "meta",
value = val,
mode = 'yaml',
tabSize = 2
)
conf$editor <- val
} else {
val <- conf$editor
}
val
})
observeEvent(input$saveMeta, {
printer('observeEvent: input$saveMeta')
path <- isolate(conf$metapath)
res <- try(yaml.load(input$meta))
if(!inherits(res,'try-error')){
res <- try(read_yamlet(input$meta))
}
err <- as.character(res)
res <- !inherits(res, 'try-error')
msg <- paste(ifelse(res, 'wrote', 'did not write'), path)
if(!res) msg <- paste(msg, err)
if(res){
write(x = input$meta, file = path)
# trigger redecoration
conf$mv <- conf$mv + 1
}
dur <- 10
if(res) dur <- 5
showNotification(
duration = dur,
type = ifelse(res, 'default', 'error'),
ui = msg
)
})
})
# Create Shiny app ----
shinyApp(ui, server)
# copyright 2021 Tim Bergsma bergsmat@gmail.com
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.