diff --git a/404.html b/404.html index c4a0d0b..333b475 100644 --- a/404.html +++ b/404.html @@ -27,7 +27,7 @@ sasquatch - 0.0.0.9017 + 0.0.0.9028 @@ -48,7 +49,7 @@ - + diff --git a/CONTRIBUTING.html b/CONTRIBUTING.html index d4cbc53..114b0ec 100644 --- a/CONTRIBUTING.html +++ b/CONTRIBUTING.html @@ -7,7 +7,7 @@ sasquatch - 0.0.0.9017 + 0.0.0.9028 @@ -31,31 +32,33 @@
-

This outlines how to propose a change to sasquatch. For a detailed discussion on contributing to this and other tidyverse packages, please see the development contributing guide and our code review principles.

+

This outlines how to propose a change to sasquatch.

Fixing typos

You can fix typos, spelling mistakes, or grammatical errors in the documentation directly using the GitHub web interface, as long as the changes are made in the source file. This generally means you’ll need to edit roxygen2 comments in an .R, not a .Rd file. You can find the .R file that generates the .Rd by reading the comment in the first line.

Bigger changes

-

If you want to make a bigger change, it’s a good idea to first file an issue and make sure someone from the team agrees that it’s needed. If you’ve found a bug, please file an issue that illustrates the bug with a minimal reprex (this will also help you write a unit test, if needed). See our guide on how to create a great issue for more advice.

+

If you want to make a bigger change, it’s a good idea to first file an issue and make sure someone from the team agrees that it’s needed. If you’ve found a bug, please file an issue that illustrates the bug with a minimal reprex (this will also help you write a unit test, if needed). See the tidyverse guide on how to create a great issue for more advice.

Pull request process

-
  • Fork the package and clone onto your computer. If you haven’t done this before, we recommend using usethis::create_from_github("ryanzomorrodi/sasr", fork = TRUE).

  • -
  • Install all development dependencies with devtools::install_dev_deps(), and then make sure the package passes R CMD check by running devtools::check(). If R CMD check doesn’t pass cleanly, it’s a good idea to ask for help before continuing.

  • +
    • Fork the package and clone onto your computer. If you haven’t done this before, we recommend using usethis::create_from_github("ryanzomorrodi/sasquatch", fork = TRUE).

    • +
    • Install all development dependencies with devtools::install_dev_deps().

    • +
    • Install Python (we recommend reticulate::install_python()) or Java if not already installed.

    • +
    • Configure SASPy by using sasquatch::install_saspy(). If you do not have access to a SAS client, we recommend using SAS On Demand for Academics (SAS). Check out vignette('configuration') for more information on how to set up SASPy with ODA.

    • +
    • Make sure the package passes R CMD check by running devtools::check(). If R CMD check doesn’t pass cleanly, it’s a good idea to ask for help before continuing.

    • Create a Git branch for your pull request (PR). We recommend using usethis::pr_init("brief-description-of-change").

    • Make your changes, commit to git, and then create a PR by running usethis::pr_push(), and following the prompts in your browser. The title of your PR should briefly describe the change. The body of your PR should contain Fixes #issue-number.

    • For user-facing changes, add a bullet to the top of NEWS.md (i.e. just below the first header). Follow the style described in https://style.tidyverse.org/news.html.

Code style

-
  • New code should follow the tidyverse style guide. You can use the styler package to apply these styles, but please don’t restyle code that has nothing to do with your PR.

  • -
  • We use roxygen2, with Markdown syntax, for documentation.

  • +
diff --git a/LICENSE-text.html b/LICENSE-text.html index 9504dc9..7e0786a 100644 --- a/LICENSE-text.html +++ b/LICENSE-text.html @@ -7,7 +7,7 @@ sasquatch - 0.0.0.9017 + 0.0.0.9028 diff --git a/LICENSE.html b/LICENSE.html index 79d059a..1c4f46a 100644 --- a/LICENSE.html +++ b/LICENSE.html @@ -7,7 +7,7 @@ sasquatch - 0.0.0.9017 + 0.0.0.9028 @@ -31,7 +32,7 @@
diff --git a/articles/configuration.html b/articles/configuration.html new file mode 100644 index 0000000..aa16bfd --- /dev/null +++ b/articles/configuration.html @@ -0,0 +1,268 @@ + + + + + + + +Configuration • sasquatch + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + +
+

General configuration +

+

sasquatch works by utilizing the SASPy +python package, similar to packages like sasr +or configSAS. This +means everything we do to connect R and SAS, needs to go through +SASPy.

+

Configuration steps for SASPy can vary greatly depending +on the SAS client, but all configuration is specified within the +sascfg_personal.py file inside of the SASPy +package.

+
+

Setting up +

+

Use the following function to create a +sascfg_personal.py templated file.

+
+sasquatch::configure_saspy()
+

This will create a file like the following:

+
SAS_config_names = ['config_name']
+
+config_name = {
+    
+}
+

where config_name is an arbirtary name of a +configuration and the list SAS_config_names contains the +name(s) of your configuration(s).

+
+
+

Access methods +

+

From here, you will need to fill out the config_name +dictionary with your configuration definition. The required definition +fields will depend on the access method required to connect to your SAS +client.

+

The following is a breakdown of the access method by SAS +deployment:

+
    +
  • Stand-alone SAS 9 install +
      +
    • On Linux +
        +
      • Client Linux +
          +
        • STDIO - if on same machine
          +
        • +
        • SSH (STDIO over SSH) if not the same machine. This works from Mac OS +too.
          +
        • +
        +
      • +
      • Client Windows +
          +
        • SSH (STDIO over SSH)!
          +
        • +
        +
      • +
      +
    • +
    • On Windows +
        +
      • Client Linux +
          +
        • Can’t get there from here
          +
        • +
        +
      • +
      • Client Windows +
          +
        • IOM or COM - on same machine. Can’t get there if different +machines
          +
        • +
        +
      • +
      +
    • +
    +
  • +
  • Workspace server (this is SAS 9, and deployment on any platform is +fine) +
      +
    • Client Linux or Mac OS +
        +
      • IOM - local or remote
        +
      • +
      +
    • +
    • Client Windows +
        +
      • IOM or COM - local or remote
        +
      • +
      +
    • +
    • SAS Viya install +
        +
      • On Linux +
          +
        • Client Linux +
            +
          • HTTP - must have compute service configured and running (Viya V3.5 +and V4)
            +
          • +
          • STDIO - over SSH if not the same machine (this was for Viya V3 +before Compute Service existed, not for V4)
            +
          • +
          +
        • +
        • Client Windows +
            +
          • HTTP - must have compute service configured and running (Viya V3.5 +and V4)
            +
          • +
          +
        • +
        +
      • +
      • On Windows +
          +
        • HTTP - must have compute service configured and running (Viya V3.5 +and V4)
        • +
        +
      • +
      +
    • +
    +
  • +
