├── .Rbuildignore
├── .github
└── workflows
│ └── build-book.yaml
├── .gitignore
├── DESCRIPTION
├── LICENSE
├── README.md
├── _bookdown.yml
├── _build.R
├── _oreilly
└── .gitignore
├── _output.yml
├── action-bookmark.Rmd
├── action-dynamic.Rmd
├── action-feedback.Rmd
├── action-graphics.Rmd
├── action-layout.Rmd
├── action-tidy.Rmd
├── action-transfer.Rmd
├── action-workflow.Rmd
├── action.Rmd
├── advanced-ui.Rmd
├── apps
└── shiny-test
│ └── app.R
├── basic-app.Rmd
├── basic-case-study.Rmd
├── basic-reactivity.Rmd
├── basic-ui.Rmd
├── basic.Rmd
├── birthstones.csv
├── chicago-fullnote-bibliography.csl
├── common.R
├── contributors.csv
├── cover.png
├── demo-app.R
├── demo.R
├── demos
├── action-bookmark
│ ├── bookmark-auto.rds
│ ├── bookmark-server.rds
│ ├── bookmark-url.rds
│ ├── pendulum.png
│ └── pendulum.rds
├── action-dynamic
│ ├── dynamic-conditional-exponential.png
│ ├── dynamic-conditional-normal.png
│ ├── dynamic-conditional-uniform.png
│ ├── dynamic-conditional.rds
│ ├── dynamic-panels-panel2.png
│ ├── dynamic-panels-panel3.png
│ ├── dynamic-panels.png
│ ├── dynamic-panels.rds
│ ├── filtering-final.png
│ ├── filtering-final.rds
│ ├── freeze.rds
│ ├── render-filter-1.png
│ ├── render-filter-1.rds
│ ├── render-filter-2.png
│ ├── render-filter-2.rds
│ ├── render-palette-change-n.png
│ ├── render-palette-full-change-n.png
│ ├── render-palette-full-rainbow.png
│ ├── render-palette-full.rds
│ ├── render-palette-onload.png
│ ├── render-palette-set-cols.png
│ ├── render-palette.rds
│ ├── render-simple-label.png
│ ├── render-simple-numeric.png
│ ├── render-simple-onload.png
│ ├── render-simple.rds
│ ├── temperature.rds
│ ├── update-basics-max-increase.png
│ ├── update-basics-min-decrease.png
│ ├── update-basics-onload.png
│ ├── update-basics.rds
│ ├── update-button-onload.png
│ ├── update-button-set1.png
│ ├── update-button-set100.png
│ ├── update-button.rds
│ ├── update-nested-customername.png
│ ├── update-nested-orders.png
│ ├── update-nested-territory.png
│ ├── update-nested.rds
│ ├── update-reset-onload.png
│ ├── update-reset-reset.png
│ ├── update-reset-set.png
│ ├── update-reset.rds
│ ├── wizard-1.png
│ ├── wizard-2.png
│ ├── wizard-3.png
│ └── wizard.rds
├── action-feedback
│ ├── dialog.png
│ ├── dialog.rds
│ ├── feedback-even.png
│ ├── feedback-odd.png
│ ├── feedback.rds
│ ├── notification-transient.rds
│ ├── notification-updates.rds
│ ├── notify-persistent.rds
│ ├── progress.rds
│ ├── require-cancel-empty.png
│ ├── require-cancel-error.png
│ ├── require-cancel-ok.png
│ ├── require-cancel.rds
│ ├── require-simple.png
│ ├── require-simple.rds
│ ├── require-simple2-langauge.png
│ ├── require-simple2-name.png
│ ├── require-simple2-on-load.png
│ ├── require-simple2.rds
│ ├── spinner-1.rds
│ ├── spinner-2.rds
│ ├── undo.rds
│ ├── validate-init.png
│ ├── validate-log.png
│ ├── validate.rds
│ └── waiter.rds
├── action-graphics
│ ├── brushedPoints.rds
│ ├── click.rds
│ ├── modifying-size.rds
│ ├── nearPoints.rds
│ ├── persistent.rds
│ ├── puppies-corgi.png
│ ├── puppies-lab.png
│ ├── puppies.rds
│ ├── resize-narrow.png
│ ├── resize-wide.png
│ └── resize.rds
├── action-layout
│ ├── navbarPage.png
│ ├── navbarPage.rds
│ ├── navlistPanel.png
│ ├── navlistPanel.rds
│ ├── sidebar.png
│ ├── sidebar.rds
│ ├── tabset-input-1.png
│ ├── tabset-input-2.png
│ ├── tabset-input.rds
│ ├── tabset.png
│ ├── tabset.rds
│ ├── thematic.png
│ ├── thematic.rds
│ ├── theme-darkly.png
│ ├── theme-darkly.rds
│ ├── theme-flatly.png
│ ├── theme-flatly.rds
│ ├── theme-sandstone.png
│ ├── theme-sandstone.rds
│ ├── theme-united.png
│ └── theme-united.rds
├── action-tidy
│ ├── across.png
│ ├── across.rds
│ ├── dplyr.png
│ ├── dplyr.rds
│ ├── ggplot2-scatter.png
│ ├── ggplot2-swarm.png
│ ├── ggplot2.rds
│ ├── messed-up.png
│ ├── messed-up.rds
│ ├── tidied-up.png
│ ├── tidied-up.rds
│ ├── user-supplied.png
│ └── user-supplied.rds
├── action-transfer
│ ├── case-study.png
│ ├── case-study.rds
│ ├── download-data.png
│ ├── download-data.rds
│ ├── download-rmd.rds
│ ├── download.png
│ ├── download.rds
│ ├── upload-validate.rds
│ ├── upload.png
│ └── upload.rds
├── basic-app
│ ├── ex-x-times-5.png
│ ├── ex-x-times-5.rds
│ ├── ex-x-times-y.png
│ ├── ex-x-times-y.rds
│ ├── server.png
│ ├── server.rds
│ ├── ui.png
│ └── ui.rds
├── basic-case-study
│ ├── narrative.png
│ ├── polish-tables.png
│ ├── prototype.png
│ └── rate-vs-count.png
├── basic-reactivity
│ ├── action-button.png
│ ├── action-button.rds
│ ├── case-study-1.png
│ ├── case-study-1.rds
│ ├── connection-1.png
│ ├── connection-2.png
│ ├── connection-3.png
│ ├── connection.rds
│ ├── simulation-2.png
│ └── simulation-2.rds
├── basic-ui
│ ├── action-css.png
│ ├── action-css.rds
│ ├── action.png
│ ├── action.rds
│ ├── date-slider.png
│ ├── date-slider.rds
│ ├── date.png
│ ├── date.rds
│ ├── free-text.png
│ ├── free-text.rds
│ ├── limited-choices.png
│ ├── limited-choices.rds
│ ├── multi-radio.png
│ ├── multi-radio.rds
│ ├── numeric.png
│ ├── numeric.rds
│ ├── output-plot.png
│ ├── output-plot.rds
│ ├── output-table.png
│ ├── output-table.rds
│ ├── output-text.png
│ ├── output-text.rds
│ ├── placeholder.png
│ ├── placeholder.rds
│ ├── radio-icon.png
│ ├── radio-icon.rds
│ ├── text-vs-print.png
│ ├── text-vs-print.rds
│ ├── upload.png
│ ├── upload.rds
│ ├── yes-no.png
│ └── yes-no.rds
└── scaling-modules
│ ├── radio-extra-ui.png
│ ├── radio-extra-ui.rds
│ ├── radio-extra.png
│ ├── radio-extra.rds
│ ├── wizard-module-1.png
│ ├── wizard-module-2.png
│ ├── wizard-module-3.png
│ ├── wizard-module.rds
│ ├── wizard-ui-1.png
│ ├── wizard-ui-2.png
│ ├── wizard-ui-3.png
│ └── wizard-ui.rds
├── diagrams
├── action-layout.graffle
├── action-layout
│ ├── multirow.png
│ └── sidebar.png
├── basic-case-study.graffle
├── basic-reactivity.graffle
├── basic-reactivity
│ ├── case-study-1.png
│ ├── case-study-2.png
│ ├── case-study-3.png
│ ├── graph-1a.png
│ ├── graph-1b.png
│ ├── graph-2a.png
│ ├── graph-2b.png
│ ├── graph-3.png
│ ├── producers-consumers.png
│ ├── template.png
│ ├── timing-button-2.png
│ ├── timing-button.png
│ ├── timing-click.png
│ ├── timing-debounce.png
│ ├── timing-timer.png
│ └── timing.png
├── reactivity-components.graffle
├── reactivity-components
│ ├── button 3.png
│ ├── button.png
│ ├── danger-2.png
│ └── danger.png
├── reactivity-req.graffle
├── reactivity-tracking.graffle
├── reactivity-tracking
│ ├── 01.png
│ ├── 02.png
│ ├── 03.png
│ ├── 04.png
│ ├── 05.png
│ ├── 06.png
│ ├── 07.png
│ ├── 08.png
│ ├── 09.png
│ ├── 10.png
│ ├── 11.png
│ ├── 12.png
│ ├── complete.png
│ ├── dynamic.png
│ ├── dynamic2.png
│ ├── invalidate-1.png
│ └── invalidate-2.png
├── scaling-modules.graffle
├── scaling-modules
│ ├── after.png
│ └── before.png
├── scaling-performance.graffle
└── scaling-performance
│ ├── collapsed.png
│ ├── flame.png
│ ├── horizontal.png
│ ├── proportional.png
│ └── vertical.png
├── ga_script.html
├── images
├── action-feedback
│ ├── notify-1.png
│ ├── notify-2.png
│ ├── notify-3.png
│ ├── progress-1.png
│ ├── progress-2.png
│ ├── progress-3.png
│ ├── progress-4.png
│ ├── spinner-1.png
│ └── spinner-2.png
├── action-graphics
│ ├── brushedPoints.png
│ ├── click.png
│ ├── modifying-size-1.png
│ ├── modifying-size-2.png
│ ├── nearPoints.png
│ ├── persistent-1.png
│ ├── persistent-2.png
│ └── persistent-3.png
├── action-layout
│ └── fluid-page.png
├── action-workflow
│ ├── breakpoint.png
│ ├── debug-toolbar.png
│ ├── new-project.png
│ └── run-app.png
├── basic-app
│ ├── cheatsheet.png
│ ├── hello-world.png
│ └── run-app.png
├── basic-ui
│ └── multi-select.png
├── prod-best-practices
│ └── ci-screenshot.png
├── reactivity-graph
│ └── reactlog.png
├── scaling-performance
│ ├── in-app.png
│ └── profvis.png
└── scaling-testing
│ └── keyboard-shortcuts.png
├── index.Rmd
├── introduction.Rmd
├── mastering-shiny.Rproj
├── neiss
├── .gitignore
├── data.R
├── injuries.tsv.gz
├── narrative.R
├── polish-tables.R
├── population.tsv
├── products.tsv
├── prototype.R
└── rate-vs-count.R
├── preamble.tex
├── puppy-photos
├── KCdYn0xu2fU.jpg
├── TzjMd7i5WQI.jpg
└── eoqnr8ikwFE.jpg
├── reactivity-escaping.Rmd
├── reactivity-foundations.Rmd
├── reactivity-graph.Rmd
├── reactivity-motivation.Rmd
├── reactivity.Rmd
├── references.bib
├── render128dc48fa5fba.rds
├── rmarkdown-report
├── app.R
└── report.Rmd
├── sales-dashboard
├── app.R
└── sales_data_sample.csv
├── scaling-functions.Rmd
├── scaling-general.Rmd
├── scaling-modules.Rmd
├── scaling-packages.Rmd
├── scaling-performance.Rmd
├── scaling-security.Rmd
├── scaling-testing.Rmd
├── scaling-testing.rds
├── scaling.Rmd
├── style.css
└── toc.css
/.Rbuildignore:
--------------------------------------------------------------------------------
1 | ^.*\.Rproj$
2 | ^\.Rproj\.user$
3 |
--------------------------------------------------------------------------------
/.github/workflows/build-book.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - main
5 | pull_request:
6 | branches:
7 | - main
8 | workflow_dispatch:
9 | schedule:
10 | # run every day at 11 PM
11 | - cron: "0 23 * * *"
12 |
13 | name: build-book
14 |
15 | env:
16 | isExtPR: ${{ github.event.pull_request.head.repo.fork == true }}
17 |
18 | jobs:
19 | build:
20 | env:
21 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
22 | runs-on: ubuntu-latest
23 | steps:
24 | - uses: actions/checkout@v3
25 |
26 | - uses: r-lib/actions/setup-pandoc@v2
27 |
28 | - uses: r-lib/actions/setup-r@v2
29 | with:
30 | use-public-rspm: true
31 |
32 | - uses: r-lib/actions/setup-r-dependencies@v2
33 |
34 | - name: Build book
35 | run: Rscript -e 'bookdown::render_book("index.Rmd", quiet = TRUE)'
36 |
37 | - name: Upload website artifact
38 | if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' }}
39 | uses: actions/upload-pages-artifact@v3
40 | with:
41 | path: "_book"
42 |
43 | deploy:
44 | needs: build
45 |
46 | permissions:
47 | pages: write # to deploy to Pages
48 | id-token: write # to verify the deployment originates from an appropriate source
49 |
50 | environment:
51 | name: github-pages
52 | url: ${{ steps.deployment.outputs.page_url }}
53 |
54 | runs-on: ubuntu-latest
55 | steps:
56 | - name: Deploy to GitHub Pages
57 | id: deployment
58 | uses: actions/deploy-pages@v4
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .Rproj.user
2 | .Rhistory
3 | .RData
4 | _publish.R
5 | _book
6 | rsconnect
7 | _download_cache
8 | shiny-book.rds
9 | *_cache
10 | *_files
11 | shiny-book.*
12 | README.html
13 | cran-logs
14 | shiny_bookmarks
15 | scaling-testing
16 |
--------------------------------------------------------------------------------
/DESCRIPTION:
--------------------------------------------------------------------------------
1 | Package: masteringshiny
2 | Title: Mastering Shiny
3 | Version: 0.0.1
4 | Imports:
5 | gapminder,
6 | gh,
7 | ggforce,
8 | globals,
9 | openintro,
10 | profvis,
11 | RSQLite,
12 | shiny (>= 1.6.0),
13 | shinycssloaders,
14 | shinyFeedback,
15 | shinythemes,
16 | testthat,
17 | thematic (>= 0.1.1),
18 | tidyverse,
19 | vroom,
20 | waiter,
21 | xml2,
22 | zeallot
23 | Suggests:
24 | bookdown,
25 | bslib,
26 | desc,
27 | downlit,
28 | jsonlite,
29 | png,
30 | sessioninfo,
31 | shinyloadtest,
32 | shinytest (>= 1.5.0),
33 | waldo,
34 | webshot (>= 0.5.0.9000)
35 | Remotes:
36 | rstudio/bookdown,
37 | rstudio/rmarkdown
38 | Encoding: UTF-8
39 | Config/testthat/edition: 3
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This work is licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-nd/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | [](https://github.com/hadley/mastering-shiny/actions?workflow=.github/workflows/build-book.yaml)
3 |
4 |
5 | This is the repo for the book _Mastering Shiny_ by Hadley Wickham. It is licensed under the Creative Commons [Attribution-NonCommercial-NoDerivatives 4.0 International License](http://creativecommons.org/licenses/by-nc-nd/4.0/).
6 |
7 | Built with [bookdown](https://bookdown.org/yihui/bookdown/).
8 |
9 | ## Images
10 |
11 | There are three directories for images:
12 |
13 | * `diagrams/` contains omnigraffle diagrams. Source of truth is `.graffle`
14 | files. Can delete all subdirectories.
15 |
16 | * `screenshots/` contains programmatic screenshots. Source of truth is
17 | book code. Can delete all subdirectories.
18 |
19 | * `images/` contains images created some other way. Images are source of
20 | truth and should not be deleted.
21 |
--------------------------------------------------------------------------------
/_bookdown.yml:
--------------------------------------------------------------------------------
1 | book_filename: "shiny-book"
2 | language:
3 | ui:
4 | chapter_name: "Chapter "
5 | delete_merged_file: true
6 | new_session: yes
7 |
8 | rmd_files:
9 | [
10 | "index.Rmd",
11 |
12 | "introduction.Rmd",
13 |
14 | "basic.Rmd",
15 | "basic-app.Rmd",
16 | "basic-ui.Rmd",
17 | "basic-reactivity.Rmd",
18 | "basic-case-study.Rmd",
19 |
20 | "action.Rmd",
21 | "action-workflow.Rmd",
22 | "action-layout.Rmd",
23 | "action-graphics.Rmd",
24 | "action-feedback.Rmd",
25 | "action-transfer.Rmd",
26 | "action-dynamic.Rmd",
27 | "action-bookmark.Rmd",
28 | "action-tidy.Rmd",
29 |
30 | "reactivity.Rmd",
31 | "reactivity-motivation.Rmd",
32 | "reactivity-graph.Rmd",
33 | "reactivity-foundations.Rmd",
34 | "reactivity-escaping.Rmd",
35 |
36 | "scaling.Rmd",
37 | "scaling-general.Rmd",
38 | "scaling-functions.Rmd",
39 | "scaling-modules.Rmd",
40 | "scaling-packages.Rmd",
41 | "scaling-testing.Rmd",
42 | "scaling-security.Rmd",
43 | "scaling-performance.Rmd",
44 | ]
45 |
46 |
47 |
--------------------------------------------------------------------------------
/_build.R:
--------------------------------------------------------------------------------
1 | library(tidyverse)
2 | library(fs)
3 |
4 | chapters <- setdiff(yaml::read_yaml("_bookdown.yml")$rmd_files, "index.Rmd")
5 | chapters_md <- path("_oreilly", path_ext_set(chapters, ".md"))
6 | names(chapters_md) <- path_ext_remove(path_file(chapters_md))
7 |
8 | # Build book --------------------------------------------------------------
9 |
10 | render_clean <- function(path, ...) {
11 | message("Rendering ", path)
12 | callr::r(function(...) rmarkdown::render(...), list(path, ...), spinner = TRUE)
13 | }
14 |
15 | format <- rmarkdown::md_document(variant = "markdown-fenced_code_attributes-raw_attribute")
16 | format$pandoc$args <- c(format$pandoc$args, "--wrap=preserve")
17 |
18 | Sys.setenv(CI = "true") # don't rebuild demos
19 | chapters %>% walk(
20 | render_clean,
21 | output_format = format,
22 | output_dir = "_oreilly"
23 | )
24 |
25 | # Convert from md to asciidoc ---------------------------------------------
26 | #
27 | # replace_lines <- function(file, pattern, replacement, comments = FALSE) {
28 | # str_replace_all(file, regex(pattern, multiline = TRUE, comments = comments), replacement)
29 | # }
30 | #
31 | # # Regular expressions mostly contributed by Nicholas Adams, O'Reilly
32 | # md2asciidoc <- function(file) {
33 | # # Headings with and without ids
34 | # file <- replace_lines(file, r"(
35 | # ^\#\ \(PART\\\*\) # standard part marker
36 | # (.*?)\ # title
37 | # \{\#
38 | # ([-a-zA-Z]+) # id
39 | # (\ .unnumbered)?
40 | # \}
41 | # )", "[part]\n== \\1", comments = TRUE)
42 | # file <- replace_lines(file, '(^# )(.*?)(\\{#)(.*?)(\\})', '[[\\4]]\n== \\2') # Chapter heading with ID
43 | # file <- replace_lines(file, '(^# )(.*?)(\\{#)(.*?)(\\})', '[[\\4]]\n== \\2') # Chapter heading with ID
44 | # file <- replace_lines(file, '(^## )(.*?)(\\{#)(.*?)(\\})', '[[\\4]]\n=== \\2') # A-Head with ID
45 | # file <- replace_lines(file, '(^## )(.*?)', '=== \\2') # A-Head no ID
46 | # file <- replace_lines(file, '(^### )(.*?)(\\{#)(.*?)(\\})', '[[\\4]]\n==== \\2') # B-Head with ID
47 | # file <- replace_lines(file, '(^### )(.*?)', '==== \\2') # B-Head no ID
48 | #
49 | # # Code blocks
50 | # file <- replace_lines(file, '(^ *)(```)(.*?)(\n)((.|\n)*?)(```)', '\\1[source,\\3]\n\\1----\n\\5----')
51 | #
52 | # # Figures
53 | # file <- replace_lines(file, '(
)(\n)(
)\n(.*?)\n(
)', '.\\8\nimage::\\2["\\8"]')
54 | # file <- replace_lines(file, '(
)', 'image::\\2[]')
55 | # file <- replace_lines(file, '(::: \\{.figure\\})((.|\n)*?)(:::)', '\\2') # Remove figures
56 | #
57 | # # Cross refs
58 | # file <- replace_lines(file, '(Section )(\\\\@ref\\()(.*?)(\\))', '<<\\3>>') # Section
59 | # file <- replace_lines(file, '(Chapter )(\\\\@ref\\()(.*?)(\\))', '<<\\3>>') # Chapter
60 | # file <- replace_lines(file, '(Figure )(\\\\@ref\\()(fig:)(.*?)(\\))', '<>') # Figures
61 | #
62 | # # Other formatting
63 | # file <- replace_lines(file, '(::: \\{.rmdnote\\})((.|\n)*?)(:::)', '****\\2****') # Sidebar
64 | # file <- replace_lines(file, '()', 'https:\\2[]') # Links
65 | # file <- replace_lines(file, '(\\[.*?\\])(\\()(https?:)(.*?)(\\))', '\\3\\4\\1') # Links with anchor text
66 | # file <- replace_lines(file, '(\\[\\^.*?\\])((.|\n)*?)(\\1: )(.*?)(\n)', 'footnote:[\\5]\\2') # Footnotes
67 | # file
68 | # }
69 | #
70 | # asciidoc <- chapters_md %>% map(~ md2asciidoc(read_file(.)))
71 | #
72 | # walk2(asciidoc, path_ext_set(chapters_md, ".asciidoc"), write_file)
73 |
74 | # Copy over images ------------------------------------------------------------
75 |
76 | dir_copy("demos", "_oreilly")
77 | file_delete(dir_ls("_oreilly/demos", recurse = T, glob = "*.rds"))
78 |
79 | dir_copy("diagrams", "_oreilly")
80 | file_delete(dir_ls("_oreilly/diagrams", recurse = T, glob = "*.graffle"))
81 |
82 | dir_copy("images", "_oreilly")
83 |
84 |
85 | # Copy everything into O'Reilly git repo ----------------------------------
86 | # https://atlas.oreilly.com/oreillymedia/mastering-shiny
87 |
88 | system("cp -r _oreilly/* ../oreilly")
89 |
--------------------------------------------------------------------------------
/_oreilly/.gitignore:
--------------------------------------------------------------------------------
1 | *.md
2 | demos
3 | diagrams
4 | images
5 |
--------------------------------------------------------------------------------
/_output.yml:
--------------------------------------------------------------------------------
1 | bookdown::bs4_book:
2 | theme:
3 | primary: "#4D6F8D"
4 | repo: https://github.com/hadley/mastering-shiny
5 | includes:
6 | in_header: [ga_script.html]
7 | bookdown::gitbook:
8 | css: style.css
9 | includes:
10 | in_header: [ga_script.html]
11 | config:
12 | toc:
13 | collapse: section
14 | before: |
15 | Mastering Shiny
16 | edit: https://github.com/hadley/mastering-shiny/edit/main/%s
17 | download: []
18 | sharing:
19 | facebook: no
20 | twitter: no
21 | github: yes
22 | all: []
23 | bookdown::pdf_book:
24 | includes:
25 | in_header: preamble.tex
26 | latex_engine: xelatex
27 | citation_package: natbib
28 | keep_tex: yes
29 | bookdown::epub_book: default
30 |
--------------------------------------------------------------------------------
/action-bookmark.Rmd:
--------------------------------------------------------------------------------
1 | # Bookmarking {#action-bookmark}
2 |
3 | ```{r, include = FALSE}
4 | source("common.R")
5 | source("demo.R")
6 | ```
7 |
8 | By default, Shiny apps have one major drawback compared to most web sites: you can't bookmark the app to return to the same place in the future or share your work with someone else with a link in an email.
9 | That's because, by default, Shiny does not expose the current state of the app in its URL.
10 | Fortunately, however, you can change this behaviour with a little extra work and this chapter will show you how.
11 |
12 | ```{r setup}
13 | library(shiny)
14 | ```
15 |
16 | ## Basic idea
17 |
18 | Let's take a simple app that we want to make bookmarkable.
19 | This app draws Lissajous figures, which replicate the motion of a pendulum.
20 | This app can produce a variety of interesting patterns that you might want to share.
21 |
22 | ```{r}
23 | ui <- fluidPage(
24 | sidebarLayout(
25 | sidebarPanel(
26 | sliderInput("omega", "omega", value = 1, min = -2, max = 2, step = 0.01),
27 | sliderInput("delta", "delta", value = 1, min = 0, max = 2, step = 0.01),
28 | sliderInput("damping", "damping", value = 1, min = 0.9, max = 1, step = 0.001),
29 | numericInput("length", "length", value = 100)
30 | ),
31 | mainPanel(
32 | plotOutput("fig")
33 | )
34 | )
35 | )
36 | server <- function(input, output, session) {
37 | t <- reactive(seq(0, input$length, length.out = input$length * 100))
38 | x <- reactive(sin(input$omega * t() + input$delta) * input$damping ^ t())
39 | y <- reactive(sin(t()) * input$damping ^ t())
40 |
41 | output$fig <- renderPlot({
42 | plot(x(), y(), axes = FALSE, xlab = "", ylab = "", type = "l", lwd = 2)
43 | }, res = 96)
44 | }
45 | ```
46 |
47 | ```{r pendulum, fig.cap = "This app allows you to generate intersting figures using a model of a pendulum. Wouldn't it be cool to share a link with your friends?", echo = FALSE, message = FALSE, out.width = NULL}
48 | demo <- demoApp$new("action-bookmark/pendulum", ui, server)
49 | demo$resize(800)
50 | demo$setInputs(omega = -0.13, damping = 0.997)
51 | demo$takeScreenshot()
52 | ```
53 |
54 | There are three things we need to do to make this app bookmarkable:
55 |
56 | 1. Add a `bookmarkButton()` to the UI.
57 | This generates a button that the user clicks to generate the bookmarkable URL.
58 |
59 | 2. Turn `ui` into a function.
60 | You need to do this because bookmarked apps have to replay the bookmarked values: effectively, Shiny modifies the default `value` for each input control.
61 | This means there's no longer a single static UI but multiple possible UIs that depend on parameters in the URL; i.e. it has to be a function.
62 |
63 | 3. Add `enableBookmarking = "url"` to the `shinyApp()` call.
64 |
65 | Making those changes gives us:
66 |
67 | ```{r}
68 | ui <- function(request) {
69 | fluidPage(
70 | sidebarLayout(
71 | sidebarPanel(
72 | sliderInput("omega", "omega", value = 1, min = -2, max = 2, step = 0.01),
73 | sliderInput("delta", "delta", value = 1, min = 0, max = 2, step = 0.01),
74 | sliderInput("damping", "damping", value = 1, min = 0.9, max = 1, step = 0.001),
75 | numericInput("length", "length", value = 100),
76 | bookmarkButton()
77 | ),
78 | mainPanel(
79 | plotOutput("fig")
80 | )
81 | )
82 | )
83 | }
84 | ```
85 |
86 | ```{r, eval = FALSE}
87 | shinyApp(ui, server, enableBookmarking = "url")
88 | ```
89 |
90 | You can try it out at .
91 | If you play around with the app and bookmark a few interesting states, you'll see that the generated URLs look something like this:
92 |
93 | - `https://hadley.shinyapps.io/ms-bookmark-url/?_inputs_&damping=1&delta=1&length=100&omega=1`
94 |
95 | - `https://hadley.shinyapps.io/ms-bookmark-url/?_inputs_&damping=0.966&delta=1.25&length=100&omega=-0.54`
96 |
97 | - `https://hadley.shinyapps.io/ms-bookmark-url/?_inputs_&damping=0.997&delta=1.37&length=500&omega=-0.9`
98 |
99 | To understand what's happening, let's take the first URL and tease it apart into pieces:
100 |
101 | - `http://` is the "protocol" used to communicate with the app.
102 | This will always be `http` or `https`.
103 |
104 | - `hadley.shinyapps.io/ms-bookmark-url` is the location of the app.
105 |
106 | - Everything after `?` is a "parameter".
107 | Each parameter is separated by `&`, and if you break it apart you can see the values of each input in the app:
108 |
109 | - `damping=1`
110 | - `delta=1`
111 | - `length=100`
112 | - `omega=1`
113 |
114 | So "generating a bookmark" means recording the current values of the inputs in the parameters of URL.
115 | If you play around with locally, the urls will look slightly different:
116 |
117 | - `http://127.0.0.1:4087/?_inputs_&damping=1&delta=1&length=100&omega=1`
118 | - `http://127.0.0.1:4087/?_inputs_&damping=0.966&delta=1.25&length=100&omega=-0.54`
119 | - `http://127.0.0.1:4087/?_inputs_&damping=0.997&delta=1.37&length=500&omega=-0.9`
120 |
121 | Most of the pieces are the same except that instead of `hadley.shinyapps.io/ms-bookmark-url` you'll see something like `127.0.0.1:4087`.
122 | `127.0.0.1` is a special address that always points to your own computer, and `4087` is a randomly assigned port.
123 | Normally, different apps get different paths or IP addresses, but that's not possible when you're hosting multiple apps on your own computer.
124 |
125 | ### Updating the URL
126 |
127 | Instead of providing an explicit button, another option is to automatically update the URL in the browser.
128 | This allows your users to use the user bookmark command in their browser, or copy and paste the URL from the location bar.
129 |
130 | Automatically updating the URL requires a little boilerplate in the server function:
131 |
132 | ```{r, eval = FALSE}
133 | # Automatically bookmark every time an input changes
134 | observe({
135 | reactiveValuesToList(input)
136 | session$doBookmark()
137 | })
138 | # Update the query string
139 | onBookmarked(updateQueryString)
140 | ```
141 |
142 | Which gives us an updated server function as follows:
143 |
144 | ```{r}
145 | server <- function(input, output, session) {
146 | t <- reactive(seq(0, input$length, length = input$length * 100))
147 | x <- reactive(sin(input$omega * t() + input$delta) * input$damping ^ t())
148 | y <- reactive(sin(t()) * input$damping ^ t())
149 |
150 | output$fig <- renderPlot({
151 | plot(x(), y(), axes = FALSE, xlab = "", ylab = "", type = "l", lwd = 2)
152 | }, res = 96)
153 |
154 | observe({
155 | reactiveValuesToList(input)
156 | session$doBookmark()
157 | })
158 | onBookmarked(updateQueryString)
159 | }
160 | ```
161 |
162 | ```{r, eval = FALSE}
163 | shinyApp(ui, server, enableBookmarking = "url")
164 | ```
165 |
166 | ```{r, echo = FALSE, message = FALSE}
167 | demo <- demoApp$new("action-bookmark/bookmark-auto", ui, server, bookmark = "url")
168 | demo$deploy()
169 | ```
170 |
171 | And this yields --- since the URL now automatically updates, you could now remove the bookmark button from the UI..
172 |
173 | ### Storing richer state
174 |
175 | So far we've used `enableBookmarking = "url"` which stores the state directly in the URL.
176 | This a good place to start because it's very simple and works everywhere you might deploy your Shiny app.
177 | As you can imagine, however, the URL is going to get very long if you have a large number of inputs, and it's obviously not going to be able to capture an uploaded file.
178 |
179 | For these cases, you might instead want to use `enableBookmarking = "server"`, which saves the state to an `.rds` file on the server.
180 | This always generates a short, opaque, URL but requires additional storage on the server.
181 |
182 | ```{r, eval = FALSE}
183 | shinyApp(ui, server, enableBookmarking = "server")
184 | ```
185 |
186 | ```{r, echo = FALSE, message = FALSE}
187 | demo <- demoApp$new("action-bookmark/bookmark-server", ui, server, bookmark = "server")
188 | demo$deploy()
189 | ```
190 |
191 | shinyapps.io doesn't currently support server side bookmarking, so you'll need to try this out locally.
192 | If you do so, you'll see that the bookmark button generates URLs like:
193 |
194 | - `http://127.0.0.1:4087/?_state_id_=0d645f1b28f05c97`
195 | - `http://127.0.0.1:4087/?_state_id_=87b56383d8a1062c`
196 | - `http://127.0.0.1:4087/?_state_id_=c8b0291ba622b69c`
197 |
198 | Which are paired with matching directories in your working directory:
199 |
200 | - `shiny_bookmarks/0d645f1b28f05c97`
201 | - `shiny_bookmarks/87b56383d8a1062c`
202 | - `shiny_bookmarks/c8b0291ba622b69c`
203 |
204 | The main drawbacks with server bookmarking is that it requires files to be saved on the server, and it's not obvious how long these need to hang around for.
205 | If you're bookmarking complex state and you never delete these files, your app is going to take up more and more disk space over time.
206 | If you do delete the files, some old bookmarks are going to stop working.
207 |
208 | ## Bookmarking challenges
209 |
210 | Automated bookmarking relies on the reactive graph.
211 | It seeds the inputs with the saved values then replays all reactive expressions and outputs, which will yield the same app that you see, as long as your app's reactive graph is straightforward.
212 | This section briefly covers some of the cases which need a little extra care:
213 |
214 | - If your app uses random numbers, the results might be different even if all the inputs are the same.
215 | If it's really important to always generate the same numbers, you'll need to think about how to make your random process reproducible.
216 | The easiest way to do this is use `repeatable()`; see the documentation for more details.
217 |
218 | - If you have tabs and want to bookmark and restore the active tab, make sure to supply an `id` in your call to `tabsetPanel()`.
219 |
220 | - If there are inputs that should not be bookmarked, e.g. they contain private information that shouldn't be shared, include a called to `setBookmarkExclude()` somewhere in your server function.
221 | For example, `setBookmarkExclude(c("secret1", "secret2"))` will ensure that the `secret1` and `secret2` inputs are not bookmarked.
222 |
223 | - If you are manually managing reactive state in your own `reactiveValues()` object (as we'll discuss in Chapter \@ref(reactivity-components)), you'll need to use the `onBookmark()` and `onRestore()` callbacks to manually save and load your additional state.
224 | See [*Advanced Bookmarking*](https://shiny.rstudio.com/articles/advanced-bookmarking.html) for more details.
225 |
226 | ## Exercises
227 |
228 | 1. Generate app for visualising the results of [ambient::noise_simplex()](https://ambient.data-imaginist.com/reference/noise_simplex.html). Your app should allow the user to control the frequency, fractal, lacunarity, and gain, and be bookmarkable. How can you ensure the image looks exactly the same when reloaded from the bookmark? (Think about what the `seed` argument implies).
229 | 2. Make a simple app that lets you upload a csv file and then bookmark it. Upload a few files and then look in `shiny_bookmarks`. How do the files correspond to the bookmarks? (Hint: you can use `readRDS()` to look inside the cache files that Shiny is generating).
230 |
231 | ## Summary
232 |
233 | This chapter has showed how to enable bookmarking for your app.
234 | This is a great feature to provide your users because it allows them to easily share their work with others.
235 | Next, we'll talk about how to use tidy evaluation within Shiny apps.
236 | Tidy evaluation is a feature of many tidyverse functions, and you'll need to learn about it if you want to allow the user to change variables in (e.g.) dplyr pipelines or ggplot2 graphics.
237 |
--------------------------------------------------------------------------------
/action.Rmd:
--------------------------------------------------------------------------------
1 | # (PART\*) Shiny in action {.unnumbered}
2 |
3 | # Introduction {#action-intro .unnumbered}
4 |
5 | The following chapters give you a grab bag of useful techniques.
6 | I think everyone should start with Chapter \@ref(action-workflow), because it gives you important tools for developing and debugging apps, and getting help when you're stuck.
7 |
8 | After that, there's no prescribed order and relatively few connections between the chapters: I'd suggest quickly skimming to get the lay of the land (and so you might remember these tools if related problems crop up in the future), and otherwise only deeply reading the bits that you currently need.
9 | Here's a quick run down of the main topics:
10 |
11 | - Chapter \@ref(action-layout) details the various ways you can layout input and output components on a page, and how you can customise their appearance with themes.
12 |
13 | - Chapter \@ref(action-graphics) shows you how to add direct interaction to your plot and how to display images generated in other ways.
14 |
15 | - Chapter \@ref(action-feedback) covers a family of techniques (including inline errors, notifications, progress bars, and dialog boxes) for giving feedback to your users while your app runs.
16 |
17 | - Chapter \@ref(action-transfer) discusses how to transfer files to and from your app.
18 |
19 | - Chapter \@ref(action-dynamic) shows you how to dynamic modify your apps user interface while it runs.
20 |
21 | - Chapter \@ref(action-bookmark) shows how to record app state in such a way that your users can bookmark it.
22 |
23 | - Chapter \@ref(action-tidy) shows you how to allow users to select variables when working with tidyverse packages.
24 |
25 | Let's begin by working on your workflow for developing apps.
26 |
--------------------------------------------------------------------------------
/apps/shiny-test/app.R:
--------------------------------------------------------------------------------
1 | library(shiny)
2 | ui <- fluidPage(
3 | numericInput("x", "x", 0),
4 | numericInput("y", "y", 0)
5 | )
6 | server <- function(input, output, session) {
7 | observeEvent(input$x, {
8 | updateNumericInput(session, "y", value = input$x * 2)
9 | })
10 | }
11 |
12 | shinyApp(ui, server)
13 |
--------------------------------------------------------------------------------
/basic.Rmd:
--------------------------------------------------------------------------------
1 | # (PART\*) Getting started {.unnumbered}
2 |
3 | # Introduction {#basic-intro .unnumbered}
4 |
5 | The goal of the next four chapters is to get you writing Shiny apps as quickly as possible.
6 | In Chapter \@ref(basic-app), I'll start small, but complete, showing you all the major pieces of an app and how they fit together.
7 | Then in Chapters \@ref(basic-ui) and \@ref(basic-reactivity) you'll start to get into the details of the two major parts of a Shiny app: the frontend (what the user sees in the browser) and the backend (the code that makes it all work).
8 | We'll finish up in Chapter \@ref(basic-case-study) with a case study to help cement the concepts you've learned so far.
9 |
--------------------------------------------------------------------------------
/birthstones.csv:
--------------------------------------------------------------------------------
1 | month,stone
2 | January,garnet
3 | February,amethyst
4 | March,aquamarine
5 | April,diamond
6 | May,emerald
7 | June,pearl
8 | July,ruby
9 | August,peridot
10 | September,sapphire
11 | October,opal
12 | November,topaz
13 | December,tanzanite
14 |
--------------------------------------------------------------------------------
/common.R:
--------------------------------------------------------------------------------
1 | # load shiny first to avoid any conflict messages later
2 | library(shiny)
3 |
4 | # Don't run apps when knitting
5 | shinyApp <- function(...) {
6 | if (isTRUE(getOption("knitr.in.progress"))) {
7 | invisible()
8 | } else {
9 | shiny::shinyApp(...)
10 | }
11 | }
12 |
13 | knitr::opts_chunk$set(
14 | comment = "#>",
15 | collapse = TRUE,
16 | # cache = TRUE,
17 | fig.retina = 0.8, # figures are either vectors or 300 dpi diagrams
18 | dpi = 300,
19 | out.width = "70%",
20 | # fig.align = 'center',
21 | fig.width = 6,
22 | fig.asp = 0.618, # 1 / phi
23 | fig.show = "hold",
24 | eval.after = 'fig.cap' # so captions can use link to demos
25 | )
26 |
27 | options(
28 | digits = 3,
29 |
30 | # Suppress crayon since it's falsely on in GHA
31 | crayon.enabled = FALSE,
32 |
33 | # Better rlang tracebacks
34 | rlang_trace_top_env = rlang::current_env()
35 | )
36 |
37 | # In final book can go up to 81
38 | # http://oreillymedia.github.io/production-resources/styleguide/#code
39 | # See preamble.tex for tweak that makes this work in pdf output
40 | knitr::opts_chunk$set(width = 81)
41 | options(width = 81)
42 |
43 | # Reactive console simulation -------------------------------------------------
44 | # From https://github.com/rstudio/shiny/issues/2518#issuecomment-507408379
45 | reactive_console_funs <- list(
46 | reactiveVal = function(value = NULL, label = NULL) {
47 | if (missing(label)) {
48 | call <- sys.call()
49 | label <- shiny:::rvalSrcrefToLabel(attr(call, "srcref", exact = TRUE))
50 | }
51 |
52 | rv <- shiny::reactiveVal(value, label)
53 | function(x) {
54 | if (missing(x)) {
55 | rv()
56 | } else {
57 | on.exit(shiny:::flushReact(), add = TRUE, after = FALSE)
58 | rv(x)
59 | }
60 | }
61 | },
62 | reactiveValues = function(...) {
63 | rv <- shiny::reactiveValues(...)
64 | class(rv) <- c("rv_flush_on_write", class(rv))
65 | rv
66 | },
67 | `$<-.rv_flush_on_write` = function(x, name, value) {
68 | on.exit(shiny:::flushReact(), add = TRUE, after = FALSE)
69 | NextMethod()
70 | },
71 | observe = function(...) {
72 | on.exit(shiny:::flushReact(), add = TRUE, after = FALSE)
73 | shiny::observe(...)
74 | }
75 | )
76 |
77 | # override shiny::reactiveConsole() with shims that work in knitr
78 | reactiveConsole <- function(enabled = TRUE) {
79 | options(shiny.suppressMissingContextError = enabled)
80 | if (enabled) {
81 | attach(reactive_console_funs, name = "reactive_console", warn.conflicts = FALSE)
82 | vctrs::s3_register("base::$<-", "rv_flush_on_write")
83 | } else {
84 | detach("reactive_console")
85 | }
86 | }
87 |
88 | # Code extraction ---------------------------------------------------------
89 |
90 | section_get <- function(path, name) {
91 | lines <- vroom::vroom_lines(path)
92 | start <- which(grepl(paste0("^\\s*#<< ", name), lines))
93 |
94 | if (length(start) == 0) {
95 | stop("Couldn't find '#<<", name, "'", call. = FALSE)
96 | }
97 | if (length(start) > 1) {
98 | stop("Found multiple '#<< ", name, "'", call. = FALSE)
99 | }
100 |
101 | # need to build stack of #<< #>> so we can have nested components
102 |
103 | end <- which(grepl("\\s*#>>", lines))
104 | end <- end[end > start]
105 |
106 | if (length(end) == 0) {
107 | stop("Couldn't find '#>>", call. = FALSE)
108 | }
109 | end <- end[[1]]
110 |
111 | lines[(start + 1):(end - 1)]
112 | }
113 |
114 | section_strip <- function(path) {
115 | lines <- vroom::vroom_lines(path)
116 | sections <- grepl("^#(>>|<<)", lines)
117 | lines[!sections]
118 | }
119 |
120 | # Errors ------------------------------------------------------------------
121 |
122 | # Make error messages closer to base R
123 | sew.error <- function(x, options) {
124 | msg <- conditionMessage(x)
125 |
126 | call <- conditionCall(x)
127 | if (is.null(call)) {
128 | msg <- paste0("Error: ", msg)
129 | } else {
130 | msg <- paste0("Error in ", deparse(call)[[1]], ": ", msg)
131 | }
132 |
133 | msg <- error_wrap(msg)
134 | knitr:::msg_wrap(msg, "error", options)
135 | }
136 |
137 | error_wrap <- function(x, width = getOption("width")) {
138 | lines <- strsplit(x, "\n", fixed = TRUE)[[1]]
139 | paste(strwrap(lines, width = width), collapse = "\n")
140 | }
141 |
142 |
--------------------------------------------------------------------------------
/contributors.csv:
--------------------------------------------------------------------------------
1 | login,n,name,blog
2 | 1wheel,1,Adam Pearce,https://roadtolarissa.com/
3 | adisarid,1,Adi Sarid,https://adisarid.github.io/
4 | alex-m-ffm,1,Alexandros Melemenidis,NA
5 | antonvsdata,1,Anton Klåvus,NA
6 | betsyrosalen,1,Betsy Rosalen,NA
7 | brooklynbagel,1,Michael Beigelmacher,NA
8 | BSCowboy,2,Bryan Smith,NA
9 | c1au6i0,1,c1au6io_hh,NA
10 | canovasjm,1,NA,NA
11 | ChrisBeeley,1,Chris Beeley,chrisbeeley.net
12 | chsafouane,19,NA,NA
13 | ChuliangXiao,1,Chuliang Xiao,https://cxiao.netlify.com
14 | condwanaland,1,Conor Neilson,NA
15 | d-edison,1,NA,NA
16 | daattali,5,Dean Attali,https://attalitech.com
17 | Danieldavid521,3,DanielDavid521,http://persiv.xyz
18 | DivadNojnarg,1,David Granjon,divadnojnarg.github.io
19 | edovtp,7,Eduardo Vásquez,NA
20 | EmilHvitfeldt,1,Emil Hvitfeldt,https://www.hvitfeldt.me/
21 | emilopezcano,8,Emilio,http://emilio.lcano.com
22 | emilyriederer,1,Emily Riederer,https://emilyriederer.netlify.com/
23 | esimms999,2,Eric Simms,NA
24 | federicomarini,2,Federico Marini,NA
25 | fkoh111,1,Frederik Kok Hansen,fkhonline.net
26 | FvD,1,Frans van Dunné,www.fransvandunne.com
27 | giocomai,1,Giorgio Comai,https://giorgiocomai.eu/
28 | hadley,628,Hadley Wickham,http://hadley.nz
29 | heds1,2,Hedley,NA
30 | henningsway,1,Henning,https://henningsway.rbind.io
31 | hlynurhallgrims,3,Hlynur,NA
32 | hsm207,4,NA,https://bit.ly/2SmH0uG
33 | jacobxk,1,NA,NA
34 | jamespooley,16,James Pooley,https://jamespooley.github.io/
35 | jcheng5,44,Joe Cheng,NA
36 | jcolomb,1,Julien Colomb,https://orcid.org/0000-0002-3127-5520
37 | jcrodriguez1989,1,Juan C Rodriguez,https://jcrodriguez.rbind.io/
38 | jennybc,1,Jennifer (Jenny) Bryan,https://jennybryan.org
39 | jimhester,1,Jim Hester,http://www.jimhester.com
40 | joachim-gassen,1,Joachim Gassen,NA
41 | jonmcalder,4,Jon Calder,http://joncalder.co.za
42 | jonocarroll,1,Jonathan Carroll,https://www.jcarroll.com.au
43 | julianstanley,1,Julian Stanley,https://www.julianstanley.com
44 | jyuu,1,NA,https://jcahoon.netlify.com/
45 | kaanpekel,1,NA,NA
46 | kdpsingh,1,Karandeep Singh,kdpsingh.lab.medicine.umich.edu
47 | KirkDCO,1,Robert Kirk DeLisle,NA
48 | loomalaine,1,Elaine,NA
49 | malcolmbarrett,14,Malcolm Barrett,malco.io
50 | marlycormar,1,Marly Gotti,www.marlygotti.com
51 | MattW-Geospatial,1,Matthew Wilson,https://geospatial.ac.nz/
52 | mattwarkentin,2,Matthew T. Warkentin,NA
53 | maurolepore,16,Mauro Lepore,https://maurolepore.github.io
54 | maxdrohde,1,Maximilian Rohde,maximilianrohde.com
55 | mbergins,1,Matthew Berginski,matthew.berginski.com
56 | michael-dewar,3,Michael Dewar,NA
57 | mine-cetinkaya-rundel,3,Mine Cetinkaya-Rundel,http://mine-cr.com
58 | mpaulacaldas,2,Maria Paula Caldas,mpaulacaldas.com
59 | nthobservation,1,nthobservation,NA
60 | pitmonticone,1,Pietro Monticone,NA
61 | psychometrician,5,psychometrician,NA
62 | raamthapa,1,Ram Thapa,NA
63 | rappster,1,Janko Thyson,http://rappster.wordpress.com
64 | rbjanis,1,Rebecca Janis,www.thewayir.com
65 | remlapmot,1,Tom Palmer,https://research-information.bris.ac.uk/en/persons/tom-m-palmer(bd750b43-03d6-4e2e-af74-c24eb2bf5c5a).html
66 | russHyde,1,Russ Hyde,NA
67 | schloerke,1,Barret Schloerke,schloerke.com
68 | scottyd22,8,Scott,datascott.com
69 | sedaghatfar,2,Matthew Sedaghatfar,Tevunah.com
70 | ShixiangWang,1,Shixiang Wang,https://shixiangwang.github.io/
71 | sowla,2,Praer (Suthira Owlarn),www.twitter.com/s_owla
72 | statnmap,1,Sébastien Rochette,https://statnmap.com
73 | stevensbr,3,NA,NA
74 | Sumidu,1,André Calero Valdez,http://www.calerovaldez.com
75 | tmstauss,11,Tanner Stauss,www.linkedin.com/in/tanner-stauss
76 | tonyfujs,1,Tony Fujs,NA
77 | trekonom,3,Stefan Moog,NA
78 | trestletech,1,Jeff Allen,https://trestletech.com
79 | treygilliland,1,Trey Gilliland,www.treygill.com
80 | Tungurahua,1,Albrecht,NA
81 | ValeriVoev,3,Valeri Voev,NA
82 | Vickusr,1,Vickus,NA
83 | WilDoane,1,William Doane,http://DrDoane.com
84 | XiangyunHuang,1,黄湘云,https://xiangyun.rbind.io/
85 | xwydq,1,gXcloud,NA
86 |
--------------------------------------------------------------------------------
/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/cover.png
--------------------------------------------------------------------------------
/demo-app.R:
--------------------------------------------------------------------------------
1 | library(shiny)
2 | attach(readRDS("data.rds"))
3 |
4 | lapply(`_packages`, library, character.only = TRUE)
5 | if (exists("_before")) {
6 | `_before`()
7 | }
8 |
9 | if (!exists("_server")) {
10 | `_server` <- function(input, output, session) {}
11 | }
12 |
13 | shinyApp(`_ui`, `_server`, enableBookmarking = `_bookmark`)
14 |
--------------------------------------------------------------------------------
/demo.R:
--------------------------------------------------------------------------------
1 | shiny:::withPrivateSeed(set.seed(100))
2 |
3 | #' @examples
4 | #' library(shiny)
5 | #' ui <- fluidPage("Hello world!")
6 | #' app <- demoApp$new("hello-world", ui)
7 | #' app$running
8 | #' app$reset()
9 | #' app$resize(100)$takeScreenshot("test-100")
10 | #' app$resize(600)$takeScreenshot("test-600")
11 | #' app$deploy()
12 |
13 |
14 | demoApp <- R6::R6Class("demoApp", public = list(
15 | name = character(),
16 | ui = NULL,
17 | server = NULL,
18 | data = NULL,
19 | assets = NULL,
20 | before = NULL,
21 |
22 | running = FALSE,
23 | driver = NULL,
24 |
25 | initialize = function(name, ui,
26 | server = NULL,
27 | packages = character(),
28 | assets = NULL,
29 | bookmark = NULL,
30 | before = NULL,
31 | env = parent.frame()
32 | ) {
33 | self$name <- name
34 | self$ui <- ui
35 | self$server <- rlang::zap_srcref(server)
36 | self$data <- app_data(server, ui, packages, bookmark, env, before)
37 | self$assets <- assets
38 |
39 | fs::dir_create(fs::path("demos", fs::path_dir(name)))
40 | self$run()
41 | },
42 |
43 | run = function() {
44 | self$running <- rlang::is_interactive() || (!is_ci() && self$outdated())
45 | if (!self$running) {
46 | return()
47 | }
48 |
49 | saveRDS(self$data, self$path("rds"))
50 |
51 | rlang::inform("Starting ShinyDriver")
52 | self$driver <- shinytest::ShinyDriver$new(self$saveApp())
53 | self$resize(600)
54 | },
55 |
56 | saveApp = function(path = tempfile()) {
57 | dir.create(path)
58 | if (!is.null(self$assets)) {
59 | file.copy(self$assets, path, recursive = TRUE)
60 | }
61 |
62 | if (length(self$data$`_packages`) > 0) {
63 | # Add extra dependencies in a way that rsconnect understands
64 | writeLines(
65 | paste0("library(", self$data$`_packages`, ")"),
66 | file.path(path, "deps.R")
67 | )
68 | }
69 |
70 | file.copy("demo-app.R", file.path(path, "app.R"))
71 | saveRDS(self$data, file.path(path, "data.rds"))
72 | path
73 | },
74 |
75 | reset = function() {
76 | self$finalize()
77 | fs::file_delete(self$path("rds"))
78 | self$run()
79 | },
80 |
81 | outdated = function() {
82 | if (!file.exists(self$path("rds"))) {
83 | rlang::inform(paste0("Initialising ", self$name))
84 | return(TRUE)
85 | }
86 | data_old <- readRDS(self$path("rds"))
87 |
88 | diff <- waldo::compare(
89 | data_old,
90 | self$data,
91 | x_arg = "old",
92 | y_arg = "new",
93 | ignore_function_env = TRUE # ignore varying env from theme function
94 | )
95 | if (length(diff) == 0) {
96 | FALSE
97 | } else {
98 | rlang::inform(paste0(
99 | self$name, " has changed:\n",
100 | paste0(diff, collapse = "\n\n")
101 | ))
102 | TRUE
103 | }
104 | },
105 |
106 | path = function(ext, name = NULL) {
107 | if (is.null(name)) {
108 | fs::path("demos", self$name, ext = ext)
109 | } else {
110 | fs::path("demos", paste0(self$name, "-", name), ext = ext)
111 | }
112 | },
113 |
114 | resize = function(width, height = NULL) {
115 | if (self$running) {
116 | if (!is.null(height)) {
117 | self$driver$setWindowSize(width, height)
118 | } else {
119 | self$driver$setWindowSize(width, 100)
120 | height <- self$driver$findElement("body")$getRect()$height
121 | self$driver$setWindowSize(width, height)
122 | }
123 | self$driver$waitForShiny()
124 | }
125 | invisible(self)
126 | },
127 |
128 | setInputs = function(...) {
129 | if (self$running) {
130 | self$driver$setInputs(...)
131 | }
132 | invisible(self)
133 | },
134 |
135 | sendKeys = function(name, keys) {
136 | if (self$running) {
137 | self$driver$sendKeys(name, keys)
138 | }
139 | invisible(self)
140 | },
141 | click = function(id) {
142 | if (self$running) {
143 | self$driver$click(id)
144 | }
145 | invisible(self)
146 | },
147 |
148 | uploadFile = function(...) {
149 | if (self$running) {
150 | self$driver$uploadFile(...)
151 | }
152 | invisible(self)
153 | },
154 |
155 | wait = function() {
156 | if (self$running) {
157 | self$driver$waitForShiny()
158 | }
159 | invisible(self)
160 | },
161 |
162 | execute_js = function(js) {
163 | if (self$running) {
164 | self$driver$executeScript(js)
165 | }
166 | invisible(self)
167 | },
168 |
169 | dropDown = function(id, pos = NULL) {
170 | js <- glue::glue('
171 | $("#{id}")
172 | .siblings()
173 | .filter(".selectize-control")
174 | .find(".selectize-input")
175 | .click();
176 | ')
177 | self$execute_js(js)
178 |
179 | if (!is.null(pos)) {
180 | js <- glue::glue('
181 | $($("#{id}")
182 | .siblings()
183 | .filter(".selectize-control")
184 | .find(".selectize-dropdown-content")
185 | .children()
186 | .get({pos - 1}))
187 | .mouseenter();
188 | ')
189 | self$execute_js(js)
190 | }
191 |
192 | invisible(self)
193 | },
194 |
195 | takeScreenshot = function(name = NULL, id = NULL, parent = FALSE) {
196 | path <- self$path("png", name)
197 | if (self$running) {
198 | rlang::inform("Taking screenshot")
199 | self$driver$takeScreenshot(path, id = id, parent = parent)
200 | } else {
201 | if (!fs::file_exists(path)) {
202 | stop("'", path, "' doesn't exist and app isn't running", call. = FALSE)
203 | }
204 | }
205 |
206 | knitr::include_graphics(path, dpi = screenshot_dpi())
207 | },
208 |
209 | finalize = function() {
210 | if (self$running) {
211 | self$driver$stop()
212 | self$running <- FALSE
213 | }
214 | },
215 |
216 | launch = function() {
217 | if (self$running) {
218 | browseURL(demo$driver$getUrl())
219 | }
220 | },
221 |
222 | deploy = function(quiet = TRUE) {
223 | if (self$running) {
224 | name <- fs::path_file(self$name)
225 | rlang::inform(paste0("Deploying ", name, " to shinyapps.io"))
226 | if (!requireNamespace("rsconnect", quietly = TRUE)) {
227 | return(invisible(self))
228 | }
229 |
230 | rsconnect::deployApp(
231 | appDir = self$saveApp(),
232 | appName = paste0("ms-", name),
233 | appTitle = paste0("Mastering Shiny: ", name),
234 | server = "shinyapps.io",
235 | forceUpdate = TRUE,
236 | logLevel = if (quiet) "quiet" else "normal",
237 | launch.browser = FALSE
238 | )
239 | }
240 |
241 | invisible(self)
242 | },
243 |
244 | link = function() {
245 | paste0("")
246 | },
247 |
248 | figure = function() {
249 | paste0("Figure \\@ref(fig:", self$name, ")")
250 | },
251 |
252 | caption = function(text = NULL) {
253 | paste0(
254 | text, if (!is.null(text)) " ",
255 | "See live at ", self$link(), "."
256 | )
257 | }
258 | ))
259 |
260 | # server + ui -> app ------------------------------------------------------
261 |
262 | app_data <- function(server, ui, packages = character(), bookmark = NULL, env = parent.frame(), before = NULL) {
263 | globals <- app_server_globals(server, env)
264 |
265 | data <- globals$globals
266 | data$`_ui` <- ui
267 | data$`_server` <- server
268 | data$`_packages` <- union(globals$packages, packages)
269 | data["_bookmark"] <- list(bookmark) # NULLs grrrrr
270 | data$`_before` <- if (!is.null(before)) {
271 | rlang::as_function(before, env = global_env())
272 | }
273 |
274 | data
275 | }
276 |
277 | app_server_globals <- function(server, env = parent.frame()) {
278 | # Work around for https://github.com/HenrikBengtsson/globals/issues/61
279 | env <- new.env(parent = env)
280 | env$output <- NULL
281 |
282 | globals <- globals::globalsOf(server, envir = env, recursive = FALSE, mustExist = FALSE)
283 | globals <- globals::cleanup(globals)
284 |
285 | # remove globals found in packages
286 | pkgs <- globals::packagesOf(globals)
287 | in_package <- vapply(
288 | attr(globals, "where"),
289 | function(x) !is.null(attr(x, "name")),
290 | logical(1)
291 | )
292 | globals <- globals[!in_package]
293 | attributes(globals) <- list(names = names(globals))
294 |
295 | # https://github.com/HenrikBengtsson/globals/issues/61
296 | globals$output <- NULL
297 |
298 | list(
299 | globals = globals,
300 | packages = pkgs
301 | )
302 | }
303 |
304 | # Helpers -----------------------------------------------------------------
305 |
306 | # Controls the size of automated shiny screenshots via app_screenshot().
307 | # I don't understand why these values need to be different, they've been
308 | # determined empirically.
309 | screenshot_dpi <- function() {
310 | if (knitr::is_latex_output()) {
311 | 120
312 | } else {
313 | 96
314 | }
315 | }
316 |
317 | is_ci <- function() isTRUE(as.logical(Sys.getenv("CI")))
318 |
--------------------------------------------------------------------------------
/demos/action-bookmark/bookmark-auto.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-bookmark/bookmark-auto.rds
--------------------------------------------------------------------------------
/demos/action-bookmark/bookmark-server.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-bookmark/bookmark-server.rds
--------------------------------------------------------------------------------
/demos/action-bookmark/bookmark-url.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-bookmark/bookmark-url.rds
--------------------------------------------------------------------------------
/demos/action-bookmark/pendulum.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-bookmark/pendulum.png
--------------------------------------------------------------------------------
/demos/action-bookmark/pendulum.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-bookmark/pendulum.rds
--------------------------------------------------------------------------------
/demos/action-dynamic/dynamic-conditional-exponential.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/dynamic-conditional-exponential.png
--------------------------------------------------------------------------------
/demos/action-dynamic/dynamic-conditional-normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/dynamic-conditional-normal.png
--------------------------------------------------------------------------------
/demos/action-dynamic/dynamic-conditional-uniform.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/dynamic-conditional-uniform.png
--------------------------------------------------------------------------------
/demos/action-dynamic/dynamic-conditional.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/dynamic-conditional.rds
--------------------------------------------------------------------------------
/demos/action-dynamic/dynamic-panels-panel2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/dynamic-panels-panel2.png
--------------------------------------------------------------------------------
/demos/action-dynamic/dynamic-panels-panel3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/dynamic-panels-panel3.png
--------------------------------------------------------------------------------
/demos/action-dynamic/dynamic-panels.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/dynamic-panels.png
--------------------------------------------------------------------------------
/demos/action-dynamic/dynamic-panels.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/dynamic-panels.rds
--------------------------------------------------------------------------------
/demos/action-dynamic/filtering-final.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/filtering-final.png
--------------------------------------------------------------------------------
/demos/action-dynamic/filtering-final.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/filtering-final.rds
--------------------------------------------------------------------------------
/demos/action-dynamic/freeze.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/freeze.rds
--------------------------------------------------------------------------------
/demos/action-dynamic/render-filter-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/render-filter-1.png
--------------------------------------------------------------------------------
/demos/action-dynamic/render-filter-1.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/render-filter-1.rds
--------------------------------------------------------------------------------
/demos/action-dynamic/render-filter-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/render-filter-2.png
--------------------------------------------------------------------------------
/demos/action-dynamic/render-filter-2.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/render-filter-2.rds
--------------------------------------------------------------------------------
/demos/action-dynamic/render-palette-change-n.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/render-palette-change-n.png
--------------------------------------------------------------------------------
/demos/action-dynamic/render-palette-full-change-n.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/render-palette-full-change-n.png
--------------------------------------------------------------------------------
/demos/action-dynamic/render-palette-full-rainbow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/render-palette-full-rainbow.png
--------------------------------------------------------------------------------
/demos/action-dynamic/render-palette-full.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/render-palette-full.rds
--------------------------------------------------------------------------------
/demos/action-dynamic/render-palette-onload.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/render-palette-onload.png
--------------------------------------------------------------------------------
/demos/action-dynamic/render-palette-set-cols.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/render-palette-set-cols.png
--------------------------------------------------------------------------------
/demos/action-dynamic/render-palette.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/render-palette.rds
--------------------------------------------------------------------------------
/demos/action-dynamic/render-simple-label.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/render-simple-label.png
--------------------------------------------------------------------------------
/demos/action-dynamic/render-simple-numeric.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/render-simple-numeric.png
--------------------------------------------------------------------------------
/demos/action-dynamic/render-simple-onload.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/render-simple-onload.png
--------------------------------------------------------------------------------
/demos/action-dynamic/render-simple.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/render-simple.rds
--------------------------------------------------------------------------------
/demos/action-dynamic/temperature.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/temperature.rds
--------------------------------------------------------------------------------
/demos/action-dynamic/update-basics-max-increase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/update-basics-max-increase.png
--------------------------------------------------------------------------------
/demos/action-dynamic/update-basics-min-decrease.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/update-basics-min-decrease.png
--------------------------------------------------------------------------------
/demos/action-dynamic/update-basics-onload.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/update-basics-onload.png
--------------------------------------------------------------------------------
/demos/action-dynamic/update-basics.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/update-basics.rds
--------------------------------------------------------------------------------
/demos/action-dynamic/update-button-onload.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/update-button-onload.png
--------------------------------------------------------------------------------
/demos/action-dynamic/update-button-set1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/update-button-set1.png
--------------------------------------------------------------------------------
/demos/action-dynamic/update-button-set100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/update-button-set100.png
--------------------------------------------------------------------------------
/demos/action-dynamic/update-button.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/update-button.rds
--------------------------------------------------------------------------------
/demos/action-dynamic/update-nested-customername.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/update-nested-customername.png
--------------------------------------------------------------------------------
/demos/action-dynamic/update-nested-orders.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/update-nested-orders.png
--------------------------------------------------------------------------------
/demos/action-dynamic/update-nested-territory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/update-nested-territory.png
--------------------------------------------------------------------------------
/demos/action-dynamic/update-nested.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/update-nested.rds
--------------------------------------------------------------------------------
/demos/action-dynamic/update-reset-onload.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/update-reset-onload.png
--------------------------------------------------------------------------------
/demos/action-dynamic/update-reset-reset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/update-reset-reset.png
--------------------------------------------------------------------------------
/demos/action-dynamic/update-reset-set.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/update-reset-set.png
--------------------------------------------------------------------------------
/demos/action-dynamic/update-reset.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/update-reset.rds
--------------------------------------------------------------------------------
/demos/action-dynamic/wizard-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/wizard-1.png
--------------------------------------------------------------------------------
/demos/action-dynamic/wizard-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/wizard-2.png
--------------------------------------------------------------------------------
/demos/action-dynamic/wizard-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/wizard-3.png
--------------------------------------------------------------------------------
/demos/action-dynamic/wizard.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-dynamic/wizard.rds
--------------------------------------------------------------------------------
/demos/action-feedback/dialog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/dialog.png
--------------------------------------------------------------------------------
/demos/action-feedback/dialog.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/dialog.rds
--------------------------------------------------------------------------------
/demos/action-feedback/feedback-even.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/feedback-even.png
--------------------------------------------------------------------------------
/demos/action-feedback/feedback-odd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/feedback-odd.png
--------------------------------------------------------------------------------
/demos/action-feedback/feedback.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/feedback.rds
--------------------------------------------------------------------------------
/demos/action-feedback/notification-transient.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/notification-transient.rds
--------------------------------------------------------------------------------
/demos/action-feedback/notification-updates.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/notification-updates.rds
--------------------------------------------------------------------------------
/demos/action-feedback/notify-persistent.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/notify-persistent.rds
--------------------------------------------------------------------------------
/demos/action-feedback/progress.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/progress.rds
--------------------------------------------------------------------------------
/demos/action-feedback/require-cancel-empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/require-cancel-empty.png
--------------------------------------------------------------------------------
/demos/action-feedback/require-cancel-error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/require-cancel-error.png
--------------------------------------------------------------------------------
/demos/action-feedback/require-cancel-ok.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/require-cancel-ok.png
--------------------------------------------------------------------------------
/demos/action-feedback/require-cancel.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/require-cancel.rds
--------------------------------------------------------------------------------
/demos/action-feedback/require-simple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/require-simple.png
--------------------------------------------------------------------------------
/demos/action-feedback/require-simple.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/require-simple.rds
--------------------------------------------------------------------------------
/demos/action-feedback/require-simple2-langauge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/require-simple2-langauge.png
--------------------------------------------------------------------------------
/demos/action-feedback/require-simple2-name.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/require-simple2-name.png
--------------------------------------------------------------------------------
/demos/action-feedback/require-simple2-on-load.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/require-simple2-on-load.png
--------------------------------------------------------------------------------
/demos/action-feedback/require-simple2.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/require-simple2.rds
--------------------------------------------------------------------------------
/demos/action-feedback/spinner-1.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/spinner-1.rds
--------------------------------------------------------------------------------
/demos/action-feedback/spinner-2.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/spinner-2.rds
--------------------------------------------------------------------------------
/demos/action-feedback/undo.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/undo.rds
--------------------------------------------------------------------------------
/demos/action-feedback/validate-init.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/validate-init.png
--------------------------------------------------------------------------------
/demos/action-feedback/validate-log.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/validate-log.png
--------------------------------------------------------------------------------
/demos/action-feedback/validate.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/validate.rds
--------------------------------------------------------------------------------
/demos/action-feedback/waiter.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-feedback/waiter.rds
--------------------------------------------------------------------------------
/demos/action-graphics/brushedPoints.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-graphics/brushedPoints.rds
--------------------------------------------------------------------------------
/demos/action-graphics/click.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-graphics/click.rds
--------------------------------------------------------------------------------
/demos/action-graphics/modifying-size.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-graphics/modifying-size.rds
--------------------------------------------------------------------------------
/demos/action-graphics/nearPoints.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-graphics/nearPoints.rds
--------------------------------------------------------------------------------
/demos/action-graphics/persistent.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-graphics/persistent.rds
--------------------------------------------------------------------------------
/demos/action-graphics/puppies-corgi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-graphics/puppies-corgi.png
--------------------------------------------------------------------------------
/demos/action-graphics/puppies-lab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-graphics/puppies-lab.png
--------------------------------------------------------------------------------
/demos/action-graphics/puppies.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-graphics/puppies.rds
--------------------------------------------------------------------------------
/demos/action-graphics/resize-narrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-graphics/resize-narrow.png
--------------------------------------------------------------------------------
/demos/action-graphics/resize-wide.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-graphics/resize-wide.png
--------------------------------------------------------------------------------
/demos/action-graphics/resize.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-graphics/resize.rds
--------------------------------------------------------------------------------
/demos/action-layout/navbarPage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/navbarPage.png
--------------------------------------------------------------------------------
/demos/action-layout/navbarPage.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/navbarPage.rds
--------------------------------------------------------------------------------
/demos/action-layout/navlistPanel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/navlistPanel.png
--------------------------------------------------------------------------------
/demos/action-layout/navlistPanel.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/navlistPanel.rds
--------------------------------------------------------------------------------
/demos/action-layout/sidebar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/sidebar.png
--------------------------------------------------------------------------------
/demos/action-layout/sidebar.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/sidebar.rds
--------------------------------------------------------------------------------
/demos/action-layout/tabset-input-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/tabset-input-1.png
--------------------------------------------------------------------------------
/demos/action-layout/tabset-input-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/tabset-input-2.png
--------------------------------------------------------------------------------
/demos/action-layout/tabset-input.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/tabset-input.rds
--------------------------------------------------------------------------------
/demos/action-layout/tabset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/tabset.png
--------------------------------------------------------------------------------
/demos/action-layout/tabset.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/tabset.rds
--------------------------------------------------------------------------------
/demos/action-layout/thematic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/thematic.png
--------------------------------------------------------------------------------
/demos/action-layout/thematic.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/thematic.rds
--------------------------------------------------------------------------------
/demos/action-layout/theme-darkly.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/theme-darkly.png
--------------------------------------------------------------------------------
/demos/action-layout/theme-darkly.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/theme-darkly.rds
--------------------------------------------------------------------------------
/demos/action-layout/theme-flatly.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/theme-flatly.png
--------------------------------------------------------------------------------
/demos/action-layout/theme-flatly.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/theme-flatly.rds
--------------------------------------------------------------------------------
/demos/action-layout/theme-sandstone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/theme-sandstone.png
--------------------------------------------------------------------------------
/demos/action-layout/theme-sandstone.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/theme-sandstone.rds
--------------------------------------------------------------------------------
/demos/action-layout/theme-united.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/theme-united.png
--------------------------------------------------------------------------------
/demos/action-layout/theme-united.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-layout/theme-united.rds
--------------------------------------------------------------------------------
/demos/action-tidy/across.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-tidy/across.png
--------------------------------------------------------------------------------
/demos/action-tidy/across.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-tidy/across.rds
--------------------------------------------------------------------------------
/demos/action-tidy/dplyr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-tidy/dplyr.png
--------------------------------------------------------------------------------
/demos/action-tidy/dplyr.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-tidy/dplyr.rds
--------------------------------------------------------------------------------
/demos/action-tidy/ggplot2-scatter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-tidy/ggplot2-scatter.png
--------------------------------------------------------------------------------
/demos/action-tidy/ggplot2-swarm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-tidy/ggplot2-swarm.png
--------------------------------------------------------------------------------
/demos/action-tidy/ggplot2.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-tidy/ggplot2.rds
--------------------------------------------------------------------------------
/demos/action-tidy/messed-up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-tidy/messed-up.png
--------------------------------------------------------------------------------
/demos/action-tidy/messed-up.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-tidy/messed-up.rds
--------------------------------------------------------------------------------
/demos/action-tidy/tidied-up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-tidy/tidied-up.png
--------------------------------------------------------------------------------
/demos/action-tidy/tidied-up.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-tidy/tidied-up.rds
--------------------------------------------------------------------------------
/demos/action-tidy/user-supplied.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-tidy/user-supplied.png
--------------------------------------------------------------------------------
/demos/action-tidy/user-supplied.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-tidy/user-supplied.rds
--------------------------------------------------------------------------------
/demos/action-transfer/case-study.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-transfer/case-study.png
--------------------------------------------------------------------------------
/demos/action-transfer/case-study.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-transfer/case-study.rds
--------------------------------------------------------------------------------
/demos/action-transfer/download-data.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-transfer/download-data.png
--------------------------------------------------------------------------------
/demos/action-transfer/download-data.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-transfer/download-data.rds
--------------------------------------------------------------------------------
/demos/action-transfer/download-rmd.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-transfer/download-rmd.rds
--------------------------------------------------------------------------------
/demos/action-transfer/download.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-transfer/download.png
--------------------------------------------------------------------------------
/demos/action-transfer/download.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-transfer/download.rds
--------------------------------------------------------------------------------
/demos/action-transfer/upload-validate.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-transfer/upload-validate.rds
--------------------------------------------------------------------------------
/demos/action-transfer/upload.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-transfer/upload.png
--------------------------------------------------------------------------------
/demos/action-transfer/upload.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/action-transfer/upload.rds
--------------------------------------------------------------------------------
/demos/basic-app/ex-x-times-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-app/ex-x-times-5.png
--------------------------------------------------------------------------------
/demos/basic-app/ex-x-times-5.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-app/ex-x-times-5.rds
--------------------------------------------------------------------------------
/demos/basic-app/ex-x-times-y.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-app/ex-x-times-y.png
--------------------------------------------------------------------------------
/demos/basic-app/ex-x-times-y.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-app/ex-x-times-y.rds
--------------------------------------------------------------------------------
/demos/basic-app/server.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-app/server.png
--------------------------------------------------------------------------------
/demos/basic-app/server.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-app/server.rds
--------------------------------------------------------------------------------
/demos/basic-app/ui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-app/ui.png
--------------------------------------------------------------------------------
/demos/basic-app/ui.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-app/ui.rds
--------------------------------------------------------------------------------
/demos/basic-case-study/narrative.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-case-study/narrative.png
--------------------------------------------------------------------------------
/demos/basic-case-study/polish-tables.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-case-study/polish-tables.png
--------------------------------------------------------------------------------
/demos/basic-case-study/prototype.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-case-study/prototype.png
--------------------------------------------------------------------------------
/demos/basic-case-study/rate-vs-count.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-case-study/rate-vs-count.png
--------------------------------------------------------------------------------
/demos/basic-reactivity/action-button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-reactivity/action-button.png
--------------------------------------------------------------------------------
/demos/basic-reactivity/action-button.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-reactivity/action-button.rds
--------------------------------------------------------------------------------
/demos/basic-reactivity/case-study-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-reactivity/case-study-1.png
--------------------------------------------------------------------------------
/demos/basic-reactivity/case-study-1.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-reactivity/case-study-1.rds
--------------------------------------------------------------------------------
/demos/basic-reactivity/connection-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-reactivity/connection-1.png
--------------------------------------------------------------------------------
/demos/basic-reactivity/connection-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-reactivity/connection-2.png
--------------------------------------------------------------------------------
/demos/basic-reactivity/connection-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-reactivity/connection-3.png
--------------------------------------------------------------------------------
/demos/basic-reactivity/connection.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-reactivity/connection.rds
--------------------------------------------------------------------------------
/demos/basic-reactivity/simulation-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-reactivity/simulation-2.png
--------------------------------------------------------------------------------
/demos/basic-reactivity/simulation-2.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-reactivity/simulation-2.rds
--------------------------------------------------------------------------------
/demos/basic-ui/action-css.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/action-css.png
--------------------------------------------------------------------------------
/demos/basic-ui/action-css.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/action-css.rds
--------------------------------------------------------------------------------
/demos/basic-ui/action.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/action.png
--------------------------------------------------------------------------------
/demos/basic-ui/action.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/action.rds
--------------------------------------------------------------------------------
/demos/basic-ui/date-slider.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/date-slider.png
--------------------------------------------------------------------------------
/demos/basic-ui/date-slider.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/date-slider.rds
--------------------------------------------------------------------------------
/demos/basic-ui/date.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/date.png
--------------------------------------------------------------------------------
/demos/basic-ui/date.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/date.rds
--------------------------------------------------------------------------------
/demos/basic-ui/free-text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/free-text.png
--------------------------------------------------------------------------------
/demos/basic-ui/free-text.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/free-text.rds
--------------------------------------------------------------------------------
/demos/basic-ui/limited-choices.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/limited-choices.png
--------------------------------------------------------------------------------
/demos/basic-ui/limited-choices.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/limited-choices.rds
--------------------------------------------------------------------------------
/demos/basic-ui/multi-radio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/multi-radio.png
--------------------------------------------------------------------------------
/demos/basic-ui/multi-radio.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/multi-radio.rds
--------------------------------------------------------------------------------
/demos/basic-ui/numeric.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/numeric.png
--------------------------------------------------------------------------------
/demos/basic-ui/numeric.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/numeric.rds
--------------------------------------------------------------------------------
/demos/basic-ui/output-plot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/output-plot.png
--------------------------------------------------------------------------------
/demos/basic-ui/output-plot.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/output-plot.rds
--------------------------------------------------------------------------------
/demos/basic-ui/output-table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/output-table.png
--------------------------------------------------------------------------------
/demos/basic-ui/output-table.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/output-table.rds
--------------------------------------------------------------------------------
/demos/basic-ui/output-text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/output-text.png
--------------------------------------------------------------------------------
/demos/basic-ui/output-text.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/output-text.rds
--------------------------------------------------------------------------------
/demos/basic-ui/placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/placeholder.png
--------------------------------------------------------------------------------
/demos/basic-ui/placeholder.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/placeholder.rds
--------------------------------------------------------------------------------
/demos/basic-ui/radio-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/radio-icon.png
--------------------------------------------------------------------------------
/demos/basic-ui/radio-icon.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/radio-icon.rds
--------------------------------------------------------------------------------
/demos/basic-ui/text-vs-print.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/text-vs-print.png
--------------------------------------------------------------------------------
/demos/basic-ui/text-vs-print.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/text-vs-print.rds
--------------------------------------------------------------------------------
/demos/basic-ui/upload.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/upload.png
--------------------------------------------------------------------------------
/demos/basic-ui/upload.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/upload.rds
--------------------------------------------------------------------------------
/demos/basic-ui/yes-no.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/yes-no.png
--------------------------------------------------------------------------------
/demos/basic-ui/yes-no.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/basic-ui/yes-no.rds
--------------------------------------------------------------------------------
/demos/scaling-modules/radio-extra-ui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/scaling-modules/radio-extra-ui.png
--------------------------------------------------------------------------------
/demos/scaling-modules/radio-extra-ui.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/scaling-modules/radio-extra-ui.rds
--------------------------------------------------------------------------------
/demos/scaling-modules/radio-extra.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/scaling-modules/radio-extra.png
--------------------------------------------------------------------------------
/demos/scaling-modules/radio-extra.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/scaling-modules/radio-extra.rds
--------------------------------------------------------------------------------
/demos/scaling-modules/wizard-module-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/scaling-modules/wizard-module-1.png
--------------------------------------------------------------------------------
/demos/scaling-modules/wizard-module-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/scaling-modules/wizard-module-2.png
--------------------------------------------------------------------------------
/demos/scaling-modules/wizard-module-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/scaling-modules/wizard-module-3.png
--------------------------------------------------------------------------------
/demos/scaling-modules/wizard-module.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/scaling-modules/wizard-module.rds
--------------------------------------------------------------------------------
/demos/scaling-modules/wizard-ui-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/scaling-modules/wizard-ui-1.png
--------------------------------------------------------------------------------
/demos/scaling-modules/wizard-ui-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/scaling-modules/wizard-ui-2.png
--------------------------------------------------------------------------------
/demos/scaling-modules/wizard-ui-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/scaling-modules/wizard-ui-3.png
--------------------------------------------------------------------------------
/demos/scaling-modules/wizard-ui.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/demos/scaling-modules/wizard-ui.rds
--------------------------------------------------------------------------------
/diagrams/action-layout.graffle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/action-layout.graffle
--------------------------------------------------------------------------------
/diagrams/action-layout/multirow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/action-layout/multirow.png
--------------------------------------------------------------------------------
/diagrams/action-layout/sidebar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/action-layout/sidebar.png
--------------------------------------------------------------------------------
/diagrams/basic-case-study.graffle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/basic-case-study.graffle
--------------------------------------------------------------------------------
/diagrams/basic-reactivity.graffle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/basic-reactivity.graffle
--------------------------------------------------------------------------------
/diagrams/basic-reactivity/case-study-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/basic-reactivity/case-study-1.png
--------------------------------------------------------------------------------
/diagrams/basic-reactivity/case-study-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/basic-reactivity/case-study-2.png
--------------------------------------------------------------------------------
/diagrams/basic-reactivity/case-study-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/basic-reactivity/case-study-3.png
--------------------------------------------------------------------------------
/diagrams/basic-reactivity/graph-1a.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/basic-reactivity/graph-1a.png
--------------------------------------------------------------------------------
/diagrams/basic-reactivity/graph-1b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/basic-reactivity/graph-1b.png
--------------------------------------------------------------------------------
/diagrams/basic-reactivity/graph-2a.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/basic-reactivity/graph-2a.png
--------------------------------------------------------------------------------
/diagrams/basic-reactivity/graph-2b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/basic-reactivity/graph-2b.png
--------------------------------------------------------------------------------
/diagrams/basic-reactivity/graph-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/basic-reactivity/graph-3.png
--------------------------------------------------------------------------------
/diagrams/basic-reactivity/producers-consumers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/basic-reactivity/producers-consumers.png
--------------------------------------------------------------------------------
/diagrams/basic-reactivity/template.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/basic-reactivity/template.png
--------------------------------------------------------------------------------
/diagrams/basic-reactivity/timing-button-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/basic-reactivity/timing-button-2.png
--------------------------------------------------------------------------------
/diagrams/basic-reactivity/timing-button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/basic-reactivity/timing-button.png
--------------------------------------------------------------------------------
/diagrams/basic-reactivity/timing-click.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/basic-reactivity/timing-click.png
--------------------------------------------------------------------------------
/diagrams/basic-reactivity/timing-debounce.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/basic-reactivity/timing-debounce.png
--------------------------------------------------------------------------------
/diagrams/basic-reactivity/timing-timer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/basic-reactivity/timing-timer.png
--------------------------------------------------------------------------------
/diagrams/basic-reactivity/timing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/basic-reactivity/timing.png
--------------------------------------------------------------------------------
/diagrams/reactivity-components.graffle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-components.graffle
--------------------------------------------------------------------------------
/diagrams/reactivity-components/button 3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-components/button 3.png
--------------------------------------------------------------------------------
/diagrams/reactivity-components/button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-components/button.png
--------------------------------------------------------------------------------
/diagrams/reactivity-components/danger-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-components/danger-2.png
--------------------------------------------------------------------------------
/diagrams/reactivity-components/danger.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-components/danger.png
--------------------------------------------------------------------------------
/diagrams/reactivity-req.graffle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-req.graffle
--------------------------------------------------------------------------------
/diagrams/reactivity-tracking.graffle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-tracking.graffle
--------------------------------------------------------------------------------
/diagrams/reactivity-tracking/01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-tracking/01.png
--------------------------------------------------------------------------------
/diagrams/reactivity-tracking/02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-tracking/02.png
--------------------------------------------------------------------------------
/diagrams/reactivity-tracking/03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-tracking/03.png
--------------------------------------------------------------------------------
/diagrams/reactivity-tracking/04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-tracking/04.png
--------------------------------------------------------------------------------
/diagrams/reactivity-tracking/05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-tracking/05.png
--------------------------------------------------------------------------------
/diagrams/reactivity-tracking/06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-tracking/06.png
--------------------------------------------------------------------------------
/diagrams/reactivity-tracking/07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-tracking/07.png
--------------------------------------------------------------------------------
/diagrams/reactivity-tracking/08.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-tracking/08.png
--------------------------------------------------------------------------------
/diagrams/reactivity-tracking/09.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-tracking/09.png
--------------------------------------------------------------------------------
/diagrams/reactivity-tracking/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-tracking/10.png
--------------------------------------------------------------------------------
/diagrams/reactivity-tracking/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-tracking/11.png
--------------------------------------------------------------------------------
/diagrams/reactivity-tracking/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-tracking/12.png
--------------------------------------------------------------------------------
/diagrams/reactivity-tracking/complete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-tracking/complete.png
--------------------------------------------------------------------------------
/diagrams/reactivity-tracking/dynamic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-tracking/dynamic.png
--------------------------------------------------------------------------------
/diagrams/reactivity-tracking/dynamic2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-tracking/dynamic2.png
--------------------------------------------------------------------------------
/diagrams/reactivity-tracking/invalidate-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-tracking/invalidate-1.png
--------------------------------------------------------------------------------
/diagrams/reactivity-tracking/invalidate-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/reactivity-tracking/invalidate-2.png
--------------------------------------------------------------------------------
/diagrams/scaling-modules.graffle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/scaling-modules.graffle
--------------------------------------------------------------------------------
/diagrams/scaling-modules/after.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/scaling-modules/after.png
--------------------------------------------------------------------------------
/diagrams/scaling-modules/before.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/scaling-modules/before.png
--------------------------------------------------------------------------------
/diagrams/scaling-performance.graffle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/scaling-performance.graffle
--------------------------------------------------------------------------------
/diagrams/scaling-performance/collapsed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/scaling-performance/collapsed.png
--------------------------------------------------------------------------------
/diagrams/scaling-performance/flame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/scaling-performance/flame.png
--------------------------------------------------------------------------------
/diagrams/scaling-performance/horizontal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/scaling-performance/horizontal.png
--------------------------------------------------------------------------------
/diagrams/scaling-performance/proportional.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/scaling-performance/proportional.png
--------------------------------------------------------------------------------
/diagrams/scaling-performance/vertical.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/diagrams/scaling-performance/vertical.png
--------------------------------------------------------------------------------
/ga_script.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
--------------------------------------------------------------------------------
/images/action-feedback/notify-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-feedback/notify-1.png
--------------------------------------------------------------------------------
/images/action-feedback/notify-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-feedback/notify-2.png
--------------------------------------------------------------------------------
/images/action-feedback/notify-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-feedback/notify-3.png
--------------------------------------------------------------------------------
/images/action-feedback/progress-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-feedback/progress-1.png
--------------------------------------------------------------------------------
/images/action-feedback/progress-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-feedback/progress-2.png
--------------------------------------------------------------------------------
/images/action-feedback/progress-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-feedback/progress-3.png
--------------------------------------------------------------------------------
/images/action-feedback/progress-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-feedback/progress-4.png
--------------------------------------------------------------------------------
/images/action-feedback/spinner-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-feedback/spinner-1.png
--------------------------------------------------------------------------------
/images/action-feedback/spinner-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-feedback/spinner-2.png
--------------------------------------------------------------------------------
/images/action-graphics/brushedPoints.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-graphics/brushedPoints.png
--------------------------------------------------------------------------------
/images/action-graphics/click.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-graphics/click.png
--------------------------------------------------------------------------------
/images/action-graphics/modifying-size-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-graphics/modifying-size-1.png
--------------------------------------------------------------------------------
/images/action-graphics/modifying-size-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-graphics/modifying-size-2.png
--------------------------------------------------------------------------------
/images/action-graphics/nearPoints.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-graphics/nearPoints.png
--------------------------------------------------------------------------------
/images/action-graphics/persistent-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-graphics/persistent-1.png
--------------------------------------------------------------------------------
/images/action-graphics/persistent-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-graphics/persistent-2.png
--------------------------------------------------------------------------------
/images/action-graphics/persistent-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-graphics/persistent-3.png
--------------------------------------------------------------------------------
/images/action-layout/fluid-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-layout/fluid-page.png
--------------------------------------------------------------------------------
/images/action-workflow/breakpoint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-workflow/breakpoint.png
--------------------------------------------------------------------------------
/images/action-workflow/debug-toolbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-workflow/debug-toolbar.png
--------------------------------------------------------------------------------
/images/action-workflow/new-project.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-workflow/new-project.png
--------------------------------------------------------------------------------
/images/action-workflow/run-app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/action-workflow/run-app.png
--------------------------------------------------------------------------------
/images/basic-app/cheatsheet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/basic-app/cheatsheet.png
--------------------------------------------------------------------------------
/images/basic-app/hello-world.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/basic-app/hello-world.png
--------------------------------------------------------------------------------
/images/basic-app/run-app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/basic-app/run-app.png
--------------------------------------------------------------------------------
/images/basic-ui/multi-select.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/basic-ui/multi-select.png
--------------------------------------------------------------------------------
/images/prod-best-practices/ci-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/prod-best-practices/ci-screenshot.png
--------------------------------------------------------------------------------
/images/reactivity-graph/reactlog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/reactivity-graph/reactlog.png
--------------------------------------------------------------------------------
/images/scaling-performance/in-app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/scaling-performance/in-app.png
--------------------------------------------------------------------------------
/images/scaling-performance/profvis.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/scaling-performance/profvis.png
--------------------------------------------------------------------------------
/images/scaling-testing/keyboard-shortcuts.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/images/scaling-testing/keyboard-shortcuts.png
--------------------------------------------------------------------------------
/index.Rmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Mastering Shiny"
3 | author: "Hadley Wickham"
4 | site: bookdown::bookdown_site
5 |
6 | bibliography: references.bib
7 | csl: chicago-fullnote-bibliography.csl
8 | suppress-bibliography: true
9 | ---
10 |
11 | `r if (knitr::is_latex_output()) ''`
37 |
--------------------------------------------------------------------------------
/introduction.Rmd:
--------------------------------------------------------------------------------
1 | # Preface {.unnumbered}
2 |
3 | ```{r include = FALSE}
4 | source("common.R")
5 | ```
6 |
7 | ## What is Shiny?
8 |
9 | If you've never used Shiny before, welcome!
10 | Shiny is an R package that allows you to easily create rich, interactive web apps.
11 | Shiny allows you to take your work in R and expose it via a web browser so that anyone can use it.
12 | Shiny makes you look awesome by making it easy to produce polished web apps with a minimum amount of pain.
13 |
14 | In the past, creating web apps was hard for most R users because:
15 |
16 | - You need a deep knowledge of web technologies like HTML, CSS and JavaScript.
17 |
18 | - Making complex interactive apps requires careful analysis of interaction flows to make sure that when an input changes, only the related outputs are updated.
19 |
20 | Shiny makes it significantly easier for the R programmer to create web apps by:
21 |
22 | - Providing a carefully curated set of user interface (**UI** for short) functions that generate the HTML, CSS, and JavaScript needed for common tasks.
23 | This means that you don't need to know the details of HTML/CSS/JavaScript until you want to go beyond the basics that Shiny provides for you.
24 |
25 | - Introducing a new style of programming called **reactive programming** which automatically tracks the dependencies of pieces of code.
26 | This means that whenever an input changes, Shiny can automatically figure out how to do the smallest amount of work to update all the related outputs.
27 |
28 | People use Shiny to:
29 |
30 | - Create dashboards that track important high-level performance indicators, while facilitating drill down into metrics that need more investigation.
31 |
32 | - Replace hundreds of pages of PDFs with interactive apps that allow the user to jump to the exact slice of the results that they care about.
33 |
34 | - Communicate complex models to a non-technical audience with informative visualisations and interactive sensitivity analysis.
35 |
36 | - Provide self-service data analysis for common workflows, replacing email requests with a Shiny app that allows people to upload their own data and perform standard analyses.
37 | You can make sophisticated R analyses available to users with no programming skills.
38 |
39 | - Create interactive demos for teaching statistics and data science concepts that allow learners to tweak inputs and observe the downstream effects of those changes in an analysis.
40 |
41 | In short, Shiny gives you the ability to pass on some of your R superpowers to anyone who can use the web.
42 |
43 | ## Who should read this book?
44 |
45 | This book is aimed at two main audiences:
46 |
47 | - R users who are interested in learning about Shiny in order to turn their analyses into interactive web apps.
48 | To get the most out of this book, you should be comfortable using R to do data analysis, and should have written at least a few functions.
49 |
50 | - Existing Shiny users who want to improve their knowledge of the theory underlying Shiny in order to write higher-quality apps faster and more easily.
51 | You should find this book particularly helpful if your apps are starting to get bigger and you're starting to have problems managing the complexity.
52 |
53 | ## What will you learn?
54 |
55 | The book is divided into four parts:
56 |
57 | 1. In "Getting started", you'll learn the basics of Shiny so you can get up and running as quickly as possible.
58 | You'll learn about the basics of app structure, useful UI components, and the foundations of reactive programming.
59 |
60 | 2. "Shiny in action" builds on the basics to help you solve common problems including giving feedback to the user, uploading and downloading data, generating UI with code, reducing code duplication, and using Shiny to program the tidyverse.
61 |
62 | 3. In "Mastering reactivity", you'll go deep into the theory and practice of reactive programming, the programming paradigm that underlies Shiny.
63 | If you're an existing Shiny user, you'll get the most value out of this chapter as it will give you a solid theoretical underpinning that will allow you to create new tools specifically tailored for your problems.
64 |
65 | 4. Finally, in "Best practices" we'll finish up with a survey of useful techniques for making your Shiny apps work well in production.
66 | You'll learn how to decompose complex apps into functions and modules, how to use packages to organise your code, how to test your code to ensure it's correct, and how to measure and improve performance.
67 |
68 | ## What won't you learn?
69 |
70 | The focus of this book is making effective Shiny apps and understanding the underlying theory of reactivity.
71 | I'll do my best to showcase best practices for data science, R programming, and software engineering, but you'll need other references to master these important skills.
72 | If you enjoy my writing in this book, you might enjoy my other books on these topics: [R for Data Science](http://r4ds.had.co.nz/), [Advanced R](http://adv-r.hadley.nz/), and [R Packages](http://r-pkgs.org/).
73 |
74 | There are also a number of important topics specific to Shiny that I don't cover:
75 |
76 | - This book only covers the built-in user interface toolkit.
77 | This doesn't provide the sexiest possible design, but it's simple to learn and gets you a long way.
78 | If you have additional needs (or just get bored with the defaults), there are other packages that provide alternative front ends.
79 |
80 | - Deployment of Shiny apps.
81 | Putting Shiny "into production" is outside the scope of this book because it hugely varies from company to company, and much of it is unrelated to R (the majority of challenges tend to be cultural or organisational, not technical).
82 | If you're new to Shiny in production, I recommend by starting with Joe Cheng's 2019 rstudio::conf keynote: .
83 | That will give you the lay of the land, discussing broadly what putting Shiny into production entails and how to overcome some of the challenges that you're likely to face.
84 | Once you've done that, see the [RStudio Connect website](https://rstudio.com/products/connect/) to learn about RStudio's product for deploying apps within your company and the [Shiny website](https://shiny.rstudio.com/articles/#deployment) for other common deployment scenarios.
85 |
86 | ## Prerequisites {#prerequisites}
87 |
88 | Before we continue, make sure you have all the software you need for this book:
89 |
90 | - **R**: If you don't have R installed already, you may be reading the wrong book; I assume a basic familiarity with R throughout this book.
91 | If you'd like to learn how to use R, I'd recommend my [*R for Data Science*](https://r4ds.had.co.nz/) which is designed to get you up and running with R with a minimum of fuss.
92 |
93 | - **RStudio**: RStudio is a free and open source integrated development environment (IDE) for R.
94 | While you can write and use Shiny apps with any R environment (including R GUI and [ESS](http://ess.r-project.org)), RStudio has some nice features specifically for authoring, debugging, and deploying Shiny apps.
95 | We recommend giving it a try, but it's not required to be successful with Shiny or with this book.
96 | You can download RStudio Desktop from
97 |
98 | - **R packages**: This book uses a bunch of R packages.
99 | You can install them all at once by running:
100 |
101 | ```{r, echo = FALSE, cache = FALSE}
102 | deps <- desc::desc_get_deps()
103 | pkgs <- sort(deps$package[deps$type == "Imports"])
104 | pkgs2 <- strwrap(paste(encodeString(pkgs, quote = '"'), collapse = ", "), exdent = 2)
105 |
106 | install <- paste0(
107 | "install.packages(c(\n ",
108 | paste(pkgs2, "\n", collapse = ""),
109 | "))"
110 | )
111 | ```
112 |
113 | ```{r code = install, eval = FALSE}
114 | ```
115 |
116 | If you've downloaded Shiny in the past, make sure that you have at least version 1.6.0.
117 |
118 | ## Acknowledgements
119 |
120 | This book was written in the open and chapters were advertised on twitter when complete.
121 | It is truly a community effort: many people read drafts, fixed typos, suggested improvements, and contributed content.
122 | Without those contributors, the book wouldn't be nearly as good as it is, and I'm deeply grateful for their help.
123 |
124 | ```{r, eval = FALSE, echo = FALSE}
125 | library(tidyverse)
126 | contribs_all_json <- gh::gh("/repos/:owner/:repo/contributors",
127 | owner = "hadley",
128 | repo = "mastering-shiny",
129 | .limit = Inf
130 | )
131 | contribs_all <- tibble(
132 | login = contribs_all_json %>% map_chr("login"),
133 | n = contribs_all_json %>% map_int("contributions")
134 | )
135 |
136 | contribs_old <- read_csv("contributors.csv", col_types = list())
137 | contribs_new <- contribs_all %>% anti_join(contribs_old, by = "login")
138 |
139 | # Get info for new contributors
140 | needed_json <- map(
141 | contribs_new$login,
142 | ~ gh::gh("/users/:username", username = .x)
143 | )
144 | info_new <- tibble(
145 | login = contribs_new$login,
146 | name = map_chr(needed_json, "name", .default = NA),
147 | blog = map_chr(needed_json, "blog", .default = NA)
148 | )
149 | info_old <- contribs_old %>% select(login, name, blog)
150 | info_all <- bind_rows(info_old, info_new)
151 |
152 | contribs_all <- contribs_all %>%
153 | left_join(info_all, by = "login") %>%
154 | arrange(login)
155 | write_csv(contribs_all, "contributors.csv")
156 | ```
157 |
158 | ```{r, results = "asis", echo = FALSE, message = FALSE}
159 | library(dplyr)
160 | contributors <- readr::read_csv("contributors.csv")
161 | contributors <- contributors %>%
162 | filter(login != "hadley") %>%
163 | mutate(
164 | login = paste0("\\@", login),
165 | desc = ifelse(is.na(name), login, paste0(name, " (", login, ")"))
166 | )
167 |
168 | cat("A big thank you to all ", nrow(contributors), " people who contributed specific improvements via GitHub pull requests (in alphabetical order by username): ", sep = "")
169 | cat(paste0(contributors$desc, collapse = ", "))
170 | cat(".\n")
171 | ```
172 |
173 | ## Colophon
174 |
175 | This book was written in [RStudio](http://www.rstudio.com/ide/) using [bookdown](http://bookdown.org/).
176 | The [website](http://mastering-shiny.org/) is hosted with [netlify](http://netlify.com/), and automatically updated after every commit by [Github Actions](https://github.com/features/actions).
177 | The complete source is available from [GitHub](https://github.com/hadley/mastering-shiny).
178 |
179 | This version of the book was built with `r R.version.string` and the following packages:
180 |
181 | ```{r, echo = FALSE, results="asis"}
182 | pkgs <- sessioninfo::package_info(pkgs, dependencies = FALSE)
183 | df <- tibble(
184 | package = pkgs$package,
185 | version = pkgs$ondiskversion,
186 | source = gsub("@", "\\\\@", pkgs$source)
187 | )
188 | knitr::kable(df, format = "markdown")
189 | ```
190 |
191 | ```{r, echo = FALSE}
192 | ruler <- function(width = getOption("width")) {
193 | x <- seq_len(width)
194 | y <- dplyr::case_when(
195 | x %% 10 == 0 ~ as.character((x %/% 10) %% 10),
196 | x %% 5 == 0 ~ "+",
197 | TRUE ~ "-"
198 | )
199 | cat(y, "\n", sep = "")
200 | cat(x %% 10, "\n", sep = "")
201 | }
202 | ```
203 |
204 | ```{r, include = FALSE}
205 | ruler()
206 | ```
207 |
--------------------------------------------------------------------------------
/mastering-shiny.Rproj:
--------------------------------------------------------------------------------
1 | Version: 1.0
2 |
3 | RestoreWorkspace: Default
4 | SaveWorkspace: Default
5 | AlwaysSaveHistory: Default
6 |
7 | EnableCodeIndexing: Yes
8 | UseSpacesForTab: Yes
9 | NumSpacesForTab: 2
10 | Encoding: UTF-8
11 |
12 | RnwWeave: knitr
13 | LaTeX: pdfLaTeX
14 |
15 | AutoAppendNewline: Yes
16 | StripTrailingWhitespace: Yes
17 |
18 | BuildType: Website
19 |
20 | MarkdownWrap: Sentence
21 |
--------------------------------------------------------------------------------
/neiss/.gitignore:
--------------------------------------------------------------------------------
1 | app.R
2 |
--------------------------------------------------------------------------------
/neiss/data.R:
--------------------------------------------------------------------------------
1 | library(tidyverse)
2 | # install_github("hadley/neiss")
3 | library(neiss)
4 |
5 | top_prod <- injuries %>%
6 | filter(trmt_date >= as.Date("2017-01-01"), trmt_date < as.Date("2018-01-01")) %>%
7 | count(prod1, sort = TRUE) %>%
8 | filter(n > 5 * 365)
9 |
10 | injuries %>%
11 | filter(trmt_date >= as.Date("2017-01-01"), trmt_date < as.Date("2018-01-01")) %>%
12 | semi_join(top_prod, by = "prod1") %>%
13 | mutate(age = floor(age), sex = tolower(sex), race = tolower(race)) %>%
14 | filter(sex != "unknown") %>%
15 | select(trmt_date, age, sex, race, body_part, diag, location, prod_code = prod1, weight, narrative) %>%
16 | vroom::vroom_write("neiss/injuries.tsv.gz")
17 |
18 | products %>%
19 | semi_join(top_prod, by = c("code" = "prod1")) %>%
20 | rename(prod_code = code) %>%
21 | vroom::vroom_write("neiss/products.tsv")
22 |
23 | population %>%
24 | filter(year == 2017) %>%
25 | select(-year) %>%
26 | rename(population = n) %>%
27 | vroom::vroom_write("neiss/population.tsv")
28 |
--------------------------------------------------------------------------------
/neiss/injuries.tsv.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/neiss/injuries.tsv.gz
--------------------------------------------------------------------------------
/neiss/narrative.R:
--------------------------------------------------------------------------------
1 | library(dplyr)
2 | library(ggplot2)
3 | library(forcats)
4 | library(vroom)
5 | library(shiny)
6 |
7 | if (!exists("injuries")) {
8 | injuries <- vroom::vroom("injuries.tsv.gz")
9 | products <- vroom::vroom("products.tsv")
10 | population <- vroom::vroom("population.tsv")
11 | }
12 |
13 | ui <- fluidPage(
14 | #<< first-row
15 | fluidRow(
16 | column(8,
17 | selectInput("code", "Product",
18 | choices = setNames(products$prod_code, products$title),
19 | width = "100%"
20 | )
21 | ),
22 | column(2, selectInput("y", "Y axis", c("rate", "count")))
23 | ),
24 | #>>
25 | fluidRow(
26 | column(4, tableOutput("diag")),
27 | column(4, tableOutput("body_part")),
28 | column(4, tableOutput("location"))
29 | ),
30 | fluidRow(
31 | column(12, plotOutput("age_sex"))
32 | ),
33 | #<< narrative-ui
34 | fluidRow(
35 | column(2, actionButton("story", "Tell me a story")),
36 | column(10, textOutput("narrative"))
37 | )
38 | #>>
39 | )
40 |
41 | count_top <- function(df, var, n = 5) {
42 | df %>%
43 | mutate({{ var }} := fct_lump(fct_infreq({{ var }}), n = n)) %>%
44 | group_by({{ var }}) %>%
45 | summarise(n = as.integer(sum(weight)))
46 | }
47 |
48 | server <- function(input, output, session) {
49 | selected <- reactive(injuries %>% filter(prod_code == input$code))
50 |
51 | #<< tables
52 | output$diag <- renderTable(count_top(selected(), diag), width = "100%")
53 | output$body_part <- renderTable(count_top(selected(), body_part), width = "100%")
54 | output$location <- renderTable(count_top(selected(), location), width = "100%")
55 | #>>
56 |
57 | summary <- reactive({
58 | selected() %>%
59 | count(age, sex, wt = weight) %>%
60 | left_join(population, by = c("age", "sex")) %>%
61 | mutate(rate = n / population * 1e4)
62 | })
63 |
64 | #<< plot
65 | output$age_sex <- renderPlot({
66 | if (input$y == "count") {
67 | summary() %>%
68 | ggplot(aes(age, n, colour = sex)) +
69 | geom_line() +
70 | labs(y = "Estimated number of injuries")
71 | } else {
72 | summary() %>%
73 | ggplot(aes(age, rate, colour = sex)) +
74 | geom_line(na.rm = TRUE) +
75 | labs(y = "Injuries per 10,000 people")
76 | }
77 | }, res = 96)
78 | #>>
79 |
80 | #<< narrative-server
81 | narrative_sample <- eventReactive(
82 | list(input$story, selected()),
83 | selected() %>% pull(narrative) %>% sample(1)
84 | )
85 | output$narrative <- renderText(narrative_sample())
86 | #>>
87 | }
88 |
89 | shinyApp(ui, server)
90 |
--------------------------------------------------------------------------------
/neiss/polish-tables.R:
--------------------------------------------------------------------------------
1 | library(dplyr)
2 | library(ggplot2)
3 | library(forcats)
4 | library(vroom)
5 | library(shiny)
6 |
7 | if (!exists("injuries")) {
8 | injuries <- vroom::vroom("injuries.tsv.gz")
9 | products <- vroom::vroom("products.tsv")
10 | population <- vroom::vroom("population.tsv")
11 | }
12 |
13 | ui <- fluidPage(
14 | fluidRow(
15 | column(8,
16 | selectInput("code", "Product",
17 | choices = setNames(products$prod_code, products$title),
18 | width = "100%"
19 | )
20 | )
21 | ),
22 | fluidRow(
23 | column(4, tableOutput("diag")),
24 | column(4, tableOutput("body_part")),
25 | column(4, tableOutput("location"))
26 | ),
27 | fluidRow(
28 | column(12, plotOutput("age_sex"))
29 | )
30 | )
31 |
32 | #<< count_top
33 | count_top <- function(df, var, n = 5) {
34 | df %>%
35 | mutate({{ var }} := fct_lump(fct_infreq({{ var }}), n = n)) %>%
36 | group_by({{ var }}) %>%
37 | summarise(n = as.integer(sum(weight)))
38 | }
39 | #>>
40 |
41 | server <- function(input, output, session) {
42 | selected <- reactive(injuries %>% filter(prod_code == input$code))
43 |
44 | #<< tables
45 | output$diag <- renderTable(count_top(selected(), diag), width = "100%")
46 | output$body_part <- renderTable(count_top(selected(), body_part), width = "100%")
47 | output$location <- renderTable(count_top(selected(), location), width = "100%")
48 | #>>
49 |
50 | summary <- reactive({
51 | selected() %>%
52 | count(age, sex, wt = weight) %>%
53 | left_join(population, by = c("age", "sex")) %>%
54 | mutate(rate = n / population * 1e4)
55 | })
56 |
57 | output$age_sex <- renderPlot({
58 | summary() %>%
59 | ggplot(aes(age, n, colour = sex)) +
60 | geom_line() +
61 | labs(y = "Estimated number of injuries")
62 | }, res = 96)
63 | }
64 |
65 | shinyApp(ui, server)
66 |
--------------------------------------------------------------------------------
/neiss/population.tsv:
--------------------------------------------------------------------------------
1 | age sex population
2 | 0 female 1924145
3 | 0 male 2015150
4 | 1 female 1943534
5 | 1 male 2031718
6 | 2 female 1965150
7 | 2 male 2056625
8 | 3 female 1956281
9 | 3 male 2050474
10 | 4 female 1953782
11 | 4 male 2042001
12 | 5 female 1956268
13 | 5 male 2045050
14 | 6 female 1976331
15 | 6 male 2069201
16 | 7 female 1979376
17 | 7 male 2063003
18 | 8 female 1980666
19 | 8 male 2062172
20 | 9 female 2043456
21 | 9 male 2128715
22 | 10 female 2051237
23 | 10 male 2140682
24 | 11 female 2034143
25 | 11 male 2123819
26 | 12 female 2029693
27 | 12 male 2116260
28 | 13 female 2035757
29 | 13 male 2119558
30 | 14 female 2022552
31 | 14 male 2104753
32 | 15 female 2015751
33 | 15 male 2098809
34 | 16 female 2066782
35 | 16 male 2155909
36 | 17 female 2098704
37 | 17 male 2197871
38 | 18 female 2071758
39 | 18 male 2169468
40 | 19 female 2078174
41 | 19 male 2178434
42 | 20 female 2089336
43 | 20 male 2187409
44 | 21 female 2104329
45 | 21 male 2213975
46 | 22 female 2150615
47 | 22 male 2270148
48 | 23 female 2196524
49 | 23 male 2318784
50 | 24 female 2228689
51 | 24 male 2358826
52 | 25 female 2295484
53 | 25 male 2413370
54 | 26 female 2350352
55 | 26 male 2443974
56 | 27 female 2351456
57 | 27 male 2432679
58 | 28 female 2260383
59 | 28 male 2335023
60 | 29 female 2210555
61 | 29 male 2277184
62 | 30 female 2174279
63 | 30 male 2236352
64 | 31 female 2183649
65 | 31 male 2241759
66 | 32 female 2204770
67 | 32 male 2248948
68 | 33 female 2142863
69 | 33 male 2168114
70 | 34 female 2177520
71 | 34 male 2193958
72 | 35 female 2179052
73 | 35 male 2185895
74 | 36 female 2157640
75 | 36 male 2156347
76 | 37 female 2190170
77 | 37 male 2204181
78 | 38 female 2063849
79 | 38 male 2056619
80 | 39 female 2025301
81 | 39 male 2012943
82 | 40 female 2009156
83 | 40 male 1985267
84 | 41 female 1948683
85 | 41 male 1927055
86 | 42 female 2003193
87 | 42 male 1987507
88 | 43 female 1947353
89 | 43 male 1916102
90 | 44 female 1981873
91 | 44 male 1937184
92 | 45 female 2066554
93 | 45 male 2025025
94 | 46 female 2183846
95 | 46 male 2136737
96 | 47 female 2201202
97 | 47 male 2169762
98 | 48 female 2089271
99 | 48 male 2050739
100 | 49 female 2046810
101 | 49 male 2003912
102 | 50 female 2053593
103 | 50 male 1997485
104 | 51 female 2089015
105 | 51 male 2032379
106 | 52 female 2214239
107 | 52 male 2148966
108 | 53 female 2261664
109 | 53 male 2175762
110 | 54 female 2262401
111 | 54 male 2165590
112 | 55 female 2258814
113 | 55 male 2154452
114 | 56 female 2290060
115 | 56 male 2171286
116 | 57 female 2303341
117 | 57 male 2190232
118 | 58 female 2232903
119 | 58 male 2102988
120 | 59 female 2222318
121 | 59 male 2081562
122 | 60 female 2202546
123 | 60 male 2047375
124 | 61 female 2128409
125 | 61 male 1963253
126 | 62 female 2111180
127 | 62 male 1938979
128 | 63 female 2032205
129 | 63 male 1843702
130 | 64 female 1956079
131 | 64 male 1763974
132 | 65 female 1878995
133 | 65 male 1688983
134 | 66 female 1825058
135 | 66 male 1626011
136 | 67 female 1778116
137 | 67 male 1583631
138 | 68 female 1723919
139 | 68 male 1527330
140 | 69 female 1700425
141 | 69 male 1503913
142 | 70 female 1765955
143 | 70 male 1558707
144 | 71 female 1307666
145 | 71 male 1136177
146 | 72 female 1290521
147 | 72 male 1108765
148 | 73 female 1255382
149 | 73 male 1066091
150 | 74 female 1280269
151 | 74 male 1077532
152 | 75 female 1115294
153 | 75 male 923942
154 | 76 female 1019726
155 | 76 male 827432
156 | 77 female 959502
157 | 77 male 769612
158 | 78 female 894185
159 | 78 male 713055
160 | 79 female 853738
161 | 79 male 664775
162 | 80 female 782413
163 | 80 male 596081
164 | 81 female 741699
165 | 81 male 552132
166 | 82 female 706475
167 | 82 male 510165
168 | 83 female 626409
169 | 83 male 441701
170 | 84 female 599235
171 | 84 male 408980
172 |
--------------------------------------------------------------------------------
/neiss/products.tsv:
--------------------------------------------------------------------------------
1 | prod_code title
2 | 464 knives, not elsewhere classified
3 | 474 tableware and accessories
4 | 604 desks, chests, bureaus or buffets
5 | 611 bathtubs or showers
6 | 649 toilets
7 | 676 rugs or carpets, not specified
8 | 679 sofas, couches, davenports, divans or st
9 | 1141 containers, not specified
10 | 1200 sports or recreational activity, n.e.c.
11 | 1205 basketball (activity, apparel or equip.)
12 | 1211 football (activity, apparel or equip.)
13 | 1233 trampolines
14 | 1242 slides or sliding boards
15 | 1244 monkey bars or other playground climbing
16 | 1267 soccer (activity, apparel or equip.)
17 | 1333 skateboards
18 | 1615 footwear
19 | 1616 jewelry
20 | 1807 floors or flooring materials
21 | 1817 porches, balconies, open-side floors or
22 | 1819 nails, screws, tacks or bolts
23 | 1842 stairs or steps
24 | 1871 fences or fence posts
25 | 1884 ceilings and walls (part of completed st
26 | 1893 doors, other or not specified
27 | 1894 windows & window glass, excl storm windo
28 | 3265 weight lifting (activity, apparel or equ
29 | 3274 swimming (activity, apparel or equipment
30 | 3299 exercise (activity or apparel, w/o equip
31 | 4014 furniture, not specified
32 | 4056 cabinets, racks, room dividers and shelv
33 | 4057 tables, not elsewhere classified
34 | 4074 chairs, other or not specified
35 | 4076 beds or bedframes, other or not spec
36 | 4078 ladders, other or not specified
37 | 5034 softball (activity, apparel or equipment
38 | 5040 bicycles and accessories (excl mountain
39 | 5041 baseball (activity, apparel or equipment
40 |
--------------------------------------------------------------------------------
/neiss/prototype.R:
--------------------------------------------------------------------------------
1 | library(dplyr)
2 | library(ggplot2)
3 | library(forcats)
4 | library(vroom)
5 | library(shiny)
6 |
7 | if (!exists("injuries")) {
8 | injuries <- vroom::vroom("injuries.tsv.gz")
9 | products <- vroom::vroom("products.tsv")
10 | population <- vroom::vroom("population.tsv")
11 | }
12 |
13 | #<< ui
14 | prod_codes <- setNames(products$prod_code, products$title)
15 |
16 | ui <- fluidPage(
17 | fluidRow(
18 | column(6,
19 | selectInput("code", "Product", choices = prod_codes)
20 | )
21 | ),
22 | fluidRow(
23 | column(4, tableOutput("diag")),
24 | column(4, tableOutput("body_part")),
25 | column(4, tableOutput("location"))
26 | ),
27 | fluidRow(
28 | column(12, plotOutput("age_sex"))
29 | )
30 | )
31 | #>>
32 |
33 | #<< server
34 | server <- function(input, output, session) {
35 | selected <- reactive(injuries %>% filter(prod_code == input$code))
36 |
37 | output$diag <- renderTable(
38 | selected() %>% count(diag, wt = weight, sort = TRUE)
39 | )
40 | output$body_part <- renderTable(
41 | selected() %>% count(body_part, wt = weight, sort = TRUE)
42 | )
43 | output$location <- renderTable(
44 | selected() %>% count(location, wt = weight, sort = TRUE)
45 | )
46 |
47 | summary <- reactive({
48 | selected() %>%
49 | count(age, sex, wt = weight) %>%
50 | left_join(population, by = c("age", "sex")) %>%
51 | mutate(rate = n / population * 1e4)
52 | })
53 |
54 | output$age_sex <- renderPlot({
55 | summary() %>%
56 | ggplot(aes(age, n, colour = sex)) +
57 | geom_line() +
58 | labs(y = "Estimated number of injuries")
59 | }, res = 96)
60 | }
61 | #>>
62 |
63 | shinyApp(ui, server)
64 |
--------------------------------------------------------------------------------
/neiss/rate-vs-count.R:
--------------------------------------------------------------------------------
1 | library(dplyr)
2 | library(ggplot2)
3 | library(forcats)
4 | library(vroom)
5 | library(shiny)
6 |
7 | if (!exists("injuries")) {
8 | injuries <- vroom::vroom("injuries.tsv.gz")
9 | products <- vroom::vroom("products.tsv")
10 | population <- vroom::vroom("population.tsv")
11 | }
12 |
13 | ui <- fluidPage(
14 | #<< first-row
15 | fluidRow(
16 | column(8,
17 | selectInput("code", "Product",
18 | choices = setNames(products$prod_code, products$title),
19 | width = "100%"
20 | )
21 | ),
22 | column(2, selectInput("y", "Y axis", c("rate", "count")))
23 | ),
24 | #>>
25 | fluidRow(
26 | column(4, tableOutput("diag")),
27 | column(4, tableOutput("body_part")),
28 | column(4, tableOutput("location"))
29 | ),
30 | fluidRow(
31 | column(12, plotOutput("age_sex"))
32 | )
33 | )
34 |
35 | count_top <- function(df, var, n = 5) {
36 | df %>%
37 | mutate({{ var }} := fct_lump(fct_infreq({{ var }}), n = n)) %>%
38 | group_by({{ var }}) %>%
39 | summarise(n = as.integer(sum(weight)))
40 | }
41 |
42 | server <- function(input, output, session) {
43 | selected <- reactive(injuries %>% filter(prod_code == input$code))
44 |
45 | #<< tables
46 | output$diag <- renderTable(count_top(selected(), diag), width = "100%")
47 | output$body_part <- renderTable(count_top(selected(), body_part), width = "100%")
48 | output$location <- renderTable(count_top(selected(), location), width = "100%")
49 | #>>
50 |
51 | summary <- reactive({
52 | selected() %>%
53 | count(age, sex, wt = weight) %>%
54 | left_join(population, by = c("age", "sex")) %>%
55 | mutate(rate = n / population * 1e4)
56 | })
57 |
58 | #<< plot
59 | output$age_sex <- renderPlot({
60 | if (input$y == "count") {
61 | summary() %>%
62 | ggplot(aes(age, n, colour = sex)) +
63 | geom_line() +
64 | labs(y = "Estimated number of injuries")
65 | } else {
66 | summary() %>%
67 | ggplot(aes(age, rate, colour = sex)) +
68 | geom_line(na.rm = TRUE) +
69 | labs(y = "Injuries per 10,000 people")
70 | }
71 | }, res = 96)
72 | #>>
73 | }
74 |
75 | shinyApp(ui, server)
76 |
--------------------------------------------------------------------------------
/preamble.tex:
--------------------------------------------------------------------------------
1 | \usepackage{booktabs}
2 | \usepackage{amsthm}
3 | \makeatletter
4 | \def\thm@space@setup{%
5 | \thm@preskip=8pt plus 2pt minus 4pt
6 | \thm@postskip=\thm@preskip
7 | }
8 | \makeatother
9 |
10 | % Match code width in O'Reilly books
11 | \setmonofont[Mapping=tex-ansi]{Inconsolata}
12 | \usepackage{anyfontsize}
13 | \DefineVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\{\},fontsize=\fontsize{10.5}{11}\selectfont}
14 |
15 | % box drawing characters
16 | %
17 | \usepackage{newunicodechar}
18 | \newfontfamily{\fallbackfont}{Andale Mono}
19 | \DeclareTextFontCommand{\textfallback}{\fallbackfont}
20 | \newunicodechar{█}{\textfallback{█}}
21 | \newunicodechar{└}{\textfallback{└}}
22 | \newunicodechar{─}{\textfallback{─}}
23 | \newunicodechar{├}{\textfallback{├}}
24 | \newunicodechar{│}{\textfallback{│}}
25 |
26 |
27 | % sidebar environment
28 | \usepackage{framed,color}
29 | \definecolor{shadecolor}{RGB}{242,242,242}
30 | \makeatletter
31 | \newenvironment{sidebar}{%
32 | \medskip{}
33 | \setlength{\fboxsep}{.8em}
34 | \def\at@end@of@kframe{}%
35 | \ifinner\ifhmode%
36 | \def\at@end@of@kframe{\end{minipage}}%
37 | \begin{minipage}{\columnwidth}%
38 | \fi\fi%
39 | \def\FrameCommand##1{\hskip\@totalleftmargin \hskip-\fboxsep
40 | \colorbox{shadecolor}{##1}\hskip-\fboxsep
41 | % There is no \\@totalrightmargin, so:
42 | \hskip-\linewidth \hskip-\@totalleftmargin \hskip\columnwidth}%
43 | \MakeFramed {\advance\hsize-\width
44 | \@totalleftmargin\z@ \linewidth\hsize
45 | \@setminipage}}%
46 | {\par\unskip\endMakeFramed%
47 | \at@end@of@kframe}
48 | \makeatother
49 |
50 |
--------------------------------------------------------------------------------
/puppy-photos/KCdYn0xu2fU.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/puppy-photos/KCdYn0xu2fU.jpg
--------------------------------------------------------------------------------
/puppy-photos/TzjMd7i5WQI.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/puppy-photos/TzjMd7i5WQI.jpg
--------------------------------------------------------------------------------
/puppy-photos/eoqnr8ikwFE.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/puppy-photos/eoqnr8ikwFE.jpg
--------------------------------------------------------------------------------
/reactivity-escaping.Rmd:
--------------------------------------------------------------------------------
1 | # Escaping the graph {#reactivity-components}
2 |
3 | ```{r setup, include=FALSE}
4 | source("common.R")
5 | ```
6 |
7 | ## Introduction
8 |
9 | Shiny's reactive programming framework is incredibly useful because it automatically determines the minimal set of computations needed to update all outputs when an input changes.
10 | But this framework is deliberately constraining, and sometimes you need to break free to do something risky but necessary.
11 |
12 | In this chapter you'll learn how you can combine `reactiveValues()` and `observe()`/`observeEvent()` to connect the right hand side of the reactive graph back to the left hand side.
13 | These techniques are powerful because they give you manual control over parts of the graph.
14 | But they're also dangerous because they allow your app to do unnecessary work.
15 | Most importantly, you can now create infinite loops where your app gets stuck in a cycle of updates that never ends.
16 |
17 | If you find the ideas explored in this chapter to be interesting, you might also want to look at the [shinySignals](https://github.com/hadley/shinySignals/) and [rxtools](https://github.com/jcheng5/rxtools) packages.
18 | These are both experimental packages, designed to explore "higher order" reactivity, reactives that are created programmatically from other reactives.
19 | I wouldn't recommend you use them in "real" apps, but reading the source code might be illuminating.
20 |
21 | ```{r}
22 | library(shiny)
23 | ```
24 |
25 | ## What doesn't the reactive graph capture?
26 |
27 | In Section \@ref(input-changes) we discussed what happens when the user causes an input to be invalidated.
28 | There are two other important cases where you as the app author might invalidate an input:
29 |
30 | - You call an `update` function setting the `value` argument.
31 | This sends a message to the browser to change the value of an input, which then notifies R that the input value has been changed.
32 |
33 | - You modify the value of a reactive value (created with `reactiveVal()` or `reactiveValues()`).
34 |
35 | It's important to understand that in both of these cases a reactive dependency is *not* created between the reactive value and the observer.
36 | While these actions cause the graph to invalidate, they are not recorded through new connections[^reactivity-escaping-1].
37 |
38 | [^reactivity-escaping-1]: As a debugging aid, the reactlog package can capture and draw these connection when you modify reactive values from an observer, but this information is not used by Shiny.
39 |
40 | To make this idea concrete, take the following simple app, with reactive graph shown in Figure \@ref(fig:graph-mutation).
41 |
42 | ```{r}
43 | ui <- fluidPage(
44 | textInput("nm", "name"),
45 | actionButton("clr", "Clear"),
46 | textOutput("hi")
47 | )
48 | server <- function(input, output, session) {
49 | hi <- reactive(paste0("Hi ", input$nm))
50 | output$hi <- renderText(hi())
51 | observeEvent(input$clr, {
52 | updateTextInput(session, "nm", value = "")
53 | })
54 | }
55 | ```
56 |
57 | ```{r graph-mutation, echo = FALSE, out.width = NULL, fig.cap = "The reactive graph does not record the connection between the unnamed observer and the `nm` input; this dependency is outside of its scope."}
58 | knitr::include_graphics("diagrams/reactivity-tracking/invalidate-1.png", dpi = 300)
59 | ```
60 |
61 | What happens when you press the clear button?
62 |
63 | 1. `input$clr` invalidates, which then invalidates the observer.
64 | 2. The observer recomputes, recreating the dependency on `input$clr`, and telling the browser to change the value of the input control.
65 | 3. The browser changes the value of `nm`.
66 | 4. `input$nm` invalidates, invalidating `hi()` then `output$hi`.
67 | 5. `output$hi` recomputes, forcing `hi()` to recompute.
68 |
69 | None of these actions change the reactive graph, so it remains as in Figure \@ref(fig:graph-mutation) and the graph does not capture the connection from the observer to `input$nm`.
70 |
71 | ## Case studies
72 |
73 | Next, lets take a look at a few useful cases where you might combine `reactiveValues()` and `observeEvent()` or `observe()` to solve problems that are otherwise very challenging (if not impossible).
74 | These are useful templates for your own apps.
75 |
76 | ### One output modified by multiple inputs
77 |
78 | To get started we'll tackle a very simple problem: I want a common text box that's updated by multiple events[^reactivity-escaping-2].
79 |
80 | [^reactivity-escaping-2]: This is rather similar to a notification, as seen in Section \@ref(notifications).
81 |
82 | ```{r, eval = FALSE}
83 | ui <- fluidPage(
84 | actionButton("drink", "drink me"),
85 | actionButton("eat", "eat me"),
86 | textOutput("notice")
87 | )
88 | server <- function(input, output, session) {
89 | r <- reactiveValues(notice = "")
90 | observeEvent(input$drink, {
91 | r$notice <- "You are no longer thirsty"
92 | })
93 | observeEvent(input$eat, {
94 | r$notice <- "You are no longer hungry"
95 | })
96 | output$notice <- renderText(r$notice)
97 | }
98 | ```
99 |
100 | Things get slightly more complicated in the next example, where we have an app with two buttons that let you increase and decrease values.
101 | We use a `reactiveValues()` to store the current value, and then use `observeEvent()` to increment and decrement the value when the appropriate button is pushed.
102 | The main additional complexity here is that the new value of `r$n` depends on the previous value.
103 |
104 | ```{r}
105 | ui <- fluidPage(
106 | actionButton("up", "up"),
107 | actionButton("down", "down"),
108 | textOutput("n")
109 | )
110 | server <- function(input, output, session) {
111 | r <- reactiveValues(n = 0)
112 | observeEvent(input$up, {
113 | r$n <- r$n + 1
114 | })
115 | observeEvent(input$down, {
116 | r$n <- r$n - 1
117 | })
118 |
119 | output$n <- renderText(r$n)
120 | }
121 | ```
122 |
123 | Figure \@ref(fig:button-graph) shows the reactive graph for this example.
124 | Again note that the reactive graph does not include any connection from the observers back to the reactive value `n`.
125 |
126 | ```{r button-graph, echo = FALSE, out.width = NULL, fig.cap = "The reactive graph does not capture connections from observers to input values"}
127 | knitr::include_graphics("diagrams/reactivity-components/button.png", dpi = 300)
128 | ```
129 |
130 | ### Accumulating inputs
131 |
132 | It's a similar pattern if you want to accumulate data in order to support data entry.
133 | Here the main difference is that we use `updateTextInput()` to reset the text box after the user clicks the add button.
134 |
135 | ```{r}
136 | ui <- fluidPage(
137 | textInput("name", "name"),
138 | actionButton("add", "add"),
139 | textOutput("names")
140 | )
141 | server <- function(input, output, session) {
142 | r <- reactiveValues(names = character())
143 | observeEvent(input$add, {
144 | r$names <- c(input$name, r$names)
145 | updateTextInput(session, "name", value = "")
146 | })
147 |
148 | output$names <- renderText(r$names)
149 | }
150 | ```
151 |
152 | We could make this slightly more useful by providing a delete button and making sure that the add button doesn't create duplicate names:
153 |
154 | ```{r}
155 | ui <- fluidPage(
156 | textInput("name", "name"),
157 | actionButton("add", "add"),
158 | actionButton("del", "delete"),
159 | textOutput("names")
160 | )
161 | server <- function(input, output, session) {
162 | r <- reactiveValues(names = character())
163 | observeEvent(input$add, {
164 | r$names <- union(r$names, input$name)
165 | updateTextInput(session, "name", value = "")
166 | })
167 | observeEvent(input$del, {
168 | r$names <- setdiff(r$names, input$name)
169 | updateTextInput(session, "name", value = "")
170 | })
171 |
172 | output$names <- renderText(r$names)
173 | }
174 | ```
175 |
176 | ### Pausing animations
177 |
178 | Another common use case is to provide a start and stop button that lets you control some recurring event.
179 | This example uses a `running` reactive value to control whether or not the number increments, and `invalidateLater()` to ensure that the observer is invalidated every 250 ms when running.
180 |
181 | ```{r}
182 | ui <- fluidPage(
183 | actionButton("start", "start"),
184 | actionButton("stop", "stop"),
185 | textOutput("n")
186 | )
187 | server <- function(input, output, session) {
188 | r <- reactiveValues(running = FALSE, n = 0)
189 |
190 | observeEvent(input$start, {
191 | r$running <- TRUE
192 | })
193 | observeEvent(input$stop, {
194 | r$running <- FALSE
195 | })
196 |
197 | observe({
198 | if (r$running) {
199 | r$n <- isolate(r$n) + 1
200 | invalidateLater(250)
201 | }
202 | })
203 | output$n <- renderText(r$n)
204 | }
205 | ```
206 |
207 | Notice in this case we can't easily use `observeEvent()` because we perform different actions depending on whether `running()` is `TRUE` or `FALSE`.
208 | Since we can't use `observeEvent()`, we must use `isolate()` --- if we don't this observer would also take a reactive dependency on `n`, which it updates, so it would get stuck in an infinite loop.
209 |
210 | Hopefully these examples start to give you a flavour of what programming with `reactiveValues()` and `observe()` feels like.
211 | It's very imperative: when this happens, do that; when that happens, do the other thing.
212 | This makes it easier to understand on a small scale, but harder to understand when bigger pieces start interacting.
213 | So generally, you'll want to use this as sparingly as possible, and keep it isolated so that the smallest possible number of observers modify the reactive value.
214 |
215 | ### Exercises
216 |
217 | 1. Provide a server function that draws a histogram of 100 random numbers from a normal distribution when normal is clicked, and 100 random uniforms.
218 |
219 | ```{r}
220 | ui <- fluidPage(
221 | actionButton("rnorm", "Normal"),
222 | actionButton("runif", "Uniform"),
223 | plotOutput("plot")
224 | )
225 | ```
226 |
227 | 2. Modify your code from above for to work with this UI:
228 |
229 | ```{r}
230 | ui <- fluidPage(
231 | selectInput("type", "type", c("Normal", "Uniform")),
232 | actionButton("go", "go"),
233 | plotOutput("plot")
234 | )
235 | ```
236 |
237 | 3. Rewrite your code from the previous answer to eliminate the use of `observe()`/`observeEvent()` and only use `reactive()`.
238 | Why can you do that for the second UI but not the first?
239 |
240 | ## Anti-patterns
241 |
242 | Once you get the hang of this pattern it's easy to fall into bad habits:
243 |
244 | ```{r}
245 | server <- function(input, output, session) {
246 | r <- reactiveValues(df = cars)
247 | observe({
248 | r$df <- head(cars, input$nrows)
249 | })
250 |
251 | output$plot <- renderPlot(plot(r$df))
252 | output$table <- renderTable(r$df)
253 | }
254 | ```
255 |
256 | In this simple case, this code doesn't do much extra work compared to the alternative that uses `reactive()`:
257 |
258 | ```{r}
259 | server <- function(input, output, session) {
260 | df <- reactive(head(cars, input$nrows))
261 |
262 | output$plot <- renderPlot(plot(df()))
263 | output$table <- renderTable(df())
264 | }
265 | ```
266 |
267 | But there are still two drawbacks:
268 |
269 | - If the table or plot are in tabs that are not currently visible, the observer will still draw/plot them.
270 |
271 | - If the `head()` throws an error, the `observe()` will terminate the app, but `reactive()` will propagate it so it's displayed.
272 |
273 | And things will get progressively worse as the app gets more complicated.
274 | It's very easy to revert to the event-driven programming situation described in Section \@ref(event-driven).
275 | You end up doing a lot of hard work to analyse the flow of events in your app, rather than relying on Shiny to handle it for you automatically.
276 |
277 | It's informative to compare the two reactive graphs.
278 | Figure \@ref(fig:anti-pattern) shows the graph from the first example.
279 | It's misleading because it doesn't look like `nrows` is connected to `df()`.
280 | Using a reactive, as in Figure \@ref(fig:pattern), makes the precise connection easy to see.
281 | Having a reactive graph that is as simple as possible is important for both humans and for Shiny.
282 | A simple graph is easier for humans to understand, and a simple graph is easier for Shiny to optimise.
283 |
284 | ```{r anti-pattern, echo = FALSE, out.width = NULL, fig.cap = "Using reactive values and observers leaves part of the graph disconnected"}
285 | knitr::include_graphics("diagrams/reactivity-components/danger.png", dpi = 300)
286 | ```
287 |
288 | ```{r pattern, echo = FALSE, out.width = NULL, fig.cap = "Using a reactives makes the dependencies between the components very clear."}
289 | knitr::include_graphics("diagrams/reactivity-components/danger-2.png", dpi = 300)
290 | ```
291 |
292 | ## Summary
293 |
294 | In the last four chapters, you have learned much more about the reactive programming model used by Shiny.
295 | You've learned why reactive programming is important (it allows Shiny to do just as much work as is required and no more), and the details of the reactive graph.
296 | You've also learned a bit about how the fundamental building blocks work under the hood, and how you can use them to escape the constraints of the reactive graph when needed.
297 |
298 | The remainder of the book discusses Shiny through the lens of software engineering.
299 | In the next seven chapters, you'll learn how to keep you Shiny apps maintainable, performant, and safe as they continue to grow in size and impact.
300 |
--------------------------------------------------------------------------------
/reactivity-motivation.Rmd:
--------------------------------------------------------------------------------
1 | # Why reactivity? {#reactive-motivation}
2 |
3 | ```{r setup, include=FALSE}
4 | source("common.R")
5 | ```
6 |
7 | ## Introduction
8 |
9 | The initial impression of Shiny is often that it's "magic".
10 | Magic is great when you get started because you can make simple apps very very quickly.
11 | But magic in software usually leads to disillusionment: without a solid mental model, it's extremely difficult to predict how the software will act when you venture beyond the borders of its demos and examples.
12 | And when things don't go the way you expect, debugging is almost impossible.
13 |
14 | Fortunately, Shiny is "good" magic.
15 | As Tom Dale said of his Ember.js JavaScript framework: "We do a lot of magic, but it's *good magic*, which means it decomposes into sane primitives." This is the quality that the Shiny team aspires to for Shiny, especially when it comes to reactive programming.
16 | When you peel back the layers of reactive programming, you won't find a pile of heuristics, special cases, and hacks; instead you'll find a clever, but ultimately fairly straightforward mechanism.
17 | Once you've formed an accurate mental model of reactivity, you'll see that there's nothing up Shiny's sleeves: the magic comes from simple concepts combined in consistent ways.
18 |
19 | In this chapter, we'll motivate reactive programming by trying to do without it and then give a brief history of reactivity as it pertains to Shiny.
20 |
21 | ## Why do we need reactive programming? {#motivation}
22 |
23 | Reactive programming is a style of programming that focuses on values that change over time, and calculations and actions that depend on those values.
24 | Reactivity is important for Shiny apps because they're interactive: users change input controls (dragging sliders, typing in textboxes, checking checkboxes, ...) which causes logic to run on the server (reading CSVs, subsetting data, fitting models, ...) ultimately resulting in outputs updating (plots redrawing, tables updating, ...).
25 | This is quite different from most R code, which typically deals with fairly static data.
26 |
27 | For Shiny apps to be maximally useful, we need reactive expressions and outputs to update if and only if their inputs change.
28 | We want outputs to stay in sync with inputs, while ensuring that we never do more work than necessary.
29 | To see why reactivity is so helpful here, we'll take a stab at solving a simple problem without reactivity.
30 |
31 | ### Why can't you use variables?
32 |
33 | In one sense, you already know how to handle "values that change over time": they're called "variables".
34 | Variables in R represent values and they can change over time, but they're not designed to help you when they change.
35 | Take this simple example of converting a temperature from Celsius to Fahrenheit:
36 |
37 | ```{r}
38 | temp_c <- 10
39 | temp_f <- (temp_c * 9 / 5) + 32
40 | temp_f
41 | ```
42 |
43 | So far so good: the `temp_c` variable has the value `r temp_c`, the `temp_f` variable has the value `r temp_f`, and we can change `temp_c`:
44 |
45 | ```{r}
46 | temp_c <- 30
47 | ```
48 |
49 | But changing `temp_c` does not affect `temp_f`:
50 |
51 | ```{r}
52 | temp_f
53 | ```
54 |
55 | Variables can change over time, but they never change automatically.
56 |
57 | ### What about functions?
58 |
59 | You could instead attack this problem with a function:
60 |
61 | ```{r}
62 | temp_c <- 10
63 | temp_f <- function() {
64 | message("Converting")
65 | (temp_c * 9 / 5) + 32
66 | }
67 | temp_f()
68 | ```
69 |
70 | (This is a slightly weird function because it doesn't have any arguments, instead accessing `temp_c` from its enclosing environment[^reactivity-motivation-1], but it's perfectly valid R code.)
71 |
72 | [^reactivity-motivation-1]: R uses "lexical scoping" for looking up the values associated with variable names.
73 | You can learn more about it in .
74 |
75 | This solves the first problem that reactivity is trying to solve: whenever you access `temp_f()` you get the latest computation:
76 |
77 | ```{r}
78 | temp_c <- -3
79 | temp_f()
80 | ```
81 |
82 | It doesn't, however, minimise computation.
83 | Every time you call `temp_f()` it recomputes, even if `temp_c` hasn't changed:
84 |
85 | ```{r}
86 | temp_f()
87 | ```
88 |
89 | Computation is cheap in this trivial example, so needlessly repeating it isn't a big deal, but it's still unnecessary: if the inputs haven't changed, why do we need to recompute the output?
90 |
91 | ### Event-driven programming {#event-driven}
92 |
93 | Since neither variables nor functions work, we need to create something new.
94 | In previous decades, we would've jumped directly to *event-driven programming*.
95 | Event-driven programming is an appealingly simple paradigm: you register callback functions that will be executed in response to events.
96 |
97 | We could implement a very simple event-driven toolkit using R6, as in the example below.
98 | Here we define a `DynamicValue` that has three important methods: `get()` and `set()` to access and change the underlying value, and `onUpdate()` to register code to run whenever the value is modified.
99 | If you're not familiar with R6, don't worry about the details, and instead focus on following examples.
100 |
101 | ```{r}
102 | DynamicValue <- R6::R6Class("DynamicValue", list(
103 | value = NULL,
104 | on_update = NULL,
105 |
106 | get = function() self$value,
107 |
108 | set = function(value) {
109 | self$value <- value
110 | if (!is.null(self$on_update))
111 | self$on_update(value)
112 | invisible(self)
113 | },
114 |
115 | onUpdate = function(on_update) {
116 | self$on_update <- on_update
117 | invisible(self)
118 | }
119 | ))
120 | ```
121 |
122 | So if Shiny had been invented five years earlier, it might have looked more like this, where `temp_c` uses `<<-`[^reactivity-motivation-2] to update `temp_f` whenever needed.
123 |
124 | [^reactivity-motivation-2]: `<<-` is called the super-assignment operator, and here it modifies `temp_f` in the global environment, rather than creating a new `temp_f` variable inside the function as `<-` would.
125 | You can learn more about `<<-` in .
126 |
127 | ```{r}
128 | temp_c <- DynamicValue$new()
129 | temp_c$onUpdate(function(value) {
130 | message("Converting")
131 | temp_f <<- (value * 9 / 5) + 32
132 | })
133 |
134 | temp_c$set(10)
135 | temp_f
136 |
137 | temp_c$set(-3)
138 | temp_f
139 | ```
140 |
141 | Event-driven programming solves the problem of unnecessary computation, but it creates a new problem: you have to carefully track which inputs affect which computations.
142 | Before long, you start to trade off correctness (just update everything whenever anything changes) against performance (try to update only the necessary parts, and hope that you didn't miss any edge cases) because it's so difficult to do both.
143 |
144 | ### Reactive programming
145 |
146 | Reactive programming elegantly solves both problems by combining features of the solutions above.
147 | Now we can show you some real Shiny code, using a special Shiny mode, `reactiveConsole(TRUE)`, that makes it possible to experiment with reactivity directly in the console.
148 |
149 | ```{r, cache = FALSE}
150 | library(shiny)
151 | reactiveConsole(TRUE)
152 | ```
153 |
154 | As with event-driven programming, we need some way to indicate that we have a special type of variable.
155 | In Shiny, we create a **reactive value** with `reactiveVal()`.
156 | A reactive value has special syntax[^reactivity-motivation-3] for getting its value (calling it like a zero-argument function) and setting its value (set its value by calling it like a one-argument function).
157 |
158 | [^reactivity-motivation-3]: If you happen to have ever used R's active bindings, you might notice that the syntax is very similar.
159 | This is not a coincidence.
160 |
161 | ```{r}
162 | temp_c <- reactiveVal(10) # create
163 | temp_c() # get
164 | temp_c(20) # set
165 | temp_c() # get
166 | ```
167 |
168 | Now we can create a reactive expression that depends on this value:
169 |
170 | ```{r}
171 | temp_f <- reactive({
172 | message("Converting")
173 | (temp_c() * 9 / 5) + 32
174 | })
175 | temp_f()
176 | ```
177 |
178 | As you've learned when creating apps, a reactive expression automatically tracks all of its dependencies.
179 | So that later, if `temp_c` changes, `temp_f` will automatically update:
180 |
181 | ```{r}
182 | temp_c(-3)
183 | temp_c(-10)
184 | temp_f()
185 | ```
186 |
187 | But if `temp_c()` hasn't changed, then `temp_f()` doesn't need to recompute[^reactivity-motivation-4], and can just be retrieved from the cache:
188 |
189 | [^reactivity-motivation-4]: You can tell it doesn't re-compute because "Converting" is not printed.
190 |
191 | ```{r}
192 | temp_f()
193 | ```
194 |
195 | A reactive expression has two important properties:
196 |
197 | - It's **lazy**: it doesn't do any work until it's called.
198 |
199 | - It's **cached**: it doesn't do any work the second and subsequent times it's called because it caches the previous result.
200 |
201 | We'll come back to these important properties in Chapter \@ref(reactive-graph).
202 |
203 | ## A brief history of reactive programming
204 |
205 | If you want to learn more about reactive programming in other languages, a little history might be helpful.
206 | You can see the genesis of reactive programming over 40 years ago in [VisiCalc](https://en.wikipedia.org/wiki/VisiCalc), the first spreadsheet:
207 |
208 | > I imagined a magic blackboard that if you erased one number and wrote a new thing in, all of the other numbers would automatically change, like word processing with numbers.\
209 | > --- [Dan Bricklin](https://youtu.be/YDvbDiJZpy0)
210 |
211 | Spreadsheets are closely related to reactive programming: you declare the relationship between cells using formulas, and when one cell changes, all of its dependencies automatically update.
212 | So you've probably already done a bunch of reactive programming without knowing it!
213 |
214 | While the ideas of reactivity have been around for a long time, it wasn't until the late 1990s that they were seriously studied in academic computer science.
215 | Research in reactive programming was kicked off by FRAN [@fran], **f**unctional **r**eactive **an**imation, a novel system for incorporating changes over time and user input into a functional programming language. This spawned a rich literature [@rp-survey], but had little impact on the practice of programming.
216 |
217 | It wasn't until the 2010s that reactive programming roared into the programming mainstream through the fast-paced world of JavaScript UI frameworks.
218 | Pioneering frameworks like [Knockout](https://knockoutjs.com/), [Ember](https://emberjs.com/), and [Meteor](https://www.meteor.com) (Joe Cheng's personal inspiration for Shiny) demonstrated that reactive programming could make UI programming dramatically easier.
219 | Within a few short years, reactive programming has come to dominate web programming through hugely popular frameworks like [React](https://reactjs.org), [Vue.js](https://vuejs.org), and [Angular](https://angularjs.org), which are all either inherently reactive or designed to work hand-in-hand with reactive back ends.
220 |
221 | It's worth bearing in mind that "reactive programming" is a fairly general term.
222 | While all reactive programming libraries, frameworks, and languages are broadly concerned with writing programs that respond to changing values, they vary enormously in their terminology, designs, and implementations.
223 | In this book, whenever we refer to "reactive programming", we are referring specifically to reactive programming as implemented in Shiny.
224 | So if you read material about reactive programming that isn't specifically about Shiny, it's unlikely that those concepts or even terminology will be relevant to writing Shiny apps.
225 | For readers who do have some experience with other reactive programming frameworks, Shiny's approach is similar to [Meteor](https://www.meteor.com/) and [MobX](https://mobx.js.org/), and very different than the [ReactiveX](http://reactivex.io/) family or anything that labels itself Functional Reactive Programming.
226 |
227 | ## Summary
228 |
229 | Now that you understand why reactive programming is needed and have learned a little bit of history, the next chapter will discuss more details of the underlying theory.
230 | Most importantly, you'll solidify your understanding of the reactive graph, which connects reactive values, reactive expressions, and observers, and controls exactly what is run when.
231 |
--------------------------------------------------------------------------------
/reactivity.Rmd:
--------------------------------------------------------------------------------
1 | # (PART\*) Mastering reactivity {.unnumbered}
2 |
3 | # Introduction {#reactivity-intro .unnumbered}
4 |
5 | You now have a bundle of useful techniques under your belt, giving you the ability to create a wide range of useful apps.
6 | Next we'll turn our attention to the theory of reactivity that underlies the magic of Shiny:
7 |
8 | - In Chapter \@ref(reactive-motivation) you'll learn why the reactivity programming model is needed, and a little bit about the history of reactive programming outside of R.
9 |
10 | - In Chapter \@ref(reactive-graph), you'll learn the full details of the reactive graph, which determines exactly when reactive components are updated.
11 |
12 | - In Chapter \@ref(reactivity-objects), you'll learn about the underlying building blocks, particularly observers and timed invalidation.
13 |
14 | - In Chapter \@ref(reactivity-components), you'll learn how to escape the constraints of the reactive graph using `reactiveVal()` and `observe()`.
15 |
16 | You certainly don't need to understand all these details for routine development of Shiny apps.
17 | But improving your understanding will help you write correct apps from the get-go, and when something behaves unexpectedly you can more quickly narrow in on the underlying issue.
18 |
--------------------------------------------------------------------------------
/references.bib:
--------------------------------------------------------------------------------
1 | @article{rp-survey,
2 | title={A survey on reactive programming},
3 | author={Bainomugisha, Engineer and Carreton, Andoni Lombide and Cutsem, Tom van and Mostinckx, Stijn and Meuter, Wolfgang de},
4 | journal={ACM Computing Surveys (CSUR)},
5 | volume={45},
6 | number={4},
7 | pages={52},
8 | year={2013},
9 | publisher={ACM},
10 | url = {http://soft.vub.ac.be/Publications/2012/vub-soft-tr-12-13.pdf}
11 | }
12 |
13 | @InProceedings{fran,
14 | title = {Functional Reactive Animation},
15 | url = {http://conal.net/papers/icfp97/},
16 | author = {Conal Elliott and Paul Hudak},
17 | booktitle = "International Conference on Functional Programming",
18 | year = 1997
19 | }
20 |
21 | @article{flame-graph,
22 | title={The flame graph},
23 | author={Gregg, Brendan},
24 | journal={Communications of the ACM},
25 | volume={59},
26 | number={6},
27 | pages={48--57},
28 | year={2016},
29 | publisher={ACM New York, NY, USA},
30 | url = {https://queue.acm.org/detail.cfm?id=2927301}
31 | }
32 |
--------------------------------------------------------------------------------
/render128dc48fa5fba.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/render128dc48fa5fba.rds
--------------------------------------------------------------------------------
/rmarkdown-report/app.R:
--------------------------------------------------------------------------------
1 | library(shiny)
2 |
3 | # Copy report to temporary directory. This is mostly important when
4 | # deploying the app, since often the working directory won't be writable
5 | report_path <- tempfile(fileext = ".Rmd")
6 | file.copy("report.Rmd", report_path, overwrite = TRUE)
7 |
8 | render_report <- function(input, output, params) {
9 | rmarkdown::render(input,
10 | output_file = output,
11 | params = params,
12 | envir = new.env(parent = globalenv())
13 | )
14 | }
15 |
16 | ui <- fluidPage(
17 | sliderInput("n", "Number of points", 1, 100, 50),
18 | downloadButton("report", "Generate report")
19 | )
20 |
21 | server <- function(input, output) {
22 | output$report <- downloadHandler(
23 | filename = "report.html",
24 | content = function(file) {
25 | params <- list(n = input$n)
26 | callr::r(
27 | render_report,
28 | list(input = report_path, output = file, params = params)
29 | )
30 | }
31 | )
32 | }
33 |
34 | shinyApp(ui, server)
35 |
--------------------------------------------------------------------------------
/rmarkdown-report/report.Rmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Dynamic report"
3 | output: html_document
4 | params:
5 | n: NA
6 | ---
7 |
8 | A plot of `r params$n` random points.
9 |
10 | ```{r}
11 | plot(rnorm(params$n), rnorm(params$n))
12 | ```
13 |
--------------------------------------------------------------------------------
/sales-dashboard/app.R:
--------------------------------------------------------------------------------
1 | library(dplyr)
2 | library(shiny)
3 |
4 | # https://www.kaggle.com/kyanyoga/sample-sales-data
5 | # License: CC-0
6 | sales <- vroom::vroom("sales_data_sample.csv", na = "")
7 |
8 | library(shiny)
9 | ui <- fluidPage(
10 | titlePanel("Sales Dashboard"),
11 | sidebarLayout(
12 | sidebarPanel(
13 | selectInput("territory", "Territory", choices = unique(sales$TERRITORY)),
14 | selectInput("customername", "Customer", choices = NULL),
15 | selectInput("ordernumber", "Order number", choices = NULL, size = 5, selectize = FALSE),
16 | ),
17 | mainPanel(
18 | uiOutput("customer"),
19 | tableOutput("data")
20 | )
21 | )
22 | )
23 | server <- function(input, output, session) {
24 | territory <- reactive({
25 | req(input$territory)
26 | filter(sales, TERRITORY == input$territory)
27 | })
28 | customer <- reactive({
29 | req(input$customername)
30 | filter(territory(), CUSTOMERNAME == input$customername)
31 | })
32 |
33 | output$customer <- renderUI({
34 | row <- customer()[1, ]
35 | tags$div(
36 | class = "well",
37 | tags$p(tags$strong("Name: "), row$CUSTOMERNAME),
38 | tags$p(tags$strong("Phone: "), row$PHONE),
39 | tags$p(tags$strong("Contact: "), row$CONTACTFIRSTNAME, " ", row$CONTACTLASTNAME)
40 | )
41 | })
42 |
43 | order <- reactive({
44 | req(input$ordernumber)
45 | customer() %>%
46 | filter(ORDERNUMBER == input$ordernumber) %>%
47 | arrange(ORDERLINENUMBER) %>%
48 | select(PRODUCTLINE, QUANTITYORDERED, PRICEEACH, SALES, STATUS)
49 | })
50 |
51 | output$data <- renderTable(order())
52 |
53 | observeEvent(territory(), {
54 | updateSelectInput(session, "customername", choices = unique(territory()$CUSTOMERNAME), selected = character())
55 | })
56 | observeEvent(customer(), {
57 | updateSelectInput(session, "ordernumber", choices = unique(customer()$ORDERNUMBER))
58 | })
59 |
60 | }
61 | shinyApp(ui, server)
62 |
--------------------------------------------------------------------------------
/sales-dashboard/sales_data_sample.csv:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/sales-dashboard/sales_data_sample.csv
--------------------------------------------------------------------------------
/scaling-general.Rmd:
--------------------------------------------------------------------------------
1 | # General guidelines {#best-practices}
2 |
3 | ## Introduction
4 |
5 | This chapter introduces the most important software engineering skills you'll need when writing Shiny apps: code organisation, testing, dependency management, source code control, continuous integration, and code reviews.
6 | These skills are not specific to Shiny apps, but you'll need to learn a bit about all of them if you want to write complex apps that get easier to maintain over time, not harder.
7 |
8 | Improving your software engineering skills is a lifelong journey.
9 | Expect to have frustrations as you start learning them, but understand that everyone experiences the same issues, and if you persevere you'll get past them.
10 | Most people go through the same evolution when learning a new technique: "I don't understand it and have to look it up every time I use it" to "I vaguely understand it but still read the documentation a lot" to eventually "I understand it and can use it fluidly".
11 | It takes time and practice to get to the final stage.
12 |
13 | I recommend setting aside some time each week to practice your software development skills.
14 | During this time, try to avoid touching the behaviour or appearance of your app, but instead focus your efforts on making the app easier to understand and develop.
15 | This will make your app easier to change in the future, and as you improve your software development skills your first attempt at an app will also become higher quality.
16 |
17 | (Thanks to my colleague Jeff Allen for contributing the bulk of this chapter)
18 |
19 | ## Code organization
20 |
21 | > Any fool can write code that a computer can understand.
22 | > Good programmers write code that humans can understand.
23 | > --- Martin Fowler
24 |
25 | One of the most obvious ways to improve the quality of an application is to improve the readability and understandability of its code.
26 | The best programmers in the world can't maintain a code-base that they can't understand, so this is a good place to start.
27 |
28 | Being a good programmer means developing empathy for others who will need to interact with this code-base in the future (even if it's just future-you!).
29 | Like all forms of empathy, this takes practice and becomes easier only after you've done it many times.
30 | Over time, you'll start to notice that certain practices improve the readability of your code.
31 | There are no universal rules, but some general guidelines include:
32 |
33 | - Are the variable and function names clear and concise?
34 | If not, what names would better communicate the intent of the code?
35 |
36 | - Do I have comments where needed to explain complex bits of code?
37 |
38 | - Does this whole function fit on my screen or could it be printed on a single piece of paper?
39 | If not, is there a way to break it up into smaller pieces?
40 |
41 | - Am I copying-and-pasting the same block of code many times throughout my app?
42 | If so, is there a way to use a function or a variable to avoid the repetition?
43 |
44 | - Are all the parts of my application tangled together, or can I manage the different components of my application in isolation?
45 |
46 | There's no silver bullet to address all of these points---and many times they involve subjective judgement calls---but there are two particularly important tools:
47 |
48 | - **Functions**, the topic of Chapter \@ref(scaling-functions), allow you to reduce duplication in your UI code, make your server functions easier to understand and test, and allow you to more flexibly organise your app code.
49 |
50 | - **Shiny modules**, the topic of Chapter \@ref(scaling-modules), make it possible to write isolated, re-usable code, that coordinates front end and back end behaviour.
51 | Modules allow you to gracefully separate concerns so that (e.g.) individual pages in your application can operate independently, or repeated components no longer need to be copied and pasted.
52 |
53 | ## Testing
54 |
55 | Developing a test plan for an application is critical to ensure its ongoing stability.
56 | Without a test plan, every change jeopardizes the application.
57 | When the application is small enough that you can hold it all in your head, you might feel that there's no need for an additional test plan.
58 | And sure, testing very simple apps can seem like more trouble than its worth.
59 | However, the lack of a plan is likely to cause pain as soon as someone else starts contributing to your app, or when you've spent enough time away from it that you've forgotten how it all fits together.
60 |
61 | A testing plan could be entirely manual.
62 | A great place to start is a simple text file giving a script to follow to check that all is well.
63 | However, that script will have to grow as the application becomes more complex, and you'll either spend more and more of your time manually testing the application, or you'll start skipping some of the script.
64 |
65 | So the next step is to start to automate some of your testing.
66 | Automation takes time to set up, but it pays off over time because you can run the tests more frequently.
67 | For that reason, various forms of automated testing have been developed for Shiny, as outlined in Chapter \@ref(scaling-testing).
68 | As that chapter will explain, you can develop:
69 |
70 | - Unit tests that confirm the correct behaviour of an individual function.
71 | - Integration tests to confirm the interactions between reactives.
72 | - Functional tests to validate the end-to-end experience from a browser
73 | - Load tests to ensure that the application can withstand the amount of traffic you anticipate for it.
74 |
75 | The beauty of writing an automated test is that once you've taken the time to write it, you'll never need to manually test that portion of the application again.
76 | You can even leverage continuous integration (more on that shortly) to run these tests every time you make a change to your code before publishing the application.
77 |
78 | ## Dependency management
79 |
80 | If you've ever tried to reproduce some analysis in R written by someone else, or even tried to rerun some analysis or Shiny application you wrote some time ago, you may have run into trouble around dependencies.
81 | An app's dependencies are anything beyond the source code that it requires to run.
82 | These could include files on the hard drive, an external database or API, or other R packages that are used by the app.
83 |
84 | For any analysis that you may want to reproduce in the future, consider using [renv](https://rstudio.github.io/renv/) which enables you to create **r**eproducible R **env**ironments.
85 | Using renv, you can capture the exact package versions that your application uses so that when you go to use this application on another computer, you can use exactly the same package versions.
86 | This is vital for apps run in production, not just because it gets the versions right on the first run, but because it also isolates your app from version changes over time.
87 |
88 | Another tool for managing dependencies is the [config package](https://github.com/rstudio/config).
89 | The config package doesn't actually manage dependencies itself, but it does provide a convenient place for you to track and manage dependencies other than R packages.
90 | For instance, you might specify the path to a CSV file that your application depends on, or the URL of an API that you require.
91 | Having these enumerated in the config file gives you a single place where you can track and manage these dependencies.
92 | Even better, it enables you to create different configurations for different environments.
93 | For example, if your application analyses a database with lots of data, you might choose to configure a few different environments:
94 |
95 | - In the production environment, you connect the app to the real "production" database.
96 |
97 | - In a test environment, you can configure the app to use a test database so that you properly exercise the database connections in your tests but you don't risk corrupting your production database if you accidentally make a change that corrupts the data.
98 |
99 | - In development, you might configure the application to use a small CSV with a subset of data to allow for faster iterating.
100 |
101 | Lastly, be wary of making assumptions about the local file system.
102 | If your code has references to data at `C:\data\cars.csv` or `~/my-projects/genes.rds`, for example, you need to realise that it's very unlikely that these files will exist on another computer.
103 | Instead, either use a path relative to the app directory (e.g. `data/cars.csv` or `genes.rds`), or use the config package to make the external path explicit and configurable.
104 |
105 | ## Source code management
106 |
107 | Anyone who's been programming for a long time has inevitably arrived at a state where they've accidentally broken their app and want to roll back to a previous working state.
108 | This is incredibly arduous when done manually.
109 | Fortunately, however, you can rely on a "version-control system" that makes it easy to track atomic changes, roll back to previous work, and integrate the work of multiple contributors.
110 |
111 | The most popular version-control system in the R community is Git.
112 | Git is typically paired with GitHub, a website that makes it easy to share your git repos with others.
113 | It definitely takes work to become proficient with Git and GitHub, but any experienced developer will confirm that the effort is well worth it.
114 | If you're new to git, I'd highly recommend starting with [*Happy Git and GitHub for the useR*](https://happygitwithr.com/), by Jenny Bryan.
115 |
116 | ## Continuous integration/deployment (CI, CD)
117 |
118 | Once you are using a version control system and have a robust set of automated tests, you might benefit from continuous integration (CI).
119 | CI is a way to perpetually validate that the changes you're making to your application haven't broken anything.
120 | You can use it retroactively (to notify you if a change you just made broke your application) or proactively (to notify you if a *proposed* change would break your app).
121 |
122 | There are a variety of services that can connect to a Git repo and automatically run tests when you push a new commit or propose changes.
123 | Depending on where your code is hosted, you can consider [GitHub actions](https://github.com/features/actions), [Travis CI](https://travis-ci.org/), [Azure Pipelines](https://azure.microsoft.com/en-us/services/devops/pipelines/), [AppVeyor](https://www.appveyor.com/), [Jenkins](https://jenkins.io/), or [GitLab CI/CD](https://about.gitlab.com/product/continuous-integration/), to name a few.
124 |
125 | ```{r ci, echo = FALSE, out.width = NULL, fig.cap="An example CI run, showing successful results across four independent testing environments"}
126 | knitr::include_graphics("images/prod-best-practices/ci-screenshot.png", dpi = 300)
127 | ```
128 |
129 | Figure \@ref(fig:ci) shows what this looks like when a CI system is connected to GitHub to test pull requests.
130 | As you can see, all the CI tests show green checks, meaning that each of the automated test environments were successful.
131 | If any of the tests had failed, you would be alerted to the failure before you merge the changes into your app.
132 | Having a CI process not only prevents experienced developers from making accidental mistakes, but also helps new contributors feel confident in their changes.
133 |
134 | ## Code reviews
135 |
136 | Many software companies have found the benefits of having someone else review code before it's formally incorporated into a code base.
137 | This process of "code review" has a number of benefits:
138 |
139 | - Catches bugs before they get incorporated into the application making them much less expensive to fix.
140 |
141 | - Offers teaching opportunities --- programmers at all levels often learn something new by reviewing others' code or by having their code reviewed.
142 |
143 | - Facilitates cross-pollination and knowledge sharing across a team to eliminate having only one person who understands the app.
144 |
145 | - The resulting conversation often improves the readability of the code.
146 |
147 | Typically, a code review involves someone other than you, but you can still benefit even if it's only you.
148 | Most experienced developers will agree that taking a moment to review your own code often reveals some small flaw, particularly if you can let it sit for at least a few hours between writing and review.
149 |
150 | Here are few questions to hold in your head when reviewing code:
151 |
152 | - Do new functions have concise but evocative names?
153 |
154 | - Are there parts of the code you find confusing?
155 |
156 | - What areas are likely to change in the future, and would particularly benefit from automated testing?
157 |
158 | - Does the style of the code match the rest of the app?
159 | (Or even better, your group's documented code style.)
160 |
161 | If you're embedded in an organisation with a strong engineering culture, setting up code reviews for data science code should be relatively straightforward, and you'll have existing tools and experience to draw on.
162 | If you're in an organisation that has few other software engineers, you may need to do more convincing.
163 |
164 | Two resources I'd recommend:
165 |
166 | -
167 | -
168 |
169 | ## Summary
170 |
171 | Now that you've learned a little bit of the software engineer mindset, the next chapters are going to dive into the details of function writing, testing, security, and performance as they apply to Shiny apps.
172 | You'll need to read Chapter \@ref(scaling-functions) before the other chapters, but otherwise you can skip around.
173 |
--------------------------------------------------------------------------------
/scaling-security.Rmd:
--------------------------------------------------------------------------------
1 | # Security {#scaling-security}
2 |
3 | ```{r, include = FALSE}
4 | source("common.R")
5 | ```
6 |
7 | Most Shiny apps are deployed within a company firewall and since you can generally assume that your colleagues aren't going to try and hack your app[^scaling-security-1], you don't need to think about security.
8 | If, however, your app contains data that only some of your colleagues should be able to access, or you want to expose your app to the public, you will need to spend some time on security.
9 | When securing your app, there are two main things to protect:
10 |
11 | [^scaling-security-1]: If you can't assume that, you have bigger problems!
12 | That said, some companies do have a "zero-trust" model, so you should double check with your IT team.
13 |
14 | - Your data: you want to make sure an attacker can't access any sensitive data.
15 |
16 | - Your compute resources: you want to make sure an attacker can't mine bitcoin or use your server as part of a spam farm.
17 |
18 | Fortunately your job is made a little easier because security is a team sport.
19 | Whoever deploys your app is responsible for security **between** apps, ensuring that app A can't access the code or data in app B, and can't steal all the memory and compute power on the server.
20 | Your responsibility is the security **within** your app, making sure that an attacker can't abuse your app to achieve their ends.
21 | This chapter will give the basics of securing your Shiny, broken down into securing your data and securing your compute resources.
22 |
23 | If you're interested in learning a little more about security and R in general, I highly recommend Colin Gillespie's entertaining and educational useR!
24 | 2019 talk, "[R and Security](https://www.youtube.com/watch?v=5odJxZj9LE4)".
25 |
26 | ```{r setup}
27 | library(shiny)
28 | ```
29 |
30 | ## Data
31 |
32 | The most sensitive data is stuff like personally identifying information (PII), regulated data, credit card data, health data, or anything else that would be a legal nightmare for your company if was made public.
33 | Fortunately, most Shiny apps don't deal with those types of data[^scaling-security-2], but there is an important type of data you do need to worry about: passwords.
34 | You should never include passwords in the source code of your app.
35 | Instead either put them in environment variables, or if you have many use the [config](https://github.com/rstudio/config) package.
36 | Either way, make sure that they are never included in your source code control by adding the appropriate files to `.gitignore`. I also recommend documenting how a new developer can get the appropriate credentials.
37 |
38 | [^scaling-security-2]: If your app does work these types of data, it's imperative that you partner with a software engineer with security expertise.
39 |
40 | Alternatively, you may have data that is user-specific.
41 | If you need to **authenticate** users, i.e. identify them through a user name and password, never attempt to roll a solution yourself.
42 | There are just too many things that might go wrong.
43 | Instead, you'll need to work with your IT team to design a secure access mechanism.
44 | You can see some best practices at and .
45 | Note that code within `server()` is isolated so there's no way for one user session to see data from another.
46 | The only exception is if you use caching --- see Section \@ref(cache-scope) for details.
47 |
48 | Finally, note that Shiny inputs use client-side validation, i.e. the checks for valid input are performed by JavaScript in the browser, not by R.
49 | This means it's possible for a knowledgeable attacker to send values that you don't expect.
50 | For example, take this simple app:
51 |
52 | ```{r, eval = FALSE}
53 | secrets <- list(
54 | a = "my name",
55 | b = "my birthday",
56 | c = "my social security number",
57 | d = "my credit card"
58 | )
59 |
60 | allowed <- c("a", "b")
61 | ui <- fluidPage(
62 | selectInput("x", "x", choices = allowed),
63 | textOutput("secret")
64 | )
65 | server <- function(input, output, session) {
66 | output$secret <- renderText({
67 | secrets[[input$x]]
68 | })
69 | }
70 | ```
71 |
72 | You might expect that a user could access my name and birthday, but not my social security number or credit card details.
73 | But a knowledgeable attacker can open up a JavaScript console in their browser and run `Shiny.setInputValue("x", "c")` to see my SSN.
74 | So to be safe, you need to check all user inputs from your R code:
75 |
76 | ```{r}
77 | server <- function(input, output, session) {
78 | output$secret <- renderText({
79 | req(input$x %in% allowed)
80 | secrets[[input$x]]
81 | })
82 | }
83 | ```
84 |
85 | I deliberately didn't create a user friendly error message --- the only time you'd see it was if you're trying to break the app, and we don't need to help out an attacker.
86 |
87 | ## Compute resources
88 |
89 | It's hopefully obvious that the following app is very dangerous, because it allows the user to run any R code they want.
90 | They could delete important files, modify data, or send confidential data back to the user of the app.
91 |
92 | ```{r}
93 | ui <- fluidPage(
94 | textInput("code", "Enter code here"),
95 | textOutput("results")
96 | )
97 | server <- function(input, output, session) {
98 | output$results <- renderText({
99 | eval(parse(text = input$code))
100 | })
101 | }
102 | ```
103 |
104 | In general, the combination of `parse()` and `eval()` is a big warning sign for any Shiny app[^scaling-security-3]: they instantly make your app vulnerable.
105 | Similarly, you should never `source()` an uploaded `.R` file, or `rmarkdown::render()` an uploaded `.Rmd`. But these cases are pretty obvious, and are unlikely to be source of real problems.
106 |
107 | [^scaling-security-3]: The only exception is if they don't involve user-supplied data in any way.
108 |
109 | The bigger challenge arises because there are a number of functions that `parse()`, `eval()`, or both, in a way that you're not aware of.
110 | Here are the most common:
111 |
112 | - **Model formulas**.
113 | It's possible to construct a model that executes arbitrary R code:
114 |
115 | ```{r}
116 | df <- data.frame(x = 1:5, y = runif(5))
117 | mod <- lm(y ~ {print("Hi!"); x}, data = df)
118 | ```
119 |
120 | This makes it difficult to safely allow a user to define their own models.
121 |
122 | - **Glue labels**.
123 | The glue package provides a powerful way to create strings from data:
124 |
125 | ```{r}
126 | title <- "foo"
127 | number <- 1
128 | glue::glue("{title}-{number}")
129 | ```
130 |
131 | But `glue()` evaluates anything inside of `{}`:
132 |
133 | ```{r}
134 | glue::glue("{title}-{print('Hi'); number}")
135 | ```
136 |
137 | If you want to allow a user to supply a glue string to generate a label, instead use `glue::glue_safe()` which only looks up variable names and doesn't evaluate code:
138 |
139 | ```{r, error = TRUE}
140 | glue::glue_safe("{title}-{number}")
141 | glue::glue_safe("{title}-{print('Hi'); number}")
142 | ```
143 |
144 | - **Variable transformation.** There's no way to safely allow a user to provide code snippets to transform a variable for dplyr or ggplot2.
145 | You might expect they'll write `log10(x)` but they could write `{print("Hi"); log10(x)}`
146 |
147 | This also means that you should never use the older `ggplot2::aes_string()` with user supplied input.
148 | Instead, stick with the techniques in Chapter \@ref(action-tidy).
149 |
150 | The same problem can occur with SQL.
151 | For example, if you construct SQL with `paste()`, e.g.:
152 |
153 | ```{r}
154 | find_student <- function(name) {
155 | paste0("SELECT * FROM Students WHERE name = ('", name, "');")
156 | }
157 | find_student("Hadley")
158 | ```
159 |
160 | An attacker can provide a malicious username:[^scaling-security-4]
161 |
162 | [^scaling-security-4]: [\](https://xkcd.com/327/){.uri}
163 |
164 | ```{r}
165 | find_student("Robert'); DROP TABLE Students; --")
166 | ```
167 |
168 | This looks a bit odd, but it's a valid SQL query in three parts:
169 |
170 | - `SELECT * FROM Students WHERE name = ('Robert');` finds a student with name Robert.
171 |
172 | - `DROP TABLE Students;` deletes the `Students` table (!!).
173 |
174 | - `--'` is a comment needed to prevent the extra `'` from turning into a syntax error.
175 |
176 | To avoid this problem, never generate SQL strings with paste and instead use system that automatically escapes user input (like [dbplyr](https://dbplyr.tidyverse.org)), or use `glue::glue_sql()`:
177 |
178 | ```{r}
179 | con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:")
180 | find_student <- function(name) {
181 | glue::glue_sql("SELECT * FROM Students WHERE name = ({name});", .con = con)
182 | }
183 | find_student("Robert'); DROP TABLE Students; --")
184 | ```
185 |
186 | It's a little hard to tell at first glance, but this is safe, because SQL's equivalent of `\'` is `''` so the query returns all rows of the `Students` table where the name is literally "Robert'); DROP TABLE Students; --".
187 |
--------------------------------------------------------------------------------
/scaling-testing.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadley/mastering-shiny/6ca51f8fd52ac73c43cfc69ea1444c8832db751e/scaling-testing.rds
--------------------------------------------------------------------------------
/scaling.Rmd:
--------------------------------------------------------------------------------
1 | # (PART\*) Best practices {.unnumbered}
2 |
3 | # Introduction {#scaling-intro .unnumbered}
4 |
5 | When you start using Shiny, it'll take you a long time to make even small apps, because you have to learn the fundamentals.
6 | Over time, however, you'll become more comfortable with the basic interface of the package and the key ideas of reactivity, and you'll be able to create larger, more complex applications.
7 | As you start to write larger apps, you'll encounter a new set of challenges: keeping a complex and growing code-base organized, stable, and maintainable.
8 | This will include problems like:
9 |
10 | - "I can't find the code I'm looking for in this huge file."
11 |
12 | - "I haven't worked on this code in 6 months and I'm afraid I'm going to break it if I make any changes."
13 |
14 | - "Someone else started working with me on the application and we keep standing on each others toes."
15 |
16 | - "The app works on my computer but doesn't work on my collaborator's or in production."
17 |
18 | In this, the "best practices", part of the book, you'll learn some key concepts and tools from software engineering that will help you overcome these challenges:
19 |
20 | - In Chapter \@ref(best-practices), I'll briefly introduce you to the big ideas of software engineering.
21 |
22 | - In Chapter \@ref(scaling-functions), I'll show you how to extract code out of your Shiny app into independent apps, and discuss why you might want to do so.
23 |
24 | - In Chapter \@ref(scaling-modules), you'll learn about Shiny's module system, which allows you to extract coupled UI and server code into isolated and reusable components.
25 |
26 | - In Chapter \@ref(scaling-packaging), I'll show you how to turn your app into an R package, and motivate why that investment will pay off for bigger apps.
27 |
28 | - In Chapter \@ref(scaling-testing), you'll learn how to turn your existing informal tests into automated tests that can easily be re-run whenever your app changes.
29 |
30 | - In Chapter \@ref(performance), you'll learn how to identify and resolve performance bottlenecks in your apps, ensuring they remain speedy even when used by hundreds of users.
31 |
32 | Of course you can't learn everything about software engineering in one part of one book, so I'll also point you to good places to learn more.
33 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | p.caption {
2 | color: #777;
3 | margin-top: 10px;
4 | }
5 | p code {
6 | white-space: inherit;
7 | }
8 | pre {
9 | word-break: normal;
10 | word-wrap: normal;
11 | }
12 | pre code {
13 | white-space: inherit;
14 | }
15 |
16 |
17 | /* Sidebar formating --------------------------------------------*/
18 |
19 | div.sidebar {
20 | border-left: 7px solid #ccc;
21 | margin: 1em 0;
22 | padding-left: 1em;
23 | }
24 |
25 | /* .book .book-body .page-wrapper .page-inner section.normal is needed
26 | to override the styles produced by gitbook, which are ridiculously
27 | overspecified. Goal of the selectors is to ensure internal "margins"
28 | controlled only by padding of container */
29 |
30 | .book .book-body .page-wrapper .page-inner section.normal div.sidebar > :first-child {
31 | margin-top: 0;
32 | }
33 |
34 | .book .book-body .page-wrapper .page-inner section.normal div.sidebar > :last-child {
35 | margin-bottom: 0;
36 | }
37 |
--------------------------------------------------------------------------------
/toc.css:
--------------------------------------------------------------------------------
1 | #TOC ul,
2 | #TOC li,
3 | #TOC span,
4 | #TOC a {
5 | margin: 0;
6 | padding: 0;
7 | position: relative;
8 | }
9 | #TOC {
10 | line-height: 1;
11 | border-radius: 5px 5px 0 0;
12 | background: #141414;
13 | background: linear-gradient(to bottom, #333333 0%, #141414 100%);
14 | border-bottom: 2px solid #0fa1e0;
15 | width: auto;
16 | }
17 | #TOC:after,
18 | #TOC ul:after {
19 | content: '';
20 | display: block;
21 | clear: both;
22 | }
23 | #TOC a {
24 | background: #141414;
25 | background: linear-gradient(to bottom, #333333 0%, #141414 100%);
26 | color: #ffffff;
27 | display: block;
28 | padding: 19px 20px;
29 | text-decoration: none;
30 | text-shadow: none;
31 | }
32 | #TOC ul {
33 | list-style: none;
34 | }
35 | #TOC > ul > li {
36 | display: inline-block;
37 | float: left;
38 | margin: 0;
39 | }
40 | #TOC > ul > li > a {
41 | color: #ffffff;
42 | }
43 | #TOC > ul > li:hover:after {
44 | content: '';
45 | display: block;
46 | width: 0;
47 | height: 0;
48 | position: absolute;
49 | left: 50%;
50 | bottom: 0;
51 | border-left: 10px solid transparent;
52 | border-right: 10px solid transparent;
53 | border-bottom: 10px solid #0fa1e0;
54 | margin-left: -10px;
55 | }
56 | #TOC > ul > li:first-child > a {
57 | border-radius: 5px 0 0 0;
58 | }
59 | #TOC.align-right > ul > li:first-child > a,
60 | #TOC.align-center > ul > li:first-child > a {
61 | border-radius: 0;
62 | }
63 | #TOC.align-right > ul > li:last-child > a {
64 | border-radius: 0 5px 0 0;
65 | }
66 | #TOC > ul > li.active > a,
67 | #TOC > ul > li:hover > a {
68 | color: #ffffff;
69 | box-shadow: inset 0 0 3px #000000;
70 | background: #070707;
71 | background: linear-gradient(to bottom, #262626 0%, #070707 100%);
72 | }
73 | #TOC .has-sub {
74 | z-index: 1;
75 | }
76 | #TOC .has-sub:hover > ul {
77 | display: block;
78 | }
79 | #TOC .has-sub ul {
80 | display: none;
81 | position: absolute;
82 | width: 200px;
83 | top: 100%;
84 | left: 0;
85 | }
86 | #TOC .has-sub ul li a {
87 | background: #0fa1e0;
88 | border-bottom: 1px dotted #31b7f1;
89 | filter: none;
90 | display: block;
91 | line-height: 120%;
92 | padding: 10px;
93 | color: #ffffff;
94 | }
95 | #TOC .has-sub ul li:hover a {
96 | background: #0c7fb0;
97 | }
98 | #TOC ul ul li:hover > a {
99 | color: #ffffff;
100 | }
101 | #TOC .has-sub .has-sub:hover > ul {
102 | display: block;
103 | }
104 | #TOC .has-sub .has-sub ul {
105 | display: none;
106 | position: absolute;
107 | left: 100%;
108 | top: 0;
109 | }
110 | #TOC .has-sub .has-sub ul li a {
111 | background: #0c7fb0;
112 | border-bottom: 1px dotted #31b7f1;
113 | }
114 | #TOC .has-sub .has-sub ul li a:hover {
115 | background: #0a6d98;
116 | }
117 | #TOC ul ul li.last > a,
118 | #TOC ul ul li:last-child > a,
119 | #TOC ul ul ul li.last > a,
120 | #TOC ul ul ul li:last-child > a,
121 | #TOC .has-sub ul li:last-child > a,
122 | #TOC .has-sub ul li.last > a {
123 | border-bottom: 0;
124 | }
125 | #TOC ul {
126 | font-size: 1.2rem;
127 | }
128 |
--------------------------------------------------------------------------------