Making Modules

Introduction

This vignette will provide an overview of the formods framework for creating reproducable modules that interact with each other. Each module has its own namespace that is mantained by using a module short name as a prefix for functions. For example the figure generation module uses FG. If you want to create a module, please submit an issue at the formods github repository with the following information:

Current modules

The current modules in development:

formods framework

To get started you need to create some template files. The example below assumes you are creating this module for a package called mypackage and that you are running this command in a git repository. Say that this module is used to produce widgets, the short name is MM which stands for My Module:

use_formods(SN          = "MM",
            Module_Name = "My Module",
            element     = "widgets", 
            package     = "mypackage")

Expected functions

The module template will create a standard set of functions for you. The MM below will be replaced with whatever short name you choose above when you create the templates. These functions can be customized for your specific module. Some are optional and can be deleted. For example the MM_fetch_ds function is only needed if your module creates datasets and provides them for other modules to use (like the DW module exports data views to be used by other modules). The modules are designed to create elements. For example the DW module creates data view elements, the FG module is used to create figure elements, etc.

Expected UI components

Module interaction

Say you are using the UD module to feed data into the DW module and the user goes back to the upload form and uploads a different data set. This will need to trigger a reset of the Data Wrangling module as well as tell your larger app that something has changed.

Module state and reacting to changes

Changes in module states are detected with the react_state object. For a given module of type "MM" with a module id of "ID" you would detect changes by reacting to react_state[["ID"]] and looking for changes in the checksum element below:

react_state[["ID"]][["MM"]][["checksum"]]

Helper functions in formods

The examples below require a Shiny session variable and a formods state object. Here we create some examples and other objects needed to demonstrate the functions below.

library(formods)
# This creates the state and session objects
sess_res = UD_test_mksession(session=list())
#> → UD: including file
#> → UD:   source: file.path(system.file(package="onbrand"), "templates", "report.docx")
#> → UD:   dest:   file.path("config","report.docx")
#> → UD: including file
#> → UD:   source: file.path(system.file(package="onbrand"), "templates", "report.pptx")
#> → UD:   dest:   file.path("config","report.pptx")
#> → UD: including file
#> → UD:   source: file.path(system.file(package="onbrand"), "templates", "report.yaml")
#> → UD:   dest:   file.path("config","report.yaml")
#> → UD: State initialized
#> → UD: module checksum updated:897d952fecbc804999396a96f9df4b20
state    = sess_res$state
session  = sess_res$session

# Here we load an example dataset into the df object.
data_file_local =  system.file(package="formods", "test_data", "TEST_DATA.xlsx")
sheet           = "DATA"

df = readxl::read_excel(path=data_file_local, sheet=sheet)

Setting holds on UI elements

The mechanics of the fetch state functions mean that each time a fetch state is called, all of the UI elements in the App are pulled and placed in the app state. This generally works well with some exceptions. The main exception is when you want to have a UI element that changes another UI element. Say for example you have a selection box with a UI id of my_selection. You want that selection to alter a text input with an id of my_text. However if you just poll the ui elements you may update my_text based on changes to my_selection then have those overwritten by the current value of my_text. To prevent this, you need to do two things:

Lastly you need to remove the hold. This is done after the UI has refreshed with the new text value populated in to my_text (with the appropriate reactions set). This is done with an observeEvent that is triggered after everything else (with a priority of -100 below):

remove_hold_listen  <- reactive({
  list(input$my_selection)
})
observeEvent(remove_hold_listen(), {
  # Once the UI has been regenerated we
  # remove any holds for this module
  state = MM_fetch_state(id              = id,
                         input           = input,
                         session         = session,
                         FM_yaml_file    = FM_yaml_file,
                         MOD_yaml_file   = MOD_yaml_file,
                         react_state     = react_state)

  FM_le(state, "removing holds")
  # Removing all holds
  for(hname in names(state[["MM"]][["ui_hold"]])){
    remove_hold(state, session, hname)
  }
}, priority = -100)

The remove_hold_listen object should contain all of the inputs that create holds.

Dataframe formatting information

If you want to tables and pulldown menues based on the types of data in each column you can use the FM_fetch_data_format() function.

hfmt = FM_fetch_data_format(df, state)

# Descriptive headers 
head(as.vector(unlist( hfmt[["col_heads"]])))
#> [1] "<span style='color:#3C8DBC'><b>ID</b><br/><font size='-3'>num</font></span>"      
#> [2] "<span style='color:#3C8DBC'><b>TIME_DY</b><br/><font size='-3'>num</font></span>" 
#> [3] "<span style='color:#3C8DBC'><b>TIME_HR</b><br/><font size='-3'>num</font></span>" 
#> [4] "<span style='color:#3C8DBC'><b>NTIME_DY</b><br/><font size='-3'>num</font></span>"
#> [5] "<span style='color:#3C8DBC'><b>NTIME_HR</b><br/><font size='-3'>num</font></span>"
#> [6] "<span style='color:#3C8DBC'><b>TIME</b><br/><font size='-3'>num</font></span>"

