Shiny’s marvelous execution algorithm

You need a framework with range

  • Data projects start simple but end complicated
  • Limited frameworks lead to painful refactoring
  • Does your framework solve tomorrow’s problems?

What is Shiny?

  • Framework for building fast, extensible applications
  • Pure Python implementation released last year
  • Easy enough for rapid prototyping
  • Everything you need to build a product

Example: Model training

Gradio implementation

with gr.Blocks() as demo:
    sampled_data = gr.State(None)
    ...
    def plot_metrics(data, metric):
        if metric == "ROC Curve":
            return plot_auc_curve(data, "is_electronics", "training_score")
        else:
            return plot_precision_recall_curve(
                data, "is_electronics", "training_score"
            )

    account.select(sample_data, [slider], [sampled_data]).then(
        dist_plot, [sampled_data, log_scale], [tip_plot]
    ).then(plot_metrics, [sampled_data], [hist_plot])

    metric.select(plot_tips, [sampled_data, log_scale], [tip_plot])

Manual state management

with gr.Blocks#| () as demo:
    sampled_data = gr.State(None)
    ...
    def plot_metrics(data, metric):
        if input.metric() == "ROC Curve":
            return plot_auc_curve(data, "is_electronics", "training_score")
        else:rn plot_precision_recall_curve(    data, "is_electronics", "training_score"
            )

    account.select(sample_data, [slider], [sampled_data]).then(
        dist_plot, [sampled_data, log_scale], [tip_plot]
    ).then(plot_metrics, [sampled_data], [hist_plot])

    metric.select(plot_tips, [sampled_data, log_scale], [tip_plot])

Manual callback management

with gr.Blocks() as demo:
    sampled_data = gr.State(None)
    ...
    def plot_metrics(data, metric):
        if input.metric() == "ROC Curve":
            return plot_auc_curve(data, "is_electronics", "training_score")
        else:
            return plot_precision_recall_curve(
                data, "is_electronics", "training_score"
            )

    account.select(sample_data, [slider], [sampled_data]).then(
        dist_plot, [sampled_data, log_scale], [tip_plot]
    ).then(plot_metrics, [sampled_data], [hist_plot])

    metric.select(plot_metrics, [sampled_data, log_scale], [tip_plot])

How did Shiny do that?

    @render.plot
    def score_dist():
        df_filtered = df[df["account"] == input.account()]
        return dist_plot(df_filtered)

    @render.plot
    def metric():
        df_filtered = df[df["account"] == input.account()]
        if input.metric() == "ROC Curve":
            return plot_auc_curve(df_filtered, "is_electronics", "training_score")
        else:
            return plot_precision_recall_curve(
                df_filtered, "is_electronics", "training_score"
            )
  • We told Shiny what to do
  • We didn’t tell Shiny when to do it

How do other frameworks work?

  • Streamlit: re-render everything everywhere all the time
  • Dash/Gradio/Solara: Event-handling

Event driven programming

  • Manually define which behaviour triggers callback function
  • You have to do it
  • Easy to get wrong
  • Hard to tell when you’ve gotten it wrong

What’s a better way?

Generic DAG

Shiny’s Strategy

  • Infer the relationships between components
  • Build a computation graph
  • Use graph to minimally re-execute your application

Does that really work?

  • For this to work, the inference has to be 100% reliable
  • Only useful if you understand and trust the inference

How would you do this?

    @render.plot
    def score_dist():
        df_filtered = df[df["account"] == input.account()]
        return dist_plot(df_filtered)

    @render.plot
    def metric():
        df_filtered = df[df["account"] == input.account()]
        if input.metric() == "ROC Curve":
            return plot_auc_curve(df_filtered, "is_electronics", "training_score")
        else:
            return plot_precision_recall_curve(
                df_filtered, "is_electronics", "training_score"
            )

Static code analysis

#| standalone: true
#| components: [viewer]
#| layout: horizontal
#| viewerHeight: 500
import shinyswatch
from htmltools import css

from shiny import App, module, reactive, render, ui

