SpreadsheetsIntermediate

Turn an Excel Spreadsheet into a Web App with AI and Python

You have a sales spreadsheet, and every week someone asks you a slightly different question about it.

How did the North region do last month? Can you show only Keyboard sales? Can you remove Standing Desks? Can you send me the numbers for the first two weeks of January?

You open the spreadsheet, apply a filter, calculate the numbers, take a screenshot, and send it. Then another question arrives and you repeat the same work.

Instead of answering each question yourself, you can turn the spreadsheet analysis into a small web app. The people who need the numbers can choose a date range, region, or product themselves, and the results update automatically.

In this tutorial, we will build that app with Python, Mercury, and MLJAR Studio. We will not ask AI to create the whole dashboard in one prompt. We will build it one piece at a time, run each part, inspect the result, and then continue. This makes it much easier to understand what the app is doing and to change the layout while we work.

The final result is still a normal Jupyter notebook. We are adding an interface around our Python analysis rather than rewriting it as a separate web application.

What we are building

We will start with one month of sales data and create a small dashboard with a date filter, region filter, and product filter. The app will show four summary numbers, a daily revenue chart, and a table with results for each region.

The important part is that all of these elements use the same filtered dataframe. When somebody changes a filter, the calculations update together.

Get the data

Download the demo files from datasets-for-start and save them in:

~/Documents/sales/

For this tutorial, we use 2025-01.xlsx. It contains 503 January orders.

Mercury, the open-source Python library we will use to turn the notebook into a web app, is included with MLJAR Studio, so there is no separate Mercury installation needed for this tutorial.

How the notebook becomes an app

A Mercury app is still a notebook. We use normal pandas code for loading, filtering, and calculating the data, then add widgets such as a date picker or dropdown.

When somebody changes a widget, Mercury reruns the notebook using the new value and shows the updated output.

That is the basic idea.

Dashboard notebook in MLJAR Studio
MLJAR Studio showing the dashboard notebook for the January sales data

We will build the dashboard one cell at a time. This is especially useful when working on layout because you usually want to see the result before deciding what should come next.

A chart might look better beside the table than above it. The indicators might need another title. A filter may belong higher on the page. These are easier decisions to make while looking at the app than while describing the whole layout in advance.

Use AI to help with individual pieces

MLJAR Studio also has an AI assistant in the sidebar. When working inside a notebook, it can suggest code for the cell you are editing.

It does not automatically run that code. You can read the suggestion first and decide whether to insert it above, below, or instead of the current cell.

AI assistant suggesting notebook code
MLJAR Studio showing the AI assistant suggesting dashboard title code

For example, you can ask:

update title above indicators

and use the suggested code if it fits what you want.

I like this workflow for building a dashboard because the AI helps with small pieces while you stay in control of the notebook structure. You can see the code before adding it and immediately inspect what changes in the app.

Load the Excel data

Start with the imports and the spreadsheet:

import pandas as pd import altair as alt import mercury as mr file_path = "/home/piotr/Documents/sales/2025-01.xlsx" sales = pd.read_excel(file_path, sheet_name="Orders")

We use pandas for working with the spreadsheet, Altair for the chart, and Mercury for the app interface.

The workbook contains 503 January orders, covering January 1 through January 31.

For now, we use the full local file path. Later, before publishing the app, we will change it because /home/piotr/Documents/... only exists on this computer.

Add the filters

Next, create the controls that people will use at the top of the app:

min_date = sales["order_date"].min().date() max_date = sales["order_date"].max().date() date_filter = mr.DateRange( label="Order date", value=[min_date, max_date], min=min_date, max=max_date, ) region_filter = mr.MultiSelect( label="Region", choices=sorted(sales["region"].dropna().unique().tolist()), value=sorted(sales["region"].dropna().unique().tolist()), ) product_filter = mr.MultiSelect( label="Product", choices=sorted(sales["product"].dropna().unique().tolist()), value=sorted(sales["product"].dropna().unique().tolist()), )

This creates three controls: a date range, a region selector, and a product selector.

Notice that we do not type region or product names manually. They come directly from the spreadsheet:

sales["region"].dropna().unique()

and:

sales["product"].dropna().unique()

That makes the notebook easier to reuse. If a new region or product appears in another file, it can automatically appear in the filter too.

The value= argument controls what is selected when the app first opens. We select everything by default, so the visitor starts by seeing the full month.

Keep this cell near the top of the notebook because Mercury displays widgets in notebook order, and filters make more sense before the results they control.

Apply the filters to the data

Creating the widgets is only the first half. Now we need to use their values to filter the dataframe:

start_date, end_date = pd.to_datetime(date_filter.value) filtered_sales = sales[ (sales["order_date"] >= start_date) & (sales["order_date"] <= end_date) & (sales["region"].isin(region_filter.value)) & (sales["product"].isin(product_filter.value)) ].copy()

This creates a new dataframe called filtered_sales.

The date condition keeps orders inside the selected period. .isin(region_filter.value) keeps only the selected regions, and the same pattern is used for products.

The & between the conditions means that all conditions need to be true.