+
+
+

More information +

+

Further documentation and examples for each access type can be found +within the SASPy +configuration documentation

+
+
+
+

SAS On Demand for Academics configuration +

+
+

Registration +

+

SAS On Demand for Academics (ODA) is free SAS client for professors, +students, and independent learners. Create an account at https://welcome.oda.sas.com/.

+

Once you have set up your account, log in and note the ODA server (in +the picture below United States 2) and your username (under the email in +the profile dropdown). We will need these for later.

+

+
+
+

Java installation +

+

ODA relies on the IOM access method, which requires Java. Make sure +Java is installed on your system. You can download Java from their website. Note the +Java installation path.

+
+
+

Configuration +

+

Set up for ODA is super easy. Run config_saspy() and +follow the prompts (you may need to recall your username, server, and +java installation path from earlier).

+
+sasquatch::configure_saspy(template = "oda")
+

config_saspy(template = "oda") will create a +sascfg_personal.py file with all the relevant configuration +information and create an authinfo file, which will store +your ODA credentials. More information about ODA configuration can be +found in the ODA +section of SASPy configuration documentation.

+
+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/articles/files/rstudio_shrtcts.png b/articles/files/rstudio_shrtcts.png new file mode 100644 index 0000000..f4add3f Binary files /dev/null and b/articles/files/rstudio_shrtcts.png differ diff --git a/articles/files/sas_oda.png b/articles/files/sas_oda.png new file mode 100644 index 0000000..7db6ddc Binary files /dev/null and b/articles/files/sas_oda.png differ diff --git a/articles/index.html b/articles/index.html index a2405d2..f0d6d25 100644 --- a/articles/index.html +++ b/articles/index.html @@ -7,7 +7,7 @@ sasquatch - 0.0.0.9017 + 0.0.0.9028 @@ -37,7 +38,9 @@

All vignettes

-
Setting Up
+
Configuration
+
+
Getting Started
diff --git a/articles/sasquatch.html b/articles/sasquatch.html new file mode 100644 index 0000000..1ea896d --- /dev/null +++ b/articles/sasquatch.html @@ -0,0 +1,306 @@ + + + + + + + +Getting Started • sasquatch + + + + + + + + + + + + + + Skip to contents + + +
+ + + + +
+
+ + + + +
+

Session management +

+

Every sasquatch script starts with +sas_connect(). By default, you will be connected to your +first SAS configuration (for more about configurations see +vignette("configuration")).

+
+sas_connect()
+#> SAS Connection established.
+

You can also specify your configuration by name:

+
+sas_connect(cfgname = "my_config")
+#> SAS Connection established.
+

If you ever need to end a connection, you can do so by using:

+
+sas_disconnect()
+#> SAS Connection terminated.
+

All connection information is stored within a +saspy.sasbase.SASsession object. For most individuals, you +will never need to interact with this object, but if you would like to +implement functionality not currently within sasquatch or +would like to access the current session from Python, you can via:

+
+sas_get_session()
+#> Access Method         = IOM
+#> SAS Config name       = my_config
+#> SAS Config file       = /home/user/.virtualenvs/r-saspy/lib/python3.12/site-packages/saspy/sascfg_personal.py
+#> WORK Path             = /saswork/SAS_work0D3600010B4E_odaws01-usw2-2.oda.sas.com/SAS_work920700010B4E_odaws01-usw2-2.oda.sas.com/
+#> SAS Version           = 9.04.01M7P08062020
+#> SASPy Version         = 5.101.1
+#> Teach me SAS          = False
+#> Batch                 = False
+#> Results               = Pandas
+#> SAS Session Encoding  = utf-8
+#> Python Encoding value = utf-8
+#> SAS process Pid value = 68430
+#> SASsession started    = Wed Dec 25 13:58:22 2024
+
+
+

Executing SAS code +

+

You can execute SAS code in a variety of different ways. All +sas_run_*() functions generate htmlwidgets, which display +both the output and log.

+

To execute a string of SAS code use sas_run_string()

+
+sas_run_string("PROC MEANS DATA = sashelp.cars;RUN;")
+
+

To execute a SAS script use sas_run_file()

+
+cat("PROC MEANS DATA = sashelp.cars;RUN;", file = "script.sas")
+sas_widget <- sas_run_file("script.sas")
+
+

Within quarto +

+

Quarto documents are a great way to use SAS and R together because +they couple R and SAS within a single reproducable document.

+

You can create a sasquatch quarto document by:

+
    +
  • Specifying the format as html (currenly, only html is well +supported)
  • +
  • Specifying the engine as knitr
  • +
  • Creating an R code block with +library(sasquatch); sas_connect() +
  • +
+

Now, SAS code can be contained within “sas” code blocks.

+
---
+format: html
+engine: knitr
+---
+
+```{r}
+library(sasquatch)
+sas_connect()
+```
+
+```{sas}
+PROC MEANS DATA = sashelp.cars;
+RUN;
+```
+
+

RStudio +

+

In RStudio, you will be able to run SAS chunks as you would any other +chunk.

+

+

If you want to be able to view SAS output within the Viewer instead +of beneath the chunk, you can utilize the +sas_run_selected() addin. To add a keyboard shortcut for +this addin, open Tools -> Modify Keyboard Shortcuts and search “Run +selected in SAS”, type in the box under Shortcut to set the keyboard +shortcut to your liking and click Apply.

+

+
+
+

Positron +

+

In Positron, you will not be able to run SAS chunks as you would R or +Python chunks. However, just as in RStudio, you can create a keyboard +shortcut which will allow you to view SAS output within the Plots pane. +Open up the command palette with ctrl+shift+p or +command+shift+p and search “Preferences: Open Keyboard +Shortcuts (JSON)”. Add the following to your shortcuts.

+
{
+    "key": "ctrl+shift+enter",
+    "command": "workbench.action.executeCode.console",
+    "when": "editorTextFocus",
+    "args": {
+        "langId": "r",
+        "code": "sasquatch::sas_run_selected()",
+        "focus": true
+    }
+}
+

Edit the key argument to set your preferred +shortcut.

+
+
+
+
+

Data conversion +

+

R data.frames can be automatically converted to and from +SAS tables. However, data.frames must only contain logical, +integer, double, factor, character, POSIXct, or Date class columns.