app_ui = ui.page_fixed(
    {"class": "my-5"},
    shinyswatch.theme.minty(),
    ui.panel_title("Shiny TodoMVC"),
    ui.layout_sidebar(
        ui.panel_sidebar(
            ui.input_text("todo_input_text", "", placeholder="Todo text"),
            ui.input_action_button("add", "Add to-do"),
        ),
        ui.panel_main(
            ui.output_text("cleared_tasks"),
            ui.div(id="tasks", style="margin-top: 0.5em"),
        ),
    ),
)


def server(input, output, session):
    finished_tasks = reactive.Value(0)
    task_counter = reactive.Value(0)

    @output
    @render.text
    def cleared_tasks():
        return f"Finished tasks: {finished_tasks()}"

    @reactive.Effect
    @reactive.event(input.add)
    def add():
        counter = task_counter.get() + 1
        task_counter.set(counter)
        id = "task_" + str(counter)
        ui.insert_ui(
            selector="#tasks",
            where="beforeEnd",
            ui=task_ui(id),
        )

        finish = task_server(id, text=input.todo_input_text())

        # Defining a nested reactive effect like this might feel a bit funny but it's the
        # correct pattern in this case. We are reacting to the `finish`
        # event within the `add` closure, so nesting the reactive effects
        # means that we don't have to worry about conflicting with
        # finish events from other task elements.
        @reactive.Effect
        @reactive.event(finish)
        def iterate_counter():
            finished_tasks.set(finished_tasks.get() + 1)

        ui.update_text("todo_input_text", value="")


# Modules to define the rows


@module.ui
def task_ui():
    return ui.output_ui("button_row")


@module.server
def task_server(input, output, session, text):
    finished = reactive.Value(False)

    @output
    @render.ui
    def button_row():
        button = None
        if finished():
            button = ui.input_action_button("clear", "Clear", class_="btn-warning")
        else:
            button = ui.input_action_button("finish", "Finish", class_="btn-default")

        return ui.row(
            ui.column(4, button),
            ui.column(8, text),
            class_="mt-3 p-3 border align-items-center",
            style=css(text_decoration="line-through" if finished() else None),
        )

    @reactive.Effect
    @reactive.event(input.finish)
    def finish_task():
        finished.set(True)

    @reactive.Effect
    @reactive.event(input.clear)
    def clear_task():
        ui.remove_ui(selector=f"div#{session.ns('button_row')}")

        # Since remove_ui only removes the HTML the reactive effects will be held
        # in memory unless they're explicitly destroyed. This isn't a big
        # deal because they're very small, but it's good to clean them up.
        finish_task.destroy()
        clear_task.destroy()

    # Returning the input.finish button to the parent scope allows us
    # to react to it in the parent context to keep track of the number of
    # completed tasks.
    #
    # This is a good pattern because it makes the module more general.
    # The same module can be used by different applications which may
    # do different things when the task is completed.
    return input.finish


app = App(app_ui, server)

## file: requirements.txt
shinyswatch

Runtime tracing

  • Watch what components ask for
  • Keep track of those relationships
  • Use relationships to trigger rendering

User asks for output

from shiny import Inputs, Outputs, Session, App, render, ui

app_ui = ui.page_fluid(
    ui.input_slider("n", "N", 0, 100, 20),
    ui.output_text_verbatim("txt"),
)

def server(input: Inputs, output: Outputs, session: Session):
    @render.text
    def txt():
        return f"n*2 is {input.n() * 2}"


app = App(app_ui, server)

Rendering function is triggered

from shiny import Inputs, Outputs, Session, App, render, ui

app_ui = ui.page_fluid(
    ui.input_slider("n", "N", 0, 100, 20),
    ui.output_text_verbatim("txt"),
)

def server(input: Inputs, output: Outputs, session: Session):
    @render.text
    def txt():
        return f"n*2 is {input.n() * 2}"


app = App(app_ui, server)

Renderer needs input

from shiny import Inputs, Outputs, Session, App, render, ui

app_ui = ui.page_fluid(
    ui.input_slider("n", "N", 0, 100, 20),
    ui.output_text_verbatim("txt"),
)

def server(input: Inputs, output: Outputs, session: Session):
    @render.text
    def txt():
        return f"n*2 is {input.n() * 2}"


app = App(app_ui, server)

Input retrieved from UI

from shiny import Inputs, Outputs, Session, App, render, ui

