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.
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.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.
# 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.
Two halves: ui and server
Every Shiny app has two parts.
- The ui describes the page: headings, the controls a reader can change (inputs) and empty slots where results will appear (outputs). Each input and output has an id, a short name in quotes such as
"state". - The server is a function that fills the output slots. It reads the inputs by id and recomputes an output whenever an input it uses changes.
- The last line joins the two:
shinyApp(ui, server)in R,App(app_ui, server)in Python.
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.
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:
- Drop-down menu:
selectInput()in R,ui.input_select()in Python. - Slider:
sliderInput()in R,ui.input_slider()in Python. - Tick box:
checkboxInput()in R,ui.input_checkbox()in Python. - Text box:
textInput()in R,ui.input_text()in Python. - Text output:
textOutput()withrenderText()in R,ui.output_text()with@render.textin Python. - Table output:
tableOutput()withrenderTable()in R,ui.output_data_frame()with@render.data_framein Python. - Chart output:
plotOutput()withrenderPlot()in R,ui.output_plot()with@render.plotin Python.
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.
Try it: change the slider's default value to 70 and its step to 1, then re-run the app in the Shinylive editor.
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.
- The menu feeds
selected(). selected()feeds both the count and the table.- Change the menu, and Shiny marks
selected()out of date, then recomputes it and both outputs.
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().
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.
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.
# 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
sidebarLayout()puts the inputs on the left and the outputs on the right; on a narrow screen they stack.setNames(names(labels), labels)shows readable labels in the menu while the server receives the column name, such aspct_bank.selected()is the same filter-and-sort as the Step 2 cell, withinput$stateandinput$indicatorin place of the plain variables.- The chart uses base R graphics, so the app needs no package beyond shiny and starts faster in Shinylive.
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.
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.
# 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
ui.page_sidebar()withui.sidebar()plays the part ofsidebarLayout().ui.input_select()accepts a dictionary forchoices: the keys are what the server receives, the values are what the reader sees.- Inputs are read with brackets,
input.indicator(), and outputs are functions named after their ids, decorated with@render.plotor@render.data_frame. @render.plotdisplays the matplotlib figure the function returns.
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.
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:
- A Shinylive link. The link in your browser's address bar, after you open an app from this page, contains the whole app. Anyone who opens it runs the app in their own browser. Good for small apps with small, non-sensitive data.
- A static Shinylive site. The shinylive package turns an app folder into plain web files you can put on any static host (GitHub Pages, Netlify, your organisation's web server). It needs a web server: opening the files straight from disk does not work.
- A Shiny server. For large data, private data, or heavy computation, run the app on a server with R or Python installed, such as Posit Connect or a server your organisation runs. The data then stays on the server.
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:
install.packages("shinylive")
shinylive::export("myapp", "site")
httpuv::runStaticServer("site/") # preview it locally
uvx shinylive export myapp site
Sources: the shinylive R package site and the Shiny for Python Shinylive guide, both read in October 2026.
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.