+

Convert R data.frames to SAS tables with +r_to_sas().

+
+df <- data.frame(
+  double = c(1, 2.5, NA),
+  integer = c(1:2, NA),
+  logical = c(T, F, NA),
+  character = c("a", "b", NA),
+  factor = factor(c("a", "b", NA)),
+  date = as.Date("2015-12-09") + c(1:2, NA),
+  datetime = as.POSIXct("2015-12-09 10:51:34.5678", tz = "UTC") + c(1:2, NA)
+)
+tibble::tibble(df)
+#> # A tibble: 3 × 7
+#>   double integer logical character factor date       datetime           
+#>    <dbl>   <int> <lgl>   <chr>     <fct>  <date>     <dttm>             
+#> 1    1         1 TRUE    a         a      2015-12-10 2015-12-09 10:51:35
+#> 2    2.5       2 FALSE   b         b      2015-12-11 2015-12-09 10:51:36
+#> 3   NA        NA NA      NA        NA     NA         NA                 
+
+r_to_sas(df, "df", libref = "WORK")
+

SAS only has two data types (numeric and character). R data types are +converted as follows:

+
    +
  • logical -> numeric
  • +
  • integer -> numeric
  • +
  • double -> numeric
  • +
  • factor -> character
  • +
  • character -> character
  • +
  • POSIXct -> numeric (datetime)
  • +
  • Date -> numeric (date)
  • +
+

And back to SAS tables with sas_to_r().

+
+df <- sas_to_r("df", libref = "WORK")
+
+tibble::tibble(df)
+#> # A tibble: 3 × 7
+#>   double integer logical character factor date                datetime           
+#>    <dbl>   <dbl>   <dbl> <chr>     <chr>  <dttm>              <dttm>             
+#> 1    1         1       1 a         a      2015-12-09 18:00:00 2015-12-09 04:51:35
+#> 2    2.5       2       0 b         b      2015-12-10 18:00:00 2015-12-09 04:51:36
+#> 3   NA        NA      NA NA        NA     NA                  NA                 
+

SAS data types are converted as follows:

+
    +
  • numeric -> double
  • +
  • character -> character
  • +
  • numeric (datetime) -> POSIXct
  • +
  • numeric (date) -> POSIXct
  • +
+

In the conversion process dates and datetimes are converted to local +time. If utilizing another timezone, use as.POSIXct() or +lubridate::with_tz() to convert back to the desired time +zone.

+
+
+

File management +

+

sasquatch offers a few different functions to manage +remote SAS files.

+

Upload files to a remote SAS server with +sas_file_upload().

+
+cat("PROC MEANS DATA = sashelp.cars;RUN;", file = "script.sas")
+sas_file_upload(local_path = "script.sas", sas_path = "~/script.sas")
+

Download files from a remote SAS server with +sas_file_download().

+
+sas_file_download(sas_path = "~/script.sas", local_path = "script.sas")
+

Copy files on the remote SAS server with +sas_file_copy().

+
+sas_file_copy("~/script.sas", "~/script_copy.sas")
+

Remove files from a remote SAS server with +sas_file_remove().

+
+sas_file_remove("~/script_copy.sas")
+

List all files and directories within a remote SAS server with +sas_list().