# Subtext
head(as.vector(unlist( hfmt[["col_subtext"]])))
#> [1] "num: 1,⋅⋅⋅,360"  "num: 0,⋅⋅⋅,84"   "num: 0,⋅⋅⋅,2016" "num: 0,⋅⋅⋅,42"  
#> [5] "num: 0,⋅⋅⋅,1008" "num: 0,⋅⋅⋅,2016"

The custom headers can be used with the {rhandsontable} package.

hot = rhandsontable::rhandsontable(
  head(df),
  width      = "100%",
  height     = "100%",
  colHeaders = as.vector(unlist(hfmt[["col_heads"]])),
  rowHeaders = NULL
  )

To add subtext to a selection widget in Shiny you need to use the {shinyWidgets} package.

sel_subtext = as.vector(unlist( hfmt[["col_subtext"]]))
library(shinyWidgets)
shinyWidgets::pickerInput(
    inputId    = "select_example",
    choices    = names(df),
    label      = "Select with subtext",
    choicesOpt = list(subtext = sel_subtext))

To alter the formats shown here you need to edit the formods.yaml configuration file and look at the FM\(\rightarrow\)data_meta section.

Notifications

Notifications are created using the {shinybusy} package and are produced with two different functions: FM_set_notification() and FM_notify(). This is done in a centralized fashion where notifications are added to the state object as user information is processed. This will set a notification called Example Notification. Along with that a timestamp is set:

   state = FM_set_notification(state, "Something happened", "Example Notification")

That timestamp is used to track and prevent the notification from being shown multiple times. Next you need to setup the reactions to display the notifications. Here you can create a reactive expression of the inputs that will lead to a notification:

    toNotify <- reactive({
      list(input$input1,
           input$input2)
    })

Next you use observeEvent() with that reactive expression to trigger notifications. You need to use the fetch state function for that molecule to get the state object with the notifications. Then FM_notify() will be called an any unprocessed notifications will be displayed:

    observeEvent(toNotify(), {
      state = MM_fetch_state(id              = id,
                             input           = input,
                             session         = session,
                             FM_yaml_file    = FM_yaml_file,
                             MOD_yaml_file   = MOD_yaml_file,
                             react_state     = react_state)

      # Triggering optional notifications
      notify_res =
      FM_notify(state    = state,
                session  = session)
    })

Adding tooltips

Tooltips are created interally using the suggested {prompter} package. To add a tool tip to a ui element you would use the FM_add_ui_tooltip() function. For example to add the tool tip, You need to type harder! to a text input you would do the following:

uiele = shiny::textInput(
          inputId = "some_text", 
          label   = "You need to type harder!")
uiele = FM_add_ui_tooltip(state, uiele, 
      tooltip  = "This is a tooltip",
      position = "left")

Pausing the screen

To pause the screen the {shinybusy} package is also used. This is controlled with two functions: FM_pause_screen() is used to pause the screen and/or update the pause message, and FM_resume_screen() is used end the pause and resume interaction with the user.

FM_pause_screen(state, session)
FM_resume_screen(state, session)

formods state objects

When you create a formods state object it can have the following fields:

App information in MM

This field state$MM is relatively free form but there are some reserved elements. These reserved keyword are:

Other than those fields you can store whatever else you need for your module.

Configuration file

YAML configuration files

