class: center, middle, inverse, title-slide # R Package Development ## Albany R Users Group and CUNY MSDS ### Jason Bryer, Ph.D. ### March 1, 2022 --- # Agenda * Overview of packages * Creating a package * Documenting a package * Testing a package * Building a package * Demo * Including Shiny apps in packages * Releasing packages to Github and CRAN * Conclusions / Additional Resources --- class: inverse, middle, center # Overview of R Packages --- # What is an R package? R packages are the basic unit of sharing code, data, documentation, and tests. It is a standardized format that allows for extending the R language. There are currently 18,994 packages listed on the [Comprehensive R Archive Network](https://cran.r-project.org). You are probably already using packages, installed using `install.packages` (or `remotes::install_github`) and loaded using `library` or `require`. --- # Setup To develop R packages we are going to need some additional developer tools. This command will install the packages necessary for package development: ```r install.packages(c('devtools', 'roxygen2', 'usethis', 'testthat', 'kntir', 'vdiffr')) ``` Windows users will need to have Rtools installed. It can be downloaded from here: https://cran.r-project.org/bin/windows/Rtools/ Mac users need to have Xcode command line tools installed. Download Xcode from here: https://apps.apple.com/us/app/xcode/id497799835?mt=12 Once installed, fun the following command in the Terminal: ``` xcode-select --install ``` Linux users need to install the R development tools. If on Ubuntu, for example, install `r-base-dev`. --- # Creating an R Package <img src="images/hex/usethis.png" class="title-hex"> The `usethis` package provides a helper function that will initialize an R package for you. ```r library(usethis) path <- '~/loess' create_package(path) proj_activate(path) ``` The result of above will create a new directory with the basic files for an R package. Additionally, it will create a new RStudio project and open that project to begin editing. --- # Package Structure .pull-left[ * `.gitignore` - anticipates Git usage and ignores some standard, behind-the-scenes files created by R and RStudio. Even if you do not plan to use Git, this is harmless. * `.Rbuildignore` - lists files that we need to have around but that should not be included when building the R package from source. * `DESCRIPTION` - provides metadata about your package. * `loess.Rproj` - RStudio project file (note that this will have the name specified in `create_package`). * `NAMESPACE` - declares the functions your package exports for external use and the external functions your package imports from other packages. *Do not edit this file directly.* * `R/` - Directory where your R functions will reside. ] .pull-right[ ![Screenshot of files](images/new_project_files.png) ] --- # DESCRIPTION File The `DESCRIPTION` file contains important metadata about your package. The following is the default after creating your package with `create_package()`: ``` Package: loess Title: What the Package Does (One Line, Title Case) Version: 0.0.0.9000 Authors@R: person("First", "Last", , "first.last@example.com", role = c("aut", "cre"), comment = c(ORCID = "YOUR-ORCID-ID")) Description: What the package does (one paragraph). License: MIT + file LICENSE Encoding: UTF-8 Roxygen: list(markdown = TRUE) RoxygenNote: 7.1.2 ``` The title and description are particularly important as this is what will show up in the listing on CRAN if you publish there. --- # DESCRIPTION File (cont.) For the author(s), use the `person` function which includes the following parameters: given, family, middle, email, role, comment, first, last. Roles can include any of the following: * `cre`: the creator or maintainer, the person you should bother if you have problems. Despite being short for “creator”, this is the correct role to use for the current maintainer, even if they are not the initial creator of the package. * `aut`: authors, those who have made significant contributions to the package. * `ctb`: contributors, those who have made smaller contributions, like patches. * `cph`: copyright holder. This is used if the copyright is held by someone other than the author, typically a company (i.e. the author’s employer). * `fnd`: funder, the people or organizations that have provided financial support for the development of the package. There are other fields (described [here](https://r-pkgs.org/description.html#description-other-fields)) that may useful. The `URL` and `BugReports` are two common fields to add: ``` URL: https://github.com/jbryer/mypkg BugReports: https://github.com/jbryer/mypkg/issues ``` --- # Package License <img src="images/hex/usethis.png" class="title-hex"> The `usethis` package provides a number of helper functions to set the license for your package. If you plan to publish your package to CRAN, you must have a license. But even if you publish only to Github providing a license helps other useRs know the rules for using your package. ```r ls('package:usethis')[grep('_license$', ls('package:usethis'))] ``` ``` ## [1] "use_agpl_license" "use_agpl3_license" "use_apache_license" "use_apl2_license" ## [5] "use_cc0_license" "use_ccby_license" "use_gpl_license" "use_gpl3_license" ## [9] "use_lgpl_license" "use_mit_license" "use_proprietary_license" ``` See https://choosealicense.com for more information on how to choose a license. --- class: font80 # Package Dependencies <img src="images/hex/usethis.png" class="title-hex"> It is very likely your package will require other packages to work. There are several ways you can determine the level of requirement for the dependency package. * **Imports** - packages that needed for your package to work. * **Suggests** - packages required for development or optional features. * **Depends** - prior to R version 2.14.0 this was the only way to specify other packages your package requires. It is generally preferred to use *Imports* or *Suggests* now. * **LinkingTo** - packages listed here rely on C or C++ code in another package. * **Enhances** - packages listed here are *enhanced* by your package. Not commonly used so won't discuss here. The `use_package` will add the specifications to the `DESCRIPTION` file. The `NAMESPACE` fill will also need to reflect what packages need to be loaded (and what objects from packages specifically), however that is done using Roxygen tags as described later. ```r usethis::use_package('ggplot2', type = 'Imports') ``` Occasionally call the `use_tidy_description` function to cleanup your dependency list to a common format. .font70[See this section of *R Packages* for more details: https://r-pkgs.org/description.html] --- # Documenting with `roxygen2` <img src="images/hex/roxygen2.png" class="title-hex"> R documentation is located in `.Rd` files and uses a LaTeX style syntax for formatting. The `roxygen2` package provides two key features: 1. Allows documentation to be located next to the source code (so you don't have to edit Rd files directly). 2. Allows documentation to be written in a more readable format using markdown. However, it will sometimes be necessary to use LaTeX style markup for some features. Roxygen will look for comments within the R files that begin with `#'` (note the comment must start on the left margin). We will cover the most common documentation features that will allow the package to pass a CRAN check. See https://roxygen2.r-lib.org/articles/rd.html for much more info. --- # Titles and Descriptions <img src="images/hex/roxygen2.png" class="title-hex"> Each documentation block starts with some text that defines the title, description, and details of the function or data. Here’s an example showing what the documentation for `sum()` might look like if it had been written with roxygen: .pull-left[ ``` #' Sum of vector elements #' #' `sum` returns the sum of all the values present in its arguments. #' #' This is a generic function: methods can be defined for it directly #' or via the [Summary()] group generic. For this to work properly, #' the arguments `...` should be unnamed, and dispatch is on the #' first argument. sum <- function(..., na.rm = TRUE) {} ``` * First sentence is the **title**. * Second paragraph is the **description** which comes first and should be brief. * The remaining paragraphs are the **details** which will appear after the argument descriptions. ] .pull-right[ ![Screenshot of sum function](images/sum_help_screenshot.png) ] --- # Documentation Tags <img src="images/hex/roxygen2.png" class="title-hex"> Object documentation using Roxygen has a number of tags to identify key parts of the help documentation. Most functions will have, at minimum, `@param`, `@return`, and `@examples`. And if it is a function available to the end user, it will also have`@export`. * `@param name description` - Description for a function parameter. Note that all parameters must be documented to pass `check()`. * `@return description` - Description of what the function returns. * `@examples` - Example code that demonstrates the functionality for the function. This code will be run at build time. If there is code that you don't want to run at install/build time, you can surrond it with `\dontrun{}`. You should also this for any code that takes more than a few seconds to run. * `@section title` - Adds arbitrary sections to teh documentation. * `@inherit`, `@inheritParams`, and `@inheritSection` - Allows you to include documentation from another function. * `@seealso` - Links to documentation of another function or dataset. * `@export` - This function should be exported (i.e. made public) when the package is loaded by a user. If this is missing, then the function can only be used internally (or using the `package:::function` syntax). --- class: font60 # Complete documentation for the `sum` function <img src="images/hex/roxygen2.png" class="title-hex"> .pull-left[ ``` #' Sum of vector elements #' #' `sum()` returns the sum of all the values present in its arguments. #' #' This is a generic function: methods can be defined for it directly #' or via the [Summary] group generic. For this to work properly, #' the arguments `...` should be unnamed, and dispatch is on the #' first argument. #' #' @param ... Numeric, complex, or logical vectors. #' @param na.rm A logical scalar. Should missing values (including `NaN`) #' be removed? #' @return If all inputs are integer and logical, then the output #' will be an integer. If integer overflow #' (<http://en.wikipedia.org/wiki/Integer_overflow>) occurs, the output #' will be NA with a warning. Otherwise it will be a length-one numeric or #' complex vector. #' #' Zero-length vectors have sum 0 by definition. See #' <http://en.wikipedia.org/wiki/Empty_sum> for more details. #' @export #' @examples #' sum(1:10) #' sum(1:5, 6:10) #' sum(F, F, F, T, T) #' #' sum(.Machine$integer.max, 1L) #' sum(.Machine$integer.max, 1) #' #' \dontrun{ #' sum("a") #' } sum <- function(..., na.rm = TRUE) {} ``` ] .pull-right[ Run `?sum` to see the built documentation (the code has been truncated some to fit). ![Screenshot of sum function](images/sum_help_screenshot.png) ] --- # Documenting data <img src="images/hex/roxygen2.png" class="title-hex"> .pull-left[ Documentation for data follows the same structure as functions in terms of title, description, and details. However, there are two additional tags that are useful: * `@format` - Gives an overview of the structure of the dataset * `@source` - Reference or URL where the data was retrieved from. ] .pull-right[ ``` #' x and y coordinates generated from a cubic function. #' #' This \code{data.frame} is used to show the features #' of the \code{\link{loess_vis}} function with cubic #' data. It was generated using the following code: #' #' \code{ #' set.seed(2112) #' cubic_df <- tibble( #' x = seq(-1, 1, by = 0.01), #' y = x^3 + rnorm(length(x), mean = 0, sd = 0.05) - x #' } #' #' @format A data frame with 201 rows and 2 variables: #' \describe{ #' \item{x}{independent variable} #' \item{y}{dependent variable} #' ... #' } #' @source Randomly generated data. "cubic_df" ``` ] --- class: font90 # Package Documentation <img src="images/hex/roxygen2.png" class="title-hex"> In addition to documenting the objects (e.g. functions and data), you can use Roxygen to document the package. The title and description will be pulled from the `DESCRIPTION` file, so this is useful for providing additional details, keywords, and to define package dependencies. ``` usethis::use_package_doc() ``` * `@keywords` - List of keywords related to your package. * `@import package` - This will indicate that the package needs to load the specified package to work. * `@importFrom package function(s)` - This will indicate that the function(s) in the specified package are required to work. Note that the list of functions is space separated. There are two approaches to handling `@import` and `@importFrome`: 1. Include them in all one location in the package documentation or 2. Include them with each function based upon what that function needs. If the later, it is ok if they are duplicated as Roxygen will handle that when we build the documentation files. --- # Formatting within Documentation <img src="images/hex/roxygen2.png" class="title-hex"> There will be a few instances where you will need to use LaTeX style markup within your documentation. * `\code{}` - Will format the enclosing text in a fixed-width font typically for code references. * `\link{}` - Will link to another function or dataset within the help documentation. Alternatively, you can no use `[function()]` markdown syntax to link to other function documentation. * `\dontrun{}` - Used in `@examples` sections for code that should not be run when the package is built or installed. * `\describe{\item{}{}}` - When you wish to create a list. Often used for describing data and functions that return complex lists. --- # Vignettes <img src="images/hex/rmarkdown.png" class="title-hex"><img src="images/hex/knitr.png" class="title-hex"> Vignettes are long form documents describing utilizing your package. I recommend writing your vignettes in Rmarkdown. The `use_vignette` function will create a new vignette. ``` usethis::use_vignette("loess") ``` This will specifically: 1. Create the `vignettes/` directory. 2. Add the necessary dependencies to the `DESCRIPTION` file. 3. Create a draft file `vignettes/loess.Rmd`. You can edit this file using the same Rmarkdown syntax used elsewhere. For details on formatting, see https://r-pkgs.org/vignettes.html --- class: font80 # Testing <img src="images/hex/usethis.png" class="title-hex"><img src="images/hex/testthat.png" class="title-hex"> It is important to test your package. The `testthat` package provides a framework for writing tests that integrates into the development process. This way, each time you build your package all tests are run. .pull-left[ First, we need to setup our package for testing using the `testthat` package. ```r usethis::use_testthat() ``` This will: 1. Create a `tests/testthat` directory. 2. Add testthat to the Suggests field in the DESCRIPTION. 3. Create a file `tests/testthat.R` that runs all your tests when R CMD check runs. ] .pull-right[ Typical workflow will be: 1. Create a test with `usethis::use_test('TEST_NAME')`. 2. Modify your code and/or test. 3. Run your tests with `devtools::test()`. 4. Repeat 2 and 3 until your tests run without error. 5. Repeat steps 1 through 4 until all of your code within the package has been tested. ] --- # Testing <img src="images/hex/testthat.png" class="title-hex"> Tests are organized as: * **Expectations** - The basic level of testing. * **Test Groups** - A grouping of one or more expectations. Consider the following test group with three expectations: ```r test_that("numbers are equivelent", { expect_equal(10, 10 + 1e-7) # This will pass. expect_identical(10, 11) # This will not pass expect_identical(10, 10 + 1e-7) # This will not pass }) ``` -- > Whenever you are tempted to type something into a print statement or a debugger expression, write it as a test instead. — Martin Fowler --- # Expectations <img src="images/hex/testthat.png" class="title-hex"> The `testthat` package provides a lot of functions to check the expected outcome from your tests. They all have two arguements: 1. The actual result and 2. What is expected. If they don't match, an error is thrown. ```r ls('package:testthat')[grep('^expect_', ls('package:testthat'))] ``` ``` ## [1] "expect_condition" "expect_cpp_tests_pass" "expect_equal" "expect_equal_to_reference" ## [5] "expect_equivalent" "expect_error" "expect_failure" "expect_false" ## [9] "expect_gt" "expect_gte" "expect_identical" "expect_invisible" ## [13] "expect_is" "expect_known_hash" "expect_known_output" "expect_known_value" ## [17] "expect_length" "expect_less_than" "expect_lt" "expect_lte" ## [21] "expect_mapequal" "expect_match" "expect_message" "expect_more_than" ## [25] "expect_named" "expect_no_match" "expect_null" "expect_output" ## [29] "expect_output_file" "expect_reference" "expect_s3_class" "expect_s4_class" ## [33] "expect_setequal" "expect_silent" "expect_snapshot" "expect_snapshot_error" ## [37] "expect_snapshot_file" "expect_snapshot_output" "expect_snapshot_value" "expect_snapshot_warning" ## [41] "expect_success" "expect_that" "expect_true" "expect_type" ## [45] "expect_vector" "expect_visible" "expect_warning" ``` --- # Testing Visualizations <img src="images/hex/usethis.png" class="title-hex"><img src="images/hex/testthat.png" class="title-hex"><img src="images/hex/ggplot2.png" class="title-hex"> The [`vdiffr` package](https://github.com/r-lib/vdiffr) is an extension to `testthat` that will monitor R plots. The first time the test is run the image is saved so that subsequent tests will compare the output to the previous version. If the there are differences, the `testthat::snapshot_review()` will allow you to review the differences. ```r test_that("loess_vis works", { data("cubic_df") p <- loess_vis(y ~ x, data = cubic_df) vdiffr::expect_doppelganger("default loess_vis", p) }) ``` --- # Building your package <img src="images/hex/testthat.png" class="title-hex"><img src="images/hex/roxygen2.png" class="title-hex"><img src="images/hex/devtools.png" class="title-hex"> .pull-left[ ### Building Generate the documentation files from the source files. ```r document() ``` Build the package as a binary. ```r build() ``` Install the package. ```r install() ``` ] .pull-right[ ### Testing Run the tests. ```r test() ``` Check your package for any errors. ```r check() ``` ] --- class: inverse, middle, center # Demo --- # Working Example We will convert the Loess regression function and Shiny app created in a past talk into an R package. https://albanyrusers.org/post/2021-11-30-intro_to_shiny/ ```r source('2022-03-01-R_Package_Development/Shiny_Loess/loess_vis.R') data("faithful") loess_vis(eruptions ~ waiting, data = faithful) ``` <img src="2022-03-01-R_Package_Development_files/figure-html/unnamed-chunk-14-1.png" style="display: block; margin: auto;" /> --- class: inverse, middle, center # Shiny Apps in R Packages --- class: font90 # Including shiny apps in R Packages <img src="images/hex/shiny.png" class="title-hex"> **Option One**: Include the shiny app in the `inst/` directory, for example `inst/shiny/`. You can then write a function that starts the app from that director. ```r #' My Shiny App #' @export my_shiny_app <- function() { shiny::runApp(appDir = system.file('shiny', package='loess')) } ``` --- class: font90 # Including shiny apps in R Packages <img src="images/hex/shiny.png" class="title-hex"> **Option Two**: Define the Shiny server and ui as functions within the package. The advantage of this approach is you can pass startup parameters to the Shiny app. Consider this simple Shiny app that displays a data frame. .pull-left[ ```r shiny_server <- function(input, output, session) { if(!exists('thedata', envir = parent.env(environment()), inherits = FALSE)) { message('thedata not available...') data(faithful, envir = environment()) thedata <- faithful } output$thedata <- renderTable({ return(thedata) }) } ``` ] .pull-right[ ```r shiny_ui <- function() { fluidPage( titlePanel('Shiny Parameter Test'), tableOutput('thedata') ) } ``` ] Note that function checks for `thedata` in the environment. If it doesn't exist it creates the object and sets it equal the `faithful` data frame. In the standalone Shiny app, `thedata` was set in `global.R`. --- # Including shiny apps in R Packages <img src="images/hex/shiny.png" class="title-hex"> .pull-left[ ```r runShinyApp <- function(thedata, ...) { shiny_env <- new.env() # Set names parameters if(!missing(thedata)) { assign('thedata', thedata, shiny_env) } # Set other parameters from the ... operator params <- list(...) for(i in seq_len(length(params))) { assign(names(params[i]), params[[i]], shiny_env) } environment(shiny_ui) <- shiny_env environment(shiny_server) <- shiny_env app <- shiny::shinyApp( ui = shiny_ui, server = shiny_server ) environment(app) <- shiny_env runApp(app) } ``` ] .pull-right[ This function can easily be reused in your own package. Note that it assigns both named parameters (in this example `thedata`) as well as arbitrary parameters specified with the `...` operator. For example, this call will not only change `thedata` in the Shiny app, but will also pass `some_other_var` to the Shiny app. ```r runShinyApp( thedata = mtcars, some_other_var = 'Some value') ``` .font70[Read more here: https://bryer.org/post/2021-02-12-shiny_apps_in_r_packages/] ] --- class: inverse, middle, center # Releasing the package to the world --- # Github <img src="images/hex/usethis.png" class="title-hex"><img src="images/hex/devtools.png" class="title-hex"> The `use_git` will initialize a git repository for your package (from the current working directory). The `use_github` will then publish it to Github. ```r usethis::use_git() usethis::use_github() ``` Once the package is on Github, it can be installed using: ```r remotes::install_github('jbryer/loess') ``` --- # CRAN <img src="images/hex/devtools.png" class="title-hex"> If your package is ready to release to CRAN (no errors, warnings, or notes from running `check()`), the `devtools::release()` will guide you through the process of publishing your package to CRAN. You will: 1. Confirm that you have read the [CRAN Repository Policy](https://cran.r-project.org/web/packages/policies.html) 2. Created a `cran-comments.md` file with comments submitted to the CRAN maintainers. ```r release() ``` Good luck and don't be discouraged if your package doesn't get approved on the first attempt. --- # Build a website <img src="images/hex/pkgdown.png" class="title-hex"> The `pkgdown` package is a quick and easy way to create a website for your package. It will use the documentation you have already written within your R scripts, vignettes, and README for the site contents. The `use_pkgdown()` call will configure your package to use `pkgdown` (only needs to be called once). Then `build_site()` will build the site into the `docs/` direcotry. ```r usethis::use_pkgdown() pkgdown::build_site() ``` Once published to Github, you can configure [Github Pages](https://pages.github.com) to host the site from the `docs/` directory. This is located in the Setting section of your repository. --- class: inverse, middle, center # Wrap Up --- # Additional Resources * *R Packages* book by Hadley Wickham: https://r-pkgs.org/index.html * *Happy Git and Github for the useR* by Jennifer Bryan * `usethis` package documentation: https://usethis.r-lib.org/index.html * `devtools` package documentation: https://devtools.r-lib.org * `roxygen2` package documentation: https://roxygen2.r-lib.org/index.html * `pkgdown` package documentation: https://pkgdown.r-lib.org * Writing R Extensions documentation: https://cran.r-project.org/manuals.html#R-exts --- # Devtools Cheatsheet [![Devtools Cheatsheet](images/package-development-thumbs.png)](https://rawgit.com/rstudio/cheatsheets/main/package-development.pdf) Other cheatsheets available here: https://www.rstudio.com/resources/cheatsheets/ --- class: inverse, right, middle, hide-logo # Thank you! [<svg viewBox="0 0 512 512" style="height:1em;position:relative;display:inline-block;top:.1em;" xmlns="http://www.w3.org/2000/svg"> <path d="M440 6.5L24 246.4c-34.4 19.9-31.1 70.8 5.7 85.9L144 379.6V464c0 46.4 59.2 65.5 86.6 28.6l43.8-59.1 111.9 46.2c5.9 2.4 12.1 3.6 18.3 3.6 8.2 0 16.3-2.1 23.6-6.2 12.8-7.2 21.6-20 23.9-34.5l59.4-387.2c6.1-40.1-36.9-68.8-71.5-48.9zM192 464v-64.6l36.6 15.1L192 464zm212.6-28.7l-153.8-63.5L391 169.5c10.7-15.5-9.5-33.5-23.7-21.2L155.8 332.6 48 288 464 48l-59.4 387.3z"></path></svg> jason.bryer@cuny.edu](mailto:jason.bryer@cuny.edu) [<svg viewBox="0 0 496 512" style="height:1em;position:relative;display:inline-block;top:.1em;" xmlns="http://www.w3.org/2000/svg"> <path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path></svg> @jbryer](https://github.com/jbryer) [<svg viewBox="0 0 512 512" style="height:1em;position:relative;display:inline-block;top:.1em;" xmlns="http://www.w3.org/2000/svg"> <path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path></svg> @jbryer](https://twitter.com/jbryer) [<svg viewBox="0 0 512 512" style="height:1em;position:relative;display:inline-block;top:.1em;" xmlns="http://www.w3.org/2000/svg"> <path d="M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z"></path></svg> bryer.org](https://bryer.org)