Skip to content
ImpactMojo ImpactMojo
Browse Membership
Back to Code Studio
Code Studio · Runs in your browser

Shiny Dashboards for Development Data

Build interactive dashboards in R and in Python with Shiny. Learn the ui and server model, inputs, outputs and reactivity, prototype the summary on this page, then open a working district dashboard in Posit's Shinylive editor.

Two kinds of cell on this page. Ordinary R and Python cells run here, as in the other Code Studio courses: the first Run downloads the engine once (R about 7 MB, Python about 10 MB), with households.csv and districts.csv loaded for you. Shiny cells carry an Open in Shinylive button instead. It opens the app on Posit's Shinylive site in a new tab. A Shinylive app cannot read this page's files, so every app here carries its own small data frame typed into the code.
Module 1 of 8

What Shiny is

Shiny is a framework from Posit for building interactive web pages out of R or Python code. You write the analysis you already know, add a few controls, and a programme officer can pick a state or an indicator from a menu and see the chart change, without opening R or Python themselves.

It began as an R package. Shiny for Python is a separate package with the same ideas and slightly different spelling. This course teaches both side by side, so you can use whichever language your team already works in.

A Shiny app usually needs a server running R or Python. Shinylive removes that: it runs R (through webR) or Python (through Pyodide) inside the visitor's browser, so the browser is both the client and the server. That is how the apps in this course open with nothing installed.

Where the apps run. A Shiny cell does not run on this page. Open in Shinylive packs the code into a link and opens it in a new tab on shinylive.io, Posit's site, where the app starts in an editor beside its code. The first start downloads R or Python into your browser (about 13 MB for a Python app, by Posit's own count), so give it time on a slow connection. The code travels after the # in the link, which browsers do not send to the server, so Posit's site does not receive your app's code.

Your first app

Click Open in Shinylive. When the app appears on the right of the Shinylive editor, type a different district into the box and watch the line below it change.

The same app in Shiny for Python:

Try it: in the Shinylive editor, change the text inside paste() or the f-string, then re-run the app from the editor. Edits made there stay on Posit's page; to keep them, copy the code back into a file of your own.

Module 2 of 8

Two halves: ui and server

Every Shiny app has two parts.

In R the server reads input$state and writes to output$summary. In Python it reads input.state() (note the brackets: it is a function call) and the output is a function decorated with @render.text whose name is the output id.

This app embeds a ten-row data frame of district results. The data is invented for teaching Illustrative data; module 5 shows where the numbers come from. Pick a state and read the sentence.

Try it: change pct_toilet to report the highest value instead of the mean (max() in R, .max() in Python), and edit the sentence to match.

Module 3 of 8

Inputs and outputs

Inputs and outputs come in pairs of functions: one in the ui to place them, one in the server to fill an output. The ones a monitoring dashboard needs most:

This app uses a slider, a tick box and a table. Move the slider: the table lists the districts whose toilet coverage is below the threshold.

A threshold slider makes a cut-off visible and adjustable. When a dashboard flags districts as "lagging", say where the cut-off came from, and let the reader see what moves when it changes.

Try it: change the slider's default value to 70 and its step to 1, then re-run the app in the Shinylive editor.

Module 4 of 8

Reactivity: write the filter once

Shiny keeps track of which inputs each output reads. When an input changes, Shiny reruns only the outputs that depend on it. This is reactivity, and it is why you never write "when the menu changes, redraw the chart" yourself.

When two outputs need the same filtered data, do not filter twice. Put the filter in a reactive expression: reactive({ ... }) in R, a function decorated with @reactive.calc in Python. Call it like a function, selected(), from each output. Shiny computes it once per change and both outputs share the result.

Keep heavy work (reading a file, joining tables, fitting a model) outside the server function or inside a reactive expression. Code at the top of the app runs once when the app starts; code inside an output runs every time that output updates.

Try it: add a third output that shows the highest mean_pc_exp among the selected districts. Place it in the ui and fill it from selected().

Module 5 of 8

Prototype the summary before it goes into the app

A dashboard is an analysis with controls attached. Get the analysis right first, in ordinary code you can run and check, and only then wrap it in Shiny. These cells run on this page, on households.csv and districts.csv. Illustrative data Both tables are invented for teaching; the district names are real places, but no number describes them.

Step 1: one row per district