app_ui = ui.page_fluid(
    ui.input_slider("n", "N", 0, 100, 20),
    ui.output_text_verbatim("txt"),
)


def server(input: Inputs, output: Outputs, session: Session):
    @render.text
    def txt():
        return f"n*2 is {input.n() * 2}"


app = App(app_ui, server)

Reactive graph

flowchart TD
  S[Input] --> Sc((Output))

Drawing our application graph

Initial state

flowchart TD
  C[Metric\nSelector] --> Sc((Metric\nPlot))
    Sl[Account\nSelector]  --> Sc 
  Sl --> M((Dist Plot))
  linkStyle 0 display:none
  linkStyle 1 display:none
  linkStyle 2 display:none

Calculate scatter plot

flowchart TD
  C[Metric\nSelector] --> Sc((Metric\nPlot)):::changed
    Sl[Account\nSelector]  --> Sc 
  Sl --> M((Dist Plot))
  linkStyle 0 display:none
  linkStyle 1 display:none
  linkStyle 2 display:none
  classDef changed fill:#f96

Calculate metric plot

flowchart TD
  C[Metric\nSelector] --> Sc((Metric\nPlot)):::changed
    Sl[Account\nSelector]  --> Sc 
  Sl --> M((Dist Plot))
  linkStyle 2 display:none
  classDef changed fill:#f96

Calculate distribution

flowchart TD
  C[Metric\nSelector] --> Sc((Metric\nPlot))
    Sl[Account\nSelector]  --> Sc 
  Sl --> M((Dist Plot)):::changed
  linkStyle 2 display:none
  classDef changed fill:#f96

Calculate distribution

flowchart TD
  C[Metric\nSelector] --> Sc((Metric\nPlot))
    Sl[Account\nSelector]  --> Sc 
  Sl --> M((Dist Plot)):::changed
  classDef changed fill:#f96

Reactive graph

flowchart TD
  C[Metric\nSelector] --> Sc((Metric\nPlot))
    Sl[Account\nSelector]  --> Sc 
  Sl --> M((Dist Plot))

Account changes

flowchart TD
  C[Metric\nSelector] --> Sc((Metric\nPlot))
  Sl[Account\nSelector]:::changed --> Sc 
  Sl --> M((Dist Plot))
  
  classDef changed fill:#f96

Invalidated

flowchart TD
  C[Metric\nSelector] --> Sc((Metric\nPlot)):::changed
  Sl[Account\nSelector]:::changed --> Sc 
  Sl --> M((Dist Plot)):::changed
  
  classDef changed fill:#f96

Forget dependencies

flowchart TD
  C[Metric\nSelector] --> Sc((Metric\nPlot))
  Sl[Account\nSelector] --> Sc 
  Sl --> M((Dist Plot))
  
  classDef changed fill:#f96
  linkStyle 0 display:none
  linkStyle 1 display:none
  linkStyle 2 display:none

Recalculate

flowchart TD
  C[Metric\nSelector] --> Sc((Metric\nPlot)):::changed
  Sl[Account\nSelector] --> Sc 
  Sl --> M((Dist Plot))
  
  classDef changed fill:#f96
  linkStyle 0 display:none
  linkStyle 1 display:none
  linkStyle 2 display:none

Recalculate

flowchart TD
  C[Metric\nSelector] --> Sc((Metric\nPlot)):::changed
  Sl[Account\nSelector] --> Sc 
  Sl --> M((Dist Plot))
  
  classDef changed fill:#f96
  linkStyle 2 display:none

Recalculate

flowchart TD
  C[Metric\nSelector] --> Sc((Metric\nPlot))
  Sl[Account\nSelector] --> Sc 
  Sl --> M((Dist Plot)):::changed
  
  classDef changed fill:#f96
  linkStyle 2 display:none

Recalculate

flowchart TD
  C[Metric\nSelector] --> Sc((Metric\nPlot))
  Sl[Account\nSelector] --> Sc 
  Sl --> M((Dist Plot)):::changed
  
  classDef changed fill:#f96

Updated

flowchart TD
  C[Metric\nSelector] --> Sc((Metric\nPlot))
  Sl[Account\nSelector] --> Sc 
  Sl --> M((Dist Plot))
  
  classDef changed fill:#f96

