diff --git a/_extensions/quarto-ext/fontawesome/_extension.yml b/_extensions/quarto-ext/fontawesome/_extension.yml new file mode 100644 index 0000000..c0787a8 --- /dev/null +++ b/_extensions/quarto-ext/fontawesome/_extension.yml @@ -0,0 +1,7 @@ +title: Font Awesome support +author: Carlos Scheidegger +version: 1.1.0 +quarto-required: ">=1.2.269" +contributes: + shortcodes: + - fontawesome.lua diff --git a/_extensions/quarto-ext/fontawesome/assets/css/all.css b/_extensions/quarto-ext/fontawesome/assets/css/all.css new file mode 100644 index 0000000..3e24980 --- /dev/null +++ b/_extensions/quarto-ext/fontawesome/assets/css/all.css @@ -0,0 +1,7971 @@ +/*! + * Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + * Copyright 2023 Fonticons, Inc. + */ +.fa { + font-family: var(--fa-style-family, "Font Awesome 6 Free"); + font-weight: var(--fa-style, 900); } + +.fa, +.fa-classic, +.fa-sharp, +.fas, +.fa-solid, +.far, +.fa-regular, +.fab, +.fa-brands, +.fal, +.fa-light, +.fat, +.fa-thin, +.fad, +.fa-duotone { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: var(--fa-display, inline-block); + font-style: normal; + font-variant: normal; + line-height: 1; + text-rendering: auto; } + +.fas, +.fa-classic, +.fa-solid, +.far, +.fa-regular { + font-family: 'Font Awesome 6 Free'; } + +.fab, +.fa-brands { + font-family: 'Font Awesome 6 Brands'; } + +.fa-1x { + font-size: 1em; } + +.fa-2x { + font-size: 2em; } + +.fa-3x { + font-size: 3em; } + +.fa-4x { + font-size: 4em; } + +.fa-5x { + font-size: 5em; } + +.fa-6x { + font-size: 6em; } + +.fa-7x { + font-size: 7em; } + +.fa-8x { + font-size: 8em; } + +.fa-9x { + font-size: 9em; } + +.fa-10x { + font-size: 10em; } + +.fa-2xs { + font-size: 0.625em; + line-height: 0.1em; + vertical-align: 0.225em; } + +.fa-xs { + font-size: 0.75em; + line-height: 0.08333em; + vertical-align: 0.125em; } + +.fa-sm { + font-size: 0.875em; + line-height: 0.07143em; + vertical-align: 0.05357em; } + +.fa-lg { + font-size: 1.25em; + line-height: 0.05em; + vertical-align: -0.075em; } + +.fa-xl { + font-size: 1.5em; + line-height: 0.04167em; + vertical-align: -0.125em; } + +.fa-2xl { + font-size: 2em; + line-height: 0.03125em; + vertical-align: -0.1875em; } + +.fa-fw { + text-align: center; + width: 1.25em; } + +.fa-ul { + list-style-type: none; + margin-left: var(--fa-li-margin, 2.5em); + padding-left: 0; } + .fa-ul > li { + position: relative; } + +.fa-li { + left: calc(var(--fa-li-width, 2em) * -1); + position: absolute; + text-align: center; + width: var(--fa-li-width, 2em); + line-height: inherit; } + +.fa-border { + border-color: var(--fa-border-color, #eee); + border-radius: var(--fa-border-radius, 0.1em); + border-style: var(--fa-border-style, solid); + border-width: var(--fa-border-width, 0.08em); + padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); } + +.fa-pull-left { + float: left; + margin-right: var(--fa-pull-margin, 0.3em); } + +.fa-pull-right { + float: right; + margin-left: var(--fa-pull-margin, 0.3em); } + +.fa-beat { + -webkit-animation-name: fa-beat; + animation-name: fa-beat; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); + animation-timing-function: var(--fa-animation-timing, ease-in-out); } + +.fa-bounce { + -webkit-animation-name: fa-bounce; + animation-name: fa-bounce; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); } + +.fa-fade { + -webkit-animation-name: fa-fade; + animation-name: fa-fade; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); } + +.fa-beat-fade { + -webkit-animation-name: fa-beat-fade; + animation-name: fa-beat-fade; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); } + +.fa-flip { + -webkit-animation-name: fa-flip; + animation-name: fa-flip; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); + animation-timing-function: var(--fa-animation-timing, ease-in-out); } + +.fa-shake { + -webkit-animation-name: fa-shake; + animation-name: fa-shake; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, linear); + animation-timing-function: var(--fa-animation-timing, linear); } + +.fa-spin { + -webkit-animation-name: fa-spin; + animation-name: fa-spin; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 2s); + animation-duration: var(--fa-animation-duration, 2s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, linear); + animation-timing-function: var(--fa-animation-timing, linear); } + +.fa-spin-reverse { + --fa-animation-direction: reverse; } + +.fa-pulse, +.fa-spin-pulse { + -webkit-animation-name: fa-spin; + animation-name: fa-spin; + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, steps(8)); + animation-timing-function: var(--fa-animation-timing, steps(8)); } + +@media (prefers-reduced-motion: reduce) { + .fa-beat, + .fa-bounce, + .fa-fade, + .fa-beat-fade, + .fa-flip, + .fa-pulse, + .fa-shake, + .fa-spin, + .fa-spin-pulse { + -webkit-animation-delay: -1ms; + animation-delay: -1ms; + -webkit-animation-duration: 1ms; + animation-duration: 1ms; + -webkit-animation-iteration-count: 1; + animation-iteration-count: 1; + -webkit-transition-delay: 0s; + transition-delay: 0s; + -webkit-transition-duration: 0s; + transition-duration: 0s; } } + +@-webkit-keyframes fa-beat { + 0%, 90% { + -webkit-transform: scale(1); + transform: scale(1); } + 45% { + -webkit-transform: scale(var(--fa-beat-scale, 1.25)); + transform: scale(var(--fa-beat-scale, 1.25)); } } + +@keyframes fa-beat { + 0%, 90% { + -webkit-transform: scale(1); + transform: scale(1); } + 45% { + -webkit-transform: scale(var(--fa-beat-scale, 1.25)); + transform: scale(var(--fa-beat-scale, 1.25)); } } + +@-webkit-keyframes fa-bounce { + 0% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 10% { + -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); } + 30% { + -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); } + 50% { + -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); } + 57% { + -webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); + transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); } + 64% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 100% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } } + +@keyframes fa-bounce { + 0% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 10% { + -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); } + 30% { + -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); } + 50% { + -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); } + 57% { + -webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); + transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); } + 64% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 100% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } } + +@-webkit-keyframes fa-fade { + 50% { + opacity: var(--fa-fade-opacity, 0.4); } } + +@keyframes fa-fade { + 50% { + opacity: var(--fa-fade-opacity, 0.4); } } + +@-webkit-keyframes fa-beat-fade { + 0%, 100% { + opacity: var(--fa-beat-fade-opacity, 0.4); + -webkit-transform: scale(1); + transform: scale(1); } + 50% { + opacity: 1; + -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); + transform: scale(var(--fa-beat-fade-scale, 1.125)); } } + +@keyframes fa-beat-fade { + 0%, 100% { + opacity: var(--fa-beat-fade-opacity, 0.4); + -webkit-transform: scale(1); + transform: scale(1); } + 50% { + opacity: 1; + -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); + transform: scale(var(--fa-beat-fade-scale, 1.125)); } } + +@-webkit-keyframes fa-flip { + 50% { + -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } } + +@keyframes fa-flip { + 50% { + -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } } + +@-webkit-keyframes fa-shake { + 0% { + -webkit-transform: rotate(-15deg); + transform: rotate(-15deg); } + 4% { + -webkit-transform: rotate(15deg); + transform: rotate(15deg); } + 8%, 24% { + -webkit-transform: rotate(-18deg); + transform: rotate(-18deg); } + 12%, 28% { + -webkit-transform: rotate(18deg); + transform: rotate(18deg); } + 16% { + -webkit-transform: rotate(-22deg); + transform: rotate(-22deg); } + 20% { + -webkit-transform: rotate(22deg); + transform: rotate(22deg); } + 32% { + -webkit-transform: rotate(-12deg); + transform: rotate(-12deg); } + 36% { + -webkit-transform: rotate(12deg); + transform: rotate(12deg); } + 40%, 100% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } } + +@keyframes fa-shake { + 0% { + -webkit-transform: rotate(-15deg); + transform: rotate(-15deg); } + 4% { + -webkit-transform: rotate(15deg); + transform: rotate(15deg); } + 8%, 24% { + -webkit-transform: rotate(-18deg); + transform: rotate(-18deg); } + 12%, 28% { + -webkit-transform: rotate(18deg); + transform: rotate(18deg); } + 16% { + -webkit-transform: rotate(-22deg); + transform: rotate(-22deg); } + 20% { + -webkit-transform: rotate(22deg); + transform: rotate(22deg); } + 32% { + -webkit-transform: rotate(-12deg); + transform: rotate(-12deg); } + 36% { + -webkit-transform: rotate(12deg); + transform: rotate(12deg); } + 40%, 100% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } } + +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); } } + +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); } } + +.fa-rotate-90 { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); } + +.fa-rotate-180 { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); } + +.fa-rotate-270 { + -webkit-transform: rotate(270deg); + transform: rotate(270deg); } + +.fa-flip-horizontal { + -webkit-transform: scale(-1, 1); + transform: scale(-1, 1); } + +.fa-flip-vertical { + -webkit-transform: scale(1, -1); + transform: scale(1, -1); } + +.fa-flip-both, +.fa-flip-horizontal.fa-flip-vertical { + -webkit-transform: scale(-1, -1); + transform: scale(-1, -1); } + +.fa-rotate-by { + -webkit-transform: rotate(var(--fa-rotate-angle, none)); + transform: rotate(var(--fa-rotate-angle, none)); } + +.fa-stack { + display: inline-block; + height: 2em; + line-height: 2em; + position: relative; + vertical-align: middle; + width: 2.5em; } + +.fa-stack-1x, +.fa-stack-2x { + left: 0; + position: absolute; + text-align: center; + width: 100%; + z-index: var(--fa-stack-z-index, auto); } + +.fa-stack-1x { + line-height: inherit; } + +.fa-stack-2x { + font-size: 2em; } + +.fa-inverse { + color: var(--fa-inverse, #fff); } + +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen +readers do not read off random characters that represent icons */ + +.fa-0::before { + content: "\30"; } + +.fa-1::before { + content: "\31"; } + +.fa-2::before { + content: "\32"; } + +.fa-3::before { + content: "\33"; } + +.fa-4::before { + content: "\34"; } + +.fa-5::before { + content: "\35"; } + +.fa-6::before { + content: "\36"; } + +.fa-7::before { + content: "\37"; } + +.fa-8::before { + content: "\38"; } + +.fa-9::before { + content: "\39"; } + +.fa-fill-drip::before { + content: "\f576"; } + +.fa-arrows-to-circle::before { + content: "\e4bd"; } + +.fa-circle-chevron-right::before { + content: "\f138"; } + +.fa-chevron-circle-right::before { + content: "\f138"; } + +.fa-at::before { + content: "\40"; } + +.fa-trash-can::before { + content: "\f2ed"; } + +.fa-trash-alt::before { + content: "\f2ed"; } + +.fa-text-height::before { + content: "\f034"; } + +.fa-user-xmark::before { + content: "\f235"; } + +.fa-user-times::before { + content: "\f235"; } + +.fa-stethoscope::before { + content: "\f0f1"; } + +.fa-message::before { + content: "\f27a"; } + +.fa-comment-alt::before { + content: "\f27a"; } + +.fa-info::before { + content: "\f129"; } + +.fa-down-left-and-up-right-to-center::before { + content: "\f422"; } + +.fa-compress-alt::before { + content: "\f422"; } + +.fa-explosion::before { + content: "\e4e9"; } + +.fa-file-lines::before { + content: "\f15c"; } + +.fa-file-alt::before { + content: "\f15c"; } + +.fa-file-text::before { + content: "\f15c"; } + +.fa-wave-square::before { + content: "\f83e"; } + +.fa-ring::before { + content: "\f70b"; } + +.fa-building-un::before { + content: "\e4d9"; } + +.fa-dice-three::before { + content: "\f527"; } + +.fa-calendar-days::before { + content: "\f073"; } + +.fa-calendar-alt::before { + content: "\f073"; } + +.fa-anchor-circle-check::before { + content: "\e4aa"; } + +.fa-building-circle-arrow-right::before { + content: "\e4d1"; } + +.fa-volleyball::before { + content: "\f45f"; } + +.fa-volleyball-ball::before { + content: "\f45f"; } + +.fa-arrows-up-to-line::before { + content: "\e4c2"; } + +.fa-sort-down::before { + content: "\f0dd"; } + +.fa-sort-desc::before { + content: "\f0dd"; } + +.fa-circle-minus::before { + content: "\f056"; } + +.fa-minus-circle::before { + content: "\f056"; } + +.fa-door-open::before { + content: "\f52b"; } + +.fa-right-from-bracket::before { + content: "\f2f5"; } + +.fa-sign-out-alt::before { + content: "\f2f5"; } + +.fa-atom::before { + content: "\f5d2"; } + +.fa-soap::before { + content: "\e06e"; } + +.fa-icons::before { + content: "\f86d"; } + +.fa-heart-music-camera-bolt::before { + content: "\f86d"; } + +.fa-microphone-lines-slash::before { + content: "\f539"; } + +.fa-microphone-alt-slash::before { + content: "\f539"; } + +.fa-bridge-circle-check::before { + content: "\e4c9"; } + +.fa-pump-medical::before { + content: "\e06a"; } + +.fa-fingerprint::before { + content: "\f577"; } + +.fa-hand-point-right::before { + content: "\f0a4"; } + +.fa-magnifying-glass-location::before { + content: "\f689"; } + +.fa-search-location::before { + content: "\f689"; } + +.fa-forward-step::before { + content: "\f051"; } + +.fa-step-forward::before { + content: "\f051"; } + +.fa-face-smile-beam::before { + content: "\f5b8"; } + +.fa-smile-beam::before { + content: "\f5b8"; } + +.fa-flag-checkered::before { + content: "\f11e"; } + +.fa-football::before { + content: "\f44e"; } + +.fa-football-ball::before { + content: "\f44e"; } + +.fa-school-circle-exclamation::before { + content: "\e56c"; } + +.fa-crop::before { + content: "\f125"; } + +.fa-angles-down::before { + content: "\f103"; } + +.fa-angle-double-down::before { + content: "\f103"; } + +.fa-users-rectangle::before { + content: "\e594"; } + +.fa-people-roof::before { + content: "\e537"; } + +.fa-people-line::before { + content: "\e534"; } + +.fa-beer-mug-empty::before { + content: "\f0fc"; } + +.fa-beer::before { + content: "\f0fc"; } + +.fa-diagram-predecessor::before { + content: "\e477"; } + +.fa-arrow-up-long::before { + content: "\f176"; } + +.fa-long-arrow-up::before { + content: "\f176"; } + +.fa-fire-flame-simple::before { + content: "\f46a"; } + +.fa-burn::before { + content: "\f46a"; } + +.fa-person::before { + content: "\f183"; } + +.fa-male::before { + content: "\f183"; } + +.fa-laptop::before { + content: "\f109"; } + +.fa-file-csv::before { + content: "\f6dd"; } + +.fa-menorah::before { + content: "\f676"; } + +.fa-truck-plane::before { + content: "\e58f"; } + +.fa-record-vinyl::before { + content: "\f8d9"; } + +.fa-face-grin-stars::before { + content: "\f587"; } + +.fa-grin-stars::before { + content: "\f587"; } + +.fa-bong::before { + content: "\f55c"; } + +.fa-spaghetti-monster-flying::before { + content: "\f67b"; } + +.fa-pastafarianism::before { + content: "\f67b"; } + +.fa-arrow-down-up-across-line::before { + content: "\e4af"; } + +.fa-spoon::before { + content: "\f2e5"; } + +.fa-utensil-spoon::before { + content: "\f2e5"; } + +.fa-jar-wheat::before { + content: "\e517"; } + +.fa-envelopes-bulk::before { + content: "\f674"; } + +.fa-mail-bulk::before { + content: "\f674"; } + +.fa-file-circle-exclamation::before { + content: "\e4eb"; } + +.fa-circle-h::before { + content: "\f47e"; } + +.fa-hospital-symbol::before { + content: "\f47e"; } + +.fa-pager::before { + content: "\f815"; } + +.fa-address-book::before { + content: "\f2b9"; } + +.fa-contact-book::before { + content: "\f2b9"; } + +.fa-strikethrough::before { + content: "\f0cc"; } + +.fa-k::before { + content: "\4b"; } + +.fa-landmark-flag::before { + content: "\e51c"; } + +.fa-pencil::before { + content: "\f303"; } + +.fa-pencil-alt::before { + content: "\f303"; } + +.fa-backward::before { + content: "\f04a"; } + +.fa-caret-right::before { + content: "\f0da"; } + +.fa-comments::before { + content: "\f086"; } + +.fa-paste::before { + content: "\f0ea"; } + +.fa-file-clipboard::before { + content: "\f0ea"; } + +.fa-code-pull-request::before { + content: "\e13c"; } + +.fa-clipboard-list::before { + content: "\f46d"; } + +.fa-truck-ramp-box::before { + content: "\f4de"; } + +.fa-truck-loading::before { + content: "\f4de"; } + +.fa-user-check::before { + content: "\f4fc"; } + +.fa-vial-virus::before { + content: "\e597"; } + +.fa-sheet-plastic::before { + content: "\e571"; } + +.fa-blog::before { + content: "\f781"; } + +.fa-user-ninja::before { + content: "\f504"; } + +.fa-person-arrow-up-from-line::before { + content: "\e539"; } + +.fa-scroll-torah::before { + content: "\f6a0"; } + +.fa-torah::before { + content: "\f6a0"; } + +.fa-broom-ball::before { + content: "\f458"; } + +.fa-quidditch::before { + content: "\f458"; } + +.fa-quidditch-broom-ball::before { + content: "\f458"; } + +.fa-toggle-off::before { + content: "\f204"; } + +.fa-box-archive::before { + content: "\f187"; } + +.fa-archive::before { + content: "\f187"; } + +.fa-person-drowning::before { + content: "\e545"; } + +.fa-arrow-down-9-1::before { + content: "\f886"; } + +.fa-sort-numeric-desc::before { + content: "\f886"; } + +.fa-sort-numeric-down-alt::before { + content: "\f886"; } + +.fa-face-grin-tongue-squint::before { + content: "\f58a"; } + +.fa-grin-tongue-squint::before { + content: "\f58a"; } + +.fa-spray-can::before { + content: "\f5bd"; } + +.fa-truck-monster::before { + content: "\f63b"; } + +.fa-w::before { + content: "\57"; } + +.fa-earth-africa::before { + content: "\f57c"; } + +.fa-globe-africa::before { + content: "\f57c"; } + +.fa-rainbow::before { + content: "\f75b"; } + +.fa-circle-notch::before { + content: "\f1ce"; } + +.fa-tablet-screen-button::before { + content: "\f3fa"; } + +.fa-tablet-alt::before { + content: "\f3fa"; } + +.fa-paw::before { + content: "\f1b0"; } + +.fa-cloud::before { + content: "\f0c2"; } + +.fa-trowel-bricks::before { + content: "\e58a"; } + +.fa-face-flushed::before { + content: "\f579"; } + +.fa-flushed::before { + content: "\f579"; } + +.fa-hospital-user::before { + content: "\f80d"; } + +.fa-tent-arrow-left-right::before { + content: "\e57f"; } + +.fa-gavel::before { + content: "\f0e3"; } + +.fa-legal::before { + content: "\f0e3"; } + +.fa-binoculars::before { + content: "\f1e5"; } + +.fa-microphone-slash::before { + content: "\f131"; } + +.fa-box-tissue::before { + content: "\e05b"; } + +.fa-motorcycle::before { + content: "\f21c"; } + +.fa-bell-concierge::before { + content: "\f562"; } + +.fa-concierge-bell::before { + content: "\f562"; } + +.fa-pen-ruler::before { + content: "\f5ae"; } + +.fa-pencil-ruler::before { + content: "\f5ae"; } + +.fa-people-arrows::before { + content: "\e068"; } + +.fa-people-arrows-left-right::before { + content: "\e068"; } + +.fa-mars-and-venus-burst::before { + content: "\e523"; } + +.fa-square-caret-right::before { + content: "\f152"; } + +.fa-caret-square-right::before { + content: "\f152"; } + +.fa-scissors::before { + content: "\f0c4"; } + +.fa-cut::before { + content: "\f0c4"; } + +.fa-sun-plant-wilt::before { + content: "\e57a"; } + +.fa-toilets-portable::before { + content: "\e584"; } + +.fa-hockey-puck::before { + content: "\f453"; } + +.fa-table::before { + content: "\f0ce"; } + +.fa-magnifying-glass-arrow-right::before { + content: "\e521"; } + +.fa-tachograph-digital::before { + content: "\f566"; } + +.fa-digital-tachograph::before { + content: "\f566"; } + +.fa-users-slash::before { + content: "\e073"; } + +.fa-clover::before { + content: "\e139"; } + +.fa-reply::before { + content: "\f3e5"; } + +.fa-mail-reply::before { + content: "\f3e5"; } + +.fa-star-and-crescent::before { + content: "\f699"; } + +.fa-house-fire::before { + content: "\e50c"; } + +.fa-square-minus::before { + content: "\f146"; } + +.fa-minus-square::before { + content: "\f146"; } + +.fa-helicopter::before { + content: "\f533"; } + +.fa-compass::before { + content: "\f14e"; } + +.fa-square-caret-down::before { + content: "\f150"; } + +.fa-caret-square-down::before { + content: "\f150"; } + +.fa-file-circle-question::before { + content: "\e4ef"; } + +.fa-laptop-code::before { + content: "\f5fc"; } + +.fa-swatchbook::before { + content: "\f5c3"; } + +.fa-prescription-bottle::before { + content: "\f485"; } + +.fa-bars::before { + content: "\f0c9"; } + +.fa-navicon::before { + content: "\f0c9"; } + +.fa-people-group::before { + content: "\e533"; } + +.fa-hourglass-end::before { + content: "\f253"; } + +.fa-hourglass-3::before { + content: "\f253"; } + +.fa-heart-crack::before { + content: "\f7a9"; } + +.fa-heart-broken::before { + content: "\f7a9"; } + +.fa-square-up-right::before { + content: "\f360"; } + +.fa-external-link-square-alt::before { + content: "\f360"; } + +.fa-face-kiss-beam::before { + content: "\f597"; } + +.fa-kiss-beam::before { + content: "\f597"; } + +.fa-film::before { + content: "\f008"; } + +.fa-ruler-horizontal::before { + content: "\f547"; } + +.fa-people-robbery::before { + content: "\e536"; } + +.fa-lightbulb::before { + content: "\f0eb"; } + +.fa-caret-left::before { + content: "\f0d9"; } + +.fa-circle-exclamation::before { + content: "\f06a"; } + +.fa-exclamation-circle::before { + content: "\f06a"; } + +.fa-school-circle-xmark::before { + content: "\e56d"; } + +.fa-arrow-right-from-bracket::before { + content: "\f08b"; } + +.fa-sign-out::before { + content: "\f08b"; } + +.fa-circle-chevron-down::before { + content: "\f13a"; } + +.fa-chevron-circle-down::before { + content: "\f13a"; } + +.fa-unlock-keyhole::before { + content: "\f13e"; } + +.fa-unlock-alt::before { + content: "\f13e"; } + +.fa-cloud-showers-heavy::before { + content: "\f740"; } + +.fa-headphones-simple::before { + content: "\f58f"; } + +.fa-headphones-alt::before { + content: "\f58f"; } + +.fa-sitemap::before { + content: "\f0e8"; } + +.fa-circle-dollar-to-slot::before { + content: "\f4b9"; } + +.fa-donate::before { + content: "\f4b9"; } + +.fa-memory::before { + content: "\f538"; } + +.fa-road-spikes::before { + content: "\e568"; } + +.fa-fire-burner::before { + content: "\e4f1"; } + +.fa-flag::before { + content: "\f024"; } + +.fa-hanukiah::before { + content: "\f6e6"; } + +.fa-feather::before { + content: "\f52d"; } + +.fa-volume-low::before { + content: "\f027"; } + +.fa-volume-down::before { + content: "\f027"; } + +.fa-comment-slash::before { + content: "\f4b3"; } + +.fa-cloud-sun-rain::before { + content: "\f743"; } + +.fa-compress::before { + content: "\f066"; } + +.fa-wheat-awn::before { + content: "\e2cd"; } + +.fa-wheat-alt::before { + content: "\e2cd"; } + +.fa-ankh::before { + content: "\f644"; } + +.fa-hands-holding-child::before { + content: "\e4fa"; } + +.fa-asterisk::before { + content: "\2a"; } + +.fa-square-check::before { + content: "\f14a"; } + +.fa-check-square::before { + content: "\f14a"; } + +.fa-peseta-sign::before { + content: "\e221"; } + +.fa-heading::before { + content: "\f1dc"; } + +.fa-header::before { + content: "\f1dc"; } + +.fa-ghost::before { + content: "\f6e2"; } + +.fa-list::before { + content: "\f03a"; } + +.fa-list-squares::before { + content: "\f03a"; } + +.fa-square-phone-flip::before { + content: "\f87b"; } + +.fa-phone-square-alt::before { + content: "\f87b"; } + +.fa-cart-plus::before { + content: "\f217"; } + +.fa-gamepad::before { + content: "\f11b"; } + +.fa-circle-dot::before { + content: "\f192"; } + +.fa-dot-circle::before { + content: "\f192"; } + +.fa-face-dizzy::before { + content: "\f567"; } + +.fa-dizzy::before { + content: "\f567"; } + +.fa-egg::before { + content: "\f7fb"; } + +.fa-house-medical-circle-xmark::before { + content: "\e513"; } + +.fa-campground::before { + content: "\f6bb"; } + +.fa-folder-plus::before { + content: "\f65e"; } + +.fa-futbol::before { + content: "\f1e3"; } + +.fa-futbol-ball::before { + content: "\f1e3"; } + +.fa-soccer-ball::before { + content: "\f1e3"; } + +.fa-paintbrush::before { + content: "\f1fc"; } + +.fa-paint-brush::before { + content: "\f1fc"; } + +.fa-lock::before { + content: "\f023"; } + +.fa-gas-pump::before { + content: "\f52f"; } + +.fa-hot-tub-person::before { + content: "\f593"; } + +.fa-hot-tub::before { + content: "\f593"; } + +.fa-map-location::before { + content: "\f59f"; } + +.fa-map-marked::before { + content: "\f59f"; } + +.fa-house-flood-water::before { + content: "\e50e"; } + +.fa-tree::before { + content: "\f1bb"; } + +.fa-bridge-lock::before { + content: "\e4cc"; } + +.fa-sack-dollar::before { + content: "\f81d"; } + +.fa-pen-to-square::before { + content: "\f044"; } + +.fa-edit::before { + content: "\f044"; } + +.fa-car-side::before { + content: "\f5e4"; } + +.fa-share-nodes::before { + content: "\f1e0"; } + +.fa-share-alt::before { + content: "\f1e0"; } + +.fa-heart-circle-minus::before { + content: "\e4ff"; } + +.fa-hourglass-half::before { + content: "\f252"; } + +.fa-hourglass-2::before { + content: "\f252"; } + +.fa-microscope::before { + content: "\f610"; } + +.fa-sink::before { + content: "\e06d"; } + +.fa-bag-shopping::before { + content: "\f290"; } + +.fa-shopping-bag::before { + content: "\f290"; } + +.fa-arrow-down-z-a::before { + content: "\f881"; } + +.fa-sort-alpha-desc::before { + content: "\f881"; } + +.fa-sort-alpha-down-alt::before { + content: "\f881"; } + +.fa-mitten::before { + content: "\f7b5"; } + +.fa-person-rays::before { + content: "\e54d"; } + +.fa-users::before { + content: "\f0c0"; } + +.fa-eye-slash::before { + content: "\f070"; } + +.fa-flask-vial::before { + content: "\e4f3"; } + +.fa-hand::before { + content: "\f256"; } + +.fa-hand-paper::before { + content: "\f256"; } + +.fa-om::before { + content: "\f679"; } + +.fa-worm::before { + content: "\e599"; } + +.fa-house-circle-xmark::before { + content: "\e50b"; } + +.fa-plug::before { + content: "\f1e6"; } + +.fa-chevron-up::before { + content: "\f077"; } + +.fa-hand-spock::before { + content: "\f259"; } + +.fa-stopwatch::before { + content: "\f2f2"; } + +.fa-face-kiss::before { + content: "\f596"; } + +.fa-kiss::before { + content: "\f596"; } + +.fa-bridge-circle-xmark::before { + content: "\e4cb"; } + +.fa-face-grin-tongue::before { + content: "\f589"; } + +.fa-grin-tongue::before { + content: "\f589"; } + +.fa-chess-bishop::before { + content: "\f43a"; } + +.fa-face-grin-wink::before { + content: "\f58c"; } + +.fa-grin-wink::before { + content: "\f58c"; } + +.fa-ear-deaf::before { + content: "\f2a4"; } + +.fa-deaf::before { + content: "\f2a4"; } + +.fa-deafness::before { + content: "\f2a4"; } + +.fa-hard-of-hearing::before { + content: "\f2a4"; } + +.fa-road-circle-check::before { + content: "\e564"; } + +.fa-dice-five::before { + content: "\f523"; } + +.fa-square-rss::before { + content: "\f143"; } + +.fa-rss-square::before { + content: "\f143"; } + +.fa-land-mine-on::before { + content: "\e51b"; } + +.fa-i-cursor::before { + content: "\f246"; } + +.fa-stamp::before { + content: "\f5bf"; } + +.fa-stairs::before { + content: "\e289"; } + +.fa-i::before { + content: "\49"; } + +.fa-hryvnia-sign::before { + content: "\f6f2"; } + +.fa-hryvnia::before { + content: "\f6f2"; } + +.fa-pills::before { + content: "\f484"; } + +.fa-face-grin-wide::before { + content: "\f581"; } + +.fa-grin-alt::before { + content: "\f581"; } + +.fa-tooth::before { + content: "\f5c9"; } + +.fa-v::before { + content: "\56"; } + +.fa-bangladeshi-taka-sign::before { + content: "\e2e6"; } + +.fa-bicycle::before { + content: "\f206"; } + +.fa-staff-snake::before { + content: "\e579"; } + +.fa-rod-asclepius::before { + content: "\e579"; } + +.fa-rod-snake::before { + content: "\e579"; } + +.fa-staff-aesculapius::before { + content: "\e579"; } + +.fa-head-side-cough-slash::before { + content: "\e062"; } + +.fa-truck-medical::before { + content: "\f0f9"; } + +.fa-ambulance::before { + content: "\f0f9"; } + +.fa-wheat-awn-circle-exclamation::before { + content: "\e598"; } + +.fa-snowman::before { + content: "\f7d0"; } + +.fa-mortar-pestle::before { + content: "\f5a7"; } + +.fa-road-barrier::before { + content: "\e562"; } + +.fa-school::before { + content: "\f549"; } + +.fa-igloo::before { + content: "\f7ae"; } + +.fa-joint::before { + content: "\f595"; } + +.fa-angle-right::before { + content: "\f105"; } + +.fa-horse::before { + content: "\f6f0"; } + +.fa-q::before { + content: "\51"; } + +.fa-g::before { + content: "\47"; } + +.fa-notes-medical::before { + content: "\f481"; } + +.fa-temperature-half::before { + content: "\f2c9"; } + +.fa-temperature-2::before { + content: "\f2c9"; } + +.fa-thermometer-2::before { + content: "\f2c9"; } + +.fa-thermometer-half::before { + content: "\f2c9"; } + +.fa-dong-sign::before { + content: "\e169"; } + +.fa-capsules::before { + content: "\f46b"; } + +.fa-poo-storm::before { + content: "\f75a"; } + +.fa-poo-bolt::before { + content: "\f75a"; } + +.fa-face-frown-open::before { + content: "\f57a"; } + +.fa-frown-open::before { + content: "\f57a"; } + +.fa-hand-point-up::before { + content: "\f0a6"; } + +.fa-money-bill::before { + content: "\f0d6"; } + +.fa-bookmark::before { + content: "\f02e"; } + +.fa-align-justify::before { + content: "\f039"; } + +.fa-umbrella-beach::before { + content: "\f5ca"; } + +.fa-helmet-un::before { + content: "\e503"; } + +.fa-bullseye::before { + content: "\f140"; } + +.fa-bacon::before { + content: "\f7e5"; } + +.fa-hand-point-down::before { + content: "\f0a7"; } + +.fa-arrow-up-from-bracket::before { + content: "\e09a"; } + +.fa-folder::before { + content: "\f07b"; } + +.fa-folder-blank::before { + content: "\f07b"; } + +.fa-file-waveform::before { + content: "\f478"; } + +.fa-file-medical-alt::before { + content: "\f478"; } + +.fa-radiation::before { + content: "\f7b9"; } + +.fa-chart-simple::before { + content: "\e473"; } + +.fa-mars-stroke::before { + content: "\f229"; } + +.fa-vial::before { + content: "\f492"; } + +.fa-gauge::before { + content: "\f624"; } + +.fa-dashboard::before { + content: "\f624"; } + +.fa-gauge-med::before { + content: "\f624"; } + +.fa-tachometer-alt-average::before { + content: "\f624"; } + +.fa-wand-magic-sparkles::before { + content: "\e2ca"; } + +.fa-magic-wand-sparkles::before { + content: "\e2ca"; } + +.fa-e::before { + content: "\45"; } + +.fa-pen-clip::before { + content: "\f305"; } + +.fa-pen-alt::before { + content: "\f305"; } + +.fa-bridge-circle-exclamation::before { + content: "\e4ca"; } + +.fa-user::before { + content: "\f007"; } + +.fa-school-circle-check::before { + content: "\e56b"; } + +.fa-dumpster::before { + content: "\f793"; } + +.fa-van-shuttle::before { + content: "\f5b6"; } + +.fa-shuttle-van::before { + content: "\f5b6"; } + +.fa-building-user::before { + content: "\e4da"; } + +.fa-square-caret-left::before { + content: "\f191"; } + +.fa-caret-square-left::before { + content: "\f191"; } + +.fa-highlighter::before { + content: "\f591"; } + +.fa-key::before { + content: "\f084"; } + +.fa-bullhorn::before { + content: "\f0a1"; } + +.fa-globe::before { + content: "\f0ac"; } + +.fa-synagogue::before { + content: "\f69b"; } + +.fa-person-half-dress::before { + content: "\e548"; } + +.fa-road-bridge::before { + content: "\e563"; } + +.fa-location-arrow::before { + content: "\f124"; } + +.fa-c::before { + content: "\43"; } + +.fa-tablet-button::before { + content: "\f10a"; } + +.fa-building-lock::before { + content: "\e4d6"; } + +.fa-pizza-slice::before { + content: "\f818"; } + +.fa-money-bill-wave::before { + content: "\f53a"; } + +.fa-chart-area::before { + content: "\f1fe"; } + +.fa-area-chart::before { + content: "\f1fe"; } + +.fa-house-flag::before { + content: "\e50d"; } + +.fa-person-circle-minus::before { + content: "\e540"; } + +.fa-ban::before { + content: "\f05e"; } + +.fa-cancel::before { + content: "\f05e"; } + +.fa-camera-rotate::before { + content: "\e0d8"; } + +.fa-spray-can-sparkles::before { + content: "\f5d0"; } + +.fa-air-freshener::before { + content: "\f5d0"; } + +.fa-star::before { + content: "\f005"; } + +.fa-repeat::before { + content: "\f363"; } + +.fa-cross::before { + content: "\f654"; } + +.fa-box::before { + content: "\f466"; } + +.fa-venus-mars::before { + content: "\f228"; } + +.fa-arrow-pointer::before { + content: "\f245"; } + +.fa-mouse-pointer::before { + content: "\f245"; } + +.fa-maximize::before { + content: "\f31e"; } + +.fa-expand-arrows-alt::before { + content: "\f31e"; } + +.fa-charging-station::before { + content: "\f5e7"; } + +.fa-shapes::before { + content: "\f61f"; } + +.fa-triangle-circle-square::before { + content: "\f61f"; } + +.fa-shuffle::before { + content: "\f074"; } + +.fa-random::before { + content: "\f074"; } + +.fa-person-running::before { + content: "\f70c"; } + +.fa-running::before { + content: "\f70c"; } + +.fa-mobile-retro::before { + content: "\e527"; } + +.fa-grip-lines-vertical::before { + content: "\f7a5"; } + +.fa-spider::before { + content: "\f717"; } + +.fa-hands-bound::before { + content: "\e4f9"; } + +.fa-file-invoice-dollar::before { + content: "\f571"; } + +.fa-plane-circle-exclamation::before { + content: "\e556"; } + +.fa-x-ray::before { + content: "\f497"; } + +.fa-spell-check::before { + content: "\f891"; } + +.fa-slash::before { + content: "\f715"; } + +.fa-computer-mouse::before { + content: "\f8cc"; } + +.fa-mouse::before { + content: "\f8cc"; } + +.fa-arrow-right-to-bracket::before { + content: "\f090"; } + +.fa-sign-in::before { + content: "\f090"; } + +.fa-shop-slash::before { + content: "\e070"; } + +.fa-store-alt-slash::before { + content: "\e070"; } + +.fa-server::before { + content: "\f233"; } + +.fa-virus-covid-slash::before { + content: "\e4a9"; } + +.fa-shop-lock::before { + content: "\e4a5"; } + +.fa-hourglass-start::before { + content: "\f251"; } + +.fa-hourglass-1::before { + content: "\f251"; } + +.fa-blender-phone::before { + content: "\f6b6"; } + +.fa-building-wheat::before { + content: "\e4db"; } + +.fa-person-breastfeeding::before { + content: "\e53a"; } + +.fa-right-to-bracket::before { + content: "\f2f6"; } + +.fa-sign-in-alt::before { + content: "\f2f6"; } + +.fa-venus::before { + content: "\f221"; } + +.fa-passport::before { + content: "\f5ab"; } + +.fa-heart-pulse::before { + content: "\f21e"; } + +.fa-heartbeat::before { + content: "\f21e"; } + +.fa-people-carry-box::before { + content: "\f4ce"; } + +.fa-people-carry::before { + content: "\f4ce"; } + +.fa-temperature-high::before { + content: "\f769"; } + +.fa-microchip::before { + content: "\f2db"; } + +.fa-crown::before { + content: "\f521"; } + +.fa-weight-hanging::before { + content: "\f5cd"; } + +.fa-xmarks-lines::before { + content: "\e59a"; } + +.fa-file-prescription::before { + content: "\f572"; } + +.fa-weight-scale::before { + content: "\f496"; } + +.fa-weight::before { + content: "\f496"; } + +.fa-user-group::before { + content: "\f500"; } + +.fa-user-friends::before { + content: "\f500"; } + +.fa-arrow-up-a-z::before { + content: "\f15e"; } + +.fa-sort-alpha-up::before { + content: "\f15e"; } + +.fa-chess-knight::before { + content: "\f441"; } + +.fa-face-laugh-squint::before { + content: "\f59b"; } + +.fa-laugh-squint::before { + content: "\f59b"; } + +.fa-wheelchair::before { + content: "\f193"; } + +.fa-circle-arrow-up::before { + content: "\f0aa"; } + +.fa-arrow-circle-up::before { + content: "\f0aa"; } + +.fa-toggle-on::before { + content: "\f205"; } + +.fa-person-walking::before { + content: "\f554"; } + +.fa-walking::before { + content: "\f554"; } + +.fa-l::before { + content: "\4c"; } + +.fa-fire::before { + content: "\f06d"; } + +.fa-bed-pulse::before { + content: "\f487"; } + +.fa-procedures::before { + content: "\f487"; } + +.fa-shuttle-space::before { + content: "\f197"; } + +.fa-space-shuttle::before { + content: "\f197"; } + +.fa-face-laugh::before { + content: "\f599"; } + +.fa-laugh::before { + content: "\f599"; } + +.fa-folder-open::before { + content: "\f07c"; } + +.fa-heart-circle-plus::before { + content: "\e500"; } + +.fa-code-fork::before { + content: "\e13b"; } + +.fa-city::before { + content: "\f64f"; } + +.fa-microphone-lines::before { + content: "\f3c9"; } + +.fa-microphone-alt::before { + content: "\f3c9"; } + +.fa-pepper-hot::before { + content: "\f816"; } + +.fa-unlock::before { + content: "\f09c"; } + +.fa-colon-sign::before { + content: "\e140"; } + +.fa-headset::before { + content: "\f590"; } + +.fa-store-slash::before { + content: "\e071"; } + +.fa-road-circle-xmark::before { + content: "\e566"; } + +.fa-user-minus::before { + content: "\f503"; } + +.fa-mars-stroke-up::before { + content: "\f22a"; } + +.fa-mars-stroke-v::before { + content: "\f22a"; } + +.fa-champagne-glasses::before { + content: "\f79f"; } + +.fa-glass-cheers::before { + content: "\f79f"; } + +.fa-clipboard::before { + content: "\f328"; } + +.fa-house-circle-exclamation::before { + content: "\e50a"; } + +.fa-file-arrow-up::before { + content: "\f574"; } + +.fa-file-upload::before { + content: "\f574"; } + +.fa-wifi::before { + content: "\f1eb"; } + +.fa-wifi-3::before { + content: "\f1eb"; } + +.fa-wifi-strong::before { + content: "\f1eb"; } + +.fa-bath::before { + content: "\f2cd"; } + +.fa-bathtub::before { + content: "\f2cd"; } + +.fa-underline::before { + content: "\f0cd"; } + +.fa-user-pen::before { + content: "\f4ff"; } + +.fa-user-edit::before { + content: "\f4ff"; } + +.fa-signature::before { + content: "\f5b7"; } + +.fa-stroopwafel::before { + content: "\f551"; } + +.fa-bold::before { + content: "\f032"; } + +.fa-anchor-lock::before { + content: "\e4ad"; } + +.fa-building-ngo::before { + content: "\e4d7"; } + +.fa-manat-sign::before { + content: "\e1d5"; } + +.fa-not-equal::before { + content: "\f53e"; } + +.fa-border-top-left::before { + content: "\f853"; } + +.fa-border-style::before { + content: "\f853"; } + +.fa-map-location-dot::before { + content: "\f5a0"; } + +.fa-map-marked-alt::before { + content: "\f5a0"; } + +.fa-jedi::before { + content: "\f669"; } + +.fa-square-poll-vertical::before { + content: "\f681"; } + +.fa-poll::before { + content: "\f681"; } + +.fa-mug-hot::before { + content: "\f7b6"; } + +.fa-car-battery::before { + content: "\f5df"; } + +.fa-battery-car::before { + content: "\f5df"; } + +.fa-gift::before { + content: "\f06b"; } + +.fa-dice-two::before { + content: "\f528"; } + +.fa-chess-queen::before { + content: "\f445"; } + +.fa-glasses::before { + content: "\f530"; } + +.fa-chess-board::before { + content: "\f43c"; } + +.fa-building-circle-check::before { + content: "\e4d2"; } + +.fa-person-chalkboard::before { + content: "\e53d"; } + +.fa-mars-stroke-right::before { + content: "\f22b"; } + +.fa-mars-stroke-h::before { + content: "\f22b"; } + +.fa-hand-back-fist::before { + content: "\f255"; } + +.fa-hand-rock::before { + content: "\f255"; } + +.fa-square-caret-up::before { + content: "\f151"; } + +.fa-caret-square-up::before { + content: "\f151"; } + +.fa-cloud-showers-water::before { + content: "\e4e4"; } + +.fa-chart-bar::before { + content: "\f080"; } + +.fa-bar-chart::before { + content: "\f080"; } + +.fa-hands-bubbles::before { + content: "\e05e"; } + +.fa-hands-wash::before { + content: "\e05e"; } + +.fa-less-than-equal::before { + content: "\f537"; } + +.fa-train::before { + content: "\f238"; } + +.fa-eye-low-vision::before { + content: "\f2a8"; } + +.fa-low-vision::before { + content: "\f2a8"; } + +.fa-crow::before { + content: "\f520"; } + +.fa-sailboat::before { + content: "\e445"; } + +.fa-window-restore::before { + content: "\f2d2"; } + +.fa-square-plus::before { + content: "\f0fe"; } + +.fa-plus-square::before { + content: "\f0fe"; } + +.fa-torii-gate::before { + content: "\f6a1"; } + +.fa-frog::before { + content: "\f52e"; } + +.fa-bucket::before { + content: "\e4cf"; } + +.fa-image::before { + content: "\f03e"; } + +.fa-microphone::before { + content: "\f130"; } + +.fa-cow::before { + content: "\f6c8"; } + +.fa-caret-up::before { + content: "\f0d8"; } + +.fa-screwdriver::before { + content: "\f54a"; } + +.fa-folder-closed::before { + content: "\e185"; } + +.fa-house-tsunami::before { + content: "\e515"; } + +.fa-square-nfi::before { + content: "\e576"; } + +.fa-arrow-up-from-ground-water::before { + content: "\e4b5"; } + +.fa-martini-glass::before { + content: "\f57b"; } + +.fa-glass-martini-alt::before { + content: "\f57b"; } + +.fa-rotate-left::before { + content: "\f2ea"; } + +.fa-rotate-back::before { + content: "\f2ea"; } + +.fa-rotate-backward::before { + content: "\f2ea"; } + +.fa-undo-alt::before { + content: "\f2ea"; } + +.fa-table-columns::before { + content: "\f0db"; } + +.fa-columns::before { + content: "\f0db"; } + +.fa-lemon::before { + content: "\f094"; } + +.fa-head-side-mask::before { + content: "\e063"; } + +.fa-handshake::before { + content: "\f2b5"; } + +.fa-gem::before { + content: "\f3a5"; } + +.fa-dolly::before { + content: "\f472"; } + +.fa-dolly-box::before { + content: "\f472"; } + +.fa-smoking::before { + content: "\f48d"; } + +.fa-minimize::before { + content: "\f78c"; } + +.fa-compress-arrows-alt::before { + content: "\f78c"; } + +.fa-monument::before { + content: "\f5a6"; } + +.fa-snowplow::before { + content: "\f7d2"; } + +.fa-angles-right::before { + content: "\f101"; } + +.fa-angle-double-right::before { + content: "\f101"; } + +.fa-cannabis::before { + content: "\f55f"; } + +.fa-circle-play::before { + content: "\f144"; } + +.fa-play-circle::before { + content: "\f144"; } + +.fa-tablets::before { + content: "\f490"; } + +.fa-ethernet::before { + content: "\f796"; } + +.fa-euro-sign::before { + content: "\f153"; } + +.fa-eur::before { + content: "\f153"; } + +.fa-euro::before { + content: "\f153"; } + +.fa-chair::before { + content: "\f6c0"; } + +.fa-circle-check::before { + content: "\f058"; } + +.fa-check-circle::before { + content: "\f058"; } + +.fa-circle-stop::before { + content: "\f28d"; } + +.fa-stop-circle::before { + content: "\f28d"; } + +.fa-compass-drafting::before { + content: "\f568"; } + +.fa-drafting-compass::before { + content: "\f568"; } + +.fa-plate-wheat::before { + content: "\e55a"; } + +.fa-icicles::before { + content: "\f7ad"; } + +.fa-person-shelter::before { + content: "\e54f"; } + +.fa-neuter::before { + content: "\f22c"; } + +.fa-id-badge::before { + content: "\f2c1"; } + +.fa-marker::before { + content: "\f5a1"; } + +.fa-face-laugh-beam::before { + content: "\f59a"; } + +.fa-laugh-beam::before { + content: "\f59a"; } + +.fa-helicopter-symbol::before { + content: "\e502"; } + +.fa-universal-access::before { + content: "\f29a"; } + +.fa-circle-chevron-up::before { + content: "\f139"; } + +.fa-chevron-circle-up::before { + content: "\f139"; } + +.fa-lari-sign::before { + content: "\e1c8"; } + +.fa-volcano::before { + content: "\f770"; } + +.fa-person-walking-dashed-line-arrow-right::before { + content: "\e553"; } + +.fa-sterling-sign::before { + content: "\f154"; } + +.fa-gbp::before { + content: "\f154"; } + +.fa-pound-sign::before { + content: "\f154"; } + +.fa-viruses::before { + content: "\e076"; } + +.fa-square-person-confined::before { + content: "\e577"; } + +.fa-user-tie::before { + content: "\f508"; } + +.fa-arrow-down-long::before { + content: "\f175"; } + +.fa-long-arrow-down::before { + content: "\f175"; } + +.fa-tent-arrow-down-to-line::before { + content: "\e57e"; } + +.fa-certificate::before { + content: "\f0a3"; } + +.fa-reply-all::before { + content: "\f122"; } + +.fa-mail-reply-all::before { + content: "\f122"; } + +.fa-suitcase::before { + content: "\f0f2"; } + +.fa-person-skating::before { + content: "\f7c5"; } + +.fa-skating::before { + content: "\f7c5"; } + +.fa-filter-circle-dollar::before { + content: "\f662"; } + +.fa-funnel-dollar::before { + content: "\f662"; } + +.fa-camera-retro::before { + content: "\f083"; } + +.fa-circle-arrow-down::before { + content: "\f0ab"; } + +.fa-arrow-circle-down::before { + content: "\f0ab"; } + +.fa-file-import::before { + content: "\f56f"; } + +.fa-arrow-right-to-file::before { + content: "\f56f"; } + +.fa-square-arrow-up-right::before { + content: "\f14c"; } + +.fa-external-link-square::before { + content: "\f14c"; } + +.fa-box-open::before { + content: "\f49e"; } + +.fa-scroll::before { + content: "\f70e"; } + +.fa-spa::before { + content: "\f5bb"; } + +.fa-location-pin-lock::before { + content: "\e51f"; } + +.fa-pause::before { + content: "\f04c"; } + +.fa-hill-avalanche::before { + content: "\e507"; } + +.fa-temperature-empty::before { + content: "\f2cb"; } + +.fa-temperature-0::before { + content: "\f2cb"; } + +.fa-thermometer-0::before { + content: "\f2cb"; } + +.fa-thermometer-empty::before { + content: "\f2cb"; } + +.fa-bomb::before { + content: "\f1e2"; } + +.fa-registered::before { + content: "\f25d"; } + +.fa-address-card::before { + content: "\f2bb"; } + +.fa-contact-card::before { + content: "\f2bb"; } + +.fa-vcard::before { + content: "\f2bb"; } + +.fa-scale-unbalanced-flip::before { + content: "\f516"; } + +.fa-balance-scale-right::before { + content: "\f516"; } + +.fa-subscript::before { + content: "\f12c"; } + +.fa-diamond-turn-right::before { + content: "\f5eb"; } + +.fa-directions::before { + content: "\f5eb"; } + +.fa-burst::before { + content: "\e4dc"; } + +.fa-house-laptop::before { + content: "\e066"; } + +.fa-laptop-house::before { + content: "\e066"; } + +.fa-face-tired::before { + content: "\f5c8"; } + +.fa-tired::before { + content: "\f5c8"; } + +.fa-money-bills::before { + content: "\e1f3"; } + +.fa-smog::before { + content: "\f75f"; } + +.fa-crutch::before { + content: "\f7f7"; } + +.fa-cloud-arrow-up::before { + content: "\f0ee"; } + +.fa-cloud-upload::before { + content: "\f0ee"; } + +.fa-cloud-upload-alt::before { + content: "\f0ee"; } + +.fa-palette::before { + content: "\f53f"; } + +.fa-arrows-turn-right::before { + content: "\e4c0"; } + +.fa-vest::before { + content: "\e085"; } + +.fa-ferry::before { + content: "\e4ea"; } + +.fa-arrows-down-to-people::before { + content: "\e4b9"; } + +.fa-seedling::before { + content: "\f4d8"; } + +.fa-sprout::before { + content: "\f4d8"; } + +.fa-left-right::before { + content: "\f337"; } + +.fa-arrows-alt-h::before { + content: "\f337"; } + +.fa-boxes-packing::before { + content: "\e4c7"; } + +.fa-circle-arrow-left::before { + content: "\f0a8"; } + +.fa-arrow-circle-left::before { + content: "\f0a8"; } + +.fa-group-arrows-rotate::before { + content: "\e4f6"; } + +.fa-bowl-food::before { + content: "\e4c6"; } + +.fa-candy-cane::before { + content: "\f786"; } + +.fa-arrow-down-wide-short::before { + content: "\f160"; } + +.fa-sort-amount-asc::before { + content: "\f160"; } + +.fa-sort-amount-down::before { + content: "\f160"; } + +.fa-cloud-bolt::before { + content: "\f76c"; } + +.fa-thunderstorm::before { + content: "\f76c"; } + +.fa-text-slash::before { + content: "\f87d"; } + +.fa-remove-format::before { + content: "\f87d"; } + +.fa-face-smile-wink::before { + content: "\f4da"; } + +.fa-smile-wink::before { + content: "\f4da"; } + +.fa-file-word::before { + content: "\f1c2"; } + +.fa-file-powerpoint::before { + content: "\f1c4"; } + +.fa-arrows-left-right::before { + content: "\f07e"; } + +.fa-arrows-h::before { + content: "\f07e"; } + +.fa-house-lock::before { + content: "\e510"; } + +.fa-cloud-arrow-down::before { + content: "\f0ed"; } + +.fa-cloud-download::before { + content: "\f0ed"; } + +.fa-cloud-download-alt::before { + content: "\f0ed"; } + +.fa-children::before { + content: "\e4e1"; } + +.fa-chalkboard::before { + content: "\f51b"; } + +.fa-blackboard::before { + content: "\f51b"; } + +.fa-user-large-slash::before { + content: "\f4fa"; } + +.fa-user-alt-slash::before { + content: "\f4fa"; } + +.fa-envelope-open::before { + content: "\f2b6"; } + +.fa-handshake-simple-slash::before { + content: "\e05f"; } + +.fa-handshake-alt-slash::before { + content: "\e05f"; } + +.fa-mattress-pillow::before { + content: "\e525"; } + +.fa-guarani-sign::before { + content: "\e19a"; } + +.fa-arrows-rotate::before { + content: "\f021"; } + +.fa-refresh::before { + content: "\f021"; } + +.fa-sync::before { + content: "\f021"; } + +.fa-fire-extinguisher::before { + content: "\f134"; } + +.fa-cruzeiro-sign::before { + content: "\e152"; } + +.fa-greater-than-equal::before { + content: "\f532"; } + +.fa-shield-halved::before { + content: "\f3ed"; } + +.fa-shield-alt::before { + content: "\f3ed"; } + +.fa-book-atlas::before { + content: "\f558"; } + +.fa-atlas::before { + content: "\f558"; } + +.fa-virus::before { + content: "\e074"; } + +.fa-envelope-circle-check::before { + content: "\e4e8"; } + +.fa-layer-group::before { + content: "\f5fd"; } + +.fa-arrows-to-dot::before { + content: "\e4be"; } + +.fa-archway::before { + content: "\f557"; } + +.fa-heart-circle-check::before { + content: "\e4fd"; } + +.fa-house-chimney-crack::before { + content: "\f6f1"; } + +.fa-house-damage::before { + content: "\f6f1"; } + +.fa-file-zipper::before { + content: "\f1c6"; } + +.fa-file-archive::before { + content: "\f1c6"; } + +.fa-square::before { + content: "\f0c8"; } + +.fa-martini-glass-empty::before { + content: "\f000"; } + +.fa-glass-martini::before { + content: "\f000"; } + +.fa-couch::before { + content: "\f4b8"; } + +.fa-cedi-sign::before { + content: "\e0df"; } + +.fa-italic::before { + content: "\f033"; } + +.fa-church::before { + content: "\f51d"; } + +.fa-comments-dollar::before { + content: "\f653"; } + +.fa-democrat::before { + content: "\f747"; } + +.fa-z::before { + content: "\5a"; } + +.fa-person-skiing::before { + content: "\f7c9"; } + +.fa-skiing::before { + content: "\f7c9"; } + +.fa-road-lock::before { + content: "\e567"; } + +.fa-a::before { + content: "\41"; } + +.fa-temperature-arrow-down::before { + content: "\e03f"; } + +.fa-temperature-down::before { + content: "\e03f"; } + +.fa-feather-pointed::before { + content: "\f56b"; } + +.fa-feather-alt::before { + content: "\f56b"; } + +.fa-p::before { + content: "\50"; } + +.fa-snowflake::before { + content: "\f2dc"; } + +.fa-newspaper::before { + content: "\f1ea"; } + +.fa-rectangle-ad::before { + content: "\f641"; } + +.fa-ad::before { + content: "\f641"; } + +.fa-circle-arrow-right::before { + content: "\f0a9"; } + +.fa-arrow-circle-right::before { + content: "\f0a9"; } + +.fa-filter-circle-xmark::before { + content: "\e17b"; } + +.fa-locust::before { + content: "\e520"; } + +.fa-sort::before { + content: "\f0dc"; } + +.fa-unsorted::before { + content: "\f0dc"; } + +.fa-list-ol::before { + content: "\f0cb"; } + +.fa-list-1-2::before { + content: "\f0cb"; } + +.fa-list-numeric::before { + content: "\f0cb"; } + +.fa-person-dress-burst::before { + content: "\e544"; } + +.fa-money-check-dollar::before { + content: "\f53d"; } + +.fa-money-check-alt::before { + content: "\f53d"; } + +.fa-vector-square::before { + content: "\f5cb"; } + +.fa-bread-slice::before { + content: "\f7ec"; } + +.fa-language::before { + content: "\f1ab"; } + +.fa-face-kiss-wink-heart::before { + content: "\f598"; } + +.fa-kiss-wink-heart::before { + content: "\f598"; } + +.fa-filter::before { + content: "\f0b0"; } + +.fa-question::before { + content: "\3f"; } + +.fa-file-signature::before { + content: "\f573"; } + +.fa-up-down-left-right::before { + content: "\f0b2"; } + +.fa-arrows-alt::before { + content: "\f0b2"; } + +.fa-house-chimney-user::before { + content: "\e065"; } + +.fa-hand-holding-heart::before { + content: "\f4be"; } + +.fa-puzzle-piece::before { + content: "\f12e"; } + +.fa-money-check::before { + content: "\f53c"; } + +.fa-star-half-stroke::before { + content: "\f5c0"; } + +.fa-star-half-alt::before { + content: "\f5c0"; } + +.fa-code::before { + content: "\f121"; } + +.fa-whiskey-glass::before { + content: "\f7a0"; } + +.fa-glass-whiskey::before { + content: "\f7a0"; } + +.fa-building-circle-exclamation::before { + content: "\e4d3"; } + +.fa-magnifying-glass-chart::before { + content: "\e522"; } + +.fa-arrow-up-right-from-square::before { + content: "\f08e"; } + +.fa-external-link::before { + content: "\f08e"; } + +.fa-cubes-stacked::before { + content: "\e4e6"; } + +.fa-won-sign::before { + content: "\f159"; } + +.fa-krw::before { + content: "\f159"; } + +.fa-won::before { + content: "\f159"; } + +.fa-virus-covid::before { + content: "\e4a8"; } + +.fa-austral-sign::before { + content: "\e0a9"; } + +.fa-f::before { + content: "\46"; } + +.fa-leaf::before { + content: "\f06c"; } + +.fa-road::before { + content: "\f018"; } + +.fa-taxi::before { + content: "\f1ba"; } + +.fa-cab::before { + content: "\f1ba"; } + +.fa-person-circle-plus::before { + content: "\e541"; } + +.fa-chart-pie::before { + content: "\f200"; } + +.fa-pie-chart::before { + content: "\f200"; } + +.fa-bolt-lightning::before { + content: "\e0b7"; } + +.fa-sack-xmark::before { + content: "\e56a"; } + +.fa-file-excel::before { + content: "\f1c3"; } + +.fa-file-contract::before { + content: "\f56c"; } + +.fa-fish-fins::before { + content: "\e4f2"; } + +.fa-building-flag::before { + content: "\e4d5"; } + +.fa-face-grin-beam::before { + content: "\f582"; } + +.fa-grin-beam::before { + content: "\f582"; } + +.fa-object-ungroup::before { + content: "\f248"; } + +.fa-poop::before { + content: "\f619"; } + +.fa-location-pin::before { + content: "\f041"; } + +.fa-map-marker::before { + content: "\f041"; } + +.fa-kaaba::before { + content: "\f66b"; } + +.fa-toilet-paper::before { + content: "\f71e"; } + +.fa-helmet-safety::before { + content: "\f807"; } + +.fa-hard-hat::before { + content: "\f807"; } + +.fa-hat-hard::before { + content: "\f807"; } + +.fa-eject::before { + content: "\f052"; } + +.fa-circle-right::before { + content: "\f35a"; } + +.fa-arrow-alt-circle-right::before { + content: "\f35a"; } + +.fa-plane-circle-check::before { + content: "\e555"; } + +.fa-face-rolling-eyes::before { + content: "\f5a5"; } + +.fa-meh-rolling-eyes::before { + content: "\f5a5"; } + +.fa-object-group::before { + content: "\f247"; } + +.fa-chart-line::before { + content: "\f201"; } + +.fa-line-chart::before { + content: "\f201"; } + +.fa-mask-ventilator::before { + content: "\e524"; } + +.fa-arrow-right::before { + content: "\f061"; } + +.fa-signs-post::before { + content: "\f277"; } + +.fa-map-signs::before { + content: "\f277"; } + +.fa-cash-register::before { + content: "\f788"; } + +.fa-person-circle-question::before { + content: "\e542"; } + +.fa-h::before { + content: "\48"; } + +.fa-tarp::before { + content: "\e57b"; } + +.fa-screwdriver-wrench::before { + content: "\f7d9"; } + +.fa-tools::before { + content: "\f7d9"; } + +.fa-arrows-to-eye::before { + content: "\e4bf"; } + +.fa-plug-circle-bolt::before { + content: "\e55b"; } + +.fa-heart::before { + content: "\f004"; } + +.fa-mars-and-venus::before { + content: "\f224"; } + +.fa-house-user::before { + content: "\e1b0"; } + +.fa-home-user::before { + content: "\e1b0"; } + +.fa-dumpster-fire::before { + content: "\f794"; } + +.fa-house-crack::before { + content: "\e3b1"; } + +.fa-martini-glass-citrus::before { + content: "\f561"; } + +.fa-cocktail::before { + content: "\f561"; } + +.fa-face-surprise::before { + content: "\f5c2"; } + +.fa-surprise::before { + content: "\f5c2"; } + +.fa-bottle-water::before { + content: "\e4c5"; } + +.fa-circle-pause::before { + content: "\f28b"; } + +.fa-pause-circle::before { + content: "\f28b"; } + +.fa-toilet-paper-slash::before { + content: "\e072"; } + +.fa-apple-whole::before { + content: "\f5d1"; } + +.fa-apple-alt::before { + content: "\f5d1"; } + +.fa-kitchen-set::before { + content: "\e51a"; } + +.fa-r::before { + content: "\52"; } + +.fa-temperature-quarter::before { + content: "\f2ca"; } + +.fa-temperature-1::before { + content: "\f2ca"; } + +.fa-thermometer-1::before { + content: "\f2ca"; } + +.fa-thermometer-quarter::before { + content: "\f2ca"; } + +.fa-cube::before { + content: "\f1b2"; } + +.fa-bitcoin-sign::before { + content: "\e0b4"; } + +.fa-shield-dog::before { + content: "\e573"; } + +.fa-solar-panel::before { + content: "\f5ba"; } + +.fa-lock-open::before { + content: "\f3c1"; } + +.fa-elevator::before { + content: "\e16d"; } + +.fa-money-bill-transfer::before { + content: "\e528"; } + +.fa-money-bill-trend-up::before { + content: "\e529"; } + +.fa-house-flood-water-circle-arrow-right::before { + content: "\e50f"; } + +.fa-square-poll-horizontal::before { + content: "\f682"; } + +.fa-poll-h::before { + content: "\f682"; } + +.fa-circle::before { + content: "\f111"; } + +.fa-backward-fast::before { + content: "\f049"; } + +.fa-fast-backward::before { + content: "\f049"; } + +.fa-recycle::before { + content: "\f1b8"; } + +.fa-user-astronaut::before { + content: "\f4fb"; } + +.fa-plane-slash::before { + content: "\e069"; } + +.fa-trademark::before { + content: "\f25c"; } + +.fa-basketball::before { + content: "\f434"; } + +.fa-basketball-ball::before { + content: "\f434"; } + +.fa-satellite-dish::before { + content: "\f7c0"; } + +.fa-circle-up::before { + content: "\f35b"; } + +.fa-arrow-alt-circle-up::before { + content: "\f35b"; } + +.fa-mobile-screen-button::before { + content: "\f3cd"; } + +.fa-mobile-alt::before { + content: "\f3cd"; } + +.fa-volume-high::before { + content: "\f028"; } + +.fa-volume-up::before { + content: "\f028"; } + +.fa-users-rays::before { + content: "\e593"; } + +.fa-wallet::before { + content: "\f555"; } + +.fa-clipboard-check::before { + content: "\f46c"; } + +.fa-file-audio::before { + content: "\f1c7"; } + +.fa-burger::before { + content: "\f805"; } + +.fa-hamburger::before { + content: "\f805"; } + +.fa-wrench::before { + content: "\f0ad"; } + +.fa-bugs::before { + content: "\e4d0"; } + +.fa-rupee-sign::before { + content: "\f156"; } + +.fa-rupee::before { + content: "\f156"; } + +.fa-file-image::before { + content: "\f1c5"; } + +.fa-circle-question::before { + content: "\f059"; } + +.fa-question-circle::before { + content: "\f059"; } + +.fa-plane-departure::before { + content: "\f5b0"; } + +.fa-handshake-slash::before { + content: "\e060"; } + +.fa-book-bookmark::before { + content: "\e0bb"; } + +.fa-code-branch::before { + content: "\f126"; } + +.fa-hat-cowboy::before { + content: "\f8c0"; } + +.fa-bridge::before { + content: "\e4c8"; } + +.fa-phone-flip::before { + content: "\f879"; } + +.fa-phone-alt::before { + content: "\f879"; } + +.fa-truck-front::before { + content: "\e2b7"; } + +.fa-cat::before { + content: "\f6be"; } + +.fa-anchor-circle-exclamation::before { + content: "\e4ab"; } + +.fa-truck-field::before { + content: "\e58d"; } + +.fa-route::before { + content: "\f4d7"; } + +.fa-clipboard-question::before { + content: "\e4e3"; } + +.fa-panorama::before { + content: "\e209"; } + +.fa-comment-medical::before { + content: "\f7f5"; } + +.fa-teeth-open::before { + content: "\f62f"; } + +.fa-file-circle-minus::before { + content: "\e4ed"; } + +.fa-tags::before { + content: "\f02c"; } + +.fa-wine-glass::before { + content: "\f4e3"; } + +.fa-forward-fast::before { + content: "\f050"; } + +.fa-fast-forward::before { + content: "\f050"; } + +.fa-face-meh-blank::before { + content: "\f5a4"; } + +.fa-meh-blank::before { + content: "\f5a4"; } + +.fa-square-parking::before { + content: "\f540"; } + +.fa-parking::before { + content: "\f540"; } + +.fa-house-signal::before { + content: "\e012"; } + +.fa-bars-progress::before { + content: "\f828"; } + +.fa-tasks-alt::before { + content: "\f828"; } + +.fa-faucet-drip::before { + content: "\e006"; } + +.fa-cart-flatbed::before { + content: "\f474"; } + +.fa-dolly-flatbed::before { + content: "\f474"; } + +.fa-ban-smoking::before { + content: "\f54d"; } + +.fa-smoking-ban::before { + content: "\f54d"; } + +.fa-terminal::before { + content: "\f120"; } + +.fa-mobile-button::before { + content: "\f10b"; } + +.fa-house-medical-flag::before { + content: "\e514"; } + +.fa-basket-shopping::before { + content: "\f291"; } + +.fa-shopping-basket::before { + content: "\f291"; } + +.fa-tape::before { + content: "\f4db"; } + +.fa-bus-simple::before { + content: "\f55e"; } + +.fa-bus-alt::before { + content: "\f55e"; } + +.fa-eye::before { + content: "\f06e"; } + +.fa-face-sad-cry::before { + content: "\f5b3"; } + +.fa-sad-cry::before { + content: "\f5b3"; } + +.fa-audio-description::before { + content: "\f29e"; } + +.fa-person-military-to-person::before { + content: "\e54c"; } + +.fa-file-shield::before { + content: "\e4f0"; } + +.fa-user-slash::before { + content: "\f506"; } + +.fa-pen::before { + content: "\f304"; } + +.fa-tower-observation::before { + content: "\e586"; } + +.fa-file-code::before { + content: "\f1c9"; } + +.fa-signal::before { + content: "\f012"; } + +.fa-signal-5::before { + content: "\f012"; } + +.fa-signal-perfect::before { + content: "\f012"; } + +.fa-bus::before { + content: "\f207"; } + +.fa-heart-circle-xmark::before { + content: "\e501"; } + +.fa-house-chimney::before { + content: "\e3af"; } + +.fa-home-lg::before { + content: "\e3af"; } + +.fa-window-maximize::before { + content: "\f2d0"; } + +.fa-face-frown::before { + content: "\f119"; } + +.fa-frown::before { + content: "\f119"; } + +.fa-prescription::before { + content: "\f5b1"; } + +.fa-shop::before { + content: "\f54f"; } + +.fa-store-alt::before { + content: "\f54f"; } + +.fa-floppy-disk::before { + content: "\f0c7"; } + +.fa-save::before { + content: "\f0c7"; } + +.fa-vihara::before { + content: "\f6a7"; } + +.fa-scale-unbalanced::before { + content: "\f515"; } + +.fa-balance-scale-left::before { + content: "\f515"; } + +.fa-sort-up::before { + content: "\f0de"; } + +.fa-sort-asc::before { + content: "\f0de"; } + +.fa-comment-dots::before { + content: "\f4ad"; } + +.fa-commenting::before { + content: "\f4ad"; } + +.fa-plant-wilt::before { + content: "\e5aa"; } + +.fa-diamond::before { + content: "\f219"; } + +.fa-face-grin-squint::before { + content: "\f585"; } + +.fa-grin-squint::before { + content: "\f585"; } + +.fa-hand-holding-dollar::before { + content: "\f4c0"; } + +.fa-hand-holding-usd::before { + content: "\f4c0"; } + +.fa-bacterium::before { + content: "\e05a"; } + +.fa-hand-pointer::before { + content: "\f25a"; } + +.fa-drum-steelpan::before { + content: "\f56a"; } + +.fa-hand-scissors::before { + content: "\f257"; } + +.fa-hands-praying::before { + content: "\f684"; } + +.fa-praying-hands::before { + content: "\f684"; } + +.fa-arrow-rotate-right::before { + content: "\f01e"; } + +.fa-arrow-right-rotate::before { + content: "\f01e"; } + +.fa-arrow-rotate-forward::before { + content: "\f01e"; } + +.fa-redo::before { + content: "\f01e"; } + +.fa-biohazard::before { + content: "\f780"; } + +.fa-location-crosshairs::before { + content: "\f601"; } + +.fa-location::before { + content: "\f601"; } + +.fa-mars-double::before { + content: "\f227"; } + +.fa-child-dress::before { + content: "\e59c"; } + +.fa-users-between-lines::before { + content: "\e591"; } + +.fa-lungs-virus::before { + content: "\e067"; } + +.fa-face-grin-tears::before { + content: "\f588"; } + +.fa-grin-tears::before { + content: "\f588"; } + +.fa-phone::before { + content: "\f095"; } + +.fa-calendar-xmark::before { + content: "\f273"; } + +.fa-calendar-times::before { + content: "\f273"; } + +.fa-child-reaching::before { + content: "\e59d"; } + +.fa-head-side-virus::before { + content: "\e064"; } + +.fa-user-gear::before { + content: "\f4fe"; } + +.fa-user-cog::before { + content: "\f4fe"; } + +.fa-arrow-up-1-9::before { + content: "\f163"; } + +.fa-sort-numeric-up::before { + content: "\f163"; } + +.fa-door-closed::before { + content: "\f52a"; } + +.fa-shield-virus::before { + content: "\e06c"; } + +.fa-dice-six::before { + content: "\f526"; } + +.fa-mosquito-net::before { + content: "\e52c"; } + +.fa-bridge-water::before { + content: "\e4ce"; } + +.fa-person-booth::before { + content: "\f756"; } + +.fa-text-width::before { + content: "\f035"; } + +.fa-hat-wizard::before { + content: "\f6e8"; } + +.fa-pen-fancy::before { + content: "\f5ac"; } + +.fa-person-digging::before { + content: "\f85e"; } + +.fa-digging::before { + content: "\f85e"; } + +.fa-trash::before { + content: "\f1f8"; } + +.fa-gauge-simple::before { + content: "\f629"; } + +.fa-gauge-simple-med::before { + content: "\f629"; } + +.fa-tachometer-average::before { + content: "\f629"; } + +.fa-book-medical::before { + content: "\f7e6"; } + +.fa-poo::before { + content: "\f2fe"; } + +.fa-quote-right::before { + content: "\f10e"; } + +.fa-quote-right-alt::before { + content: "\f10e"; } + +.fa-shirt::before { + content: "\f553"; } + +.fa-t-shirt::before { + content: "\f553"; } + +.fa-tshirt::before { + content: "\f553"; } + +.fa-cubes::before { + content: "\f1b3"; } + +.fa-divide::before { + content: "\f529"; } + +.fa-tenge-sign::before { + content: "\f7d7"; } + +.fa-tenge::before { + content: "\f7d7"; } + +.fa-headphones::before { + content: "\f025"; } + +.fa-hands-holding::before { + content: "\f4c2"; } + +.fa-hands-clapping::before { + content: "\e1a8"; } + +.fa-republican::before { + content: "\f75e"; } + +.fa-arrow-left::before { + content: "\f060"; } + +.fa-person-circle-xmark::before { + content: "\e543"; } + +.fa-ruler::before { + content: "\f545"; } + +.fa-align-left::before { + content: "\f036"; } + +.fa-dice-d6::before { + content: "\f6d1"; } + +.fa-restroom::before { + content: "\f7bd"; } + +.fa-j::before { + content: "\4a"; } + +.fa-users-viewfinder::before { + content: "\e595"; } + +.fa-file-video::before { + content: "\f1c8"; } + +.fa-up-right-from-square::before { + content: "\f35d"; } + +.fa-external-link-alt::before { + content: "\f35d"; } + +.fa-table-cells::before { + content: "\f00a"; } + +.fa-th::before { + content: "\f00a"; } + +.fa-file-pdf::before { + content: "\f1c1"; } + +.fa-book-bible::before { + content: "\f647"; } + +.fa-bible::before { + content: "\f647"; } + +.fa-o::before { + content: "\4f"; } + +.fa-suitcase-medical::before { + content: "\f0fa"; } + +.fa-medkit::before { + content: "\f0fa"; } + +.fa-user-secret::before { + content: "\f21b"; } + +.fa-otter::before { + content: "\f700"; } + +.fa-person-dress::before { + content: "\f182"; } + +.fa-female::before { + content: "\f182"; } + +.fa-comment-dollar::before { + content: "\f651"; } + +.fa-business-time::before { + content: "\f64a"; } + +.fa-briefcase-clock::before { + content: "\f64a"; } + +.fa-table-cells-large::before { + content: "\f009"; } + +.fa-th-large::before { + content: "\f009"; } + +.fa-book-tanakh::before { + content: "\f827"; } + +.fa-tanakh::before { + content: "\f827"; } + +.fa-phone-volume::before { + content: "\f2a0"; } + +.fa-volume-control-phone::before { + content: "\f2a0"; } + +.fa-hat-cowboy-side::before { + content: "\f8c1"; } + +.fa-clipboard-user::before { + content: "\f7f3"; } + +.fa-child::before { + content: "\f1ae"; } + +.fa-lira-sign::before { + content: "\f195"; } + +.fa-satellite::before { + content: "\f7bf"; } + +.fa-plane-lock::before { + content: "\e558"; } + +.fa-tag::before { + content: "\f02b"; } + +.fa-comment::before { + content: "\f075"; } + +.fa-cake-candles::before { + content: "\f1fd"; } + +.fa-birthday-cake::before { + content: "\f1fd"; } + +.fa-cake::before { + content: "\f1fd"; } + +.fa-envelope::before { + content: "\f0e0"; } + +.fa-angles-up::before { + content: "\f102"; } + +.fa-angle-double-up::before { + content: "\f102"; } + +.fa-paperclip::before { + content: "\f0c6"; } + +.fa-arrow-right-to-city::before { + content: "\e4b3"; } + +.fa-ribbon::before { + content: "\f4d6"; } + +.fa-lungs::before { + content: "\f604"; } + +.fa-arrow-up-9-1::before { + content: "\f887"; } + +.fa-sort-numeric-up-alt::before { + content: "\f887"; } + +.fa-litecoin-sign::before { + content: "\e1d3"; } + +.fa-border-none::before { + content: "\f850"; } + +.fa-circle-nodes::before { + content: "\e4e2"; } + +.fa-parachute-box::before { + content: "\f4cd"; } + +.fa-indent::before { + content: "\f03c"; } + +.fa-truck-field-un::before { + content: "\e58e"; } + +.fa-hourglass::before { + content: "\f254"; } + +.fa-hourglass-empty::before { + content: "\f254"; } + +.fa-mountain::before { + content: "\f6fc"; } + +.fa-user-doctor::before { + content: "\f0f0"; } + +.fa-user-md::before { + content: "\f0f0"; } + +.fa-circle-info::before { + content: "\f05a"; } + +.fa-info-circle::before { + content: "\f05a"; } + +.fa-cloud-meatball::before { + content: "\f73b"; } + +.fa-camera::before { + content: "\f030"; } + +.fa-camera-alt::before { + content: "\f030"; } + +.fa-square-virus::before { + content: "\e578"; } + +.fa-meteor::before { + content: "\f753"; } + +.fa-car-on::before { + content: "\e4dd"; } + +.fa-sleigh::before { + content: "\f7cc"; } + +.fa-arrow-down-1-9::before { + content: "\f162"; } + +.fa-sort-numeric-asc::before { + content: "\f162"; } + +.fa-sort-numeric-down::before { + content: "\f162"; } + +.fa-hand-holding-droplet::before { + content: "\f4c1"; } + +.fa-hand-holding-water::before { + content: "\f4c1"; } + +.fa-water::before { + content: "\f773"; } + +.fa-calendar-check::before { + content: "\f274"; } + +.fa-braille::before { + content: "\f2a1"; } + +.fa-prescription-bottle-medical::before { + content: "\f486"; } + +.fa-prescription-bottle-alt::before { + content: "\f486"; } + +.fa-landmark::before { + content: "\f66f"; } + +.fa-truck::before { + content: "\f0d1"; } + +.fa-crosshairs::before { + content: "\f05b"; } + +.fa-person-cane::before { + content: "\e53c"; } + +.fa-tent::before { + content: "\e57d"; } + +.fa-vest-patches::before { + content: "\e086"; } + +.fa-check-double::before { + content: "\f560"; } + +.fa-arrow-down-a-z::before { + content: "\f15d"; } + +.fa-sort-alpha-asc::before { + content: "\f15d"; } + +.fa-sort-alpha-down::before { + content: "\f15d"; } + +.fa-money-bill-wheat::before { + content: "\e52a"; } + +.fa-cookie::before { + content: "\f563"; } + +.fa-arrow-rotate-left::before { + content: "\f0e2"; } + +.fa-arrow-left-rotate::before { + content: "\f0e2"; } + +.fa-arrow-rotate-back::before { + content: "\f0e2"; } + +.fa-arrow-rotate-backward::before { + content: "\f0e2"; } + +.fa-undo::before { + content: "\f0e2"; } + +.fa-hard-drive::before { + content: "\f0a0"; } + +.fa-hdd::before { + content: "\f0a0"; } + +.fa-face-grin-squint-tears::before { + content: "\f586"; } + +.fa-grin-squint-tears::before { + content: "\f586"; } + +.fa-dumbbell::before { + content: "\f44b"; } + +.fa-rectangle-list::before { + content: "\f022"; } + +.fa-list-alt::before { + content: "\f022"; } + +.fa-tarp-droplet::before { + content: "\e57c"; } + +.fa-house-medical-circle-check::before { + content: "\e511"; } + +.fa-person-skiing-nordic::before { + content: "\f7ca"; } + +.fa-skiing-nordic::before { + content: "\f7ca"; } + +.fa-calendar-plus::before { + content: "\f271"; } + +.fa-plane-arrival::before { + content: "\f5af"; } + +.fa-circle-left::before { + content: "\f359"; } + +.fa-arrow-alt-circle-left::before { + content: "\f359"; } + +.fa-train-subway::before { + content: "\f239"; } + +.fa-subway::before { + content: "\f239"; } + +.fa-chart-gantt::before { + content: "\e0e4"; } + +.fa-indian-rupee-sign::before { + content: "\e1bc"; } + +.fa-indian-rupee::before { + content: "\e1bc"; } + +.fa-inr::before { + content: "\e1bc"; } + +.fa-crop-simple::before { + content: "\f565"; } + +.fa-crop-alt::before { + content: "\f565"; } + +.fa-money-bill-1::before { + content: "\f3d1"; } + +.fa-money-bill-alt::before { + content: "\f3d1"; } + +.fa-left-long::before { + content: "\f30a"; } + +.fa-long-arrow-alt-left::before { + content: "\f30a"; } + +.fa-dna::before { + content: "\f471"; } + +.fa-virus-slash::before { + content: "\e075"; } + +.fa-minus::before { + content: "\f068"; } + +.fa-subtract::before { + content: "\f068"; } + +.fa-chess::before { + content: "\f439"; } + +.fa-arrow-left-long::before { + content: "\f177"; } + +.fa-long-arrow-left::before { + content: "\f177"; } + +.fa-plug-circle-check::before { + content: "\e55c"; } + +.fa-street-view::before { + content: "\f21d"; } + +.fa-franc-sign::before { + content: "\e18f"; } + +.fa-volume-off::before { + content: "\f026"; } + +.fa-hands-asl-interpreting::before { + content: "\f2a3"; } + +.fa-american-sign-language-interpreting::before { + content: "\f2a3"; } + +.fa-asl-interpreting::before { + content: "\f2a3"; } + +.fa-hands-american-sign-language-interpreting::before { + content: "\f2a3"; } + +.fa-gear::before { + content: "\f013"; } + +.fa-cog::before { + content: "\f013"; } + +.fa-droplet-slash::before { + content: "\f5c7"; } + +.fa-tint-slash::before { + content: "\f5c7"; } + +.fa-mosque::before { + content: "\f678"; } + +.fa-mosquito::before { + content: "\e52b"; } + +.fa-star-of-david::before { + content: "\f69a"; } + +.fa-person-military-rifle::before { + content: "\e54b"; } + +.fa-cart-shopping::before { + content: "\f07a"; } + +.fa-shopping-cart::before { + content: "\f07a"; } + +.fa-vials::before { + content: "\f493"; } + +.fa-plug-circle-plus::before { + content: "\e55f"; } + +.fa-place-of-worship::before { + content: "\f67f"; } + +.fa-grip-vertical::before { + content: "\f58e"; } + +.fa-arrow-turn-up::before { + content: "\f148"; } + +.fa-level-up::before { + content: "\f148"; } + +.fa-u::before { + content: "\55"; } + +.fa-square-root-variable::before { + content: "\f698"; } + +.fa-square-root-alt::before { + content: "\f698"; } + +.fa-clock::before { + content: "\f017"; } + +.fa-clock-four::before { + content: "\f017"; } + +.fa-backward-step::before { + content: "\f048"; } + +.fa-step-backward::before { + content: "\f048"; } + +.fa-pallet::before { + content: "\f482"; } + +.fa-faucet::before { + content: "\e005"; } + +.fa-baseball-bat-ball::before { + content: "\f432"; } + +.fa-s::before { + content: "\53"; } + +.fa-timeline::before { + content: "\e29c"; } + +.fa-keyboard::before { + content: "\f11c"; } + +.fa-caret-down::before { + content: "\f0d7"; } + +.fa-house-chimney-medical::before { + content: "\f7f2"; } + +.fa-clinic-medical::before { + content: "\f7f2"; } + +.fa-temperature-three-quarters::before { + content: "\f2c8"; } + +.fa-temperature-3::before { + content: "\f2c8"; } + +.fa-thermometer-3::before { + content: "\f2c8"; } + +.fa-thermometer-three-quarters::before { + content: "\f2c8"; } + +.fa-mobile-screen::before { + content: "\f3cf"; } + +.fa-mobile-android-alt::before { + content: "\f3cf"; } + +.fa-plane-up::before { + content: "\e22d"; } + +.fa-piggy-bank::before { + content: "\f4d3"; } + +.fa-battery-half::before { + content: "\f242"; } + +.fa-battery-3::before { + content: "\f242"; } + +.fa-mountain-city::before { + content: "\e52e"; } + +.fa-coins::before { + content: "\f51e"; } + +.fa-khanda::before { + content: "\f66d"; } + +.fa-sliders::before { + content: "\f1de"; } + +.fa-sliders-h::before { + content: "\f1de"; } + +.fa-folder-tree::before { + content: "\f802"; } + +.fa-network-wired::before { + content: "\f6ff"; } + +.fa-map-pin::before { + content: "\f276"; } + +.fa-hamsa::before { + content: "\f665"; } + +.fa-cent-sign::before { + content: "\e3f5"; } + +.fa-flask::before { + content: "\f0c3"; } + +.fa-person-pregnant::before { + content: "\e31e"; } + +.fa-wand-sparkles::before { + content: "\f72b"; } + +.fa-ellipsis-vertical::before { + content: "\f142"; } + +.fa-ellipsis-v::before { + content: "\f142"; } + +.fa-ticket::before { + content: "\f145"; } + +.fa-power-off::before { + content: "\f011"; } + +.fa-right-long::before { + content: "\f30b"; } + +.fa-long-arrow-alt-right::before { + content: "\f30b"; } + +.fa-flag-usa::before { + content: "\f74d"; } + +.fa-laptop-file::before { + content: "\e51d"; } + +.fa-tty::before { + content: "\f1e4"; } + +.fa-teletype::before { + content: "\f1e4"; } + +.fa-diagram-next::before { + content: "\e476"; } + +.fa-person-rifle::before { + content: "\e54e"; } + +.fa-house-medical-circle-exclamation::before { + content: "\e512"; } + +.fa-closed-captioning::before { + content: "\f20a"; } + +.fa-person-hiking::before { + content: "\f6ec"; } + +.fa-hiking::before { + content: "\f6ec"; } + +.fa-venus-double::before { + content: "\f226"; } + +.fa-images::before { + content: "\f302"; } + +.fa-calculator::before { + content: "\f1ec"; } + +.fa-people-pulling::before { + content: "\e535"; } + +.fa-n::before { + content: "\4e"; } + +.fa-cable-car::before { + content: "\f7da"; } + +.fa-tram::before { + content: "\f7da"; } + +.fa-cloud-rain::before { + content: "\f73d"; } + +.fa-building-circle-xmark::before { + content: "\e4d4"; } + +.fa-ship::before { + content: "\f21a"; } + +.fa-arrows-down-to-line::before { + content: "\e4b8"; } + +.fa-download::before { + content: "\f019"; } + +.fa-face-grin::before { + content: "\f580"; } + +.fa-grin::before { + content: "\f580"; } + +.fa-delete-left::before { + content: "\f55a"; } + +.fa-backspace::before { + content: "\f55a"; } + +.fa-eye-dropper::before { + content: "\f1fb"; } + +.fa-eye-dropper-empty::before { + content: "\f1fb"; } + +.fa-eyedropper::before { + content: "\f1fb"; } + +.fa-file-circle-check::before { + content: "\e5a0"; } + +.fa-forward::before { + content: "\f04e"; } + +.fa-mobile::before { + content: "\f3ce"; } + +.fa-mobile-android::before { + content: "\f3ce"; } + +.fa-mobile-phone::before { + content: "\f3ce"; } + +.fa-face-meh::before { + content: "\f11a"; } + +.fa-meh::before { + content: "\f11a"; } + +.fa-align-center::before { + content: "\f037"; } + +.fa-book-skull::before { + content: "\f6b7"; } + +.fa-book-dead::before { + content: "\f6b7"; } + +.fa-id-card::before { + content: "\f2c2"; } + +.fa-drivers-license::before { + content: "\f2c2"; } + +.fa-outdent::before { + content: "\f03b"; } + +.fa-dedent::before { + content: "\f03b"; } + +.fa-heart-circle-exclamation::before { + content: "\e4fe"; } + +.fa-house::before { + content: "\f015"; } + +.fa-home::before { + content: "\f015"; } + +.fa-home-alt::before { + content: "\f015"; } + +.fa-home-lg-alt::before { + content: "\f015"; } + +.fa-calendar-week::before { + content: "\f784"; } + +.fa-laptop-medical::before { + content: "\f812"; } + +.fa-b::before { + content: "\42"; } + +.fa-file-medical::before { + content: "\f477"; } + +.fa-dice-one::before { + content: "\f525"; } + +.fa-kiwi-bird::before { + content: "\f535"; } + +.fa-arrow-right-arrow-left::before { + content: "\f0ec"; } + +.fa-exchange::before { + content: "\f0ec"; } + +.fa-rotate-right::before { + content: "\f2f9"; } + +.fa-redo-alt::before { + content: "\f2f9"; } + +.fa-rotate-forward::before { + content: "\f2f9"; } + +.fa-utensils::before { + content: "\f2e7"; } + +.fa-cutlery::before { + content: "\f2e7"; } + +.fa-arrow-up-wide-short::before { + content: "\f161"; } + +.fa-sort-amount-up::before { + content: "\f161"; } + +.fa-mill-sign::before { + content: "\e1ed"; } + +.fa-bowl-rice::before { + content: "\e2eb"; } + +.fa-skull::before { + content: "\f54c"; } + +.fa-tower-broadcast::before { + content: "\f519"; } + +.fa-broadcast-tower::before { + content: "\f519"; } + +.fa-truck-pickup::before { + content: "\f63c"; } + +.fa-up-long::before { + content: "\f30c"; } + +.fa-long-arrow-alt-up::before { + content: "\f30c"; } + +.fa-stop::before { + content: "\f04d"; } + +.fa-code-merge::before { + content: "\f387"; } + +.fa-upload::before { + content: "\f093"; } + +.fa-hurricane::before { + content: "\f751"; } + +.fa-mound::before { + content: "\e52d"; } + +.fa-toilet-portable::before { + content: "\e583"; } + +.fa-compact-disc::before { + content: "\f51f"; } + +.fa-file-arrow-down::before { + content: "\f56d"; } + +.fa-file-download::before { + content: "\f56d"; } + +.fa-caravan::before { + content: "\f8ff"; } + +.fa-shield-cat::before { + content: "\e572"; } + +.fa-bolt::before { + content: "\f0e7"; } + +.fa-zap::before { + content: "\f0e7"; } + +.fa-glass-water::before { + content: "\e4f4"; } + +.fa-oil-well::before { + content: "\e532"; } + +.fa-vault::before { + content: "\e2c5"; } + +.fa-mars::before { + content: "\f222"; } + +.fa-toilet::before { + content: "\f7d8"; } + +.fa-plane-circle-xmark::before { + content: "\e557"; } + +.fa-yen-sign::before { + content: "\f157"; } + +.fa-cny::before { + content: "\f157"; } + +.fa-jpy::before { + content: "\f157"; } + +.fa-rmb::before { + content: "\f157"; } + +.fa-yen::before { + content: "\f157"; } + +.fa-ruble-sign::before { + content: "\f158"; } + +.fa-rouble::before { + content: "\f158"; } + +.fa-rub::before { + content: "\f158"; } + +.fa-ruble::before { + content: "\f158"; } + +.fa-sun::before { + content: "\f185"; } + +.fa-guitar::before { + content: "\f7a6"; } + +.fa-face-laugh-wink::before { + content: "\f59c"; } + +.fa-laugh-wink::before { + content: "\f59c"; } + +.fa-horse-head::before { + content: "\f7ab"; } + +.fa-bore-hole::before { + content: "\e4c3"; } + +.fa-industry::before { + content: "\f275"; } + +.fa-circle-down::before { + content: "\f358"; } + +.fa-arrow-alt-circle-down::before { + content: "\f358"; } + +.fa-arrows-turn-to-dots::before { + content: "\e4c1"; } + +.fa-florin-sign::before { + content: "\e184"; } + +.fa-arrow-down-short-wide::before { + content: "\f884"; } + +.fa-sort-amount-desc::before { + content: "\f884"; } + +.fa-sort-amount-down-alt::before { + content: "\f884"; } + +.fa-less-than::before { + content: "\3c"; } + +.fa-angle-down::before { + content: "\f107"; } + +.fa-car-tunnel::before { + content: "\e4de"; } + +.fa-head-side-cough::before { + content: "\e061"; } + +.fa-grip-lines::before { + content: "\f7a4"; } + +.fa-thumbs-down::before { + content: "\f165"; } + +.fa-user-lock::before { + content: "\f502"; } + +.fa-arrow-right-long::before { + content: "\f178"; } + +.fa-long-arrow-right::before { + content: "\f178"; } + +.fa-anchor-circle-xmark::before { + content: "\e4ac"; } + +.fa-ellipsis::before { + content: "\f141"; } + +.fa-ellipsis-h::before { + content: "\f141"; } + +.fa-chess-pawn::before { + content: "\f443"; } + +.fa-kit-medical::before { + content: "\f479"; } + +.fa-first-aid::before { + content: "\f479"; } + +.fa-person-through-window::before { + content: "\e5a9"; } + +.fa-toolbox::before { + content: "\f552"; } + +.fa-hands-holding-circle::before { + content: "\e4fb"; } + +.fa-bug::before { + content: "\f188"; } + +.fa-credit-card::before { + content: "\f09d"; } + +.fa-credit-card-alt::before { + content: "\f09d"; } + +.fa-car::before { + content: "\f1b9"; } + +.fa-automobile::before { + content: "\f1b9"; } + +.fa-hand-holding-hand::before { + content: "\e4f7"; } + +.fa-book-open-reader::before { + content: "\f5da"; } + +.fa-book-reader::before { + content: "\f5da"; } + +.fa-mountain-sun::before { + content: "\e52f"; } + +.fa-arrows-left-right-to-line::before { + content: "\e4ba"; } + +.fa-dice-d20::before { + content: "\f6cf"; } + +.fa-truck-droplet::before { + content: "\e58c"; } + +.fa-file-circle-xmark::before { + content: "\e5a1"; } + +.fa-temperature-arrow-up::before { + content: "\e040"; } + +.fa-temperature-up::before { + content: "\e040"; } + +.fa-medal::before { + content: "\f5a2"; } + +.fa-bed::before { + content: "\f236"; } + +.fa-square-h::before { + content: "\f0fd"; } + +.fa-h-square::before { + content: "\f0fd"; } + +.fa-podcast::before { + content: "\f2ce"; } + +.fa-temperature-full::before { + content: "\f2c7"; } + +.fa-temperature-4::before { + content: "\f2c7"; } + +.fa-thermometer-4::before { + content: "\f2c7"; } + +.fa-thermometer-full::before { + content: "\f2c7"; } + +.fa-bell::before { + content: "\f0f3"; } + +.fa-superscript::before { + content: "\f12b"; } + +.fa-plug-circle-xmark::before { + content: "\e560"; } + +.fa-star-of-life::before { + content: "\f621"; } + +.fa-phone-slash::before { + content: "\f3dd"; } + +.fa-paint-roller::before { + content: "\f5aa"; } + +.fa-handshake-angle::before { + content: "\f4c4"; } + +.fa-hands-helping::before { + content: "\f4c4"; } + +.fa-location-dot::before { + content: "\f3c5"; } + +.fa-map-marker-alt::before { + content: "\f3c5"; } + +.fa-file::before { + content: "\f15b"; } + +.fa-greater-than::before { + content: "\3e"; } + +.fa-person-swimming::before { + content: "\f5c4"; } + +.fa-swimmer::before { + content: "\f5c4"; } + +.fa-arrow-down::before { + content: "\f063"; } + +.fa-droplet::before { + content: "\f043"; } + +.fa-tint::before { + content: "\f043"; } + +.fa-eraser::before { + content: "\f12d"; } + +.fa-earth-americas::before { + content: "\f57d"; } + +.fa-earth::before { + content: "\f57d"; } + +.fa-earth-america::before { + content: "\f57d"; } + +.fa-globe-americas::before { + content: "\f57d"; } + +.fa-person-burst::before { + content: "\e53b"; } + +.fa-dove::before { + content: "\f4ba"; } + +.fa-battery-empty::before { + content: "\f244"; } + +.fa-battery-0::before { + content: "\f244"; } + +.fa-socks::before { + content: "\f696"; } + +.fa-inbox::before { + content: "\f01c"; } + +.fa-section::before { + content: "\e447"; } + +.fa-gauge-high::before { + content: "\f625"; } + +.fa-tachometer-alt::before { + content: "\f625"; } + +.fa-tachometer-alt-fast::before { + content: "\f625"; } + +.fa-envelope-open-text::before { + content: "\f658"; } + +.fa-hospital::before { + content: "\f0f8"; } + +.fa-hospital-alt::before { + content: "\f0f8"; } + +.fa-hospital-wide::before { + content: "\f0f8"; } + +.fa-wine-bottle::before { + content: "\f72f"; } + +.fa-chess-rook::before { + content: "\f447"; } + +.fa-bars-staggered::before { + content: "\f550"; } + +.fa-reorder::before { + content: "\f550"; } + +.fa-stream::before { + content: "\f550"; } + +.fa-dharmachakra::before { + content: "\f655"; } + +.fa-hotdog::before { + content: "\f80f"; } + +.fa-person-walking-with-cane::before { + content: "\f29d"; } + +.fa-blind::before { + content: "\f29d"; } + +.fa-drum::before { + content: "\f569"; } + +.fa-ice-cream::before { + content: "\f810"; } + +.fa-heart-circle-bolt::before { + content: "\e4fc"; } + +.fa-fax::before { + content: "\f1ac"; } + +.fa-paragraph::before { + content: "\f1dd"; } + +.fa-check-to-slot::before { + content: "\f772"; } + +.fa-vote-yea::before { + content: "\f772"; } + +.fa-star-half::before { + content: "\f089"; } + +.fa-boxes-stacked::before { + content: "\f468"; } + +.fa-boxes::before { + content: "\f468"; } + +.fa-boxes-alt::before { + content: "\f468"; } + +.fa-link::before { + content: "\f0c1"; } + +.fa-chain::before { + content: "\f0c1"; } + +.fa-ear-listen::before { + content: "\f2a2"; } + +.fa-assistive-listening-systems::before { + content: "\f2a2"; } + +.fa-tree-city::before { + content: "\e587"; } + +.fa-play::before { + content: "\f04b"; } + +.fa-font::before { + content: "\f031"; } + +.fa-rupiah-sign::before { + content: "\e23d"; } + +.fa-magnifying-glass::before { + content: "\f002"; } + +.fa-search::before { + content: "\f002"; } + +.fa-table-tennis-paddle-ball::before { + content: "\f45d"; } + +.fa-ping-pong-paddle-ball::before { + content: "\f45d"; } + +.fa-table-tennis::before { + content: "\f45d"; } + +.fa-person-dots-from-line::before { + content: "\f470"; } + +.fa-diagnoses::before { + content: "\f470"; } + +.fa-trash-can-arrow-up::before { + content: "\f82a"; } + +.fa-trash-restore-alt::before { + content: "\f82a"; } + +.fa-naira-sign::before { + content: "\e1f6"; } + +.fa-cart-arrow-down::before { + content: "\f218"; } + +.fa-walkie-talkie::before { + content: "\f8ef"; } + +.fa-file-pen::before { + content: "\f31c"; } + +.fa-file-edit::before { + content: "\f31c"; } + +.fa-receipt::before { + content: "\f543"; } + +.fa-square-pen::before { + content: "\f14b"; } + +.fa-pen-square::before { + content: "\f14b"; } + +.fa-pencil-square::before { + content: "\f14b"; } + +.fa-suitcase-rolling::before { + content: "\f5c1"; } + +.fa-person-circle-exclamation::before { + content: "\e53f"; } + +.fa-chevron-down::before { + content: "\f078"; } + +.fa-battery-full::before { + content: "\f240"; } + +.fa-battery::before { + content: "\f240"; } + +.fa-battery-5::before { + content: "\f240"; } + +.fa-skull-crossbones::before { + content: "\f714"; } + +.fa-code-compare::before { + content: "\e13a"; } + +.fa-list-ul::before { + content: "\f0ca"; } + +.fa-list-dots::before { + content: "\f0ca"; } + +.fa-school-lock::before { + content: "\e56f"; } + +.fa-tower-cell::before { + content: "\e585"; } + +.fa-down-long::before { + content: "\f309"; } + +.fa-long-arrow-alt-down::before { + content: "\f309"; } + +.fa-ranking-star::before { + content: "\e561"; } + +.fa-chess-king::before { + content: "\f43f"; } + +.fa-person-harassing::before { + content: "\e549"; } + +.fa-brazilian-real-sign::before { + content: "\e46c"; } + +.fa-landmark-dome::before { + content: "\f752"; } + +.fa-landmark-alt::before { + content: "\f752"; } + +.fa-arrow-up::before { + content: "\f062"; } + +.fa-tv::before { + content: "\f26c"; } + +.fa-television::before { + content: "\f26c"; } + +.fa-tv-alt::before { + content: "\f26c"; } + +.fa-shrimp::before { + content: "\e448"; } + +.fa-list-check::before { + content: "\f0ae"; } + +.fa-tasks::before { + content: "\f0ae"; } + +.fa-jug-detergent::before { + content: "\e519"; } + +.fa-circle-user::before { + content: "\f2bd"; } + +.fa-user-circle::before { + content: "\f2bd"; } + +.fa-user-shield::before { + content: "\f505"; } + +.fa-wind::before { + content: "\f72e"; } + +.fa-car-burst::before { + content: "\f5e1"; } + +.fa-car-crash::before { + content: "\f5e1"; } + +.fa-y::before { + content: "\59"; } + +.fa-person-snowboarding::before { + content: "\f7ce"; } + +.fa-snowboarding::before { + content: "\f7ce"; } + +.fa-truck-fast::before { + content: "\f48b"; } + +.fa-shipping-fast::before { + content: "\f48b"; } + +.fa-fish::before { + content: "\f578"; } + +.fa-user-graduate::before { + content: "\f501"; } + +.fa-circle-half-stroke::before { + content: "\f042"; } + +.fa-adjust::before { + content: "\f042"; } + +.fa-clapperboard::before { + content: "\e131"; } + +.fa-circle-radiation::before { + content: "\f7ba"; } + +.fa-radiation-alt::before { + content: "\f7ba"; } + +.fa-baseball::before { + content: "\f433"; } + +.fa-baseball-ball::before { + content: "\f433"; } + +.fa-jet-fighter-up::before { + content: "\e518"; } + +.fa-diagram-project::before { + content: "\f542"; } + +.fa-project-diagram::before { + content: "\f542"; } + +.fa-copy::before { + content: "\f0c5"; } + +.fa-volume-xmark::before { + content: "\f6a9"; } + +.fa-volume-mute::before { + content: "\f6a9"; } + +.fa-volume-times::before { + content: "\f6a9"; } + +.fa-hand-sparkles::before { + content: "\e05d"; } + +.fa-grip::before { + content: "\f58d"; } + +.fa-grip-horizontal::before { + content: "\f58d"; } + +.fa-share-from-square::before { + content: "\f14d"; } + +.fa-share-square::before { + content: "\f14d"; } + +.fa-child-combatant::before { + content: "\e4e0"; } + +.fa-child-rifle::before { + content: "\e4e0"; } + +.fa-gun::before { + content: "\e19b"; } + +.fa-square-phone::before { + content: "\f098"; } + +.fa-phone-square::before { + content: "\f098"; } + +.fa-plus::before { + content: "\2b"; } + +.fa-add::before { + content: "\2b"; } + +.fa-expand::before { + content: "\f065"; } + +.fa-computer::before { + content: "\e4e5"; } + +.fa-xmark::before { + content: "\f00d"; } + +.fa-close::before { + content: "\f00d"; } + +.fa-multiply::before { + content: "\f00d"; } + +.fa-remove::before { + content: "\f00d"; } + +.fa-times::before { + content: "\f00d"; } + +.fa-arrows-up-down-left-right::before { + content: "\f047"; } + +.fa-arrows::before { + content: "\f047"; } + +.fa-chalkboard-user::before { + content: "\f51c"; } + +.fa-chalkboard-teacher::before { + content: "\f51c"; } + +.fa-peso-sign::before { + content: "\e222"; } + +.fa-building-shield::before { + content: "\e4d8"; } + +.fa-baby::before { + content: "\f77c"; } + +.fa-users-line::before { + content: "\e592"; } + +.fa-quote-left::before { + content: "\f10d"; } + +.fa-quote-left-alt::before { + content: "\f10d"; } + +.fa-tractor::before { + content: "\f722"; } + +.fa-trash-arrow-up::before { + content: "\f829"; } + +.fa-trash-restore::before { + content: "\f829"; } + +.fa-arrow-down-up-lock::before { + content: "\e4b0"; } + +.fa-lines-leaning::before { + content: "\e51e"; } + +.fa-ruler-combined::before { + content: "\f546"; } + +.fa-copyright::before { + content: "\f1f9"; } + +.fa-equals::before { + content: "\3d"; } + +.fa-blender::before { + content: "\f517"; } + +.fa-teeth::before { + content: "\f62e"; } + +.fa-shekel-sign::before { + content: "\f20b"; } + +.fa-ils::before { + content: "\f20b"; } + +.fa-shekel::before { + content: "\f20b"; } + +.fa-sheqel::before { + content: "\f20b"; } + +.fa-sheqel-sign::before { + content: "\f20b"; } + +.fa-map::before { + content: "\f279"; } + +.fa-rocket::before { + content: "\f135"; } + +.fa-photo-film::before { + content: "\f87c"; } + +.fa-photo-video::before { + content: "\f87c"; } + +.fa-folder-minus::before { + content: "\f65d"; } + +.fa-store::before { + content: "\f54e"; } + +.fa-arrow-trend-up::before { + content: "\e098"; } + +.fa-plug-circle-minus::before { + content: "\e55e"; } + +.fa-sign-hanging::before { + content: "\f4d9"; } + +.fa-sign::before { + content: "\f4d9"; } + +.fa-bezier-curve::before { + content: "\f55b"; } + +.fa-bell-slash::before { + content: "\f1f6"; } + +.fa-tablet::before { + content: "\f3fb"; } + +.fa-tablet-android::before { + content: "\f3fb"; } + +.fa-school-flag::before { + content: "\e56e"; } + +.fa-fill::before { + content: "\f575"; } + +.fa-angle-up::before { + content: "\f106"; } + +.fa-drumstick-bite::before { + content: "\f6d7"; } + +.fa-holly-berry::before { + content: "\f7aa"; } + +.fa-chevron-left::before { + content: "\f053"; } + +.fa-bacteria::before { + content: "\e059"; } + +.fa-hand-lizard::before { + content: "\f258"; } + +.fa-notdef::before { + content: "\e1fe"; } + +.fa-disease::before { + content: "\f7fa"; } + +.fa-briefcase-medical::before { + content: "\f469"; } + +.fa-genderless::before { + content: "\f22d"; } + +.fa-chevron-right::before { + content: "\f054"; } + +.fa-retweet::before { + content: "\f079"; } + +.fa-car-rear::before { + content: "\f5de"; } + +.fa-car-alt::before { + content: "\f5de"; } + +.fa-pump-soap::before { + content: "\e06b"; } + +.fa-video-slash::before { + content: "\f4e2"; } + +.fa-battery-quarter::before { + content: "\f243"; } + +.fa-battery-2::before { + content: "\f243"; } + +.fa-radio::before { + content: "\f8d7"; } + +.fa-baby-carriage::before { + content: "\f77d"; } + +.fa-carriage-baby::before { + content: "\f77d"; } + +.fa-traffic-light::before { + content: "\f637"; } + +.fa-thermometer::before { + content: "\f491"; } + +.fa-vr-cardboard::before { + content: "\f729"; } + +.fa-hand-middle-finger::before { + content: "\f806"; } + +.fa-percent::before { + content: "\25"; } + +.fa-percentage::before { + content: "\25"; } + +.fa-truck-moving::before { + content: "\f4df"; } + +.fa-glass-water-droplet::before { + content: "\e4f5"; } + +.fa-display::before { + content: "\e163"; } + +.fa-face-smile::before { + content: "\f118"; } + +.fa-smile::before { + content: "\f118"; } + +.fa-thumbtack::before { + content: "\f08d"; } + +.fa-thumb-tack::before { + content: "\f08d"; } + +.fa-trophy::before { + content: "\f091"; } + +.fa-person-praying::before { + content: "\f683"; } + +.fa-pray::before { + content: "\f683"; } + +.fa-hammer::before { + content: "\f6e3"; } + +.fa-hand-peace::before { + content: "\f25b"; } + +.fa-rotate::before { + content: "\f2f1"; } + +.fa-sync-alt::before { + content: "\f2f1"; } + +.fa-spinner::before { + content: "\f110"; } + +.fa-robot::before { + content: "\f544"; } + +.fa-peace::before { + content: "\f67c"; } + +.fa-gears::before { + content: "\f085"; } + +.fa-cogs::before { + content: "\f085"; } + +.fa-warehouse::before { + content: "\f494"; } + +.fa-arrow-up-right-dots::before { + content: "\e4b7"; } + +.fa-splotch::before { + content: "\f5bc"; } + +.fa-face-grin-hearts::before { + content: "\f584"; } + +.fa-grin-hearts::before { + content: "\f584"; } + +.fa-dice-four::before { + content: "\f524"; } + +.fa-sim-card::before { + content: "\f7c4"; } + +.fa-transgender::before { + content: "\f225"; } + +.fa-transgender-alt::before { + content: "\f225"; } + +.fa-mercury::before { + content: "\f223"; } + +.fa-arrow-turn-down::before { + content: "\f149"; } + +.fa-level-down::before { + content: "\f149"; } + +.fa-person-falling-burst::before { + content: "\e547"; } + +.fa-award::before { + content: "\f559"; } + +.fa-ticket-simple::before { + content: "\f3ff"; } + +.fa-ticket-alt::before { + content: "\f3ff"; } + +.fa-building::before { + content: "\f1ad"; } + +.fa-angles-left::before { + content: "\f100"; } + +.fa-angle-double-left::before { + content: "\f100"; } + +.fa-qrcode::before { + content: "\f029"; } + +.fa-clock-rotate-left::before { + content: "\f1da"; } + +.fa-history::before { + content: "\f1da"; } + +.fa-face-grin-beam-sweat::before { + content: "\f583"; } + +.fa-grin-beam-sweat::before { + content: "\f583"; } + +.fa-file-export::before { + content: "\f56e"; } + +.fa-arrow-right-from-file::before { + content: "\f56e"; } + +.fa-shield::before { + content: "\f132"; } + +.fa-shield-blank::before { + content: "\f132"; } + +.fa-arrow-up-short-wide::before { + content: "\f885"; } + +.fa-sort-amount-up-alt::before { + content: "\f885"; } + +.fa-house-medical::before { + content: "\e3b2"; } + +.fa-golf-ball-tee::before { + content: "\f450"; } + +.fa-golf-ball::before { + content: "\f450"; } + +.fa-circle-chevron-left::before { + content: "\f137"; } + +.fa-chevron-circle-left::before { + content: "\f137"; } + +.fa-house-chimney-window::before { + content: "\e00d"; } + +.fa-pen-nib::before { + content: "\f5ad"; } + +.fa-tent-arrow-turn-left::before { + content: "\e580"; } + +.fa-tents::before { + content: "\e582"; } + +.fa-wand-magic::before { + content: "\f0d0"; } + +.fa-magic::before { + content: "\f0d0"; } + +.fa-dog::before { + content: "\f6d3"; } + +.fa-carrot::before { + content: "\f787"; } + +.fa-moon::before { + content: "\f186"; } + +.fa-wine-glass-empty::before { + content: "\f5ce"; } + +.fa-wine-glass-alt::before { + content: "\f5ce"; } + +.fa-cheese::before { + content: "\f7ef"; } + +.fa-yin-yang::before { + content: "\f6ad"; } + +.fa-music::before { + content: "\f001"; } + +.fa-code-commit::before { + content: "\f386"; } + +.fa-temperature-low::before { + content: "\f76b"; } + +.fa-person-biking::before { + content: "\f84a"; } + +.fa-biking::before { + content: "\f84a"; } + +.fa-broom::before { + content: "\f51a"; } + +.fa-shield-heart::before { + content: "\e574"; } + +.fa-gopuram::before { + content: "\f664"; } + +.fa-earth-oceania::before { + content: "\e47b"; } + +.fa-globe-oceania::before { + content: "\e47b"; } + +.fa-square-xmark::before { + content: "\f2d3"; } + +.fa-times-square::before { + content: "\f2d3"; } + +.fa-xmark-square::before { + content: "\f2d3"; } + +.fa-hashtag::before { + content: "\23"; } + +.fa-up-right-and-down-left-from-center::before { + content: "\f424"; } + +.fa-expand-alt::before { + content: "\f424"; } + +.fa-oil-can::before { + content: "\f613"; } + +.fa-t::before { + content: "\54"; } + +.fa-hippo::before { + content: "\f6ed"; } + +.fa-chart-column::before { + content: "\e0e3"; } + +.fa-infinity::before { + content: "\f534"; } + +.fa-vial-circle-check::before { + content: "\e596"; } + +.fa-person-arrow-down-to-line::before { + content: "\e538"; } + +.fa-voicemail::before { + content: "\f897"; } + +.fa-fan::before { + content: "\f863"; } + +.fa-person-walking-luggage::before { + content: "\e554"; } + +.fa-up-down::before { + content: "\f338"; } + +.fa-arrows-alt-v::before { + content: "\f338"; } + +.fa-cloud-moon-rain::before { + content: "\f73c"; } + +.fa-calendar::before { + content: "\f133"; } + +.fa-trailer::before { + content: "\e041"; } + +.fa-bahai::before { + content: "\f666"; } + +.fa-haykal::before { + content: "\f666"; } + +.fa-sd-card::before { + content: "\f7c2"; } + +.fa-dragon::before { + content: "\f6d5"; } + +.fa-shoe-prints::before { + content: "\f54b"; } + +.fa-circle-plus::before { + content: "\f055"; } + +.fa-plus-circle::before { + content: "\f055"; } + +.fa-face-grin-tongue-wink::before { + content: "\f58b"; } + +.fa-grin-tongue-wink::before { + content: "\f58b"; } + +.fa-hand-holding::before { + content: "\f4bd"; } + +.fa-plug-circle-exclamation::before { + content: "\e55d"; } + +.fa-link-slash::before { + content: "\f127"; } + +.fa-chain-broken::before { + content: "\f127"; } + +.fa-chain-slash::before { + content: "\f127"; } + +.fa-unlink::before { + content: "\f127"; } + +.fa-clone::before { + content: "\f24d"; } + +.fa-person-walking-arrow-loop-left::before { + content: "\e551"; } + +.fa-arrow-up-z-a::before { + content: "\f882"; } + +.fa-sort-alpha-up-alt::before { + content: "\f882"; } + +.fa-fire-flame-curved::before { + content: "\f7e4"; } + +.fa-fire-alt::before { + content: "\f7e4"; } + +.fa-tornado::before { + content: "\f76f"; } + +.fa-file-circle-plus::before { + content: "\e494"; } + +.fa-book-quran::before { + content: "\f687"; } + +.fa-quran::before { + content: "\f687"; } + +.fa-anchor::before { + content: "\f13d"; } + +.fa-border-all::before { + content: "\f84c"; } + +.fa-face-angry::before { + content: "\f556"; } + +.fa-angry::before { + content: "\f556"; } + +.fa-cookie-bite::before { + content: "\f564"; } + +.fa-arrow-trend-down::before { + content: "\e097"; } + +.fa-rss::before { + content: "\f09e"; } + +.fa-feed::before { + content: "\f09e"; } + +.fa-draw-polygon::before { + content: "\f5ee"; } + +.fa-scale-balanced::before { + content: "\f24e"; } + +.fa-balance-scale::before { + content: "\f24e"; } + +.fa-gauge-simple-high::before { + content: "\f62a"; } + +.fa-tachometer::before { + content: "\f62a"; } + +.fa-tachometer-fast::before { + content: "\f62a"; } + +.fa-shower::before { + content: "\f2cc"; } + +.fa-desktop::before { + content: "\f390"; } + +.fa-desktop-alt::before { + content: "\f390"; } + +.fa-m::before { + content: "\4d"; } + +.fa-table-list::before { + content: "\f00b"; } + +.fa-th-list::before { + content: "\f00b"; } + +.fa-comment-sms::before { + content: "\f7cd"; } + +.fa-sms::before { + content: "\f7cd"; } + +.fa-book::before { + content: "\f02d"; } + +.fa-user-plus::before { + content: "\f234"; } + +.fa-check::before { + content: "\f00c"; } + +.fa-battery-three-quarters::before { + content: "\f241"; } + +.fa-battery-4::before { + content: "\f241"; } + +.fa-house-circle-check::before { + content: "\e509"; } + +.fa-angle-left::before { + content: "\f104"; } + +.fa-diagram-successor::before { + content: "\e47a"; } + +.fa-truck-arrow-right::before { + content: "\e58b"; } + +.fa-arrows-split-up-and-left::before { + content: "\e4bc"; } + +.fa-hand-fist::before { + content: "\f6de"; } + +.fa-fist-raised::before { + content: "\f6de"; } + +.fa-cloud-moon::before { + content: "\f6c3"; } + +.fa-briefcase::before { + content: "\f0b1"; } + +.fa-person-falling::before { + content: "\e546"; } + +.fa-image-portrait::before { + content: "\f3e0"; } + +.fa-portrait::before { + content: "\f3e0"; } + +.fa-user-tag::before { + content: "\f507"; } + +.fa-rug::before { + content: "\e569"; } + +.fa-earth-europe::before { + content: "\f7a2"; } + +.fa-globe-europe::before { + content: "\f7a2"; } + +.fa-cart-flatbed-suitcase::before { + content: "\f59d"; } + +.fa-luggage-cart::before { + content: "\f59d"; } + +.fa-rectangle-xmark::before { + content: "\f410"; } + +.fa-rectangle-times::before { + content: "\f410"; } + +.fa-times-rectangle::before { + content: "\f410"; } + +.fa-window-close::before { + content: "\f410"; } + +.fa-baht-sign::before { + content: "\e0ac"; } + +.fa-book-open::before { + content: "\f518"; } + +.fa-book-journal-whills::before { + content: "\f66a"; } + +.fa-journal-whills::before { + content: "\f66a"; } + +.fa-handcuffs::before { + content: "\e4f8"; } + +.fa-triangle-exclamation::before { + content: "\f071"; } + +.fa-exclamation-triangle::before { + content: "\f071"; } + +.fa-warning::before { + content: "\f071"; } + +.fa-database::before { + content: "\f1c0"; } + +.fa-share::before { + content: "\f064"; } + +.fa-arrow-turn-right::before { + content: "\f064"; } + +.fa-mail-forward::before { + content: "\f064"; } + +.fa-bottle-droplet::before { + content: "\e4c4"; } + +.fa-mask-face::before { + content: "\e1d7"; } + +.fa-hill-rockslide::before { + content: "\e508"; } + +.fa-right-left::before { + content: "\f362"; } + +.fa-exchange-alt::before { + content: "\f362"; } + +.fa-paper-plane::before { + content: "\f1d8"; } + +.fa-road-circle-exclamation::before { + content: "\e565"; } + +.fa-dungeon::before { + content: "\f6d9"; } + +.fa-align-right::before { + content: "\f038"; } + +.fa-money-bill-1-wave::before { + content: "\f53b"; } + +.fa-money-bill-wave-alt::before { + content: "\f53b"; } + +.fa-life-ring::before { + content: "\f1cd"; } + +.fa-hands::before { + content: "\f2a7"; } + +.fa-sign-language::before { + content: "\f2a7"; } + +.fa-signing::before { + content: "\f2a7"; } + +.fa-calendar-day::before { + content: "\f783"; } + +.fa-water-ladder::before { + content: "\f5c5"; } + +.fa-ladder-water::before { + content: "\f5c5"; } + +.fa-swimming-pool::before { + content: "\f5c5"; } + +.fa-arrows-up-down::before { + content: "\f07d"; } + +.fa-arrows-v::before { + content: "\f07d"; } + +.fa-face-grimace::before { + content: "\f57f"; } + +.fa-grimace::before { + content: "\f57f"; } + +.fa-wheelchair-move::before { + content: "\e2ce"; } + +.fa-wheelchair-alt::before { + content: "\e2ce"; } + +.fa-turn-down::before { + content: "\f3be"; } + +.fa-level-down-alt::before { + content: "\f3be"; } + +.fa-person-walking-arrow-right::before { + content: "\e552"; } + +.fa-square-envelope::before { + content: "\f199"; } + +.fa-envelope-square::before { + content: "\f199"; } + +.fa-dice::before { + content: "\f522"; } + +.fa-bowling-ball::before { + content: "\f436"; } + +.fa-brain::before { + content: "\f5dc"; } + +.fa-bandage::before { + content: "\f462"; } + +.fa-band-aid::before { + content: "\f462"; } + +.fa-calendar-minus::before { + content: "\f272"; } + +.fa-circle-xmark::before { + content: "\f057"; } + +.fa-times-circle::before { + content: "\f057"; } + +.fa-xmark-circle::before { + content: "\f057"; } + +.fa-gifts::before { + content: "\f79c"; } + +.fa-hotel::before { + content: "\f594"; } + +.fa-earth-asia::before { + content: "\f57e"; } + +.fa-globe-asia::before { + content: "\f57e"; } + +.fa-id-card-clip::before { + content: "\f47f"; } + +.fa-id-card-alt::before { + content: "\f47f"; } + +.fa-magnifying-glass-plus::before { + content: "\f00e"; } + +.fa-search-plus::before { + content: "\f00e"; } + +.fa-thumbs-up::before { + content: "\f164"; } + +.fa-user-clock::before { + content: "\f4fd"; } + +.fa-hand-dots::before { + content: "\f461"; } + +.fa-allergies::before { + content: "\f461"; } + +.fa-file-invoice::before { + content: "\f570"; } + +.fa-window-minimize::before { + content: "\f2d1"; } + +.fa-mug-saucer::before { + content: "\f0f4"; } + +.fa-coffee::before { + content: "\f0f4"; } + +.fa-brush::before { + content: "\f55d"; } + +.fa-mask::before { + content: "\f6fa"; } + +.fa-magnifying-glass-minus::before { + content: "\f010"; } + +.fa-search-minus::before { + content: "\f010"; } + +.fa-ruler-vertical::before { + content: "\f548"; } + +.fa-user-large::before { + content: "\f406"; } + +.fa-user-alt::before { + content: "\f406"; } + +.fa-train-tram::before { + content: "\e5b4"; } + +.fa-user-nurse::before { + content: "\f82f"; } + +.fa-syringe::before { + content: "\f48e"; } + +.fa-cloud-sun::before { + content: "\f6c4"; } + +.fa-stopwatch-20::before { + content: "\e06f"; } + +.fa-square-full::before { + content: "\f45c"; } + +.fa-magnet::before { + content: "\f076"; } + +.fa-jar::before { + content: "\e516"; } + +.fa-note-sticky::before { + content: "\f249"; } + +.fa-sticky-note::before { + content: "\f249"; } + +.fa-bug-slash::before { + content: "\e490"; } + +.fa-arrow-up-from-water-pump::before { + content: "\e4b6"; } + +.fa-bone::before { + content: "\f5d7"; } + +.fa-user-injured::before { + content: "\f728"; } + +.fa-face-sad-tear::before { + content: "\f5b4"; } + +.fa-sad-tear::before { + content: "\f5b4"; } + +.fa-plane::before { + content: "\f072"; } + +.fa-tent-arrows-down::before { + content: "\e581"; } + +.fa-exclamation::before { + content: "\21"; } + +.fa-arrows-spin::before { + content: "\e4bb"; } + +.fa-print::before { + content: "\f02f"; } + +.fa-turkish-lira-sign::before { + content: "\e2bb"; } + +.fa-try::before { + content: "\e2bb"; } + +.fa-turkish-lira::before { + content: "\e2bb"; } + +.fa-dollar-sign::before { + content: "\24"; } + +.fa-dollar::before { + content: "\24"; } + +.fa-usd::before { + content: "\24"; } + +.fa-x::before { + content: "\58"; } + +.fa-magnifying-glass-dollar::before { + content: "\f688"; } + +.fa-search-dollar::before { + content: "\f688"; } + +.fa-users-gear::before { + content: "\f509"; } + +.fa-users-cog::before { + content: "\f509"; } + +.fa-person-military-pointing::before { + content: "\e54a"; } + +.fa-building-columns::before { + content: "\f19c"; } + +.fa-bank::before { + content: "\f19c"; } + +.fa-institution::before { + content: "\f19c"; } + +.fa-museum::before { + content: "\f19c"; } + +.fa-university::before { + content: "\f19c"; } + +.fa-umbrella::before { + content: "\f0e9"; } + +.fa-trowel::before { + content: "\e589"; } + +.fa-d::before { + content: "\44"; } + +.fa-stapler::before { + content: "\e5af"; } + +.fa-masks-theater::before { + content: "\f630"; } + +.fa-theater-masks::before { + content: "\f630"; } + +.fa-kip-sign::before { + content: "\e1c4"; } + +.fa-hand-point-left::before { + content: "\f0a5"; } + +.fa-handshake-simple::before { + content: "\f4c6"; } + +.fa-handshake-alt::before { + content: "\f4c6"; } + +.fa-jet-fighter::before { + content: "\f0fb"; } + +.fa-fighter-jet::before { + content: "\f0fb"; } + +.fa-square-share-nodes::before { + content: "\f1e1"; } + +.fa-share-alt-square::before { + content: "\f1e1"; } + +.fa-barcode::before { + content: "\f02a"; } + +.fa-plus-minus::before { + content: "\e43c"; } + +.fa-video::before { + content: "\f03d"; } + +.fa-video-camera::before { + content: "\f03d"; } + +.fa-graduation-cap::before { + content: "\f19d"; } + +.fa-mortar-board::before { + content: "\f19d"; } + +.fa-hand-holding-medical::before { + content: "\e05c"; } + +.fa-person-circle-check::before { + content: "\e53e"; } + +.fa-turn-up::before { + content: "\f3bf"; } + +.fa-level-up-alt::before { + content: "\f3bf"; } + +.sr-only, +.fa-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; } + +.sr-only-focusable:not(:focus), +.fa-sr-only-focusable:not(:focus) { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; } +:root, :host { + --fa-style-family-brands: 'Font Awesome 6 Brands'; + --fa-font-brands: normal 400 1em/1 'Font Awesome 6 Brands'; } + +@font-face { + font-family: 'Font Awesome 6 Brands'; + font-style: normal; + font-weight: 400; + font-display: block; + src: url("../webfonts/FontAwesome6Brands-Regular-400.woff2") format("woff2"), url("../webfonts/FontAwesome6Brands-Regular-400.ttf") format("truetype"); } + +.fab, +.fa-brands { + font-weight: 400; } + +.fa-monero:before { + content: "\f3d0"; } + +.fa-hooli:before { + content: "\f427"; } + +.fa-yelp:before { + content: "\f1e9"; } + +.fa-cc-visa:before { + content: "\f1f0"; } + +.fa-lastfm:before { + content: "\f202"; } + +.fa-shopware:before { + content: "\f5b5"; } + +.fa-creative-commons-nc:before { + content: "\f4e8"; } + +.fa-aws:before { + content: "\f375"; } + +.fa-redhat:before { + content: "\f7bc"; } + +.fa-yoast:before { + content: "\f2b1"; } + +.fa-cloudflare:before { + content: "\e07d"; } + +.fa-ups:before { + content: "\f7e0"; } + +.fa-wpexplorer:before { + content: "\f2de"; } + +.fa-dyalog:before { + content: "\f399"; } + +.fa-bity:before { + content: "\f37a"; } + +.fa-stackpath:before { + content: "\f842"; } + +.fa-buysellads:before { + content: "\f20d"; } + +.fa-first-order:before { + content: "\f2b0"; } + +.fa-modx:before { + content: "\f285"; } + +.fa-guilded:before { + content: "\e07e"; } + +.fa-vnv:before { + content: "\f40b"; } + +.fa-square-js:before { + content: "\f3b9"; } + +.fa-js-square:before { + content: "\f3b9"; } + +.fa-microsoft:before { + content: "\f3ca"; } + +.fa-qq:before { + content: "\f1d6"; } + +.fa-orcid:before { + content: "\f8d2"; } + +.fa-java:before { + content: "\f4e4"; } + +.fa-invision:before { + content: "\f7b0"; } + +.fa-creative-commons-pd-alt:before { + content: "\f4ed"; } + +.fa-centercode:before { + content: "\f380"; } + +.fa-glide-g:before { + content: "\f2a6"; } + +.fa-drupal:before { + content: "\f1a9"; } + +.fa-hire-a-helper:before { + content: "\f3b0"; } + +.fa-creative-commons-by:before { + content: "\f4e7"; } + +.fa-unity:before { + content: "\e049"; } + +.fa-whmcs:before { + content: "\f40d"; } + +.fa-rocketchat:before { + content: "\f3e8"; } + +.fa-vk:before { + content: "\f189"; } + +.fa-untappd:before { + content: "\f405"; } + +.fa-mailchimp:before { + content: "\f59e"; } + +.fa-css3-alt:before { + content: "\f38b"; } + +.fa-square-reddit:before { + content: "\f1a2"; } + +.fa-reddit-square:before { + content: "\f1a2"; } + +.fa-vimeo-v:before { + content: "\f27d"; } + +.fa-contao:before { + content: "\f26d"; } + +.fa-square-font-awesome:before { + content: "\e5ad"; } + +.fa-deskpro:before { + content: "\f38f"; } + +.fa-sistrix:before { + content: "\f3ee"; } + +.fa-square-instagram:before { + content: "\e055"; } + +.fa-instagram-square:before { + content: "\e055"; } + +.fa-battle-net:before { + content: "\f835"; } + +.fa-the-red-yeti:before { + content: "\f69d"; } + +.fa-square-hacker-news:before { + content: "\f3af"; } + +.fa-hacker-news-square:before { + content: "\f3af"; } + +.fa-edge:before { + content: "\f282"; } + +.fa-threads:before { + content: "\e618"; } + +.fa-napster:before { + content: "\f3d2"; } + +.fa-square-snapchat:before { + content: "\f2ad"; } + +.fa-snapchat-square:before { + content: "\f2ad"; } + +.fa-google-plus-g:before { + content: "\f0d5"; } + +.fa-artstation:before { + content: "\f77a"; } + +.fa-markdown:before { + content: "\f60f"; } + +.fa-sourcetree:before { + content: "\f7d3"; } + +.fa-google-plus:before { + content: "\f2b3"; } + +.fa-diaspora:before { + content: "\f791"; } + +.fa-foursquare:before { + content: "\f180"; } + +.fa-stack-overflow:before { + content: "\f16c"; } + +.fa-github-alt:before { + content: "\f113"; } + +.fa-phoenix-squadron:before { + content: "\f511"; } + +.fa-pagelines:before { + content: "\f18c"; } + +.fa-algolia:before { + content: "\f36c"; } + +.fa-red-river:before { + content: "\f3e3"; } + +.fa-creative-commons-sa:before { + content: "\f4ef"; } + +.fa-safari:before { + content: "\f267"; } + +.fa-google:before { + content: "\f1a0"; } + +.fa-square-font-awesome-stroke:before { + content: "\f35c"; } + +.fa-font-awesome-alt:before { + content: "\f35c"; } + +.fa-atlassian:before { + content: "\f77b"; } + +.fa-linkedin-in:before { + content: "\f0e1"; } + +.fa-digital-ocean:before { + content: "\f391"; } + +.fa-nimblr:before { + content: "\f5a8"; } + +.fa-chromecast:before { + content: "\f838"; } + +.fa-evernote:before { + content: "\f839"; } + +.fa-hacker-news:before { + content: "\f1d4"; } + +.fa-creative-commons-sampling:before { + content: "\f4f0"; } + +.fa-adversal:before { + content: "\f36a"; } + +.fa-creative-commons:before { + content: "\f25e"; } + +.fa-watchman-monitoring:before { + content: "\e087"; } + +.fa-fonticons:before { + content: "\f280"; } + +.fa-weixin:before { + content: "\f1d7"; } + +.fa-shirtsinbulk:before { + content: "\f214"; } + +.fa-codepen:before { + content: "\f1cb"; } + +.fa-git-alt:before { + content: "\f841"; } + +.fa-lyft:before { + content: "\f3c3"; } + +.fa-rev:before { + content: "\f5b2"; } + +.fa-windows:before { + content: "\f17a"; } + +.fa-wizards-of-the-coast:before { + content: "\f730"; } + +.fa-square-viadeo:before { + content: "\f2aa"; } + +.fa-viadeo-square:before { + content: "\f2aa"; } + +.fa-meetup:before { + content: "\f2e0"; } + +.fa-centos:before { + content: "\f789"; } + +.fa-adn:before { + content: "\f170"; } + +.fa-cloudsmith:before { + content: "\f384"; } + +.fa-pied-piper-alt:before { + content: "\f1a8"; } + +.fa-square-dribbble:before { + content: "\f397"; } + +.fa-dribbble-square:before { + content: "\f397"; } + +.fa-codiepie:before { + content: "\f284"; } + +.fa-node:before { + content: "\f419"; } + +.fa-mix:before { + content: "\f3cb"; } + +.fa-steam:before { + content: "\f1b6"; } + +.fa-cc-apple-pay:before { + content: "\f416"; } + +.fa-scribd:before { + content: "\f28a"; } + +.fa-debian:before { + content: "\e60b"; } + +.fa-openid:before { + content: "\f19b"; } + +.fa-instalod:before { + content: "\e081"; } + +.fa-expeditedssl:before { + content: "\f23e"; } + +.fa-sellcast:before { + content: "\f2da"; } + +.fa-square-twitter:before { + content: "\f081"; } + +.fa-twitter-square:before { + content: "\f081"; } + +.fa-r-project:before { + content: "\f4f7"; } + +.fa-delicious:before { + content: "\f1a5"; } + +.fa-freebsd:before { + content: "\f3a4"; } + +.fa-vuejs:before { + content: "\f41f"; } + +.fa-accusoft:before { + content: "\f369"; } + +.fa-ioxhost:before { + content: "\f208"; } + +.fa-fonticons-fi:before { + content: "\f3a2"; } + +.fa-app-store:before { + content: "\f36f"; } + +.fa-cc-mastercard:before { + content: "\f1f1"; } + +.fa-itunes-note:before { + content: "\f3b5"; } + +.fa-golang:before { + content: "\e40f"; } + +.fa-kickstarter:before { + content: "\f3bb"; } + +.fa-grav:before { + content: "\f2d6"; } + +.fa-weibo:before { + content: "\f18a"; } + +.fa-uncharted:before { + content: "\e084"; } + +.fa-firstdraft:before { + content: "\f3a1"; } + +.fa-square-youtube:before { + content: "\f431"; } + +.fa-youtube-square:before { + content: "\f431"; } + +.fa-wikipedia-w:before { + content: "\f266"; } + +.fa-wpressr:before { + content: "\f3e4"; } + +.fa-rendact:before { + content: "\f3e4"; } + +.fa-angellist:before { + content: "\f209"; } + +.fa-galactic-republic:before { + content: "\f50c"; } + +.fa-nfc-directional:before { + content: "\e530"; } + +.fa-skype:before { + content: "\f17e"; } + +.fa-joget:before { + content: "\f3b7"; } + +.fa-fedora:before { + content: "\f798"; } + +.fa-stripe-s:before { + content: "\f42a"; } + +.fa-meta:before { + content: "\e49b"; } + +.fa-laravel:before { + content: "\f3bd"; } + +.fa-hotjar:before { + content: "\f3b1"; } + +.fa-bluetooth-b:before { + content: "\f294"; } + +.fa-sticker-mule:before { + content: "\f3f7"; } + +.fa-creative-commons-zero:before { + content: "\f4f3"; } + +.fa-hips:before { + content: "\f452"; } + +.fa-behance:before { + content: "\f1b4"; } + +.fa-reddit:before { + content: "\f1a1"; } + +.fa-discord:before { + content: "\f392"; } + +.fa-chrome:before { + content: "\f268"; } + +.fa-app-store-ios:before { + content: "\f370"; } + +.fa-cc-discover:before { + content: "\f1f2"; } + +.fa-wpbeginner:before { + content: "\f297"; } + +.fa-confluence:before { + content: "\f78d"; } + +.fa-mdb:before { + content: "\f8ca"; } + +.fa-dochub:before { + content: "\f394"; } + +.fa-accessible-icon:before { + content: "\f368"; } + +.fa-ebay:before { + content: "\f4f4"; } + +.fa-amazon:before { + content: "\f270"; } + +.fa-unsplash:before { + content: "\e07c"; } + +.fa-yarn:before { + content: "\f7e3"; } + +.fa-square-steam:before { + content: "\f1b7"; } + +.fa-steam-square:before { + content: "\f1b7"; } + +.fa-500px:before { + content: "\f26e"; } + +.fa-square-vimeo:before { + content: "\f194"; } + +.fa-vimeo-square:before { + content: "\f194"; } + +.fa-asymmetrik:before { + content: "\f372"; } + +.fa-font-awesome:before { + content: "\f2b4"; } + +.fa-font-awesome-flag:before { + content: "\f2b4"; } + +.fa-font-awesome-logo-full:before { + content: "\f2b4"; } + +.fa-gratipay:before { + content: "\f184"; } + +.fa-apple:before { + content: "\f179"; } + +.fa-hive:before { + content: "\e07f"; } + +.fa-gitkraken:before { + content: "\f3a6"; } + +.fa-keybase:before { + content: "\f4f5"; } + +.fa-apple-pay:before { + content: "\f415"; } + +.fa-padlet:before { + content: "\e4a0"; } + +.fa-amazon-pay:before { + content: "\f42c"; } + +.fa-square-github:before { + content: "\f092"; } + +.fa-github-square:before { + content: "\f092"; } + +.fa-stumbleupon:before { + content: "\f1a4"; } + +.fa-fedex:before { + content: "\f797"; } + +.fa-phoenix-framework:before { + content: "\f3dc"; } + +.fa-shopify:before { + content: "\e057"; } + +.fa-neos:before { + content: "\f612"; } + +.fa-square-threads:before { + content: "\e619"; } + +.fa-hackerrank:before { + content: "\f5f7"; } + +.fa-researchgate:before { + content: "\f4f8"; } + +.fa-swift:before { + content: "\f8e1"; } + +.fa-angular:before { + content: "\f420"; } + +.fa-speakap:before { + content: "\f3f3"; } + +.fa-angrycreative:before { + content: "\f36e"; } + +.fa-y-combinator:before { + content: "\f23b"; } + +.fa-empire:before { + content: "\f1d1"; } + +.fa-envira:before { + content: "\f299"; } + +.fa-square-gitlab:before { + content: "\e5ae"; } + +.fa-gitlab-square:before { + content: "\e5ae"; } + +.fa-studiovinari:before { + content: "\f3f8"; } + +.fa-pied-piper:before { + content: "\f2ae"; } + +.fa-wordpress:before { + content: "\f19a"; } + +.fa-product-hunt:before { + content: "\f288"; } + +.fa-firefox:before { + content: "\f269"; } + +.fa-linode:before { + content: "\f2b8"; } + +.fa-goodreads:before { + content: "\f3a8"; } + +.fa-square-odnoklassniki:before { + content: "\f264"; } + +.fa-odnoklassniki-square:before { + content: "\f264"; } + +.fa-jsfiddle:before { + content: "\f1cc"; } + +.fa-sith:before { + content: "\f512"; } + +.fa-themeisle:before { + content: "\f2b2"; } + +.fa-page4:before { + content: "\f3d7"; } + +.fa-hashnode:before { + content: "\e499"; } + +.fa-react:before { + content: "\f41b"; } + +.fa-cc-paypal:before { + content: "\f1f4"; } + +.fa-squarespace:before { + content: "\f5be"; } + +.fa-cc-stripe:before { + content: "\f1f5"; } + +.fa-creative-commons-share:before { + content: "\f4f2"; } + +.fa-bitcoin:before { + content: "\f379"; } + +.fa-keycdn:before { + content: "\f3ba"; } + +.fa-opera:before { + content: "\f26a"; } + +.fa-itch-io:before { + content: "\f83a"; } + +.fa-umbraco:before { + content: "\f8e8"; } + +.fa-galactic-senate:before { + content: "\f50d"; } + +.fa-ubuntu:before { + content: "\f7df"; } + +.fa-draft2digital:before { + content: "\f396"; } + +.fa-stripe:before { + content: "\f429"; } + +.fa-houzz:before { + content: "\f27c"; } + +.fa-gg:before { + content: "\f260"; } + +.fa-dhl:before { + content: "\f790"; } + +.fa-square-pinterest:before { + content: "\f0d3"; } + +.fa-pinterest-square:before { + content: "\f0d3"; } + +.fa-xing:before { + content: "\f168"; } + +.fa-blackberry:before { + content: "\f37b"; } + +.fa-creative-commons-pd:before { + content: "\f4ec"; } + +.fa-playstation:before { + content: "\f3df"; } + +.fa-quinscape:before { + content: "\f459"; } + +.fa-less:before { + content: "\f41d"; } + +.fa-blogger-b:before { + content: "\f37d"; } + +.fa-opencart:before { + content: "\f23d"; } + +.fa-vine:before { + content: "\f1ca"; } + +.fa-paypal:before { + content: "\f1ed"; } + +.fa-gitlab:before { + content: "\f296"; } + +.fa-typo3:before { + content: "\f42b"; } + +.fa-reddit-alien:before { + content: "\f281"; } + +.fa-yahoo:before { + content: "\f19e"; } + +.fa-dailymotion:before { + content: "\e052"; } + +.fa-affiliatetheme:before { + content: "\f36b"; } + +.fa-pied-piper-pp:before { + content: "\f1a7"; } + +.fa-bootstrap:before { + content: "\f836"; } + +.fa-odnoklassniki:before { + content: "\f263"; } + +.fa-nfc-symbol:before { + content: "\e531"; } + +.fa-ethereum:before { + content: "\f42e"; } + +.fa-speaker-deck:before { + content: "\f83c"; } + +.fa-creative-commons-nc-eu:before { + content: "\f4e9"; } + +.fa-patreon:before { + content: "\f3d9"; } + +.fa-avianex:before { + content: "\f374"; } + +.fa-ello:before { + content: "\f5f1"; } + +.fa-gofore:before { + content: "\f3a7"; } + +.fa-bimobject:before { + content: "\f378"; } + +.fa-facebook-f:before { + content: "\f39e"; } + +.fa-square-google-plus:before { + content: "\f0d4"; } + +.fa-google-plus-square:before { + content: "\f0d4"; } + +.fa-mandalorian:before { + content: "\f50f"; } + +.fa-first-order-alt:before { + content: "\f50a"; } + +.fa-osi:before { + content: "\f41a"; } + +.fa-google-wallet:before { + content: "\f1ee"; } + +.fa-d-and-d-beyond:before { + content: "\f6ca"; } + +.fa-periscope:before { + content: "\f3da"; } + +.fa-fulcrum:before { + content: "\f50b"; } + +.fa-cloudscale:before { + content: "\f383"; } + +.fa-forumbee:before { + content: "\f211"; } + +.fa-mizuni:before { + content: "\f3cc"; } + +.fa-schlix:before { + content: "\f3ea"; } + +.fa-square-xing:before { + content: "\f169"; } + +.fa-xing-square:before { + content: "\f169"; } + +.fa-bandcamp:before { + content: "\f2d5"; } + +.fa-wpforms:before { + content: "\f298"; } + +.fa-cloudversify:before { + content: "\f385"; } + +.fa-usps:before { + content: "\f7e1"; } + +.fa-megaport:before { + content: "\f5a3"; } + +.fa-magento:before { + content: "\f3c4"; } + +.fa-spotify:before { + content: "\f1bc"; } + +.fa-optin-monster:before { + content: "\f23c"; } + +.fa-fly:before { + content: "\f417"; } + +.fa-aviato:before { + content: "\f421"; } + +.fa-itunes:before { + content: "\f3b4"; } + +.fa-cuttlefish:before { + content: "\f38c"; } + +.fa-blogger:before { + content: "\f37c"; } + +.fa-flickr:before { + content: "\f16e"; } + +.fa-viber:before { + content: "\f409"; } + +.fa-soundcloud:before { + content: "\f1be"; } + +.fa-digg:before { + content: "\f1a6"; } + +.fa-tencent-weibo:before { + content: "\f1d5"; } + +.fa-symfony:before { + content: "\f83d"; } + +.fa-maxcdn:before { + content: "\f136"; } + +.fa-etsy:before { + content: "\f2d7"; } + +.fa-facebook-messenger:before { + content: "\f39f"; } + +.fa-audible:before { + content: "\f373"; } + +.fa-think-peaks:before { + content: "\f731"; } + +.fa-bilibili:before { + content: "\e3d9"; } + +.fa-erlang:before { + content: "\f39d"; } + +.fa-x-twitter:before { + content: "\e61b"; } + +.fa-cotton-bureau:before { + content: "\f89e"; } + +.fa-dashcube:before { + content: "\f210"; } + +.fa-42-group:before { + content: "\e080"; } + +.fa-innosoft:before { + content: "\e080"; } + +.fa-stack-exchange:before { + content: "\f18d"; } + +.fa-elementor:before { + content: "\f430"; } + +.fa-square-pied-piper:before { + content: "\e01e"; } + +.fa-pied-piper-square:before { + content: "\e01e"; } + +.fa-creative-commons-nd:before { + content: "\f4eb"; } + +.fa-palfed:before { + content: "\f3d8"; } + +.fa-superpowers:before { + content: "\f2dd"; } + +.fa-resolving:before { + content: "\f3e7"; } + +.fa-xbox:before { + content: "\f412"; } + +.fa-searchengin:before { + content: "\f3eb"; } + +.fa-tiktok:before { + content: "\e07b"; } + +.fa-square-facebook:before { + content: "\f082"; } + +.fa-facebook-square:before { + content: "\f082"; } + +.fa-renren:before { + content: "\f18b"; } + +.fa-linux:before { + content: "\f17c"; } + +.fa-glide:before { + content: "\f2a5"; } + +.fa-linkedin:before { + content: "\f08c"; } + +.fa-hubspot:before { + content: "\f3b2"; } + +.fa-deploydog:before { + content: "\f38e"; } + +.fa-twitch:before { + content: "\f1e8"; } + +.fa-ravelry:before { + content: "\f2d9"; } + +.fa-mixer:before { + content: "\e056"; } + +.fa-square-lastfm:before { + content: "\f203"; } + +.fa-lastfm-square:before { + content: "\f203"; } + +.fa-vimeo:before { + content: "\f40a"; } + +.fa-mendeley:before { + content: "\f7b3"; } + +.fa-uniregistry:before { + content: "\f404"; } + +.fa-figma:before { + content: "\f799"; } + +.fa-creative-commons-remix:before { + content: "\f4ee"; } + +.fa-cc-amazon-pay:before { + content: "\f42d"; } + +.fa-dropbox:before { + content: "\f16b"; } + +.fa-instagram:before { + content: "\f16d"; } + +.fa-cmplid:before { + content: "\e360"; } + +.fa-facebook:before { + content: "\f09a"; } + +.fa-gripfire:before { + content: "\f3ac"; } + +.fa-jedi-order:before { + content: "\f50e"; } + +.fa-uikit:before { + content: "\f403"; } + +.fa-fort-awesome-alt:before { + content: "\f3a3"; } + +.fa-phabricator:before { + content: "\f3db"; } + +.fa-ussunnah:before { + content: "\f407"; } + +.fa-earlybirds:before { + content: "\f39a"; } + +.fa-trade-federation:before { + content: "\f513"; } + +.fa-autoprefixer:before { + content: "\f41c"; } + +.fa-whatsapp:before { + content: "\f232"; } + +.fa-slideshare:before { + content: "\f1e7"; } + +.fa-google-play:before { + content: "\f3ab"; } + +.fa-viadeo:before { + content: "\f2a9"; } + +.fa-line:before { + content: "\f3c0"; } + +.fa-google-drive:before { + content: "\f3aa"; } + +.fa-servicestack:before { + content: "\f3ec"; } + +.fa-simplybuilt:before { + content: "\f215"; } + +.fa-bitbucket:before { + content: "\f171"; } + +.fa-imdb:before { + content: "\f2d8"; } + +.fa-deezer:before { + content: "\e077"; } + +.fa-raspberry-pi:before { + content: "\f7bb"; } + +.fa-jira:before { + content: "\f7b1"; } + +.fa-docker:before { + content: "\f395"; } + +.fa-screenpal:before { + content: "\e570"; } + +.fa-bluetooth:before { + content: "\f293"; } + +.fa-gitter:before { + content: "\f426"; } + +.fa-d-and-d:before { + content: "\f38d"; } + +.fa-microblog:before { + content: "\e01a"; } + +.fa-cc-diners-club:before { + content: "\f24c"; } + +.fa-gg-circle:before { + content: "\f261"; } + +.fa-pied-piper-hat:before { + content: "\f4e5"; } + +.fa-kickstarter-k:before { + content: "\f3bc"; } + +.fa-yandex:before { + content: "\f413"; } + +.fa-readme:before { + content: "\f4d5"; } + +.fa-html5:before { + content: "\f13b"; } + +.fa-sellsy:before { + content: "\f213"; } + +.fa-sass:before { + content: "\f41e"; } + +.fa-wirsindhandwerk:before { + content: "\e2d0"; } + +.fa-wsh:before { + content: "\e2d0"; } + +.fa-buromobelexperte:before { + content: "\f37f"; } + +.fa-salesforce:before { + content: "\f83b"; } + +.fa-octopus-deploy:before { + content: "\e082"; } + +.fa-medapps:before { + content: "\f3c6"; } + +.fa-ns8:before { + content: "\f3d5"; } + +.fa-pinterest-p:before { + content: "\f231"; } + +.fa-apper:before { + content: "\f371"; } + +.fa-fort-awesome:before { + content: "\f286"; } + +.fa-waze:before { + content: "\f83f"; } + +.fa-cc-jcb:before { + content: "\f24b"; } + +.fa-snapchat:before { + content: "\f2ab"; } + +.fa-snapchat-ghost:before { + content: "\f2ab"; } + +.fa-fantasy-flight-games:before { + content: "\f6dc"; } + +.fa-rust:before { + content: "\e07a"; } + +.fa-wix:before { + content: "\f5cf"; } + +.fa-square-behance:before { + content: "\f1b5"; } + +.fa-behance-square:before { + content: "\f1b5"; } + +.fa-supple:before { + content: "\f3f9"; } + +.fa-rebel:before { + content: "\f1d0"; } + +.fa-css3:before { + content: "\f13c"; } + +.fa-staylinked:before { + content: "\f3f5"; } + +.fa-kaggle:before { + content: "\f5fa"; } + +.fa-space-awesome:before { + content: "\e5ac"; } + +.fa-deviantart:before { + content: "\f1bd"; } + +.fa-cpanel:before { + content: "\f388"; } + +.fa-goodreads-g:before { + content: "\f3a9"; } + +.fa-square-git:before { + content: "\f1d2"; } + +.fa-git-square:before { + content: "\f1d2"; } + +.fa-square-tumblr:before { + content: "\f174"; } + +.fa-tumblr-square:before { + content: "\f174"; } + +.fa-trello:before { + content: "\f181"; } + +.fa-creative-commons-nc-jp:before { + content: "\f4ea"; } + +.fa-get-pocket:before { + content: "\f265"; } + +.fa-perbyte:before { + content: "\e083"; } + +.fa-grunt:before { + content: "\f3ad"; } + +.fa-weebly:before { + content: "\f5cc"; } + +.fa-connectdevelop:before { + content: "\f20e"; } + +.fa-leanpub:before { + content: "\f212"; } + +.fa-black-tie:before { + content: "\f27e"; } + +.fa-themeco:before { + content: "\f5c6"; } + +.fa-python:before { + content: "\f3e2"; } + +.fa-android:before { + content: "\f17b"; } + +.fa-bots:before { + content: "\e340"; } + +.fa-free-code-camp:before { + content: "\f2c5"; } + +.fa-hornbill:before { + content: "\f592"; } + +.fa-js:before { + content: "\f3b8"; } + +.fa-ideal:before { + content: "\e013"; } + +.fa-git:before { + content: "\f1d3"; } + +.fa-dev:before { + content: "\f6cc"; } + +.fa-sketch:before { + content: "\f7c6"; } + +.fa-yandex-international:before { + content: "\f414"; } + +.fa-cc-amex:before { + content: "\f1f3"; } + +.fa-uber:before { + content: "\f402"; } + +.fa-github:before { + content: "\f09b"; } + +.fa-php:before { + content: "\f457"; } + +.fa-alipay:before { + content: "\f642"; } + +.fa-youtube:before { + content: "\f167"; } + +.fa-skyatlas:before { + content: "\f216"; } + +.fa-firefox-browser:before { + content: "\e007"; } + +.fa-replyd:before { + content: "\f3e6"; } + +.fa-suse:before { + content: "\f7d6"; } + +.fa-jenkins:before { + content: "\f3b6"; } + +.fa-twitter:before { + content: "\f099"; } + +.fa-rockrms:before { + content: "\f3e9"; } + +.fa-pinterest:before { + content: "\f0d2"; } + +.fa-buffer:before { + content: "\f837"; } + +.fa-npm:before { + content: "\f3d4"; } + +.fa-yammer:before { + content: "\f840"; } + +.fa-btc:before { + content: "\f15a"; } + +.fa-dribbble:before { + content: "\f17d"; } + +.fa-stumbleupon-circle:before { + content: "\f1a3"; } + +.fa-internet-explorer:before { + content: "\f26b"; } + +.fa-stubber:before { + content: "\e5c7"; } + +.fa-telegram:before { + content: "\f2c6"; } + +.fa-telegram-plane:before { + content: "\f2c6"; } + +.fa-old-republic:before { + content: "\f510"; } + +.fa-odysee:before { + content: "\e5c6"; } + +.fa-square-whatsapp:before { + content: "\f40c"; } + +.fa-whatsapp-square:before { + content: "\f40c"; } + +.fa-node-js:before { + content: "\f3d3"; } + +.fa-edge-legacy:before { + content: "\e078"; } + +.fa-slack:before { + content: "\f198"; } + +.fa-slack-hash:before { + content: "\f198"; } + +.fa-medrt:before { + content: "\f3c8"; } + +.fa-usb:before { + content: "\f287"; } + +.fa-tumblr:before { + content: "\f173"; } + +.fa-vaadin:before { + content: "\f408"; } + +.fa-quora:before { + content: "\f2c4"; } + +.fa-square-x-twitter:before { + content: "\e61a"; } + +.fa-reacteurope:before { + content: "\f75d"; } + +.fa-medium:before { + content: "\f23a"; } + +.fa-medium-m:before { + content: "\f23a"; } + +.fa-amilia:before { + content: "\f36d"; } + +.fa-mixcloud:before { + content: "\f289"; } + +.fa-flipboard:before { + content: "\f44d"; } + +.fa-viacoin:before { + content: "\f237"; } + +.fa-critical-role:before { + content: "\f6c9"; } + +.fa-sitrox:before { + content: "\e44a"; } + +.fa-discourse:before { + content: "\f393"; } + +.fa-joomla:before { + content: "\f1aa"; } + +.fa-mastodon:before { + content: "\f4f6"; } + +.fa-airbnb:before { + content: "\f834"; } + +.fa-wolf-pack-battalion:before { + content: "\f514"; } + +.fa-buy-n-large:before { + content: "\f8a6"; } + +.fa-gulp:before { + content: "\f3ae"; } + +.fa-creative-commons-sampling-plus:before { + content: "\f4f1"; } + +.fa-strava:before { + content: "\f428"; } + +.fa-ember:before { + content: "\f423"; } + +.fa-canadian-maple-leaf:before { + content: "\f785"; } + +.fa-teamspeak:before { + content: "\f4f9"; } + +.fa-pushed:before { + content: "\f3e1"; } + +.fa-wordpress-simple:before { + content: "\f411"; } + +.fa-nutritionix:before { + content: "\f3d6"; } + +.fa-wodu:before { + content: "\e088"; } + +.fa-google-pay:before { + content: "\e079"; } + +.fa-intercom:before { + content: "\f7af"; } + +.fa-zhihu:before { + content: "\f63f"; } + +.fa-korvue:before { + content: "\f42f"; } + +.fa-pix:before { + content: "\e43a"; } + +.fa-steam-symbol:before { + content: "\f3f6"; } +:root, :host { + --fa-style-family-classic: 'Font Awesome 6 Free'; + --fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free'; } + +@font-face { + font-family: 'Font Awesome 6 Free'; + font-style: normal; + font-weight: 400; + font-display: block; + src: url("../webfonts/FontAwesome6Free-Regular-400.woff2") format("woff2"), url("../webfonts/FontAwesome6Free-Regular-400.ttf") format("truetype"); } + +.far, +.fa-regular { + font-weight: 400; } +:root, :host { + --fa-style-family-classic: 'Font Awesome 6 Free'; + --fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free'; } + +@font-face { + font-family: 'Font Awesome 6 Free'; + font-style: normal; + font-weight: 900; + font-display: block; + src: url("../webfonts/FontAwesome6Free-Solid-900.woff2") format("woff2"), url("../webfonts/FontAwesome6Free-Solid-900.ttf") format("truetype"); } + +.fas, +.fa-solid { + font-weight: 900; } +@font-face { + font-family: 'Font Awesome 6 Brands'; + font-display: block; + font-weight: 400; + src: url("../webfonts/FontAwesome6Brands-Regular-400.woff2") format("woff2"), url("../webfonts/FontAwesome6Brands-Regular-400.ttf") format("truetype"); } + +@font-face { + font-family: 'Font Awesome 6 Free'; + font-display: block; + font-weight: 900; + src: url("../webfonts/FontAwesome6Free-Solid-900.woff2") format("woff2"), url("../webfonts/FontAwesome6Free-Solid-900.ttf") format("truetype"); } + +@font-face { + font-family: 'Font Awesome 6 Free'; + font-display: block; + font-weight: 400; + src: url("../webfonts/../webfonts/FontAwesome6Free-Regular-400.woff2") format("woff2"), url("../webfonts/FontAwesome6Free-Regular-400.ttf") format("truetype"); } +@font-face { + font-family: 'Font Awesome 6 Free'; + font-display: block; + src: url("../webfonts/FontAwesome6Free-Solid-900.woff2") format("woff2"), url("../webfonts/FontAwesome6Free-Solid-900.ttf") format("truetype"); } + +@font-face { + font-family: 'Font Awesome 6 Brands'; + font-display: block; + src: url("../webfonts/FontAwesome6Brands-Regular-400.woff2") format("woff2"), url("../webfonts/FontAwesome6Brands-Regular-400.ttf") format("truetype"); } + +@font-face { + font-family: 'Font Awesome 6 Free'; + font-display: block; + src: url("../webfonts/FontAwesome6Free-Regular-400.woff2") format("woff2"), url("../../webfonts/FontAwesome6Free-Regular-400.ttf") format("truetype"); + unicode-range: U+F003,U+F006,U+F014,U+F016-F017,U+F01A-F01B,U+F01D,U+F022,U+F03E,U+F044,U+F046,U+F05C-F05D,U+F06E,U+F070,U+F087-F088,U+F08A,U+F094,U+F096-F097,U+F09D,U+F0A0,U+F0A2,U+F0A4-F0A7,U+F0C5,U+F0C7,U+F0E5-F0E6,U+F0EB,U+F0F6-F0F8,U+F10C,U+F114-F115,U+F118-F11A,U+F11C-F11D,U+F133,U+F147,U+F14E,U+F150-F152,U+F185-F186,U+F18E,U+F190-F192,U+F196,U+F1C1-F1C9,U+F1D9,U+F1DB,U+F1E3,U+F1EA,U+F1F7,U+F1F9,U+F20A,U+F247-F248,U+F24A,U+F24D,U+F255-F25B,U+F25D,U+F271-F274,U+F278,U+F27B,U+F28C,U+F28E,U+F29C,U+F2B5,U+F2B7,U+F2BA,U+F2BC,U+F2BE,U+F2C0-F2C1,U+F2C3,U+F2D0,U+F2D2,U+F2D4,U+F2DC; } + diff --git a/_extensions/quarto-ext/fontawesome/assets/css/latex-fontsize.css b/_extensions/quarto-ext/fontawesome/assets/css/latex-fontsize.css new file mode 100644 index 0000000..45545ec --- /dev/null +++ b/_extensions/quarto-ext/fontawesome/assets/css/latex-fontsize.css @@ -0,0 +1,30 @@ +.fa-tiny { + font-size: 0.5em; +} +.fa-scriptsize { + font-size: 0.7em; +} +.fa-footnotesize { + font-size: 0.8em; +} +.fa-small { + font-size: 0.9em; +} +.fa-normalsize { + font-size: 1em; +} +.fa-large { + font-size: 1.2em; +} +.fa-Large { + font-size: 1.5em; +} +.fa-LARGE { + font-size: 1.75em; +} +.fa-huge { + font-size: 2em; +} +.fa-Huge { + font-size: 2.5em; +} diff --git a/_extensions/quarto-ext/fontawesome/assets/webfonts/FontAwesome6Brands-Regular-400.ttf b/_extensions/quarto-ext/fontawesome/assets/webfonts/FontAwesome6Brands-Regular-400.ttf new file mode 100644 index 0000000..34a1436 Binary files /dev/null and b/_extensions/quarto-ext/fontawesome/assets/webfonts/FontAwesome6Brands-Regular-400.ttf differ diff --git a/_extensions/quarto-ext/fontawesome/assets/webfonts/FontAwesome6Brands-Regular-400.woff2 b/_extensions/quarto-ext/fontawesome/assets/webfonts/FontAwesome6Brands-Regular-400.woff2 new file mode 100644 index 0000000..d1a319f Binary files /dev/null and b/_extensions/quarto-ext/fontawesome/assets/webfonts/FontAwesome6Brands-Regular-400.woff2 differ diff --git a/_extensions/quarto-ext/fontawesome/assets/webfonts/FontAwesome6Free-Regular-400.ttf b/_extensions/quarto-ext/fontawesome/assets/webfonts/FontAwesome6Free-Regular-400.ttf new file mode 100644 index 0000000..d0aeac9 Binary files /dev/null and b/_extensions/quarto-ext/fontawesome/assets/webfonts/FontAwesome6Free-Regular-400.ttf differ diff --git a/_extensions/quarto-ext/fontawesome/assets/webfonts/FontAwesome6Free-Regular-400.woff2 b/_extensions/quarto-ext/fontawesome/assets/webfonts/FontAwesome6Free-Regular-400.woff2 new file mode 100644 index 0000000..f3918d2 Binary files /dev/null and b/_extensions/quarto-ext/fontawesome/assets/webfonts/FontAwesome6Free-Regular-400.woff2 differ diff --git a/_extensions/quarto-ext/fontawesome/assets/webfonts/FontAwesome6Free-Solid-900.ttf b/_extensions/quarto-ext/fontawesome/assets/webfonts/FontAwesome6Free-Solid-900.ttf new file mode 100644 index 0000000..deab676 Binary files /dev/null and b/_extensions/quarto-ext/fontawesome/assets/webfonts/FontAwesome6Free-Solid-900.ttf differ diff --git a/_extensions/quarto-ext/fontawesome/assets/webfonts/FontAwesome6Free-Solid-900.woff2 b/_extensions/quarto-ext/fontawesome/assets/webfonts/FontAwesome6Free-Solid-900.woff2 new file mode 100644 index 0000000..53c1987 Binary files /dev/null and b/_extensions/quarto-ext/fontawesome/assets/webfonts/FontAwesome6Free-Solid-900.woff2 differ diff --git a/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-brands-400.ttf b/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-brands-400.ttf new file mode 100644 index 0000000..430a02e Binary files /dev/null and b/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-brands-400.ttf differ diff --git a/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-brands-400.woff2 b/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-brands-400.woff2 new file mode 100644 index 0000000..4d904aa Binary files /dev/null and b/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-brands-400.woff2 differ diff --git a/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-regular-400.ttf b/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-regular-400.ttf new file mode 100644 index 0000000..23e3feb Binary files /dev/null and b/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-regular-400.ttf differ diff --git a/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-regular-400.woff2 b/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-regular-400.woff2 new file mode 100644 index 0000000..80e3b12 Binary files /dev/null and b/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-regular-400.woff2 differ diff --git a/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-solid-900.ttf b/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-solid-900.ttf new file mode 100644 index 0000000..da90824 Binary files /dev/null and b/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-solid-900.ttf differ diff --git a/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-solid-900.woff2 b/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-solid-900.woff2 new file mode 100644 index 0000000..360ba11 Binary files /dev/null and b/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-solid-900.woff2 differ diff --git a/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-v4compatibility.ttf b/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-v4compatibility.ttf new file mode 100644 index 0000000..e9545ed Binary files /dev/null and b/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-v4compatibility.ttf differ diff --git a/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-v4compatibility.woff2 b/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-v4compatibility.woff2 new file mode 100644 index 0000000..db5b0b9 Binary files /dev/null and b/_extensions/quarto-ext/fontawesome/assets/webfonts/fa-v4compatibility.woff2 differ diff --git a/_extensions/quarto-ext/fontawesome/fontawesome.lua b/_extensions/quarto-ext/fontawesome/fontawesome.lua new file mode 100644 index 0000000..ff64dca --- /dev/null +++ b/_extensions/quarto-ext/fontawesome/fontawesome.lua @@ -0,0 +1,84 @@ +local function ensureLatexDeps() + quarto.doc.use_latex_package("fontawesome5") +end + +local function ensureHtmlDeps() + quarto.doc.add_html_dependency({ + name = 'fontawesome6', + version = '0.1.0', + stylesheets = {'assets/css/all.css', 'assets/css/latex-fontsize.css'} + }) +end + +local function isEmpty(s) + return s == nil or s == '' +end + +local function isValidSize(size) + local validSizes = { + "tiny", + "scriptsize", + "footnotesize", + "small", + "normalsize", + "large", + "Large", + "LARGE", + "huge", + "Huge" + } + for _, v in ipairs(validSizes) do + if v == size then + return size + end + end + return "" +end + +return { + ["fa"] = function(args, kwargs) + + local group = "solid" + local icon = pandoc.utils.stringify(args[1]) + if #args > 1 then + group = icon + icon = pandoc.utils.stringify(args[2]) + end + + local title = pandoc.utils.stringify(kwargs["title"]) + if not isEmpty(title) then + title = " title=\"" .. title .. "\"" + end + + local label = pandoc.utils.stringify(kwargs["label"]) + if isEmpty(label) then + label = " aria-label=\"" .. icon .. "\"" + else + label = " aria-label=\"" .. label .. "\"" + end + + local size = pandoc.utils.stringify(kwargs["size"]) + + -- detect html (excluding epub which won't handle fa) + if quarto.doc.is_format("html:js") then + ensureHtmlDeps() + if not isEmpty(size) then + size = " fa-" .. size + end + return pandoc.RawInline( + 'html', + "" + ) + -- detect pdf / beamer / latex / etc + elseif quarto.doc.is_format("pdf") then + ensureLatexDeps() + if isEmpty(isValidSize(size)) then + return pandoc.RawInline('tex', "\\faIcon{" .. icon .. "}") + else + return pandoc.RawInline('tex', "{\\" .. size .. "\\faIcon{" .. icon .. "}}") + end + else + return pandoc.Null() + end + end +} diff --git a/index.html b/index.html index c21e4fb..c3e1033 100644 --- a/index.html +++ b/index.html @@ -5,10 +5,13 @@ + - + + + - + index @@ -24,9 +27,10 @@ ul.task-list{list-style: none;} ul.task-list li input[type="checkbox"] { width: 0.8em; - margin: 0 0.8em 0.2em -1.6em; + margin: 0 0.8em 0.2em -1em; /* quarto-specific, see https://github.com/quarto-dev/quarto-cli/issues/4556 */ vertical-align: middle; } + /* CSS for syntax highlighting */ pre > code.sourceCode { white-space: pre; position: relative; } pre > code.sourceCode > span { display: inline-block; line-height: 1.25; } pre > code.sourceCode > span:empty { height: 1.2em; } @@ -92,7 +96,7 @@ code span.vs { color: #008000; } /* VerbatimString */ code span.wa { color: #696969; font-style: italic; } /* Warning */ - + @@ -137,44 +141,44 @@ font-weight: 400; } - .callout.callout-captioned.callout-style-simple .callout-body { + .callout.callout-titled.callout-style-simple .callout-body { margin-top: 0.2em; } - .callout:not(.callout-captioned) .callout-body { + .callout:not(.callout-titled) .callout-body { display: flex; } - .callout:not(.no-icon).callout-captioned.callout-style-simple .callout-content { + .callout:not(.no-icon).callout-titled.callout-style-simple .callout-content { padding-left: 1.6em; } - .callout.callout-captioned .callout-header { + .callout.callout-titled .callout-header { padding-top: 0.2em; margin-bottom: -0.2em; } - .callout.callout-captioned .callout-caption p { + .callout.callout-titled .callout-title p { margin-top: 0.5em; margin-bottom: 0.5em; } - .callout.callout-captioned.callout-style-simple .callout-content p { + .callout.callout-titled.callout-style-simple .callout-content p { margin-top: 0; } - .callout.callout-captioned.callout-style-default .callout-content p { + .callout.callout-titled.callout-style-default .callout-content p { margin-top: 0.7em; } - .callout.callout-style-simple div.callout-caption { + .callout.callout-style-simple div.callout-title { border-bottom: none; font-size: .9rem; font-weight: 600; opacity: 75%; } - .callout.callout-style-default div.callout-caption { + .callout.callout-style-default div.callout-title { border-bottom: none; font-weight: 600; opacity: 85%; @@ -206,7 +210,7 @@ background-size: 0.9rem 0.9rem; } - .callout-caption { + .callout-title { display: flex } @@ -219,16 +223,16 @@ display: none !important; } - .callout.callout-captioned .callout-body > .callout-content > :last-child { + .callout.callout-titled .callout-body > .callout-content > :last-child { margin-bottom: 0.5rem; } - .callout.callout-captioned .callout-icon::before { + .callout.callout-titled .callout-icon::before { margin-top: .5rem; padding-right: .5rem; } - .callout:not(.callout-captioned) .callout-icon::before { + .callout:not(.callout-titled) .callout-icon::before { margin-top: 1rem; padding-right: .5rem; } @@ -243,7 +247,7 @@ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAACshmLzAAAEU0lEQVRYCcVXTWhcVRQ+586kSUMMxkyaElstCto2SIhitS5Ek8xUKV2poatCcVHtUlFQk8mbaaziwpWgglJwVaquitBOfhQXFlqlzSJpFSpIYyXNjBNiTCck7x2/8/LeNDOZxDuEkgOXe++553zfefee+/OYLOXFk3+1LLrRdiO81yNqZ6K9cG0P3MeFaMIQjXssE8Z1JzLO9ls20MBZX7oG8w9GxB0goaPrW5aNMp1yOZIa7Wv6o2ykpLtmAPs/vrG14Z+6d4jpbSKuhdcSyq9wGMPXjonwmESXrriLzFGOdDBLB8Y6MNYBu0dRokSygMA/mrun8MGFN3behm6VVAwg4WR3i6FvYK1T7MHo9BK7ydH+1uurECoouk5MPRyVSBrBHMYwVobG2aOXM07sWrn5qgB60rc6mcwIDJtQrnrEr44kmy+UO9r0u9O5/YbkS9juQckLed3DyW2XV/qWBBB3ptvI8EUY3I9p/67OW+g967TNr3Sotn3IuVlfMLVnsBwH4fsnebJvyGm5GeIUA3jljERmrv49SizPYuq+z7c2H/jlGC+Ghhupn/hcapqmcudB9jwJ/3jvnvu6vu5lVzF1fXyZuZZ7U8nRmVzytvT+H3kilYvH09mLWrQdwFSsFEsxFVs5fK7A0g8gMZjbif4ACpKbjv7gNGaD8bUrlk8x+KRflttr22JEMRUbTUwwDQScyzPgedQHZT0xnx7ujw2jfVfExwYHwOsDTjLdJ2ebmeQIlJ7neo41s/DrsL3kl+W2lWvAga0tR3zueGr6GL78M3ifH0rGXrBC2aAR8uYcIA5gwV8zIE8onoh8u0Fca/ciF7j1uOzEnqcIm59sEXoGc0+z6+H45V1CvAvHcD7THztu669cnp+L0okAeIc6zjbM/24LgGM1gZk7jnRu1aQWoU9sfUOuhrmtaPIO3YY1KLLWZaEO5TKUbMY5zx8W9UJ6elpLwKXbsaZ4EFl7B4bMtDv0iRipKoDQT2sNQI9b1utXFdYisi+wzZ/ri/1m7QfDgEuvgUUEIJPq3DhX/5DWNqIXDOweC2wvIR90Oq3lDpdMIgD2r0dXvGdsEW5H6x6HLRJYU7C69VefO1x8Gde1ZFSJLfWS1jbCnhtOPxmpfv2LXOA2Xk2tvnwKKPFuZ/oRmwBwqRQDcKNeVQkYcOjtWVBuM/JuYw5b6isojIkYxyYAFn5K7ZBF10fea52y8QltAg6jnMqNHFBmGkQ1j+U43HMi2xMar1Nv0zGsf1s8nUsmUtPOOrbFIR8bHFDMB5zL13Gmr/kGlCkUzedTzzmzsaJXhYawnA3UmARpiYj5ooJZiUoxFRtK3X6pgNPv+IZVPcnwbOl6f+aBaO1CNvPW9n9LmCp01nuSaTRF2YxHqZ8DYQT6WsXT+RD6eUztwYLZ8rM+rcPxamv1VQzFUkzFXvkiVrySGQgJNvXHJAxiU3/NwiC03rSf05VBaPtu/Z7/B8Yn/w7eguloAAAAAElFTkSuQmCC'); } - div.callout-note.callout-style-default .callout-caption { + div.callout-note.callout-style-default .callout-title { background-color: #dae6fb } @@ -255,7 +259,7 @@ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAACshmLzAAAEKklEQVRYCcVXTWhcVRS+575MJym48A+hSRFr00ySRQhURRfd2HYjk2SSTokuBCkU2o0LoSKKraKIBTcuFCoidGFD08nkBzdREbpQ1EDNIv8qSGMFUboImMSZd4/f9zJv8ibJMC8xJQfO3HPPPef7zrvvvnvviIkpC9nsw0UttFunbUhpFzFtarSd6WJkStVMw5xyVqYTvkwfzuf/5FgtkVoB0729j1rjXwThS7Vio+Mo6DNnvLfahoZ+i/o32lULuJ3NNiz7q6+pyAUkJaFF6JwaM2lUJlV0MlnQn5aTRbEu0SEqHUa0A4AdiGuB1kFXRfVyg5d87+Dg4DL6m2TLAub60ilj7A1Ec4odSAc8X95sHh7+ZRPCFo6Fnp7HfU/fBng/hi10CjCnWnJjsxvDNxWw0NfV6Rv5GgP3I3jGWXumdTD/3cbEOP2ZbOZp69yniG3FQ9z1jD7bnBu9Fc2tKGC2q+uAJOQHBDRiZX1x36o7fWBs7J9ownbtO+n0/qWkvW7UPIfc37WgT6ZGR++EOJyeQDSb9UB+DZ1G6DdLDzyS+b/kBCYGsYgJbSQHuThGKRcw5xdeQf8YdNHsc6ePXrlSYMBuSIAFTGAtQo+VuALo4BX83N190NWZWbynBjhOHsmNfFWLeL6v+ynsA58zDvvAC8j5PkbOcXCMg2PZFk3q8MjI7WAG/Dp9AwP7jdGBOOQkAvlFUB+irtm16I1Zw9YBcpGTGXYmk3kQIC/Cds55l+iMI3jqhjAuaoe+am2Jw5GT3Nbz3CkE12NavmzN5+erJW7046n/CH1RO/RVa8lBLozXk9uqykkGAyRXLWlLv5jyp4RFsG5vGVzpDLnIjTWgnRy2Rr+tDKvRc7Y8AyZq10jj8DqXdnIRNtFZb+t/ZRtXcDiVnzpqx8mPcDWxgARUqx0W1QB9MeUZiNrV4qP+Ehc+BpNgATsTX8ozYKL2NtFYAHc84fG7ndxUPr+AR/iQSns7uSUufAymwDOb2+NjK27lEFocm/EE2WpyIy/Hi66MWuMKJn8RvxIcj87IM5Vh9663ziW36kR0HNenXuxmfaD8JC7tfKbrhFr7LiZCrMjrzTeGx+PmkosrkNzW94ObzwocJ7A1HokLolY+AvkTiD/q1H0cN48c5EL8Crkttsa/AXQVDmutfyku0E7jShx49XqV3MFK8IryDhYVbj7Sj2P2eBxwcXoe8T8idsKKPRcnZw1b+slFTubwUwhktrfnAt7J++jwQtLZcm3sr9LQrjRzz6cfMv9aLvgmnAGvpoaGLxM4mAEaLV7iAzQ3oU0IvD5x9ix3yF2RAAuYAOO2f7PEFWCXZ4C9Pb2UsgDeVnFSpbFK7/IWu7TPTvBqzbGdCHOJQSxiEjt6IyZmxQyEJHv6xyQsYk//moVFsN2zP6fRImjfq7/n/wFDguUQFNEwugAAAABJRU5ErkJggg=='); } - div.callout-important.callout-style-default .callout-caption { + div.callout-important.callout-style-default .callout-title { background-color: #f7dddc } @@ -267,7 +271,7 @@ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAACshmLzAAAETklEQVRYCeVWW2gcVRg+58yaTUnizqbipZeX4uWhBEniBaoUX1Ioze52t7sRq6APio9V9MEaoWlVsFasRq0gltaAPuxms8lu0gcviE/FFOstVbSIxgcv6SU7EZqmdc7v9+9mJtNks51NTUH84ed889/PP+cmxP+d5FIbMJmNbpREu4WUkiTtCicKny0l1pIKmBzovF2S+hIJHX8iEu3hZJ5lNZGqyRrGSIQpq15AzF28jgpeY6yk6GVdrfFqdrD6Iw+QlB8g0YS2g7dyQmXM/IDhBhT0UCiRf59lfqmmDvzRt6kByV/m4JjtzuaujMUM2c5Z2d6JdKrRb3K2q6mA+oYVz8JnDdKPmmNthzkAk/lN63sYPgevrguc72aZX/L9C6x09GYyxBgCX4NlvyGUHOKELlm5rXeR1kchuChJt4SSwyddZRXgvwMGvYo4QSlk3/zkHD8UHxwVJA6zjZZqP8v8kK8OWLnIZtLyCAJagYC4rTGW/9Pqj92N/c+LUaAj27movwbi19tk/whRCIE7Q9vyI6yvRpftAKVTdUjOW40X3h5OXsKCdmFcx0xlLJoSuQngnrJe7Kcjm4OMq9FlC7CMmScQANuNvjfP3PjGXDBaUQmbp296S5L4DrpbrHN1T87ZVEZVCzg1FF0Ft+dKrlLukI+/c9ENo+TvlTDbYFvuKPtQ9+l052rXrgKoWkDAFnvh0wTOmYn8R5f4k/jN/fZiCM1tQx9jQQ4ANhqG4hiL0qIFTGViG9DKB7GYzgubnpofgYRwO+DFjh0Zin2m4b/97EDkXkc+f6xYAPX0KK2I/7fUQuwzuwo/L3AkcjugPNixC8cHf0FyPjWlItmLxWw4Ou9YsQCr5fijMGoD/zpdRy95HRysyXA74MWOnscpO4j2y3HAVisw85hX5+AFBRSHt4ShfLFkIMXTqyKFc46xdzQM6XbAi702a7sy04J0+feReMFKp5q9esYLCqAZYw/k14E/xcLLsFElaornTuJB0svMuJINy8xkIYuL+xPAlWRceH6+HX7THJ0djLUom46zREu7tTkxwmf/FdOZ/sh6Q8qvEAiHpm4PJ4a/doJe0gH1t+aHRgCzOvBvJedEK5OFE5jpm4AGP2a8Dxe3gGJ/pAutug9Gp6he92CsSsWBaEcxGx0FHytmIpuqGkOpldqNYQK8cSoXvd+xLxXADw0kf6UkJNFtdo5MOgaLjiQOQHcn+A6h5NuL2s0qsC2LOM75PcF3yr5STuBSAcGG+meA14K/CI21HcS4LBT6tv0QAh8Dr5l93AhZzG5ZJ4VxAqdZUEl9z7WJ4aN+svMvwHHL21UKTd1mqvChH7/Za5xzXBBKrUcB0TQ+Ulgkfbi/H/YT5EptrGzsEK7tR1B7ln9BBwckYfMiuSqklSznIuoIIOM42MQO+QnduCoFCI0bpkzjCjddHPN/F+2Yu+sd9bKNpVwHhbS3LluK/0zgfwD0xYI5dXuzlQAAAABJRU5ErkJggg=='); } - div.callout-warning.callout-style-default .callout-caption { + div.callout-warning.callout-style-default .callout-title { background-color: #fcefdc } @@ -279,7 +283,7 @@ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAACshmLzAAADr0lEQVRYCe1XTWgTQRj9ZjZV8a9SPIkKgj8I1bMHsUWrqYLVg4Ue6v9BwZOxSYsIerFao7UiUryIqJcqgtpimhbBXoSCVxUFe9CTiogUrUp2Pt+3aUI2u5vdNh4dmMzOzHvvezuz8xNFM0mjnbXaNu1MvFWRXkXEyE6aYOYJpdW4IXuA4r0fo8qqSMDBU0v1HJUgVieAXxzCsdE/YJTdFcVIZQNMyhruOMJKXYFoLfIfIvVIMWdsrd+Rpd86ZmyzzjJmLStqRn0v8lzkb4rVIXvnpScOJuAn2ACC65FkPzEdEy4TPWRLJ2h7z4cArXzzaOdKlbOvKKX25Wl00jSnrwVxAg3o4dRxhO13RBSdNvH0xSARv3adTXbBdTf64IWO2vH0LT+cv4GR1DJt+DUItaQogeBX/chhbTBxEiZ6gftlDNXTrvT7co4ub5A6gp9HIcHvzTa46OS5fBeP87Qm0fQkr4FsYgVQ7Qg+ZayaDg9jhg1GkWj8RG6lkeSacrrHgDaxdoBiZPg+NXV/KifMuB6//JmYH4CntVEHy/keA6x4h4CU5oFy8GzrBS18cLJMXcljAKB6INjWsRcuZBWVaS3GDrqB7rdapVIeA+isQ57Eev9eCqzqOa81CY05VLd6SamW2wA2H3SiTbnbSxmzfp7WtKZkqy4mdyAlGx7ennghYf8voqp9cLSgKdqNfa6RdRsAAkPwRuJZNbpByn+RrJi1RXTwdi8RQF6ymDwGMAtZ6TVE+4uoKh+MYkcLsT0Hk8eAienbiGdjJHZTpmNjlbFJNKDVAp2fJlYju6IreQxQ08UJDNYdoLSl6AadO+fFuCQqVMB1NJwPm69T04Wv5WhfcWyfXQB+wXRs1pt+nCknRa0LVzSA/2B+a9+zQJadb7IyyV24YAxKp2Jqs3emZTuNnKxsah+uabKbMk7CbTgJx/zIgQYErIeTKRQ9yD9wxVof5YolPHqaWo7TD6tJlh7jQnK5z2n3+fGdggIOx2kaa2YI9QWarc5Ce1ipNWMKeSG4DysFF52KBmTNMmn5HqCFkwy34rDg05gDwgH3bBi+sgFhN/e8QvRn8kbamCOhgrZ9GJhFDgfcMHzFb6BAtjKpFhzTjwv1KCVuxHvCbsSiEz4CANnj84cwHdFXAbAOJ4LTSAawGWFn5tDhLMYz6nWeU2wJfIhmIJBefcd/A5FWQWGgrWzyORZ3Q6HuV+Jf0Bj+BTX69fm1zWgK7By1YTXchFDORywnfQ7GpzOo6S+qECrsx2ifVQAAAABJRU5ErkJggg=='); } - div.callout-tip.callout-style-default .callout-caption { + div.callout-tip.callout-style-default .callout-title { background-color: #ccf1e3 } @@ -291,7 +295,7 @@ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAACshmLzAAACV0lEQVRYCdVWzWoUQRCuqp2ICBLJXgITZL1EfQDBW/bkzUMUD7klD+ATSHBEfAIfQO+iXsWDxJsHL96EHAwhgzlkg8nBg25XWb0zIb0zs9muYYWkoKeru+vn664fBqElyZNuyh167NXJ8Ut8McjbmEraKHkd7uAnAFku+VWdb3reSmRV8PKSLfZ0Gjn3a6Xlcq9YGb6tADjn+lUfTXtVmaZ1KwBIvFI11rRXlWlatwIAAv2asaa9mlB9wwygiDX26qaw1yYPzFXg2N1GgG0FMF8Oj+VIx7E/03lHx8UhvYyNZLN7BwSPgekXXLribw7w5/c8EF+DBK5idvDVYtEEwMeYefjjLAdEyQ3M9nfOkgnPTEkYU+sxMq0BxNR6jExrAI31H1rzvLEfRIdgcv1XEdj6QTQAS2wtstEALLG1yEZ3QhH6oDX7ExBSFEkFINXH98NTrme5IOaaA7kIfiu2L8A3qhH9zRbukdCqdsA98TdElyeMe5BI8Rs2xHRIsoTSSVFfCFCWGPn9XHb4cdobRIWABNf0add9jakDjQJpJ1bTXOJXnnRXHRf+dNL1ZV1MBRCXhMbaHqGI1JkKIL7+i8uffuP6wVQAzO7+qVEbF6NbS0LJureYcWXUUhH66nLR5rYmva+2tjRFtojkM2aD76HEGAD3tPtKM309FJg5j/K682ywcWJ3PASCcycH/22u+Bh7Aa0ehM2Fu4z0SAE81HF9RkB21c5bEn4Dzw+/qNOyXr3DCTQDMBOdhi4nAgiFDGCinIa2owCEChUwD8qzd03PG+qdW/4fDzjUMcE1ZpIAAAAASUVORK5CYII='); } - div.callout-caution.callout-style-default .callout-caption { + div.callout-caution.callout-style-default .callout-title { background-color: #ffe5d0 } @@ -384,10 +388,10 @@ } - + - + @@ -395,15 +399,22 @@
-

+

EDS 430 - Intro to The word 'Shiny' in cursive lettering, which is the logo used by Posit for the shiny package.

Building reactive apps & dashboards

-



+
+
+

Published: Oct 10, 2022

+
+

Last updated: Oct 16, 2023

+
+
+

Sam Csik |
Data Training Coordinator
National Center for Ecological Analysis & Synthesis

-

Master of Environmental Data Science |
Winter 2023
Bren School of Environmental Science & Management

+

Master of Environmental Data Science |
Bren School of Environmental Science & Management


@@ -412,260 +423,231 @@

-
-Check - Prerequisites -
+

Prerequisites

+

This workshop assumes that participants have the following:

-angle-right - R/RStudio installed & a basic familiarity with the language + R/RStudio installed & a basic familiarity with the language
-angle-right - A GitHub profile & git installed/configured + A GitHub profile & git installed/configured
-
-A blue hexagon with the word 'Shiny' in cursive lettering printed across the center. -
+
+
+
+

A blue hexagon with the word 'Shiny' in cursive lettering printed across the center.

+
+
+
-
-angle-right - You have the required R packages installed. You can install/update them all at once by running: +
+
+ You have the required R packages installed. You can install/update them all at once by running:
install.packages(pkgs = c("shiny", "shinydashboard", "shinyWidgets", "DT", "leaflet", "shinycssloaders", "tidyverse", "bslib", "fresh", "sass", "reactlog", "shinytest2", "palmerpenguins", "lterdatasampler", "gapminder", "markdown"))
-


-
+
No prior Shiny experience necessary!

-
-Star - Table of Contents Star - -
+

Table of Contents

+

We’re going to pack a lot into two days. Here’s what to expect:

-

+

(1) High-level overview of Shiny

-

+

What is Shiny? ~ Anatomy of a shiny app ~ Where to find examples

-

+

(2) Building shiny apps & dashboards

-

+

Setting up your repo & files ~ App #1 (single-file app) ~ App #2 (two-file app) ~ App #3 (shinydashboard) ~ Deploying apps

-

+

(3) Beautifying your user interface (UI)

-

+

Custom themes with bslib ~ Custom themes with fresh ~ Styling with CSS & Sass

-

+

(4) Improving your app’s user experience (UX)

-

+

Important UX considerations ~ Web accessibility

-

+

(5) Debugging & Testing

-

+

Debugging approaches ~ Testing apps

-

+

(6) Streamlining code

-

+

Writing functions ~ Shiny modules

-

+

(7) Wrap-up

-

+

Shiny alternatives ~ Words of wisdom ~ More resources

-
-

+

+

See the source code

+
+



+

You can reference the source code of all the apps we’ll be building/playing with throughout this workshop on GitHub GitHub.

-

-

-Star - Part 1: High-level overview of Shiny -

-


-

-angle-right - What is Shiny? +

Part 1: High-level overview of Shiny

+
+

+ What is Shiny?

-

-angle-right - Anatomy of a shiny app +

+ Anatomy of a shiny app

-

-angle-right - Where to find examples +

+ Where to find examples

-R Project - What is Shiny? + What is Shiny?

Think interactive web pages built by people who love to code in R (i.e. hopefully many of you!), no JavaScript experience necessary.

-
-Shiny makes building web apps easy -
+

Shiny makes building web apps easy

+

Shiny is an R package that makes it easy to build interactive web apps straight from R. You can host standalone apps on a webpage or embed them in R Markdown documents or build dashboards. You can also extend your Shiny apps with CSS themes, htmlwidgets, and JavaScript actions. - RStudio

-
-A gif of Andre Duarte's 'Worldbank-Shiny' app. On the lefthand side of the app, the title 'Gapminder Interactive Plot' sits above a series of three widgets. The first is a dropdown menu where the user can select a region (e.g. Europe & Central Asia) or view all regions at the same time. The next two widgets are slider inputs -- the first allows the user to select a year between 1960 and 2014, and the second allows the user to select a population size between 500 and 5000. On the right hand side of the app is a bubble plot of Fertility Rate vs. Life Expectancy, which updates as inputs are changed by the user. Hovering a bubble displays thge corresponding Country, Region, Population, Life Expectancy, and Fertility Rate. -
-
-

+ +A gif of Andre Duarte's 'Worldbank-Shiny' app. On the lefthand side of the app, the title 'Gapminder Interactive Plot' sits above a series of three widgets. The first is a dropdown menu where the user can select a region (e.g. Europe & Central Asia) or view all regions at the same time. The next two widgets are slider inputs -- the first allows the user to select a year between 1960 and 2014, and the second allows the user to select a population size between 500 and 5000. On the right hand side of the app is a bubble plot of Fertility Rate vs. Life Expectancy, which updates as inputs are changed by the user. Hovering a bubble displays thge corresponding Country, Region, Population, Life Expectancy, and Fertility Rate.

+

Worldbank-Shiny app to visualize fertility rate vs. life expectancy from 1960 to 2015, by Andre Duarte

-Puzzle Piece - The anatomy of a Shiny app + The anatomy of a Shiny app

What does a Shiny app look like under the hood?

-
-The basic anatomy of a Shiny app -
+

The basic anatomy of a Shiny app

+

Shiny apps are composed in two parts: (1) a web page that displays the app to a user (i.e. the user interface, or UI for short), and (2) a computer that powers the app (i.e. the server)

-
-A simple schematic of a Shiny app, which includes the User Interface (UI, colored in blue) and the Server (colored in orange). The UI creates what the user will see and interact with, while the server builds the outputs that react and update based on user inputs. -
-

+ +A simple schematic of a Shiny app, which includes the User Interface (UI, colored in blue) and the Server (colored in orange). The UI creates what the user will see and interact with, while the server builds the outputs that react and update based on user inputs.

The UI controls the layout and appearance of your app and is written in HTML (except we use functions from the shiny package to write that HTML). The server handles the logic of the app – in other words, it is a set of instructions that tells the webpage what to display when a user interacts with it.

-
-Widgets are web elements that users can interact with via the UI -
+

Widgets are web elements that users can interact with via the UI

+
-
-Examples of Shiny's pre-built widget options. These include buttons, single checkbox, checkbox groups, date input, date range, file input, numeric input, radio buttons, select box, sliders, and text input. The default color scheme is black and gray with selections highlighted in blue. -
+
+
+
+

Examples of Shiny's pre-built widget options. These include buttons, single checkbox, checkbox groups, date input, date range, file input, numeric input, radio buttons, select box, sliders, and text input. The default color scheme is black and gray with selections highlighted in blue.

+
+
+
+
-


-angle-right - Widgets collect information from the user which is then used to update outputs created in the server. + Widgets collect information from the user which is then used to update outputs created in the server.

-angle-right - Shiny comes with a set of of standard widgets (see left), but you can also explore widget extensions using a variety of other packages (e.g. {shinyWidgets}, {DT}, {plotly}) + Shiny comes with a set of of standard widgets (see left), but you can also explore widget extensions using a variety of other packages (e.g. {shinyWidgets}, {DT}, {plotly})

-
-Reactivity: a brief intro -
+

Reactivity: a brief intro

+

Reactivity is what makes Shiny apps responsive i.e. it lets the app instantly update itself whenever the user makes a change. At a very basic level, it looks something like this:

-
-A schematic of Shiny reactivity. The UI is represented by a light blue box. Inside the blue UI box, there is a radio button widget that says, 'Make a choice:' and three round radio buttons beneath it. Underneath that, there is a placeholder space for a reactive output to be created by the server. The server is to the left of the UI and is represented by an orange box. At a basic level, reactivity occurs after the following steps: (1) A widget gets information from a user which (2) is then passed to the server where it is used to update a data frame based on the users choice. (3) The new data frame is used to update outputs in the server, and (4) those outputs are then rendered in the UI. -
-

+ +A schematic of Shiny reactivity. The UI is represented by a light blue box. Inside the blue UI box, there is a radio button widget that says, 'Make a choice:' and three round radio buttons beneath it. Underneath that, there is a placeholder space for a reactive output to be created by the server. The server is to the left of the UI and is represented by an orange box. At a basic level, reactivity occurs after the following steps: (1) A widget gets information from a user which (2) is then passed to the server where it is used to update a data frame based on the users choice. (3) The new data frame is used to update outputs in the server, and (4) those outputs are then rendered in the UI.

Check out Garrett Grolemund’s article, How to understand reactivity in R for a more detailed overview of Shiny reactivity.

-Laptop Code - Can I see an example please? + Can I see an example please?

I’m glad you asked! There are lots of great examples online, including those developed by Bren alumni, as well as built-in teaching examples.

-
-Example shiny apps built by some familiar folks Face Smile - -
+

Example shiny apps built by some familiar folks

+

-Droplet -HydroTech Helper (video tutorial), by MEDS 2022 alumn, Daniel Kerstan, developed during his time as a USGS Hydrologic Technician – access real-time monitoring of USGS hydrology sites and equipment + HydroTech Helper (video tutorial), by MEDS 2022 alumn, Daniel Kerstan, developed during his time as a USGS Hydrologic Technician – access real-time monitoring of USGS hydrology sites and equipment

-Fish Fins -Moorea Coral Reef LTER Shiny Application (source code), by MEDS 2022 alumni, Allie Cole, Felicia Cruz, Jake Eisaguirre & Charles Henrickson as part of their MEDS capstone project – visualize spatial and temporal patterns of coral reef stressors surrounding Moorea, French Polynesia + Moorea Coral Reef LTER Shiny Application (source code), by MEDS 2022 alumni, Allie Cole, Felicia Cruz, Jake Eisaguirre & Charles Henrickson as part of their MEDS capstone project – visualize spatial and temporal patterns of coral reef stressors surrounding Moorea, French Polynesia

-Water -Marine Mammal Bycatch Impacts Exploration Tool (source code) by Dr. Megsie Siple and colleagues – compute population projections under different bycatch mortality levels + Marine Mammal Bycatch Impacts Exploration Tool (source code) by Dr. Megsie Siple and colleagues – compute population projections under different bycatch mortality levels

-Book open reader -Novel-gazing (source code) by Dr. Megsie Siple – a fun app for exploring your Goodreads data, inspired by community ecology + Novel-gazing (source code) by Dr. Megsie Siple – a fun app for exploring your Goodreads data, inspired by community ecology

-
-The Shiny packages comes with 11 built-in examples -
-

-angle-right - Check out the available Shiny app examples by running this code in your console: +

The Shiny packages comes with 11 built-in examples

+
+

+ Check out the available Shiny app examples by running this code in your console:

library(shiny)
 runExample(example = NA)
-

-angle-right - Run the first example, which plots R’s built-in faithful data set with a configurable number of bins: +

+ Run the first example, which plots R’s built-in faithful data set with a configurable number of bins:

runExample("01_hello")
-

+

Change the number of bins using the sliderInput widget and watch the histogram re-render.

-

-angle-right - These working examples also come paired with source code for you to see how the app is built. For example, the sliderInput is built with the following code: +

+ These working examples also come paired with source code for you to see how the app is built. For example, the sliderInput is built with the following code:

# Input: Slider for the number of bins ----
@@ -678,7 +660,7 @@
 
-

+

Now let’s build our own!

@@ -686,7 +668,7 @@

-play +Play Setting up your Shiny app

@@ -694,52 +676,42 @@

-
-Create your GitHub repo -
+

Create your GitHub repo

+

Let’s start by creating a GitHub repo to house our soon-to-be app(s), then we’ll clone our repo to our computer. I’m using RStudio to clone my repo in the example below, but you can also do this via the command line using git clone <repo-url>.

-
-A gif demonstrating how to set up a GitHub repo and how to clone that repo to your computer. Start by clicking on the 'Repositories' tab from your GitHub profile, then click the green 'New' button. Give your repo a name, check the box next to 'Add a README file', Add a .gitigore by choosing 'R' from the drop down menu, then click the green 'Create repository' button. From your repo landing page, click the green 'Code' button, then copy the URL to your clipboard. In RStudio, select 'New Project' from the top left 'Project' button, select 'Version Control', then 'Git', and paste your URL in the 'Repository URL field'. Your repo name should be auto completed in the 'Project directory name:' field, but if not, press the 'Tab' key. Click 'Create Project' to complete the process. -
-

-
-Shiny app repo structure -
-

+ +A gif demonstrating how to set up a GitHub repo and how to clone that repo to your computer. Start by clicking on the 'Repositories' tab from your GitHub profile, then click the green 'New' button. Give your repo a name, check the box next to 'Add a README file', Add a .gitigore by choosing 'R' from the drop down menu, then click the green 'Create repository' button. From your repo landing page, click the green 'Code' button, then copy the URL to your clipboard. In RStudio, select 'New Project' from the top left 'Project' button, select 'Version Control', then 'Git', and paste your URL in the 'Repository URL field'. Your repo name should be auto completed in the 'Project directory name:' field, but if not, press the 'Tab' key. Click 'Create Project' to complete the process.

+

Shiny app repo structure

+
+

Not much is required to make a functional app (which is awesome) – for a basic app, you really just need an app.R file where you’ll write the code for your UI and server. To stay organized, we’ll place app.R into a subdirectory (e.g. /myapp), which will also house any dependencies (e.g. other scripts/files/etc.) used by app.R.

-
-A visual representation of a basic shiny app repository file/folder structure. -
-

-
-All Shiny apps begin (in almost) the same way -
+ +A visual representation of a basic shiny app repository file/folder structure.

+

All Shiny apps begin (in almost) the same way

+

You have the option of creating either a single-file app or two-file app, and they look nearly the same (we’ll see both formats in the coming slides).

-
-Shiny apps can be built using a single app.R file, or using ui.R plus server.R, and most often a global.R in conjunction. -
-

+ +Shiny apps can be built using a single app.R file, or using ui.R plus server.R, and most often a global.R in conjunction.

Why two options? Before v0.10.2, Shiny apps needed to be split into two separate files, ui.R and server.R, that defined the UI and server components, respectively. With v0.10.2+, users can create a single-file app, app.R, which contains both the UI and server components together. While it largely comes down to personal preference, a single-file format is best for smaller apps or when creating a reprex, while the two-file format is beneficial when writing large, complex apps where breaking apart code can make things a bit more navigable/maintainable.

-
-Create a single-file Shiny app -
+

Create a single-file Shiny app

+

You can create a single-file app using RStudio’s built-in Shiny app template (e.g. File > New Project… > New Directory > Shiny Application), but it’s just as easy to create it from scratch (and you’ll memorize the structure faster!). Let’s do that now.

-

+

1. In your project repo, create a subdirectory to house your app – I’m calling mine, single-file-app.

-

+

2. Create a new R script inside /single-file-app and name it app.R – you must name your script app.R. Copy/type the following code into app.R, or use the shinyapp snippet to automatically generate a shiny app template.

@@ -757,195 +729,191 @@ # combine UI & server into an app ---- shinyApp(ui = ui, server = server)

-

+

Tip: Use code sections (denoted by # some text ----) to make navigating different sections of your app code a bit easier. Code sections will appear in your document outline (find the button at the top right corner of the script/editor panel).

-
-Run your app -
-

+

Run your app

+
+

Once you have saved your app.R file, the “Run” code button should turn into a “Run App” button that looks like: A green, right facing triangular arrow next to the words 'Run App'. Click that button to run your app (alternatively, run runApp("directory-name") in your console – for me, that looks like, runApp("single-file-app"))!

-

+

You won’t see much yet, as we have only built a blank app (but a functioning app, nonetheless!). In your RStudio console, you should see something like: Listening on http://127.0.0.1:XXXX, which is the URL where your app can be found. 127.0.0.1 is a standard address that means “this computer,” and the last four digits represent a randomly assigned port number. You can click the “Open in Browser” button, A button found on the top left-hand side of the RStudio viewer window that says 'Open in Browser' next to a small browser window icon with an arrow pointing up and to the right., to see how your app will appear when viewed in your web browser.

-

+

You should also notice a red stop sign, A red hexagon with the word 'STOP' printed in white across the center., appear in the top right corner of your console indicating that R is busy–this is because your R session is currently acting as your Shiny app server and listening for any user interaction with your app. Because of this, you won’t be able to run any commands in the console until you quit your app. Do so by pressing the stop button.

-
-The RStudio viewer showing a running app consisting of only a blank white screen since no elements have been added yet. -
+
+
+
+

The RStudio viewer showing a running app consisting of only a blank white screen since no elements have been added yet.

+
+
+
+
-
-RStudio running our blank app. In the console, we see the text 'Listening on http://127.0.0.1:6341' and a red stop sign indicating that RStudio is busy. -
+
+
+
+

RStudio running our blank app. In the console, we see the text 'Listening on http://127.0.0.1:6341' and a red stop sign indicating that RStudio is busy.

+
+
-

-
-Create a two-file Shiny app
+ +

+

Create a two-file Shiny app

+

In practice, you will likely find yourself opting for the the two-file format – code expands quickly, even when building relatively small apps. This two-file approach (well, three if you use a global.R file, which is encouraged) will help to keep your code a bit more manageable.

-

+

1. In your project repo, create a new subdirectory to house your app – I’m calling mine, two-file-app.

-

+

2. Create two new R scripts inside /two-file-app named ui.R and server.R – you must name your scripts ui.R and server.R. Copy the following code into the respective files. Note: When splitting your UI and server into separate files, you do not need to include the shinyApp(ui = ui, server = server) line of code (as required in your single-file app).

-

-ui.R -

+
+
+
+
ur.R
+
# user interface ----
 ui <- fluidPage()
+
-

-server.R -

+
+
+
+
server.R
+
# server instructions ----
 server <- function(input, output) {}
+
-

+

3. Lastly, let’s create a global.R file within /two-file-app and add dependencies (right now, that’s just loading the shiny package). Run your app as we did earlier.

-

-global.R -

+
+
+
+
global.R
+
# load libraries ----
 library(shiny)
+

-

-Star - Part 2: Building shiny apps & dashboards -

-


-

-angle-right - Setting up your repo & files +

Part 2: Building shiny apps & dashboards

+
+

+ Setting up your repo & files

-

-angle-right - App #1 (single-file app) - source code +

+ App #1 (single-file app) - source code

-

-angle-right - App #2 (two-file app) - source code +

+ App #2 (two-file app) - source code

-

-angle-right - App #3 (shinydashboard) - source code +

+ App #3 (shinydashboard) - source code

-

-angle-right - Deploying (& redeploying) apps +

+ Deploying (& redeploying) apps

-Screwdriver wrench - Building out your 1st app + Building out your 1st app

Here, we’ll create our first reactive objects and establish a general Shiny coding workflow.

-
-Book Open - Learning Objectives - App #1 (single-file app) -
-

+

Learning Objectives - App #1 (single-file app)

+
+

By the end of building out this first app, you should be a bit more familiar with:

-

-angle-right - writing a single-file (app.R) shiny app +

+ writing a single-file (app.R) shiny app

-

-angle-right - adding and styling text in the UI using tags +

+ adding and styling text in the UI using tags

-

-angle-right - practicing data wrangling and visualization outside of your shiny app +

+ practicing data wrangling and visualization outside of your shiny app

-

-angle-right - following a general workflow for building reactive apps, which includes adding inputs and outputs to the UI, then writing the server instructions on how to assemble user input values into outputs +

+ following a general workflow for building reactive apps, which includes adding inputs and outputs to the UI, then writing the server instructions on how to assemble user input values into outputs

-

-angle-right - running and quitting apps in RStudio +

+ running and quitting apps in RStudio

-

+

Packages introduced:

-

-Box Open -shiny: framework for building our reactive app + standard widgets +

+ shiny: framework for building our reactive app + standard widgets

-

-Box Open -DT: interactive datatable widgets (that can be made reactive using shiny!) +

+ DT: interactive datatable widgets (that can be made reactive using shiny!)

-

-Box Open -tidyverse: collection of packages for wrangling & visualizing data +

+ tidyverse: collection of packages for wrangling & visualizing data

-

-Box Open -palmerpenguins: data +

+ palmerpenguins: data

-
-Roadmap for App #1 -
+

Roadmap for App #1

+

We’ll start by building a small single-file app using data from the palmerpenguins package. We’ll build out the the following features:

@@ -964,19 +932,27 @@
-
-A gif of our current Shiny app, demonstrating reactivity. At the top left of our app is the title, 'My App Title' in large header font. Beneath it is a subtitle, 'Exploring Antarctic Penguins and Temperatures'. Below the subtitle is the slider input with the label, 'Select a range of body masses (g)'. A gray horizontal slider bar ranges from the values 2,700 to 6,300. The interactive slider value selectors are two round white circles, which, when moved apart from one another highlight the selected value range in blue. The user is adjusting the slider value selectors and the scatterplot of penguin bill length (mm) vs. flipper length (mm) is automatically updating. -
+
+
+
+

A gif of our current Shiny app, demonstrating reactivity. At the top left of our app is the title, 'My App Title' in large header font. Beneath it is a subtitle, 'Exploring Antarctic Penguins and Temperatures'. Below the subtitle is the slider input with the label, 'Select a range of body masses (g)'. A gray horizontal slider bar ranges from the values 2,700 to 6,300. The interactive slider value selectors are two round white circles, which, when moved apart from one another highlight the selected value range in blue. The user is adjusting the slider value selectors and the scatterplot of penguin bill length (mm) vs. flipper length (mm) is automatically updating.

+
+
-

-
-Add text in the UI
-

+ +

+

Add text in the UI

+
+

We’ll do this in the UI within fluidPage(), a layout function that sets up the basic visual structure of the page and scales components in real time to fill all available browser width. Add a title and subtitle to your app (be sure to separate each with a comma, ,), save, and run:

+
+
+
~/one-file-app/app.R
+
# user interface ----
 ui <- fluidPage(
   
@@ -988,11 +964,16 @@
   
   )
+
-

+

Recall that the UI is actually just an HTML document. We can style our text by adding static HTML elements using tags – a list of functions that parallel common HTML tags (e.g. <h1> == tags$h1()) The most common tags also have wrapper functions (e.g. h1()).

+
+
+
~/one-file-app/app.R
+
# user interface ----
 ui <- fluidPage(
   
@@ -1005,17 +986,17 @@
   )
-

-
-What are inputs and outputs?
-

+

+

What are inputs and outputs?

+
+

Next, we will begin to add some inputs and outputs to our UI inside fluidPage() (anything that you put into fluidPage() will appear in our app’s user interface…and we want inputs and outputs to show up there!).

-

+

Inputs (or widgets) are the things that users can interact with (e.g. toggle, slide) and provide values to your app. The input functions below correspond to the widgets you see on slide #9. Outputs are the R objects that your user sees (e.g. tables, plots) and are what respond when a user interacts with/changes an input value.

-

+

The shiny package comes with a number of input and output functions, but you can extend these with additional packages (e.g. shinyWidgets, plotly, DT, etc.; more on those later).

@@ -1023,45 +1004,35 @@

Examples of Input Functions:

-

-angle-right -actionButton() +

+ actionButton()

-

-angle-right -checkboxInput() +

+ checkboxInput()

-

-angle-right -checkboxGroupInput() +

+ checkboxGroupInput()

-

-angle-right -dateInput() +

+ dateInput()

-

-angle-right -dateRangeInput() +

+ dateRangeInput()

-

-angle-right -radioButtons() +

+ radioButtons()

-

-angle-right -selectInput() +

+ selectInput()

-

-angle-right -sliderInput() +

+ sliderInput()

-

-angle-right -textInput() +

+ textInput()

-

-angle-right - See full list of shiny input functions here +

+ See a full list of shiny input functions

@@ -1069,76 +1040,70 @@

Examples of Output Functions:

-

-angle-right -dataTableOutput() (inserts an interactive table) +

+ dataTableOutput() (inserts an interactive table)

-

-angle-right -imageOutput() (inserts an image) +

+ imageOutput() (inserts an image)

-

-angle-right -plotOutput() (inserts a plot) +

+ plotOutput() (inserts a plot)

-

-angle-right -tableOutput() (inserts a table) +

+ tableOutput() (inserts a table)

-

-angle-right -textOutput() (inserts text) +

+ textOutput() (inserts text)

-

-angle-right - See full list of shiny output functions here +

+ See a full list of shiny output functions

-
-Adding our reactive plot -
-

+

Adding our reactive plot

+
+

Next, we’ll create a scatterplot of penguin bill lengths vs. penguin flipper lengths using the penguins data set from the {palmerpengiuns} package. We will make this scatterplot reactive by adding a sliderInput that allows users to filter the displayed data points by selecting a range of penguin body masses (e.g. only plot bill and flipper lengths for penguins with body masses ranging from 4,500 grams to 6,000 grams).

-

+

To create a reactive plot, we will follow these steps:

-

+

1. Add an input (e.g. sliderInput) to the UI that users can interact with

-

+

2. Add an output (e.g. plotOutput) to the UI that creates a placeholder space to fill with our eventual reactive output

-

+

3. Tell the server how to assemble inputs into outputs

-
-A gif of our current Shiny app, demonstrating reactivity. At the top left of our app is the title, 'My App Title' in large header font. Beneath it is a subtitle, 'Exploring Antarctic Penguins and Temperatures'. Below the subtitle is the slider input with the label, 'Select a range of body masses (g)'. A gray horizontal slider bar ranges from the values 2,700 to 6,300. The interactive slider value selectors are two round white circles, which, when moved apart from one another highlight the selected value range in blue. The user is adjusting the slider value selectors and the scatterplot of penguin bill length (mm) vs. flipper length (mm) is automatically updating. -
+
+
+
+

A gif of our current Shiny app, demonstrating reactivity. At the top left of our app is the title, 'My App Title' in large header font. Beneath it is a subtitle, 'Exploring Antarctic Penguins and Temperatures'. Below the subtitle is the slider input with the label, 'Select a range of body masses (g)'. A gray horizontal slider bar ranges from the values 2,700 to 6,300. The interactive slider value selectors are two round white circles, which, when moved apart from one another highlight the selected value range in blue. The user is adjusting the slider value selectors and the scatterplot of penguin bill length (mm) vs. flipper length (mm) is automatically updating.

+
+
-

-
-Input function syntax
-

-angle-right -All input functions have the same first argument, inputId (NOTE: Id not ID), which is used to connect the front end of your app (the UI) with the back end (the server). For example, if your UI has an inputId = "name", the server function will access that input value using the syntax input$name. The inputId has two constraints: (1) it must be a simple string containing only letters, numbers, and underscores, (2) it must be unique within your app. + +

+

Input function syntax

+
+

+ All input functions have the same first argument, inputId (NOTE: Id not ID), which is used to connect the front end of your app (the UI) with the back end (the server). For example, if your UI has an inputId = "name", the server function will access that input value using the syntax input$name. The inputId has two constraints: (1) it must be a simple string containing only letters, numbers, and underscores, (2) it must be unique within your app.

-

-angle-right - Most input functions have a second parameter called label, which is used to create a human-readable label for the control, which will appear in the UI. +

+ Most input functions have a second parameter called label, which is used to create a human-readable label for the control, which will appear in the UI.

-

-angle-right - The remaining arguments are unique to each input function. Oftentimes, these include a value parameter, which lets you set the default value of your widget, where applicable. +

+ The remaining arguments are unique to each input function. Oftentimes, these include a value parameter, which lets you set the default value of your widget, where applicable.

A couple examples: @@ -1148,17 +1113,20 @@ selectInput(inputId = "island_input", label = "Choose and island:", ...) -

+

Check out the interactive Shiny Widgets Gallery to learn how to implement the most common widgets.

-
-Step 1: Add an input to your app -
+

Step 1: Add an input to your app

+

First let’s add a sliderInput() that will allow users to select a range of penguin body masses (g).

+
+
+
~/one-file-app/app.R
+
# user interface ----
 ui <- fluidPage(
   
@@ -1169,18 +1137,23 @@
               min = 2700, max = 6300, value = c(3000, 4000))
   )
+

When you run your app, you should see something similar to the image below. It’s operable, but does not yet have an associated output.

-
-A basic Shiny app, with a title that says 'My App Title', a subtitle that says, 'Exploring Palmer Penguins and Antarctic Temperatures, and a slider input with a label that says 'Select a range of body masses (g)'. The slider bar has a minimum value of 2,700 and a maximum value of 6,300, and the moveable slider selectors currently range from 3,000 to 4,000. -
+
+
+
+

A basic Shiny app, with a title that says 'My App Title', a subtitle that says, 'Exploring Palmer Penguins and Antarctic Temperatures, and a slider input with a label that says 'Select a range of body masses (g)'. The slider bar has a minimum value of 2,700 and a maximum value of 6,300, and the moveable slider selectors currently range from 3,000 to 4,000.

+
+
+
-

-
-Output function syntax
+

+

Output function syntax

+

Outputs in the UI create placeholders which are later filled by the server function.

@@ -1197,13 +1170,16 @@ dataTableOutput(outputId = "penguin_data")

-
-Step 2: Add an output to your app -
-

+

Step 2: Add an output to your app

+
+

Let’s now add a plotOutput(), which will be updated based on the user inputs via the sliderInput(), then run the app.

+
+
+
~/one-file-app/app.R
+
# user interface ----
 ui <- fluidPage(
   
@@ -1218,33 +1194,36 @@
   
   )
+
-

+

Okay, it looks like nothing changed?? Remember, *Output() functions create placeholders, but we have not yet written the server instructions on how to fill and update those placeholders. We can inspect the HTML and see that there is, in fact, a placeholder area awaiting our eventual output, which will be a plot named “bodyMass_scatterPlot”:

-
-A Google Chrome browser window with our Shiny app open on the left-hand side and the underlying HTML document open on the right. The app looks the same as before, except hovering over the HTML associated with our new plotOutput highlights a blue square region beneath the sliderInput. This highlighted region is the placeholder where our plot will eventually be rendered. -
+
+
+
+

A Google Chrome browser window with our Shiny app open on the left-hand side and the underlying HTML document open on the right. The app looks the same as before, except hovering over the HTML associated with our new plotOutput highlights a blue square region beneath the sliderInput. This highlighted region is the placeholder where our plot will eventually be rendered.

+
-

-
-Rendering outputs
+ + +

+

Rendering outputs

+

Each *Output() function in the UI is coupled with a render*() function in the server, which contains the “instructions” for creating the output based on user inputs (or in other words, the instructions for making your output reactive).

-
-

-Examples of *Output() functions and their corresponding render*() functions: +

+Examples of *Output() functions and their corresponding render*() functions:

-
@@ -1273,19 +1252,18 @@
Output function

-
-Step 3: Tell the server how to assemble inputs into outputs -
-

+

Step 3: Tell the server how to assemble inputs into outputs

+
+

Now that we’ve designed our input/output in the UI, we need to write the server instructions (i.e. write the server function) on how to use the input value(s) (i.e. penguin body mass range via a slider input) to update the output (scatter plot).

-

+

The server function is defined with two arguments, input and output, both of which are list-like objects. You must define both of these arguments within the server function. input contains the values of all the different inputs at any given time, while output is where you’ll save output objects to display in the app.

-

+

This part can be intimidating, but if you follow these three rules, you will successfully create reactivity within your shiny app!

-

+

Rules:

@@ -1300,12 +1278,15 @@

-
-Rule 1: Save objects you want to display to output$<id> -
+

Rule 1: Save objects you want to display to output$<id>

+
+
+
+
~/one-file-app/app.R
+
# load packages ----
 library(shiny)
 
@@ -1331,28 +1312,29 @@
   
 }
+
-

+

In our UI, we created a placeholder for our plot using the plotOutput() function and gave it the Id "bodyMass_scatterplot". In our server, we will save our plot to the output argument by its outputId.

-

+

Note: In the UI, our outputId is quoted ("bodyMass_scatterPlot"), but not in the server (bodyMass_scatterPlot).

-
-Rule 2: Build reactive objects with render*() -
-

-Use the appropriate render*() function to make your output reactive (e.g. if you have a plotOutput in your UI, you will need to use renderPlot() in your server). -

-

-Within your render*(), write any code inside a set of curly braces, {}. This allows you to include as many lines of code as it takes to build your object. +

Rule 2: Build reactive objects with render*()

+
+

+Use the appropriate render*() function to make your output reactive (e.g. if you have a plotOutput in your UI, you will need to use renderPlot() in your server). Within your render*(), write any code inside a set of curly braces, {}. This allows you to include as many lines of code as it takes to build your object.

+
+
+
~/one-file-app/app.R
+
# load packages ----
 library(shiny)
 
@@ -1381,16 +1363,20 @@
   }) 
 }
-

-
-An Aside: Draft objects (e.g. plots) in a separate script first
+

+

An Aside: Draft objects (e.g. plots) in a separate script first

+

I find it easier to experiment and draft my objects (e.g. plots) first in a separate script, then copy the code over to the server after. I want to make a plot that looks like this:

+
+
+
~/scratch/practice-script-app1-penguins.R
+
# load packages
 library(palmerpenguins)
 library(tidyverse)
@@ -1408,25 +1394,34 @@
   theme(legend.position = c(0.85, 0.2),
         legend.background = element_rect(color = "white"))
+
-
-A scatterplot with Flipper length (mm) on the x-axis and Bill length (mm) on the y-axis. Data points are colored by penguins species: Adelie in orange circles, Chinstrap in purple triangles, and Gentoo in green squares. -
+
+
+
+

A scatterplot with Flipper length (mm) on the x-axis and Bill length (mm) on the y-axis. Data points are colored by penguins species: Adelie in orange circles, Chinstrap in purple triangles, and Gentoo in green squares.

+
+
+
-

-Tip: Save your practice script in a separate directory (i.e. not inside your app directory) – I typically save mine to something like ~/scratch/practice_script.R. +

+

+Tip: Save your practice script in a separate directory (i.e. not inside your app directory) – I typically save mine to something like ~/scratch/practice-script.R.

-
-Copy your plot code into the server -
-

+

Copy your plot code into the server

+
+

Copy your code over to your app, placing it inside the {} (and make sure to add any additional required packages to the top of your app.R script). Run your app. What do you notice?

+
+
+
~/one-file-app/app.R
+
# load packages ----
 library(shiny)
 library(palmerpenguins)
@@ -1464,25 +1459,26 @@
               legend.background = element_rect(color = "white")) 
   }) 
-

-
-A non-reactive plot now lives in our plotOutput() placeholder
+

+

A non-reactive plot now lives in our plotOutput() placeholder

+

We have a plot (yay!), but it isn’t reactive. We have not yet told the server how to update the plot based on user inputs via the sliderInput() in the UI. Let’s do that next…

-
-A user changes the sliderInput meant associated with the penguin data scatterplot, but the plot does not update (i.e. it is not yet reactive). -
-

-
-Practice filtering data in our separate script -
-

+ +A user changes the sliderInput meant associated with the penguin data scatterplot, but the plot does not update (i.e. it is not yet reactive).

+

Practice filtering data in our separate script

+
+

First, create a new data frame where we filter the body_mass_g column for observations within a specific range of values (in this example, values ranging from 3000 - 4000):

+
+
+
~/scratch/practice-script-app1-penguins.R
+
# load packages
 library(palmerpenguins)
 library(tidyverse)
@@ -1492,13 +1488,18 @@
   filter(body_mass_g %in% 3000:4000)
+
-

+

Then, plot the new filtered data frame:

+
+
+
~/scratch/practice-script-app1-penguins.R
+
# plot new, filtered data
 ggplot(na.omit(body_mass_df), # plot 'body_mass_df' rather than 'penguins' df
        aes(x = flipper_length_mm, y = bill_length_mm, 
@@ -1512,25 +1513,35 @@
   theme(legend.position = c(0.85, 0.2),
         legend.background = element_rect(color = "white"))
+
-
-A scatterplot of penguin Bill length (mm) vs. Flipper length (mm) for Adelie (orange circles), Chinstrap (purple triangles), and Gentoo (green squares) penguins. For all species, bill length tends to increase with flipper length. -
+
+
+
+

A scatterplot of penguin Bill length (mm) vs. Flipper length (mm) for Adelie (orange circles), Chinstrap (purple triangles), and Gentoo (green squares) penguins. For all species, bill length tends to increase with flipper length.

+
+
-

-
-Which part of our code needs to be updated when a user changes the slider range input?
+ +

+

Which part of our code needs to be updated when a user changes the slider range input?

+
+
+
+
~/scratch/practice-script-app1-penguins.R
+
body_mass_df <- penguins |> 
   filter(body_mass_g %in% 3000:4000) # 3000:4000 needs to be update-able (or in other words, reactive)!
+

For example: @@ -1541,9 +1552,14 @@

body_mass_df <- penguins |> 
   filter(body_mass_g %in% 2857:5903)
-
-Our Shiny app with just a title, subtitle, and slider input, where the input values are at a minimum of 2,857 and a maximum of 5,903. -
+
+
+
+

Our Shiny app with just a title, subtitle, and slider input, where the input values are at a minimum of 2,857 and a maximum of 5,903.

+
+
+
+
@@ -1551,20 +1567,28 @@
body_mass_df <- penguins |> 
   filter(body_mass_g %in% 3725:5191)
-
-Our Shiny app with just a title, subtitle, and slider input, where the input values are at a minimum of 3,725 and a maximum of 5,191. -
+
+
+
+

Our Shiny app with just a title, subtitle, and slider input, where the input values are at a minimum of 3,725 and a maximum of 5,191.

+
+
-

-
-Rule 3: Access input values with input$<id>
-

+ +

+

Rule 3: Access input values with input$<id>

+
+

Recall that in our UI, we gave our sliderInput() an inputId = "body_mass_input".

+
+
+
~/one-file-app/app.R
+
# load packages (omitted for brevity) ----
 
 # user interface ----
@@ -1591,17 +1615,21 @@
     
 }
-

-
-Rule 3: Access input values with input$<id>
-

+

+

Rule 3: Access input values with input$<id>

+
+

In our server, we can access the values of that slider input using the syntax, input$body_mass_input. If you want your output to change according to the input values, substitute hard-coded values (e.g. 3725:5191) with the input values from the UI (e.g. input$body_mass_input[1]:input$body_mass_input[2]).

-

+

Importantly, we need to use reactive() to create reactive data frames that update with user inputs. When you call your reactive data frame in your ggplot, the data frame name must be followed by ().

+
+
+
~/one-file-app/app.R
+
# load packages (omitted for brevity) ----
 
 # user interface ----
@@ -1634,20 +1662,17 @@
     
 }
-

-
-Okay, RUN THAT APP!
+

+

Okay, RUN THAT APP!

+

You should now have a reactive Shiny app! Note that reactivity automatically occurs whenever you use an input value to render an output object.

-
-A gif of our current Shiny app, demonstrating reactivity. At the top left of our app is the title, 'My App Title' in large header font. Beneath it is a subtitle, 'Exploring Antarctic Penguins and Temperatures'. Below the subtitle is the slider input with the label, 'Select a range of body masses (g)'. A gray horizontal slider bar ranges from the values 2,700 to 6,300. The interactive slider value selectors are two round white circles, which, when moved apart from one another highlight the selected value range in blue. The user is adjusting the slider value selectors and the scatterplot of penguin bill length (mm) vs. flipper length (mm) is automatically updating. -
-

-
-Recap: We created our first reactive Shiny app following these steps: -
+ +A gif of our current Shiny app, demonstrating reactivity. At the top left of our app is the title, 'My App Title' in large header font. Beneath it is a subtitle, 'Exploring Antarctic Penguins and Temperatures'. Below the subtitle is the slider input with the label, 'Select a range of body masses (g)'. A gray horizontal slider bar ranges from the values 2,700 to 6,300. The interactive slider value selectors are two round white circles, which, when moved apart from one another highlight the selected value range in blue. The user is adjusting the slider value selectors and the scatterplot of penguin bill length (mm) vs. flipper length (mm) is automatically updating.

+

Recap: We created our first reactive Shiny app following these steps:

+

1. We created an app.R file in it’s own directory and began our app with the template, though you can also create a two-file Shiny app by using separate ui.R and server.R files. @@ -1667,125 +1692,124 @@

4. We wrote the server instructions for how to assemble inputs into outputs, following these rules:

-

-angle-right - save objects that you want to display to output$<id> +

+ save objects that you want to display to output$<id>

-

-angle-right - build reactive objects using a render*() function (and similarly, build reactive data frames using reactive() +

+ build reactive objects using a render*() function (and similarly, build reactive data frames using reactive()

-

-angle-right - access input values with input$<id> +

+ access input values with input$<id>

-

+

And we saw that reactivity automatically occurs whenever we use an input value to render an output object.

-
-Question - Exercise 1: Add another reactive widget -
-

+

Question + Exercise 1: Add another reactive widget

+
+

The {DT} package provides an R interface to the JavaScript library DataTables (you may have already used the DT package in your knitted RMarkdown/Quarto HTML documents). DT datatables allow for filtering, pagination, sorting, and lots of other neat features for tables on your HTML pages.

-

+

Working alone or in groups, add a reactive DT datatable to your app with a checkboxGroupInput that allows users to select which year(s) to include in the table. Configure your checkboxGroupInput so that the years 2007 and 2008 are pre-selected.

-

+

In the end, your app should look something like the example to the right.

-
-A gif of our current Shiny app, demonstrating the newly added DT::datatable. The user is able to select which years (2007, 2008, 2009) to display data for by clicking on one more more checkboxes. -
+
+
+
+

A gif of our current Shiny app, demonstrating the newly added DT::datatable. The user is able to select which years (2007, 2008, 2009) to display data for by clicking on one more more checkboxes.

+
+
+
+


-

+

See next slide for some tips on getting started!

-
-Lightbulb - Exercise 1: Tips -
-

+

Lightbulb + Exercise 1: Tips

+
+

Tips:

-

-angle-right - Use ?checkboxGroupInput to learn more about which arguments you need (remember, all inputs require an inputId and oftentimes a label, but there are others required to make this work as well) +

+ Use ?checkboxGroupInput to learn more about which arguments you need (remember, all inputs require an inputId and oftentimes a label, but there are others required to make this work as well)

-

-angle-right - Both shiny and DT packages have functions named dataTableOutput() and renderDataTable()DT::renderDataTable() allows you to create both server-side and client-side DataTables and supports additional DataTables features while shiny::renderDataTable() only provides server-side DataTables. Be sure to use the one from the DT package using the syntax packageName::functionName(). +

+ Both shiny and DT packages have functions named dataTableOutput() and renderDataTable()DT::renderDataTable() allows you to create both server-side and client-side DataTables and supports additional DataTables features while shiny::renderDataTable() only provides server-side DataTables. Be sure to use the one from the DT package using the syntax packageName::functionName().

-

-angle-right - There are lots of ways to customize DT tables, but to create a basic one, all you need is to DT::dataTable(your_dataframe) +

+ There are lots of ways to customize DT tables, but to create a basic one, all you need is DT::dataTable(your_dataframe)

-

+

And remember to follow the steps outlined on the previous slides (jump back to slide 27):

-

+

1. Add an input (e.g. checkboxGroupInput) to the UI that users can interact with

-

+

2. Add an output (e.g. DT::datatableOutput) to the UI that creates a placeholder space to fill with our eventual reactive output

-

+

3. Tell the server how to assemble inputs into outputs following 3 rules:

-

+

3.1 Save objects you want to display to output$<id>

-

+

3.2 Build reactive objects using a render*() function

-

+

3.3 Access input values with input$<id>

-

+

See next slide for a solution!

-
-Check - Exercise 1: A solution -
-

+

Exercise 1: A solution

+
+

Press the right arrow key to advance through the newly added lines of code.

+
+
+
~/one-file-app/app.R
+
# load packages ----
 library(shiny)
 library(palmerpenguins)
@@ -1865,49 +1889,49 @@
 # combine UI & server into an app ----
 shinyApp(ui = ui, server = server)
-

-
-Common mistakes to look out for
+

+

Common mistakes to look out for

+

It’s inevitable that you’ll make mistakes here and there as you build out your app…and they can be frustrating to catch. A few that I find myself making over and over again are:


-

-angle-right - misspelling inputId as inputID (or outputId as outputID) +

+ misspelling inputId as inputID (or outputId as outputID)

-

-angle-right - misspelling your inputId (or outputId) name in the server (e.g. UI: inputId = "myInputID", server: input$my_Input_ID) +

+ misspelling your inputId (or outputId) name in the server (e.g. UI: inputId = "myInputID", server: input$my_Input_ID)

-

-angle-right - repeating inputIds (each must be unique) +

+ repeating inputIds (each must be unique)

-

-angle-right - forgetting to separate UI elements with a comma, , +

+ forgetting to separate UI elements with a comma, ,

-

-angle-right - forgetting the set of parentheses when calling the name of a reactive data frame in a plot (e.g. ggplot(my_reactive_df(), aes(...))) +

+ forgetting the set of parentheses when calling the name of a reactive data frame in a plot (e.g. ggplot(my_reactive_df(), aes(...)))

-
-A gif of Britney Spears in her iconic red spandex outfit singing 'Oops I did it again.' -
+


+
+
+
+

A gif of Britney Spears in her iconic red spandex outfit singing 'Oops I did it again.'

+
+
+
+

-Mug Hot - Break + Break

Give your eyes a break from the computer screen! @@ -1915,7 +1939,7 @@

-
+
@@ -1926,90 +1950,78 @@

-Screwdriver wrench - Building out your 2nd app + Building out your 2nd app

Up until now, we’ve been adding our text and widgets in a pretty unstructured way – elements are stacked on top of one another within a single column. Next, we’ll learn how to customize the layout of our app to make it a bit more visually pleasing.

-
-Book Open - Learning Objectives - App #2v1 (two-file app) -
-

+

Learning Objectives - App #2v1 (two-file app)

+
+

By the end of building out this second app, you should:

-

-angle-right - be comfortable creating a shiny app using the two-file (ui.R & server.R) format along with a global.R file +

+ be comfortable creating a shiny app using the two-file (ui.R & server.R) format along with a global.R file

-

-angle-right - understand how to use layout functions to customize the visual structure of your app’s UI +

+ understand how to use layout functions to customize the visual structure of your app’s UI

-

-angle-right - have more practice building reactive outputs – and placing them within the layout structure of your app +

+ have more practice building reactive outputs – and placing them within the layout structure of your app

-

-angle-right - be able to create multiple inputs that control a given output +

+ be able to create multiple inputs that control a given output

-

-angle-right - know how to import larger bodies of text using includeMarkdown() (rather than writing & styling text within your UI) +

+ know how to import larger bodies of text using includeMarkdown() (rather than writing & styling text within your UI)

-

-angle-right - successfully publish an app using shinyapps.io +

+ successfully publish an app using shinyapps.io

-

+

Packages introduced:

-

-Box Open -shinyWidgets: extend shiny widgets with some different, fun options +

+ shinyWidgets: extend shiny widgets with some different, fun options

-

-Box Open -lterdatasampler: data +

+ lterdatasampler: data

-
-Roadmap for App #2v1 -
+

Roadmap for App #2v1

+

We’ll be building out our two-file app using data from the lterdatasampler and palmerpenguins packages. We’ll focus on creating a functional app that has a more visually pleasing UI layout (and we’ll refine it’s appearance even further in v2). By the end of v1, we’ll have created:

-

+

(a) A navigation bar with two pages, one of which will contain two tabs (one tab for each plot)

-

+

(b) A pickerInput and checkboxGroupButtons for users to filter cutthroat trout data in a reactive scatterplot

-

+

(c) A pickerInput for users to filter penguin data and a sliderInput to adjust the number of bins in a reactive histogram

@@ -2021,14 +2033,13 @@
-

+

You’ll notice that there are some UI quirks (most notably, blank plots that appear when no data is selected) that can make the user experience less than ideal (and even confusing) – we’ll learn about ways to improve this in v2 of our app.

-
-Two files? Try two panes! -
+

Two files? Try two panes!

+

We’ll be building out a two-file shiny app this time around. You can open multiple scripts up side-by-side by navigating to Tools > Global Options > Pane Layout > Add Column

@@ -2039,10 +2050,9 @@ This setup is certainly not required/necessary – organize your IDE however you work best!

-
-Practice data wrangling, filtering & viz first! -
-

+

Practice data wrangling, filtering & viz first!

+
+

Here’s what I’ve done in my ~scratch/practice_script_app2_lter.R file:

@@ -2055,6 +2065,10 @@
+
+
+
practice_script_app2_lter.R
+
#..........................load packages.........................
 library(lterdatasampler)
 library(tidyverse)
@@ -2104,6 +2118,7 @@
   labs(x = "Trout Length (mm)", y = "Trout Weight (g)", color = "Channel Type", shape = "Channel Type") +
   myCustomTheme
+
@@ -2111,7 +2126,7 @@ A scatterplot with Trout Length (mm) on the x-axis and Trout Weight (g) on the y-axis. Data points are colored and shaped by Channel Type, with data collected from pools represented with green diamonds and data collected from rapids represented by red stars.
-

+

We’ll use the and_vertebrates data set from lterdatasampler to create a scatter plot of trout weights by lengths. When we move to shiny, we’ll build 2 inputs for filtering our data: one to select channel_type and one to select section.

@@ -2122,6 +2137,10 @@
+
+
+
practice_script_app2_lter.R
+
#..........................load packages.........................
 library(palmerpenguins)
 library(tidyverse)
@@ -2138,6 +2157,7 @@
        fill = "Penguin species") +
   myCustomTheme
+
@@ -2145,7 +2165,7 @@ A histogram of penguin Flipper lengths (mm) with bars colored by species. Adelie are orange and Chinstrap are purple.
-

+

We’ll use the penguins data set from palmerpenguins to create a histogram of penguin flipper lengths. When we move to shiny, we’ll build 2 inputs for filtering our data: one to select island and one to change the number of histogram bins.

@@ -2155,36 +2175,29 @@

-
-A global.R file can help you keep code organized -
+

A global.R file can help you keep code organized

+

While not a requirement of a shiny app, a global.R file will help reduce redundant code, increase your app’s speed, and help keep code more clearly organized. It works by running once when your app is first launched, making any logic, objects, etc. contained in it available to both the ui.R and server.R files (or, in the case of a single-file shiny app, the app.R file). It’s a great place for things like:

-

-angle-right - loading packages +

+ loading packages

-

-angle-right - importing data +

+ importing data

-

-angle-right - sourcing scripts (particularly functions – we’ll talk more about functions later) +

+ sourcing scripts (particularly functions – we’ll talk more about functions later)

-

-angle-right - data wrangling (though you’ll want to do any major data cleaning before bringing your data into your app) +

+ data wrangling (though you’ll want to do any major data cleaning before bringing your data into your app)

-

-angle-right - building custom ggplot themes +

+ building custom ggplot themes

-

-angle-right - etc. +

+ etc.


@@ -2194,22 +2207,18 @@

-
-We created a perfectly functional first app, but it’s not so visually pleasing -
+

We created a perfectly functional first app, but it’s not so visually pleasing

+
-

-angle-right - nothing really grabs your eye +

+ nothing really grabs your eye

-

-angle-right - inputs & outputs are stacked vertically on top of one another (which requires a lot of vertical scrolling) +

+ inputs & outputs are stacked vertically on top of one another (which requires a lot of vertical scrolling)

-

-angle-right - widget label text is difficult to distinguish from other text +

+ widget label text is difficult to distinguish from other text

@@ -2224,13 +2233,12 @@ Before we jump into adding reactive outputs to our next app, we’ll first plan out the visual structure of our UI – first on paper, then with layout functions.

-
-Layout functions provide the high-level visual structure of your app -
-

+

Layout functions provide the high-level visual structure of your app

+
+

Layouts are created using a hierarchy of function calls (typically) inside fluidPage(). Layouts often require a series functions – container functions establish the larger area within which other layout elements are placed. See a few minimal examples of layout functions on the following slides (though more exist!).

-

+

Some useful layout function pairings:

@@ -2254,10 +2262,9 @@ )

-
-Example UI layouts -
-

+

Example UI layouts

+
+

Note: You can combine multiple layout function groups to really customize your UI – for example, you can create a navbar, include tabs, and also establish sidebar and main panel areas for inputs and outputs.

@@ -2386,29 +2393,30 @@
-

+

Examples adapted from Mastering Shiny, Ch. 6, by Hadley Wickham:

-
-Overview of layout functions used in App #2 -
-


+

Overview of layout functions used in App #2

+

-
-Build a navbar with two pages -
-

+

Build a navbar with two pages

+
+

First, let’s build a UI that has a navigation bar with two tabs – one for background information and one to contain our data visualizations. To do this, we’ll use navbarPage() instead of fluidPage() to create our webpage.

-

+

Tip: It can be super helpful add code comments at the start and end of each UI element – for example, see # data viz tabPanel--- and # END data viz tabPanel, below. Adding text that you will eventually replace with content (e.g. plots, tables, images, longer text) may help to visualize what you’re working towards as well.

+
+
+
ui.R
+
ui <- navbarPage(
   
   title = "LTER Animal Data Explorer",
@@ -2429,14 +2437,18 @@
   
 ) # END navbarPage
-

-
-Add two tabs to the “Explore the Data” page
-

+

+

Add two tabs to the “Explore the Data” page

+
+

Give your tabs the following titles: Trout and Penguins.

+
+
+
ui.R
+
ui <- navbarPage(
   
   title = "LTER Animal Data Explorer",
@@ -2474,14 +2486,18 @@
   
 ) # END navbarPage
-

-
-Question - Exercise 2: Add sidebar and main panels to the Penguins tab
+

+

Exercise 2: Add sidebar and main panels to the Penguins tab

+


@@ -2556,16 +2571,17 @@


-

+

See next slide for a solution!

-
-Check - Exercise 2: A solution -
+

Exercise 2: A solution

+
+
+
ui.R
+
ui <- navbarPage(
   
   title = "LTER Animal Data Explorer",
@@ -2637,83 +2653,80 @@
   
 ) # END navbarPage
-

-
-Some important things to remember when building your UI’s layout:
+

+

Some important things to remember when building your UI’s layout:

+

-angle-right - try creating a rough sketch of your intended layout before hitting the keyboard (I like to think of this as UI layout “pseudocode”) + try creating a rough sketch of your intended layout before hitting the keyboard (I like to think of this as UI layout “pseudocode”)

-angle-right - keeping clean code is important – we haven’t even any added any content yet and our UI is already >70 lines of code! + keeping clean code is important – we haven’t even any added any content yet and our UI is already >70 lines of code!

-angle-right - use rainbow parentheses, code comments and plenty of space between lines to keep things looking manageable and navigable + use rainbow parentheses, code comments and plenty of space between lines to keep things looking manageable and navigable

-angle-right - use the keyboard shortcut, command + I (Mac) or control + I (Windows), to align messy code – this helps put those off-alignment parentheses back where they belong + use the keyboard shortcut, command + I (Mac) or control + I (Windows), to align messy code – this helps put those off-alignment parentheses back where they belong

-angle-right - things can get out of hand quickly – add one layout section at a time, run your app to check that things look as you intend, then continue + things can get out of hand quickly – add one layout section at a time, run your app to check that things look as you intend, then continue

-

-Add data viz: First up, trout -

-

+

Add data viz: First up, trout

+
+

We’ll be using the and_vertebrates dataset from the {lterdatasampler} package to create our first reactive plot. These data contain coastal cutthroat trout (Oncorhynchus clarkii clarkii) lengths and weights collected in Mack Creek, Andrews Forest LTER. Original data can be found on the EDI Data Portal. Refer back to this slide to revisit our practice data wrangling & visualization script.


A drawing of the right-side profile of a coastal cutthroat trout.
-
-

+

Image Source: Joseph R. Tomelleri, as found on the Western Native Trout Initiative

-

-
-Add packages & wrangle data in global.R -
-

+

+

Add packages & wrangle data in global.R

+
+

In addition to the {lterdatasampler} package, we’ll also be using the tidyverse for data wrangling/visualization, and the {shinyWidgets} package to add a pickerInput and a checkboxGroupInput to our app.

-

+

Import those three packages at the top of your global.R file

+
+
+
global.R
+
# LOAD LIBRARIES ----
 library(shiny)
 library(lterdatasampler) 
 library(tidyverse)
 library(shinyWidgets) 
-

-
-Add packages & wrangle data in global.R
-

-We can also do the bulk of our data wrangling here, rather than in the server (to keep our server code a bit more manageable). If we were reading in a data file (e.g. .csv), we would do that here too. Our new data object clean_trout, will now be available for us to call directly in our server. -

-

-We can easily copy our wrangling code over from our practice script. +

+

Add packages & wrangle data in global.R

+
+

+We can also do the bulk of our data wrangling here, rather than in the server (to keep our server code a bit more manageable). If we were reading in a data file (e.g. .csv), we would do that here too. Our new data object clean_trout, will now be available for us to call directly in our server (NOTE: we can easily copy our wrangling code over from our practice script).

+
+
+
global.R
+
# LOAD LIBRARIES ----
 library(shiny)
 library(lterdatasampler)
@@ -2739,18 +2752,22 @@
   )) |> 
   drop_na()
-

-
-Add a pickerInput for selecting channel_type to your UI
-

+

+

Add a pickerInput for selecting channel_type to your UI

+
+

The channel_type variable (originally called unittype – we updated the name when wrangling data (see line 9 on previous slide)) represents the type of water body (cascade, riffle, isolated pool, pool, rapid, step (small falls), or side channel) data were collected in. We’ll start by building a shinyWidgets::pickerInput() to allow users to filter data based on channel_type.

-

+

Reminder: When we we designed our UI layout, we added a sidebarPanel to our Trout tab with the placeholder text "trout plot input(s) go here". Replace that text with the code for your pickerInput:

+
+
+
ui.R
+
# channel type pickerInput ----
 pickerInput(inputId = "channel_type_input", label = "Select channel type(s):",
             choices = unique(clean_trout$channel_type), # alternatively: choices = c("rapid", "cascade" ...)
@@ -2758,29 +2775,33 @@
             selected = c("cascade", "pool"), 
             multiple = TRUE) # END channel type pickerInput
-

+

+

Save and run your app – a functional pickerInput should now appear in your UI.

-


-

-A shinyWidgets::pickerInput() is functionally equivalent to shiny::selectInput(), though it allows for greater customization and looks (in my opinion) a bit nicer. +

+A shinyWidgets::pickerInput() is functionally equivalent to shiny::selectInput(), though it allows for greater customization and looks (in my opinion) a bit nicer.

-
-Add a plot output to your UI -
-

+

Add a plot output to your UI

+
+

Next, we need to create a placeholder in our UI for our trout scatterplot to live. Because we’ll be creating a reactive plot, we can use the plotOutput() function to do so.

-

+

Reminder: When we we designed our UI layout, we added a mainPanel to our Trout tab with the placeholder text "trout plot output goes here". Replace that text with the code for your plotOuput():

+
+
+
ui.R
+
plotOutput(outputId = "trout_scatterplot")
-

+

+

Save and run your app – it won’t look different at first glance, but inspecting your app in a browser window (using Chrome, right click > Inspect) will reveal a placeholder box for your plot output to eventually live:

@@ -2788,15 +2809,18 @@

-
-Tell the server how to assemble pickerInput values into your plotOutput -
-

+

Tell the server how to assemble pickerInput values into your plotOutput

+
+

Remember the three rules for building reactive outputs: (1) save objects you want to display to output$<id>, (2) build reactive objects using a render*() function, and (3) access input values with input$<id>. When complete, your server should contain the following code:

+
+
+
server.R
+
server <- function(input, output) {
   
   # filter trout data ----
@@ -2825,34 +2849,35 @@
   
 } # END server
+

A couple notes/reminders:

-

-angle-right - If needed, reference your practice script to remind yourself how you planned to filter and plot your data +

+ If needed, reference your practice script to remind yourself how you planned to filter and plot your data

-

-angle-right - Reactive data frames need a set of parentheses, (), following the name of the df (see ggplot(trout_filtered_df() ...)) +

+ Reactive data frames need a set of parentheses, (), following the name of the df (see ggplot(trout_filtered_df() ...))

-

-angle-right - For a cohesive appearance, save your ggplot theme parameters to a named object in global.R (here, myCustomTheme), then apply to all plots in your app. See the following slide for code. +

+ For a cohesive appearance, save your ggplot theme parameters to a named object in global.R (here, myCustomTheme), then apply to all plots in your app. See the following slide for code.

-
-Save a custom ggplot theme to global.R -
-

+

Save a custom ggplot theme to global.R

+
+

Rather than re-typing your ggplot theme parameters out for every plot in your app, do so once in global.R, and save to an object name. You can then easily add your custom theme as a layer to each of your ggplots. Bonus: If you decide to modify your plot theme, you only have to do so in one place!

+
+
+
global.R
+
# LOAD LIBRARIES ----
 library(shiny)
 library(lterdatasampler)
@@ -2888,24 +2913,27 @@
     legend.position = "bottom",
     panel.border = element_rect(colour = "black", fill = NA, linewidth = 0.7))
-

-
-Run your app and try out your pickerInput widget!
+

+

Run your app and try out your pickerInput widget!

+
A user navigates to the 'Explore the Data' tab of our app, 'LTER Animal Data Explorer.' There is a scatterplot with Trout Length (mm) on the x-axis, and Trout Weight (g) on the y-axis. Points are colored and shaped by channel type. There is a pickerWidget to the left of the plot with the channel_types 'cascade' and 'pool' already selected. The user clicks other channel types on/off to see how the data points on the plot change.

-
-Add a second input that will update the same output -
-

+

Add a second input that will update the same output

+
+

You can have more than one input control the same output. Let’s now add a checkboxGroupButtons widget to our UI for selecting forest section (either clear cut forest or old growth forest). Check out the function documentation for more information on how to customize the appearance of your buttons.

-

-Be sure to add the widget to the same sidebarPanel as our pickerInput (and separate them with a comma, ,!: +

+Be sure to add the widget to the same sidebarPanel as our pickerInput (and separate them with a comma!:

+
+
+
ui.R
+
# trout plot sidebarPanel ----
 sidebarPanel(
   
@@ -2925,14 +2953,18 @@
                    
             ) # END trout plot sidebarPanel
-

-
-Update your reactive df to also filter based on the new checkboxGroupInput
-

+

+

Update your reactive df to also filter based on the new checkboxGroupInput

+
+

Return to your server to modify trout_filtered_df – our data frame needs to be updated based on both the pickerInput, which selects for channel_type, and the checkboxGrouptInput, which selects for forest section:

+
+
+
server.R
+
# filter trout data ----
 trout_filtered_df <- reactive({
 
@@ -2958,18 +2990,17 @@
 
  })
-

-
-Run your app and try out your pickerInput & checkboxGrouptInput widgets!
+

+

Run your app and try out your pickerInput & checkboxGrouptInput widgets!

+
Our updated app, which includes the same trout scatterplot, but this time, two inputs: one selectInput that allows the user to filter for channel_type, and one checkboxGroupInput, which includes two buttons to select/deselect data collected from the clear cut forest section and the old growth forest section.

-Mug Hot - Break + Break

Up next: your turn to add a reactive penguin plot to our “Penguin” tab! @@ -2977,7 +3008,7 @@

-
+
@@ -2986,45 +3017,36 @@

-

-Add data viz: Next up, penguins -

-

+

Add data viz: Next up, penguins

+
+

We’ll be using the penguins dataset from the {palmerpenguins} package to create our second reactive plot. These data contain penguin (genus Pygoscelis) body size measurements collected from three islands in the Palmer Archipelago, Antarctica, as part of the Palmer Station LTER. Original data can be found on the EDI Data Portal (Adélie data, Gentoo data, and Chinstrap data). Refer back to this slide to revisit our practice data wrangling & visualization script.


-
-A cartoon drawing of Chinstrap (atop a purple background), Gentoo (atop a green background) and Adélie (atop an orange background) penguins. -
-
-

+ +A cartoon drawing of Chinstrap (atop a purple background), Gentoo (atop a green background) and Adélie (atop an orange background) penguins.

+

Artwork by @allison_horst

-
-Question - Exercise 3: Add a reactive plot to the ‘Penguins’ tab -
-

+

Exercise 3: Add a reactive plot to the ‘Penguins’ tab

+
+

Working alone or in groups, add a reactive histogram of penguin flipper lengths (using the penguins data set from the {palmerpenguins} package) to the Penguins tab. Your plot should have the following features and look like the example below, when complete:

-

-angle-right - data colored by penguin species +

+ data colored by penguin species

-

-angle-right - a shinyWidgets::pickerInput that allows users to filter data based on island, and that includes buttons to Select All / Deselect All island options at once +

+ a shinyWidgets::pickerInput that allows users to filter data based on island, and that includes buttons to Select All / Deselect All island options at once

-

-angle-right - a shiny::sliderInput that allows users to change the number of histogram bins and that by default, displays a histogram with 25 bins +

+ a shiny::sliderInput that allows users to change the number of histogram bins and that by default, displays a histogram with 25 bins

-

-angle-right - the two widgets should be placed in the sidebarPanel and the reactive histogram should be placed in the mainPanel of the Penguins tab +

+ the two widgets should be placed in the sidebarPanel and the reactive histogram should be placed in the mainPanel of the Penguins tab

@@ -3035,44 +3057,37 @@
-

+

See next slide for some tips on getting started!

-
-Lightbulb - Exercise 3: Tips -
+

Exercise 3: Tips

+

Tips:

-

-angle-right - Remember to load the palmerpenguins package at the top of global.R so that your app can find the data +

+ Remember to load the palmerpenguins package at the top of global.R so that your app can find the data

-

-angle-right - Add your widgets to the sidebarPanel and your plot output to the mainPanel of the Penguins tab – look for that placeholder text we added earlier to help place your new code in the correct spot within your UI! +

+ Add your widgets to the sidebarPanel and your plot output to the mainPanel of the Penguins tab – look for that placeholder text we added earlier to help place your new code in the correct spot within your UI!

-

-angle-right - Try changing the histogram bin number in your practice code script first, before attempting to make it reactive +

+ Try changing the histogram bin number in your practice code script first, before attempting to make it reactive

And remember to follow the our three steps for building reactive outputs (1. add input to UI, 2. add output to UI, 3. tell server how to assemble inputs into outputs)!




-

+

See next slide for a solution!

-
-Check - Exercise 3: A solution -
+

Exercise 3: A solution

+

-Mug Hot - Break + Break

Next, we’ll finish up v1 of our app by adding some intro text to the landing page @@ -3280,7 +3294,7 @@

-
+
@@ -3289,9 +3303,8 @@

-
-Lastly: add background/other important information text -
+

Lastly: add background/other important information text

+

It’s usually valuable (and important) to provide some background information/context for your app – the landing page of your app can be a great place for this. We’re going to add text to our app’s landing page (i.e. the About this App page) so that it looks like the example below:

@@ -3307,28 +3320,28 @@

Some important pieces for information to consider adding:

-

-angle-right - motivation for building the app +

+ motivation for building the app

-

-angle-right - brief instructions for exploring the data +

+ brief instructions for exploring the data

-

-angle-right - who maintains the app, where the code lives, how to submit issues/suggestions +

+ who maintains the app, where the code lives, how to submit issues/suggestions

-
-Adding long text to the UI can get unruly -
-

+

Adding long text to the UI can get unruly

+
+

For example, I’ve added and formatted my landing page’s text directly in the UI using lots of nested tags – I’ve done this inside the tabPanel titled About this App (Note: I’ve formatted the layout of this page a bit using fluidRow and columns to create some white space around the edges. I’ve also created a faint gray horizontal line, using hr(), beneath which I added a footnote):

+
+
+
ui.R
+
ui <- navbarPage(
   
   title = "LTER Animal Data Explorer",
@@ -3437,14 +3450,14 @@
   
 ) # END navbarPage
-

-
-Instead, use includeMarkdown() to read in text from separate .md files
-

+

+

Instead, use includeMarkdown() to read in text from separate .md files

+
+

To maintain readability and an overall tidier-looking UI, you can write and style long bodies of text in separate markdown (.md) files that you then read into your UI using the includeMarkdown() function (Important: the includeMarkdown() function requires the markdown package – be sure to add library(markdown) to your global.R file!).

-

+

I recommend saving those .md files in a subdirectory named /text within your app’s directory (e.g. ~/two-file-app/text/mytext.md). See how I simplified my UI by saving my long landing page text to two new files, about.md and footer.md, then imported them into my UI using includeMarkdown().

@@ -3456,6 +3469,10 @@
+
+
+
ui.R
+
ui <- navbarPage(
   
   title = "LTER Animal Data Explorer",
@@ -3562,8 +3579,13 @@
 ) # END navbarPage
+
+
+
+
text/about.md
+
## Welcome to the LTER Animal Data Explorer!
 
 <br>
@@ -3579,28 +3601,30 @@
 Check out the **Explore the Data** page to find interactive data visualizations looking at Cutthroat trout of the [Andrews Forest LTER](https://andrewsforest.oregonstate.edu/) and Adelie, Gentoo & Chinstrap penguins of the [Palmer Station LTER](https://pallter.marine.rutgers.edu/).
+
+
+
+
text/footer/md
+
*This app is maintained by [Samantha Csik](https://samanthacsik.github.io/) and is updated as needed for teaching purposes. Please report any issues [here](https://github.com/samanthacsik/EDS430-shiny-app/issues). Source code can be found on [GitHub](https://github.com/samanthacsik/EDS430-shiny-app).*
-

-
-Run your app one more time to admire your beautiful creation!
-
-An app titled 'LTER Animal Data Explorer'. The user starts on the 'About this App' page, which includes a couple paragraphs of intro text. The user then clicks on the 'Explore the Data' page which has two tabs: Trout and Penguins. On the Trout tab, we see a scatterplot with Trout Length (mm) on the x-axis and Trout Weight (g) on the y-axis. The user first filters data by Channel Type using a drop down list selector widget, then by Forest Section using two buttons that toggle on data from clear cut vs. old growth forest. In the Penguin tab, we see a histogram displaying Flipper Lengths (mm) of three penguin species. The user uses a drop down list selector widget to filter data by Island, then uses a slider widget to adjust the number of histogram bins. -
-

+

+

Run your app one more time to admire your beautiful creation!

+
+ +An app titled 'LTER Animal Data Explorer'. The user starts on the 'About this App' page, which includes a couple paragraphs of intro text. The user then clicks on the 'Explore the Data' page which has two tabs: Trout and Penguins. On the Trout tab, we see a scatterplot with Trout Length (mm) on the x-axis and Trout Weight (g) on the y-axis. The user first filters data by Channel Type using a drop down list selector widget, then by Forest Section using two buttons that toggle on data from clear cut vs. old growth forest. In the Penguin tab, we see a histogram displaying Flipper Lengths (mm) of three penguin species. The user uses a drop down list selector widget to filter data by Island, then uses a slider widget to adjust the number of histogram bins.

Again, we have some UX/UI quirks to fix (most notably, blank plots when all widget options are deselected), which we’ll handle soon. But for now, we have a functioning app that we can practice deploying for the first time!

-
-Code recap for app #2v1, so far: -
-

+

Code recap for app #2v1, so far:

+
+

Additionally, you should have a /text folder within your app’s directory (/two-file-app, if you named it as I did) that contains two markdown files, about.md and footer.md.

@@ -3814,71 +3838,57 @@

-Upload - Deploying apps with shinyapps.io + Deploying apps with shinyapps.io

Sharing your Shiny app with others isn’t so easy when it just lives on your computer (and your R session has to act as the server that powers it). We’ll now learn how to host your app using shinyapps.io, a free service for sharing your Shiny apps online.

-
-Connect your shinyapps.io account to RStudio -
-

+

Connect your shinyapps.io account to RStudio

+
+

Go to shinyapps.io and login or create an account (if you don’t already have one) – I created my account and login with GitHub. To use shinyapps.io, you first need to link your account with RStudio on your computer. Follow the instructions on shinyapps.io when you first create your account to install the {rsconnect} package and authorize your account:

-
-shinyapps.io displays setup instructions: (1) INSTALL RSCONNECT: The `rsconnect` package can be installed directly from CRAN. To make sure you have the latest version run following code in your R console: `install.packages('rsconnect')`. (2) AUTHORIZE ACCOUNT: The `rsconnect` package must be authorized to your account using a token and secret. To do this, click the copy button below and we'll copy the whole command you need to your clipboard. Just paste it into your console to authorize your account. Once you've entered the command successfully in R, that computer is now authorized to deploy applications to your shinyapps.io account. (3) DEPLOY: Once the `rsconnect` package has been configured, you're ready to deploy your first application. If you haven't written any applications yet, you can also checkout the Getting Started Guide for instruction son how to deploy our demo application. Run the following code in your R console: `library(rsconnect)`, then `rsconnect::deployApp('path/to/your/app')`. -
-

-
-Deploy your app to shinyapps.io -
-

+ +shinyapps.io displays setup instructions: (1) INSTALL RSCONNECT: The `rsconnect` package can be installed directly from CRAN. To make sure you have the latest version run following code in your R console: `install.packages('rsconnect')`. (2) AUTHORIZE ACCOUNT: The `rsconnect` package must be authorized to your account using a token and secret. To do this, click the copy button below and we'll copy the whole command you need to your clipboard. Just paste it into your console to authorize your account. Once you've entered the command successfully in R, that computer is now authorized to deploy applications to your shinyapps.io account. (3) DEPLOY: Once the `rsconnect` package has been configured, you're ready to deploy your first application. If you haven't written any applications yet, you can also checkout the Getting Started Guide for instruction son how to deploy our demo application. Run the following code in your R console: `library(rsconnect)`, then `rsconnect::deployApp('path/to/your/app')`.

+

Deploy your app to shinyapps.io

+
+

Once your account has been authorized, run rsconnect::deployApp("<app_directory_name>") in your console to deploy your app to shinyapps.io. Here, we’ll run rsconnect::deployApp("two-file-app") to deploy the app we’ve been working on.

-

+

Once deployed, a browser will open to your application. The URL will take the form: https://username.shinyapps.io/your_app_directory_name. You should also now see an /rsconnect folder within your app’s directory – this is generated when an application bundle is successfully deployed and contains a DCF file with information on the deployed content (i.e. the name, title, server address, account, URL, and time). This /rsconnect folder should be added and committed into version control (i.e. push it to GitHub!) so that future re-deployments target the same endpoint (i.e. your app’s URL).

-
-Our shiny app, now hosted at the URL https://samanthacsik.shinyapps.io/two-file-app which functions the same as when it was hosted locally. -
-

-
-The shinyapps.io dashboard -
-

+ +Our shiny app, now hosted at the URL https://samanthacsik.shinyapps.io/two-file-app which functions the same as when it was hosted locally.

+

The shinyapps.io dashboard

+
+

Your shinyapps.io dashboard provides tons of information about your application metrics, instance (the virtualized server that your app is hosted on) and worker (a special type of R process that an Application Instance runs to service requests to an application) settings, plan management, and more. The free plan (the plan we’re using here today) allows you to deploy five Shiny apps. You are able to archive and/or delete once-deployed apps to make space for new ones.

-
-The shinyapps.io dashboard displaying information about our two-file-app, now hosted at https://samanthacsik.shinyapps.io/two-file-app/. You can select different menu items from the navbar at the top of the page, including Overview, Metrics, URLs, Settings, Users, Logs, Restart, Archive, and Trash. -
-

+ +The shinyapps.io dashboard displaying information about our two-file-app, now hosted at https://samanthacsik.shinyapps.io/two-file-app/. You can select different menu items from the navbar at the top of the page, including Overview, Metrics, URLs, Settings, Users, Logs, Restart, Archive, and Trash.

Check out the shinyapps.io user guide for more information on hosting your apps on shinyapps.io.

-
-Other ways to host your Shiny apps -
-

+

Other ways to host your Shiny apps

+
+

shinyapps.io is not the only Shiny app hosting service (though it’s the easiest to get started with and the only one we’ll be covering in detail in this workshop).

Posit also offers the following:

-

-angle-right -Shiny server is an open source server which you can deploy for free on your own hardware. It requires more setup and configuration, but can be used without a fee. The Bren and NCEAS servers are configured with Shiny Server for hosting for some in-house apps. +

+ Shiny server is an open source server which you can deploy for free on your own hardware. It requires more setup and configuration, but can be used without a fee. The Bren and NCEAS servers are configured with Shiny Server for hosting for some in-house apps.

-

-angle-right -Posit connect is a paid product that provides an advanced suite of services for hosting Shiny apps, Quarto and R Markdown reports, APIs, and more. +

+ Posit connect is a paid product that provides an advanced suite of services for hosting Shiny apps, Quarto and R Markdown reports, APIs, and more.

-
-So how should I host my app? -
+

So how should I host my app?

+

The Bren compute team will work with groups to deploy and maintain apps on in-house servers for up to 6 months after capstone/GP presentations or until they break.

@@ -3888,90 +3898,80 @@

-

-angle-right -Preferred: Stick with the free tier of shinyapps.io, if you can! This is by far the most straightforward option that requires no server maintenance for you or your client. If your app exceeds the limitations set by the free tier (e.g. requires more active hours, needs more RAM or instances to support high traffic usage, etc.), you/your client have the option to upgrade to a paid tier – there are 5 paid plan types. Check out the shinyapps.io user guide for more information. Consider setting aside your allocated capstone/GP funds to help support a paid shinyapps.io plan. +

+ Preferred: Stick with the free tier of shinyapps.io, if you can! This is by far the most straightforward option that requires no server maintenance for you or your client. If your app exceeds the limitations set by the free tier (e.g. requires more active hours, needs more RAM or instances to support high traffic usage, etc.), you/your client have the option to upgrade to a paid tier – there are 5 paid plan types. Check out the shinyapps.io user guide for more information. Consider setting aside your allocated capstone/GP funds to help support a paid shinyapps.io plan.

-

-angle-right - If you have a server-savvy client, they may want to deploy/host your app using their own infrastructure. If your client plans to pursue this option, but does not yet have a their own server configured to do so, we recommend directing them to the online instructions for getting started with Shiny Server. PLEASE NOTE that Bren staff (including the compute team) are unable to provide technical support for clients in server configuration and app deployment/maintenance. +

+ If you have a server-savvy client, they may want to deploy/host your app using their own infrastructure. If your client plans to pursue this option, but does not yet have a their own server configured to do so, we recommend directing them to the online instructions for getting started with Shiny Server. PLEASE NOTE that Bren staff (including the compute team) are unable to provide technical support for clients in server configuration and app deployment/maintenance.

-

+

IMPORTANT: Hosting on a server means that shiny applications will be prone to breaking as updates to server software are made. It is important to have an application maintenance plan in place. This may mean identifying who is responsible for maintaining code, or even deciding to decommission applications and archive the code repository when appropriate.

-Users - Improving user experience + Improving user experience

Our two-file-app is looking pretty good! It’s functional and deployed via shinyapps.io. Next, we’ll focus on making some minor tweaks that can help to improve usability

-
-Book Open - Learning Objectives - App #2v2 (two-file app) -
-

+

Learning Objectives - App #2v2 (two-file app)

+
+

By the end of this section, you should:

-

-angle-right - understand how to provide users with helpful error messages using validate() +

+ understand how to provide users with helpful error messages using validate()

-

-angle-right - know how to add customizable loading animations to alert users when reactive objects are re-rendering +

+ know how to add customizable loading animations to alert users when reactive objects are re-rendering

-

-angle-right - know how to add alternate (alt) text to rendered plots +

+ know how to add alternate (alt) text to rendered plots

-

-angle-right - understand how to republish an app using shinyapps.io +

+ understand how to republish an app using shinyapps.io

-

+

Packages introduced:

-

+

Box Open shinycssloaders: add loading animations to shiny outputs

-
-Roadmap for App #2v2 -
+

Roadmap for App #2v2

+

We’ll be refining our two-file app with a focus on creating a more user-friendly experience. When finished with v2, we’ll have added:


-

+

(a) user-friendly validation error messages that appear when widgets are used to deselect all data

-

+

(b) loading animations for both two reactive plots

-

+

(c) alternate (alt) text for all data visualizations

@@ -3983,18 +3983,17 @@

-
-Take out any guesswork for your app’s users -
+

Take out any guesswork for your app’s users

+

It’s important to remove any possible points of confusion for successfully using your app.

-

+

In version 1 of our published app, you’ll notice that users are able to (1) Deselect All data using the pickerInputs for both the trout and penguin plots, and (2) “uncheck” both clear cut forest and old growth forest sampling section buttons using the checkboxGroupInput. When any of these actions are taken by the user, all data are removed from the plot, leaving a completely blank box behind.

-

+

While this response is expected (and normal), we can generate a user-friendly validation error message to provide clear guidance to our users on what is expected by the app in order to display data.

@@ -4006,16 +4005,19 @@

-
-Writing validation tests -
-

+

Writing validation tests

+
+

validate() tests a condition and returns an error if that conditions fails. It’s used in conjunction with need(), which takes an expression that returns TRUE or FALSE, along with a character string to return if the condition is FALSE.

-

+

Place your validation test(s) at the start of any reactive() or render*() expression that calls input$data. For example, we can add two validation tests inside the reactive that generates our trout_filtered_df – we’ll need two separate validation tests, one for each of our inputs where users can deselect all data.

+
+
+
server.R
+
server <- function(input, output) {
   
   # filter for channel types ----
@@ -4033,40 +4035,38 @@
     })
 }
-

-
-Question - & Lightbulb - Exercise 4: Add a validation test for your penguin histogram
+

+

& Exercise 4: Add a validation test for your penguin histogram

+


To Do:

-

-angle-right - Construct a validation test that displays a clear but succinct message when a user deselects all islands using the pickerWidget +

+ Construct a validation test that displays a clear but succinct message when a user deselects all islands using the pickerWidget


Tips:

-

-angle-right - Despite having two inputs, we only need one validation test for our Penguins plot. Why is this? +

+ Despite having two inputs, we only need one validation test for our Penguins plot. Why is this?



-

+

See next slide for a solution!

-
-Check - Exercise 4: A solution -
+

Exercise 4: A solution

+
+
+
+
server.R
+
server <- function(input, output) {
   
   # filter for channel types ----
@@ -4128,29 +4128,33 @@
   
 } # END server
-

-
-Add loading animations to re-rendering outputs
-

+

+

Add loading animations to re-rendering outputs

+
+

The {shinycssloaders} package makes it easy to add visual indicators to outputs as they’re loading or re-rendering. This can be particularly helpful if you have outputs that take a few seconds to render – it alerts users that their updated inputs were recognized and that the app is working to re-render outputs.

-

+

Check out the demo app to start designing your own “spinner” (choose style, color, size).

-

+

We can pipe the withSpinner() function directly into our plotOutputs in ui.R (be sure to load the package in global.R first) – here, we define the spinner color and style (there are 8 different spinner types to choose from) and adjust the size of the penguin plot spinner.


-
# in ui.R
-plotOutput(outputId = "trout_scatterplot") |> 
-  withSpinner(color = "#006792", type = 1)
-
-plotOutput(outputId = "flipperLength_histogram") |> 
-  withSpinner(color = "#4BA4A4", type = 4, size = 2)
+
+
+
ui.R
+
+
plotOutput(outputId = "trout_scatterplot") |> 
+  withSpinner(color = "#006792", type = 1)
+
+plotOutput(outputId = "flipperLength_histogram") |> 
+  withSpinner(color = "#4BA4A4", type = 4, size = 2)
+
@@ -4161,94 +4165,95 @@

-
-Include alt text for all data visualizations -
-

+

Include alt text for all data visualizations

+
+

Alt text are written descriptions added to images, and importantly, to data visualizations, to help more users understand the content. Assistive technologies (e.g. screen readers) read alt text out loud for users to hear. When alt text is successfully added, the alt tag (along with your text) should appear in the HTML (right click on your app’s data viz to Inspect and ensure that it was added).



-

+

We’ll talk a bit more about alt text later on, but for now we can add alt text easily to our data visuzliations using the alt argument. Place this outside of the {} but inside the () of renderPlot{()}. For example, we can add alt text to our trout and penguin plots in server.R:

-
# in server.R
-# render trout scatterplot ----
-output$trout_scatterplot <- renderPlot({
-
-  ggplot(trout_filtered_df(), aes(x = length_mm, y = weight_g, color = channel_type, shape = channel_type)) +
-      geom_point(alpha = 0.7, size = 5) +
-      scale_color_manual(values = c("cascade" = "#2E2585", "riffle" = "#337538", "isolated pool" = "#DCCD7D",
-                                    "pool" = "#5DA899", "rapid" = "#C16A77", "step (small falls)" = "#9F4A96",
-                                    "side channel" = "#94CBEC")) +
-      scale_shape_manual(values = c("cascade" = 15, "riffle" = 17, "isolated pool" = 19,
-                                    "pool" = 18, "rapid" = 8, "step (small falls)" = 23,
-                                    "side channel" = 25)) +
-      labs(x = "Trout Length (mm)", y = "Trout Weight (g)", color = "Channel Type", shape = "Channel Type") +
-      myCustomTheme
-
- },
-  
-  alt = "A scatterplot of the relationship between cutthroat trout lengths (mm) (x-axis) and weights (g) (y-axis), with data points colored and shaped based on the water channel type from which they were collected. Trout tend to be longer, but weight less in waterways within the old growth forest. Trout tend to be shorter, but weight more in waterways within the clear cut forest."
-  
-) # END render trout scatterplot
+
+
+
server.R
+
+
# render trout scatterplot ----
+output$trout_scatterplot <- renderPlot({
+
+  ggplot(trout_filtered_df(), aes(x = length_mm, y = weight_g, color = channel_type, shape = channel_type)) +
+      geom_point(alpha = 0.7, size = 5) +
+      scale_color_manual(values = c("cascade" = "#2E2585", "riffle" = "#337538", "isolated pool" = "#DCCD7D",
+                                    "pool" = "#5DA899", "rapid" = "#C16A77", "step (small falls)" = "#9F4A96",
+                                    "side channel" = "#94CBEC")) +
+      scale_shape_manual(values = c("cascade" = 15, "riffle" = 17, "isolated pool" = 19,
+                                    "pool" = 18, "rapid" = 8, "step (small falls)" = 23,
+                                    "side channel" = 25)) +
+      labs(x = "Trout Length (mm)", y = "Trout Weight (g)", color = "Channel Type", shape = "Channel Type") +
+      myCustomTheme
+
+ },
+  
+  alt = "A scatterplot of the relationship between cutthroat trout lengths (mm) (x-axis) and weights (g) (y-axis), with data points colored and shaped based on the water channel type from which they were collected. Trout tend to be longer, but weight less in waterways within the old growth forest. Trout tend to be shorter, but weight more in waterways within the clear cut forest."
+  
+) # END render trout scatterplot
+
 
-
-# render flipperLength hisogram ----
-output$flipperLength_histogram <- renderPlot({
-
-  ggplot(na.omit(island_df()), aes(x = flipper_length_mm, fill = species)) +
-    geom_histogram(alpha = 0.6, bins = input$bin_num) +
-    scale_fill_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) +
-    labs(x = "Flipper length (mm)", y = "Frequency",
-         fill = "Penguin species") +
-    myCustomTheme
-    
- },
-  
-  alt = "A histogram of penguin flipper lengths (mm), with data colored by penguin species. Flipper lengths tend to be smallest on Adélie penguins and largest on Gentoo penguins."
-  
-) # END render flipperLength histogram
+# render flipperLength hisogram ---- +output$flipperLength_histogram <- renderPlot({ + + ggplot(na.omit(island_df()), aes(x = flipper_length_mm, fill = species)) + + geom_histogram(alpha = 0.6, bins = input$bin_num) + + scale_fill_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) + + labs(x = "Flipper length (mm)", y = "Frequency", + fill = "Penguin species") + + myCustomTheme + + }, + + alt = "A histogram of penguin flipper lengths (mm), with data colored by penguin species. Flipper lengths tend to be smallest on Adélie penguins and largest on Gentoo penguins." + +) # END render flipperLength histogram
+

-Upload - Redeploying apps with shinyapps.io + Redeploying apps with shinyapps.io

Now that we’ve added some updates to our app, it’s time to redeploy our newest version.

-
-Redeploying is similar to deploying for the first time -
+

Redeploying is similar to deploying for the first time

+

Just a few quick steps before your updates are live at your URL from earlier:

-

+

1. double check to make sure any required packages are being imported in global.R

-

+

2. Rerun rsconnect::deployApp("<app-directory-nam")> in your console and type Y when you see the prompt Update application currently deployed at https://githubUserName.shinyapps.io/yourAppName? [Y/n]

-

+

3. Give it a minute (or few) to deploy. Your deployed app will open in a browser window once processed

-

+

4. Push all your files (including the /rsconnect directory) to GitHub


-

+

Check out my deployed app at https://samanthacsik.shinyapps.io/two-file-app/

@@ -4264,7 +4269,7 @@
-
+
@@ -4275,66 +4280,56 @@

-Gauge med - Building dashboards with shinydashboard + Building dashboards with shinydashboard

Shiny alone is powerful and flexible, however it can take a lot of work to create a sleek/modern UI. shinydashboard provides a “template” for quickly building visually appealing dashboard apps.

-
-Book Open - Learning Objectives - App #3 (shinydashboard) -
-

+

Learning Objectives - App #3 (shinydashboard)

+
+

After this section, you should:

-

-angle-right - understand the general workflow for pre-processing, saving & reading data into an app +

+ understand the general workflow for pre-processing, saving & reading data into an app

-

-angle-right - be comfortable building out a dashboard UI using shinydashboard layout functions +

+ be comfortable building out a dashboard UI using shinydashboard layout functions

-

-angle-right - understand how to add static images to your app +

+ understand how to add static images to your app

-

-angle-right - feel comfortable creating a basic reactive leaflet map +

+ feel comfortable creating a basic reactive leaflet map

-

+

Packages introduced:

-

-Box Open -shinydashboard: provides an alternative UI framework for easily building dashboard-style shiny applications +

+ shinydashboard: provides an alternative UI framework for easily building dashboard-style shiny applications

-

-Box Open -leaflet: for building interactive maps +

+ leaflet: for building interactive maps

-
-Roadmap for App #3 -
+

Roadmap for App #3

+

In this section, we’ll be building a shinydashboard using data downloaded from the Arctic Data Center. We’ll be building out the following features:

@@ -4342,16 +4337,16 @@


-

+

(a) a dashboardHeader with the name of your app

-

+

(b) a dashboardSidebar with two menuItems

-

+

(c) a landing page with background information about your app

-

+

(d) an interactive and reactive leaflet map

@@ -4363,9 +4358,8 @@

-
-But first, what do we mean by a shiny “dashboard”? -
+

But first, what do we mean by a shiny “dashboard”?

+

shinydashboard is just an alternative framework for building shiny apps. In other words, shiny dashboards are just shiny apps, but with some different UI elements that make building apps with a classic “dashboard” feel to them a little bit easier.

@@ -4373,10 +4367,9 @@ A simple shinydashboard with two boxes (one containing a histogram and one containing a sliderInput) in the body. The header reads 'Basic tabs' and the sidebar has two menu items: Dashboard and Widgets.

-
-The most basic shinydashboard is made up of a header, a sidebar, and a body -
-

+

The most basic shinydashboard is made up of a header, a sidebar, and a body

+
+

The main difference between a shiny app and a shinydashboard are the UI elements. Rather than a fluidPage() (as used in our previous shiny apps), we’ll create a dashboardPage(), which expects three main parts: a header, a sidebar, and a body. Below is the most minimal possible UI for a shinydashboard page (you can run this code in an app.R file, if you wish).

@@ -4410,41 +4403,33 @@

-
-Example shiny dashboards built by some familiar folks Face Smile - -
-

-School -Bren Student Data Explorer (source code), by MEDS 2022 alum, Halina Do-Linh, during her Bren Summer Fellowship (and continued by future MEDS students!) – explore Bren school student demographics and career outcomes +

Example shiny dashboards built by some familiar folks

+
+

+ Bren Student Data Explorer (source code), by MEDS 2022 alum, Halina Do-Linh, during her Bren Summer Fellowship (and continued by future MEDS students!) – explore Bren school student demographics and career outcomes

-

-Person hiking -Sam’s Strava Stats (source code), by yours truly, Sam Csik – a new and ongoing side project exploring my Strava hiking/biking/walking data +

+ Sam’s Strava Stats (source code), by yours truly, Sam Csik – a new and ongoing side project exploring my Strava hiking/biking/walking data

-

-Fish -Channel Islands National Park’s Kelp Forest Monitoring Program (source code), by MEDS 2022 alum, Cullen Molitor – explore subtidal monitoring data collected from our closest National Park +

+ Channel Islands National Park’s Kelp Forest Monitoring Program (source code), by MEDS 2022 alum, Cullen Molitor – explore subtidal monitoring data collected from our closest National Park

-

-Campground -The Outdoor Equity App (source code), developed by MEDS 2022 alumni Halina Do-Linh & Clarissa Boyajian as part of their MEDS capstone project – analyze patterns in the access and demand of visitors at reservable overnight sites +

+ The Outdoor Equity App (source code), developed by MEDS 2022 alumni Halina Do-Linh & Clarissa Boyajian as part of their MEDS capstone project – analyze patterns in the access and demand of visitors at reservable overnight sites

-

-Fish Fins -Visualizing human impacts on at-risk marine biodiversity (source code, developed by MESM 2022 alum, Ian Brunjes & Dr. Casey O’Hara) – explore how human activities and climate change impact marine biodiversity worldwide +

+ Visualizing human impacts on at-risk marine biodiversity (source code, developed by MESM 2022 alum, Ian Brunjes & Dr. Casey O’Hara) – explore how human activities and climate change impact marine biodiversity worldwide

-
-Setup our shiny dashboard -
+

Setup our shiny dashboard

+

First, create a subdirectory called /shinydashboard and add a ui.R, server.R, and global.R file.

-

+

Add the server function to server.R and the three main UI components (header, sidebar, and body) to our dashboard page. You can do so just as the example a few slides back, or alternatively, you can split the UI into separate pieces, then combine them into a dashboardPage the end of ui.R (as shown below) – this can help with organization as you app grows in complexity.

-

+

We’ll set our dashboard aside for now while we work on downloading and pre-processing our data, as well as practice creating our data visualization outside of our app.

@@ -4484,9 +4469,8 @@

-
-As always, let’s start with the data -
+

As always, let’s start with the data

+

Building an app doesn’t make much sense if we don’t know what we’re going to put in it. So, just like the last two apps, we’ll start with some data wrangling and practice data visualization.

@@ -4497,7 +4481,7 @@


-

+

Christopher Arp, Matthew Whitman, Katie Drew, and Allen Bondurant. 2022. Water depth, surface elevation, and water temperature of lakes in the Fish Creek Watershed in northern Alaska, USA, 2011-2022. Arctic Data Center. doi:10.18739/A2JH3D41P.

@@ -4510,35 +4494,37 @@

-
-Pre-processing data is critical -
-

+

Pre-processing data is critical

+
+

Where you choose to store the data used by your Shiny app will depend largely on the type and size of the file(s) and who “owns” those data. It is likely that you’ll be working with data stored in a database or on a server. This is outside the scope of this workshop, but I suggest reading Dean Attali’s article, Persistent data storage in Shiny apps to start. Because we are going to be working with a relatively small data set, we’ll be downloading and storing our data locally (i.e. on our machines and in our GitHub repo).

-

+

Regardless of where you choose to store your data, you can help your application more quickly process inputs/outputs by providing it only as much data as needed to run. This means pre-processing your data.

-


-

+

FCWO_lakemonitoringdata_2011_2022_daily.csv contains 8 attributes (variables) and 18,894 observations collected from a set of 11 lakes located in the Fish Creek Watershed in northern Alaska between 2011-2022. We’ll download and save the file to a raw_data/ folder in the root directory of our repository. We’ll then pre-process the data in a separate script(s) saved to scratch/ and save a cleaned/processed version of the data to our app’s directory, /shinydashboard/data/lake_data_processed.csv. Your repository structure should look similar to example on the right:

-
-A schematic of the repository structure, as described. -
+
+
+
+

A schematic of the repository structure, as described.

+
+
-

-
-The Goal:
+ +

+

The Goal:

+
@@ -4554,13 +4540,16 @@

-
-Process lake data & save new file -
-

+

Process lake data & save new file

+
+

NOTE: In this example exercise, I’ve removed all rows with missing values (i.e. NaNs in the Depth column & NAs in the BedTemperature column) before calculating averages. This is NOT good practice – exploring and thinking critically about missing data is an important part of data analysis, and in a real-life scenario, you should consider the most appropriate method for handling them.

+
+
+
scratch/data_processing_app3_shinydashboard.R
+
#....................SETUP & DATA PROCESSING.....................
 
 # load packages ----
@@ -4591,24 +4580,23 @@
 # save processed data to your app's data directory ----
 write_csv(unique_lakes, "shinydashboard/data/lake_data_processed.csv")
-

-
-A note on file types
-

+

+

A note on file types

+
+

Oftentimes, you may choose to save your processed data frame as a .rds file (a data file format, native to R, which stores a single R object). .rds file are relatively small (and therefore take up little storage space), take less time to import/export, and preserve data types and classes (e.g. factors and dates), eliminating the need to redefine data types after loading the file. Bear in mind that this increased speed and space-saving may come at the cost of generality – you can’t open a .rds file outside of R or read it in with another programming language (e.g. Python).

-

+

While we’ll be sticking to .csv files in this workshop, it’s worth experimenting with .rds when you begin working with your own (likely larger) data. You can read in (readRDS()) and write out to (saveRDS()) .rds files as easily as .csv files.

The R language logo overlaid on top of a database stack -- often used to represent the rds data file format.

-
-Draft leaflet map -
-

+

Draft leaflet map

+
+

There are lots of ways to customize leaflet maps. We’ll be keeping ours relatively simple, but check out the Leaflet for R documentation for more ways to get creative with your maps.

@@ -4620,13 +4608,17 @@
-
- +
+
+
+
+
scratch/practice_script_app3_shinydashboard.R
+
#....................SETUP & DATA PROCESSING.....................
 
 # omitted for brevity (see slide 112 for code)
@@ -4655,11 +4647,11 @@
 
-

-
-Practice filtering leaflet observations
-

+

+

Practice filtering leaflet observations

+
+

We’ll eventually build three sliderInputs to filter lake makers by Elevation, AvgDepth, and AvgTemp. Practice filtering here first (and be sure to update the data frame name in your leaflet code!):

@@ -4671,13 +4663,17 @@
-
- +
+
+
+
+
scratch/practice_script_app3_shinydashboard.R
+
#....................SETUP & DATA PROCESSING.....................
 
 # omitted for brevity (see slide 112 for code)
@@ -4714,11 +4710,11 @@
 
-

-
-Sketch out our dashboard UI
-

+

+

Sketch out our dashboard UI

+
+

I want my dashboard to have two menu items: a welcome page with some background information, and a dashboard page with my reactive map. All elements will be placed inside boxes, the primary building blocks of shinydashboards (more on that soon).


@@ -4734,16 +4730,19 @@

-
-Add a title & menuItems -
-

+

Add a title & menuItems

+
+

First, add a title to dashboardHeader() and make more space using titleWidth, if necessary.

-

+

Next, we’ll build our dashboardSidebar(). Add a sidebarMenu() that contains two menuItems. Be sure to provide each menuItem() with text as you’d like it to appear in your app (for me, that’s Welcome and Dashboard), and a tabName which will be used to place dashboardBody() content in the appropriate menuItem(). Optionally, you can provide an icon. By default, icon() uses icons from FontAwesome.

+
+
+
ui.R
+
#........................dashboardHeader.........................
 header <- dashboardHeader(
   
@@ -4772,14 +4771,18 @@
 #..................combine all in dashboardPage..................
 dashboardPage(header, sidebar, body)
-

-
-Add tabItems to your dashboardBody
-

+

+

Add tabItems to your dashboardBody

+
+

Next, we’ll create tabItems in our dashboardBody – we’ll make a tabItem (singular) for each menuItem in our dashboardSidebar. In order to match a menuItem and a tabItem, ensure that they have matching a tabName (e.g. any content added to the dashboard tabItem will appear under the dashboard menuItem).

+
+
+
ui.R
+
#........................dashboardHeader.........................
 header <- dashboardHeader(
   
@@ -4829,14 +4832,18 @@
 #..................combine all in dashboardPage..................
 dashboardPage(header, sidebar, body)
-

-
-Add boxes to contain UI content (part 1)
-

+

+

Add boxes to contain UI content (part 1)

+
+

Boxes are the primary building blocks of shinydashboards and can contain almost any Shiny UI element (e.g. text, inputs, outputs). Start by adding two side-by-side boxes to our dashboard tab inside a fluidRow(). Together, their widths will add up to 12 (the total width of a browser page). These boxes will eventually contain our sliderInputs and our leafletOutput.

+
+
+
ui.R
+
#........................dashboardHeader.........................
 header <- dashboardHeader(
   
@@ -4903,14 +4910,18 @@
 #..................combine all in dashboardPage..................
 dashboardPage(header, sidebar, body)
-

-
-Add boxes to contain UI content (part 2)
-

+

+

Add boxes to contain UI content (part 2)

+
+

Lastly, add boxes to our welcome tab We’ll use columns to place one box on the left-hand side of our page, and two stacked boxes on the right-hand side. Each column will take up half the page (Note: For column-based layouts, use NULL for the box width, as the width is set by the column that contains the box). We can create two fluidRows within the right-hand column to stack two boxes vertically.

+
+
+
ui.R
+
#........................dashboardHeader.........................
 header <- dashboardHeader(
   
@@ -5016,14 +5027,18 @@
 #..................combine all in dashboardPage..................
 dashboardPage(header, sidebar, body)
-

-
-Read data into global.R & add necessary packages
+

+

Read data into global.R & add necessary packages

+

Remember to load your pre-processed data, which should live in the /data folder within your app’s directory.

+
+
+
global.R
+
# LOAD LIBRARIES ----
 library(shiny)
 library(shinydashboard)
@@ -5034,14 +5049,18 @@
 # READ IN DATA ----
 lake_data <- read_csv("data/lake_data_processed.csv")
-

-
-Add a sliderInput & leafletOutput to the UI
-

+

+

Add a sliderInput & leafletOutput to the UI

+
+

Start by adding just one sliderInput (for selecting a range of lake Elevations) to the left-hand box in the dashboard tab. Then, add a leafletOutput to create a placeholder space for our map, along with a Spinner animation (from the shinycssloaders package). While we’re here, we can also add titles to each box.

+
+
+
ui.R
+
#........................dashboardHeader.........................
 header <- dashboardHeader(
   
@@ -5155,14 +5174,18 @@
 #..................combine all in dashboardPage..................
 dashboardPage(header, sidebar, body)
-

-
-Assemble inputs & outputs in server.R
-

+

+

Assemble inputs & outputs in server.R

+
+

Remember to reference your practice data viz script and to follow our three steps for creating reactive outputs. And don’t forget to add () following each reactive data frame called in your leaflet map!

+
+
+
server.R
+
server <- function(input, output) {
   
   # filter lake data ----
@@ -5201,10 +5224,10 @@
   
 }
-

-
-Run your app & test out your first widget
+

+

Run your app & test out your first widget

+

If all is good, you should see something similar to this:

@@ -5212,32 +5235,26 @@ Our Fish Creek Watershed Lake Monitoring dashboard. The 'Welcome' page has a box on the left for background info, and two stacked boxes on the right for data citation and disclaimer info. The 'Dashboard' tab has a sliderInput in the left-hand box, where users can select a range of elevations. When a range is selected, the blue markers on the leaflet map in the right-hand box are filtered accordingly. Clicking on the markers reveals more information about the specific site, including Site Name, Elevation, Avg Depth, and AvgTemp.

-
-Question - Exercise 5: Add two more sliderInputs to filter for AvgDepth & AvgTemp -
+

Exercise 5: Add two more sliderInputs to filter for AvgDepth & AvgTemp

+

To Do:

-

-angle-right - Add two more sliderInputs, one for AvgDepth and one for AvgTemp beneath our first Elevation sliderInput in the UI +

+ Add two more sliderInputs, one for AvgDepth and one for AvgTemp beneath our first Elevation sliderInput in the UI

-

-angle-right - Update our reactive data frame so that all three widgets filter the leaflet map +

+ Update our reactive data frame so that all three widgets filter the leaflet map





-

+

See next slide for a solution!

-
-Check - Exercise 5: A solution -
+

Exercise 5: A solution

+
  • ui.R
  • @@ -5416,8 +5433,7 @@

-Mug Hot - Break + Break

Up next: Adding background information and an image to our Welcome tab @@ -5425,7 +5441,7 @@

-
+
@@ -5434,21 +5450,16 @@

-
-Question - & Lightbulb - Exercise 6: Add titles & text to Welcome page boxes -
+

& Exercise 6: Add titles & text to Welcome page boxes

+

To Do:

-

-angle-right - Add titles to each box +

+ Add titles to each box

-

-angle-right - Create a /text folder within your app’s directory and add three markdown (.md) files. Write/format text for the background info (left), data citation (top-right), and disclaimer (bottom-right) boxes. Example text below: +

+ Create a /text folder within your app’s directory and add three markdown (.md) files. Write/format text for the background info (left), data citation (top-right), and disclaimer (bottom-right) boxes. Example text below:

    @@ -5459,37 +5470,49 @@
    +
    +
    +
    text/intro.md
    +
    The [Fish Creek Watershed Observatory (FCWO)](http://www.fishcreekwatershed.org/) is a focal watershed within the [National Petroleum Reserve in Alaska (NPR-A)](https://www.blm.gov/programs/energy-and-minerals/oil-and-gas/about/alaska/NPR-A). Targeted lake and stream monitoring within the watershed provide site-specific data prior to and after the establishment of new petroleum development, as well as insight into dynamics related to climate change and variability. Eleven lakes of interest (Harry Potter, Hipbone, Iceshove, L9817, L9819, L9820, Little Alaska, Lower Snowman, M9925, Middle Snowman, and Serenity) are featured in this dashboard.
    +
    +
    +
    +
    text/citation.md
    +
    Data presented in this dashboard were collected as part of the [Fish Creek Watershed Observatory](http://www.fishcreekwatershed.org/) are archived and publicly accessible on the NSF [Arctic Data Center](https://arcticdata.io/). **Citation:** 
     
     *Christopher Arp, Matthew Whitman, Katie Drew, and Allen Bondurant. 2022. Water depth, surface elevation, and water temperature of lakes in the Fish Creek Watershed in northern Alaska, USA, 2011-2022. Arctic Data Center [doi:10.18739/A2JH3D41P](https://arcticdata.io/catalog/view/doi%3A10.18739%2FA2JH3D41P).*
    +
    +
    +
    +
    text/disclaimer.md
    +
    This app is build for demonstration/teaching purposes only and is not paid for or endorsed by the Fish Creek Watershed Observatory or affiliates in any way. The data as presented here are not intended for publication nor scientific interpretation. 
    +


Tips:

-

-angle-right -Titles can include icons! For example: title = tagList(icon("icon-name"), strong("title text here")) +

+ Titles can include icons! For example: title = tagList(icon("icon-name"), strong("title text here"))

-
-Check - Exercise 6: A solution -
-

+

Exercise 6: A solution

+
+

Press the right arrow key to advance through the newly added lines of code.

@@ -5642,16 +5665,19 @@

-
-Add a static image -
-

+

Add a static image

+
+

As a final touch, let’s add an image to the Welcome page, inside the left-hand box beneath our intro text. First, create a /www folder inside your app’s directory (refer back to this slide for a description of this special directory). Download the map of the Fish Creek Watershed from FCWO’s website here and save it to your /www directory.

-

+

Next, use the img tag to add your image. Supply a file path, relative to your /www directory, using the src argument, and alt text using the alt argument.

+
+
+
ui.R
+
#........................dashboardHeader.........................
 header <- dashboardHeader(
   
@@ -5778,25 +5804,27 @@
 #..................combine all in dashboardPage..................
 dashboardPage(header, sidebar, body)
-

-
-Unfortunately, our image doesn’t look so great as-is…
-


+

+

Unfortunately, our image doesn’t look so great as-is…

+
Our Welcome page, with a map of Fish Creek Watershed beneath the intro text. The image is extremely large, spilling out of the box and across the page.

-
-Use in-line CSS to adjust the image size -
-

+

Use in-line CSS to adjust the image size

+
+

We can use in-line CSS to style our image element, as shown below (see style argument). It’s okay if you don’t fully understand what’s going on here for now – we’ll talk in greater detail about how CSS (and Sass) can be used to customize the appearance of your apps in just a bit.

-

+

I’ve also added a caption below our image that links to the image source, and used in-line CSS to center my text within the box.

+
+
+
ui.R
+
#........................dashboardHeader.........................
 header <- dashboardHeader(
   
@@ -5926,20 +5954,19 @@
 #..................combine all in dashboardPage..................
 dashboardPage(header, sidebar, body)
-

-
-Check out your finished dashboard!
+

+

Check out your finished dashboard!

+
Our completed dashboard, with our image styled so that it fits within the background info box on the Welcome page, and three functional sliderInputs that update the leaflet map on the Dashboard page.
-

+

There’s a ton more to learn about building shinydashboards. Check out the documentation to find instructions on adding components like infoBoxes and valueBoxes, building inputs in the sidebar, easy ways to update the color theme using skins, and more.

-
-Complete code for our dashboard thus far: -
+

Complete code for our dashboard thus far:

+
  • ui.R
  • @@ -6142,115 +6169,95 @@

-

-Star - Part 3: Beautifying your user interface (UI) -

-


-

-angle-right - Custom themes with bslib +

Part 3: Beautifying your user interface (UI)

+
+

+ Custom themes with bslib

-

-angle-right - Custom themes with fresh +

+ Custom themes with fresh

-

-angle-right - Styling with CSS & Sass +

+ Styling with CSS & Sass

-
-Book Open - Learning Objectives - Themeing/Styling Apps -
-

+

Learning Objectives - Themeing/Styling Apps

+
+

By the end of this section, you should be equipped with:

-

-angle-right - a number of different approaches for themeing and styling your shiny apps and dashboards +

+ a number of different approaches for themeing and styling your shiny apps and dashboards

-

-angle-right - a basic understanding of how to apply CSS & Sass styling to your app +

+ a basic understanding of how to apply CSS & Sass styling to your app

-

+

Packages introduced:

-

-Box Open -bslib: provides tools for customizing Bootstrap themes directly from R for shiny apps and RMarkdowns +

+ bslib: provides tools for customizing Bootstrap themes directly from R for shiny apps and RMarkdowns

-

-Box Open -fresh: provides tools for creating custom themes for use with shiny, shinydashboard, and bs4Dash apps +

+ fresh: provides tools for creating custom themes for use with shiny, shinydashboard, and bs4Dash apps

-Paint Roller - Creating custom themes + Creating custom themes

We’ve built some really cool apps so far, but they all have a pretty standard and similar appearance. In this section, we’ll explore two packages for creating custom themes for your apps.

-
-Using the {bslib} package to theme Shiny apps -
+

Using the {bslib} package to theme Shiny apps

+

The {bslib} package provides tools for customizing Bootstrap themes directly from R, making custom themeing for Shiny apps (and R Markdown docs!) quite easy.

-

+

Pros:

-

-Check - easy to use +

+ easy to use

-

-Check - includes a real-time themeing widget to try out themes before applying them to your own app +

+ includes a real-time themeing widget to try out themes before applying them to your own app

-

-Check - plays well with the thematic package for matching plot styling to app +

+ plays well with the thematic package for matching plot styling to app

-

-Check -bslib does more than just themeing! Check out the December 2022 announcement of new UI components made possible with the latest package release +

+ bslib does more than just themeing! Check out the December 2022 announcement of new UI components made possible with the latest package release

-

+

Cons:

-

-X Mark - does not work with shinydashboard (bslib is only intended for use with shiny apps) +

+ does not work with shinydashboard (bslib is only intended for use with shiny apps)

-

-X Mark - styling is constrained by the arguments available to bs_theme() +

+ styling is constrained by the arguments available to bs_theme()

@@ -6262,16 +6269,19 @@

-
-Apply a pre-built theme with {bslib} -
-

+

Apply a pre-built theme with {bslib}

+
+

By default, Shiny uses the Bootstrap v3 theme (which is not so exciting). Change the theme to a slightly more modern Bootstrap v5 theme by setting the theme argument of fluidPage() to bslib::bs_theme(version = 5), or supply bs_theme() with a pre-built bootswatch theme, as shown below (for a list of theme names, run bootswatch_themes() in your console):


+
+
+
~/one-file-app/ui.R
+
ui <- fluidPage(
   
   theme = bslib::bs_theme(bootswatch = "solar")
@@ -6280,7 +6290,8 @@
   
 )
-

+

+

Check out the complete source code for App #1 here (NOTE: applied themes are commented out).

@@ -6292,16 +6303,19 @@

-
-Create a custom theme with {bslib} -
-

+

Create a custom theme with {bslib}

+
+

Alternatively, you can fully customize your own theme. Explore the bslib vignette for detailed instructions. A small example here:


+
+
+
~/one-file-app/ui.R
+
ui <- fluidPage(
   
   theme = bslib::bs_theme(
@@ -6314,7 +6328,8 @@
   
 )
-

+

+

Check out the complete source code for App #1 here (NOTE: applied themes are commented out).

@@ -6325,20 +6340,23 @@
-

+

Be sure to check out the interactive themeing widget to test custom color/font/etc. combos by running bs_theme_preview() in your console, or visit the hosted version here. You can also call bs_themer() within your server function to open the theme customization UI alongside your own app.

-
-Use {thematic} to extend your theme to plots -
-

+

Use {thematic} to extend your theme to plots

+
+

You probably noticed that our scatterplot looks a little silly against the darker background of our themed app. Enter the {thematic} package, which is built to help simplify plot themeing. Call thematic_shiny() before launching your app to generate plots that reflect your application’s bs_theme(). For example:


+
+
+
~/one-file-app/ui.R
+
thematic::thematic_shiny()
 
 ui <- fluidPage(
@@ -6353,7 +6371,8 @@
   
 )
-

+

+

Check out the complete source code for App #1 here (NOTE: applied themes are commented out).

@@ -6364,13 +6383,12 @@
-

+

Read the vignette to learn more about using the thematic package to help match plot fonts to the fonts applied across your app.

-
-Using the {fresh} package to theme Shiny apps & dashboards -
+

Using the {fresh} package to theme Shiny apps & dashboards

+

The {fresh} package provides tools for creating custom themes to use in Shiny apps and dashboards – set parameters of your theme using create_theme(), generate a stylesheet based off your specifications, and apply your stylesheet to your app.

@@ -6378,29 +6396,26 @@
-

+

Pros:

-

-Check - easy to use +

+ easy to use

-

-Check - supports theme creation for both shiny apps and dashboards (and also flexdashboards and {b4dash} applications) +

+ supports theme creation for both shiny apps and dashboards (and also flexdashboards and {b4dash} applications)

-

+

Cons:

-

-X Mark - styling is constrained by the variables available to create_theme() +

+ styling is constrained by the variables available to create_theme()

@@ -6413,16 +6428,15 @@

-
-A general workflow for using fresh themes -
+

A general workflow for using fresh themes

+

Whether you’re working on a shiny app or a shiny dashboard, you’ll need the following:

-

+

1. a /www folder within your app’s directory – this is where we’ll save the stylesheet (a .css file) that fresh will generate for us

-

+

2. a separate script for building our theme using the create_theme() function – I recommend saving this to ~/scratch (it seemed to cause issues when saved anywhere within my app directory)

@@ -6434,52 +6448,53 @@

-
-Creating a fresh theme for two-file-app -
-

-In this example, we update the colors of our app’s body, navbar, and tabPanels using the appropriate fresh variables for shiny apps. We specify a file path, two-file-app/www (you’ll need to create the /www directory, since we don’t have one yet), where our stylesheet (e.g. shiny_fresh_themes.css, as shown here) file will be saved to. -

-

-Of course, these color combos are not recommended, but chosen purely for demonstration purposes Face Smile Wink -. -

-
-
# in a separate script: ~/R/create_fresh_theme_shiny.R
-# load library ----
-library(fresh)
-
-# create theme -----
-create_theme(
-  
-  # you can supply a bootstrap theme to begin with
-  theme = "default",
-
-  # global styling
-  bs_vars_global(
-    body_bg = "#D2D0CA", # beige
-    text_color = "#F23ACB", # hot pink
-    link_color = "#0E4BE3" # royal blue
-  ),
-
-  bs_vars_navbar(
-    default_bg = "#13CC13", # lime green
-    default_color = "#66656C" # gray
-  ),
-
-  # tabPanels
-  bs_vars_tabs(
-    border_color = "#F90909" # red
-  ),
-
-  # generate css file
-  output_file = "two-file-app/www/shiny_fresh_theme.css"
-)
+

Creating a fresh theme for two-file-app

+
+

+In this example, we update the colors of our app’s body, navbar, and tabPanels using the appropriate fresh variables for shiny apps. We specify a file path, two-file-app/www (you’ll need to create the /www directory, since we don’t have one yet), where our stylesheet (e.g. shiny-fresh-theme.css, as shown here) file will be saved to. +

+

+Of course, these color combos are not recommended, but chosen purely for demonstration purposes . +

+
+
+
+
~/R/create-fresh-theme-shiny.R
+
+
# load library ----
+library(fresh)
+
+# create theme -----
+create_theme(
+  
+  # you can supply a bootstrap theme to begin with
+  theme = "default",
+
+  # global styling
+  bs_vars_global(
+    body_bg = "#D2D0CA", # beige
+    text_color = "#F23ACB", # hot pink
+    link_color = "#0E4BE3" # royal blue
+  ),
+
+  bs_vars_navbar(
+    default_bg = "#13CC13", # lime green
+    default_color = "#66656C" # gray
+  ),
+
+  # tabPanels
+  bs_vars_tabs(
+    border_color = "#F90909" # red
+  ),
+
+  # generate css file
+  output_file = "two-file-app/www/shiny-fresh-theme.css"
+)
-

-
-Apply a fresh theme to our app
+

+

Apply a fresh theme to our app

+

To apply our theme, provide the theme argument of your fluidPage() or navbarPage() with the name of our stylesheet. Note: shiny knows to look in the /www directory, so you can omit that from your file path, as shown below:

@@ -6488,6 +6503,10 @@


+
+
+
~/two-file-app/ui.R
+
# navbar page ----
 ui <- navbarPage(
 
@@ -6497,7 +6516,8 @@
   
 ) # END navbarPage
-

+

+

Check out the complete source code for App #2 here (NOTE: applied themes are commented out).

@@ -6509,47 +6529,49 @@

-
-Creating a fresh theme for our shinydashboard -
-

-In this example, we update the colors of our app’s header, body, and sidebar using the appropriate fresh variables for shiny dashboards. We specify a file path, shinydashboard/www where our stylesheet (e.g. shinydashboard_fresh_themes.css, as shown here) file will be saved to. Again, these color combos are not recommended, but chosen purely for demonstration purposes. -

-
-
# in a separate script: ~/R/create_fresh_theme_shinydashboard.R
-# load libraries ----
-library(fresh)
-
-# create theme ----
-create_theme(
-  
-  # change "light-blue"/"primary" color
-  adminlte_color(
-    light_blue = "#150B5A" # dark blue
-  ),
-  
-  # dashboardBody styling (includes boxes)
-  adminlte_global(
-    content_bg = "#E7B5B5" # blush pink
-  ),
-  
-  # dashboardSidebar styling
-  adminlte_sidebar(
-    width = "400px", 
-    dark_bg = "#57F8F3", # light blue
-    dark_hover_bg = "#BF21E6", # magenta
-    dark_color = "#F90000" # red
-  ),
-  
-  # generate css file
-  output_file = "shinydashboard/www/shinydashboard_fresh_theme.css"
-  
-)
+

Creating a fresh theme for our shinydashboard

+
+

+In this example, we update the colors of our app’s header, body, and sidebar using the appropriate fresh variables for shiny dashboards. We specify a file path, shinydashboard/www where our stylesheet (e.g. shinydashboard-fresh-theme.css, as shown here) file will be saved to. Again, these color combos are not recommended, but chosen purely for demonstration purposes. +

+
+
+
+
~/R/create-fresh-theme-shinydashboard.R
+
+
# load libraries ----
+library(fresh)
+
+# create theme ----
+create_theme(
+  
+  # change "light-blue"/"primary" color
+  adminlte_color(
+    light_blue = "#150B5A" # dark blue
+  ),
+  
+  # dashboardBody styling (includes boxes)
+  adminlte_global(
+    content_bg = "#E7B5B5" # blush pink
+  ),
+  
+  # dashboardSidebar styling
+  adminlte_sidebar(
+    width = "400px", 
+    dark_bg = "#57F8F3", # light blue
+    dark_hover_bg = "#BF21E6", # magenta
+    dark_color = "#F90000" # red
+  ),
+  
+  # generate css file
+  output_file = "shinydashboard/www/shinydashboard-fresh-theme.css"
+  
+)
-

-
-Apply a fresh theme to our dashboard
+

+

Apply a fresh theme to our dashboard

+

To apply our theme, use the fresh::use_theme() function inside your dashboardBody, providing it with the name of your stylesheet. Note: shiny knows to look in the /www directory, so you can omit that from your file path, as shown below:

@@ -6558,6 +6580,10 @@


+
+
+
~/shinydashboard/ui.R
+
body <- dashboardBody(
   
   # set theme
@@ -6567,7 +6593,8 @@
   
 )
-

+

+

Check out the complete source code for the shinydashboard here (NOTE: applied themes are commented out).

@@ -6581,51 +6608,44 @@

-Paint Brush - Styling apps with CSS & Sass + Styling apps with CSS & Sass

bslib & fresh are great ways to get started on your app customization journeys, but knowing some CSS & Sass can help you really fine-tune the appearance of your apps

-
-Using Sass & CSS to style Shiny apps & dashboards -
+

Using Sass & CSS to style Shiny apps & dashboards

+

You can write your own stylesheets using CSS and Sass to fully customize your apps, from background colors, to font styles, to size and shape of elements, and more. Unlike bslib and fresh, these are languages, meaning they can be a bit more challenging to get started with (but the payoff it big!).

-


-

+

Pros:

-

-Check - applies to any web page (not just shiny apps) +

+ applies to any web page (not just shiny apps)

-

-Check - allows you to customize pretty much any aspect of your app +

+ allows you to customize pretty much any aspect of your app

-

-Check - can be combined with themes generated using bslib or fresh to fine-tune your app’s styling +

+ can be combined with themes generated using bslib or fresh to fine-tune your app’s styling

-

+

Cons:

-

-X Mark - a steeper learning curve/generally more complex than packages like bslib and fresh +

+ a steeper learning curve/generally more complex than packages like bslib and fresh

@@ -6637,9 +6657,8 @@

-
-Resources for a deeper dive -
+

Resources for a deeper dive

+

We’ll be doing a rather high-level and quick overview of Sass & CSS today, though I encourage you to check out the Customizing Quarto Websites workshop, which takes a much deeper dive (the information in that workshop is largely applicable here).

@@ -6653,16 +6672,15 @@

-
-What even is CSS? Sass? -
+

What even is CSS? Sass?

+
The CSS 3 logo.
-

+

CSS (Cascading Style Sheets) is a programming language that allows you to control how HTML elements look (e.g. colors, font styles, etc.) on a webpage.

@@ -6674,16 +6692,15 @@ The Sass logo
-

+

Sass (Syntactically Awesome Stylesheets) is a CSS extension language and CSS preprocessor – meaning Sass needs to be converted (aka compiled) to CSS before it can be interpreted by your web browser.

-
-CSS is a rule-based language -
+

CSS is a rule-based language

+

CSS is a rule-based language, meaning that it allows you to define groups of styles that should be applied to particular elements or groups of elements on a web page. For example, “I want all level one headings (<h1> or tags$h1() as written in Shiny) in my app to be green with a bit of extra space between each letter” could be coded as:

@@ -6696,39 +6713,35 @@
-

-angle-right -Selectors select the HTML element(s) you want to style (e.g. level one headings, <h1>) +

+ Selectors select the HTML element(s) you want to style (e.g. level one headings, <h1>)

-

-angle-right -Declarations sit inside curly brackets, {}, and are made up of property and value pairs. Each pair specifies the property of the HTML element(s) you’re selecting (e.g. the color property of the element <h1>), and a value you’d like to assign to that property (e.g. green) +

+ Declarations sit inside curly brackets, {}, and are made up of property and value pairs. Each pair specifies the property of the HTML element(s) you’re selecting (e.g. the color property of the element <h1>), and a value you’d like to assign to that property (e.g. green)

-

-angle-right - A property and it’s corresponding value are separated by a colon, :. Declarations end with a semicolon, ; +

+ A property and it’s corresponding value are separated by a colon, :. Declarations end with a semicolon, ;

-

+

There are a variety of CSS selectors – check out some of the basics that will take you far in styling your apps, starting on this slide of the Customizing Quarto Websites workshop.

-
-3 ways to add CSS styling to your apps -
-

-You can (1) add styling directly to tags, (2) add CSS rules to your header, and/or (3) build a stylesheet that is applied to your app. Creating a stylesheet is the preferred approach, and you should do this whenever possible. +

3 ways to add CSS styling to your apps

+
+

+You can (1) add styling directly to tags, (2) add CSS rules to your header, and/or (3) build a stylesheet that is applied to your app. Creating a stylesheet is often the preferred approach.

-

+

Add styling directly to tags. It’s best not to use a lot of these! It’s easy to lose track of your “in-line” styling in large projects, you can’t reuse rules easily, it’s hard to keep styling consistent, and it’s difficult to implement large stylistic changes.

@@ -6773,7 +6786,7 @@
-

+

Add CSS rules to your app’s header (tags$head). This is a little bit better than option 1 since it allows for the reuse of rules, however, styles can’t be cached (i.e. saved for future usage when you reopen your app). Note: explore Google fonts here and check out this slide for instructions on selecting a font.

@@ -6826,7 +6839,7 @@
-

+

Build a stylesheet (a .css file) inside your app’s www/ directory and apply your styles to your app’s header (for shinydashboards, include your header inside dashboardBody(). This is the most ideal approach – it allows for style reuse, caching, and keeps styling contained in one spot. Tip: use touch styles.css in the terminal to create a new .css file.

@@ -6896,9 +6909,8 @@

-
-Let’s practice on a small dashboard first: -
+

Let’s practice on a small dashboard first:

+

-
-What if I want to style an element, but don’t know how to target it? -
-

+

What if I want to style an element, but don’t know how to target it?

+
+

Oftentimes, you’ll have to do a bit of exploration to determine how to target specific elements for styling. In either your app viewer or web browser, right click on an element of interest and choose Inspect (or Inspect Element) to open up the underlying HTML and CSS. You can make temporary edits to your app (e.g. adding a background color, changing font sizes, etc.) to see how they look first, then copy the appropriate CSS rule into your stylesheet to apply to your app.

A shiny dashboard, opened in the RStudio viewer, with the Inspect pane open to the right. The top half of the inspect pane shows the underlying HTML and the bottom half shows the underlying CSS styles.

-
-Inspect & identify how to update box styling -
-

+

Inspect & identify how to update box styling

+
+

For example, let’s say I want to change the color of this shinydashboard’s boxes and the color of the box text.

-

+

First, we need to determine which type of HTML element creates our box. Right clicking on a box and choosing Inspect Element pulls up the HTML and CSS files underlying the app. Hovering over different parts of the HTML highlights different elements in the UI. The box is highlighted when I hover over <div class="box-body"> – this tells me that boxes are formed using the <div> HTML element and they’re assigned a class called box-body.

-

+

Next, we can (temporarily) adjust the CSS rules that style these boxes to see how they work. I can hop down to the CSS file (here, that’s located in the bottom half of my sidebar, but depending on the size of your window/layout, the HTML and CSS might be side-by-side) and find the .box-body class selector. You can add property/value pairs and/or update existing property values to adjust the appearance of our box. Notice that changing the .box-body class selector updates both boxes – upon inspecting the box containing our plot, you’ll notice that it is also of class, box-body. Therefore changes to this class selector will apply to both boxes. This process is purely for testing purposes – refreshing your app will remove any of these changes.

-
-Finally, apply box styling to our dashboard -
-

+

Finally, apply box styling to our dashboard

+
+

Now that we know that we can use the box-body class to customize the appearance of our boxes, let’s create a stylesheet and add our new rules. The shinydashboard framework already provides the “standard” styling for boxes, contained in the box-body class. Anything we specify in our own stylesheet will build upon or modify existing styling.

-

+

Remember to create a header and link your stylesheet within dashboardBody() to apply our styles.

@@ -7156,9 +7165,8 @@

-
-What about Sass? -
+

What about Sass?

+

Okay, we wrote and applied some CSS styling to our apps, but what about Sass? You don’t need to write any Sass at all, however, it provides a number of benefits, including helping to reduce repetition.

@@ -7172,10 +7180,9 @@ You might imagine how often you’ll need to type those HEX codes out as you developing your stylesheet…it can get annoying rather quickly.

-
-We can define and reference Sass variables throughout our stylesheet -
-

+

We can define and reference Sass variables throughout our stylesheet

+
+

Sass allows us to define variables (in the form $var-name: value;) for our colors to reference instead of writing out their HEX codes each time:

@@ -7197,45 +7204,47 @@ border-color: $darkgray; }
-

+

If you decide that you actually like a different shade of teal better, you’ll only need to update the hex code where you first define the $teal Sass variable, saving lots of time.

-
-Sass for Shiny workflow -
+

Sass for Shiny workflow

+

To style apps using both Sass and CSS, you’ll follow this general workflow:

-

+

1. Create a .scss file inside ~/myapp/www using the touch command in the terminal (e.g. cd into the appropriate directory, then touch styles.scss). Write both your Sass variables and CSS rules in your .scss file (Note: you can write both Sass & CSS in a .scss file, but only CSS in a .css file)

-

+

2. Compile (i.e. convert) Sass to CSS in global.R (or, if using a one-file app, at the top of your script before you define your UI) using the the sass() function from the {sass} package – this will generate a .css file that our shiny app can actually use. Be sure to save your .css file to your app’s /www directory.

-

+

3. Apply your styles to your app by linking to to your .css file in your app’s header.

A chart showing the Sass to CSS workflow -- begin with writing Sass variables and CSS rule in a .scss file, then use the sass R package to compile sass to css, and finally use your compiled .css file to apply your styles to your app.

-
-Let’s build our Sass file -
-

+

Let’s build our Sass file

+
+

We’ll practice on our two-file-app – first, remove any reference to your bslib or fresh themes that we practiced applying earlier so that we’re starting off with just the default shiny styling.

-

-Next, create your Sass file within two-file-app/www/. Then, using your terminal, cd into ~/two-file-app/www and use the touch command to create a .scss file (I’m calling mine my_sass_styles.scss). +

+Next, create your Sass file within two-file-app/www/. Then, using your terminal, cd into ~/two-file-app/www and use the touch command to create a .scss file (I’m calling mine my-sass-styles.scss).

-

-Add Sass variables and CSS rules to my_sass_styles.scss: +

+Add Sass variables and CSS rules to my-sass-styles.scss:

+
+
+
~/two-file-app/www/my-sass-styles.scss
+
// import 2 fonts
 @import url('https://fonts.googleapis.com/css2?family=Karma&family=Prompt:wght@200&display=swap');
 
@@ -7286,25 +7295,25 @@
 }
-

-
-Then, compile Sass to CSS
-

+

+

Then, compile Sass to CSS

+
+

Because web browsers can only interpret CSS (not Sass), we need to compile our Sass to CSS. To do this, we can use the sass() function from the {sass} package. We can do this in global.R. The sass() function requires two arguments: a sass file input and a file path + named .css file output.

-

+

We also need to apply our styles to our app by linking this newly-generated .css file in our app’s header.

-

-Note: After running your app, you should see a my_sass_styles.css file appear in www/ – it should look quite familiar, except all of our Sass variables have been converted to CSS. +

+Note: After running your app, you should see a my-sass-styles.css file appear in www/ – it should look quite familiar, except all of our Sass variables have been converted to CSS.

-

-Star - Part 4: Improving your app’s user experience (UX) -

-


-

-angle-right - Important UX considerations +

Part 4: Improving your app’s user experience (UX)

+
+

+ Important UX considerations

-

-angle-right - Web accessibility +

+ Web accessibility

-Users - UX/UI matters + UX/UI matters

When designing your app, it’s critically important that you consider your user’s needs and how they will interact with your app – it doesn’t matter how innovative you back-end computations are if people don’t understand how to use your app!

-
-Book Open - Learning Objectives - UX/UI Design -
-

+

Learning Objectives - UX/UI Design

+
+

After this section, you should:

-

-angle-right - have a checklist of considerations to reference each time you build an app +

+ have a checklist of considerations to reference each time you build an app

-

-angle-right - have a few additional resources to dive deeper into UX/UI design +

+ have a few additional resources to dive deeper into UX/UI design

-
-Tips for designing your Shiny apps -
+

Tips for designing your Shiny apps

+

Chapters 6 and 7 of Engineering Production-Grade Shiny Apps, by Colin Fay, Sébastien Rochette, Vincent Guyader, and Cervan Girard provide a list of considerations as you embark on your app-building journey. Some of their suggestions are summarized below, but check out the book for greater detail, examples, and additional considerations:

-

-angle-right -Simplicity is gold: using the application shouldn’t require reading a manual, and interfaces should be as self-explanatory as possible. +

+ Simplicity is gold: using the application shouldn’t require reading a manual, and interfaces should be as self-explanatory as possible.

-

-angle-right -Adapt a defensive programming mindset: your app should always fail gracefully and informatively (e.g. provide users with a helpful error message) +

+ Adapt a defensive programming mindset: your app should always fail gracefully and informatively (e.g. provide users with a helpful error message)

-

-angle-right -Build a self-explanatory app: consider the following three suggestions for doing so – (a) remember the rule of least surprise (in UI design, always do the least surprising thing e.g. we often assume that underlined text is clickable, so if you include underlined text in your UI, there’s a good chance a user will try clicking on it). (b) think about progression (design a clear pattern of moving forward for your user), and (c) related to b, make sure that if an input is necessary, it is made clear to your user. Check out the {shinyjs} package for implementing nifty ways to improve the user experience of your shiny apps. +

+ Build a self-explanatory app: consider the following three suggestions for doing so – (a) remember the rule of least surprise (in UI design, always do the least surprising thing e.g. we often assume that underlined text is clickable, so if you include underlined text in your UI, there’s a good chance a user will try clicking on it). (b) think about progression (design a clear pattern of moving forward for your user), and (c) related to b, make sure that if an input is necessary, it is made clear to your user. Check out the {shinyjs} package for implementing nifty ways to improve the user experience of your shiny apps.

-

-angle-right -Avoid feature-creep: feature-creep is the process of adding features to an app that complicates its usage and maintenance – this includes adding too much reactivity and too much interactivity (e.g. plotly) – interactivity adds visual noise, so it’s best to not make elements interactive if there is no value is gained. +

+ Avoid feature-creep: feature-creep is the process of adding features to an app that complicates its usage and maintenance – this includes adding too much reactivity and too much interactivity (e.g. plotly) – interactivity adds visual noise, so it’s best to not make elements interactive if there is no value is gained.

-
-Additional UX/UI resources -
+

Additional UX/UI resources

+

-angle-right -Outstanding User Interfaces with Shiny, by David Granjon + Outstanding User Interfaces with Shiny, by David Granjon

-angle-right -Shiny Developer Series, Episode 20: Outstanding User Interfaces with David Granjon + Shiny Developer Series, Episode 20: Outstanding User Interfaces with David Granjon

-angle-right -15 User Experience Principles and Theories, by Pathum Goonawardene + 15 User Experience Principles and Theories, by Pathum Goonawardene

-Universal Access - Building accessible apps + Building accessible apps

Consider web accessibilty guidelines to ensure that your app is usable by all

-
-Book Open - Learning Objectives - Accessibility -
-

+

Learning Objectives - Accessibility

+
+

By the end of this section, you should:

-

-angle-right - have a general understanding of what web accessibility means and who it can benefit (spoiler alert: it benefits us all!) +

+ have a general understanding of what web accessibility means and who it can benefit (spoiler alert: it benefits us all!)

-

-angle-right - know how to make a few small tweaks/updates to your application to make it more accessible for all users +

+ know how to make a few small tweaks/updates to your application to make it more accessible for all users

-

-angle-right - have a few great resources to turn to to learn more +

+ have a few great resources to turn to to learn more

-
-Universal Access - What is web accessibility and why is it important? -
-

+

What is web accessibility?

+
+

From the World Wide Web Consortium (W3C)’s Introduction to Web Accessibility:

-

-Web accessibility means that websites, tools, and technologies are designed and developed so that people with disabilities can use them. More specifically, people can: +

+Web accessibility means that websites, tools, and technologies are designed and developed so that people with disabilities can use them. More specifically, people can:

-

-angle-right - perceive, understand, navigate, and interact with the Web +

+ perceive, understand, navigate, and interact with the Web

-

-angle-right - contribute to the Web +

+ contribute to the Web

-

-Web accessibility encompasses all disabilities that affect access to the Web, including: auditory, cognitive, neurological, physical, speech, and visual +

+ Web accessibility encompasses all disabilities that affect access to the Web, including: auditory, cognitive, neurological, physical, speech, and visual

-

-Web accessibility also benefits people without disabilities, for example: +

+ Web accessibility also benefits people without disabilities, for example:

-

-angle-right - people using mobile phones, smart watches, smart TVs, and other devices with small screens, different input modes, etc. +

+ people using mobile phones, smart watches, smart TVs, and other devices with small screens, different input modes, etc.

-

-angle-right - older people with changing abilities due to ageing +

+ older people with changing abilities due to ageing

-

-angle-right - people with “temporary disabilities” such as a broken arm or lost glasses +

+ people with “temporary disabilities” such as a broken arm or lost glasses

-

-angle-right - people with “situational limitations” such as in bright sunlight or in an environment where they cannot listen to audio +

+ people with “situational limitations” such as in bright sunlight or in an environment where they cannot listen to audio

-

-angle-right - people using a slow Internet connection, or who have limited or expensive bandwidth +

+ people using a slow Internet connection, or who have limited or expensive bandwidth

-

+

Check out the A11Y Project for lots of great tutorials and information about web accessibility.

-
-Small changes can lead to tangible increases in functionality for all users -
+

Small changes can lead to tangible increases in functionality for all users

+

Ensuring that your shiny apps are accessible can feel overwhelming – but considering even a few small changes can have a large impact on user experience.

@@ -7579,21 +7553,17 @@

-

-angle-right - Use HTML elements appropriately (e.g. consider hierarchy) +

+ Use HTML elements appropriately (e.g. consider hierarchy)

-

-angle-right - Include alt text for graphical elements +

+ Include alt text for graphical elements

-

-angle-right - Consider navigation from a mobility perspective +

+ Consider navigation from a mobility perspective

-

-angle-right - Use colorblind-friendly palettes +

+ Use colorblind-friendly palettes

@@ -7602,17 +7572,16 @@ In the center, we see a computer monitor with the world wide web symbol on the screen. There is a circle around the computer made up of four smaller circles, each containing on of the following symbols: and eye, a hand with a finger touching something, a human head with a brain, and an ear.
-

+

Image Source: Accessibility Stack

-
-Use HTML elements appropriately -
-

+

Use HTML elements appropriately

+
+

Screen readers use HTML elements to understand web page organization. Header elements create hierarchy within a webpage and are used by screen readers (i.e. devices used by those with visual impairments) to understand a page’s organizational structure. An <h1> element is more important (hierarchically speaking) than an <h2>, which is more important than an <h3>, and so on.

@@ -7647,24 +7616,23 @@


-

+

Ideally, you would only have one <h1> element (e.g. your app’s title), a small number of <h2> elements, more <h3> elements, and so on. See the minimal example, to the left.

-

+

You should not rely on headers for styling purposes – for example, you should not use a level-one header elsewhere in your app just because you want larger text. Instead, use CSS to increase text size (refer to the Customizing Quarto Websites workshop for instruction on how to construct CSS selectors for styling HTML elements).

-
-Include alt text with all graphical elements -
+

Include alt text with all graphical elements

+

All images and graphical elements should include alternative (alt) text that describe the image and/or information being represented. This text won’t appear in the UI, but is detected and read by screen readers.

-

+

Include the alt argument (similar to adding the alt attribute to an HTML element) when using renderPlot() to add alt text to your reactive plots – the alt argument lives outside of the {} but inside the (). For example:

@@ -7680,7 +7648,7 @@
-

+

Similarly, use the alt argument within tags$img when adding static images to your app. For example:

@@ -7691,9 +7659,8 @@

-
-Tips on writing alt text for data visualizations -
+

Tips on writing alt text for data visualizations

+

A good rule of thumb for writing alt text for data visualizations is alt=“Chart type of type of data where reason for including chart (see this post by Amy Cesal for more). One example:

@@ -7703,23 +7670,22 @@

alt=“Bar chart of gun murders per 100,000 people where America’s murder rate is 6 times worse than Canada, and 30 times Australia

-

+

For more great tips on how and when to use alt text, check out this article by the A11Y Project. For examples of how to construct good alt text, take a peek at this resource by Datawrapper.

-
-Consider UI navigation for those with mobility impairments -
-

+

Consider UI navigation for those with mobility impairments

+
+

For users with mobility impairments, using a mouse to navigate a UI packed with widgets may be challenging – some users may even be exclusively using a keyboard to navigate the web.

-

+

Ideally, actions required of your user can be done using a keyboard (e.g. pressing a button in the UI) – however from a new shiny developer standpoint, this may be technically challenging to implement (the authors of Engineering Production-Grade Shiny Apps suggest the {nter} package for building shiny action buttons that can be triggered by pressing enter, however, at the time of building this workshop, the package source code hadn’t been updated since 2019).


-

+

At a minimum, consider spacing out and/or limiting the number of widgets on any given page to make navigation with a mouse as easy as possible.

@@ -7731,13 +7697,12 @@

-
-Use colorblind-friendly palettes -
-

+

Use colorblind-friendly palettes

+
+

About 1 in 12 males and 1 in 200 females have some form of colorblindness (Wikipedia). Ensuring that your color choices are distinguishable from one another and/or providing an additional non-color-based way (e.g. patterns, shapes) of distinguishing between treatments/variables/etc. can greatly help with interpretation of data visualizations.

-

+

There are lots of great colorblind-accessible palettes and resources (check out this one by Alex Phillips to start). Google Chrome also has a built-in vision deficiency emulator (see gif, below right; right click in your web browser > Inspect > Rendering (add tab by clicking on the three stacked dots if it’s not already open) > scroll down and choose emulation type from drop down where it says “Emulate vision deficiencies”).

@@ -7746,7 +7711,7 @@ Four color palette wheels showing the difference in perceived colors for those with normal vision, protanopia, deuteranopia, and tritanopia.
-

+

Four different forms of colorblindness. Image Source: Venngage

@@ -7757,101 +7722,83 @@ A gif of a user opening up the developer tools pane in Google Chrome and emulating how users with various vision deficiencies would see the webpage.
-

+

Using Google Chrome’s vision deficiency emulator to view webpages as seen by those with vision deficiencies

-

-Star - Part 5: Debugging & testing -

-


-

-angle-right - Debugging approaches +

Part 5: Debugging & testing

+
+

+ Debugging approaches

-

-angle-right - Testing apps +

+ Testing apps

-Bug - Debugging + Debugging

Like any code, you’re bound to run into errors as you’re developing your shiny app(s). However, Shiny can be particularly challenging to debug. In this section, we’ll review a few approaches for solving pesky issues.

-
-Book Open - Learning Objectives - Debugging -
-

+

Learning Objectives - Debugging

+
+

After this section, you should:

-

-angle-right - understand some of the challenges associated with debugging shiny applications +

+ understand some of the challenges associated with debugging shiny applications

-

-angle-right - be introduced to a few approaches and tools for debugging shiny applications, including using diagnostic messages and the reactlog package. +

+ be introduced to a few approaches and tools for debugging shiny applications, including using diagnostic messages and the reactlog package.

-

+

Packages introduced:

-

-Box Open -reactlog: a reactivity visualizer for shiny +

+ reactlog: a reactivity visualizer for shiny

-
-Debugging can be challenging -
+

Debugging can be challenging

+

Shiny apps can be particularly challenging to debug for a few reasons:

-

-angle-right - Shiny is reactive, so code execution isn’t as linear as other code that you’re likely more familiar with (e.g. analytical pipelines written in “normal” R scripts, where each line of code is executed in succession) +

+ Shiny is reactive, so code execution isn’t as linear as other code that you’re likely more familiar with (e.g. analytical pipelines written in “normal” R scripts, where each line of code is executed in succession)

-

-angle-right - Shiny code runs behind a web server and the Shiny framework itself, which can obscure what’s going on +

+ Shiny code runs behind a web server and the Shiny framework itself, which can obscure what’s going on

While there are a number of different tools/strategies for debugging Shiny apps, I find myself turning to one (or more) of these approaches most often:

-

-angle-right - isolating pesky errors (typos, missing commas, unmatched parentheses) in the UI by commenting out code from the outside in +

+ isolating pesky errors (typos, missing commas, unmatched parentheses) in the UI by commenting out code from the outside in

-

-angle-right - reducing your app to just problematic code by commenting out as much correctly-functioning code as possible +

+ reducing your app to just problematic code by commenting out as much correctly-functioning code as possible

-

-angle-right - adding diagnostic messages to my reactives +

+ adding diagnostic messages to my reactives

-

-angle-right - using reactlog to visualize reactivity errors +

+ using reactlog to visualize reactivity errors

@@ -7860,19 +7807,18 @@

-
-Track down pesky UI errors by commenting out code from the outside in -
-

+

Track down pesky UI errors by commenting out code from the outside in

+
+

Many of us experienced the frustrations of finding unmatched parentheses, typos, missing commas, etc. when building out our UI layout for App #2, and tracking down the issue can require some patience and persistence.

-

+

My preferred approach for troubleshooting a situation like this is to comment out all code moving from the highest-level layout function (e.g. navbarPage()) inwards, re-running your app each time you un-comment the next little bit of code, until you find the place where your app breaks.

-

+

For example, if I were to trouble shoot the UI for App #2, I’d comment out everything except ui <- navbarPage(title = "LTER Animal Data Explorer" and the ending ) # END navbarPage, then run my app to make sure an empty app with a gray navbar and title at the top appears. It does? Great. Next, un-comment the two tabPanel()s that create the “About this App” and “Explore the Data” pages. Works? Add a little bit more back in now, and continue this process. I like to un-comment/re-run all layout function code first, then begin adding back the inputs and outputs one by one. See a short, but incomplete demo to the right:

@@ -7883,64 +7829,60 @@
-

+

Ultimately, taking your time, adding lots of code comments to mark the ending parentheses of each function, and leaving space between lines of code so that you can more easily see what’s going on will save you lots of headache!

-
-What about “larger” errors? -
-

+

What about “larger” errors?

+
+

Oftentimes, you’ll need to identify larger, more complex errors, like why an output isn’t rendering correctly or even appearing in your app at all.

-

+

I often turn to two strategies:

-

+

(1) commenting out everything except the UI elements and server logic where I believe the issue is stemming from, and

-

+

(2) adding diagnostic messages to my reactives

-

+

(3)and on rare occasions, I’ll try using the {reactlog} package to help visualize my app’s reactivity in an attempt to identify the problem.

-

+

To demo these approaches, we’ll use two pre-constructed apps as examples: (1) reactlog-working (a small app that’s functioning as intended) and (2) reactlog-broken (the same small app that’s not functioning as intended).

-
-I’m building an app that should look like this… -
-

+

I’m building an app that should look like this…

+
+

In Tab 1, both the image and text should update whenever a new radio button is chosen. In Tab 2, the scatterplot should update so that only data points for penguins with body masses within our chosen range are displayed. Find the source code for this functioning app here.

A functioning shiny app with two tabs. The first tab has radioButtons that when selected, update the penguin image and description text. The second tab has a sliderInput and scatterplot.

-
-…but let’s say it actually looks like this: -
-

+

…but let’s say it actually looks like this:

+
+

In Tab 1, only the image updates whenever a new radio button is chosen, and text is missing altogether. In Tab 2, the scatterplot updates as expected whenever the body mass range is changed. Find the source code for this buggy app here.

The same shiny app as on the previous slide, but this time when the radioButtons are updated, only the image changes -- no text appears.

-
-Start by commenting out functioning code -
-

+

Start by commenting out functioning code

+
+

Even though this is a relatively small/simple app, there is still code that, for lack of a better term, gets in the way. After a quick assessment, my reactive scatterplot on Tab 2 appears to be working as expected. To help simplify the amount of code I need to look at, I’ll start by commenting out all UI elements (sliderInput & plotOutput) and server logic for building that reactive plot.

-

+

Note: As you begin building more complex apps, you may have reactives that depend on other reactives – it’s important to think about these dependencies when commenting out parts of your app for debugging purposes.

@@ -8073,10 +8015,9 @@

-
-Next, add messages to your reactives -
-

+

Next, add messages to your reactives

+
+

You can insert diagnostic messages within your reactives using message() – here, I add a short message where each text and image output should be rendered. I can run my app and see messages successfully (or in the case of a broken app, unsuccessfully) print in my RStudio console as I interact with the app. You’ll notice that the each image message (e.g. “Displaying all penguins image”) prints when a new radioButton is selected, but those associated with the text outputs do not. This tells me that code is not being executed, beginning with first if statement inside renderText and that this is a good starting location for reviewing code (e.g. carefully crosschecking all inputIds and outputIds in that section).

@@ -8217,16 +8158,15 @@

-
-If helpful, use reactlog to visualize reactivity -
+

If helpful, use reactlog to visualize reactivity

+



reactlog is a package/tool that provides:

-

+

“A snapshot of the history (log) of all reactive interactions within a shiny application” - Barret Schloerke in his 2019 RSTUDIO::CONF talk, Reactlog 2.0: Debugging the state of Shiny

@@ -8237,89 +8177,83 @@
-

+

Reactivity can be confusing. I recommend watching Barret Schloerke’s talk, linked above, and reading through the Shiny Reactlog vignette as you get started.

-
-Using reactlog -
+

Using reactlog

+

reactlog should already be installed as a dependency of shiny (but be sure to import the package at the top of your script using library(reactlog)). When enabled, it provides an interactive browser-based tool to visualize reactive dependencies and executions in your app.

To use reactlog, follow these steps:

-

+

1. Load the reactlog library in you console (library(reactlog))

-

+

2. Call reactlog_enable() in your console

-

+

3. Run your app, interact with it, then quit your app

-

-4. Launch reactlog by running shiny::reactlogShow() in your console (or use the keyboard shortcut cmd/ctrl + F3) +

+4. Launch reactlog by running shiny::reactlogShow() in your console (or use the keyboard shortcut cmd/ctrl + F3)

-

+

5. Use your <- and -> arrow keys (or A gray, left-facing play button and A gray, right-facing play button) to move forward and backward through your app’s reactive life cycle

-



-

+


+

Read about the components of the status bar and the meaning of different reactive states in the reactlog vignette.

-
-Using reactlog to visualize reactivity in a correctly-functioning app -
-

-To visualize the reactive life cycle of the reactlog-working app, I’ll first load the reactlog library, then call reactlog_enable() in my console. Next, I’ll run my app and interact with it. By default, All penguins is selected. For demonstration purposes, I’ll click down the list (Sassy chinstrap, Staring gentoo, and finally Adorable adelie). When done, I’ll stop my app, then run shiny::reactlogShow() in the console to open the reactlog visualizer in a browser window. +

Using reactlog to visualize reactivity in a correctly-functioning app

+
+

+To visualize the reactive life cycle of the reactlog-working app, I’ll first load the reactlog library, then call reactlog_enable() in my console. Next, I’ll run my app and interact with it. By default, All penguins is selected. For demonstration purposes, I’ll click down the list (Sassy chinstrap, Staring gentoo, and finally Adorable adelie). When done, I’ll stop my app, then run shiny::reactlogShow() in the console to open the reactlog visualizer in a browser window.

-

+

Note: I’ve left the scatter plot on Tab 2 (and it’s related UI elements) commented out (as we practiced in the earlier few slides) for this demo – the reactlog package has many features that allow you to explore reactive dependencies across your whole app, but it can get complicated quickly. For demo purposes, we’re going to work with this “smaller” version of our app, which contains just the problematic code.

-
- -
-

-
-Interpreting reactlog (used with our correctly-functioning app) -
+ +

+

Interpreting reactlog (used with our correctly-functioning app)

+

There’s a lot to take when looking at the reactlog viewer, so let’s take it one step at a time:

-

+

(1) The radioButton input defaults to show the All penguins image and associated text. When we launch reactlog, our input , reactive expression , and outputs are Ready, meaning the calculated values are available (defaults in this case) and reactive elements have finished executing (i.e. the image and text is displayed). This Ready state is indicated by the green icons.

-

+

(2) I (the user) then updated the input by choosing Sassy chinstrap, invalidating (i.e. resetting) the input and thereby invalidating any dependencies – in this case both the image and text outputs. This Invalidating state is indicated by the gray icons.

-

+

(3) Once all dependencies are invalidated, the reactive elements can begin Calculating (i.e. executing) based on the new input (Sassy chinstrap). Elements are colored yellow when they are being calculated, then green when calculations are complete and the reactive element has been updated. In this example, first the image and then the text are calculated and updated.

-

+

(4) These same steps are repeated when I select the Staring gentoo, then Adorable adelie radioButtons

-
-Using reactlog to visualize reactivity in a broken app -
-

+

Using reactlog to visualize reactivity in a broken app

+
+

Let’s try out reactlog on our intentionally broken app (reactlog-broken, where our image changes when a radioButton user input is updated, but our text doesn’t appear). As in our functioning app, the All penguins image is selected by default. For demonstration purposes, I’ll select each option moving down the list (Sassy chinstrap, Staring gentoo, Adorable adelie) before launching reactlog.

-

+

Similar to our functioning app, the default input, All penguins, and image output are Ready (green). However, in this example our text output is not a dependency of our application’s input – there’s no linkage and the text output is Invalidated (gray).

-

+

As we click down the list of radioButtons, the image output is invalidated, then updated accordingly, but the text output remains disconnected from our input.

@@ -8331,13 +8265,12 @@

-
-So what’s the issue with our app? -
-

+

So what’s the issue with our app?

+
+

Evidence from our diagnostic messages and reactlog suggests that we should make sure that our UI and server are actually able to communicate about our desired text output. After careful inspection of our textOutput() and renderText() code, we find that a spelling error is to blame:

-

+

Our outputId in the UI is set to penguin_text:

@@ -8350,7 +8283,7 @@ )
-

+

But we call penguins_text when rendering our output in the server:

@@ -8366,79 +8299,70 @@ }) }
-

+

By updating our outputId to match in both the UI and the server, we fix our app.

-Code File - Testing + Testing

Creating automated tests for your apps can save time and effort, ensuring that they continue working as expected.

-
-Book Open - Learning Objectives for Testing -
-

+

Learning Objectives for Testing

+
+

After this section, you should:

-

-angle-right - understand some of the reasons why apps break and the benefit of having automated tests +

+ understand some of the reasons why apps break and the benefit of having automated tests

-

-angle-right - have a basic understanding of how to use the shinytest2 package to create regression tests +

+ have a basic understanding of how to use the shinytest2 package to create regression tests

-

-angle-right - know how to rerun tests +

+ know how to rerun tests

-

+

Packages introduced:

-

-Box Open -shinytest2: provides tools for creating and running automated tests on Shiny applications +

+ shinytest2: provides tools for creating and running automated tests on Shiny applications

Why test our Shiny apps?
+

It’s almost inevitable that apps will break – there are lots of reasons why this happens, but to name a few:

-

-angle-right - an upgraded R package(s) has a different behavior (this includes shiny) – this is especially relevant for those apps hosted on servers, where server software (including packages) may be updated by system administrators +

+ an upgraded R package(s) has a different behavior (this includes shiny) – this is especially relevant for those apps hosted on servers, where server software (including packages) may be updated by system administrators

-

-angle-right - you make changes to your app +

+ you make changes to your app

-

-angle-right - an external data source stops working or returns data in a different format than expected by your app +

+ an external data source stops working or returns data in a different format than expected by your app

@@ -8447,9 +8371,8 @@

-
-Enter the {shinytest2} package -
+

Enter the {shinytest2} package

+

The {shinytest2} package is a useful tool for conducting regression testing on shiny apps – or in other words, testing existing app behavior for consistency over time.

@@ -8459,7 +8382,7 @@

From the shinytest2 documentation:

-

+

shinytest2 uses testthat’s snapshot-based testing strategy. The first time it runs a set of tests for an application, it performs some scripted interactions with the app and takes one or more snapshots of the application’s state. These snapshots are saved to disk so that future runs of the tests can compare their results to them.

@@ -8470,13 +8393,12 @@
-

+

Rather than having to write tests by hand, you can interact with your app via the “app recorder” and shinytest2 will record the test code automatically for you. Simply rerun tests to check for consistency.

-
-shinytest2 resources & demos -
+

shinytest2 resources & demos

+

The following demo comes straight from the {shinytest2} vignette, though a similar app and testing workflow is demoed by Barret Schloerke in his recorded talk, Getting Started with {shinytest2} Part I || Example + basics.

@@ -8484,29 +8406,29 @@

Additional resources:

-

-angle-right - Barret Schloerke’s rstudio::conf(2022) talk, {shinytest2}: Unit testing for Shiny applications (recording) +

+ Barret Schloerke’s rstudio::conf(2022) talk, {shinytest2}: Unit testing for Shiny applications (recording)

-

-angle-right - Barret Schloerke’s 2022 Appsilon Shiny Confernce talk, {shinytest2} Testing Shiny with {testthat} (recording & GitHub repo) +

+ Barret Schloerke’s 2022 Appsilon Shiny Confernce talk, {shinytest2} Testing Shiny with {testthat} (recording & GitHub repo)

-

-angle-right -Shiny testing overview, by Winston Chang – this article discussed the shinytest2 predecessor, shinytest (which is now entering maintenance mode), but provides some helpful context and is worth a read +

+ Shiny testing overview, by Winston Chang – this article discussed the shinytest2 predecessor, shinytest (which is now entering maintenance mode), but provides some helpful context and is worth a read

-
-Let’s test the following app -
+

Let’s test the following app

+

This small app accepts a text input for users to type their name. When the “Greet” button is pressed, the app returns a short greeting message that says, “Hello name!

-To get started, create a subdirectory called /testing_app, add a file named app.R, and drop this code in your file. Take a moment to try out the app. +To get started, create a subdirectory called ~/testing_app, add a file named app.R, and drop this code in your file. Take a moment to try out the app.

+
+
+
~/testing_app/app.R
+
library(shiny)
 
 ui <- fluidPage(
@@ -8529,25 +8451,25 @@
 
 shinyApp(ui, server)
-

-
-Testing using shinytest2
+

+

Testing using shinytest2

+

Recording tests requires the following steps:

-

+

(1) Run record_test(<app-directory>) to launch the app recorder in browser window

-

+

(2) Interact with your application and tell the recorder to make an expectation (e.g. an expected value when inputX is updated) on the state at various points

-

+

(3) Quit the recorder to save and execute your tests

@@ -8555,23 +8477,22 @@

To test our app specifically, we’ll do the following:

-

+

(1) run shinytest2::record_test("testing_app") in the console to launch the recorder in a browser window

-

+

(2) interact with your app by first typing a name (e.g. Sam), then pressing the “Greet” button to display the output text

-

+

(3) click the “Expect Shiny values” button in the recorder app sidebar to set an expectation (this will record inputs, outputs, and exported values)

-

+

(4) give your test a name in the recorder app sidebar, then click “Save test and exit” - this will save the recorded test and setup the testing infrastructure, if it doesn’t exit already

-
-Creating our first test -
+

Creating our first test

+

Following the steps on the previous slide, creating your test should look similar to this:

@@ -8582,9 +8503,8 @@ Note: Your test is automatically run as soon as you save and exit the recorder. See the results of your test in your console (it should pass!).

-
-Test files are generated automatically -
+

Test files are generated automatically

+

After recording your first test, a /tests folder is generated, containing a number of different files and subdirectories. Some important files to note:

@@ -8597,7 +8517,7 @@
-

+

(located at <app-directory>/tests/testthat/setup-shinytest2.R); For more complex apps, you’ll often have support files (e.g. those contained in <app-directory>/R and/or global.R) – content from those files will be stored here so that it is made accessible to your test(s). Since we don’t have any support files for our rather small/somewhat simple app, you should only see the following:

@@ -8606,7 +8526,7 @@
-

+

(located at <app-directory>/tests/testthat/test-shinytest2.R); This test script contains your recorded test, and should automatically open when you finish recording and save your test. You can manually modify this test (e.g. add additional interactions and expectations), if you wish. Yours should look similar to this:

@@ -8621,7 +8541,7 @@
-

+

(located at <app-directory>/tests/testthat/_snaps/shinytest2/*_.png); This is a screenshot of your app from when app$expect_values() was called – this file should be tracked using git so that you know how your app visually changes over time. My .png file looks like this:

@@ -8629,7 +8549,7 @@
-

+

(located at <app-directory>/tests/testthat/_snaps/shinytest2/*.json); This is a JSON representation of the state of the app when app$expect_values() was called – you’ll see the state of all input, output, and export values at the time of the snapshot (we don’t have any exports in our example app, but we do have a name input and a greeting output). This file should be tracked with git so that you have a record of your expected results. Your .json file should look something like this:

@@ -8650,116 +8570,95 @@

-
-Tips for testing -
+

Tips for testing

+

-angle-right - Record subsequent tests following the same workflow, giving each a unique name. Run test_app("path/to/app") to run all test scripts in your app’s tests/testhat directory. + Record subsequent tests following the same workflow, giving each a unique name. Run test_app("path/to/app") to run all test scripts in your app’s tests/testhat directory.

-angle-right - Use record_test() fairly often – Barret Schloerke argues that you should make a test recording for each feature of your app (many little recordings are encouraged!) + Use record_test() fairly often – Barret Schloerke argues that you should make a test recording for each feature of your app (many little recordings are encouraged!)

-angle-right - Limit testing to objects under your control. For example, let’s say you have a reactive data frame that you then send to a DT::datatable – if package maintainers update the DT package, your output might change which could lead to false positive failed tests. Instead, test just your data frame that gets sent to DT. + Limit testing to objects under your control. For example, let’s say you have a reactive data frame that you then send to a DT::datatable – if package maintainers update the DT package, your output might change which could lead to false positive failed tests. Instead, test just your data frame that gets sent to DT.


-

+

This is only a brief intro to shinytest2! Dig into the documentation to learn more.

-

-Star - Part 6: Streamlining code -

-


-

-angle-right - Writing functions +

Part 6: Streamlining code

+
+

+ Writing functions

-

-angle-right - Shiny modules +

+ Shiny modules

-Code - Writing functions + Writing functions

Functions have many benefits and can improve your code base, particularly as your app grows in complexity

-
-Book Open - Learning Objectives - Functions -
-

+

Learning Objectives - Functions

+
+

By the end of this section, you should:

-

-angle-right - understand the benefits of turning UI elements and server logic into functions +

+ understand the benefits of turning UI elements and server logic into functions

-

-angle-right - know where to write/save your functions +

+ know where to write/save your functions

-

-angle-right - successfully turn a repeated input into a function +

+ successfully turn a repeated input into a function

-

-angle-right - successfully turn a piece of server logic into a function +

+ successfully turn a piece of server logic into a function

-

-
-Why write functions -
+

+

Why write functions

+

Functions are useful for a wide variety of reasons. Most notably:

-

-angle-right - reducing redundancy +

+ reducing redundancy

-

-angle-right - reducing complexity +

+ reducing complexity

-

-angle-right - increasing code comprehension +

+ increasing code comprehension

-

-angle-right - increasing testability +

+ increasing testability

Importantly, functions can live outside of your app file(s) (i.e. app.R or ui.R, server.R and global.R), helping you to break up/streamline your code. Hadley Wickham recommends creating a folder called /R inside your app’s directory (e.g. ~/app-directory/R/...) and:

-

+

(a) storing larger functions in their own files (e.g. ~/app-directory/R/{function-name}.R) and/or

-

+

(b) creating a utils.R file (e.g ~/app-directory/R/utils.R) to store smaller, simpler functions all in one script.

@@ -8767,11 +8666,10 @@

-
-Create a small app for function practice -
-

-Create a new subdirectory called functions-app/ and add your ui.R, server.R, and global.R files with the following code. Run your app to see how it functions. +

Create a small app for function practice

+
+

+Create a new subdirectory called ~/functions-app and add your ui.R, server.R, and global.R files with the following code. Run your app to see how it functions.

+
+
+
~/functions-app/ui.R
+
ui <- fluidPage(
   
   tags$h1("Demoing Functions"),
@@ -8831,8 +8733,13 @@
 ) # END fluidPage
+
+
+
+
~/functions-app/server.R
+
server <- function(input, output) {
   
   
@@ -8884,8 +8791,13 @@
 } # END server
+
+
+
+
~/functions-app/global.R
+
# load packages ----
 library(shiny)
 library(shinyWidgets)
@@ -8895,15 +8807,19 @@
 
-

-
-Identify code duplication in ui.R
+

+

Identify code duplication in ui.R

+

Let’s first focus on the UI – where do we have nearly identically duplicated code?

+
+
+
~/functions-app/ui.R
+
ui <- fluidPage(
   
   tags$h1("Demoing Functions"),
@@ -8947,21 +8863,21 @@
 ) # END fluidPage
-

-
-Write a function for adding a pickerInput to select for penguin species
-

+

+

Write a function for adding a pickerInput to select for penguin species

+
+

This app includes two pickerInputs, both of which allow users to select which penguin species to display data for. The only difference between both pickerInputs is the inputId. Let’s write a function for our penguin species pickerInput that we can use in place of these two, rather long, chunks of code.

-

+

First, create an /R folder inside your functions-app directory. Then, add a new script to this folder. I’m calling mine penguinSpp_pickerInput.R.

-

+

Since the only difference between our original two pickerInputs are their inputIds, we can write a function that takes inputId as an argument (Recall that inputIds must be unique within an app, so it makes sense that both of our pickerInputs have different inputIds).

-

+

Once written, source() your function script into global.R (if necessary) to make your function available for use in your app.

@@ -8972,6 +8888,10 @@
+
+
+
~/functions-app/R/penguinSpp_pickerInput.R
+
penguinSpp_pickerInput <- function(inputId) {
   pickerInput(inputId = inputId, label = "Select a species:",
               choices = c("Adelie", "Gentoo", "Chinstrap"),
@@ -8981,8 +8901,13 @@
 }
+
+
+
+
~/functions-app/global.R
+
# load packages ----
 library(shiny)
 library(shinyWidgets)
@@ -8996,14 +8921,18 @@
 
-

-
-Apply your function in ui.R
-

+

+

Apply your function in ui.R

+
+

Finally, replace your original UI code for building both pickerInputs with our penguinSpp_pickerInput() function, save, and run your app. It should look exactly the same as before!

+
+
+
~/functions-app/ui.R
+
ui <- fluidPage(
   
   tags$h1("Demoing Functions"),
@@ -9038,22 +8967,25 @@
   
 ) # END fluidPage
-

-
-We reduced code redundancy and increased readability!
+

+

We reduced code redundancy and increased readability!

+

By turning our pickerInput code into a function, we not only reduced ten lines of UI code into two, but we also made our UI code a bit easier to read – our function, penguinSpp_pickerInput() tells a reader/collaborator/future you exactly what that line of code is meant to do, which is create a pickerInput that allows users to select penguin species. Even without code comments or additional context, one may deduce what that line of code does.

-
-Turn reactives & rendered outputs into functions -
-

+

Turn reactives & rendered outputs into functions

+
+

Next, let’s see where we can streamline our server code using functions. We have two discrete sections of code – (1) a reactive data frame and scatterplot output and (2) a reactive data frame and histogram output.

-
server <- function(input, output) {
+
+
+
~/functions-app/server.R
+
+
server <- function(input, output) {
   
   # filter penguin species (scatterplot) ----
   filtered_spp_scatterplot <- reactive ({
@@ -9100,55 +9032,66 @@
   
 } # END server
-

-
-Create a function to builds our scatterplot
-

-First, create a new file in ~/function-app/R and name it build_penguin_histogram.R (or a name that is succinct/clear – I’m going to name my function similarly). +

+

Create a function to build our scatterplot

+
+

+First, create a new file in ~/function-app/R and name it build-penguin-scatterplot.R (or a name that is succinct/clear – I’m going to name my function similarly).

-

-The goal of my function is to filter the penguins data based on the user input and render our ggplot scatterplot. To start, I’m going to cut/paste both the code to generate the reactive filtered_spp_histogram data frame and the renderPlot() code from server.R into our build_penguin_histogram() function. +

+The goal of my function is to filter the penguins data based on the user input and render our ggplot scatterplot. To start, I’m going to cut/paste both the code to generate the reactive filtered_spp_scatterplot data frame and the renderPlot() code from server.R into our build_penguin_scatterplot() function.

-

+

Important: In isolation, our function does not know about the user input (input is not in our global environment, it’s only known within the server() function). Therefore, we must pass input as an argument to our function.

-

-Note that in R, functions return the last executed line – when we run build_penguin_histogram() in our server, it will return the object created by renderPlot(). +

+Note that in R, functions return the last executed line – when we run build_penguin_scatterplot() in our server, it will return the object created by renderPlot().

-
build_penguin_histogram <- function(input) {
+
+
+
~/functions-app/R/build-penguin-scatterplot.R
+
+
build_penguin_scatterplot <- function(input) {
   
-  # filter penguin spp ----
-  filtered_spp_histogram <- reactive ({
-    
+  # filter penguin species (scatterplot) ----
+  filtered_spp_scatterplot <- reactive ({
+
     penguins |>
-      filter(species %in% input$penguin_species_histogram_input)
-    
+      filter(species %in% input$penguin_species_scatterplot_input)
+
   })
   
-  # render histogram ----
-  renderPlot({
-   
-    ggplot(na.omit(filtered_spp_histogram()), 
-           aes(x = flipper_length_mm, fill = species)) +
-      geom_histogram() +
-      scale_fill_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) +
-      labs(x = "Flipper length (mm)", y = "Frequency",
-           fill = "Penguin species")
-    
-  })
-  
-}
+ # render the scatterplot output ---- + output$penguin_scatterplot <- renderPlot({ + + ggplot(na.omit(filtered_spp_scatterplot()), + aes(x = bill_length_mm, y = bill_depth_mm, + color = species, shape = species)) + + geom_point() + + geom_smooth(method = "lm", se = FALSE, aes(color = species)) + + scale_color_manual(values = c("Adelie" = "#FEA346", "Chinstrap" = "#B251F1", "Gentoo" = "#4BA4A4")) + + scale_shape_manual(values = c("Adelie" = 19, "Chinstrap" = 17, "Gentoo" = 15)) + + labs(x = "Flipper length (mm)", y = "Bill length (mm)", + color = "Penguin species", shape = "Penguin species") + + }) + +}
-

-
-Now use your function inside the server
-

+

+

Now use your function inside the server

+
+

Remember, the output of build_penguin_scatterplot() is renderPlot(), which is used to build our reactive scatterplot. Following our rules for creating reactivity, we need to save our function’s output to output$penguin_scatterplot. In doing so, we reduced 23 lines of code to 1 inside our server function.

+
+
+
~/functions-app/server.R
+
server <- function(input, output) {
   
   # filter data & create penguin scatterplot ----
@@ -9177,21 +9120,25 @@
 
 } # END server
-

-
-Build a function to create our histogram
-

+

+

Build a function to create our histogram

+
+

We can repeat a similar process to create a function for building our histogram:

+
+
+
~/functions-app/R/build-penguin-histogram.R
+
build_penguin_histogram <- function(input) {
   
   # filter penguin spp ----
@@ -9217,8 +9164,13 @@
 }
+
+
+
+
~/functions-app/server.R
+
server <- function(input, output) {
   
   # filter data & create penguin scatterplot ----
@@ -9232,71 +9184,66 @@
 
+

-Cubes - Writing modules + Writing modules

We can take our code abstraction a step further and bundle repeated UI & server components together into modules, streamlining our code and increasing efficiency.

-
-Book Open - Learning Objectives - Modules -
-

+

Learning Objectives - Modules

+
+

By the end of this section, you should have an intro-level understanding of:

-

-angle-right - what is a shiny module and when it might make sense to build one +

+ what is a shiny module and when it might make sense to build one

-

-angle-right - the structure of a shiny module +

+ the structure of a shiny module

-

-angle-right - how to use a module +

+ how to use a module

-

+

Packages introduced:

-

+

Box Open gapminder: data

-
-The utility of modules is best demonstrated by taking a look at an example app -
-

+

The utility of modules is best demonstrated by taking a look at an example app

+
+

This app, developed by Garrett Grolemund & Joe Cheng’s as part of their Modules lesson, taught at the 2016 Shiny Developer Conference, is a prime candidate for modularization.

-

+

It uses the gapminder data set to display life expectancy by GDP per capita from 1952 to 2007 for Africa, the Americas, Asia, Europe, Oceania, and all regions collectively. The only difference between each tab is the subset of data displayed.

-
-The gapminder app, which has 6 tabs, one for each global region. A bubble plot takes up the width of the ap and displays Life Expectancy by GDP per capita, where each bubble represents a country and the size of bubbles represent the population size of that country. An automated sliderInput advances through the years (1952-2007), and the plot updates accordingly. -
-

-
-The code for this app isn’t particularly complex, but it’s repetitive and long -
+ +The gapminder app, which has 6 tabs, one for each global region. A bubble plot takes up the width of the ap and displays Life Expectancy by GDP per capita, where each bubble represents a country and the size of bubbles represent the population size of that country. An automated sliderInput advances through the years (1952-2007), and the plot updates accordingly.

+

The code for this app isn’t particularly complex, but it’s repetitive and long

+
+
+
+
~/modularized-app/app.R
+
# app.R
 
 #..............................setup.............................
@@ -9620,16 +9567,15 @@
 # Run the application 
 shinyApp(ui = ui, server = server)
-

-
-Repeated code sections
-

+

+

Repeated code sections

+
+

Taking a closer look at the gapminder app code, we’ll see that the following sections of code are repeated for each region (6 times total; only code sections for “all” regions shown below):

-

-angle-right -tabPanel (UI) +

+ tabPanel (UI)

# "All" tabPanel (repeated 5 more times for each subregion) 
@@ -9639,9 +9585,8 @@
                      value = 1952, min = 1952, max = 2007, step = 5, 
                      animate = animationOptions(interval = 500)))
-

-angle-right - reactive data frame (server) +

+ reactive data frame (server)

# "All" reactive data frame (repeated 5 more times for each subregion)
@@ -9649,9 +9594,8 @@
   filter(all_data, year == input$all_year)
 })
-

-angle-right - reactive data frame (server) +

+ reactive data frame (server)

# "All" reactive data frame (repeated 5 more times for each subregion)
@@ -9659,9 +9603,8 @@
   filter(all_data, year == input$all_year)
 })
-

-angle-right - calculating date ranges (server) +

+ calculating date ranges (server)

# "All" date range (repeated 5 more times for each subregion)
@@ -9669,12 +9612,10 @@
 yrange_all <- range(all_data$lifeExp)

-
-Repeated code sections (cont.) -
-

-angle-right -renderPlot({}) (server) +

Repeated code sections (cont.)

+
+

+ renderPlot({}) (server)

# "All" plot (repeated 5 more times for each subregion)
@@ -9710,62 +9651,60 @@
   })

-
-Enter Shiny modules -
+

Enter Shiny modules

+

-angle-right -A shiny module is a piece of a shiny app – it can’t be run directly, but instead is included as part of a larger app. While functions work well for code that that is either completely on the client (UI) side or completely on the server side, modules can be written for code that spans both. + A shiny module is a piece of a shiny app – it can’t be run directly, but instead is included as part of a larger app. While functions work well for code that that is either completely on the client (UI) side or completely on the server side, modules can be written for code that spans both.

-angle-right -Modules can represent inputs, outputs, or both (we’ll be building a module that represents both). Motivation for building modules can range from enabling reuse of code (once created, modules can be reused within the same app or even across different apps), to breaking up a large, complex app into smaller, separate components. + Modules can represent inputs, outputs, or both (we’ll be building a module that represents both). Motivation for building modules can range from enabling reuse of code (once created, modules can be reused within the same app or even across different apps), to breaking up a large, complex app into smaller, separate components.

-angle-right -Modules help to solve a namespacing problem – recall that all Ids (e.g. inputIds) must be unique across your app. Namespacing is a system for organizing objects with identical names (similar to namespacing functions from particular packages using the syntax package::function() e.g. plyr::arrange() vs dplyr::arrange()). + Modules help to solve a namespacing problem – recall that all Ids (e.g. inputIds) must be unique across your app. Namespacing is a system for organizing objects with identical names (similar to namespacing functions from particular packages using the syntax package::function() e.g. plyr::arrange() vs dplyr::arrange()).

-
-What do modules look like? -
+

What do modules look like?

+

Modules are a coding pattern, organized into two functions: one that creates the UI elements and one that loads the server logic. They can look a bit different, depending on your module, but they generally follow this pattern:

-
# myModule.R
-#..........................ui function...........................
-
-myModuleUI <- function(id) {
-  
-  ns <- NS(id)
-  
-  tagList(
-    # inputs with ids wrapped in ns() (e.g. `sliderInput(id = ns("slider"))`)
-    # outputs with ids wrapped in ns() (e.g. `plotOutput(id = "ns(plot"))`)
-  )
-  
-}
-
-#........................server function.........................
-myModuleServer <- function(id, ...) { # where `...` includes any number of additional parameters
-  
-  moduleServer(id, function(input, output, session) {
-    
-    # server logic
-    
-  }) 
-  
-} 
+
+
+
myModule.R
+
+
#..........................ui function...........................
+
+myModuleUI <- function(id) {
+  
+  ns <- NS(id)
+  
+  tagList(
+    # inputs with ids wrapped in ns() (e.g. `sliderInput(id = ns("slider"))`)
+    # outputs with ids wrapped in ns() (e.g. `plotOutput(id = "ns(plot"))`)
+  )
+  
+}
+
+#........................server function.........................
+myModuleServer <- function(id, ...) { # where `...` includes any number of additional parameters
+  
+  moduleServer(id, function(input, output, session) {
+    
+    # server logic
+    
+  }) 
+  
+} 
-

-
-Where should I define/save my module?
+

+

Where should I define/save my module?

+

Part of the appeal of creating modules is breaking your long app.R (or ui.R & server.R) scripts into smaller pieces. Creating a separate R script to house a given module (both the UI and Server function components) is typically the best course of action (a good naming convention is giving it a descriptive name with the suffix “Module” e.g. gapModule.R). There are a variety of places you can write and/or save your modularized code to, but I recommend one of the following two options (at least while we’re just getting started on our shiny modules journey):

@@ -9773,7 +9712,7 @@

(1) save your modularized code script inside your app’s directory (e.g. ~/app-directory/myModule.R)

-

+

If you choose this option, call source("myModule.R") from global.R (if using ui.R/server.R) or app.R.

@@ -9781,21 +9720,24 @@

(2) save your modularized code script inside the /R subdirectory of your application (e.g. ~/app-directory/R/myModule.R)

-

+

If you choose this option, your module will automatically be sourced (as of Shiny 1.5.0) when the application is loaded.

-
-Breaking down the UI function: -
-

+

Breaking down the UI function:

+
+

The UI part of a module needs to do two things: (1) return a shiny element (e.g. an input & output), and (2) assign module elements to a unique namespace using NS(). NS() provides an easy way to help with namespacing within your module, ensuring that each time your module is called, a unique id is assigned.

-

+

The UI function for our gapminder module will look like this (NOTE: code comments below denote the general order of operations I followed when writing this UI function):

+
+
+
~/modularized-app/R/gapModule.R
+
# step 1: a good function naming convention is a descriptive base name, suffixed by `UI`
 
 gapModuleUI <- function(id) { # step 2: the first argument to a UI function should always be `id` -- this is the namespace for the module 
@@ -9814,20 +9756,24 @@
   
 } # END gapModuleUI function
-

+

+

Wrapping our input and output Ids in ns() will create unique Ids each time our module is called, preventing things from overwriting one one another. For example, if we call gapModuleUI(id = "myFirstModuleCall"), our outputId will be set to myFirstModuleCall-plot and our inputId will be set to myFirstModuleCall-year. Calling our module a second time (e.g. gapModuleUI(id = "mySecondModuleCall")) will generate two new unique Ids (e.g. mySecondModuleCall-plot & mySecondModuleCall-year).

-
-Breaking down the Server function: -
-

+

Breaking down the Server function:

+
+

The server part of a module looks very similar to a normal (i.e. non-modular) Shiny app server function. Begin by defining your module server function name and provide it with the first required parameter, id, along with any other necessary parameters (we also need to pass our particular function a data parameter to differentiate between data subsets (e.g. All vs. Africa vs. Asia etc.)).

-

+

Next, call moduleServer() inside your server function and pass it the id variable, along with the module function. The module function must have three parameters: input, output, and session. You do not have to use ns() to refer to inputs and outputs here. Copy server code from our original app, plop it inside the module function, and sub in our data parameter where ever a data frame subset is called.

+
+
+
~/modularized-app/R/gapModule.R
+
# step 1: a good function naming convention is a descriptive base name, suffixed by `Server`
 
 gapModuleServer <- function(id, data) { # step 2: the first argument to a server function should always be `id`, followed by any other necessary arguments; here we include a 'data' parameter, since we need to be able to tell our server function which data subset to plot in each tab
@@ -9880,14 +9826,18 @@
   
 } # END server function
-

-
-Now let’s use our module:
-

+

+

Now let’s use our module:

+
+

First, since we’ve saved our gapModule.R file to our app’s directory, we’ll need to source it at the top of our app.R file:

+
+
+
~/modularized-app/app.R
+
#..............................setup.............................
 library(shiny)
 library(gapminder) 
@@ -9903,14 +9853,18 @@
 europe_data <- filter(gapminder, continent == "Europe")
 oceania_data <- filter(gapminder, continent == "Oceania")
-

-
-Now let’s use our module:
-

+

+

Now let’s use our module:

+
+

Next, let’s use our module’s UI function. We’ll need to define/name each of our tabPanels (one for each of our six regions), but rather than building a plotOutput and sliderInput inside each tabPanel (each with unique Ids), we can instead call our gapModuleUI() function, and ensure that each time we call it to supply a unique character string for our id parameter.

+
+
+
~/modularized-app/app.R
+
#..............................setup.............................
 library(shiny)
 library(gapminder) 
@@ -9946,14 +9900,18 @@
   
 ) # END fluidPage
-

-
-Now let’s use our module:
-

+

+

Now let’s use our module:

+
+

Finally, we can re-write our server. Rather than writing out the lengthy code required to make each plot six times over, we can instead call our gapModuleServer() function, supplying each call with ids that match those used in gapModuleUI(), along with the appropriate data subset. Now, run your app! If written correctly, your app should run exactly the same as your initial version.

+
+
+
~/modularized-app/app.R
+
#..............................setup.............................
 library(shiny)
 library(gapminder) 
@@ -10004,214 +9962,171 @@
 # Run the application 
 shinyApp(ui = ui, server = server)
-

-
-Additional module resources
+

+

Additional module resources

+

We’ve barely scratched the surface of modules. Continue on with some of the following resources:

-

-angle-right -Modularizing Shiny App Code & associated materials, by Garrett Grolemund & Joe Cheng at the 2016 Shiny Developer’s Conference – NOTE: This 2016 talk is an excellent introduction to modules and is definitely worth a watch, especially because we just explored the exact example demoed by G. Grolemund. Please note, however, that Shiny modules were overhauled in 2020 with the introduction of moduleServer(). The code on the previous slides has been updated to reflect those changes, and therefore differs slightly from what’s taught in this video. +

+ Modularizing Shiny App Code & associated materials, by Garrett Grolemund & Joe Cheng at the 2016 Shiny Developer’s Conference – NOTE: This 2016 talk is an excellent introduction to modules and is definitely worth a watch, especially because we just explored the exact example demoed by G. Grolemund. Please note, however, that Shiny modules were overhauled in 2020 with the introduction of moduleServer(). The code on the previous slides has been updated to reflect those changes, and therefore differs slightly from what’s taught in this video.

-

-angle-right -Mastering Shiny, Ch. 19 - Shiny Modules, by Hadley Wickham +

+ Mastering Shiny, Ch. 19 - Shiny Modules, by Hadley Wickham

-

-angle-right -Modularizing Shiny app code, by Winston Chang +

+ Modularizing Shiny app code, by Winston Chang

-

-angle-right -Effective use of Shiny modules in application development, by Eric Nantz at rstudio::conf(2019) +

+ Effective use of Shiny modules in application development, by Eric Nantz at rstudio::conf(2019)

-

-angle-right -A beginners guide to Shiny modules, by Emily Riederer +

+A beginners guide to Shiny modules, by Emily Riederer

-

-Star - Part 7: Wrap-up -

-


-

-angle-right - Shiny alternatives +

Part 7: Wrap-up

+
+

+ Shiny alternatives

-

-angle-right - Words of wisdom +

+ Words of wisdom

-

-angle-right - More resources +

+ More resources

-Arrows left right - Consider if you need Shiny at all + Consider if you need Shiny at all

While a well-developed shiny app is fun and appealing, it’s worth having a conversation about whether shiny is truly necessary, or if taking an alternative approach to sharing your data might be better.

-
-Additional data presentation frameworks -
-

-Shiny is awesome, but depending on your goals, vision, and end users, you may not need the full functionality that shiny provides. You can potentially save yourself (and your clients) the stress of deploying and maintaining apps by first considering other options: +

Additional data presentation frameworks

+
+

+Shiny is awesome, but depending on your goals and end users, you may not need the full functionality that shiny provides. Importantly, you also can potentially save yourself (and your clients) the stress of deploying and maintaining shiny apps by first considering other options:

-
-

-angle-right - Widgets aren’t only for Shiny! Embed interactive htmlwidgets into your R Markdown documents that range from geo-spatial mapping with leaflet to generating network graph diagrams with DiagrammeR. Check out the htmlwidgets for R - gallery for many more options. + +Logos for htmlwidgets, flexdashboard, and quarto.

+

+ Embed interactive htmlwidgets into your R Markdown & Quarto markdown documents that range from geo-spatial mapping with leaflet to generating network graph diagrams with DiagrammeR. Check out the htmlwidgets for R - gallery for many more options.

-

-angle-right - Compose multiple widgets into a dashboard using flexdashboard. Based in R Markdown, this framework allows you to produce dynamic dashboards using tools you are already familiar with. Find example projects and their source code here. +

+ Compose multiple widgets into a dashboard using flexdashboard. Based in R Markdown, this framework allows you to produce dynamic dashboards using tools you are already familiar with. Find example projects and their source code.

-

-angle-right - Embed reactive shiny components (e.g. inputs & outputs) in Quarto documents. By using some fun new code chunk options, you can instruct Quarto to spin up it’s own self-contained shiny server to run your reactives. Read the Quarto documentation to learn more and check out some teaching examples here. +

+ Embed reactive shiny components (e.g. inputs & outputs) in Quarto documents. By using some fun new code chunk options, you can instruct Quarto to spin up it’s own self-contained shiny server to run your reactives. Read the Quarto documentation to learn more and check out some teaching examples.

-
-Logos for htmlwidgets, flexdashboard, and quarto. -

-
-Example flexdashboards built by some familiar folks Face Smile - -
-

-Sun -Energy Siting Dashboard (source code), developed by MEDS 2022 alumni Paloma Cartwright, Joe DeCesaro, Daniel Kerstan & Desik Somasundaram as part of their MEDS capstone project – explore predictions of the most suitable locations for large, utility-scale wind and solar projects across the United States +

Example flexdashboards built by some familiar folks

+
+

+ Energy Siting Dashboard (source code), developed by MEDS 2022 alumni Paloma Cartwright, Joe DeCesaro, Daniel Kerstan & Desik Somasundaram as part of their MEDS capstone project – explore predictions of the most suitable locations for large, utility-scale wind and solar projects across the United States

-

-Handshake -@ADELPHIRESEARCH TWEETS Dashboard (source code), developed by R-Lady Shannon Pileggi as part of a job interview – read about this clever approach to showcasing your skills to a potential employer in Shannon’s blogpost, A job interview presentation inspired by the R community. +

+ @ADELPHIRESEARCH TWEETS Dashboard (source code), developed by R-Lady Shannon Pileggi as part of a job interview – read about this clever approach to showcasing your skills to a potential employer in Shannon’s blogpost, A job interview presentation inspired by the R community.

-Lightbulb - Final Thoughts & Additional Resources + Final Thoughts & Additional Resources

We’ve covered a lot in this workshop, and we’ve only just begun to scratch the surface – we’ll end with some final thoughts/words of wisdom, along with some resources that are worth returning to as you begin your deep dive into shiny app development.

-
-Some takeaway messages that are worth keeping in mind -
-

-Chart Column - Oftentimes, the most time consuming part of building a shiny app is deciding on how to present the data visually – make your data visualizations first, outside of shiny, refine, decide which variables you think are important enough to make reactive, etc. THEN, build them into your shiny application. +

Some takeaway messages that are worth keeping in mind

+
+

+ Oftentimes, the most time consuming part of building a shiny app is deciding on how to present the data visually – make your data visualizations first, outside of shiny, refine, decide which variables you think are important enough to make reactive, etc. THEN, build them into your shiny application.

-

-table - Get your data in the most wrangled form possible before loading it into your app to avoid unnecessary slowdowns. +

+ Get your data in the most wrangled form possible before loading it into your app to avoid unnecessary slowdowns.

-

-Code - Code expands quickly – stay organized! Create a repository map (see an example in this README), so you and your collaborators know where files live and what they do. Use rainbow parentheses, add extra space between sections of code, and include clear code comments to denote the start and end of parentheses. This will help save headaches later on. +

+ Code expands quickly – stay organized! Create a repository map (see an example in this README), so you and your collaborators know where files live and what they do. Use rainbow parentheses, add extra space between sections of code, and include clear code comments to denote the start and end of parentheses. This will help save headaches later on.

-

-Users - Keep in mind the considerations for good UX/UI design. You’re building an app for the user first and foremost – be sure to assess often if your app is going to meet their needs. +

+ Keep in mind the considerations for good UX/UI design. You’re building an app for the user first and foremost – be sure to assess often if your app is going to meet their needs.

-

-Question - Before taking the plunge, consider if you really need shiny at all – maintaining apps can be challenging. What other options might you have for sharing your data with end users? +

+ Before taking the plunge, consider if you really need shiny at all – maintaining apps can be challenging. What other options might you have for sharing your data with end users?

-
-Great Shiny resources -
-

-A quick Google search will yield lots of online resources, forum discussions, video tutorials, etc. for building Shiny applications. Many are linked throughout these slides, but here’s an attempt at putting some of those that I referenced most often all in one spot (along with some that we didn’t have time to cover). Be sure to poke around online yourself, and let me know if you come across any that you find particularly useful so that I can add them here! +

Great Shiny resources

+
+

+A quick Google search will yield lots of online resources, forum discussions, video tutorials, etc. for building Shiny applications. Many are linked throughout these slides, but here’s an attempt at putting some of those that I referenced most often all in one spot (along with some that we didn’t have time to cover).

-

+

Books

-

-angle-right -Mastering Shiny, by Hadley Wickham +

+ Mastering Shiny, by Hadley Wickham

-

-angle-right -Engineering Production-Grade Shiny Apps, by Colin Fay, Sébastien Rochette, Vincent Guyader & Cervan Girard. +

+ Engineering Production-Grade Shiny Apps, by Colin Fay, Sébastien Rochette, Vincent Guyader & Cervan Girard.

-

-angle-right -Building Web Apps with R, by Lisa DeBruine – an short course (paired with an online book with instructions, resources, etc.) +

+ Building Web Apps with R, by Lisa DeBruine – an short course (paired with an online book with instructions, resources, etc.)

-

+

Tutorials

-

-angle-right -Building Shiny apps - an interactive tutorial, by Dean Attali +

+ Building Shiny apps - an interactive tutorial, by Dean Attali

-

-angle-right -Speeding Up R Shiny, by Jakub Sobolewski in R bloggers – details methods on improving app performance +

+Speeding Up R Shiny, by Jakub Sobolewski in R bloggers – details methods on improving app performance

-

-angle-right - Allison Horst’s The Basics of Building Shiny Apps in R workshop +

+ Allison Horst’s The Basics of Building Shiny Apps in R workshop

-

+

Tools

-

-angle-right -R Shiny & FontAwesome Icons – How to Use Them in Your Dashboards, by Dario Radečić in R Bloggers – instructions for setting up your fontawesome kit +

+ R Shiny & FontAwesome Icons – How to Use Them in Your Dashboards, by Dario Radečić in R Bloggers – instructions for setting up your fontawesome kit

-

-angle-right -Shiny UI Editor and Nick Strayer’s rstudio::conf(2022) talk introducing it – a visual tool for building the UI portion of a Shiny application that generates clean and human-readable code (currently in Alpha, as of January 2023) +

+ Shiny UI Editor and Nick Strayer’s rstudio::conf(2022) talk introducing it – a visual tool for building the UI portion of a Shiny application that generates clean and human-readable code (currently in Alpha, as of January 2023)

-

-angle-right - The {golem} package provides an opinionated framework for building production-grade shiny applications and is a part of a growing ecosystem of packages called the {golemverse}. There are lots of accompanying learning materials, including the book, Engineering Production Grade Shiny Apps, by Colin Fay, Sébastien Rochette, Vincent Guyader, and Cervan Girard. +

+ The {golem} package provides an opinionated framework for building production-grade shiny applications and is a part of a growing ecosystem of packages called the {golemverse}. There are lots of accompanying learning materials, including the book, Engineering Production Grade Shiny Apps, by Colin Fay, Sébastien Rochette, Vincent Guyader, and Cervan Girard.

-
-And don’t forget about Posit’s own excellent resources -
+

And don’t forget about Posit’s own excellent resources

+

Posit/RStudio’s great instructional resources, examples, and help documentation:

-

-angle-right - A thoughtfully organized Articles page +

+ A thoughtfully organized Articles page

-

-angle-right - The Shiny User Showcase, a collection of Shiny apps and their source code developed by the Shiny developer community – many of these featured apps are winners or honorable mentions of the annual Shiny contest! +

+ The Shiny User Showcase, a collection of Shiny apps and their source code developed by the Shiny developer community – many of these featured apps are winners or honorable mentions of the annual Shiny contest!

-

-angle-right -Shiny Demos, a series of apps created by the Shiny developers to highlight specific features of the shiny package – these are excellent resources to turn to when you are learning how to implement a new type of widget, working on the layout of your app, and more. +

+ Shiny Demos, a series of apps created by the Shiny developers to highlight specific features of the shiny package – these are excellent resources to turn to when you are learning how to implement a new type of widget, working on the layout of your app, and more.

@@ -10219,9 +10134,14 @@ I’m excited to see what Shiny new apps you all create!
-
-A gif of sparkles that appear to be moving out of a dark background towards the viewer. -
+
+
+
+

A gif of sparkles that appear to be moving out of a dark background towards the viewer.

+
+
+
+