# https://www.glyphicons.com/sets/basic/
#General formods (FM) configuration across modules
FM:
  include:
    # This is where you can put files to include files in the working directory
    # of the app. For the files listed below you shouldn't change the 'dest'
    # portion but you can change the source to use custom report templates.
    # If relative paths are used they will be relative to
    # the user directory (either the temp formods directory running in shiny
    # or the top level of the zip file structure when saving the app state).
    files:
    - file:
        source: 'file.path(system.file(package="onbrand"), "templates", "report.docx")'
        dest:   'file.path("config","report.docx")'
    - file:
        source: 'file.path(system.file(package="onbrand"), "templates", "report.pptx")'
        dest:   'file.path("config","report.pptx")'
    - file:
        source: 'file.path(system.file(package="onbrand"), "templates", "report.yaml")'
        dest:   'file.path("config","report.yaml")'
  # Some features (e.g. copy to clipboard) don't work when deployed
  deployed: FALSE
  #General code options for the modules
  code:
    theme:           "vibrant_ink"
    showLineNumbers: TRUE
    # File name of the R script to contain generation code
    gen_file: run_analysis.R
    # This is the preamble used in script generation. It goes on the
    # top. Feel free to add to it if you need to. Note that packages should be
    # listed in the packages section at the same level.
    gen_preamble: |-
      # formods automated output ------------------------------------------------
      # https://formods.ubiquity.tools/
      rm(list=ls())
    # Each module should have a packages section that lists the packages
    # needed for code genereated for that module.
    packages: ["onbrand", "writexl"]
  notifications:
    config:
      # You can put any arguments here that would be arguments for
      # config_notify(). See ?shinybusy::config_notify() for more information
      success:
        useFontAwesome: FALSE
        useIcon:        FALSE
        background:     "#5bb85b"
      failure:
        useFontAwesome: FALSE
        useIcon:        FALSE
        background:     "#d9534f"
      info:
        useFontAwesome: FALSE
        useIcon:        FALSE
        background:     "#5bc0de"
      warning:
        useFontAwesome: FALSE
        useIcon:        FALSE
        background:     "#f0ac4d"
  reporting:
    # enabled here controls reporting for the app. Individual modules can be
    # controlled in their respective configuration files
    enabled: TRUE
    # The content_init section is used to initialize reports. You shouldn't
    # change the xlsx rpt but the docx and pptx rpt can be altered to
    # preprocess reports if you need to such as adding default.
    content_init:
      xlsx: |-
           rpt = list(summary = NULL,
                      sheets  = list())
      docx: |-
           rpt  = onbrand::read_template(
             template = file.path("config", "report.docx"),
             mapping  = file.path("config", "report.yaml"))
      pptx: |-
           rpt  = onbrand::read_template(
             template = file.path("config", "report.pptx"),
             mapping  = file.path("config", "report.yaml"))
    # Word template can contain placeholders. This is where you can put the
    # default values for placeholders. There are some nuances associated with
    # creating placeholders and you should see the documentation about them in
    # the onbrand package to better understand that:
    #
    #   https://onbrand.ubiquity.tools/articles/Creating_Templated_Office_Workflows.html#placeholder-text
    #
    # Each element below contains the placeholder used and should contain a
    # location and a value. For example the default document contains a header
    # placeholder in the upper right that looks like:
    #
    #  ===HEADERRIGHT===
    # The placeholder below is HEADERRIGHT, the location is header and by
    # default we will replace it with nothing. Placeholder text can have no
    # spaces. 
    #
    # These defaults can be overwritten in the save section. If your template
    # has no placeholders you can comment out the entire phs section.
    #
    phs:
      - name:      "HEADERLEFT"
        location:  "header"
        value:     ""
        tooltip:   "left header text"
      - name:      "HEADERRIGHT"
        location:  "header"
        value:     ""
        tooltip:   "right header text"
      - name:      "FOOTERLEFT"
        location:  "footer"
        value:     ""
        tooltip:   "left footer text"
    phs_formatting: 
      width:       "100%"
      tt_position: "left"
      tt_size:     "medium"
  ui:
    # See ?actionBttn for styles
    button_style: "fill"
    # Max size for picker inputs
    select_size:  10
    color_green:  "#00BB8A"
    color_red:    "#FF475E"
    color_blue:   "#0088FF"
    color_purple: "#bd2cf4"

  data_meta:
    # This controls the overall format of headers and the select subtext for
    # data frames with the following placeholders surrouned by ===:
    # COLOR  - font color
    # NAME   - colum name
    # LABEL  - type label
    # RANGE  - this depends on the nature of the data in the column:
    #        - If there are between 1 and 3 values then those values are shown.
    #        - If there are more than 3 values then the min and max are show.
    data_header:  "<span style='color:===COLOR==='><b>===NAME===</b><br/><font size='-3'>===LABEL===</font></span>"
    subtext:      "===LABEL===: ===RANGE==="
    # Separator when showing more than three in a column. For exmaple if you
    # had a dataset with 1,2,3,4,5,6 and many_sep was ",...," then it would
    # appear as "1,...,6"
    many_sep: ",⋅⋅⋅,"
    # This controls the differences for different data types. Take the output
    # of typeof(df$colname) and put an entry for that output here.
    data_types:
      character:
        color:    "#DD4B39"
        label:    "text"
      double:
        color:    "#3C8DBC"
        label:    "num"
      integer:
        color:    "#3C8DBC"
        label:    "num"
      other:
        color:    "black"
        label:    "other"
  labels:
    # JMH remove this once the datset stuff has been moved over
    default_ds:   "Original data set"
  user_files:
    use_tmpdir:     TRUE
  logging:
    enabled:        TRUE
    timestamp:      TRUE
    timestamp_fmt: "%Y-%m-%d %H:%M:%S"
    log_file:      "formods_log.txt"
    console:       TRUE