Metric changes

flowchart TD
  C[Metric\nSelector]:::changed --> Sc((Metric\nPlot))
  Sl[Account\nSelector] --> Sc 
  Sl --> M((Dist Plot))
  
  classDef changed fill:#f96

Invalidated

flowchart TD
  C[Metric\nSelector]:::changed --> Sc((Metric\nPlot)):::changed
  Sl[Account\nSelector] --> Sc 
  Sl --> M((Dist Plot))
  
  classDef changed fill:#f96

Forget dependencies

flowchart TD
  C[Metric\nSelector] --> Sc((Metric\nPlot)):::changed
  Sl[Account\nSelector] --> Sc 
  Sl --> M((Dist Plot))
  
  classDef changed fill:#f96
  linkStyle 0 display:none
  linkStyle 1 display:none

Recalculate

flowchart TD
  C[Metric\nSelector] --> Sc((Metric\nPlot)):::changed
  Sl[Account\nSelector] --> Sc 
  Sl --> M((Dist Plot))
  
  classDef changed fill:#f96
  linkStyle 0 display:none
  linkStyle 1 display:none

Recalculate

flowchart TD
  C[Metric\nSelector] --> Sc((Metric\nPlot)):::changed
  Sl[Account\nSelector] --> Sc 
  Sl --> M((Dist Plot))
  
  classDef changed fill:#f96

Updated

flowchart TD
  C[Metric\nSelector] --> Sc((Metric\nPlot))
  Sl[Account\nSelector] --> Sc 
  Sl --> M((Dist Plot))
  
  classDef changed fill:#f96

Graphs can change

#| standalone: true
#| components: [viewer]
#| layout: horizontal
#| viewerHeight: 500
from shiny import Inputs, Outputs, Session, App, reactive, render, req, ui
import random

app_ui = ui.page_sidebar(
    ui.sidebar(
        ui.input_radio_buttons(
            "choice", "Which slider?", choices=["Slider 1", "Slider 2"]
        ),
        ui.input_slider("slider_1", "Slider 1", 0, 100, 15),
        ui.input_slider("slider_2", "Slider 2", 0, 100, 45),
    ),
    ui.card({"style": "font-size: larger"}, ui.output_text("txt")),
)


def server(input: Inputs, output: Outputs, session: Session):
    @render.text
    def txt():
        random_number = random.randint(0, 100)
        if input.choice() == "Slider 1":
            return f"Slider 1 is {input.slider_1()}, a random number is {random_number}"
        else:
            return f"Slider 2 is {input.slider_2()}, a random number is {random_number}"


app = App(app_ui, server)

Different graphs

@render.text
def txt():
    random_number = random.randint(0, 100)
    if input.choice() == "Slider 1":
        return (
            f"Slider 1 is {input.slider_1()}, "
            f"a random number is {random_number}"
        )
    else: 
        return (
            f"Slider 2 is {input.slider_2()}, "
            f"a random number is {random_number}"
        )

Initial state

flowchart TD
  C[Buttons] --> T((Text Output))
  S1[Slider 1] --> T 
  S2[Slider 2] --> T

  linkStyle 0 display:none
  linkStyle 1 display:none
  linkStyle 2 display:none
  
  classDef changed fill:#f96

Calculate Text

flowchart TD
  C[Buttons] --> T((Text Output)):::changed
  S1[Slider 1] --> T 
  S2[Slider 2] --> T

  linkStyle 0 display:none
  linkStyle 1 display:none
  linkStyle 2 display:none
  
  classDef changed fill:#f96

Fetch button value

flowchart TD
  C[Buttons]:::changed --> T((Text Output)):::changed
  S1[Slider 1] --> T 
  S2[Slider 2] --> T

  linkStyle 1 display:none
  linkStyle 2 display:none
  
  classDef changed fill:#f96

Fetch Slider 1

flowchart TD
  C[Buttons] --> T((Text Output)):::changed
  S1[Slider 1]:::changed --> T 
  S2[Slider 2] --> T

  linkStyle 2 display:none
  
  classDef changed fill:#f96

Complete

