Build a Data Dashboard with FastAPI

Build a FastAPI backend that turns any CSV file into interactive charts.

Introduction

30 Second Summary

Every team has that one spreadsheet full of numbers that nobody wants to scroll through. The raw data is there, but the story hiding inside it is not.

In this project, you will build a data dashboard powered by FastAPI, a modern Python web framework. You will upload a CSV file through the browser and instantly see it visualized as interactive charts.

What You'll Build

Picture dragging a CSV file into your browser and watching a polished dashboard light up with colorful charts, dropdown selectors for your columns, and a toggle that switches between bar, line, and pie views.

By the end of this project, you'll have:

  • Upload any CSV file through the browser and see it visualized as an interactive chart within seconds.
  • Switch between bar, line, and pie charts to explore different views of the same data.
  • Pick which columns from your CSV to use as chart labels and values using dropdown selectors.
  • An optional Secret Mission to push your skills further.

Are there any prerequisites?

You need basic Python knowledge and a text editor like VS Code. No prior FastAPI or web development experience is required.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. Taking a moment to set the goal now makes every step that follows more purposeful.

Set Up Your FastAPI Project

Every dashboard needs a backend server. Before you can upload CSV files or render charts in the browser, you need a running server that accepts requests and sends back data.

In this step, you'll create your project folder, install FastAPI and its companion packages, and get a development server running. Every code change you make after this will show up instantly.

In this step, get ready to:
  • Create a project folder with main.py and a static/ directory.
  • Install FastAPI, uvicorn, and python-multipart.
  • Launch a development server and confirm it responds in the browser.
Create your project folder

Your project needs a dedicated folder on your Desktop and a static/ subdirectory where your frontend files will live later.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and its structure by running these commands:
mkdir dataviz-dashboard
cd dataviz-dashboard
mkdir static
  • Confirm the folder was created by running:
ls

You should see static listed in the output.

Don't see the static folder?

Make sure you ran all three commands in order. If you see an error about the directory already existing, that means the folder is already there. Run ls again to confirm.

help me troubleshoot my project folder setup

Now open VS Code so you can create your main Python file and edit code throughout the project.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the dataviz-dashboard folder, and click Open.
  • In the Explorer sidebar on the left, right-click in the empty area below static.
  • Click New File.
  • Type main.py and press Enter.

Your Explorer sidebar should now show main.py alongside the static folder. VS Code will also open main.py in the editor, ready for you to write code.

Install FastAPI, uvicorn, and python-multipart

FastAPI needs two companion packages to work. One runs the server. The other handles file uploads.

  • Open VS Code's built-in terminal by pressing Ctrl+` (backtick key, usually above Tab) on both macOS and Windows.
  • Install all three packages at once by running:
pip install fastapi uvicorn python-multipart

You'll see download progress bars scroll by. The last line should say Successfully installed followed by the package names.

What are these three packages?

  • fastapi is the web framework. It handles routing, request validation, and response formatting.
  • uvicorn is the server that actually runs your FastAPI app and listens for incoming requests.
  • python-multipart lets FastAPI handle file uploads. You'll need this when you add CSV upload support later.

Seeing 'command not found' for pip?

Try running pip3 instead of pip. Some systems use pip3 for Python 3 packages.

Also confirm your terminal is inside the dataviz-dashboard folder. VS Code's terminal should open there automatically since you opened that folder.

help me fix pip installation issues

Write your first FastAPI endpoint

A FastAPI app starts with just a few lines of code. You define an app instance, add a route, and return some data. That's enough for a working server.

  • Click on main.py in the Explorer sidebar to open it in the editor.
  • Add the following code to main.py:
# Import the FastAPI framework
from fastapi import FastAPI

# Create the application instance
app = FastAPI()

# Define a route that responds to GET requests at the root URL
@app.get("/")
async def root():
    return {"message": "Hello, DataViz!"}

What does this code do?

  • app = FastAPI() creates a new FastAPI application. Every route and configuration you add later attaches to this app object.
  • @app.get("/") is a decorator that tells FastAPI to call the root() function whenever someone visits the root URL (/) with a GET request.
  • The function returns a Python dictionary. FastAPI automatically converts it to JSON and sends it back to the browser.
  • Save main.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Start your development server by running this command in the VS Code terminal:
uvicorn main:app --reload

What does this command do?

  • main:app tells uvicorn to look in main.py for the object called app.
  • --reload makes the server watch your files for changes. Every time you save, the server restarts automatically so you never have to stop and restart it manually.

This command takes over your terminal while the server is running. To stop it later, press Ctrl+C.

You should see several lines of output. Look for the line that says Uvicorn running on http://127.0.0.1:8000. That means your server is live.

Before you open the browser, take a guess. What do you think you'll see when you visit that URL?

  • Open your browser and go to http://127.0.0.1:8000.

You should see {"message":"Hello, DataViz!"} displayed as JSON in the browser. That's your FastAPI endpoint responding.

Seeing an error instead of JSON?

  • Check your terminal for error messages. A common cause is a typo in main.py. Make sure every parenthesis, colon, and quote matches the code above exactly.
  • Make sure the URL is exactly http://127.0.0.1:8000 with no trailing slash or extra path.
  • Confirm you saved main.py before running the uvicorn command. A dot next to the filename in VS Code means unsaved changes.

help me troubleshoot my FastAPI server

✔️ Awesome, I've got everything!

Great. Double check you've saved main.py and that your server is still running in the terminal.