+
+sas_list("~")
+#> [1] "directory1" "file1.csv" "file2.sas"
+
+
+
+ + + +
+ + + +
+
+ + + + + + + diff --git a/articles/sasquatch_files/htmltools-fill-0.5.8.1/fill.css b/articles/sasquatch_files/htmltools-fill-0.5.8.1/fill.css new file mode 100644 index 0000000..841ea9d --- /dev/null +++ b/articles/sasquatch_files/htmltools-fill-0.5.8.1/fill.css @@ -0,0 +1,21 @@ +@layer htmltools { + .html-fill-container { + display: flex; + flex-direction: column; + /* Prevent the container from expanding vertically or horizontally beyond its + parent's constraints. */ + min-height: 0; + min-width: 0; + } + .html-fill-container > .html-fill-item { + /* Fill items can grow and shrink freely within + available vertical space in fillable container */ + flex: 1 1 auto; + min-height: 0; + min-width: 0; + } + .html-fill-container > :not(.html-fill-item) { + /* Prevent shrinking or growing of non-fill items */ + flex: 0 0 auto; + } +} diff --git a/articles/sasquatch_files/htmlwidgets-1.6.4/htmlwidgets.js b/articles/sasquatch_files/htmlwidgets-1.6.4/htmlwidgets.js new file mode 100644 index 0000000..1067d02 --- /dev/null +++ b/articles/sasquatch_files/htmlwidgets-1.6.4/htmlwidgets.js @@ -0,0 +1,901 @@ +(function() { + // If window.HTMLWidgets is already defined, then use it; otherwise create a + // new object. This allows preceding code to set options that affect the + // initialization process (though none currently exist). + window.HTMLWidgets = window.HTMLWidgets || {}; + + // See if we're running in a viewer pane. If not, we're in a web browser. + var viewerMode = window.HTMLWidgets.viewerMode = + /\bviewer_pane=1\b/.test(window.location); + + // See if we're running in Shiny mode. If not, it's a static document. + // Note that static widgets can appear in both Shiny and static modes, but + // obviously, Shiny widgets can only appear in Shiny apps/documents. + var shinyMode = window.HTMLWidgets.shinyMode = + typeof(window.Shiny) !== "undefined" && !!window.Shiny.outputBindings; + + // We can't count on jQuery being available, so we implement our own + // version if necessary. + function querySelectorAll(scope, selector) { + if (typeof(jQuery) !== "undefined" && scope instanceof jQuery) { + return scope.find(selector); + } + if (scope.querySelectorAll) { + return scope.querySelectorAll(selector); + } + } + + function asArray(value) { + if (value === null) + return []; + if ($.isArray(value)) + return value; + return [value]; + } + + // Implement jQuery's extend + function extend(target /*, ... */) { + if (arguments.length == 1) { + return target; + } + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + for (var prop in source) { + if (source.hasOwnProperty(prop)) { + target[prop] = source[prop]; + } + } + } + return target; + } + + // IE8 doesn't support Array.forEach. + function forEach(values, callback, thisArg) { + if (values.forEach) { + values.forEach(callback, thisArg); + } else { + for (var i = 0; i < values.length; i++) { + callback.call(thisArg, values[i], i, values); + } + } + } + + // Replaces the specified method with the return value of funcSource. + // + // Note that funcSource should not BE the new method, it should be a function + // that RETURNS the new method. funcSource receives a single argument that is + // the overridden method, it can be called from the new method. The overridden + // method can be called like a regular function, it has the target permanently + // bound to it so "this" will work correctly. + function overrideMethod(target, methodName, funcSource) { + var superFunc = target[methodName] || function() {}; + var superFuncBound = function() { + return superFunc.apply(target, arguments); + }; + target[methodName] = funcSource(superFuncBound); + } + + // Add a method to delegator that, when invoked, calls + // delegatee.methodName. If there is no such method on + // the delegatee, but there was one on delegator before + // delegateMethod was called, then the original version + // is invoked instead. + // For example: + // + // var a = { + // method1: function() { console.log('a1'); } + // method2: function() { console.log('a2'); } + // }; + // var b = { + // method1: function() { console.log('b1'); } + // }; + // delegateMethod(a, b, "method1"); + // delegateMethod(a, b, "method2"); + // a.method1(); + // a.method2(); + // + // The output would be "b1", "a2". + function delegateMethod(delegator, delegatee, methodName) { + var inherited = delegator[methodName]; + delegator[methodName] = function() { + var target = delegatee; + var method = delegatee[methodName]; + + // The method doesn't exist on the delegatee. Instead, + // call the method on the delegator, if it exists. + if (!method) { + target = delegator; + method = inherited; + } + + if (method) { + return method.apply(target, arguments); + } + }; + } + + // Implement a vague facsimilie of jQuery's data method + function elementData(el, name, value) { + if (arguments.length == 2) { + return el["htmlwidget_data_" + name]; + } else if (arguments.length == 3) { + el["htmlwidget_data_" + name] = value; + return el; + } else { + throw new Error("Wrong number of arguments for elementData: " + + arguments.length); + } + } + + // http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex + function escapeRegExp(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + } + + function hasClass(el, className) { + var re = new RegExp("\\b" + escapeRegExp(className) + "\\b"); + return re.test(el.className); + } + + // elements - array (or array-like object) of HTML elements + // className - class name to test for + // include - if true, only return elements with given className; + // if false, only return elements *without* given className + function filterByClass(elements, className, include) { + var results = []; + for (var i = 0; i < elements.length; i++) { + if (hasClass(elements[i], className) == include) + results.push(elements[i]); + } + return results; + } + + function on(obj, eventName, func) { + if (obj.addEventListener) { + obj.addEventListener(eventName, func, false); + } else if (obj.attachEvent) { + obj.attachEvent(eventName, func); + } + } + + function off(obj, eventName, func) { + if (obj.removeEventListener) + obj.removeEventListener(eventName, func, false); + else if (obj.detachEvent) { + obj.detachEvent(eventName, func); + } + } + + // Translate array of values to top/right/bottom/left, as usual with + // the "padding" CSS property + // https://developer.mozilla.org/en-US/docs/Web/CSS/padding + function unpackPadding(value) { + if (typeof(value) === "number") + value = [value]; + if (value.length === 1) { + return {top: value[0], right: value[0], bottom: value[0], left: value[0]}; + } + if (value.length === 2) { + return {top: value[0], right: value[1], bottom: value[0], left: value[1]}; + } + if (value.length === 3) { + return {top: value[0], right: value[1], bottom: value[2], left: value[1]}; + } + if (value.length === 4) { + return {top: value[0], right: value[1], bottom: value[2], left: value[3]}; + } + } + + // Convert an unpacked padding object to a CSS value + function paddingToCss(paddingObj) { + return paddingObj.top + "px " + paddingObj.right + "px " + paddingObj.bottom + "px " + paddingObj.left + "px"; + } + + // Makes a number suitable for CSS + function px(x) { + if (typeof(x) === "number") + return x + "px"; + else + return x; + } + + // Retrieves runtime widget sizing information for an element. + // The return value is either null, or an object with fill, padding, + // defaultWidth, defaultHeight fields. + function sizingPolicy(el) { + var sizingEl = document.querySelector("script[data-for='" + el.id + "'][type='application/htmlwidget-sizing']"); + if (!sizingEl) + return null; + var sp = JSON.parse(sizingEl.textContent || sizingEl.text || "{}"); + if (viewerMode) { + return sp.viewer; + } else { + return sp.browser; + } + } + + // @param tasks Array of strings (or falsy value, in which case no-op). + // Each element must be a valid JavaScript expression that yields a + // function. Or, can be an array of objects with "code" and "data" + // properties; in this case, the "code" property should be a string + // of JS that's an expr that yields a function, and "data" should be + // an object that will be added as an additional argument when that + // function is called. + // @param target The object that will be "this" for each function + // execution. + // @param args Array of arguments to be passed to the functions. (The + // same arguments will be passed to all functions.) + function evalAndRun(tasks, target, args) { + if (tasks) { + forEach(tasks, function(task) { + var theseArgs = args; + if (typeof(task) === "object") { + theseArgs = theseArgs.concat([task.data]); + task = task.code; + } + var taskFunc = tryEval(task); + if (typeof(taskFunc) !== "function") { + throw new Error("Task must be a function! Source:\n" + task); + } + taskFunc.apply(target, theseArgs); + }); + } + } + + // Attempt eval() both with and without enclosing in parentheses. + // Note that enclosing coerces a function declaration into + // an expression that eval() can parse + // (otherwise, a SyntaxError is thrown) + function tryEval(code) { + var result = null; + try { + result = eval("(" + code + ")"); + } catch(error) { + if (!(error instanceof SyntaxError)) { + throw error; + } + try { + result = eval(code); + } catch(e) { + if (e instanceof SyntaxError) { + throw error; + } else { + throw e; + } + } + } + return result; + } + + function initSizing(el) { + var sizing = sizingPolicy(el); + if (!sizing) + return; + + var cel = document.getElementById("htmlwidget_container"); + if (!cel) + return; + + if (typeof(sizing.padding) !== "undefined") { + document.body.style.margin = "0"; + document.body.style.padding = paddingToCss(unpackPadding(sizing.padding)); + } + + if (sizing.fill) { + document.body.style.overflow = "hidden"; + document.body.style.width = "100%"; + document.body.style.height = "100%"; + document.documentElement.style.width = "100%"; + document.documentElement.style.height = "100%"; + cel.style.position = "absolute"; + var pad = unpackPadding(sizing.padding); + cel.style.top = pad.top + "px"; + cel.style.right = pad.right + "px"; + cel.style.bottom = pad.bottom + "px"; + cel.style.left = pad.left + "px"; + el.style.width = "100%"; + el.style.height = "100%"; + + return { + getWidth: function() { return cel.getBoundingClientRect().width; }, + getHeight: function() { return cel.getBoundingClientRect().height; } + }; + + } else { + el.style.width = px(sizing.width); + el.style.height = px(sizing.height); + + return { + getWidth: function() { return cel.getBoundingClientRect().width; }, + getHeight: function() { return cel.getBoundingClientRect().height; } + }; + } + } + + // Default implementations for methods + var defaults = { + find: function(scope) { + return querySelectorAll(scope, "." + this.name); + }, + renderError: function(el, err) { + var $el = $(el); + + this.clearError(el); + + // Add all these error classes, as Shiny does + var errClass = "shiny-output-error"; + if (err.type !== null) { + // use the classes of the error condition as CSS class names + errClass = errClass + " " + $.map(asArray(err.type), function(type) { + return errClass + "-" + type; + }).join(" "); + } + errClass = errClass + " htmlwidgets-error"; + + // Is el inline or block? If inline or inline-block, just display:none it + // and add an inline error. + var display = $el.css("display"); + $el.data("restore-display-mode", display); + + if (display === "inline" || display === "inline-block") { + $el.hide(); + if (err.message !== "") { + var errorSpan = $("").addClass(errClass); + errorSpan.text(err.message); + $el.after(errorSpan); + } + } else if (display === "block") { + // If block, add an error just after the el, set visibility:none on the + // el, and position the error to be on top of the el. + // Mark it with a unique ID and CSS class so we can remove it later. + $el.css("visibility", "hidden"); + if (err.message !== "") { + var errorDiv = $("
").addClass(errClass).css("position", "absolute") + .css("top", el.offsetTop) + .css("left", el.offsetLeft) + // setting width can push out the page size, forcing otherwise + // unnecessary scrollbars to appear and making it impossible for + // the element to shrink; so use max-width instead + .css("maxWidth", el.offsetWidth) + .css("height", el.offsetHeight); + errorDiv.text(err.message); + $el.after(errorDiv); + + // Really dumb way to keep the size/position of the error in sync with + // the parent element as the window is resized or whatever. + var intId = setInterval(function() { + if (!errorDiv[0].parentElement) { + clearInterval(intId); + return; + } + errorDiv + .css("top", el.offsetTop) + .css("left", el.offsetLeft) + .css("maxWidth", el.offsetWidth) + .css("height", el.offsetHeight); + }, 500); + } + } + }, + clearError: function(el) { + var $el = $(el); + var display = $el.data("restore-display-mode"); + $el.data("restore-display-mode", null); + + if (display === "inline" || display === "inline-block") { + if (display) + $el.css("display", display); + $(el.nextSibling).filter(".htmlwidgets-error").remove(); + } else if (display === "block"){ + $el.css("visibility", "inherit"); + $(el.nextSibling).filter(".htmlwidgets-error").remove(); + } + }, + sizing: {} + }; + + // Called by widget bindings to register a new type of widget. The definition + // object can contain the following properties: + // - name (required) - A string indicating the binding name, which will be + // used by default as the CSS classname to look for. + // - initialize (optional) - A function(el) that will be called once per + // widget element; if a value is returned, it will be passed as the third + // value to renderValue. + // - renderValue (required) - A function(el, data, initValue) that will be + // called with data. Static contexts will cause this to be called once per + // element; Shiny apps will cause this to be called multiple times per + // element, as the data changes. + window.HTMLWidgets.widget = function(definition) { + if (!definition.name) { + throw new Error("Widget must have a name"); + } + if (!definition.type) { + throw new Error("Widget must have a type"); + } + // Currently we only support output widgets + if (definition.type !== "output") { + throw new Error("Unrecognized widget type '" + definition.type + "'"); + } + // TODO: Verify that .name is a valid CSS classname + + // Support new-style instance-bound definitions. Old-style class-bound + // definitions have one widget "object" per widget per type/class of + // widget; the renderValue and resize methods on such widget objects + // take el and instance arguments, because the widget object can't + // store them. New-style instance-bound definitions have one widget + // object per widget instance; the definition that's passed in doesn't + // provide renderValue or resize methods at all, just the single method + // factory(el, width, height) + // which returns an object that has renderValue(x) and resize(w, h). + // This enables a far more natural programming style for the widget + // author, who can store per-instance state using either OO-style + // instance fields or functional-style closure variables (I guess this + // is in contrast to what can only be called C-style pseudo-OO which is + // what we required before). + if (definition.factory) { + definition = createLegacyDefinitionAdapter(definition); + } + + if (!definition.renderValue) { + throw new Error("Widget must have a renderValue function"); + } + + // For static rendering (non-Shiny), use a simple widget registration + // scheme. We also use this scheme for Shiny apps/documents that also + // contain static widgets. + window.HTMLWidgets.widgets = window.HTMLWidgets.widgets || []; + // Merge defaults into the definition; don't mutate the original definition. + var staticBinding = extend({}, defaults, definition); + overrideMethod(staticBinding, "find", function(superfunc) { + return function(scope) { + var results = superfunc(scope); + // Filter out Shiny outputs, we only want the static kind + return filterByClass(results, "html-widget-output", false); + }; + }); + window.HTMLWidgets.widgets.push(staticBinding); + + if (shinyMode) { + // Shiny is running. Register the definition with an output binding. + // The definition itself will not be the output binding, instead + // we will make an output binding object that delegates to the + // definition. This is because we foolishly used the same method + // name (renderValue) for htmlwidgets definition and Shiny bindings + // but they actually have quite different semantics (the Shiny + // bindings receive data that includes lots of metadata that it + // strips off before calling htmlwidgets renderValue). We can't + // just ignore the difference because in some widgets it's helpful + // to call this.renderValue() from inside of resize(), and if + // we're not delegating, then that call will go to the Shiny + // version instead of the htmlwidgets version. + + // Merge defaults with definition, without mutating either. + var bindingDef = extend({}, defaults, definition); + + // This object will be our actual Shiny binding. + var shinyBinding = new Shiny.OutputBinding(); + + // With a few exceptions, we'll want to simply use the bindingDef's + // version of methods if they are available, otherwise fall back to + // Shiny's defaults. NOTE: If Shiny's output bindings gain additional + // methods in the future, and we want them to be overrideable by + // HTMLWidget binding definitions, then we'll need to add them to this + // list. + delegateMethod(shinyBinding, bindingDef, "getId"); + delegateMethod(shinyBinding, bindingDef, "onValueChange"); + delegateMethod(shinyBinding, bindingDef, "onValueError"); + delegateMethod(shinyBinding, bindingDef, "renderError"); + delegateMethod(shinyBinding, bindingDef, "clearError"); + delegateMethod(shinyBinding, bindingDef, "showProgress"); + + // The find, renderValue, and resize are handled differently, because we + // want to actually decorate the behavior of the bindingDef methods. + + shinyBinding.find = function(scope) { + var results = bindingDef.find(scope); + + // Only return elements that are Shiny outputs, not static ones + var dynamicResults = results.filter(".html-widget-output"); + + // It's possible that whatever caused Shiny to think there might be + // new dynamic outputs, also caused there to be new static outputs. + // Since there might be lots of different htmlwidgets bindings, we + // schedule execution for later--no need to staticRender multiple + // times. + if (results.length !== dynamicResults.length) + scheduleStaticRender(); + + return dynamicResults; + }; + + // Wrap renderValue to handle initialization, which unfortunately isn't + // supported natively by Shiny at the time of this writing. + + shinyBinding.renderValue = function(el, data) { + Shiny.renderDependencies(data.deps); + // Resolve strings marked as javascript literals to objects + if (!(data.evals instanceof Array)) data.evals = [data.evals]; + for (var i = 0; data.evals && i < data.evals.length; i++) { + window.HTMLWidgets.evaluateStringMember(data.x, data.evals[i]); + } + if (!bindingDef.renderOnNullValue) { + if (data.x === null) { + el.style.visibility = "hidden"; + return; + } else { + el.style.visibility = "inherit"; + } + } + if (!elementData(el, "initialized")) { + initSizing(el); + + elementData(el, "initialized", true); + if (bindingDef.initialize) { + var rect = el.getBoundingClientRect(); + var result = bindingDef.initialize(el, rect.width, rect.height); + elementData(el, "init_result", result); + } + } + bindingDef.renderValue(el, data.x, elementData(el, "init_result")); + evalAndRun(data.jsHooks.render, elementData(el, "init_result"), [el, data.x]); + }; + + // Only override resize if bindingDef implements it + if (bindingDef.resize) { + shinyBinding.resize = function(el, width, height) { + // Shiny can call resize before initialize/renderValue have been + // called, which doesn't make sense for widgets. + if (elementData(el, "initialized")) { + bindingDef.resize(el, width, height, elementData(el, "init_result")); + } + }; + } + + Shiny.outputBindings.register(shinyBinding, bindingDef.name); + } + }; + + var scheduleStaticRenderTimerId = null; + function scheduleStaticRender() { + if (!scheduleStaticRenderTimerId) { + scheduleStaticRenderTimerId = setTimeout(function() { + scheduleStaticRenderTimerId = null; + window.HTMLWidgets.staticRender(); + }, 1); + } + } + + // Render static widgets after the document finishes loading + // Statically render all elements that are of this widget's class + window.HTMLWidgets.staticRender = function() { + var bindings = window.HTMLWidgets.widgets || []; + forEach(bindings, function(binding) { + var matches = binding.find(document.documentElement); + forEach(matches, function(el) { + var sizeObj = initSizing(el, binding); + + var getSize = function(el) { + if (sizeObj) { + return {w: sizeObj.getWidth(), h: sizeObj.getHeight()} + } else { + var rect = el.getBoundingClientRect(); + return {w: rect.width, h: rect.height} + } + }; + + if (hasClass(el, "html-widget-static-bound")) + return; + el.className = el.className + " html-widget-static-bound"; + + var initResult; + if (binding.initialize) { + var size = getSize(el); + initResult = binding.initialize(el, size.w, size.h); + elementData(el, "init_result", initResult); + } + + if (binding.resize) { + var lastSize = getSize(el); + var resizeHandler = function(e) { + var size = getSize(el); + if (size.w === 0 && size.h === 0) + return; + if (size.w === lastSize.w && size.h === lastSize.h) + return; + lastSize = size; + binding.resize(el, size.w, size.h, initResult); + }; + + on(window, "resize", resizeHandler); + + // This is needed for cases where we're running in a Shiny + // app, but the widget itself is not a Shiny output, but + // rather a simple static widget. One example of this is + // an rmarkdown document that has runtime:shiny and widget + // that isn't in a render function. Shiny only knows to + // call resize handlers for Shiny outputs, not for static + // widgets, so we do it ourselves. + if (window.jQuery) { + window.jQuery(document).on( + "shown.htmlwidgets shown.bs.tab.htmlwidgets shown.bs.collapse.htmlwidgets", + resizeHandler + ); + window.jQuery(document).on( + "hidden.htmlwidgets hidden.bs.tab.htmlwidgets hidden.bs.collapse.htmlwidgets", + resizeHandler + ); + } + + // This is needed for the specific case of ioslides, which + // flips slides between display:none and display:block. + // Ideally we would not have to have ioslide-specific code + // here, but rather have ioslides raise a generic event, + // but the rmarkdown package just went to CRAN so the + // window to getting that fixed may be long. + if (window.addEventListener) { + // It's OK to limit this to window.addEventListener + // browsers because ioslides itself only supports + // such browsers. + on(document, "slideenter", resizeHandler); + on(document, "slideleave", resizeHandler); + } + } + + var scriptData = document.querySelector("script[data-for='" + el.id + "'][type='application/json']"); + if (scriptData) { + var data = JSON.parse(scriptData.textContent || scriptData.text); + // Resolve strings marked as javascript literals to objects + if (!(data.evals instanceof Array)) data.evals = [data.evals]; + for (var k = 0; data.evals && k < data.evals.length; k++) { + window.HTMLWidgets.evaluateStringMember(data.x, data.evals[k]); + } + binding.renderValue(el, data.x, initResult); + evalAndRun(data.jsHooks.render, initResult, [el, data.x]); + } + }); + }); + + invokePostRenderHandlers(); + } + + + function has_jQuery3() { + if (!window.jQuery) { + return false; + } + var $version = window.jQuery.fn.jquery; + var $major_version = parseInt($version.split(".")[0]); + return $major_version >= 3; + } + + /* + / Shiny 1.4 bumped jQuery from 1.x to 3.x which means jQuery's + / on-ready handler (i.e., $(fn)) is now asyncronous (i.e., it now + / really means $(setTimeout(fn)). + / https://jquery.com/upgrade-guide/3.0/#breaking-change-document-ready-handlers-are-now-asynchronous + / + / Since Shiny uses $() to schedule initShiny, shiny>=1.4 calls initShiny + / one tick later than it did before, which means staticRender() is + / called renderValue() earlier than (advanced) widget authors might be expecting. + / https://github.com/rstudio/shiny/issues/2630 + / + / For a concrete example, leaflet has some methods (e.g., updateBounds) + / which reference Shiny methods registered in initShiny (e.g., setInputValue). + / Since leaflet is privy to this life-cycle, it knows to use setTimeout() to + / delay execution of those methods (until Shiny methods are ready) + / https://github.com/rstudio/leaflet/blob/18ec981/javascript/src/index.js#L266-L268 + / + / Ideally widget authors wouldn't need to use this setTimeout() hack that + / leaflet uses to call Shiny methods on a staticRender(). In the long run, + / the logic initShiny should be broken up so that method registration happens + / right away, but binding happens later. + */ + function maybeStaticRenderLater() { + if (shinyMode && has_jQuery3()) { + window.jQuery(window.HTMLWidgets.staticRender); + } else { + window.HTMLWidgets.staticRender(); + } + } + + if (document.addEventListener) { + document.addEventListener("DOMContentLoaded", function() { + document.removeEventListener("DOMContentLoaded", arguments.callee, false); + maybeStaticRenderLater(); + }, false); + } else if (document.attachEvent) { + document.attachEvent("onreadystatechange", function() { + if (document.readyState === "complete") { + document.detachEvent("onreadystatechange", arguments.callee); + maybeStaticRenderLater(); + } + }); + } + + + window.HTMLWidgets.getAttachmentUrl = function(depname, key) { + // If no key, default to the first item + if (typeof(key) === "undefined") + key = 1; + + var link = document.getElementById(depname + "-" + key + "-attachment"); + if (!link) { + throw new Error("Attachment " + depname + "/" + key + " not found in document"); + } + return link.getAttribute("href"); + }; + + window.HTMLWidgets.dataframeToD3 = function(df) { + var names = []; + var length; + for (var name in df) { + if (df.hasOwnProperty(name)) + names.push(name); + if (typeof(df[name]) !== "object" || typeof(df[name].length) === "undefined") { + throw new Error("All fields must be arrays"); + } else if (typeof(length) !== "undefined" && length !== df[name].length) { + throw new Error("All fields must be arrays of the same length"); + } + length = df[name].length; + } + var results = []; + var item; + for (var row = 0; row < length; row++) { + item = {}; + for (var col = 0; col < names.length; col++) { + item[names[col]] = df[names[col]][row]; + } + results.push(item); + } + return results; + }; + + window.HTMLWidgets.transposeArray2D = function(array) { + if (array.length === 0) return array; + var newArray = array[0].map(function(col, i) { + return array.map(function(row) { + return row[i] + }) + }); + return newArray; + }; + // Split value at splitChar, but allow splitChar to be escaped + // using escapeChar. Any other characters escaped by escapeChar + // will be included as usual (including escapeChar itself). + function splitWithEscape(value, splitChar, escapeChar) { + var results = []; + var escapeMode = false; + var currentResult = ""; + for (var pos = 0; pos < value.length; pos++) { + if (!escapeMode) { + if (value[pos] === splitChar) { + results.push(currentResult); + currentResult = ""; + } else if (value[pos] === escapeChar) { + escapeMode = true; + } else { + currentResult += value[pos]; + } + } else { + currentResult += value[pos]; + escapeMode = false; + } + } + if (currentResult !== "") { + results.push(currentResult); + } + return results; + } + // Function authored by Yihui/JJ Allaire + window.HTMLWidgets.evaluateStringMember = function(o, member) { + var parts = splitWithEscape(member, '.', '\\'); + for (var i = 0, l = parts.length; i < l; i++) { + var part = parts[i]; + // part may be a character or 'numeric' member name + if (o !== null && typeof o === "object" && part in o) { + if (i == (l - 1)) { // if we are at the end of the line then evalulate + if (typeof o[part] === "string") + o[part] = tryEval(o[part]); + } else { // otherwise continue to next embedded object + o = o[part]; + } + } + } + }; + + // Retrieve the HTMLWidget instance (i.e. the return value of an + // HTMLWidget binding's initialize() or factory() function) + // associated with an element, or null if none. + window.HTMLWidgets.getInstance = function(el) { + return elementData(el, "init_result"); + }; + + // Finds the first element in the scope that matches the selector, + // and returns the HTMLWidget instance (i.e. the return value of + // an HTMLWidget binding's initialize() or factory() function) + // associated with that element, if any. If no element matches the + // selector, or the first matching element has no HTMLWidget + // instance associated with it, then null is returned. + // + // The scope argument is optional, and defaults to window.document. + window.HTMLWidgets.find = function(scope, selector) { + if (arguments.length == 1) { + selector = scope; + scope = document; + } + + var el = scope.querySelector(selector); + if (el === null) { + return null; + } else { + return window.HTMLWidgets.getInstance(el); + } + }; + + // Finds all elements in the scope that match the selector, and + // returns the HTMLWidget instances (i.e. the return values of + // an HTMLWidget binding's initialize() or factory() function) + // associated with the elements, in an array. If elements that + // match the selector don't have an associated HTMLWidget + // instance, the returned array will contain nulls. + // + // The scope argument is optional, and defaults to window.document. + window.HTMLWidgets.findAll = function(scope, selector) { + if (arguments.length == 1) { + selector = scope; + scope = document; + } + + var nodes = scope.querySelectorAll(selector); + var results = []; + for (var i = 0; i < nodes.length; i++) { + results.push(window.HTMLWidgets.getInstance(nodes[i])); + } + return results; + }; + + var postRenderHandlers = []; + function invokePostRenderHandlers() { + while (postRenderHandlers.length) { + var handler = postRenderHandlers.shift(); + if (handler) { + handler(); + } + } + } + + // Register the given callback function to be invoked after the + // next time static widgets are rendered. + window.HTMLWidgets.addPostRenderHandler = function(callback) { + postRenderHandlers.push(callback); + }; + + // Takes a new-style instance-bound definition, and returns an + // old-style class-bound definition. This saves us from having + // to rewrite all the logic in this file to accomodate both + // types of definitions. + function createLegacyDefinitionAdapter(defn) { + var result = { + name: defn.name, + type: defn.type, + initialize: function(el, width, height) { + return defn.factory(el, width, height); + }, + renderValue: function(el, x, instance) { + return instance.renderValue(x); + }, + resize: function(el, width, height, instance) { + return instance.resize(width, height); + } + }; + + if (defn.find) + result.find = defn.find; + if (defn.renderError) + result.renderError = defn.renderError; + if (defn.clearError) + result.clearError = defn.clearError; + + return result; + } +})(); diff --git a/articles/sasquatch_files/sas_widget-binding-0.0.0.9028/sas_widget.js b/articles/sasquatch_files/sas_widget-binding-0.0.0.9028/sas_widget.js new file mode 100644 index 0000000..40784a4 --- /dev/null +++ b/articles/sasquatch_files/sas_widget-binding-0.0.0.9028/sas_widget.js @@ -0,0 +1,43 @@ +HTMLWidgets.widget({ + + name: 'sas_widget', + + type: 'output', + + factory: function(el, width, height) { + + // TODO: define shared variables for this instance + + return { + + renderValue: function(x) { + let lst = x.lst; + let log = x.log; + + + el.innerHTML = ` + + +
+
+ +
+
${log}
+
`; + + }, + + resize: function(width, height) { + + } + + }; + } +}); \ No newline at end of file diff --git a/authors.html b/authors.html index 13536bb..de8f24e 100644 --- a/authors.html +++ b/authors.html @@ -7,7 +7,7 @@ sasquatch - 0.0.0.9017 + 0.0.0.9028 @@ -44,17 +45,17 @@

Authors

Citation

-

Source: DESCRIPTION

+

Source: DESCRIPTION

Zomorrodi R (2024). sasquatch: Use 'SAS', R, and 'quarto' Together. -R package version 0.0.0.9017, https://github.com/ryanzomorrodi/sasr, https://ryanzomorrodi.github.io/sasquatch/. +R package version 0.0.0.9028, https://github.com/ryanzomorrodi/sasquatch, https://ryanzomorrodi.github.io/sasquatch/.

@Manual{,
   title = {sasquatch: Use 'SAS', R, and 'quarto' Together},
   author = {Ryan Zomorrodi},
   year = {2024},
-  note = {R package version 0.0.0.9017, https://github.com/ryanzomorrodi/sasr},
+  note = {R package version 0.0.0.9028, https://github.com/ryanzomorrodi/sasquatch},
   url = {https://ryanzomorrodi.github.io/sasquatch/},
 }
diff --git a/index.html b/index.html index 448aa5e..45b5b98 100644 --- a/index.html +++ b/index.html @@ -29,7 +29,7 @@ sasquatch - 0.0.0.9017 + 0.0.0.9028 @@ -50,7 +51,7 @@ - +
@@ -66,39 +67,60 @@

Use SAS, R, and Quarto Together

-

sasquatch allows you to combine the power of R, SAS, and quarto together to create reproducible multilingual reports. sasquatch can run SAS code blocks interactively, send data back and forth between SAS and R, and render SAS HTML output within quarto documents.

-

sasquatch relies on the SASPy Python package. But if you…

+

sasquatch allows you to combine the power of R, SAS, and quarto together to create reproducible multilingual reports. sasquatch can:

    -
  • Don’t have SASPy already installed, or
    -
  • -
  • Don’t have a SAS License
  • +
  • Run SAS code blocks interactively
  • +
  • Send data back and forth between SAS and R
  • +
  • Conduct basic file management on a SAS client
  • +
  • Render SAS output within quarto documents.
-

Check out vignette("setting_up") for guidance on how to get started with a free SAS On Demand for Academics license (you don’t need to be an academic!).

+

sasquatch relies on the SASPy Python package and the reticulate R package to interoperate with Python. Check out vignette("configuration") for guidance on SASPy configuration.

Installation

+
+

Package installation +

You can install the development version of sasquatch like so:

 # install.packages("pak")
 pak::pkg_install("ryanzomorrodi/sasquatch")
+
+

Python installation +

+

Make sure Python is installed on your system. If Python has not been installed, you can install Python like so:

+
+reticulate::install_python()
+

or download the installer from the Python Software Foundation.

+
+
+

+SASPy installation +

+

To install the SASPy package and its dependencies within a Python virutal environment:

+
+sasquatch::install_saspy()
+

See vignette("configuration") for guidance on SASPy configuration.

+
+

Usage

Once you have setup SASPy and connected to the right python environment using reticulate (if necessary), you can create a quarto document like any other, call sas_connect(), and just get going!

-
---
-format: html
-engine: knitr
----
-
-```{r}
-library(sasquatch)
-sas_connect()
-```
-
-```{sas}
-
-```
+
---
+format: html
+engine: knitr
+---
+
+```{r}
+library(sasquatch)
+sas_connect()
+```
+
+```{sas}
+
+```

Code blocks

@@ -115,7 +137,7 @@

Sending output to viewerConverting tables

Pass tables between R and SAS with r_to_sas() and sas_to_r().

-
+
 r_to_sas(mtcars, "mtcars")
 cars <- sas_to_r("cars", libref = "sashelp")
@@ -127,16 +149,45 @@

Rendering quarto documents

-

Similar packages +

Comparison with similar packages

-

saquatch works similarly to packages like sasr or configSAS. In fact, configSAS author Johann Laurent’s talk at a useR! event inspired sasquatch’s creation. sasr, while similar to sasquatch, does not include interactive SAS functionality or a knitr engine. On the other hand, configSAS includes a knitr engine, but no interactive SAS functionality. configSAS knitr output also does not include syntax highlighting and nested SAS output interferes with the styles of the rest of the document.

+

sasr

+
    +
  • +sasr works identically to sasquatch relying on the SASPy Python package to interface with SAS, but does not include any interactive, file management, or quarto functionality.
  • +
+

configSAS

+
    +
  • Like sasr and sasquatch, configSAS relies on the SASPy Python package, but it primarily focuses on solely on knitr engine support.
  • +
  • The configSAS engine HTML output CSS styles interfere with the rest of the document and SAS code output is not contained within a code block.
  • +
+

SASmarkdown

+
    +
  • +SASmarkdown does not rely on the SASPy Python package and thus is fairly simple to set up; however, it does require a SAS executable to be installed on the same machine as R.
  • +
  • In contrast, SASPy-reliant packages can interface with both local and remote SAS installations and can easily pass data between R and SAS without the need for intermediate files.
  • +
  • +SASmarkdown features several different engines for various formats not currently implemented within sasquatch like latex pdfs or non-HTML5 HTML.
  • +
+

sasquatch may be beneficial to you if you…

+
    +
  • Rely on remote SAS client
    +
  • +
  • Desire interactive SAS functionality while developing
    +
  • +
  • Require remote SAS file management
    +
  • +
  • Would like to be able to easily send data back and forth between SAS and R
    +without the use of intermediate files
  • +
+

If you require pdf knitr support and have a local installation of SAS, I would recommend using SASmarkdownat this time.