flowchart TD
  C[Buttons] --> T((Text Output))
  S1[Slider 1] --> T 
  S2[Slider 2] --> T

  linkStyle 2 display:none
  
  classDef changed fill:#f96

Slider 2 changes

flowchart TD
  C[Buttons] --> T((Text Output))
  S1[Slider 1] --> T 
  S2[Slider 2]:::changed --> T

  linkStyle 2 display:none
  
  classDef changed fill:#f96

Nothing happens

flowchart TD
  C[Buttons] --> T((Text Output))
  S1[Slider 1] --> T 
  S2[Slider 2] --> T

  linkStyle 2 display:none
  
  classDef changed fill:#f96

Slider 1 changes

flowchart TD
  C[Buttons] --> T((Text Output))
  S1[Slider 1]:::changed --> T 
  S2[Slider 2] --> T

  linkStyle 2 display:none
  
  classDef changed fill:#f96

Invalidate

flowchart TD
  C[Buttons] --> T((Text Output)):::changed
  S1[Slider 1]:::changed --> T 
  S2[Slider 2] --> T

  linkStyle 0 display:none
  linkStyle 1 display:none
  linkStyle 2 display:none

  classDef changed fill:#f96

Calculate Text

flowchart TD
  C[Buttons] --> T((Text Output)):::changed
  S1[Slider 1] --> T 
  S2[Slider 2] --> T

  linkStyle 0 display:none
  linkStyle 1 display:none
  linkStyle 2 display:none
  
  classDef changed fill:#f96

Fetch button value

flowchart TD
  C[Buttons]:::changed --> T((Text Output)):::changed
  S1[Slider 1] --> T 
  S2[Slider 2] --> T

  linkStyle 1 display:none
  linkStyle 2 display:none
  
  classDef changed fill:#f96

Fetch Slider 1

flowchart TD
  C[Buttons] --> T((Text Output)):::changed
  S1[Slider 1]:::changed --> T 
  S2[Slider 2] --> T

  linkStyle 2 display:none
  
  classDef changed fill:#f96

Complete

flowchart TD
  C[Buttons] --> T((Text Output))
  S1[Slider 1] --> T 
  S2[Slider 2] --> T

  linkStyle 2 display:none
  
  classDef changed fill:#f96

Buttons change

flowchart TD
  C[Buttons]:::changed --> T((Text Output))
  S1[Slider 1] --> T 
  S2[Slider 2] --> T

  linkStyle 2 display:none
  
  classDef changed fill:#f96

Invalidate

flowchart TD
  C[Buttons]:::changed --> T((Text Output)):::changed
  S1[Slider 1] --> T 
  S2[Slider 2] --> T

  linkStyle 2 display:none
  
  classDef changed fill:#f96

Calculate Text

flowchart TD
  C[Buttons] --> T((Text Output)):::changed
  S1[Slider 1] --> T 
  S2[Slider 2] --> T

  linkStyle 0 display:none
  linkStyle 1 display:none
  linkStyle 2 display:none
  
  classDef changed fill:#f96

Fetch button value

flowchart TD
  C[Buttons]:::changed --> T((Text Output)):::changed
  S1[Slider 1] --> T 
  S2[Slider 2] --> T

  linkStyle 1 display:none
  linkStyle 2 display:none
  
  classDef changed fill:#f96

Fetch Slider 2!

flowchart TD
  C[Buttons] --> T((Text Output)):::changed
  S1[Slider 1] --> T 
  S2[Slider 2]:::changed --> T

  linkStyle 1 display:none
  
  classDef changed fill:#f96

Different graph

flowchart TD
  C[Buttons] --> T((Text Output))
  S1[Slider 1] --> T 
  S2[Slider 2] --> T

  linkStyle 1 display:none
  
  classDef changed fill:#f96

Reactivity scales

  • Every Shiny app uses this pattern
  • Works for dynamic UIs
  • Lazy and efficient

Reactive calculations

Saving and reusing calculated values

  • So far we’ve been working with shallow reactive graphs
    • Inputs are directly consumed by rendering functions
    • Limited
    • Not that efficient
  • @reactive.Calc creates calculations whose results are used by other functions
  • This adds depth to the reactive graph

Example: Model Monitoring

