From 102d6011695f62c70b2efbee3020f89a4896bc78 Mon Sep 17 00:00:00 2001 From: xni7 Date: Wed, 6 Jan 2021 11:34:59 -0500 Subject: [PATCH 1/3] 'added_workflow' --- R/add_chart.R | 254 ++++++++++++++++++ R/app_init_addin.R | 215 +++++++++++++++ R/create_new_safetyGraphics_app.R | 55 ++++ inst/rstudio/addins.dcf | 5 + .../project/create_safetyGraphics_app.dcf | 11 + .../templates/project/safetyGraphicsHex.png | Bin 0 -> 20169 bytes 6 files changed, 540 insertions(+) create mode 100644 R/add_chart.R create mode 100644 R/app_init_addin.R create mode 100644 R/create_new_safetyGraphics_app.R create mode 100644 inst/rstudio/addins.dcf create mode 100644 inst/rstudio/templates/project/create_safetyGraphics_app.dcf create mode 100644 inst/rstudio/templates/project/safetyGraphicsHex.png diff --git a/R/add_chart.R b/R/add_chart.R new file mode 100644 index 00000000..c07778b6 --- /dev/null +++ b/R/add_chart.R @@ -0,0 +1,254 @@ + +#' Add chart config (adapted from golem::add_module()) +#' +#' +#' This function creates a module inside the local `config/` folder to define a new chart +#' - dump yaml +#' - create R scripts +#' +#' @param path Path to store the chart configuration files, expecting a config root folder +#' +#' +-- config +#'| +-- aeExplorer.yaml +#'| +-- newChart.yaml +#' +# | \-- workflow +#'| +-- aeExplorer_init.R +#'| +-- newChart_main.R +#' +#' @param name The name of the chart (also name of the yaml file) +#' @param label Label of chart +#' @param type Type of chart: `plot`, `module`, or `htmlwidget`. Default is `plot` (static) +#' @param domain associated data domain, for example `aes`, `labs`, or `multiple` +#' @param package optional, R package that this chart is associated with. +#' @param chart_template chart template function +#' +#' @seealso [chart_template()] +#' +#' @importFrom rprojroot find_root is_rstudio_project +#' @importFrom fs path file_create file_exists +#' @importFrom yaml write_yaml +#' +#' @export +add_chart <- function( + name = "newplot", + label = "New Static Plot", + type = "plot", + domain = "labs", + package = NULL, + workflow = list(), + open = TRUE, + ... +){ + + proj_root <- rprojroot::find_root(rprojroot::is_rstudio_project) + + path <- file.path(proj_root, "config") + + yaml_where <- file.path( + path, paste0(name, ".yaml") + ) + + + # label: Safety Explorer + # type: htmlwidget + # domain: multiple + # package: safetyCharts + # workflow: + # init: aeExplorer_init + # widget: aeExplorer + # + conf <- list() + + conf$label <- label + conf$type <- type + conf$domain <- domain + conf$package <- package + conf$workflow <- workflow + + + + if (tolower(type) == "module") { + conf$workflow <- list( + ui <- paste0(name, "_ui"), + server <- paste0(name, "_server") + ) + } else if (tolower(type) == "htmlwidget") { + #TODO add htmlwidget + } else if (tolower(type) == "plot") { + conf$workflow$main <- name + } + + if (!fs::file_exists(yaml_where)){ + write_yaml(conf, yaml_where) + } + + + r_where <- file.path( + path, + "workflow", + paste0(name, ".R") + ) + + if (!fs::file_exists(r_where)){ + fs::file_create(r_where) + chart_template(name = name, path = r_where, type=type, ...) + } +} + + +#' Chart Template Function +#' @inheritParams add_chart +#' @param path The path to the R script where the module will be written. +#' Note that this path will not be set by the user but internally by +#' `add_chart()`. +#' @param ... Arguments to be passed to the template, via `add_chart()` +#' +#' @return Used for side effect +#' @export +#' @seealso [add_chart()] +chart_template <- function(name, path, type, ...){ + + write_there <- function(...){ + write(..., file = path, append = TRUE) + } + + + if (type=="plot"){ + + # template_r <- system.file("config/workflow", "safety_histogram_chart.R", package = "safetyCharts") + # file.copy(from = template_r, to = path, overwrite = T) + + write_there(sprintf("%s <- function(data, settings){", name)) + + func_body <- + ' ## Replace with your custom code ## + params <- aes_( + x=as.name(settings$studyday_col), + y=as.name(settings$value_col), + group=as.name(settings$id_col) + ) + + + if(hasName(settings, "measure_values")){ + sub <- data %>% filter(!!sym(settings$measure_col) %in% settings$measure_values) + } else { + sub <- data + } + + p <- ggplot(data=sub, params) + + geom_path(color = "black", alpha=0.15) + + labs(x="Study Day", y="Lab Value", title="Lab Overview", subtitle="")+ + facet_grid( + rows=as.name(settings$measure_col), + scales="free_y" + ) + + theme_bw() + + return(p) + + ' + write_there(func_body) + write_there("}") + + } else if (type=="module"){ + + # write UI + write_there(sprintf("%s_ui <- function(id){", name)) + write_there(" ns <- NS(id)") + write_there(" tagList(") + + ph_ui <- ' + sidebar<-sidebarPanel( + selectizeInput( + ns("measures"), + "Select Measures", + multiple=TRUE, + choices=c("") + ) + ) + main<-mainPanel(plotOutput(ns("customModOutput"))) + ui<-fluidPage( + sidebarLayout( + sidebar, + main, + position = c("right"), + fluid=TRUE + ) + ) + return(ui) + ' + write_there(ph_ui) + write_there(" )") + write_there("}") + write_there(" ") + + # write server use pre shiny v1.5 module convention + write_there(sprintf("#' %s Server Function", name)) + write_there("#'") + write_there(sprintf("%s_server <- function(input, output, session, params){", name)) + write_there(" ns <- session$ns") + ph_server <- ' + ## replace with your custom code ## + # Populate control with measures and select all by default + observe({ + measure_col <- params()$settings$measure_col + measures <- unique(params()$data[[measure_col]]) + updateSelectizeInput( + session, + "measures", + choices = measures, + selected = measures + ) + }) + + # cusomize selected measures based on input + settingsR <- reactive({ + settings <- params()$settings + settings$measure_values <- input$measures + return(settings) + }) + + + #draw the chart + output$customModOutput <- renderPlot({ + + data <- params()$data + settings <- settingsR() + + params <- aes_( + x=as.name(settings$studyday_col), + y=as.name(settings$value_col), + group=as.name(settings$id_col) + ) + + + if(hasName(settings, "measure_values")){ + sub <- data %>% filter(!!sym(settings$measure_col) %in% settings$measure_values) + } else { + sub <- data + } + + p <- ggplot(data=sub, params) + + geom_path(color = "black", alpha=0.15) + + labs(x="Study Day", y="Lab Value", title="Lab Overview", subtitle="")+ + facet_grid( + rows=as.name(settings$measure_col), + scales="free_y" + ) + + theme_bw() + + return(p) + + }) + ' + write_there(ph_server) + write_there("}") + write_there(" ") + + + } else if (type=="htmlwidget"){ + ##TODO add htmlwidget chart template code + } + +} \ No newline at end of file diff --git a/R/app_init_addin.R b/R/app_init_addin.R new file mode 100644 index 00000000..0b2bccd0 --- /dev/null +++ b/R/app_init_addin.R @@ -0,0 +1,215 @@ +#' RStudio Add-in for constructing lean ADLB and ADAE data +#' +#' +#' @import shiny +#' @import shinyFiles +#' @import listviewer +#' + + +app_init_addin <- function(){ + + + ui <- bootstrapPage( + + shiny::wellPanel( + h1("Choose Configs"), + shinyFiles::shinyDirButton('directory', label='Dir select', title='Please select a folder', multiple=FALSE), + shiny::verbatimTextOutput("directorypath") + ), + + + shiny::wellPanel( + h1("View and Edit Config"), + hr(), + + shiny::wellPanel( + h3("Edit Chart level config/Meta data"), + listviewer::reactjsonOutput( "rjed" ) + ), + hr(), + + shiny::wellPanel( + h3("Result config/chart meta data"), + DT::DTOutput("DTmeta") + ), + hr(), + + shiny::wellPanel( + h3("Add chart"), + splitLayout( + textInput("chart_name", label = "Chart Name or ID", value="new_chart"), + textInput("chart_label", label = "Chart Label", value="New Chart"), + selectizeInput("chart_type", "Type of chart", choices=c("plot", "module", "htmlwidget"), selected="plot"), + selectizeInput("chart_domain", "Choose data domain", choices=c("aes", "labs", "mutliple"), selected="labs"), + textInput("chart_package", "R package", value = "No Package"), + textInput("chart_path", "Path to chart yaml file") + ), + actionButton("addChartBtn", "Add chart") + ) + ), + + + tags$head( + tags$style(HTML(" + iframe + { + max-height: 200vh; + height:800px; + } + ")) + ), + + shiny::wellPanel( + h1("Preview App"), + uiOutput("app") + ) + + ) + + server <- function(input, output, session) { + + volumes <- c(Home = fs::path_home(), "R Installation" = R.home(), shinyFiles::getVolumes()()) + + shinyFiles::shinyFileChoose(input, "file", roots = volumes, session = session) + shinyFiles::shinyDirChoose(input, "directory", roots = volumes, session = session, restrictions = system.file(package = "base"), allowDirCreate = FALSE) + + + local_rv <- reactiveValues() + + shiny::observeEvent(input$selectFile, { + + path <- rstudioapi::selectFile() + + local_rv$filesel <- file.info(path) + + }) + + + output$fileSelected <- renderPrint({ + + local_rv$filesel + }) + + output$filepaths <- renderPrint({ + if (is.integer(input$file)) { + cat("No files have been selected (shinyFileChoose)") + } else { + shinyFiles::parseFilePaths(volumes, input$file) + } + }) + + dirParsed <- reactive({ + shinyFiles::parseDirPath(volumes, input$directory) + }) + + output$directorypath <- renderPrint({ + if (is.integer(input$directory)) { + cat("No directory has been selected (shinyDirChoose)") + } else { + dirParsed() + } + }) + + # load config + configObj <- reactive({ + req(dirParsed()) + safetyGraphics::makeChartConfig( + dirs = dirParsed() + ) + + }) + + + safetyGraphicsApp1 <- function( + domainData=list( + labs=safetyGraphics::labs, + aes=safetyGraphics::aes, + dm=safetyGraphics::dm + ), + meta = safetyGraphics::meta, + charts=NULL, + mapping=NULL, + chartSettingsPaths = NULL + ){ + + config <- safetyGraphics:::app_startup(domainData, meta, charts, mapping, chartSettingsPaths) + + app <- shinyApp( + ui = app_ui(config$meta, config$domainData, config$mapping, config$standards), + server = app_server(input, output, session, config$meta, config$mapping, config$domainData, config$charts) + ) + } + + + output$app <- renderUI({ + req(input$rjed_edit) + #browser() + print(names(newConfig())) + tagList( + safetyGraphicsApp1(charts = newConfig()) + ) + }) + + + output$rjed <- listviewer::renderReactjson({ + req(configObj()) + listviewer::reactjson( configObj() ) + }) + + + oldConfig <- reactive({ + safetyGraphics::makeChartConfig(dirs=dirParsed()) + }) + + newConfig <- eventReactive(eventExpr = input$rjed_edit, valueExpr = { + + + newChartConfig <- input$rjed_edit$value$updated_src + + for (i in seq_along(names(newChartConfig))) { + + + chartFunctions <- newChartConfig[[i]]$functions + chartFunctionsNames <- names(chartFunctions) + + chartFunctions <- lapply(chartFunctionsNames, + function(cf) { + eval(parse(text=paste(unlist(chartFunctions[[cf]]), collapse = "\n")), + envir = .GlobalEnv) + }) + names(chartFunctions) <- chartFunctionsNames + newChartConfig[[i]]$functions <- chartFunctions + } + newChartConfig + }) + + tblMeta <- function(charts){ + + bbb <- purrr::map(charts, ~{ + bb <- as_tibble(t(tibble(.x))) + names(bb) <- names(.x) + bb + }) + + bbbb<- do.call(bind_rows, bbb) + + } + + + # DT for charts meta data + output$DTmeta <- DT::renderDT({ + tblMeta(newConfig()) + }) + + + + + } + + #viewer <- dialogViewer("SafetyApp initializer", width = 1200, height = 900) + viewer <- shiny::browserViewer(browser = getOption("browser")) + shiny::runGadget(ui, server, viewer = viewer ) +} + + diff --git a/R/create_new_safetyGraphics_app.R b/R/create_new_safetyGraphics_app.R new file mode 100644 index 00000000..b2f763e5 --- /dev/null +++ b/R/create_new_safetyGraphics_app.R @@ -0,0 +1,55 @@ +#' start new project with an instance of safetyGraphicsApp() +#' @param path location for new safetyGraphicsApp +#' @param init_default_configs copy over `safetyCharts` default configs? +#' @param open open new rstudio project? +#' +#' @return Used for side effect +#' +#' @importFrom rstudioapi isAvailable openProject +#' @importFrom fs path_abs path dir_copy +#' @importFrom usethis create_project +#' +#' +#' @export +#' + +create_new_safetyGraphics_app <- function( + path, + init_default_configs, + open = TRUE, + gui = FALSE +) { + + path <- fs::path_abs(path) + + if(init_default_configs){ + from_path <- system.file("config", package = "safetyCharts") + fs::dir_copy(path=from_path, new_path = file.path(path, "config"), overwrite = TRUE) + } + + # write start_app.R + cat( + ' + library(safetyCharts) + library(safetyGraphics) + + # Start default App + #safetyGraphics::safetyGraphicsApp() + ', + file= file.path(path, "start_app.R") + ) + + + if (gui == FALSE) { + # create rstudio project + usethis::create_project(path = path, open = open) + } + + return(invisible(path)) +} + +# to be used in RStudio "new project" GUI +create_new_safetyGraphics_app_gui <- function(path,...){ + dots <- list(...) + create_new_safetyGraphics_app(path, dots$check_default_configs, open = FALSE, gui=TRUE) +} \ No newline at end of file diff --git a/inst/rstudio/addins.dcf b/inst/rstudio/addins.dcf new file mode 100644 index 00000000..2ba69611 --- /dev/null +++ b/inst/rstudio/addins.dcf @@ -0,0 +1,5 @@ +Name: Add Safety Chart +Description: Add new custom chart +Binding: app_init_addin +Interactive: false + diff --git a/inst/rstudio/templates/project/create_safetyGraphics_app.dcf b/inst/rstudio/templates/project/create_safetyGraphics_app.dcf new file mode 100644 index 00000000..5e9b85de --- /dev/null +++ b/inst/rstudio/templates/project/create_safetyGraphics_app.dcf @@ -0,0 +1,11 @@ +Binding: create_new_safetyGraphics_app_gui +Title: Create new safetyGraphics app project +OpenFiles: start_app.R +Icon: safetyGraphicsHex.png + + +Parameter: check_default_configs +Widget: CheckboxInput +Label: Start with safetyCharts default configs? +Default: On +Position: left \ No newline at end of file diff --git a/inst/rstudio/templates/project/safetyGraphicsHex.png b/inst/rstudio/templates/project/safetyGraphicsHex.png new file mode 100644 index 0000000000000000000000000000000000000000..47b0c624d5d95a9ac9d9a0ec6bf7fedb2495fbee GIT binary patch literal 20169 zcmXV21yodB*B)Sq0i;Vn7`miEkWQsjKbd?vnlw@Au!e zSd4S;J!kiR_C8x8RFqynK_^9rKp;*=Bq_Y~MQ(^qM zCy$Iy9T?cRT1MO@YWrMEEDD1e4qpz8tc$9v>Zd0ptk3EoBn)BE#fg(9ikF7Y{!wGh z^BQ|GbmE6=M?Pk1p784{_u%Q>zidVNL`vTN#HE+q7S6V@X$TOAA&KxlAXSVl`X>e? zt3QkHhoAHqCTV`B7vv`s9CP|84>BY{>;Kn27=dh&G)w%mJmdrJXH=sPuq@IoJTVsP z`|w^b=uf10$PUB6rB9Q(7zPe3%RC$Ve)$e@C$t`_!pcOC#L*7f31vfg6W)f(x5%F* z7Y{wbozCcKBZ^JEk-Q}@-?gR+)gp>W5r&xR0bu@uOOv|DaCSx>xJG7b#>+7S!s1r(_O6Xm0r zYf=L0s1OaflQBZ)AI->!iI}AaaS@~Sf6mDF-ww{<&@6>#Ms1;nkPSf3NN$UeA>dOE z@VTX8MgkRUghU3(K>qy1L3xjcqJkrZ7)O)n!qnF(N0LPV#3->|#fI$h7KW6BsD~({ zWJ4o8&+!rJ7%BsSQDI5|f`yEh(Q*TYmi--Q;~1P_aFO5udrM+5O+YDqu(&A>CLBK; zii6q#_e4XWP>pASyv31wuuI zxsJJyC<7EC4#!MFY=^8$(6%jsk7(@><91!dTV#(c6(H7%EO|(%I7tEWEOIy=wfHU$ z%1)4>e>fH1PmEo)0us#*|2{)D@thxQAq_@!nBkZW(4lsj_(?-@Sg<6mC0d8rAm9v& z^6@94N0tL1iE4zdfU*NJJ;DY-l?qIy!h{n*k)RQOq92A_VKQWerWK>|m2vXSXkZ@>sX9EG8x+M6U19SkRDq+9ya5U<1|lKe2v-h=%EB=r!D1YJ zPpHLB-=-kKpTC7kY63#(0UbDOCBalITX=uO)TYzkAw+Pg*uuh0y+K& zsL=lp6-jnj08OYk1*8D6Cq6s>oDIPUk|knEOr5tNBSr!#APb~I)Q*ONXI@;t>m2A78{_g9c?5p?r)(h2S7W(_9ti%7gvH&PLc6MIP_N*;rA70%-s_ zkUyj59E>h(2p$kf!k@p;ppynb4k(dNO!90-R3DMQAsfMFy5qI={z z4v7`WeD{AlD_cK%onS=^G$eLEPK@bD&M!d$@ux1ZBbU|Jdn7+ehIcSz{f}uL8F3b)xU~P6&Qe6z zh6PEb_{UR2Ae3KXX=KSBU-a)WCBwwSBB#QJKmrr?`4edW+gc2*z8Zu2^&=H<-4htf z=zmM&Jb4KX;&*}pQ}bu+HwvVheN+w*Oys`A`hR@KVI>=e)8hSLLxw;Q*0nm&yx0(S zK^lw`_oa*GXn;l&AV5YW%*ka{9{Gl>i|rULQ$F$y=?flTlhOU(<{7ls#DR~SQ?DO$ z=(De#dkRyex3`gO8OUqmH|duTc5_54m0LIqyi&eO-4To4YR4e~%;0owIHt1}%-!nU zU6dLL|DLW$^!TTj_xoM+D5Ij8^%O1cjw<~%U(o>ZJ+<03+=@sDN(ML)g&FE zHRiZw#fwA5kz1H#gH4V1tRh*xmP2uWgFyp{LKEv8tG7g0huP zaHWzr|G0r|-~{)s%~v&Aw>NfWaJU}1z%D;BTsWVsmlm^R3XH1jN$)*PH1HaWTMV2Y z3vkZHMoFJr`}iCi2gQ10zPZ@CUGQhIVM4~2oWjE&A9*v!eX9d(pM-v5Egj1k;j@yf z#SB+7ZC{oTL8AdtRn8@iZ(_YHORsy0R)p*2-^iAq#c^@9-@fr|Qp{>V<$TwkNosS? zYKW~seKE`-MgC`Mswrj^&jdD(aB?L`eoLl$?l(BdTkIzoIG%d+sDA zhKRK7+w%L4A8#{jiMPDvH}UR6St3JeYB_x<2w{HajBmeFlh#H;3{nph|~d2}jA zSxMHz&FJhVK9G?{da{sPAw{3)&X*+gI4Nb~aB=(Jdd`+x^2xn%GcVKi9YhwST%%d| z$~~+?3!9)gBkQZ^VX@~;jOB%4i%xFtUm9lzJFAmU8M|+u?zYyHsUQW z;16t1rg31}#6z{?(=0dddN#Ix5#jExY`ndEw|~t}?N!1_!@a#xzdDKkCH!Hpzx+4% zPfI3SPKh@y7sj3Cuc^J>4|qkL$p)fU`ZG%fx=z?$&mKG2La}Kt2$R#CmXtLPTL{~c z)QO*u($Plh$BIo1ri|My;7v-NY1;gecRH7?BnfudNH250C`6<9d~%QKS*&s9DW)3^ zjVaPUvb{SvSM86_OFMZ?M?=tAS{yayZLcP`VeDD#Ko$Njs8`GC!8xBTFRJq zBJ*}iTlM;~?g!R!(t#(WLs@bxZHOuAle{XDENQ8=%I+l3(ty- zHzFNqCRfjT^31}a$@urb-?$O8A01}d{o&adx(pXiPbIDF>ChHcUGPIkUWtLuJi~d? z0lOprsr{k8SmP^d^s+MfFeUMVYWcn9#nGL_3-!COEVdVuHQvc>F)aGtN`H*So^RnL z$!!s9u<8*G`c&k4&(baqt=c`crr;E)%*Q#gQ1;w;XQG+XcTp}SvH5j^-?or{3vay8 z`!5ew+RUf&KS~rFm?oB~pKe!en$jkG@9gNa%LOG)9*sVA04dbyJ*BU4y#@J$D}{1r zJ5quc(x`Y!mJlMt3k^~GcJV$7w>nfr*i3!{!+Uqw75x@M%% zvYAeQt4Qo_Eq=%F`+|%Iwui(bpN>$iomIl7IsQq*J(rc}Taupb^+gn7vQ6u(SRM%unqJ_%(Q_TCc|QdoHAmp`B%wx4`Uh5<>r;Q9 z{?UolNz?YQDe}l~KO&_^(F?3h7Ivn*L~&;ip}Qb0la>`G+-RY6$Ci|!K+CV9J<`{m zO?FFF;B3Rt3-mtL|4rBs(eq&^B0NMaW zUi*!tekmWJkxkEJccdGYM1xHN5Smn|9~;BhsQf@@92l3`@Y7D0TBe@sfB3o9s%hb^ zajR+XdE7rW9=T^5)RJe1$;1_-lu6cn7<7yv%$DoXEba7>nSa73uNvXHwYpZ%$C$~R zs6f{S0HR};@KI5rJX|J!<>=c0pFVDjur^fHzO_bip|X@agI+Tl1qj?@AWw092GIkK zqJBxy!S{+HobI@y_vOI{{u7J}IXI(K;4gRKj3{Esg{w_EZgE<~htCn}dGdM@u3fsW zFE3Gqsx0B00G#uPgnd%%a}>tO_)K!I=y^e{67G6L%encSROU%MoWYi_H(j>MfLb%$ zb^6nZ!gvmAV&K*90?kb1Z8)B3moNJi?`q8cEPk<+`mdbcKWySot6__;8lh-d@o+K< z?B4VmYzGgC!mcHflP$HMXn#O@!R>9nIR31fg%l4b7P7EADaAt?mC^gb(^vX0Oo1quvF%cqxFY2HK;mY^<;J3MXBbR&_}A5(VjhXJxthG*Zx`9$e^5Opy`fBiif<<_ z3eVKd_on-S<5y_dn54a^*(kR67UMfvd_G1XMa)o+@9PxfCyncW>eRv>0Uad zDuuAkr^eqE1{vQf)tsekGr)nJC5sBvjO+c2gckMsAex`AUUe}epu|HUj+zf^uCsKO z7}Nn=9|Ojd+=vrOp#DS|wj_zEP%6B2`OL2pjZnxnv;8yVKpYs4M{$wFg4y2q7gIWj zGhOSkZ9$_h|9v2wW*?EK)Oj`N+ksr_xlq3xT-&zRFQO>vl6bdN)_G8sTvD5KAJ5-t zgifWWz6_U9dS*RrMxN-C@U`JN-Y=w(VFJf*jwLa2nA8CoBZ=n6o%gY(R5*0X@2uKb z&*y4Y3#CTYHrE{CXH<9v6|v8r?y#NiyCs%DvsDut16^Uv-=d;&^bp~A>!^d5Eb2@?5x5)s5h}7b){GSd{kqg~@iT3f5!q%Y3y}c~5n%VEdp#mVm=j$Ot z#Nbkh&ou0YpLjNU{CgJ65@5;iH!Ix;eR4cG0c_w72q0MGg6IB)RA9oJA-^zJm~|qL z;Ci)Wk!ahu<^)Mv6+=I?l&C*YJH2OMkKXArpMu7(Hy3dwQfc7bCq^DKRRK}8e~w{rwG&0lG73ny(vT+_WkZ0#`K1sYp8U?@Gr4{9 z4oaceCAb8EvxGzIW%^(q`_Q;=smhKa@BhASe6=I@hRL^&1IUMfTAU5@%a_M($L;Q7 zqX>ctOI;I;c7p9xiX@gD&gsyO@J_zs~Dgz%lpM%ZP`IAT(+@ne5VBc@A zzddDIk>Ej&7ke||Wd|scQaSFZGJ3!7D61KsN+*1bxsUe~EnX}sKU?VEvsW}T5^(0S zC1v!bX%7g-x7Quh-)R1O0FE&!t8SG7Q4_Zz`8KkYI8R4Mi!n+d75JP)&~zmF!>boCyPqLPWS1)$*bw!=%dLuV`U-oJK1IkwOx9C z>PLzWWyYU%Tkk+T1dI5S&p7prKh!N;I&B9ZFQ$?^W zlbK&u>nz8oZ!{yTXvigyPC(}@ByNMaXPwvp!{%nkEQ+oO`naygk*AdwocJK{Hty2H zj8E9cazdbF{|=uPauO^Phdd=q{_Z&c6MAT;3OEEO zdM&wq+)hV$YZeiC(+hmB?{n{(1HXHXLxRXb!F}tT#3J#iwHx8E5UvNWw%T1gB5+S! z>Hx-Q2Kxg2e=S-2eH7^$&CcKI1kc`RMKnrQTk~JA{LgX7Qbn<<@AK0oUR1?CV{)OB z=)xlDY8L-<%8t(@x~&O1F!=3HnC$}N(z#hzw-EY@Ta?5y2aos-HCyZP&rt?70QAx4~rMm-kU(O zN`b!yr2^Vg-FvQY`0dOFUue2*$v#pf8`oN|#$QHmF@uo7YIH04Q(i^LPLK5q1G)<| zNra9!bwLx;5Uc36(NCHyQ^fm@S6bMwhr7e3rwj5G4+u-*by0i8x?5CP)AvYq6k1vg;=Fz^ zPddtqzT3b*Ss+cI*AdN3Ey8RjSU%a$r}4jBZx7#4r%28s_l-z&Co;y%wjI12J%oyZ zuCV|p$42J%FF(@j!d%XE9`MgD>UQmnN$B&m4pp#|F8I4gX2_kc2uBYWD=wxSJ zFZUBjKTPE17gtzio5TBv2g`+ZkHgSesfM+W)*`y3XBQx&hg4?U5aymDDhiI_pc9SS`K z%p0w;wU2p*x+L0uFmsbUidnT{qN`eDN)a#CG7Z(4$Dtq;2wRk7WNr?E$%m z1SMWf?+JhaX89L6)U;WeVz`Z&6>r2hAgX>RbL)C zAy543LJWx9Tq)+~*VZ>J$8u+aR=*sF`I%gcu@0Q1$uF&4O_PbF5PcegrFsbP?Q=dz z_Z=!Se>xEv*08DjD0sb=J-zWGgRh}81T@1UG$O8(L~*(P1_*)VJN;>+vX`zz-J-zb z19`3BxbU}0%zUMe-Nq1+5FHGVq{%O}Dg6@O(t*Bw9L8K&=Cq^M%{_PFH9?KaR4|Tv zjMw%@!PPBpfO<#wz8HN5O#+nWX3y*02{nv_DYwcRsC!8*a;w2j9HM-wXXi!kYCQ)dy>+cZi zj5Z<-qz$N06x@03`bjaf!^KS@`XSX*jHCey&tI7?2j)|Faq2hfX&t-3c7R3pk-R4M zDJ~DFvp|m~ucd(6&2bKDAO@2PlYuxg8+eG9=tcO>tM2zaQB9(eDbT27F&`p0eg_Qr zP0Q~kC`iUxF1IX~4{(!4tS;t&9$=*gfA&&3e9$V+KjJx35ZGu{++CwKFQ}`tPO8@_ z-LZL_tfX+@;T|^7$W}?}+u);#(L$&W5-`3UaMjqbUnGVIk+JDkAfZN8v!s)}W^*WZ z7ip3oilY=~9;y#pTkPtkV9*4xXWI;(>y5G*ZTt_9LAlt|LjPzpm6zr!dD#tl&zR1A|6kkistR)J6Yi&S$id#1Aum8{X}5?KZW|(c>*;O3B>;nuTrn9s>iq41R0VZJ?WoXa?ci$0DykJ&i+k z#F_Jh?Bhv9?z{2sWJ!ae)So@rr+vFSLA-HZ=(55^CubX`pg+X z0=*)cVm(J*bSHPEg;j0pH>ntNUtU971LLHG{B%q7g_c~d33{P+(LY!;f(P|}IWhjI zknH`;axHf}j>B2zeQ~0Q&ZIXqu;HT9lA{;$>O0%eL^o95LBNNE zJ91An>PzO~{jK9`E5chCub;-}ukH2EQ`F`uYg9!y4|83JTwa|<^PQ|9qouXOb9VcK z`;ZF1G&tj5B30O)=B3UPQXg>0-Fo;lLErsxPJtAyw7gXlVeQiJKKBG+BG}Wg;DUOi zx%)boR@It@z7jdg+2Od5GW;uDQ!;Jq{ikm)U)eG(1B4@(0l$C6bqUcJ>~$tEHc~KUbwneC0soYFt>+#=zWOf(R`=CT_ zkl*JNW{asWsiiG`?cRLh<)PGHGe3w=@_2$gf?0l9j@>n~Isch$TcUu18npXwKh(-! z6EtC@1HO8nv?%={6?-0e7*EA`^BK40!9Ar&>*+@jdJX*gAGnFQaHHZ**Gk56v=fMA zBxnm_^0U9*u~o-D+ZJ&6YAhV=KHG)hlM|mn2d=`{@-YbB{|e&*1>peCG6xX%gXxReK`1F55d#fp^h)-E2OZk^_gc}EaeEv!U~5@ zaPin>oOQ6w&plRfNy!O?VM22!%QopKbSK86MJukSABn%bzQy;*A`OS~a@W`nHY$Ri z4D5|W7%s-$t0+E@qas?(&;sQAF{J_rU{}|9tGKiQF@aRg-^Y^~aW$*Q;8KCtRsw{< zyyy|JqEs$akuF9_gsV_ji+LQ$3c1VkV@;H5=O|||+x7GzCKn|@F zG;B|jl0C_>4nF*dtkVW9{$M^r>rdE+gg#c^?i{l-E0oc(_2dN)99ssrX&{fU*lcet@wU6PSfj?$iD(gZvJ+vde02quamSPKTd9T6KW7e^8tdhq);=Hk=U$4WilV0%kKJD>zEI zpM2n6a^M4RhJx{;&(R-dw^?CLX-4`1=u}e!HA?9bLJ%xK`}<@k10u&oE>GkH=rcQT zyrm?T9TW*ho{rz;pT<7p+#*CPazB2#_(5TTU77;I4z{A2I%Ht^E7aNe)DyXIrZ>h| zh~nS_9)&k8Xf*VXBRPU;pIqa;5mmbvr?#5MXPAiHmYNHGFHZj=nD9{lPkhNmwSNV$ z_cj5wfdrtY-VpFf^&$gNa5p+1EO8PBmPqC|Yls`Np7LXJq0=_;GbI1tA&mbIF#dy! z_Vo88pS<#y8=^0sZ8A&6isM{;1qon=j63gp9hEQvLVpmgF>R~fJ>DyAn&p~Kh$})tI7?~^&!30t6dkkk4f})Gj@)>c%L(72kRb_*2SgPN zG$r8tVcA+;RTq6Lo<;J#*z;0xvqgA*Zfh*{d($x%FvWV{CeI{#LYiEve=i8a2y!el z2IT&Kfox#aczDh)Q(o*R{B2UmBB8_M;mn!BATL1zcX(jigt{L23!9N>1UdW?!=hpo z{_a4}>%ra=G0&VobogB2GrnT7UnY`4&_f)t(79qdd~vQ*>;8-v8mSt*gPn40mpes+ zE#?e(!13NE>=W_DTyu7`!yhYlTQ&W9jA|sA0&$FIe&3%yFr}Z=fcLU^t|{U%E{jw6 zO98r7w+Q)@6w|k^f;PQ*zWPB%0aYi|M^!DJ-D-s_@oAxws2->O?N_L9PG^`{K$dZ16`pDBqs}VoF{baCcnX{Bf3oO8N z)Ci`u8|lc8T`w{^7dkb@wA&wXLwE;Xz$C!T6s>;~+76)*MaVh2e);3O@0cz%Ia4lG zdre`C#Ca*=AK$nJ$SNvk1bsE~d`i&&sZram`s96XxPJRCp;~U?&37;Wh6JYb44AhF zgKzQiD1Dyi4JSh*5dbI!#M%6J_#Ea|HEu=;dVz@N!R>3fTu*FuS|T|~VM$1BhRbM4 zN`|N^sZO!u$-(k66tqHTj3*FNWIaSi|F)Mw<8&-86%}g4ox}SzOM*A}qOph~d&af>AnKlS!Nfqums6^iw; zR}}u#z~zxaF`41?piiYER}qLqR0VJ$0QsNzJyP3PL4>0@P}lmELHB58j2J*7B1RS% z+^arfIiipdV34GS9JESbsZBd8!wzHbX9g%5l>5y4kW{F_#zcQE(GiuEKK^Bb(1x%f z@N^o=d==8@3&k~XNA-Xdfr|S)e0!p|X4QmBXSO%m-rzZIi-nsn#b50I##`A?f?V>l zOacb-6^)y2i8qZv&E7be=@ID0(o6?N#N@)j+$&A_l!$9?+!NMIVI00K(vMHbZA6O~ zj*t(1@hCG0oXkYOZ6K%w)4A`8$=s%^j2783#vMd>Os%{atv@}Q0Q5*{;2PVGP&^c^v zOSk0-+!!2vh$atxU1ee;GSe!I(gNqT(ldk*SunTyPkq3~E}}9M%-qDIW@!RF>+G~V z)cnoCrt#%hdW9t2*@i1d6UY!Z6J4s!027=-5Uas~LB!q3NadUkV7pXX*kZ`w3#yls zKAMdg59{l}$JpIiB(y#+AqkylTw*eR=MFzMUZ5O&FlIXhqnP_3cHx|qAkPgbKmBc| z0T~EnL-02k@jwI~u#RB)qyfwXbxB zJq@j22_>_wTR}w&>R<*)pzDk>_vy1JU0n(y9iI}4DJ zl47C+^_y~iQ8+$6?*H#%Vq)69+B(<@9d8*3RHUZ=u0OUO}Oxu<)BI zu^CpF$>qU zYpZH#oL|h@X=!P3V>(@*ZM!XcY}RjP$SW#-*CRfXq-E^H7QJ0|7Vr@Tg8t-_qF;VE zmOl)>6U$+@QadDN^ zt)hn-MMg!r9n9B*6=r9(9SM;b0k}s1Uq1X>!(JM$sHk|AllM^m?4{#+5VlA`dHLlX zDLy{8`LEB5&0ZMZ?w+34K+nE+=Ti^61`o4U7O|#g*4nPFHQmO?*ycSwJr{d3iMFzE z93>5P|BeochsPxeSiT(qqs1U*JKGHCH%;Yt$UYJ}s?sif`SRuG)1>eT>vSOx$1tpC z#wI3n)i!({C+j_>tbK`jt#-`a^-i0&qGXsT%)EQk6>B-MBBUIKZCj%`g#>LJ3h&;{ zWJ^Z@g+6>z&3WarGtRX7?1jzb-r4q;aS}>^cCD?j&$Tm{E}RB4Ibzb%ArBVK866K? zyqfV$eEVl*t)A0A-Y@`?f z`{-#g`**aWdQjzY`d9S;LCg${=ZFd8wcK6kG$3yB@V9RX|aM2w_{JNELB8%%e ztTIprt*o-rajHzO6|AkQs_NoWJ-7ccB0^R{K|xVbQCWGoudnadA7dK>MIEkk1OEkepeeQKpSr~G_Hu0 z6=h|uz(&AB8lAU0?iN(ASP^f2Vul)l|HlBnv?9cByHtDWyrrV5IvT^b=sw6=`{R)C zI2osH_nbo1_jby_PuOMWjg^&^i;D{&Pfbk?7~XJ(a8*?m&D*jdAMewv)6H1&myTy! zquJTnMMcbs{o_Ba#^l-RO^=q_)iT50*UtF?lfq|E`YPyF2JAgA?^XWT)0kIPGw-Xl z)z#J0UVNBs@;Hr-j@B&IMqZFiIy7MIlq(3Bx4qOd@Ts;MqhG{c-5$$#+Zv(T(!-}$ zz^P0H!kGMbyoQgDk7Qe3TrB9gE~Ee7B`zND@Oe-rmDlEl#qX~@wNn!lKBq%`X?%9( zl;82;!oohl(hw}9rY0vT1YN;rlipZLb|$73_Z{&dlo;}tCr2xtO}o#lEk{>DOfXqG zd31^6ZsKn>nZUsxa#x8ByyKfqr3?JZ>p%!~IbQAdC*$Vkwirs?xVhS!sWJfEwg;i! zJ(Pz%AtS+F_9 zsQh?}Cj@}!`;$p~JQR^=N<4|3+t>eL*d9;w+PhWXWkLf91ye>@nU4fBIXM}~r4D2b0xqeF!jE^Eue?C9M6lr!l~V;$jIO6f?B$| z^CKf|9MRpz+u-J0&4;^u>Zz$IRBs_*5wF}o;5Ob}%*9fQ`u_d(LBG19u;+S zGRW@tZw2+Bo+W>*9wgK8@p1ohT+GhT3VB`!^_xJg$jHbcAtCp4Z=u&fcp}$ZG)sg2 zMxQA_INqPD1?eCnB7!ZY82CU|)^du0?@RY1!vc2M>{dTswD==F1E2HchjV9;*DAHl z0Bi@>{zUQABd2A$Or~}qW;=n*LK=be3z54*Hs;^H*ULGVC`ozM@$`fEk zWG~(`va?@)*miB4D%H6JY@+Uomrq?DE-hW2f|QoRrpJ7%e0F!a1d_nyW!0$Vum1k} zy~@GcOVXI0y1GaIJqPZXmuJ4%sMG-c0uX0RPv5{$->wK=%!k` zRKj%{*iP13J3bbv^m%=K-FBe?d4BHSKds38jt1SbSkZfaDynaO z5BDa$AAL^x833@j`6JKnfIS~|BWJnTbVU;@#N~dp3|trN_+`jEXVkK4=K6g1ay?Ob z5m?Dy3=o7x?Oe#++UJ)}e-%D+tN@s`Gg+#mt*x!A`?cLrMpm|dr>Ho?`$YDlZn8g- z;V!B~>JTJwfI{jYrZ`P{ew@PnyN#`Gsf}peAFT1ISJ=a_DKBE{ofH~&oCQ8k6&t*N z|9It?j_D;z40LovtlL;I01h)ne5Pwb ztPQ$Uy$|6oZ`dj7L2drm`t_QZ!imA^Pp&L`>+Ol~<@^{3k02SmEuExSOx1N7;{8^` za3wdg?OeOyN_OKHa46^9K}oSww!UuaC%@@*vfkepiMi}puUYbTh0^X9kUOhNAJx|T zrn*Ceq18xex>A1OTGC8uLAUYBG}qR=wm-=8j^x8}Nl9Jz)s>ZMVgV8$-gb)qG3NTh z{|T})Tg`Gq^R};-uRH(=wbD=6c`MHDHa*DBR5yQ24}{T}!wWV0hlE`LQX8Lto*}wKlfceC5uKP=Jmc=9Z|{5dtZ?A$d;5g zlH#~^WkbU(~oE)N`n!>fJ_>5!D;h%Mfr~&uSmN@dc|8&fT8&)zDJ0O zxARpXbo!%RO z-_MVn>%%C0`3>I*Cmb4>!FG{^qU)g(s7N=R6=h`y&6kV#8>jIc4;r1`(+>q%hLG3_cy#~_P)d1Q*f>WE@r{Cghcu5heh!M)suh2fnfviXTuPrw> z_vcTZ3)UU?he(z{?EN44DH`OMGcz-L2B)W|AadPx6C3;>rcQ444?6v2&V6}-z&)HD zjRL~IKd+qRklOmj#?_~LEP;x>C7m|bd&w!Pr19}L7fe{7STl|}4QE-7PPD{g*X+H~ zdXDR-LbgCrD^BT3RyF_&1-G5CHcS7vnKdAYOgDLO-|Q9P(!Bbi<3;(E=|d>hk`Q}f7`9PF^$vggi>bMzX1b|6&YTj>8l`lPWo5$S|Oc~pr2Z$3OkV5B@ zajicWWc>E<8>z5og04`h4gke`a-M#p|Kvm38ZguQ96jmNC)c$YzH7gsp&>J6PXL=d z29lUo4ibqMv?SVki@o(p-Xlcx&5b+aZ`>8DjD`ioaxS-^sRgKNaeN;7iwer=@j`Xc z+oMjfG#PfTd-LLB;lAFVsGhF`@HVWj7L=<1HD35BkM5qQ=()ZFi71uNF5~lO5@KQt z9#Td6vHQClt&+D?(su~un2|PK=UbyRFJI1xcBf=!8oYhmCxSS@XPpF|w>F;1KlXs^ zvBVA#odfcW)wzTmQ>>iZG|pagN?+Y;WLwg^A)WYJdFxAb?IY%t!9sx?YVDR5SM2w| zZ{YC^${Cpd@{^O2nn2mAEPBV|r8d5H{9OBI-vd+J=g<5eC#DyX$i9CRM^U}`a{2L# zr*_L*u6O)E4!_r_0q_!F>!soQYPRc({lPmcvx>KW#>QrH5cV`wH)OHOgZ&@QL8Xr; ztjdE}gl@CJh!x-Lmn>8Aw~hT@X6L>`m?wlU`B|*#i!@YB5)Oa>0j77^8u^C6z64;~ zMFRj?x3{Omdo0b-q`b^q`Tn_x)Kv2mjNKEhets7^w!paVhYKG)eDV8y&MOgw55acG zeyS{lU&!@g2$bcK;m`{v-cbw?T7l-gG3<23KekFx$&{ z_M9ln?wMi-;&1LOFQ~qwBiGH?JP^}T>(XV_p>r^>RoItZjJIb!p!N}RR~eU+rVrCX z^y;Cbr>9?@H~E6b@0xXt%q&H9bXQD(7ZQN&H%9P|nZvs2qm8lgD#)RrlG$mcTRws8 z;Rk|%D!svuV_z8?KB)1Eep$hM35K4plzvib$DAt-ibUxsA_QW?6%m?Lft~VTP&m1! z#Kp%`4dW*G4`#~>$ame`Yd}X|M1#am0QmD2Q>-W>;Y6VEUKsz%tX=jjna_+!ZNgZR{F*kJS; z{^Ee;yfX@c#i6Fx!_hMDspmi0&4U*v>ht{ zYnA}vsf16TCR=>GPI%D>;?6+O@aPJ9soLUa7H&%NPl~^(U=@aPG4Myc;`JtgmV@`i z70T^H!^8P55X&|V)E9P(SE)=O3g`?~}<2^PdDpdiThd z?js(`NBS6>wpH^_)6{^1`ddqemZm1k+4(A{sEjE$*N)wZ*mOzGyp^b?^H0ADxG+~< z?f6&FygKS~^al|aw34pbi@{$cNMY5vSa4hF{{W3xWoh4V^tqU^xHka)PI=ZLbrK&J zXa6W0yQ||~N@8|>@)?1=(I&r*Aa9`UeOSzn4lE}(*R(%@F6VVDmS@Wi06Vwk^#F50 zz01zG$1H$g?hQcC1YAo1^MXv5KL*13I~A25fI%P%**k1XPGc!w-AhQ9xo15o75O7z`kRLa43Jc%$Gi3f68WQ&Y$I;25U+xC_e~)-d z#JetTB_6ao9t)g%6C*l_-DJ?U195dl)Ry7p%d17N!{k#wj!Zz;DhSzot?QLAJ1^j{bfws2i{q5E5?d@YDLN}c$^|G8?WgGKe)Bmktk(SiZ8I&{>6Hdms>$gQzON zNsf+=e}AX%QgX+{#@Ye)Ku7Aau+Nsp2sOF~!~?jW4&XXyY;6u_T0BIQXxeap0?vdg zyxx?XyFSFXglY3Ki%C7t)XWTd-W}jx(5eYqQ zgO54_XTf)%4DA<55C#%VkZ0QlA^@oq9}n+2k-U$O52!$;rKLgNX9nQI451nKl9x0z zCXYa1mz@*S34C~L*#NRY@w>NeTs8CcUNlA5QFibXYtSc#NtX7_^C<&Smz|_Ti{c5- zxev9xA7;nRt3bVaHHpm3!t&T%0V4K(eaVHHn2^9_HjtDxs2Fyg55j2;sC+=l3;@{u zt4&}!s95BEgO8U9rK}kT2^Tl_V-5pFD4Sjb2c{F~Epc1?CLkuxe8hZLr+X`v)6>%c zSn=5{3JD80gY1wl=w_=s5&DFz<^Fp2hsYD#`i)OUouOC0L>qVV@pZduQu?9cH^$j$ zC@*t6+?IT5jXF^ljSN6{xY_Fxv=@=@;AO?dX`+6<1r=V2{XF)|ZQmrrdb+zG%jElF z)$1#RRfOG2hq~h)GEfguoUw&UewW6ZVcWLih{1Z-2&e?b0eb3qF+dscbia#FfxO+&n7&{in9Pfs6W+H)$ zt>qzcQ@MAl_-Xlaq~PAN3!Uf6Z{vmjk3;%udIoWqa|Vw$`otIB+Wzuj=Jm+vLYEIO zy?lnP;DvXuQ@<=+xaE<0eXly-7>}jr=xEOE;nAD(zdCsEJ-g10UP9NbyH?wS9afb# zZ}h>Z1;g)Zwq>;{~r(u0+iw-M~I zyWX7e@Z#W$C-5G;qVae9#0eh>jVy?ZiwkbZxzXVtzL(z7$H%{Wb~WJ5d0FY+y$dSB z@>;F9_U!3m?e$CVnzd`6MV*58B5pW|#oawU`#X~McXS_(Jce7JG1&d*5!dJSXNN=a zbmFN=x^lqfMZn-jtfZ0psVpsxUa=yv@#Y*4kDO!S`s}Edr>ww>L8VSZ3oyX5Ku6qmQRguSV)s>nxZE-zv*#Yx$%kWN6t2m1$CFXlP~D zeV1Ubo;vyNsX{nqC4%_>vSr!n_LNk3HLd8rCh}Eyf%bIHl2t1`zru*Foj_Sx)&RvQ z_Z9grerDS7u3-9xl_`^R4ul<Ek$s5x?A2Hr34JLZ!K)$)i z^LEXKqJ?*u6iE$Jmu)KMT1Po!yqujpIjW#M%_A0mC7C0=i%8Ddf9=TW^MpbMKWoM10_Xa|Abr)c^I)(GBRr>J6-Tc#PF2!fQ}gP zAHg*gGw&P}je;Iy>QX!L{6h=5QZ7Y8w0Xw9>c=65!y)5%t%MXN`_MU)q(Wdvh%DRn zDi!Qq+?Qt{rgj;fv%*Q}F90rNys@SBn>_utmQ#KljlijX$;5e1nZnQ6uS~LN^(kMD zb^VQ;_N@yT8;nWSSJQUJQUxkc)tWLNCS*=ruC#X?Ij)rmj+_~rV=|{r7VA1TC{v7_ zR#fl(qrHEbPeFd|@3VbmQh}v&XD$XY5GX0ID`o)Qsqz$16W3)c<+GNm$gFX-4!Z41 z`3%@5Jzr(DQ<(X=9L)xX6}Ag<4FHi?%w6EXgs2_qyHR#ktJ*0$o129X`csyGTFszX zL`AG-m5eTC7H}z32E$unP|DLm^sx$tP2vop0z@O3*use`;C9L+Rw%FVTSfV9=9aSDwsu92t7V{{| zrvgeYRBwXTlq10N^Pr6$=akBXNP`Vs=(e@ zwMOC_;rCAqXLW1SB!GuLwkb7ghSvgdE5DW80Srj0&{nW4qc&m(Von97eoM8UXz{xX z8tiS7HdcO;Y?ll1Ag=|2L%D$G6(lh+WlL<#QdP2Jt%JrlLl|Sp)Gh>XL=57CT+-!W zBoj3=lBecs|Ar4yRGvI_rlgB|jmNlU2#4Go>_fM!Ob_LvzKskWYEhAIWg?k?n->8f zQ7oWOGEDx}#Rd+mJKDqog9MaSpx}(iCH<6dGVINszo(oX|swP2}g#L!FnC&bxV zs|PYco!Mz(X~E(F4aaL zoFyL|zQwlv2yGK}I~?gUAL0!uN%}wbu>E?VcJ)=sW_bP>1#D}&Jrap*^k8bo6|ADxd8$8<9#_(g9>BNi!CttLO>0QCu=WzE0>vFtS0 z0w;{w=1Wg8yXr28c-=L`01I5!-BRj!8>Qey+0`*-L(63T3Sluh>b6YP=S6w^j!yG$ zhQkC*IkGnp9OO=~av_pO!^jA^%bDtd48h wZ>NU*e4m)1KU-+JBo`6V7mgtkQ^4DRSKlWwN0OZ&G$&;7qA=ZwKtt~T0gMn*A^-pY literal 0 HcmV?d00001 From 48120f7afad22cf7e9514592127635ff9c9e0f42 Mon Sep 17 00:00:00 2001 From: xni7 Date: Thu, 7 Jan 2021 11:30:56 -0500 Subject: [PATCH 2/3] exported functions and dependencies --- DESCRIPTION | 4 ++- NAMESPACE | 19 +++++++++++ R/app_init_addin.R | 2 +- R/create_new_safetyGraphics_app.R | 16 ++++++++- R/mod_settingsCharts.R | 31 +++++++++++++++++- man/add_chart.Rd | 49 ++++++++++++++++++++++++++++ man/app_init_addin.Rd | 11 +++++++ man/chart_template.Rd | 28 ++++++++++++++++ man/create_new_safetyGraphics_app.Rd | 26 +++++++++++++++ 9 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 man/add_chart.Rd create mode 100644 man/app_init_addin.Rd create mode 100644 man/chart_template.Rd create mode 100644 man/create_new_safetyGraphics_app.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 476adb11..9018d3fd 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -43,6 +43,8 @@ Imports: haven, shinyWidgets, tidyr, - shinybusy + shinybusy, + listviewer, + shinyFiles VignetteBuilder: knitr Roxygen: list(markdown = TRUE) diff --git a/NAMESPACE b/NAMESPACE index 9c9fb165..46dbda9d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,7 +1,9 @@ # Generated by roxygen2: do not edit by hand +export(add_chart) export(app_server) export(app_ui) +export(chart_template) export(chartsNav) export(chartsRenderModule) export(chartsRenderModuleUI) @@ -11,6 +13,7 @@ export(chartsRenderWidget) export(chartsRenderWidgetUI) export(chartsTab) export(chartsTabUI) +export(create_new_safetyGraphics_app) export(detectStandard) export(evaluateStandard) export(filterTab) @@ -37,18 +40,32 @@ export(settingsMappingUI) import(clisymbols) import(dplyr) import(esquisse) +import(listviewer) import(magrittr) import(shiny) +import(shinyFiles) import(tools) import(yaml) importFrom(DT,DTOutput) importFrom(DT,renderDT) +importFrom(fs,dir_copy) +importFrom(fs,file_create) +importFrom(fs,file_exists) +importFrom(fs,path) +importFrom(fs,path_abs) importFrom(haven,read_sas) +importFrom(listviewer,jsonedit) +importFrom(listviewer,jsoneditOutput) +importFrom(listviewer,renderJsonedit) importFrom(magrittr,"%>%") importFrom(purrr,keep) importFrom(purrr,map) importFrom(purrr,transpose) importFrom(rlang,.data) +importFrom(rprojroot,find_root) +importFrom(rprojroot,is_rstudio_project) +importFrom(rstudioapi,isAvailable) +importFrom(rstudioapi,openProject) importFrom(shiny,dataTableOutput) importFrom(shiny,renderDataTable) importFrom(shinyWidgets,materialSwitch) @@ -60,4 +77,6 @@ importFrom(stringr,str_detect) importFrom(stringr,str_split) importFrom(stringr,str_to_upper) importFrom(tidyr,gather) +importFrom(usethis,create_project) importFrom(utils,globalVariables) +importFrom(yaml,write_yaml) diff --git a/R/app_init_addin.R b/R/app_init_addin.R index 0b2bccd0..87ce12de 100644 --- a/R/app_init_addin.R +++ b/R/app_init_addin.R @@ -69,7 +69,7 @@ app_init_addin <- function(){ server <- function(input, output, session) { - volumes <- c(Home = fs::path_home(), "R Installation" = R.home(), shinyFiles::getVolumes()()) + volumes <- c(wd=".", Home = fs::path_home(), "R Installation" = R.home(), shinyFiles::getVolumes()()) shinyFiles::shinyFileChoose(input, "file", roots = volumes, session = session) shinyFiles::shinyDirChoose(input, "directory", roots = volumes, session = session, restrictions = system.file(package = "base"), allowDirCreate = FALSE) diff --git a/R/create_new_safetyGraphics_app.R b/R/create_new_safetyGraphics_app.R index b2f763e5..e8f96781 100644 --- a/R/create_new_safetyGraphics_app.R +++ b/R/create_new_safetyGraphics_app.R @@ -30,11 +30,25 @@ create_new_safetyGraphics_app <- function( # write start_app.R cat( ' + # load required libraries library(safetyCharts) library(safetyGraphics) # Start default App - #safetyGraphics::safetyGraphicsApp() + safetyGraphics::safetyGraphicsApp() + + # Run the RStudio app initialization Addin + + ## Option 1: run the following line of code + safetyGraphics:::app_init_addin() + + ## Option 2: open through RStudo Addin button above + + + # You can scaffold a new chart by calling the add_chart function. see ?add_chart for details + safetyGraphics::add_chart("newChart", "my new chart") + + # That is it for now! ', file= file.path(path, "start_app.R") ) diff --git a/R/mod_settingsCharts.R b/R/mod_settingsCharts.R index 450bba92..3b41b8c6 100644 --- a/R/mod_settingsCharts.R +++ b/R/mod_settingsCharts.R @@ -1,13 +1,18 @@ #' @title Settings Module - chart details #' @description Settings Module - sub-module showing details for the charts loaded in the app - UI #' +#' @importFrom listviewer renderJsonedit jsonedit jsoneditOutput #' @export settingsChartsUI <- function(id){ ns <- NS(id) list( h1("Chart Metadata"), - verbatimTextOutput(ns("chartList"))) + tabsetPanel( + tabPanel("jsonedit View", jsoneditOutput(ns("chartObj"), height = "800px") ), + tabPanel("DT format", DT::DTOutput(ns("chartMetaDT"))), + tabPanel("Verbatim", verbatimTextOutput(ns("chartList")))) + ) } #' @title Settings Module - charts details - server @@ -22,7 +27,31 @@ settingsChartsUI <- function(id){ settingsCharts <- function(input, output, session, charts){ ns <- session$ns + + output$chartObj <- listviewer::renderJsonedit({ + listviewer::jsonedit(charts) + }) output$chartList <- renderPrint({ print(charts) }) + + + tblMeta <- function(charts){ + #TODO move this function to a helper file and fix warning messages + bbb <- purrr::map(charts, ~{ + bb <- as_tibble(t(tibble(.x)), .name_repair="unique") + names(bb) <- names(.x) + bb + }) + + bbbb<- do.call(bind_rows, bbb) + + } + + + # DT for charts meta data + output$chartMetaDT <- DT::renderDT({ + DT::datatable( tblMeta(charts) ) + }) + } \ No newline at end of file diff --git a/man/add_chart.Rd b/man/add_chart.Rd new file mode 100644 index 00000000..4ec40b57 --- /dev/null +++ b/man/add_chart.Rd @@ -0,0 +1,49 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/add_chart.R +\name{add_chart} +\alias{add_chart} +\title{Add chart config (adapted from golem::add_module())} +\usage{ +add_chart( + name = "newplot", + label = "New Static Plot", + type = "plot", + domain = "labs", + package = NULL, + workflow = list(), + open = TRUE, + ... +) +} +\arguments{ +\item{name}{The name of the chart (also name of the yaml file)} + +\item{label}{Label of chart} + +\item{type}{Type of chart: \code{plot}, \code{module}, or \code{htmlwidget}. Default is \code{plot} (static)} + +\item{domain}{associated data domain, for example \code{aes}, \code{labs}, or \code{multiple}} + +\item{package}{optional, R package that this chart is associated with.} + +\item{path}{Path to store the chart configuration files, expecting a config root folder + ++-- config +| +-- aeExplorer.yaml +| +-- newChart.yaml + +| +-- aeExplorer_init.R +| +-- newChart_main.R} + +\item{chart_template}{chart template function} +} +\description{ +This function creates a module inside the local \verb{config/} folder to define a new chart +\itemize{ +\item dump yaml +\item create R scripts +} +} +\seealso{ +\code{\link[=chart_template]{chart_template()}} +} diff --git a/man/app_init_addin.Rd b/man/app_init_addin.Rd new file mode 100644 index 00000000..c88f9e5e --- /dev/null +++ b/man/app_init_addin.Rd @@ -0,0 +1,11 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/app_init_addin.R +\name{app_init_addin} +\alias{app_init_addin} +\title{RStudio Add-in for constructing lean ADLB and ADAE data} +\usage{ +app_init_addin() +} +\description{ +RStudio Add-in for constructing lean ADLB and ADAE data +} diff --git a/man/chart_template.Rd b/man/chart_template.Rd new file mode 100644 index 00000000..a77004d6 --- /dev/null +++ b/man/chart_template.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/add_chart.R +\name{chart_template} +\alias{chart_template} +\title{Chart Template Function} +\usage{ +chart_template(name, path, type, ...) +} +\arguments{ +\item{name}{The name of the chart (also name of the yaml file)} + +\item{path}{The path to the R script where the module will be written. +Note that this path will not be set by the user but internally by +\code{add_chart()}.} + +\item{type}{Type of chart: \code{plot}, \code{module}, or \code{htmlwidget}. Default is \code{plot} (static)} + +\item{...}{Arguments to be passed to the template, via \code{add_chart()}} +} +\value{ +Used for side effect +} +\description{ +Chart Template Function +} +\seealso{ +\code{\link[=add_chart]{add_chart()}} +} diff --git a/man/create_new_safetyGraphics_app.Rd b/man/create_new_safetyGraphics_app.Rd new file mode 100644 index 00000000..545c32e6 --- /dev/null +++ b/man/create_new_safetyGraphics_app.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/create_new_safetyGraphics_app.R +\name{create_new_safetyGraphics_app} +\alias{create_new_safetyGraphics_app} +\title{start new project with an instance of safetyGraphicsApp()} +\usage{ +create_new_safetyGraphics_app( + path, + init_default_configs, + open = TRUE, + gui = FALSE +) +} +\arguments{ +\item{path}{location for new safetyGraphicsApp} + +\item{init_default_configs}{copy over \code{safetyCharts} default configs?} + +\item{open}{open new rstudio project?} +} +\value{ +Used for side effect +} +\description{ +start new project with an instance of safetyGraphicsApp() +} From 956079d26205ffd0706f7e8876e31c6637588c27 Mon Sep 17 00:00:00 2001 From: xni7 Date: Fri, 8 Jan 2021 16:27:19 -0500 Subject: [PATCH 3/3] added rprojroot as pkg dependency --- DESCRIPTION | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 9018d3fd..7dcb7056 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -45,6 +45,7 @@ Imports: tidyr, shinybusy, listviewer, - shinyFiles + shinyFiles, + rprojroot VignetteBuilder: knitr Roxygen: list(markdown = TRUE)