# Install Shiny if needed
install.packages("shiny")11 Web Development for Data Scientists
11.1 Web Development Fundamentals for Data Scientists
As a data scientist, you’ll often need to share your work through web applications, dashboards, or APIs. Understanding web development basics helps you create more effective and accessible data products while giving you more control of your projects. Quarto (covered in Data Science Tools for Reporting) and the dashboard frameworks in this chapter are all wrappers around the same lower-level web technologies: HTML, CSS, and JavaScript. They handle the heavy lifting for us, but what if we wanted our HTML Quarto report to have a custom theme? That becomes possible with a basic understanding of web development.
11.1.1 Why Web Development for Data Scientists?
You need enough of it for two jobs. The first is delivery: a stakeholder who won’t install R will open a URL, and a model that other systems have to call needs an endpoint rather than a function. The second is repair — knowing where a Quarto report’s styling comes from, so that when the output looks wrong you can find the cause instead of working around it.
That’s a low bar. You aren’t going to write the page from scratch; the frameworks later in this chapter do that. You want to be able to read one.
11.1.2 HTML, CSS, and JavaScript Basics
These three technologies form the foundation of web development:
- HTML: Structures the content of web pages
- CSS: Controls the appearance and layout
- JavaScript: Adds interactivity and dynamic behaviour
Let’s create a simple web page that displays a data visualisation:
- Create a file named
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Data Visualisation Example</title>
<link rel="stylesheet" href="styles.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<div class="container">
<h1>Sales Data Analysis</h1>
<div class="chart-container">
<canvas id="salesChart"></canvas>
</div>
<div class="summary">
<h2>Key Findings</h2>
<ul>
<li>Q4 had the highest sales, driven by holiday promotions</li>
<li>Product A consistently outperformed other products</li>
<li>Year-over-year growth was 15.3%</li>
</ul>
</div>
</div>
<script src="script.js"></script>
</body>
</html>- Create a file named
styles.css:
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
background-color: white;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #2c3e50;
text-align: center;
margin-bottom: 30px;
}
.chart-container {
margin-bottom: 30px;
height: 400px;
}
.summary {
border-top: 1px solid #ddd;
padding-top: 20px;
}
h2 {
color: #2c3e50;
}
ul {
padding-left: 20px;
}- Create a file named
script.js:
// Sample data
const salesData = {
labels: ['Q1', 'Q2', 'Q3', 'Q4'],
datasets: [
{
label: 'Product A',
data: [12, 19, 15, 28],
backgroundColor: 'rgba(54, 162, 235, 0.2)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
},
{
label: 'Product B',
data: [10, 15, 12, 25],
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 1
},
{
label: 'Product C',
data: [8, 10, 14, 20],
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
}
]
};
// Get the canvas element
const ctx = document.getElementById('salesChart').getContext('2d');
// Create the chart
const salesChart = new Chart(ctx, {
type: 'bar',
data: salesData,
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Sales (millions)'
}
}
}
}
});- Open
index.htmlin a web browser
Three files, three jobs: the HTML holds the structure, the CSS decides what it looks like, and the JavaScript draws the chart with Chart.js. That division is the thing worth taking away. When a rendered report looks wrong, knowing which of the three is responsible tells you which file to open.
11.1.3 Interactive Dashboards and Web Frameworks
Most data scientists don’t build web apps from raw HTML/CSS/JavaScript. They use a framework that wraps the web machinery and lets them stay in Python or R. There are four tools worth knowing, each optimised for a different situation:
- Shiny (R, also available for Python): reactive dashboards, strongest when you live in R and need server-side reactivity
- Dash (Python): Plotly’s callback-based framework, good when you want explicit wiring between inputs and outputs
- Streamlit (Python): script-based, fastest path from analysis to a demo
- Flask / FastAPI (Python): lower-level web frameworks, for when you’re building a custom API or a page with specific backend logic rather than a dashboard
If your problem is “give stakeholders a dashboard where they can filter this chart,” pick Shiny, Dash, or Streamlit. If it’s “expose this trained model at a URL that another program can send data to and get a prediction back from,” Flask or FastAPI is the right level. We cover all four below.
The examples in this section reference placeholder files like sales_data.csv. These aren’t shipped with the book; substitute your own file, a built-in dataset (ggplot2::diamonds, sns.load_dataset("tips"), or similar), or a stable public CSV. The framework mechanics don’t depend on which data you use.
11.1.3.1 Shiny: Interactive Web Applications with R
Shiny allows you to build interactive web applications entirely in R, without requiring knowledge of HTML, CSS, or JavaScript.
A simple Shiny app consists of two components:
- UI (User Interface): Defines what the user sees
- Server: Contains the logic that responds to user input
Here’s a basic example:
library(shiny)
library(ggplot2)
library(dplyr)
# Define UI
ui <- fluidPage(
titlePanel("Diamond Explorer"),
sidebarLayout(
sidebarPanel(
sliderInput("carat_range",
"Carat Range:",
min = 0.2,
max = 5.0,
value = c(0.5, 3.0)),
selectInput("cut",
"Cut Quality:",
choices = c("All", unique(as.character(diamonds$cut))),
selected = "All")
),
mainPanel(
plotOutput("scatterplot"),
tableOutput("summary_table")
)
)
)
# Define server logic
server <- function(input, output) {
# Filter data based on inputs
filtered_data <- reactive({
data <- diamonds
# Filter by carat
data <- data |>
filter(carat >= input$carat_range[1] & carat <= input$carat_range[2])
# Filter by cut if not "All"
if (input$cut != "All") {
data <- data |> filter(cut == input$cut)
}
data
})
# Create scatter plot
output$scatterplot <- renderPlot({
ggplot(filtered_data(), aes(x = carat, y = price, color = cut)) +
geom_point(alpha = 0.5) +
theme_minimal() +
labs(title = "Diamond Price vs. Carat",
x = "Carat",
y = "Price (USD)")
})
# Create summary table
output$summary_table <- renderTable({
filtered_data() |>
group_by(cut) |>
summarise(
Count = n(),
`Avg Price` = round(mean(price), 2),
`Avg Carat` = round(mean(carat), 2)
)
})
}
# Run the application
shinyApp(ui = ui, server = server)Shiny’s distinguishing feature is reactivity. You declare relationships between inputs and outputs and the framework works out what needs recomputing, much as a spreadsheet does when you change a cell. You never write “when the dropdown changes, rerun the plot”; you write “the plot depends on the filtered data, which depends on the dropdown”, and Shiny handles the rest.
Shiny is also now available for Python via Shiny for Python, which offers the same reactive model with Python syntax. A useful option if you like the Shiny mental model but your team uses Python.
11.1.3.2 Dash: Interactive Web Applications with Python
Dash is Python’s equivalent to Shiny, created by the makers of Plotly:
# Install Dash
pip install dashA simple Dash app follows a similar structure to Shiny:
import dash
from dash import dcc, html, dash_table
from dash.dependencies import Input, Output
import plotly.express as px
import pandas as pd
# Load data - using built-in dataset for reproducibility
df = px.data.iris()
# Initialize app
app = dash.Dash(__name__)
# Define layout
app.layout = html.Div([
html.H1("Iris Dataset Explorer"),
html.Div([
html.Div([
html.Label("Select Species:"),
dcc.Dropdown(
id='species-dropdown',
options=[{'label': 'All', 'value': 'all'}] +
[{'label': i, 'value': i} for i in df['species'].unique()],
value='all'
),
html.Label("Select Y-axis:"),
dcc.RadioItems(
id='y-axis',
options=[
{'label': 'Sepal Width', 'value': 'sepal_width'},
{'label': 'Petal Length', 'value': 'petal_length'},
{'label': 'Petal Width', 'value': 'petal_width'}
],
value='sepal_width'
)
], style={'width': '25%', 'padding': '20px'}),
html.Div([
dcc.Graph(id='scatter-plot')
], style={'width': '75%'})
], style={'display': 'flex'}),
html.Div([
html.H3("Data Summary"),
dash_table.DataTable(
id='summary-table',
style_cell={'textAlign': 'left'},
style_header={
'backgroundColor': 'lightgrey',
'fontWeight': 'bold'
}
)
])
])
# Define callbacks
@app.callback(
[Output('scatter-plot', 'figure'),
Output('summary-table', 'data'),
Output('summary-table', 'columns')],
[Input('species-dropdown', 'value'),
Input('y-axis', 'value')]
)
def update_graph_and_table(selected_species, y_axis):
# Filter data
if selected_species == 'all':
filtered_df = df
else:
filtered_df = df[df['species'] == selected_species]
# Create figure
fig = px.scatter(
filtered_df,
x='sepal_length',
y=y_axis,
color='species',
title=f'Sepal Length vs {y_axis.replace("_", " ").title()}'
)
# Create summary table
summary_df = filtered_df.groupby('species').agg({
'sepal_length': ['mean', 'std'],
'sepal_width': ['mean', 'std'],
'petal_length': ['mean', 'std'],
'petal_width': ['mean', 'std']
}).reset_index()
# Flatten the multi-index
summary_df.columns = ['_'.join(col).strip('_') for col in summary_df.columns.values]
# Format table
table_data = summary_df.to_dict('records')
columns = [{"name": col.replace('_', ' ').title(), "id": col} for col in summary_df.columns]
return fig, table_data, columns
# Run app
if __name__ == '__main__':
# debug=True must NEVER be used in production.
app.run(debug=False)Dash builds its charts with Plotly and its interface with React, a widely used JavaScript framework — but you write only Python, and never touch either directly.
Unlike Shiny’s reactive programming model, Dash uses an explicit callback-based approach. You define functions that take specific inputs and produce specific outputs, with the Dash framework wiring them together. This approach may feel more familiar to Python programmers used to callback-based frameworks.
11.1.3.3 Streamlit: Rapid Application Development
Streamlit simplifies interactive app creation even further with a minimal, straightforward API. Here’s a simple Streamlit app:
import streamlit as st
import pandas as pd
import plotly.express as px
import seaborn as sns
# Set page title
st.set_page_config(page_title="Data Explorer", page_icon="📊")
# Add a title
st.title("Interactive Data Explorer")
# Add sidebar with dataset options
st.sidebar.header("Settings")
dataset_name = st.sidebar.selectbox(
"Select Dataset",
options=["Iris", "Diamonds", "Gapminder"]
)
# Load data based on selection - using built-in datasets for reproducibility
@st.cache_data
def load_data(dataset):
if dataset == "Iris":
return sns.load_dataset("iris")
elif dataset == "Diamonds":
return sns.load_dataset("diamonds").sample(n=1000, random_state=42)
else: # Gapminder
return px.data.gapminder()
df = load_data(dataset_name)
# Display basic dataset information
st.header(f"{dataset_name} Dataset")
tab1, tab2, tab3 = st.tabs(["📋 Data", "📈 Visualisation", "📊 Summary"])
with tab1:
st.subheader("Raw Data")
st.dataframe(df.head(100))
with tab2:
st.subheader("Data Visualisation")
numeric_cols = df.select_dtypes("number").columns.tolist()
if numeric_cols:
x_var = st.selectbox("X variable", options=numeric_cols)
y_var = st.selectbox("Y variable", options=numeric_cols, index=min(1, len(numeric_cols) - 1))
fig = px.scatter(df, x=x_var, y=y_var, title=f"{x_var} vs {y_var}")
st.plotly_chart(fig, use_container_width=True)
with tab3:
st.subheader("Statistical Summary")
st.dataframe(df.describe())Streamlit’s appeal lies in its simplicity. Instead of defining callbacks between inputs and outputs (as in Dash and Shiny), the entire script runs from top to bottom whenever any input changes. You write a straightforward Python script that builds the UI linearly, and Streamlit reruns it on every interaction. This procedural approach suits beginners and rapid prototyping, though it becomes less efficient for complex applications where you don’t want to recompute everything on every keystroke.
11.1.3.4 Flask: A Lightweight Framework for Custom Backends
Shiny, Dash, and Streamlit are great for dashboards, but sometimes you need a plain web endpoint: for example, exposing a trained model as a JSON API that other applications can call, or serving a custom HTML page that mixes pandas-generated content with JavaScript on the front end. Flask is the most common lightweight choice for that role. (FastAPI is a modern alternative with built-in data validation via Pydantic and automatic OpenAPI documentation; worth considering for any new API project in 2026.)
import os
from flask import Flask, jsonify
import pandas as pd
app = Flask(__name__)
@app.route('/api/sales')
def sales():
# Load and process data
df = pd.read_csv('sales_data.csv')
# Shape the data the way Chart.js expects it
chart_data = {
'labels': df['quarter'].tolist(),
'datasets': [
{
'label': 'Product A',
'data': df['product_a'].tolist(),
'backgroundColor': 'rgba(54, 162, 235, 0.2)',
'borderColor': 'rgba(54, 162, 235, 1)',
'borderWidth': 1
},
# Other products...
]
}
# jsonify sets the Content-Type header and serialises for you
return jsonify(chart_data)
if __name__ == '__main__':
# Only enable debug mode when a FLASK_DEBUG env var is set.
# Never run with debug=True in production: it exposes an
# interactive Python console to anyone who can reach the app.
app.run(debug=os.environ.get("FLASK_DEBUG") == "1")Run this and http://127.0.0.1:5000/api/sales returns JSON that the Chart.js page from earlier in this chapter can fetch, instead of having its numbers hardcoded.
To return a full page rather than JSON, Flask uses render_template('index.html'). That call has one requirement people trip over constantly: the file must live in a directory named templates/ next to your Python file. Flask looks nowhere else, and the error when it can’t find the file (TemplateNotFound) doesn’t say so.
my_app/
├── app.py
└── templates/
└── index.html
Flask is well-suited for data scientists because it lets you use your Python data-processing code alongside a web server with very little overhead. The deployment chapter shows a full Flask-based ML model API example.
11.1.3.5 Which one should you pick?
| You want to… | Pick |
|---|---|
| Build a dashboard in R | Shiny |
| Build a dashboard in Python with explicit input→output wiring | Dash |
| Build a dashboard in Python as fast as possible | Streamlit |
| Expose a model or dataset as a JSON API | Flask or FastAPI |
| Render a static report with a few interactive elements | Quarto dashboards (see the Reporting chapter) |
Don’t get paralysed by the choice. Any of the first three will produce a working dashboard in an afternoon; the right answer is usually “whichever one your team already uses.”
11.1.4 Deploying Web Applications
Once you’ve built your application, you’ll need to deploy it for others to access. Deploying Data Science Projects walks through full deployment workflows for Flask, Dash, and Shiny applications on Render, Google Cloud Run, and Posit Connect Cloud.
The short version: Flask apps are typically deployed by containerising them with Docker and pushing the image to a platform-as-a-service provider; Shiny apps are most easily deployed to Posit Connect Cloud, either from a linked GitHub repository or straight from RStudio. The best choice depends on your specific needs, including factors like:
- Expected traffic volume
- Security requirements
- Budget constraints
- Integration with other systems
- Need for custom domains or SSL
11.2 Conclusion
A light reading knowledge of HTML, CSS and JavaScript is enough to customise a report and to work out why one renders wrongly. The frameworks in this chapter — Shiny, Dash, Streamlit, Flask — cover the rest, so you can stay in Python or R for the application itself.
By the end of this chapter you should have:
- One interactive application running on your own machine, in whichever of Shiny, Dash, or Streamlit fits your language — reachable in a browser at
localhost - The ability to read an HTML file and identify where its structure, styling, and behaviour each live
- A working sense of which framework to reach for: Streamlit for the quickest route from script to app, Dash for control over layout, Shiny if you’re in R, Flask when what you need is an API rather than a page
- Understood why
debug=Truenever belongs in anything reachable by someone else
The next chapter covers containers, the packaging step that makes deploying any of these applications predictable, followed by deployment itself.