What do I want?

  1. Query the database for a sample between dates
  2. Filter sample by account name in memory
  3. Send that data to the plotting functions
  4. Cache the results of 1 and 2
  5. Invalidate a cache only when upstream inputs change
  6. Do no thinking or work

What do I want?

  1. Query the database for a sample between dates
  2. Filter sample by account id in memory
  3. Send that data to the plotting functions
  4. Cache the results of 1 and 2
  5. Invalidate a cache only when upstream inputs change
  6. Do no thinking or work

Reactive calculations

  • Defined with the @reactive.Calc decorator
  • Caches its value, so it’s cheap to call repeatedly
  • Adds a node to the reactive graph
    • Discards cached value when upstream nodes invalidate
    • Notifies downstream nodes when it invalidates

Reactive Calculation to the rescue

   @reactive.Calc
    def sampled_data():
        start_date, end_date = input.dates()
        start_date = pd.to_datetime(start_date)
        end_date = pd.to_datetime(end_date)
        return query_db(start_date, end_date, input.sample_size())

    @reactive.Calc()
    def filtered_data():
        filtered = sampled_data()
        filtered = filtered.loc[filtered["account"] == input.account()]
        return filtered

    @render.plot():
    def scores():
        return plot_scores(filtered_data())

Reactive Calculation to the rescue

   @reactive.Calc
    def sampled_data():
        start_date, end_date = input.dates()
        start_date = pd.to_datetime(start_date)
        end_date = pd.to_datetime(end_date)
        return query_db(start_date, end_date, input.sample_size())

    @reactive.Calc()
    def filtered_data():
        filtered = sampled_data()
        filtered = filtered.loc[filtered["account"] == input.account()]
        return filtered

    @render.plot():
    def scores():
        return plot_scores(filtered_data())

Initial state

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account] --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96
  linkStyle 0,1,2,3,4,5 display:none

Generate Model Scores

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account] --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores)):::changed
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96
  linkStyle 0,1,2,3,4,5 display:none

Get filtered Reactive Calc

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account] --> F
  Sa --> F{{Filtered}}:::changed
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96
  linkStyle 0,1,2,3,5 display:none

Get Account input

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account]:::changed --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96
  linkStyle 0,1,3,5 display:none

Get Sample Reactive Calc

flowchart TD
  D[Dates] --> Sa{{Sample}}:::changed 
  S[Sample Size] --> Sa
  A[Account] --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96
  linkStyle 0,1,5 display:none

Get Other inputs

flowchart TD
  D[Dates]:::changed  --> Sa{{Sample}}
  S[Sample Size]:::changed  --> Sa
  A[Account] --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96
  linkStyle 5 display:none

Plot API Responses

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account] --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores))
  F --> P1((API\nResponse)):::changed 
  
  classDef changed fill:#f96
  linkStyle 5 display:none

Get Filter reactive calc

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account] --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96

Account changes

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account]:::changed  --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96

Invalidate Filtered

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account]  --> F
  Sa --> F{{Filtered}}:::changed
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96
  linkStyle 2,3 display:none

Invalidate Plots

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account]  --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores)):::changed
  F --> P1((API\nResponse)):::changed
  
  classDef changed fill:#f96
  linkStyle 2,3,4,5 display:none

Calculate model scores

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account]  --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores)):::changed
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96
  linkStyle 2,3,4,5 display:none

Get filtered calc

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account]  --> F
  Sa --> F{{Filtered}}:::changed
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96
  linkStyle 2,3,5 display:none

Get Account and Sample

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account]:::changed  --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96
  linkStyle 5 display:none

Calculate API Response

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account]  --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores))
  F --> P1((API\nResponse)):::changed
  
  classDef changed fill:#f96
  linkStyle 5 display:none

Get Filtered Calc

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account]  --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96

Sample size changes

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size]:::changed --> Sa
  A[Account]  --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96

Invalidate Sample

flowchart TD
  D[Dates] --> Sa{{Sample}}:::changed 
  S[Sample Size] --> Sa
  A[Account]  --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96
  linkStyle 0,1 display:none

Invalidate Filter

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account]  --> F
  Sa --> F{{Filtered}}:::changed 
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96
  linkStyle 0,1,2,3 display:none