ⓧ I'd like to double check the full code

Here is the complete main.py file. Compare it with yours line by line.

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello, DataViz!"}

Your server is live and responding with JSON. Next up, you'll build a real HTML dashboard page and get your first chart on screen.

Build Your First Chart Page

Your FastAPI server is running and returning JSON at http://127.0.0.1:8000. That proves the backend works, but a raw JSON response is not a dashboard anyone would want to use.

This step transforms your project into something visual. You will create an HTML page, style it, and render your first chart using Chart.js. By the end, your browser will show a real bar chart instead of raw JSON.

In this step, get ready to:
  • Create an HTML dashboard page with a canvas element for Chart.js.
  • Style the dashboard and add a bar chart with sample data.
  • Update FastAPI to serve your HTML page instead of raw JSON.
Create the HTML page and serve it from FastAPI

Chart.js renders charts onto an HTML <canvas> element. You need a page that loads the Chart.js library from a CDN and provides that canvas.

  • In VS Code, right-click the static folder in the Explorer sidebar.
  • Click New File and name it index.html.
  • Paste the following code into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>DataViz Dashboard</title>
    <link rel="stylesheet" href="/static/style.css">
    <script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script>
</head>
<body>
    <header>
        <h1>DataViz Dashboard</h1>
    </header>
    <main>
        <section class="chart-container">
            <canvas id="dashboard-chart"></canvas>
        </section>
    </main>
    <script src="/static/app.js"></script>
</body>
</html>

What does this HTML do?

The <canvas> tag with id dashboard-chart is where Chart.js will draw the chart. The first <script> tag loads Chart.js 4.5.1 from a CDN, making the Chart class available globally.

A second <script> tag at the bottom loads your own app.js file, which you will create shortly.

Your HTML file exists, but FastAPI still returns JSON at the root URL. You need to update main.py to serve this HTML page instead.

  • Switch to main.py in VS Code.
  • Replace the entire contents of main.py with this code:
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles

app = FastAPI()

# Serve CSS and JS files from the static/ directory
app.mount("/static", StaticFiles(directory="static"), name="static")

@app.get("/")
async def root():
    # Return the HTML page instead of JSON
    with open("static/index.html") as f:
        return HTMLResponse(f.read())

What changed in main.py?

  • HTMLResponse lets FastAPI return HTML content instead of JSON.
  • StaticFiles mounts the static/ folder so the browser can load CSS, JavaScript, and other files from the /static URL path.
  • The root route now reads static/index.html and returns its contents as an HTML page.
  • Save main.py.
  • Refresh http://127.0.0.1:8000 in your browser.

You should see the text DataViz Dashboard in the top left corner. The page looks plain because the CSS file does not exist yet.

Don't see the page?

  • Make sure you saved main.py. The --reload flag picks up changes automatically, but only after you save.
  • Check that index.html is inside the static/ folder, not in the project root.
  • help me debug why my FastAPI server is not serving my HTML page
Style the dashboard

The page works but looks bare. A CSS file will give it a dark header, centered layout, and a clean card for the chart area.

  • Right-click the static folder in VS Code's Explorer sidebar and click New File.
  • Name the file style.css and press Enter.
  • Paste the following styles into style.css:
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background-color: #f0f2f5;
    color: #333;
}

header {
    background-color: #1a1a2e;
    color: #fff;
    padding: 1rem 2rem;
}

header h1 {
    font-size: 1.5rem;
    font-weight: 600;
}