Join households to districts, turn the Yes/No columns into 0 or 100, and average by district. Run it in R, then switch the tab to Python and run again.

Ten rows, one per district, and the same numbers in both languages. R lists the rows by district and Python by state; the values agree. Kozhikode has the highest mean expenditure, 6,758 rupees a month, and Rewa the lowest toilet coverage, 50 per cent. These ten rows are the data frame the apps in this course carry.

Step 2: the filter and the chart, with the inputs as plain variables

Before there is a menu, the reader's choices are just two variables. Write the filter and the chart with them. When this works, each variable becomes an input, and the code moves into the server almost unchanged.

The three Madhya Pradesh districts, sorted: Indore 83.3, Betul 75.0 and Rewa 50.0 per cent, and a bar chart with the highest at the top. The rows are reversed just before plotting because a horizontal bar chart draws the first row at the bottom.

Try it: set state to "Kerala" and run again. Then follow the same steps for has_bank_account.

Module 6 of 8

The district dashboard in R

This app puts modules 2 to 5 together. The sidebar has three inputs: a state (or All), an indicator and a sort switch. The main panel has a horizontal bar chart and a table, both fed by one reactive expression. The data frame at the top holds the ten rows that the Step 1 cell printed, typed in, because a Shinylive app cannot read households.csv from this page.

Where the apps run. A Shiny cell does not run on this page. Open in Shinylive packs the code into a link and opens it in a new tab on shinylive.io, Posit's site, where the app starts in an editor beside its code. The first start downloads R or Python into your browser (about 13 MB for a Python app, by Posit's own count), so give it time on a slow connection. The code travels after the # in the link, which browsers do not send to the server, so Posit's site does not receive your app's code.

How the code maps to what you see

Try it: add a fifth indicator. Put a column into the data frame (for example pct_shg, the share in a self-help group, computed the way Step 1 computes the others), then add a line for it to labels. The menu, chart and table pick it up with no other change.

Module 7 of 8

The district dashboard in Shiny for Python

The same dashboard in Shiny for Python, using the same ten rows. The structure is identical; the spelling differs.

Where the apps run. A Shiny cell does not run on this page. Open in Shinylive packs the code into a link and opens it in a new tab on shinylive.io, Posit's site, where the app starts in an editor beside its code. The first start downloads R or Python into your browser (about 13 MB for a Python app, by Posit's own count), so give it time on a slow connection. The code travels after the # in the link, which browsers do not send to the server, so Posit's site does not receive your app's code.

Differences from the R version

Try it: add ax.axvline(districts[input.indicator()].mean(), color="#B45309") before return fig to draw the ten-district average as a line, and re-run.

Module 8 of 8

Sharing a dashboard, and what not to put in one

Three ways to get a dashboard to the people who need it, from lightest to heaviest:

Export an app to a static site

On your own computer, with the app saved as myapp/app.R or myapp/app.py, the commands from Posit's documentation are:

R (in an R console)
install.packages("shinylive")
shinylive::export("myapp", "site")
httpuv::runStaticServer("site/")   # preview it locally
Python (in a terminal, with uv installed)
uvx shinylive export myapp site

Sources: the shinylive R package site and the Shiny for Python Shinylive guide, both read in October 2026.

A Shinylive app has no secrets. Posit's Shinylive guide says it plainly: the code and data must be sent to the browser, so they cannot be kept from the user. Anything you embed in a Shinylive app, or in its link, can be read by anyone who opens it. Put only aggregated, non-identifying figures in one: district percentages, never beneficiary names, phone numbers, ID numbers or household-level rows. For data about people, follow the Data Protection & the DPDP Act deck, and use a server-based app with access control.
Small cells mislead. A district average from 24 households moves a lot when one household changes. Before a dashboard goes to a district officer, show the number of households behind each bar, or suppress cells below a minimum size, as the HAVING example in the SQL course does.

Try it: add a households column (24 for every district here) to the dashboard's data frame and show it in the table, so every bar comes with its sample size.

Where next

→

SQL for Development Data

Pull and summarise the rows a dashboard needs, straight from a database.

→

R & Python for Development

The data-frame skills underneath every Shiny app.

→

Data Visualization 101

Choosing the chart before you build the dashboard.

→

MEL Basics 101

Which indicators a monitoring dashboard should carry.

→

Data Protection & the DPDP Act 101

What may and may not go into a shared app.