Invalidate plots

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account]  --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores)):::changed 
  F --> P1((API\nResponse)):::changed 
  
  classDef changed fill:#f96
  linkStyle 0,1,2,3,4,5 display:none

Initial state

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account] --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96
  linkStyle 0,1,2,3,4,5 display:none

Generate Model Scores

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account] --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores)):::changed
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96
  linkStyle 0,1,2,3,4,5 display:none

Get filtered Reactive Calc

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account] --> F
  Sa --> F{{Filtered}}:::changed
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96
  linkStyle 0,1,2,3,5 display:none

Get Account input

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account]:::changed --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96
  linkStyle 0,1,3,5 display:none

Get Sample Reactive Calc

flowchart TD
  D[Dates] --> Sa{{Sample}}:::changed 
  S[Sample Size] --> Sa
  A[Account] --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96
  linkStyle 0,1,5 display:none

Get Other inputs

flowchart TD
  D[Dates]:::changed  --> Sa{{Sample}}
  S[Sample Size]:::changed  --> Sa
  A[Account] --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96
  linkStyle 5 display:none

Plot API Responses

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account] --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores))
  F --> P1((API\nResponse)):::changed 
  
  classDef changed fill:#f96
  linkStyle 5 display:none

Get Filter reactive calc

flowchart TD
  D[Dates] --> Sa{{Sample}}
  S[Sample Size] --> Sa
  A[Account] --> F
  Sa --> F{{Filtered}}
  F --> P2((Model\nScores))
  F --> P1((API\nResponse))
  
  classDef changed fill:#f96

Events and Effects

  • Reactivity is a great default
  • Not everything fits this pattern
    • You want to specify when something happens
    • You want to batch inputs
    • You want to trigger a side effect
  • Event-driven programming ain’t all bad

Example: Data annotation

What do we want

  • Annotation shouldn’t react automatically
  • We want to manually specify what happens when the button is clicked

Reactive effects

@reactive.Effect
@reactive.event(input.is_electronics)
def mark_yes():
    update_annotation(df(), id=selected_row()["id"], annotation="electronics")

@reactive.Effect
@reactive.event(input.not_electronics)
def mark_no():
    update_annotation(df(), id=selected_row()["id"], annotation="not_electronics")

Reactive effect

@reactive.Effect
@reactive.event(input.is_electronics)
def mark_yes():
    update_annotation(df(), id=selected_row()["id"], annotation="electronics")

@reactive.Effect
@reactive.event(input.not_electronics)
def mark_no():
    update_annotation(df(), id=selected_row()["id"], annotation="not_electronics")

Reactive event

@reactive.Effect
@reactive.event(input.is_electronics)
def mark_yes():
    update_annotation(df(), id=selected_row()["id"], annotation="electronics")

@reactive.Effect
@reactive.event(input.not_electronics)
def mark_no():
    update_annotation(df(), id=selected_row()["id"], annotation="not_electronics"),

Reactive event

@reactive.Effect
@reactive.event(input.is_electronics)
def mark_yes():
    update_annotation(df(), id=selected_row()["id"], annotation="electronics")

@reactive.Effect
@reactive.event(input.not_electronics)
def mark_no():
    update_annotation(df(), id=selected_row()["id"], annotation="not_electronics")

Things to note

  • @reactive.event can be paired with rendering functions and reactive.Calc
  • Adds event-driven chocolate chips into the reactive cookie
  • Using reactive.event everywhere is a code smell

Side effects vs values

  • @reactive.Effect is for side effects:
    • Updating a database
    • Deploying a model
    • Writing a CSV
  • @reactive.Calc is for values
    • Running a calculation
    • Fetching data from a database
    • Filtering a data frame

Other patterns

  • Use reactive.isolate to prevent cycles
  • Include data in reactive graph with reactive.poll
  • Include time with reactive.invalidate_later
  • Store things in variables with reactive.Value

Summary

  • Shiny creates performant apps with very little work
  • Its algorithm is elegant, but not magic
  • Your framework should be able to handle what your users want
  • Choose a framework that can grow with your application

Thank you!

https://github.com/gshotwell/shiny-algorithm

Shiny for Python