What do these styles do?

  • The * reset removes default browser margins and padding so spacing is consistent.
  • The header gets a dark navy background (#1a1a2e) for a professional dashboard look.
  • body uses the system font stack for clean, native-looking text across operating systems.
  • Add the remaining styles below what you just pasted in style.css:
main {
    max-width: 900px;
    margin: 2rem auto;
    padding: 0 1rem;
}

.chart-container {
    background: #fff;
    padding: 2rem;
    border-radius: 8px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

What does this section style?

  • main centers the page content at a max width of 900px.
  • .chart-container wraps the chart in a white card with rounded corners and a subtle shadow.

✔️ Awesome, I've got everything!

Great. Make sure you have saved style.css.

ⓧ I'd like to double check the full code

Your style.css should have six selector blocks in this order: * reset, body, header, header h1, main, and .chart-container. Scroll up and confirm each block is present in that order.

  • Save style.css and refresh your browser.

The page now has a dark header bar at the top and the content sits centered on a light gray background. The chart area is inside a white card, ready for data.

Styles not showing?

  • Check that the file is named exactly style.css inside the static/ folder.
  • Open your browser's developer console (press F12) and check the Network tab for a 404 error on style.css.
  • help me debug why my CSS styles are not loading in the browser
Add the bar chart

The styled page is ready for a chart. Chart.js needs a JavaScript file that creates a Chart instance on the canvas element you added to index.html.

  • Right-click the static folder and create a new file called app.js.
  • Add these variable declarations at the top of app.js:
let chart = null;

const chartCanvas = document.getElementById("dashboard-chart");
  • Below the variables, add the createChart function:
function createChart(labels, values, chartType) {
    if (chart) {
        chart.destroy();
    }
    chart = new Chart(chartCanvas, {
        type: chartType,
        data: {
            labels: labels,
            datasets: [{
                label: "Value",
                data: values,
                borderWidth: 1,
                backgroundColor: [
                    "#4e79a7", "#f28e2b", "#e15759", "#76b7b2",
                    "#59a14f", "#edc948", "#b07aa1", "#ff9da7",
                    "#9c755f", "#bab0ac"
                ],
            }]
        },
        options: {
            responsive: true,
            plugins: {
                legend: { display: false }
            },
            scales: {
                y: { beginAtZero: true }
            }
        }
    });
}

What does createChart do?

  • It first checks if a chart already exists and calls chart.destroy(). This prevents Chart.js from stacking multiple charts on top of each other.
  • It creates a new Chart instance on the dashboard-chart canvas with whatever labels, values, and chart type you pass in.
  • The backgroundColor array provides ten distinct colors so each data bar gets its own color.
  • beginAtZero: true ensures the Y axis starts at zero instead of auto-scaling from the lowest data value.
  • At the bottom of app.js, add this function call to display sample data:
createChart(
    ["Jan", "Feb", "Mar", "Apr", "May"],
    [12, 19, 3, 5, 2],
    "bar"
);

What does this call do?

This calls createChart with five month names as labels, five sales figures as values, and "bar" as the chart type. The chart renders immediately when the browser loads app.js.

✔️ Awesome, I've got everything!

Great. Make sure you have saved app.js.

ⓧ I'd like to double check the full code

Your app.js should have three sections in this order:

  • Two variable declarations at the top: chart (set to null) and chartCanvas (grabbing the canvas element by id).
  • The createChart function that takes labels, values, and chartType, destroys any existing chart, and creates a new one.
  • A createChart(...) call at the bottom with sample month and sales data using the "bar" type.

Scroll up and confirm each section is present in that order.

Before you refresh your browser, what do you expect to see on the page?

  • Save app.js and refresh http://127.0.0.1:8000 in your browser.

A bar chart appears inside the white card. Five colored bars represent Jan through May, with heights matching the sample values (12, 19, 3, 5, 2).

Chart not showing?

  • Make sure app.js is inside the static/ folder and named exactly app.js.
  • Open the browser console (F12 then the Console tab) and look for error messages. A Chart is not defined error means the Chart.js CDN script did not load.
  • Check that the <canvas id="dashboard-chart"> in index.html matches the getElementById("dashboard-chart") call in app.js.
  • help me debug why my Chart.js bar chart is not rendering

Your dashboard has a working chart with sample data. Next up, you will add CSV file upload so the dashboard can visualize real data from your own files.

Add CSV File Upload

Right now, your dashboard shows the same five months of sample data every time you load it. That is fine for proving the page works, but a real dashboard needs to accept real data from real users.

In this step, you will build a file upload endpoint in FastAPI that accepts a CSV file, parses it into JSON, and sends it back to the browser. You will also add a file picker to the dashboard so users can choose a file from their computer.

In this step, get ready to:
  • Add a CSV upload endpoint to your FastAPI backend.
  • Add a file picker control to the dashboard page.
  • Connect the file picker to the upload endpoint.
Add a CSV upload endpoint to your backend

Your backend needs a new endpoint that can accept a CSV file, parse its rows into a list of dictionaries, and return the data as JSON. Python's built-in csv module handles the parsing with zero extra dependencies.

  • In VS Code, open main.py.
  • Find the import block at the top of the file:
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
  • Replace those three lines with the following five lines, which add the csv, io, and UploadFile imports:
import csv
import io
from fastapi import FastAPI, UploadFile
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles

What changed in the imports?

  • csv is Python's built-in module for reading CSV data row by row.
  • io provides StringIO, which lets you treat a string like a file so csv.DictReader can iterate over it.
  • UploadFile is FastAPI's type for handling file uploads. It gives you the file contents and metadata like the filename.
  • Now find the line app.mount("/static", StaticFiles(directory="static"), name="static").
  • Add the following code directly above that line, right after app = FastAPI():
# Store the most recently uploaded CSV data
uploaded_data = []
uploaded_columns = []

# Accept a CSV file and return its contents as JSON
@app.post("/api/upload")
async def upload_csv(file: UploadFile):
    global uploaded_data, uploaded_columns
    contents = await file.read()
    decoded = contents.decode("utf-8")
    reader = csv.DictReader(io.StringIO(decoded))
    uploaded_data = [row for row in reader]
    uploaded_columns = list(uploaded_data[0].keys()) if uploaded_data else []
    return {"columns": uploaded_columns, "data": uploaded_data}

What does this code do?

  • uploaded_data and uploaded_columns are module-level lists that store the most recent upload. Later steps will use these to serve column names.
  • @app.post("/api/upload") creates a POST endpoint at /api/upload. FastAPI automatically handles the multipart form data when you type-hint the parameter as UploadFile.
  • file.read() reads the raw bytes. decode("utf-8") converts them into a string. csv.DictReader then parses each row into a dictionary keyed by the column headers.
  • The endpoint returns both the column names and the parsed rows, giving the frontend everything it needs.
  • Save main.py.
  • Switch to the terminal where uvicorn is running. Because you started it with --reload, it automatically restarts when you save a Python file.
  • Confirm the terminal shows a fresh startup message with no errors.

Seeing an error in the terminal?

  • Check that import csv and import io are on their own lines above the from fastapi import. A missing line break causes a syntax error.
  • Make sure UploadFile is inside the from fastapi import FastAPI, UploadFile line, not on a separate import.
  • Confirm python-multipart is installed. FastAPI requires it for file uploads. Run pip install python-multipart if the error mentions it.

Help me debug my uvicorn reload error after adding the upload endpoint.

✔️ Awesome, I've got everything!

Great. Make sure you have saved main.py before moving on.

ⓧ I'd like to double check the full code

import csv
import io
from fastapi import FastAPI, UploadFile
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles

app = FastAPI()

uploaded_data = []
uploaded_columns = []

@app.post("/api/upload")
async def upload_csv(file: UploadFile):
    global uploaded_data, uploaded_columns
    contents = await file.read()
    decoded = contents.decode("utf-8")
    reader = csv.DictReader(io.StringIO(decoded))
    uploaded_data = [row for row in reader]
    uploaded_columns = list(uploaded_data[0].keys()) if uploaded_data else []
    return {"columns": uploaded_columns, "data": uploaded_data}

app.mount("/static", StaticFiles(directory="static"), name="static")

@app.get("/")
async def root():
    with open("static/index.html") as f:
        return HTMLResponse(f.read())
Add a file picker to the dashboard

The endpoint is ready, but users have no way to select a file in the browser yet. You need a file input element on the page and some styling to make it fit the dashboard.

  • Open static/index.html in VS Code.
  • Find the line <section class="chart-container"> inside <main>.
  • Add the following controls section directly above it:
<section class="controls">
    <label for="csv-upload">Upload CSV:</label>
    <input type="file" id="csv-upload" accept=".csv">
</section>

What does this HTML do?

  • The <input type="file"> element creates a file picker button in the browser.
  • accept=".csv" filters the file dialog so only CSV files show up by default.
  • The id="csv-upload" gives JavaScript a way to find this element later.
  • Save static/index.html.
  • Refresh http://127.0.0.1:8000 in your browser. You should see a file upload button above the chart.

Don't see the file picker?

  • Make sure you placed the new <section class="controls"> block inside <main>, before the <section class="chart-container"> block.
  • Check that you saved the file before refreshing.

Help me figure out why my file picker isn't showing up on the dashboard.

  • Now open static/style.css in VS Code.
  • Find the .chart-container rule near the bottom. Add the following styles directly above it:
.controls {
    background: #fff;
    padding: 1.5rem;
    border-radius: 8px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    align-items: center;
    margin-bottom: 1.5rem;
}

.controls label {
    font-weight: 600;
    font-size: 0.85rem;
}

.controls select,
.controls input[type="file"] {
    padding: 0.4rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 0.9rem;
}

What do these styles do?

The .controls class gives the file picker a white card with the same rounded corners and shadow as the chart below it. display: flex and gap: 1rem arrange the label and input side by side with spacing. The .controls select rule styles dropdown menus you will add in a later step.

  • Save static/style.css and refresh your browser.
  • The file picker now sits in a styled white card above the chart, with a bold label and consistent spacing.

File picker looks unstyled?

  • Confirm the class name controls in your CSS matches class="controls" in your HTML exactly.
  • Make sure the new CSS rules are placed above .chart-container, not inside it.

Help me debug my controls section styling.

✔️ Awesome, I've got everything!

Great. Confirm you have saved both static/index.html and static/style.css.

ⓧ I'd like to double check the full HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>DataViz Dashboard</title>
    <link rel="stylesheet" href="/static/style.css">
    <script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script>
</head>
<body>
    <header>
        <h1>DataViz Dashboard</h1>
    </header>
    <main>
        <section class="controls">
            <label for="csv-upload">Upload CSV:</label>
            <input type="file" id="csv-upload" accept=".csv">
        </section>
        <section class="chart-container">
            <canvas id="dashboard-chart"></canvas>
        </section>
    </main>
    <script src="/static/app.js"></script>
</body>
</html>

ⓧ I'd like to double check the full CSS

Your static/style.css is now longer than 30 lines. Scroll through and confirm these blocks appear in order:

  • The * reset, body, header, header h1, and main rules from Step 2.
  • The .controls, .controls label, and .controls select, .controls input[type="file"] rules you just added.
  • The .chart-container rule from Step 2.
Connect the file picker to the upload endpoint

The file picker exists on the page, but clicking it does nothing yet. You need JavaScript to listen for a file selection, send the file to your /api/upload endpoint, and handle the response.

  • Open static/app.js in VS Code.
  • Find this line near the top of the file:
const chartCanvas = document.getElementById("dashboard-chart");
  • Add the following line directly below it to grab a reference to the file input:
const fileInput = document.getElementById("csv-upload");
  • Now scroll to the bottom of the file. Find the createChart( call that renders the sample data.
  • Add the following event listener directly above that createChart( call:
// When a file is selected, upload it and log the response
fileInput.addEventListener("change", async (event) => {
    const file = event.target.files[0];
    if (!file) return;

    const formData = new FormData();
    formData.append("file", file);

    const response = await fetch("/api/upload", {
        method: "POST",
        body: formData,
    });
    const result = await response.json();
    console.log(result);
});

What does this code do?

  • The change event fires whenever the user selects a file through the file picker.
  • FormData packages the file into the format that FastAPI's UploadFile expects (multipart form data).
  • fetch("/api/upload", ...) sends a POST request to your backend. The response is the parsed CSV data as JSON.
  • console.log(result) prints the response to the browser's developer console so you can verify the upload worked.
  • Save static/app.js.

Before you test the upload, you need a CSV file. Create a quick test file so you have something to upload.

  • Create a file called test_data.csv on your Desktop with the following content:
Product,Sales,Quarter
Widget A,150,Q1
Widget B,280,Q2
Widget C,95,Q3
Widget D,310,Q4

Before you upload this file, take a moment to predict. What do you think will happen to the bar chart on screen when you upload the CSV?

  • Refresh http://127.0.0.1:8000 in your browser.
  • Click the file picker and select your test_data.csv file.
  • Open the browser's developer console by pressing F12 (or Cmd+Option+J on macOS), then click the Console tab.

You should see a JSON object logged in the console. It contains a columns array with Product, Sales, and Quarter, plus a data array with four row objects.

Now look at the chart. It still shows the old Jan-May sample data. The upload worked perfectly on the backend, and the data made it back to the browser, but nothing tells the chart to update yet. That gap is exactly what the next step fixes.

Don't see anything in the console?

  • Make sure you saved static/app.js and refreshed the page before uploading.
  • Check the Console tab for red error messages. A 422 error means the file parameter name in your JavaScript (formData.append("file", file)) does not match the parameter name in your Python endpoint (file: UploadFile). Both must be file.
  • If the console shows a network error, confirm uvicorn is still running in your terminal.

Help me debug why my CSV upload is not working.

✔️ Awesome, I've got everything!

Great. Confirm you have saved static/app.js.

ⓧ I'd like to double check the full code

Your static/app.js is now longer than 30 lines. Scroll through and confirm these sections appear in order:

  • The chart variable declaration.
  • The chartCanvas and fileInput variable declarations referencing dashboard-chart and csv-upload.
  • The createChart function (from Step 2).
  • The fileInput.addEventListener("change", ...) block you just added.
  • The createChart(["Jan", "Feb", ...) call at the very bottom.

Your backend can now accept CSV files and return parsed data. The data reaches the browser, but the chart has no idea it arrived. In the next step, you will wire the upload response directly into Chart.js so the chart updates the moment a file is uploaded.

Wire the Upload to Your Charts

In the last step, you built a file upload endpoint and confirmed the parsed CSV data arrives in the browser console. The data makes the round trip perfectly.

The problem is that the chart still ignores it. It keeps showing the same hardcoded sample data no matter what you upload. In this step, you will connect the upload response to Chart.js so uploading a CSV instantly refreshes the chart with your real data.

In this step, get ready to:
  • Add a chart type dropdown so you can switch between bar, line, and pie charts.
  • Build a render function that auto-detects which CSV columns to chart and updates the visualization.
  • Wire the upload response and chart type selector so the chart reacts to your data in real time.
Add a chart type dropdown to your page

Before writing the JavaScript that switches chart types, the HTML needs a dropdown for the learner to pick from. You will add a <select> element with three options: bar, line, and pie.

  • In static/index.html, find the closing <input> line for the file upload inside the .controls section.
  • Add the following chart type dropdown directly below that line, still inside the <section class="controls"> block:
            <label for="chart-type">Chart Type:</label>
            <select id="chart-type">
                <option value="bar">Bar</option>
                <option value="line">Line</option>
                <option value="pie">Pie</option>
            </select>

What does this code do?

This adds a labeled dropdown with three chart type options. Each <option> has a value attribute that matches what Chart.js expects as its type parameter.

The id="chart-type" lets your JavaScript grab a reference to this dropdown so it can read the selected value later.

  • Save static/index.html.
  • Refresh http://127.0.0.1:8000 in your browser. You should see a new Chart Type dropdown next to the file picker, with Bar selected by default.

Don't see the dropdown?

  • Make sure the new lines are inside the <section class="controls"> block, not outside it.
  • Check that you saved the file. Unsaved changes will not appear in the browser.

Still stuck? Ask for help.

Build a render function for uploaded data

Right now, the file upload listener logs data to the console and forgets about it. You need a way to store that data and a function that reads it, figures out which columns to chart, and updates the visualization.

  • In static/app.js, find the line let chart = null; at the top of the file.
  • Add these two new lines directly below it:
let currentData = [];
let currentColumns = [];
  • Next, find the line const fileInput = document.getElementById("csv-upload"); and add this line directly below it:
const chartTypeSelect = document.getElementById("chart-type");
  • Now add the renderChart function below the closing } of the createChart function by copying the following code:
function renderChart() {
    if (currentData.length === 0) return;

    // Find the first column that holds numbers
    const numericCol = currentColumns.find(col =>
        currentData.length > 0 && !isNaN(parseFloat(currentData[0][col]))
    );
    // Find the first column that holds text (labels)
    const stringCol = currentColumns.find(col =>
        currentData.length > 0 && isNaN(parseFloat(currentData[0][col]))
    );

    if (!stringCol || !numericCol) return;

    // Map the CSV rows into arrays Chart.js can use
    const labels = currentData.map(row => row[stringCol]);
    const values = currentData.map(row => parseFloat(row[numericCol]) || 0);
    const chartType = chartTypeSelect.value;

    createChart(labels, values, chartType);
}

What does this function do?

  • It exits early if no data has been uploaded yet.
  • It scans the column names to auto-detect which one holds numbers (for values) and which one holds text (for labels). This means the learner does not need to manually specify columns.
  • It maps every row in currentData into two arrays that Chart.js expects: one for labels and one for numeric values.
  • It reads the selected chart type from the dropdown, then passes everything to createChart.

The function is ready, but nothing calls it yet. The file upload listener still logs to the console instead of storing data and rendering. Time to fix that.

  • In static/app.js, scroll down to the fileInput.addEventListener block.
  • Find these lines at the end of the listener:
    const result = await response.json();
    console.log(result);
  • Replace console.log(result); with the three lines below, so the end of the listener looks like this:
    const result = await response.json();

    currentData = result.data;
    currentColumns = result.columns;

    renderChart();

Create a sample CSV for testing

If you do not have a CSV file handy, create a file called sample.csv on your Desktop with this content:

Month,Sales,Expenses
January,4200,3800
February,5100,4200
March,4800,3900
April,6200,5100
May,7500,5800
  • Save static/app.js and refresh http://127.0.0.1:8000 in your browser.
  • Upload a CSV file using the file picker.

The chart should now replace the sample January-to-May data with the data from your CSV file. You will see your CSV labels on the x-axis and your numeric values as bars.

Chart still showing sample data after upload?

  • Make sure you removed the console.log(result); line and replaced it with the three new lines. If both are present, the chart may not update.
  • Open the browser console (F12) and check for errors. A common cause is a typo in currentData or currentColumns that does not match the variable names at the top of the file.
  • Confirm your CSV has at least one text column and one numeric column. If all columns are numeric, renderChart cannot find a label column and exits early.

Need more help debugging?

Enable chart type switching

The dropdown is on the page and the chart renders from real data. But if you click the dropdown and switch to Pie, nothing happens. Two things need fixing: the createChart function needs to handle pie chart settings, and the dropdown needs an event listener.

  • In static/app.js, find this line inside the createChart function:
                legend: { display: false }
  • Replace it with this line, which shows the legend only when the chart is a pie chart:
                legend: { display: chartType === "pie" }
  • Right below that, find the scales line:
            scales: {
                y: { beginAtZero: true }
            }
  • Replace those three lines with this single line, which removes axis scales for pie charts (since they have no axes):
            scales: chartType === "pie" ? {} : {
                y: { beginAtZero: true }
            }
  • Now scroll down to the bottom of app.js. Find the closing }); of the fileInput.addEventListener block.
  • Add this event listener on a new line directly below it:
chartTypeSelect.addEventListener("change", renderChart);

What do these changes do?

  • The legend change makes the legend visible only for pie charts. Bar and line charts label the axis instead, so hiding the legend keeps the display clean.
  • The scales change removes the y-axis configuration for pie charts. Pie charts do not have axes, so passing an empty object avoids rendering artifacts.
  • The new event listener calls renderChart every time the dropdown value changes, destroying the old chart and creating a new one with the selected type.
  • Save static/app.js and refresh http://127.0.0.1:8000 in your browser.
  • Upload a CSV file using the file picker.

Before you switch the chart type, predict what you expect to see. Will the chart re-render immediately, or will you need to re-upload the file?

  • Switch the Chart Type dropdown from Bar to Pie.

The chart re-renders instantly as a pie chart with the same data. Each slice uses a different color, and the legend appears showing which color maps to which label. Switch to Line to see a line chart. All three types work from the same uploaded data without needing to re-upload.

Dropdown changes but nothing happens?

  • Make sure you added the chartTypeSelect.addEventListener("change", renderChart); line. Without it, the dropdown has no effect.
  • Check that the id on the <select> element in your HTML is exactly chart-type. A mismatch means getElementById returns null.
  • If switching to pie shows a pie chart but the axis labels are still visible, double check you replaced the scales line with the ternary version.

Still stuck? Ask for help.

✔️ Awesome, I've got everything!

Great. Double check that you have saved both static/app.js and static/index.html.

ⓧ I'd like to double check the full code

Scroll up through this step and confirm each piece is in place, in order:

  • static/index.html: The chart type <select> with bar, line, and pie options is inside the .controls section, after the file input.
  • static/app.js: let currentData = []; and let currentColumns = []; are below let chart = null;.
  • static/app.js: const chartTypeSelect line is below the fileInput line.
  • static/app.js: The createChart function uses chartType === "pie" for both legend.display and scales.
  • static/app.js: The renderChart function is below createChart, with auto-detection for string and numeric columns.
  • static/app.js: The fileInput listener stores currentData and currentColumns from the response and calls renderChart() (no console.log).
  • static/app.js: chartTypeSelect.addEventListener("change", renderChart); is below the fileInput listener.

Your dashboard now turns any CSV file into an interactive chart. Upload data, pick a chart type, and see the results instantly. Next up, you will add column picker controls so you can choose exactly which columns to visualize.

Add Column Picker Controls

Your dashboard now accepts CSV uploads and instantly renders a chart with real data. The auto-detection logic picks the first string column for labels and the first numeric column for values.

That works for simple files, but real CSV data has many columns. You need to choose which ones to visualize. In this step, you'll add dropdown selectors so you can pick any label column and any value column, making the dashboard flexible enough for any dataset.

In this step, get ready to:
  • Add a backend endpoint that serves column names from the uploaded file.
  • Add label and value dropdown selectors to the dashboard HTML.
  • Wire the dropdowns to re-render the chart when selections change.
Add a columns endpoint to your backend

The frontend needs to know which columns are available after a file upload. You already store the column names in uploaded_columns on the backend. You just need an endpoint that returns them.

  • In main.py, find the last line of the upload_csv function. Add this new endpoint below it, above the app.mount line:
# Return the column names from the most recent upload
@app.get("/api/columns")
async def get_columns():
    return {"columns": uploaded_columns}

What does this endpoint do?

This is a simple GET endpoint that returns the column names stored from the last CSV upload. The frontend will call this after each upload to fill the dropdown menus.

  • Save main.py.
  • Open your browser and visit http://127.0.0.1:8000/api/columns. You should see {"columns":[]} since no file has been uploaded yet.

Seeing a 404 or an error?

Make sure the new endpoint is placed above the app.mount("/static", ...) line. Routes defined after a mount can be shadowed.

Check your terminal for any syntax errors. The --reload flag should have restarted the server automatically after you saved.

Why is my /api/columns endpoint returning a 404?

✔️ Awesome, I've got everything!

Great. Double check you have saved main.py.

ⓧ I'd like to double check the full code

Here is the full main.py at this point:

import csv
import io
from fastapi import FastAPI, UploadFile
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles

app = FastAPI()

uploaded_data = []
uploaded_columns = []


@app.post("/api/upload")
async def upload_csv(file: UploadFile):
    global uploaded_data, uploaded_columns
    contents = await file.read()
    decoded = contents.decode("utf-8")
    reader = csv.DictReader(io.StringIO(decoded))
    uploaded_data = [row for row in reader]
    uploaded_columns = list(uploaded_data[0].keys()) if uploaded_data else []
    return {"columns": uploaded_columns, "data": uploaded_data}


@app.get("/api/columns")
async def get_columns():
    return {"columns": uploaded_columns}


app.mount("/static", StaticFiles(directory="static"), name="static")


@app.get("/")
async def root():
    with open("static/index.html") as f:
        return HTMLResponse(f.read())
Add column dropdown selectors to the HTML

The dashboard needs two new dropdown menus in the controls bar. One lets the user pick which column to use as chart labels. The other picks which column to use as chart values.

  • In static/index.html, find the closing </select> tag for the chart type dropdown. Add these lines directly below it, still inside the <section class="controls">:
            <label for="label-col">Label Column:</label>
            <select id="label-col"></select>

            <label for="value-col">Value Column:</label>
            <select id="value-col"></select>

Why are the dropdowns empty?

These <select> elements start with no <option> children. The JavaScript you'll write next will fill them with column names after a file upload.

  • Save static/index.html.
  • Refresh http://127.0.0.1:8000 in your browser. You should see two new empty dropdown menus labeled Label Column and Value Column next to the existing controls.

Don't see the new dropdowns?

Make sure the new labels and selects are inside the <section class="controls"> element, not outside it. They should sit between the chart type </select> and the closing </section> tag.

Why are my new dropdown menus not showing up on the page?

✔️ Awesome, I've got everything!

Great. Double check you have saved static/index.html.

ⓧ I'd like to double check the full code

Scroll up and confirm your static/index.html has these sections in order:

  • The <head> with the stylesheet link and the Chart.js script tag.
  • A <section class="controls"> containing the file input, chart type dropdown, label column dropdown, and value column dropdown.
  • A <section class="chart-container"> with the canvas element.
  • The app.js script tag at the bottom of the body.
Wire the dropdowns to your chart logic

The dropdowns are on the page, but the JavaScript still auto-detects columns. You need to update the code so it reads the selected values from the dropdowns instead.

  • In static/app.js, find this line:
const chartTypeSelect = document.getElementById("chart-type");
  • Add these two lines directly below it to reference the new dropdown elements:
// References to the column picker dropdowns
const labelColSelect = document.getElementById("label-col");
const valueColSelect = document.getElementById("value-col");
  • Now replace the entire renderChart function with this updated version that reads from the dropdowns instead of auto-detecting columns:
// Read selected columns from the dropdowns and update the chart
function renderChart() {
    if (currentData.length === 0) return;

    const labelCol = labelColSelect.value;
    const valueCol = valueColSelect.value;
    if (!labelCol || !valueCol) return;

    const labels = currentData.map(row => row[labelCol]);
    const values = currentData.map(row => parseFloat(row[valueCol]) || 0);
    const chartType = chartTypeSelect.value;

    createChart(labels, values, chartType);
}

What changed in renderChart?

The old version scanned every column to find the first string and first numeric column automatically. The new version simply reads labelColSelect.value and valueColSelect.value to get whatever the user selected.

The early return if (!labelCol || !valueCol) prevents errors when the dropdowns are still empty (before any upload).

  • Save static/app.js and refresh your browser.
  • The sample bar chart should still display. The new renderChart returns early because no data has been uploaded, so the initial sample chart is unaffected.

Next, you need a function that fills the dropdown menus with column names after each upload.

  • In static/app.js, add this new function directly below the renderChart function:
// Fill both dropdowns with column names from the uploaded CSV
function populateDropdowns(columns) {
    labelColSelect.innerHTML = "";
    valueColSelect.innerHTML = "";

    // Create an option in each dropdown for every column
    columns.forEach(col => {
        const labelOption = document.createElement("option");
        labelOption.value = col;
        labelOption.textContent = col;
        labelColSelect.appendChild(labelOption);

        const valueOption = document.createElement("option");
        valueOption.value = col;
        valueOption.textContent = col;
        valueColSelect.appendChild(valueOption);
    });

    // Smart defaults: first numeric col for values, first string col for labels
    const numericCol = columns.find(col =>
        currentData.length > 0 && !isNaN(parseFloat(currentData[0][col]))
    );
    if (numericCol) {
        valueColSelect.value = numericCol;
    }
    const stringCol = columns.find(col =>
        currentData.length > 0 && isNaN(parseFloat(currentData[0][col]))
    );
    if (stringCol) {
        labelColSelect.value = stringCol;
    }
}

What does populateDropdowns do?

  • It clears both dropdowns, then creates an <option> element in each dropdown for every column name in the CSV.
  • After populating, it auto-selects smart defaults. The first numeric column becomes the value selection and the first string column becomes the label selection.
  • This auto-selection logic is the same detection you had before, but now it only sets the initial dropdown selection. The user can override it by choosing a different column.

Now connect the remaining pieces. You need three small updates: make the chart legend show the selected column name, call populateDropdowns after each upload, and re-render the chart whenever a dropdown changes.

  • In the createChart function, find this line:
                label: "Value",
  • Replace it with this line so the chart legend shows the selected column name:
                label: valueColSelect.value || "Value",
  • In the fileInput.addEventListener callback, find the line renderChart(); and add populateDropdowns(currentColumns); on the line directly above it, so the two lines look like this:
    populateDropdowns(currentColumns);
    renderChart();
  • Finally, find the line chartTypeSelect.addEventListener("change", renderChart); near the bottom of the file. Add these two lines directly below it:
labelColSelect.addEventListener("change", renderChart);
valueColSelect.addEventListener("change", renderChart);

What do these event listeners do?

Each time the user picks a different column from either dropdown, the browser fires a change event. These listeners catch that event and call renderChart, which reads the new dropdown values and redraws the chart instantly.

  • Save static/app.js.

Before you test, what do you expect to happen when you upload a CSV file now? Think about how the data flows from the file input, to the backend, back to the frontend, into the dropdowns, and finally into the chart.

  • Refresh http://127.0.0.1:8000 in your browser.
  • Upload a CSV file with multiple columns using the file picker.
  • The Label Column and Value Column dropdowns should now populate with all column names from your CSV.
  • Select a different column from the Value Column dropdown. The chart should re-render with data from the newly selected column.
  • Select a different column from the Label Column dropdown. The chart labels should update to match.

Dropdowns not populating after upload?

Check that you added populateDropdowns(currentColumns); inside the fileInput event listener, right above renderChart();. Without this call, the dropdowns never get filled.

Also confirm that the dropdown id attributes in your HTML (label-col and value-col) match the getElementById calls in your JavaScript exactly.

My column dropdowns are not filling in after I upload a CSV file. Help me debug.

✔️ Awesome, I've got everything!

Great. Double check you have saved static/app.js.

ⓧ I'd like to double check the full code

Scroll up and confirm your static/app.js has these sections in order:

  • Five const declarations at the top: chartCanvas, fileInput, chartTypeSelect, labelColSelect, and valueColSelect.
  • The createChart function with label: valueColSelect.value || "Value" in the dataset.
  • The updated renderChart function that reads from labelColSelect.value and valueColSelect.value.
  • The populateDropdowns function that clears, fills, and auto-selects dropdown options.
  • The fileInput event listener with populateDropdowns(currentColumns) called before renderChart().
  • Three addEventListener("change", renderChart) calls for chartTypeSelect, labelColSelect, and valueColSelect.
  • The initial createChart call with sample data at the very bottom.

Secret mission

Add a Summary Stats Panel

Your dashboard charts the data, but real BI tools show you the numbers behind the visuals. Can you add a stats panel that shows row count, column count, and key statistics for every numeric column?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally on your machine. There are no cloud resources, paid services, or persistent infrastructure to worry about.

Resources you used:

  • Your project folder on the Desktop containing main.py and the static/ directory.
  • Python packages: fastapi, uvicorn, and python-multipart.

Keep everything running

No action needed. Choose this if you plan to keep experimenting with the dashboard or want to try the Secret Mission.

  • Leave your project folder on the Desktop as-is.
  • To start the dashboard again, open a terminal, navigate into your project folder, and run:
uvicorn main:app --reload

Your dashboard will be available at http://127.0.0.1:8000 just like before.

Pause - I'll come back to this later

Stop the running server to free up your terminal. Your project files and installed packages stay in place for when you come back.

  • Switch to the terminal where uvicorn is running.
  • Press Ctrl+C to stop the server.

Nothing else to clean up. When you are ready to resume, navigate back into your project folder and run uvicorn main:app --reload.

Delete - I don't want to use this again

Remove all project files and uninstall the Python packages you added during this project.

  • If the uvicorn server is still running, press Ctrl+C in its terminal to stop it.
  • Delete your project folder from the Desktop.

macOS

cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]

Windows

cd ~\Desktop
rmdir /s /q [[PROJECT_FOLDER="your project folder name"]]
  • Uninstall the Python packages by running:
pip uninstall -y fastapi uvicorn python-multipart

That's a Wrap!

That's a Wrap!

Nice work! You just built a full-stack data dashboard that turns any CSV file into interactive charts, all powered by Python and FastAPI.

You've learned how to:

  • Built a FastAPI backend from scratch with multiple REST API endpoints that accept CSV file uploads and return parsed JSON data.
  • Served a static HTML/CSS/JS frontend directly from FastAPI using StaticFiles and created interactive data visualizations with Chart.js including bar, line, and pie chart types.
  • Added dynamic column selection so the dashboard works with any CSV file structure, letting users pick which data to visualize.
  • Secret Mission: Added a summary statistics panel that calculates and displays min, max, mean, row count, and column count for uploaded data.

Ready to quiz yourself?