KGW
karstengweinert.datasci.social.ap.brid.gy
KGW
@karstengweinert.datasci.social.ap.brid.gy
Reposted by KGW
RE: https://mastodon.social/@freakonometrics/115792358535180595

I wondered what would be the situation in France (and found the original source in the process): only 6 % of the population live within 5 km of the railway line Lille-Lyon-Marseille […]
Original post on piaille.fr
piaille.fr
December 29, 2025 at 12:16 AM
Reposted by KGW
This is a public service announcement to **never** ever use Oracle
February 1, 2025 at 6:20 PM
"public space incubator" scheint ein gutes Suchwort zu sein aktuell. #republica
May 27, 2025 at 6:18 PM
Interesting read on discrete Kelly strategies: https://win-vector.com/2024/12/21/kelly-betting-with-discrete-stakes/
win-vector.com
December 25, 2024 at 10:42 AM
Shiny Source Code Explained: Busy Indicators
<div class="elementor elementor-2358" data-elementor-id="2358" data-elementor-post-type="post" data-elementor-type="wp-post"> <section class="elementor-section elementor-top-section elementor-element elementor-element-1502057 elementor-section-boxed elementor-section-height-default elementor-section-height-default" data-element_type="section" data-id="1502057"> <div class="elementor-container elementor-column-gap-default"> <div class="elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-a299e72" data-element_type="column" data-id="a299e72"> <div class="elementor-widget-wrap elementor-element-populated"> <div class="elementor-element elementor-element-a550feb elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="a550feb" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>Shiny 1.9 (released in July 2024) comes with a new feature: a spinner overlay on outputs or a page-level pulsing banner that shows up whenever the app is busy. No more boring “faded out” outputs when they are recalculating, but a nice visual cue! Such visual cues improve loading experience for your users, ultimately decreasing perceived waiting time. And that’s what we want! But how does it work? What happens in the Shiny source code? How does Shiny know when an output is busy, and when to display a spinner? You will find out all about it in this edition of “Shiny Source Code Explained”! </p> </div> </div> <div class="elementor-element elementor-element-f529376 elementor-widget elementor-widget-heading" data-element_type="widget" data-id="f529376" data-widget_type="heading.default"> <div class="elementor-widget-container"> <h2 class="elementor-heading-title elementor-size-default">The new busy indicators</h2> </div> </div> <div class="elementor-element elementor-element-c050a3a elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="c050a3a" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>So what are we actually talking about? Two things: spinners and a page-level pulse banner. They look like this:</p> </div> </div> <div class="elementor-element elementor-element-d8a76f0 elementor-widget elementor-widget-image" data-element_type="widget" data-id="d8a76f0" data-widget_type="image.default"> <div class="elementor-widget-container"> <img alt='Different spinners as part of the Shiny busy indicators that appear when an output is recalculating after hitting the "Simulate" button' class="attachment-full size-full wp-image-2349" decoding="async" height="1200" loading="lazy" src="https://hypebright.nl/wp-content/uploads/2024/12/busy-indicator-gif.gif" width="1600"/> </div> </div> <div class="elementor-element elementor-element-f0e45b8 elementor-widget elementor-widget-image" data-element_type="widget" data-id="f0e45b8" data-widget_type="image.default"> <div class="elementor-widget-container"> <img alt='page-level pulsing banner Shiny busy indicator which shows with a custom linear-gradient when the button "Simulate side-effect" is clicked' class="attachment-full size-full wp-image-2345" decoding="async" height="400" loading="lazy" src="https://hypebright.nl/wp-content/uploads/2024/12/busy-indicator-gif-resize.gif" width="1400"/> </div> </div> <div class="elementor-element elementor-element-8c2b00a elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="8c2b00a" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>There’s a variety of spinner looks to choose from: bars, dots, pulse, ring… And you can even provide your own SVG file! You can also change things like color, size, pulse background, speed, and more.</p><p>When using <code>bslib</code>, like the examples above, the busy indicators are enabled by default. At the time of writing, December 2024, you have to enable them otherwise. However, in the future they will be turned on by default in Shiny as well.</p> </div> </div> <div class="elementor-element elementor-element-21be048 elementor-widget elementor-widget-heading" data-element_type="widget" data-id="21be048" data-widget_type="heading.default"> <div class="elementor-widget-container"> <h2 class="elementor-heading-title elementor-size-default">HTML dependencies</h2> </div> </div> <div class="elementor-element elementor-element-f187703 elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="f187703" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>Shiny needs dependencies to make it function the way it does. These dependencies are jQuery, and a bunch of JavaScript, Sass, CSS, and other required files that are attached to the HTML. For the new busy indicators, there’s also a new Shiny dependency: <code>busyIndicatorDependency</code>, which you can find in <a href="https://github.com/rstudio/shiny/blob/5bf0701939720c06b571f432e58eed0880273443/R/busy-indicators.R#L284" rel="noopener" target="_blank"><code>busy-indicators.R</code></a>.</p> </div> </div> <div class="elementor-element elementor-element-da0f878 elementor-widget elementor-widget-code-highlight" data-element_type="widget" data-id="da0f878" data-widget_type="code-highlight.default"> <div class="elementor-widget-container"> <div class="prismjs-tomorrow copy-to-clipboard"> <pre class="highlight-height language-r line-numbers" data-line="5, 7"> <code class="language-r" readonly="true"> <xmp>busyIndicatorDependency &lt;- function() { htmlDependency( name = "shiny-busy-indicators", version = get_package_version("shiny"), src = "www/shared/busy-indicators", package = "shiny", stylesheet = "busy-indicators.css", # TODO-future: In next release make spinners and pulse opt-out # head = as.character(useBusyIndicators()) ) }</xmp> </code> </pre> </div> </div> </div> <div class="elementor-element elementor-element-f9b18d9 elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="f9b18d9" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>As you can see, a folder is used for <code>src</code> (which can be found here: <a href="https://github.com/rstudio/shiny/tree/5bf0701939720c06b571f432e58eed0880273443/inst/www/shared/busy-indicators" rel="noopener" target="_blank"><code>www/shared/busy-indicators</code></a>), and a CSS stylesheet. The folder contains the stylesheet and the SVGs that make up the spinners.</p><p>This <code>busyIndicatorDependency</code> is used within the <code>shinyDependencies</code> function in <a href="https://github.com/rstudio/shiny/blob/5bf0701939720c06b571f432e58eed0880273443/R/shinyui.R#L114" rel="noopener" target="_blank"><code>shinyui.R</code></a>: </p> </div> </div> <div class="elementor-element elementor-element-90aca45 elementor-widget elementor-widget-code-highlight" data-element_type="widget" data-id="90aca45" data-widget_type="code-highlight.default"> <div class="elementor-widget-container"> <div class="prismjs-tomorrow copy-to-clipboard"> <pre class="highlight-height language-r line-numbers" data-line="4"> <code class="language-r" readonly="true"> <xmp>shinyDependencies &lt;- function() { list( bslib::bs_dependency_defer(shinyDependencyCSS), busyIndicatorDependency(), htmlDependency( name = "shiny-javascript", version = get_package_version("shiny"), src = "www/shared", package = "shiny", script = if (isTRUE( get_devmode_option( "shiny.minified", TRUE ) )) "shiny.min.js" else "shiny.js", all_files = FALSE ) ) }</xmp> </code> </pre> </div> </div> </div> <div class="elementor-element elementor-element-c91815d elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="c91815d" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>And in turn, <code>shinyDependencies</code> is used together with the jQuery dependency (<code>jqueryDependency</code>) in the <a href="https://github.com/rstudio/shiny/blob/5bf0701939720c06b571f432e58eed0880273443/R/shinyui.R#L31" rel="noopener" target="_blank"><code>renderPage()</code></a> function, which makes sure the dependencies are in the HTML. You can read more about how page rendering works in an earlier edition of “Shiny Source Code Explained”, namely <a href="https://hypebright.nl/nl/shiny/shiny-source-code-explained-rendering-the-ui/" rel="noopener" target="_blank">“Rendering the UI”</a>.</p> </div> </div> <div class="elementor-element elementor-element-0be99b6 elementor-widget elementor-widget-heading" data-element_type="widget" data-id="0be99b6" data-widget_type="heading.default"> <div class="elementor-widget-container"> <h2 class="elementor-heading-title elementor-size-default">Displaying the spinners and banner: the (S)CSS</h2> </div> </div> <div class="elementor-element elementor-element-ec1a9f4 elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="ec1a9f4" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>So the necessary CSS and SVG files that make the spinners and banner look the way they do are in the HTML and can be used. But how are they being displayed?!</p><p>For that, we need to take a closer look at the SCSS in <a href="https://github.com/rstudio/shiny/blob/5bf0701939720c06b571f432e58eed0880273443/srcts/extras/busy-indicators/busy-indicators.scss" rel="noopener" target="_blank"><code>srcts/extras/busy-indicators/busy-indicators.scss</code></a>. Note that this is not the CSS file that was added in the dependencies! SCSS stands for <strong>Sassy Cascading Style Sheets</strong>. It’s a CSS preprocessor that adds features like variables, nesting, and mixins to make styling easier. The (minified) CSS file is actually build from this SCSS file, because browsers can only understand plain CSS, not SCSS directly.</p><p>To make it easier to grasp, we’re going to look at this SCSS file in chunks. To start, let’s look at the positioning of the spinner for busy elements:</p> </div> </div> <div class="elementor-element elementor-element-af3ddfb elementor-widget elementor-widget-code-highlight" data-element_type="widget" data-id="af3ddfb" data-widget_type="code-highlight.default"> <div class="elementor-widget-container"> <div class="prismjs-tomorrow copy-to-clipboard"> <pre class="highlight-height language-scss line-numbers" data-line=""> <code class="language-scss" readonly="true"> <xmp>:where([data-shiny-busy-spinners] .recalculating) { position: relative; } /* This data atttribute is set by ui.busy_indicators.use() */ [data-shiny-busy-spinners] { .recalculating { &amp;::after { position: absolute; content: ""; /* ui.busy_indicators.spinner_options() */ --_shiny-spinner-url: var(--shiny-spinner-url, url(spinners/ring.svg)); --_shiny-spinner-color: var(--shiny-spinner-color, var(--bs-primary, #007bc2)); --_shiny-spinner-size: var(--shiny-spinner-size, 32px); --_shiny-spinner-delay: var(--shiny-spinner-delay, 1s); background: var(--_shiny-spinner-color); width: var(--_shiny-spinner-size); height: var(--_shiny-spinner-size); inset: calc(50% - var(--_shiny-spinner-size) / 2); mask-image: var(--_shiny-spinner-url); -webkit-mask-image: var(--_shiny-spinner-url); opacity: 0; animation-delay: var(--_shiny-spinner-delay); animation-name: fade-in; animation-duration: 250ms; animation-fill-mode: forwards; } } }</xmp> </code> </pre> </div> </div> </div> <div class="elementor-element elementor-element-70326d8 elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="70326d8" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>First things first, the data attribute called <code>data-shiny-busy-spinners</code>. This attribute is added to the parent element when busy indicators are enabled in Shiny using <code>useBusyIndicators()</code>. It signals that elements inside can show a loading spinner. It basically acts as a “flag” for styling spinners when the app is recalculating. This R code sets that flag (see <a href="https://github.com/rstudio/shiny/blob/5bf0701939720c06b571f432e58eed0880273443/R/busy-indicators.R#L53" rel="noopener" target="_blank"><code>busy-indicators.R</code></a>):</p> </div> </div> <div class="elementor-element elementor-element-29b80ea elementor-widget elementor-widget-code-highlight" data-element_type="widget" data-id="29b80ea" data-widget_type="code-highlight.default"> <div class="elementor-widget-container"> <div class="prismjs-tomorrow copy-to-clipboard"> <pre class="highlight-height language-r line-numbers" data-line=""> <code class="language-r" readonly="true"> <xmp>useBusyIndicators &lt;- function(..., spinners = TRUE, pulse = TRUE, fade = TRUE) { rlang::check_dots_empty() attrs &lt;- list("shinyBusySpinners" = spinners, "shinyBusyPulse" = pulse) js &lt;- vapply(names(attrs), character(1), FUN = function(key) { if (attrs[[key]]) { sprintf("document.documentElement.dataset.%s = 'true';", key) } else { sprintf("delete document.documentElement.dataset.%s;", key) } }) if (!fade) { res &lt;- tagList(res, fadeOptions(opacity = 1)) } res }</xmp> </code> </pre> </div> </div> </div> <div class="elementor-element elementor-element-859d009 elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="859d009" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>The function modifies the <code>document.documentElement.dataset</code> object to set or remove specific <code>data-*</code> attributes on the root <code>&lt;html&gt;</code> element, like:</p><ul><li><code>data-shiny-busy-spinners</code> (<code>shinyBusySpinners</code>)</li><li><code>data-shiny-busy-pulse</code> (<code>shinyBusyPulse</code>)</li></ul><p> </p><p>This ties directly to the <code>[data-shiny-busy-spinners]</code> CSS rule in the SCSS file.</p> </div> </div> <div class="elementor-element elementor-element-afdbb90 elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="afdbb90" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>That was the first line of SCSS, what else do we have? When an element in your Shiny app is busy, Shiny applies a <code>.recalculating</code> class to that element. The CSS above ensures a spinner is displayed using <code>::after</code>. <code>::after</code> is a <strong>CSS pseudo-element</strong> that lets you insert content after an element without modifying the HTML directly. It’s often used for decorative or functional purposes, like adding icons, tooltips, or in this case: spinners.</p><p>You can see that the styles are set for the spinner: the spinner appears as a masked image over the busy element. The spinner size, color, and image are customizable using CSS variables like <code>--_shiny-spinner-size</code>. We’ll come back to that a little bit later.</p><p>Spinners aren’t the only thing available as busy indicator: there’s also a page-level pulse banner. When the entire page is busy (but no specific spinner is visible), a glowing pulse bar appears at the top of the page:</p> </div> </div> <div class="elementor-element elementor-element-8fe7d91 elementor-widget elementor-widget-code-highlight" data-element_type="widget" data-id="8fe7d91" data-widget_type="code-highlight.default"> <div class="elementor-widget-container"> <div class="prismjs-tomorrow copy-to-clipboard"> <pre class="highlight-height language-scss line-numbers" data-line=""> <code class="language-scss" readonly="true"> <xmp>@mixin shiny-page-busy { /* ui.busy_indicators.pulse_options() */ --_shiny-pulse-background: var( --shiny-pulse-background, linear-gradient( 120deg, transparent, var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), transparent ) ); --_shiny-pulse-height: var(--shiny-pulse-height, 3px); --_shiny-pulse-speed: var(--shiny-pulse-speed, 1.2s); /* Color, sizing, &amp; positioning */ position: fixed; top: 0; left: 0; height: var(--_shiny-pulse-height); background: var(--_shiny-pulse-background); z-index: 9999; /* Animation */ animation-name: busy-page-pulse; animation-duration: var(--_shiny-pulse-speed); animation-direction: alternate; animation-iteration-count: infinite; animation-timing-function: ease-in-out; content: ""; /* Used in a ::after context */ }</xmp> </code> </pre> </div> </div> </div> <div class="elementor-element elementor-element-928e17f elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="928e17f" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>For the page-level pulse banner, a <strong>mixin</strong> is used. This is a reusable block of styles in SCSS. It allows you to define a set of CSS rules once and then include (or “mix in”) them wherever needed, saving time and avoiding code repetition. Handy. In this case, the mixin is used in two places:</p><ol><li>For page-level pulse with spinners, so when both spinners and a pulse banner are enabled.</li><li>For pulse-only, so when only the pulse banner is enabled (and no spinners).</li></ol> </div> </div> <div class="elementor-element elementor-element-2a29105 elementor-widget elementor-widget-code-highlight" data-element_type="widget" data-id="2a29105" data-widget_type="code-highlight.default"> <div class="elementor-widget-container"> <div class="prismjs-tomorrow copy-to-clipboard"> <pre class="highlight-height language-scss line-numbers" data-line=""> <code class="language-scss" readonly="true"> <xmp>/* Page-level pulse with spinners */ /* In spinners+pulse mode (the recommended default), show a page-level banner if the page is busy, but there are no recalculating elements. */ [data-shiny-busy-spinners][data-shiny-busy-pulse] { &amp;.shiny-busy::after { @include shiny-page-busy; } // Hide the pulse if there are spinners on the page // (Note: UI outputs don't get spinners) &amp;.shiny-busy:has(.recalculating:not(.shiny-html-output))::after { display: none; } &amp;.shiny-busy:has(#shiny-disconnected-overlay)::after { display: none; } } /* Pulse-only */ /* In pulse _only_ mode, show a page-level banner whenever shiny is busy. */ [data-shiny-busy-pulse]:not([data-shiny-busy-spinners]) { &amp;.shiny-busy::after { @include shiny-page-busy; } &amp;.shiny-busy:has(#shiny-disconnected-overlay)::after { display: none; } } </xmp> </code> </pre> </div> </div> </div> <div class="elementor-element elementor-element-f732a9b elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="f732a9b" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>The mixin defines the pulsing animation styles for the <code>::after</code> pseudo-element when the page is busy. It applies a linear gradient background and a pulsing animation for visual feedback. The page-level banner is hidden in certain conditions, such as when spinners are visible or when a disconnection overlay is active.</p><p>There are two animations that are important for a smooth appearance of the busy indicators: <code>fade-in</code>, and <code>busy-page-pulse</code>. These are defined using <strong>keyframes</strong>, which define the steps of an animation by specifying how an element’s styles should change at different points (percentages) during the animation:</p> </div> </div> <div class="elementor-element elementor-element-c5a2c1f elementor-widget elementor-widget-code-highlight" data-element_type="widget" data-id="c5a2c1f" data-widget_type="code-highlight.default"> <div class="elementor-widget-container"> <div class="prismjs-tomorrow copy-to-clipboard"> <pre class="highlight-height language-scss line-numbers" data-line=""> <code class="language-scss" readonly="true"> <xmp>/* Keyframes for the fading spinner */ @keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } } /* Keyframes for the pulsing banner */ @keyframes busy-page-pulse { 0% { left: -14%; right: 97%; } 45% { left: 0%; right: 14%; } 55% { left: 14%; right: 0%; } to { left: 97%; right: -14%; } }</xmp> </code> </pre> </div> </div> </div> <div class="elementor-element elementor-element-a53bfe1 elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="a53bfe1" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>There’s more code in the SCSS file, like making sure the spinner is visible on “dimmed” elements (which happens by default for busy elements), disabling spinners for problematic outputs (like <code>uiOutput()</code>), and preventing redundant spinners (in the case <code>shinycssloaders</code> is used).</p> </div> </div> <div class="elementor-element elementor-element-95e21ec elementor-widget elementor-widget-heading" data-element_type="widget" data-id="95e21ec" data-widget_type="heading.default"> <div class="elementor-widget-container"> <h2 class="elementor-heading-title elementor-size-default">The recalculating and shiny-busy CSS classes</h2> </div> </div> <div class="elementor-element elementor-element-29258d0 elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="29258d0" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>As you’ve seen, there are two flags that important for displaying the busy indicators in Shiny:</p><ol><li>The data attribute (either <code>data-shiny-busy-spinners</code>, or <code>data-shiny-busy-pulse</code>)</li><li>The <code>recalculating</code> CSS class (in the case of spinners) or <code>shiny-busy</code> CSS class (in the case of the page-level pulsing banner)</li></ol><p> </p><p>You know where the data attributes come from, but where do the CSS classes come from?!</p><p>It all starts with message handlers that are added in <a href="https://github.com/rstudio/shiny/blob/5bf0701939720c06b571f432e58eed0880273443/srcts/src/shiny/shinyapp.ts#L890" rel="noopener" target="_blank"><code>shinyapp.ts</code></a>. These message handlers can be called elsewhere to, surprise, send a message. We have one for “recalculating”, and one for “busy”. Let’s start with “recalculating”:</p> </div> </div> <div class="elementor-element elementor-element-70f6225 elementor-widget elementor-widget-code-highlight" data-element_type="widget" data-id="70f6225" data-widget_type="code-highlight.default"> <div class="elementor-widget-container"> <div class="prismjs-tomorrow copy-to-clipboard"> <pre class="highlight-height language-typescript line-numbers" data-line=""> <code class="language-typescript" readonly="true"> <xmp> addMessageHandler( "recalculating", (message: { name?: string; status?: "recalculated" | "recalculating"; }) =&gt; { if ( hasOwnProperty(message, "name") &amp;&amp; hasOwnProperty(message, "status") ) { const binding = this.$bindings[message.name as string]; if (binding) { $(binding.el).trigger("shiny:" + message.status); } else { $().trigger("shiny:" + message.status); } } } );</xmp> </code> </pre> </div> </div> </div> <div class="elementor-element elementor-element-a5fea20 elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="a5fea20" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>The “recalculating” one is a bit complex, as it handles a message with a particular name (e.g. the id of a Shiny output), and a status. It first needs to look up the binding (from <code>this.$bindings</code>) with the given name/id.  If the binding is found, it triggers a custom jQuery event on the element (<code>binding.el</code>). The event name is dynamically created by appending <code>message.status</code> to <code>"shiny:"</code>. This triggers either the events <code>"shiny:recalculating"</code> or <code>"shiny:recalculated"</code>, depending on the status. You can listen for these events in your custom JavaScript for example.</p><p>However, these events don’t explain how a CSS class like <code>recalculating</code> ends up in the output element. In fact, the Shiny source code itself doesn’t make use of these events.</p><p>Instead, we need to take a closer look at how output bindings are constructed. Going in-dept would require another edition of “Shiny Source Code Explained”, but it comes down to the <code>showProgress</code> method in the <code>OutputBinding</code> class (<code><a href="https://github.com/rstudio/shiny/blob/5bf0701939720c06b571f432e58eed0880273443/srcts/src/bindings/output/outputBinding.ts#L56" rel="noopener" target="_blank">srcts/src/bindings/output/outputBinding.ts</a></code>):</p> </div> </div> <div class="elementor-element elementor-element-8de065f elementor-widget elementor-widget-code-highlight" data-element_type="widget" data-id="8de065f" data-widget_type="code-highlight.default"> <div class="elementor-widget-container"> <div class="prismjs-tomorrow copy-to-clipboard"> <pre class="highlight-height language-typescript line-numbers" data-line=""> <code class="language-typescript" readonly="true"> <xmp>showProgress(el: HTMLElement, show: boolean): void { const recalcClass = "recalculating"; if (show) $(el).addClass(recalcClass); else $(el).removeClass(recalcClass); } </xmp> </code> </pre> </div> </div> </div> <div class="elementor-element elementor-element-6ce4842 elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="6ce4842" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>So there we have the CSS class! This <code>showProgress</code> method is called in various places, but most importantly:</p><ol><li>When the output is initialised</li><li>When the output is invalidated</li></ol><p> </p><p>This <a href="https://github.com/rstudio/shiny/blob/main/inst/diagrams/outputProgressStateMachine.svg">diagram</a> displays it nicely:</p> </div> </div> <div class="elementor-element elementor-element-9785610 elementor-widget elementor-widget-image" data-element_type="widget" data-id="9785610" data-widget_type="image.default"> <div class="elementor-widget-container"> <a href="https://github.com/rstudio/shiny/blob/main/inst/diagrams/outputProgressStateMachine.svg"> <img alt="" class="attachment-medium size-medium wp-image-2311" decoding="async" height="283" loading="lazy" sizes="(max-width: 300px) 100vw, 300px" src="https://hypebright.nl/wp-content/uploads/2024/12/shiny-progress-states-300x283.png" srcset="https://hypebright.nl/wp-content/uploads/2024/12/shiny-progress-states-300x283.png 300w, https://hypebright.nl/wp-content/uploads/2024/12/shiny-progress-states-768x723.png 768w, https://hypebright.nl/wp-content/uploads/2024/12/shiny-progress-states.png 998w" width="300"/> </a> </div> </div> <div class="elementor-element elementor-element-f00f5ff elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="f00f5ff" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>So yes, rather complicated! You can also just believe that somewhere in the source code the <code>recalculating</code> class is added and the CSS for the busy indicators reacts in an appropriate manner.</p><p>Let’s now focus on the simpler “busy” message handler:</p> </div> </div> <div class="elementor-element elementor-element-a7f61da elementor-widget elementor-widget-code-highlight" data-element_type="widget" data-id="a7f61da" data-widget_type="code-highlight.default"> <div class="elementor-widget-container"> <div class="prismjs-tomorrow copy-to-clipboard"> <pre class="highlight-height language-typescript line-numbers" data-line="4"> <code class="language-typescript" readonly="true"> <xmp> addMessageHandler("busy", (message: "busy" | "idle") =&gt; { if (message === "busy") { $(document.documentElement).addClass("shiny-busy"); $(document).trigger("shiny:busy"); } else if (message === "idle") { $(document.documentElement).removeClass("shiny-busy"); $(document).trigger("shiny:idle"); } });</xmp> </code> </pre> </div> </div> </div> <div class="elementor-element elementor-element-2ea0e27 elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="2ea0e27" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>This message handler can have two messages: “busy” or “idle”. When it receives the message “busy”, it will add the <code>shiny-busy</code> class to the document. If the message is “idle”, it will remove the <code>shiny-busy</code> class. Easy peasy.</p> </div> </div> <div class="elementor-element elementor-element-741234d elementor-widget elementor-widget-heading" data-element_type="widget" data-id="741234d" data-widget_type="heading.default"> <div class="elementor-widget-container"> <h2 class="elementor-heading-title elementor-size-default">Busy indicator options</h2> </div> </div> <div class="elementor-element elementor-element-d1e25d9 elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="d1e25d9" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>As mentioned previously, it’s possible to customize the spinners and the page-level pulsing banner to your liking. You can do that by including <code>busyIndicatorOptions(...)</code> in your UI. This is the code behind this function:</p> </div> </div> <div class="elementor-element elementor-element-ccacc7e elementor-widget elementor-widget-code-highlight" data-element_type="widget" data-id="ccacc7e" data-widget_type="code-highlight.default"> <div class="elementor-widget-container"> <div class="prismjs-tomorrow copy-to-clipboard"> <pre class="highlight-height language-r line-numbers" data-line=""> <code class="language-r" readonly="true"> <xmp>busyIndicatorOptions &lt;- function( ..., spinner_type = NULL, spinner_color = NULL, spinner_size = NULL, spinner_delay = NULL, spinner_selector = NULL, fade_opacity = NULL, fade_selector = NULL, pulse_background = NULL, pulse_height = NULL, pulse_speed = NULL ) { rlang::check_dots_empty() res &lt;- tagList( spinnerOptions( type = spinner_type, color = spinner_color, size = spinner_size, delay = spinner_delay, selector = spinner_selector ), fadeOptions(opacity = fade_opacity, selector = fade_selector), pulseOptions( background = pulse_background, height = pulse_height, speed = pulse_speed ) ) bslib::as.card_item(dropNulls(res)) } spinnerOptions &lt;- function(type = NULL, color = NULL, size = NULL, delay = NULL, selector = NULL) { if (is.null(type) &amp;&amp; is.null(color) &amp;&amp; is.null(size) &amp;&amp; is.null(delay) &amp;&amp; is.null(selector)) { return(NULL) } url &lt;- NULL if (!is.null(type)) { stopifnot(is.character(type) &amp;&amp; length(type) == 1) if (file.exists(type) &amp;&amp; grepl("\\.svg$", type)) { typeRaw &lt;- readBin(type, "raw", n = file.info(type)$size) url &lt;- sprintf("url('data:image/svg+xml;base64,%s')", rawToBase64(typeRaw)) } else { type &lt;- rlang::arg_match(type, .busySpinnerTypes) url &lt;- sprintf("url('spinners/%s.svg')", type) } } # Options controlled via CSS variables. css_vars &lt;- htmltools::css( "--shiny-spinner-url" = url, "--shiny-spinner-color" = htmltools::parseCssColors(color), "--shiny-spinner-size" = htmltools::validateCssUnit(size), "--shiny-spinner-delay" = delay ) id &lt;- NULL if (is.null(selector)) { id &lt;- paste0("spinner-options-", p_randomInt(100, 1000000)) selector &lt;- sprintf(":has(&gt; #%s)", id) } css &lt;- HTML(paste0(selector, " {", css_vars, "}")) tags$style(css, id = id) } fadeOptions &lt;- function(opacity = NULL, selector = NULL) { if (is.null(opacity) &amp;&amp; is.null(selector)) { return(NULL) } css_vars &lt;- htmltools::css( "--shiny-fade-opacity" = opacity ) id &lt;- NULL if (is.null(selector)) { id &lt;- paste0("fade-options-", p_randomInt(100, 1000000)) selector &lt;- sprintf(":has(&gt; #%s)", id) } css &lt;- HTML(paste0(selector, " {", css_vars, "}")) tags$style(css, id = id) } pulseOptions &lt;- function(background = NULL, height = NULL, speed = NULL) { if (is.null(background) &amp;&amp; is.null(height) &amp;&amp; is.null(speed)) { return(NULL) } css_vars &lt;- htmltools::css( "--shiny-pulse-background" = background, "--shiny-pulse-height" = htmltools::validateCssUnit(height), "--shiny-pulse-speed" = speed ) tags$style(HTML(paste0(":root {", css_vars, "}"))) }</xmp> </code> </pre> </div> </div> </div> <div class="elementor-element elementor-element-3a7f08b elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="3a7f08b" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>There are three functions that take care of the different options:</p><ol><li><code>spinnerOptions()</code></li><li><code>fadeOptions()</code></li><li><code>pulseOptions()</code></li></ol><p> </p><p>Each of these option functions set CSS variables, like <code>--shiny-spinner-color</code>. They form a block of CSS that is injected into the page using <code>tags$style</code>. And these CSS variables are subsequently used in the SCSS you saw earlier. You can call it magic, or just very clever :).</p> </div> </div> <div class="elementor-element elementor-element-67ab024 elementor-widget elementor-widget-heading" data-element_type="widget" data-id="67ab024" data-widget_type="heading.default"> <div class="elementor-widget-container"> <h2 class="elementor-heading-title elementor-size-default">Loading experience and perceived waiting time</h2> </div> </div> <div class="elementor-element elementor-element-4ad14ec elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="4ad14ec" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>All this effort for some spinners and a banner, you might think. Why are they important?</p><p>While you do your best to create an app that feels amazingly fast, some loading time will be unavoidable. In that case, the only thing you can do, is making the waiting experience for your end-user as pleasant as possible. There are few ways to do so, and visual cues like spinners already tick some boxes to reduce the perceived waiting time and improve user satisfaction. Curious about what else you can do? You can check out <a href="https://youtu.be/YrCX0FlXsW0?si=DEBSIXgW_-J0Lg6J" rel="noopener" target="_blank">“Reduce Perceived Waiting Time in Shiny Apps”</a> on YouTube 🎥 .</p> </div> </div> <div class="elementor-element elementor-element-654a3c9 elementor-widget elementor-widget-heading" data-element_type="widget" data-id="654a3c9" data-widget_type="heading.default"> <div class="elementor-widget-container"> <h2 class="elementor-heading-title elementor-size-default">Shiny Source Code Explained: other editions</h2> </div> </div> <div class="elementor-element elementor-element-31e0379 elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="31e0379" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>In a few months there could be a book about the Shiny source code… Who knows! For now, you’ll have to do with separate blogs, and these are the previous editions:</p><ul><li><a href="https://hypebright.nl/index.php/en/2024/06/24/shiny-source-code-explained-rendering-the-ui-2/" rel="noopener" target="_blank">Rendering the UI</a></li><li><a href="https://hypebright.nl/index.php/2024/08/28/shiny-source-code-explained-launching-the-server/" rel="noopener" target="_blank">Launching the Server</a></li><li><a href="https://hypebright.nl/nl/shiny/shiny-source-code-explained-the-use-of-r6/" rel="noopener" target="_blank">The use of R6</a></li></ul> </div> </div> <div class="elementor-element elementor-element-11e13df elementor-widget elementor-widget-heading" data-element_type="widget" data-id="11e13df" data-widget_type="heading.default"> <div class="elementor-widget-container"> <h2 class="elementor-heading-title elementor-size-default">Wrap up</h2> </div> </div> <div class="elementor-element elementor-element-0334a3b elementor-widget elementor-widget-text-editor" data-element_type="widget" data-id="0334a3b" data-widget_type="text-editor.default"> <div class="elementor-widget-container"> <p>Another mystery in the Shiny source code uncovered- and this is quite a new one! I hope you had fun reading and learning about the Shiny busy indicators.</p><p>☕️ Was this useful to you and would you like to support me? You can <a href="https://bmc.link/veerlevleemput">buy me a coffee</a>!</p><p>📚 Want to learn more about Shiny? Discover my online Shiny courses: <a href="https://athlyticz.com/shiny-ii" rel="noopener" target="_blank">ProductioniZing Shiny</a> , <a href="https://athlyticz.com/shiny-iii" rel="noopener" target="_blank">CustomiZing WidgetZ</a> and <a href="https://athlyticz.com/shiny-mobile" rel="noopener" target="_blank">Mobile StructureZ</a>.</p><p>I provide R and Shiny consultancy. Do you need help with a project? Reach out and <a href="https://hypebright.nl/index.php/en/home-en/">let’s have a chat</a>!</p> </div> </div> </div> </div> </div> </section> </div>
hypebright.nl
December 18, 2024 at 6:47 PM