From this point on, the dashboard should use filtered_sales instead of the original sales dataframe. That is what connects the rest of the notebook to the filters.

Add the headline numbers

Now we can calculate the most important numbers for the selected data:

total_revenue = filtered_sales["revenue"].sum() total_orders = filtered_sales["order_id"].nunique() total_units = filtered_sales["units"].sum() average_order_value = ( filtered_sales.groupby("order_id")["revenue"].sum().mean() if total_orders > 0 else 0 ) mr.Markdown("## Key performance indicators") mr.Indicator( [ mr.Indicator( label="Total revenue", value=f"{total_revenue:,.2f} USD", ), mr.Indicator( label="Orders", value=f"{total_orders:,}", ), mr.Indicator( label="Units sold", value=f"{total_units:,}", ), mr.Indicator( label="Average order value", value=f"{average_order_value:,.2f} USD", ), ] )

With all filters selected, January gives us:

MetricValue
Total revenue363,879.30 USD
Orders503
Units sold8,762
Average order value723.42 USD

There is a small but important check inside the average calculation:

if total_orders > 0 else 0

Someone can use the filters to select a combination with no matching orders. Without this check, we could end up trying to calculate an average from an empty dataset.

Instead, the indicator shows zero and the app continues working.

Add the daily revenue chart

Next, let us show how revenue changes through the month.

First, calculate revenue for each day:

daily_sales = ( filtered_sales.groupby("order_date", as_index=False)["revenue"] .sum() .sort_values("order_date") )

Then create an Altair chart:

revenue_chart = ( alt.Chart(daily_sales) .mark_line(point=True, color="#2563eb") .encode( x=alt.X("order_date:T", title="Order date"), y=alt.Y("revenue:Q", title="Revenue (USD)"), tooltip=[ alt.Tooltip("order_date:T", title="Date"), alt.Tooltip( "revenue:Q", title="Revenue", format=",.2f", ), ], ) .properties( title="Daily revenue", width="container", height=400, ) .interactive() )

groupby("order_date") gives us one revenue value for each day in January.

The tooltip lets people hover over the line and see the exact value. width="container" allows the chart to adapt to the available space, and .interactive() adds zooming and panning.

At this point, we have created the chart but have not yet placed it in the app layout. We will do that after creating the table.

Create the summary table

Now calculate orders and revenue for each region:

summary_stats = ( filtered_sales.groupby("region", as_index=False) .agg( total_orders=("order_id", "nunique"), total_revenue=("revenue", "sum"), ) .sort_values("total_revenue", ascending=False) ) summary_stats["total_revenue"] = summary_stats["total_revenue"].map( lambda value: f"{value:,.2f} USD" )

With the full January data selected, the table looks like this:

regiontotal_orderstotal_revenue
North152114,891.70 USD
South14197,687.40 USD
West12183,745.90 USD
East8967,554.30 USD

There is one useful detail in this code: we sort the values before converting the revenue numbers into formatted text.

Once 114891.70 becomes 114,891.70 USD, it is a string rather than a number. Sorting strings follows text rules, not numeric rules.

That can produce a table that looks reasonable but is in the wrong order, so calculate and sort first, then format for display.

As another check, the four regional revenue values add up to 363,879.30 USD, which matches our total revenue indicator.

Put the chart and table side by side

Now we can create two columns:

left, right = mr.Columns(2)

Display the chart on the left:

with left: left.clear() display(revenue_chart)

and the table on the right:

with right: right.clear() mr.Table(summary_stats)

mr.Columns(2) creates a two-column layout.

The clear() calls are useful because Mercury reruns the notebook when someone changes a filter. Clearing the column before drawing prevents old versions of the chart or table from being left behind after a rerun.

At this point, the main dashboard is ready.

Preview the app while you build it

You do not need to finish the whole notebook before seeing what the app looks like.

Use the preview button in the toolbar to open the live app next to the notebook.

Live app preview beside the notebook
MLJAR Studio showing the live app preview for the sales dashboard

This is especially useful when adjusting layout. You can change a cell, run it, and immediately see how the result looks in the app.

The screenshot above has some products deselected, so the numbers are smaller than the full January totals we calculated earlier. That is the filters working.

You can also configure how the app presents itself.

App title, description, and display settings
MLJAR Studio showing the app title and display settings for the sales dashboard

For this example, we can use the title Sales dashboard, description Sales performance, and a 💰 emoji.

You can also control the background and text colors, whether visitors can see the notebook code, and whether the app uses the full width of the browser.

For a dashboard, full width usually works well because charts benefit from the extra space.

Prepare the notebook for publishing

The dashboard works on our computer, but before publishing it we need to make a few small changes.

Change the file path

Right now the notebook uses:

file_path = "/home/piotr/Documents/sales/2025-01.xlsx"

That path exists only on this computer. Once the notebook runs on a server, there will probably be no /home/piotr/Documents/sales/ directory.

Change it to:

file_path = "2025-01.xlsx"

Now Python looks for the spreadsheet next to the notebook. That works locally when the files are together, and it also works when we upload both files to the server.

Add requirements.txt

The server also needs to know which Python packages the notebook uses.

Create a requirements.txt file containing:

pandas altair openpyxl

openpyxl is easy to forget because we do not import it directly. Pandas uses it behind the scenes to read .xlsx files.

Without it, pd.read_excel(...) can work perfectly on your computer and fail on the server because the Excel engine is missing.

Mercury itself does not need to be listed because it is already available on the platform.

Keep the three files together

For this app, we need three files:

FilePurpose
dashboard.ipynbThe notebook and app
2025-01.xlsxThe sales data
requirements.txtPython dependencies

Publishing the notebook does not make the Excel file part of it. The app still needs access to the workbook it reads.

You can upload these files during publishing or manage them later on MLJAR Platform.

Application files on MLJAR Platform
MLJAR Platform showing the app files for the sales dashboard

If the app starts but reports that it cannot find 2025-01.xlsx, check whether the spreadsheet was uploaded.

If the application fails during startup, requirements.txt is one of the first things worth checking.

Publish the web app

When everything works in preview, click Publish in the toolbar.

Publish button in MLJAR Studio
MLJAR Studio showing the publish button for the dashboard notebook

Choose the Mercury page where you want to publish the notebook.

Choose a Mercury page
MLJAR Studio showing the Mercury page selection for publishing

Then select the files to upload. For this tutorial, choose the notebook, the Excel workbook, and requirements.txt.

Select deployment files
MLJAR Studio showing the file selection for publishing

After the upload, Studio shows the app address.

Published application address
MLJAR Studio showing the published app URL for the sales dashboard

Open that address in a browser.

Published sales dashboard
Mercury showing the published sales dashboard for January sales

The notebook is now a web interface.

People can choose filters without opening the spreadsheet or editing Python. With the full January dataset selected, the app shows the same results we calculated in the notebook: 363,879.30 USD in revenue from 503 orders, with North as the largest region at 114,891.70 USD.

This check matters too. Publishing changes how people interact with the analysis, but it should not change the numbers.

The app also works on a phone

The layout adapts when the browser becomes narrower.

Sales dashboard on a mobile phone

On a phone, the filters use more of the available width and the results move underneath them.

There is no separate mobile application to maintain. It is the same notebook and the same app.

That makes the dashboard useful when somebody is away from their desk and only wants to check a number quickly.

Decide who should have access

Once the dashboard is online, you also need to decide who should be able to open it.

For public or demo data, a public link may be enough.

For an internal report, you can protect the app with a password so only people who know it can access the dashboard.

Mercury is also open source, so you can run the application on your own infrastructure. That can be useful when the data should stay inside your network rather than being hosted on an external server.

The important part is to match the deployment method to the data you are working with. A public sales demo and an internal financial dashboard should not automatically use the same access settings.

What we actually built

It is worth looking back at the notebook.

Most of the code is ordinary data analysis. We load an Excel file with pandas, filter rows, calculate sums, use groupby(), and build an Altair chart.

The parts that turn it into an application are comparatively small. We added a few Mercury widgets for inputs, indicators for the headline values, and columns for the layout.

That means we do not need to maintain one analysis notebook and a completely separate web application. The notebook is the application.

When the underlying analysis changes, we update the notebook. When next month's data arrives, we can replace the spreadsheet or extend the workflow to load another file.

Take it further

One useful next step is to let visitors provide their own spreadsheet instead of keeping 2025-01.xlsx fixed in the notebook.

Mercury has an UploadFile widget for that. The same dashboard could then work with different monthly exports without changing the Python file path manually. The Jupyter notebook to web app guide covers more Mercury widgets, styling, authentication, and deployment options.

You can also customize the colors, fonts, and layout so the app matches the rest of your internal tools.

The important part is that these improvements can be added gradually. You do not need to design the final application before starting.

Build one part, inspect it, and then decide what should come next.

What we did

We started with a normal Excel sales file and a Python notebook. First, we loaded the data. Then we added a date range, region selector, and product selector.

We used those values to create filtered_sales, and every later calculation used that filtered dataframe. From it, we created four headline indicators, a daily revenue chart, and a table with regional results.

We arranged the chart and table into a simple dashboard and used the live preview to inspect the interface while building it.

Finally, we changed the local file path, added the required dependencies, uploaded the notebook and its data, and published the result as a web app.

We deliberately did not create everything with one AI prompt. Building the dashboard step by step makes it easier to understand what each part does, check the numbers, adjust the layout, and catch problems before publishing.

Next steps

If dataframe selection, filtering, sorting, and calculated columns are new to you, start with Python for Excel Users. Then continue with Excel formulas in Python to understand the pandas calculations behind this dashboard.

You can also use Merge all Excel files in a folder to build the same dashboard from a full year instead of one month.

Before publishing business data, Clean messy Excel data is also worth reading.

For more dashboard patterns and data-source options, explore Python dashboards from notebooks. For a broader guide to widgets, styling, access control, and deployment, see Turn a Jupyter notebook into a web app.

Continue in MLJAR Studio

Run this workflow with your own data

Download MLJAR Studio to run the Python workflow on your own files. Review the code, edit each step, and keep the result as a reusable notebook.

Download Studio

Related Tutorials

Continue with a closely related workflow.