Plotly

Plotly

November 11, 2026

How to Build AI Dashboards in Python with Plotly

TL;DR

  • AI dashboards combine interactive views with AI capabilities such as natural-language questions. A statistical forecast can sit alongside the AI layer.
  • Build routine views with Dash callbacks, then use Plotly Studio Embedded for questions beyond those views.
  • Define the data, reporting periods, units, and access rules before adding chat.
  • This tutorial provides a complete Dash app, synthetic sample data, and a forecast that is shown only when it beats a simple baseline on a holdout period.
  • The region dropdown filters the charts. The chat can explore all regions in the registered dataset, and the app labels that distinction.
  • Publish the AI dashboard on Plotly Cloud or Dash Enterprise. Plotly Studio Embedded adds conversational exploration on both platforms.

Your sales dashboard lets colleagues filter revenue by region and compare the top products. During the weekly review, someone wants to compare the products behind the North and West regions’ results. The existing charts do not include that comparison.

AI dashboards give viewers another way to investigate: ask a question in the application, inspect the resulting analysis, and check the query behind it. The recurring questions still belong in clear views and filters, where the calculations run consistently.

This tutorial builds a weekly-sales dashboard with Plotly Dash and Plotly’s Python graphing library. You will create the interactive views, add a statistical revenue forecast, publish the application on Plotly Cloud or Dash Enterprise, and enable AI chat through Plotly Studio Embedded on either platform.

The local app uses open-source libraries. On Plotly Cloud, the AI experience uses Plotly credits, and the Free plan includes trial credits rather than unlimited AI usage. Dash Enterprise uses your organization’s enterprise deployment and AI configuration.

Part of the AI dashboard

Built with

What it does

Data preparation

pandas

Validates dates, revenue values, and weekly records before analysis.

Interactive views

Dash callbacks and Plotly charts

Filters weekly revenue and ranks products for the selected region.

Statistical forecast

statsmodels ETS

Shows a 12-week forecast with an 80% pointwise prediction interval only when the model beats the baseline on the holdout.

AI data chat

Plotly Studio Embedded on Plotly Cloud or Dash Enterprise

Explores the registered sales dataset and lets viewers inspect the queries behind answers.

Hosting and sharing

Plotly Cloud or Dash Enterprise

Publishes the AI dashboard on Plotly-managed infrastructure or within your organization’s infrastructure, with app access controls.

What is an AI dashboard?

An AI dashboard is an interactive dashboard that uses AI to help viewers explore or interpret data. Depending on the implementation, that might include natural-language questions, generated charts, or explanations of analytical results.

In this example, the AI capability is conversational exploration through Plotly Studio Embedded. The forecast is a separate classical statistical model. Forecasting does not have to use generative AI, and an AI dashboard does not have to include a forecast.

Dash supplies the application structure and callbacks: Python functions that update charts when a viewer changes a control. Plotly supplies the charts. This division lets you combine repeatable calculations with a conversational interface in the same published experience.

Define the data and the questions first

The example uses one row for each week, region, and product.

Column

Meaning

week

Monday starting a complete seven-day reporting week

region

Sales region

product

Product name

revenue

Revenue in US dollars for that week, region, and product

The data contract matters as much as the column names. Confirm whether revenue includes refunds, how sales are attributed to regions, and when a reporting week is considered complete. Exclude incomplete periods upstream.

For this walkthrough, every product must have a record in every region for every reporting week. A verified zero-sales week should have a row with zero revenue. Missing records remain missing data until you establish what happened; the app rejects them rather than silently replacing them with zeros.

That requirement is deliberately strict. If your products launch at different times or are available only in selected regions, adapt the validation to an explicit availability calendar before using your own data.

Use ordinary views for questions such as weekly revenue by region and the leading products over four complete weeks. Send additional descriptive comparisons to chat, with the relevant dates and region stated explicitly. Questions about causes, customer behavior, or future events need supporting data and methods beyond the four columns used here.

1. Set up the project and sample data

Use Python 3.11 and create an empty project folder. Save these pinned top-level dependencies as requirements.txt:

dash==4.4.1
numpy==2.0.2
pandas==2.3.3
plotly==7.1.0
statsmodels==0.14.6

Create and activate a virtual environment, then install the dependencies:

python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt

On Windows, create the environment with py -3.11 -m venv .venv and activate it with .venv\Scripts\Activate.ps1 in PowerShell.

Save the following as make_sample_data.py. It generates 104 weeks of synthetic sales for four regions and three products. The data ends with the complete week starting October 19, 2026; it is a fixed teaching dataset, not a live sales feed.

from pathlib import Path
import numpy as np
import pandas as pd
rng = np.random.default_rng(42)
weeks = pd.date_range(end="2026-10-19", periods=104, freq="W-MON")
rows = []
for i, week in enumerate(weeks):
for region, multiplier in {"North": 1.1, "South": 0.9, "East": 1.0, "West": 1.2}.items():
for product, base in {"Atlas": 3500, "Beacon": 2300, "Comet": 1200}.items():
revenue = max(0, multiplier * (base + 12 * i) + rng.normal(0, 80))
rows.append([week, region, product, round(revenue, 2)])
path = Path(__file__).with_name("sales.csv")
pd.DataFrame(rows, columns=["week", "region", "product", "revenue"]).to_csv(path, index=False)
print(f"Wrote {len(rows)} synthetic records to {path.name}.")

Run it once:

python make_sample_data.py

Keep sales.csv, app.py, and requirements.txt together in the project folder. The generator is only needed to create the sample CSV.

If you use a coding agent, give it this data contract and the dependency file. Plotly’s coding-assistant guide explains how to connect agents to current Dash documentation through the Plotly Cloud MCP server and use an AGENTS.md file for project conventions.

A useful prompt begins with the intended application:

Build a Dash app to explore weekly revenue from sales.csv. Add a region dropdown, a weekly revenue chart, and a chart of the leading products over the latest four complete reporting weeks. Label the data coverage and units. Keep the AI chat's dataset scope explicit.

Plotly Studio offers an alternative authoring route: connect data, explore it conversationally, and generate an interactive application. This walkthrough continues with the standalone Python project below. Studio’s Enterprise export is a project package with code, dependencies, and potentially commercially licensed components.

2. Build the dashboard and forecast in one file

Save this complete application as app.py:

from functools import lru_cache
from pathlib import Path
import logging
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from dash import Dash, Input, Output, dcc, html
from statsmodels.tsa.exponential_smoothing.ets import ETSModel
log = logging.getLogger(__name__)
HORIZON = 12
MIN_WEEKS = 64
KEYS = ["week", "region", "product"]
def load_sales(path):
frame = pd.read_csv(path)
required = KEYS + ["revenue"]
if not set(required).issubset(frame.columns):
raise ValueError(f"CSV must contain {required}.")
frame = frame[required].copy()
frame["week"] = pd.to_datetime(frame["week"], errors="raise")
if frame.empty or frame.isna().any().any():
raise ValueError("Sales data must be non-empty, with no missing values.")
if frame["week"].dt.tz is not None:
raise ValueError("Use timezone-free week-start dates.")
if ((frame["week"].dt.dayofweek != 0)
| (frame["week"] != frame["week"].dt.normalize())).any():
raise ValueError("Each week must be a Monday date with no time.")
for column in ["region", "product"]:
frame[column] = frame[column].astype(str).str.strip()
if frame[column].eq("").any():
raise ValueError(f"{column} cannot be blank.")
frame["revenue"] = pd.to_numeric(frame["revenue"], errors="raise")
if not np.isfinite(frame["revenue"]).all():
raise ValueError("Revenue must contain only finite numbers.")
if frame.duplicated(KEYS).any():
raise ValueError("Expected one row per week, region, and product.")
weeks = pd.date_range(frame["week"].min(), frame["week"].max(), freq="W-MON")
expected = pd.MultiIndex.from_product(
[weeks, sorted(frame["region"].unique()), sorted(frame["product"].unique())],
names=KEYS,
)
actual = pd.MultiIndex.from_frame(frame[KEYS])
if len(expected.difference(actual)):
raise ValueError("Missing weekly records: resolve gaps before using this app.")
return frame.sort_values(KEYS).reset_index(drop=True)
# This is the only module-level dataframe exposed to Studio Embedded.
sales = load_sales(Path(__file__).with_name("sales.csv"))
REGIONS = sorted(sales["region"].unique())
DATA_START = sales["week"].min()
DATA_END = sales["week"].max()
def message_figure(title, message):
figure = go.Figure()
figure.add_annotation(
text=message, x=0.5, y=0.5, xref="paper", yref="paper", showarrow=False
)
figure.update_layout(title=title, template="plotly_white")
return figure
def fit_ets(history):
fitted = ETSModel(
history, error="add", trend="add", damped_trend=True, seasonal=None
).fit(disp=False, maxiter=2000)
if not fitted.mle_retvals.get("converged", False):
raise ValueError("The model did not converge.")
return fitted
@lru_cache(maxsize=32)
def forecast_view(region):
history = (
sales.loc[sales["region"].eq(region)]
.groupby("week")["revenue"].sum().sort_index().asfreq("W-MON")
)
title = f"Weekly revenue forecast: {region}"
if len(history) < MIN_WEEKS:
return message_figure(title, "Forecast withheld"), (
f"At least {MIN_WEEKS} complete weeks are required for this example."
)
try:
train, test = history.iloc[:-HORIZON], history.iloc[-HORIZON:]
predicted = np.asarray(fit_ets(train).forecast(HORIZON))
model_mae = float(np.mean(np.abs(test.to_numpy() - predicted)))
naive_mae = float(np.mean(np.abs(test.to_numpy() - train.iloc[-1])))
if not np.isfinite([model_mae, naive_mae]).all():
raise ValueError("The holdout evaluation produced non-finite values.")
note = (
f"Last {HORIZON} weeks held out: ETS MAE USD {model_mae:,.2f}; "
f"last-value baseline MAE USD {naive_mae:,.2f}."
)
if model_mae >= naive_mae:
return message_figure(title, "Forecast withheld"), (
note + " ETS did not beat the baseline on this holdout."
)
fitted = fit_ets(history)
prediction = fitted.get_prediction(
start=len(history), end=len(history) + HORIZON - 1, method="exact"
).summary_frame(alpha=0.2)
values = prediction[["mean", "pi_lower", "pi_upper"]].to_numpy()
if not np.isfinite(values).all():
raise ValueError("The forecast produced non-finite values.")
figure = go.Figure()
figure.add_scatter(x=history.index, y=history, name="Actual revenue")
figure.add_scatter(
x=prediction.index, y=prediction["pi_upper"],
line={"width": 0}, showlegend=False, hoverinfo="skip",
)
figure.add_scatter(
x=prediction.index, y=prediction["pi_lower"], fill="tonexty",
fillcolor="rgba(0,110,180,0.15)", line={"width": 0},
name="80% pointwise prediction interval", hoverinfo="skip",
)
figure.add_scatter(
x=prediction.index, y=prediction["mean"], name="ETS forecast",
line={"dash": "dash"},
)
figure.add_vline(x=history.index[-1], line_dash="dot")
figure.update_layout(
title=title, xaxis_title="Week starting Monday",
yaxis_title="Revenue (USD)", template="plotly_white",
)
return figure, note
except Exception:
log.exception("Forecast failed for region=%s", region)
return message_figure(title, "Forecast withheld"), (
"Model fitting or prediction failed. Review the server log."
)
app = Dash(__name__)
server = app.server
app.title = "Weekly sales explorer"
app.layout = html.Main([
html.H1("Weekly sales explorer"),
html.P(
f"Data coverage: weeks starting {DATA_START:%Y-%m-%d} to "
f"{DATA_END:%Y-%m-%d}. All records must represent complete weeks."
),
html.P(
"Charts follow the selected region. AI chat can query all regions; "
"include the region and exact dates in your question."
),
html.Label("Region", htmlFor="region"),
dcc.Dropdown(
options=REGIONS, value=REGIONS[0], id="region", clearable=False
),
dcc.Graph(id="revenue-trend"),
dcc.Graph(id="top-products"),
html.H2("Forecast and validation"),
dcc.Loading(dcc.Graph(id="forecast-chart")),
html.P(id="forecast-check", role="status"),
html.P(
"The shaded band is an 80% pointwise prediction interval under the "
"fitted model. It is not a guarantee or a simultaneous 12-week band."
),
html.H2("Ask about sales"),
html.P(
"Publish on Plotly Cloud or Dash Enterprise and enable Studio Embedded. "
"Open its chat button to ask questions and inspect the methodology."
),
html.Details([
html.Summary("Data definitions"),
html.Ul([
html.Li("week: Monday starting a complete seven-day reporting week."),
html.Li("region: sales region; the dropdown does not restrict chat access."),
html.Li("product: product name."),
html.Li("revenue: revenue in USD for one week, region, and product."),
]),
]),
], style={
"maxWidth": "1100px", "margin": "0 auto", "padding": "24px",
"fontFamily": "Arial, sans-serif",
})
@app.callback(
Output("revenue-trend", "figure"),
Output("top-products", "figure"),
Output("forecast-chart", "figure"),
Output("forecast-check", "children"),
Input("region", "value"),
)
def update_views(region):
if region not in REGIONS:
blank = message_figure("No region selected", "Choose a valid region")
return blank, blank, blank, "Choose a valid region."
rows = sales.loc[sales["region"].eq(region)]
weekly = rows.groupby("week", as_index=False)["revenue"].sum().sort_values("week")
trend = px.line(
weekly, x="week", y="revenue", title=f"Weekly revenue: {region}",
labels={"week": "Week starting Monday", "revenue": "Revenue (USD)"},
template="plotly_white",
)
start = DATA_END - pd.Timedelta(weeks=3)
recent = rows.loc[rows["week"].between(start, DATA_END)]
top = (
recent.groupby("product", as_index=False)["revenue"].sum()
.sort_values(["revenue", "product"], ascending=[False, True]).head(5)
)
bars = px.bar(
top, x="revenue", y="product", orientation="h",
title=f"Top products: {region}, {start:%Y-%m-%d} to {DATA_END:%Y-%m-%d}",
labels={"revenue": "Revenue (USD)", "product": "Product"},
template="plotly_white",
)
bars.update_yaxes(categoryorder="total ascending")
forecast, note = forecast_view(region)
return trend, bars, forecast, note
if __name__ == "__main__":
app.run(debug=True)

Run the application:

python app.py

Open the local address printed in the terminal. Changing the region updates the revenue trend, the leading products, and the forecast panel. The product chart uses exactly four reporting weeks, anchored to the latest week in the dataset.

The application rejects duplicate keys, missing records, non-Monday dates, blank categories, and invalid revenue values. It reads the CSV once when the process starts. Updating the source file does not automatically refresh a running process.

How the forecast earns its place

The forecast uses a non-seasonal exponential-smoothing model with an additive error, an additive trend, and damping. Statsmodels documents the ETS model family and its prediction intervals.

The code reserves the latest 12 weeks as a holdout. It fits on the earlier history, predicts those 12 weeks, and compares the mean absolute error, or MAE, with a baseline that repeats the final training value. Lower MAE means smaller average absolute errors in US dollars.

Only if ETS beats that baseline does the application refit on the full history and display the next 12 weeks. A failed fit, insufficient history, or an unsuccessful baseline comparison produces an explicit Forecast withheld panel. It does not retain a previous region’s forecast.

The 64-week minimum is a choice for this example, giving the holdout exercise at least 52 training observations. It is not a universal forecasting requirement.

The dashed line shows the forecast, and the shaded band shows an 80% pointwise prediction interval conditional on the model. It does not mean there is an 80% chance that the entire 12-week path will fall inside the band.

Before using the forecast commercially, evaluate several rolling holdouts, inspect interval coverage, and test appropriate seasonal baselines. This model does not include annual seasonality, promotions, or external drivers. Additive models can also produce negative predictions or bounds; review the model specification rather than silently clipping the output. The synthetic dataset demonstrates the workflow, not forecast accuracy on your business.

3. Publish on Plotly Cloud or Dash Enterprise

Publish the same Python application on either platform, then enable Studio Embedded in the deployed app. Choose the publishing route that fits your infrastructure and access requirements.

Publish on Plotly Cloud

Use the Plotly Cloud file-upload workflow:

  1. Sign in to Plotly Cloud.
  2. Select Publish and upload app.py, sales.csv, and requirements.txt.
  3. Set app.py as the main file and select Python 3.11.
  4. Select Save & publish, then inspect the build logs and open the running app.
  5. Configure sharing before inviting viewers.

Cloud configures the production server; this route does not require a Procfile. Keep your local virtual environment out of the uploaded files.

As of November 10, 2026, the pricing page lists 10 Dash or Studio apps, three private viewers, and 10 trial Plotly credits on the Free plan. Check the current allowances before choosing a plan. Trial AI credits and ongoing hosting allowances are different entitlements.

Publish on Dash Enterprise

Use your organization’s Dash Enterprise instance to deploy the application within its infrastructure.

1. Keep app.py, sales.csv, and requirements.txt together in the project folder.

2. Add gunicorn to requirements.txt and create a file named Procfile with the following content:

web: gunicorn app:server --workers 2

3. Set up and authenticate the Dash Enterprise CLI using the instructions for your instance.

4. From the project folder, deploy the application:

de deploy --name sales-ai-dashboard

5. Review the build and runtime logs, open the deployed application, and configure viewer access.

6. Enable Plotly Studio Embedded for the app using your organization’s Enterprise configuration.

These CLI steps apply to instances using the standard CLI deployment workflow. If your instance uses the GitHub integration, publish through its configured repository and deployment branch instead.

Follow the Dash Enterprise deployment guide and app-structure requirements for your installed version. The worker count is an example; size the application for its workload and available resources.

4. Enable AI chat with Plotly Studio Embedded

Plotly Studio Embedded runs inside apps published on Plotly Cloud or Dash Enterprise. On either platform, it adds a conversational interface for exploring the data the application makes available.

On Plotly Cloud, open the published app’s Plotly Studio Embedded tab and turn on Enable Plotly Studio Embedded for this app. The setting triggers a rebuild; the chat appears when the build completes.

On Dash Enterprise, enable Studio Embedded in the deployed environment using the AI and access configuration provided by your administrator. Confirm that the configured model connection and data access meet your organization’s requirements.

The sales dataframe is defined at module level so it can be made available to Studio Embedded. In the documented Plotly Cloud setup, it is registered automatically at initialization; changes made while the application runs are not picked up by the chat. Check the registered datasets and refresh behavior in your deployment before sharing the app.

This example deliberately gives chat access to all regions, while the dropdown filters the three charts. The application tells viewers to include the region and exact dates in their questions. Selecting West does not restrict what the chat can query.

Only register data that every authorized viewer of this application may explore. App sign-in and chart filters serve different purposes; filtering a chart is not a row-level access policy.

On Plotly Cloud, viewers authenticate with their own Plotly Cloud accounts, and conversations consume credits from the viewer’s team. On Dash Enterprise, use the authentication and AI configuration established for your organization’s instance. Plotly’s Embedded announcement also describes the Methodology section, where viewers can inspect the queries used to produce an answer. You can inspect the experience in the live Embedded demo.

This route does not require you to implement an LLM provider callback or an executor for model-generated SQL. For sensitive data, evaluate the service’s processing and privacy terms separately from its app-access settings.

5. Check an AI answer against a known result

Start with a question whose answer you can calculate independently. For the unmodified synthetic dataset, ask:

Using sales, sum revenue in USD separately for each product in the West region for the weeks starting 2026-09-28, 2026-10-05, 2026-10-12, and 2026-10-19. Return a table and a bar chart. Show the query used.

The expected totals are:

Product

Revenue, USD

Atlas

22,785.81

Beacon

16,983.41

Comet

11,421.85

Inspect the methodology for the region condition, the four week-start dates, grouping by product, and the sum of revenue. The table should reconcile with the West product chart.

Then try a cross-region comparison using the same dates. A question such as why did revenue fall? requires care: this dataset can identify which recorded products contributed to a change, but it cannot establish the underlying cause.

Visible queries expose the executed analytical process. They do not guarantee that the requested business question was interpreted correctly. Check the metric definition and data coverage as well as the SQL.

Questions that recur in reviews are candidates for new callbacks and fixed views. That gives colleagues a dependable route to common answers while keeping chat available for additional exploration.

Choose the publishing environment for your AI dashboard

Plotly Cloud provides a managed publishing route. Dash Enterprise runs analytical applications within your organization’s infrastructure and provides enterprise identity, access, and lifecycle controls.

Studio Embedded is available on both platforms. Choose between them based on where the application, data access, compute, and model processing need to run, along with who will manage that environment.

For a deployment using a private model, configure the approved model endpoint and network access for your Enterprise environment. Self-hosting the application and configuring its AI processing are related responsibilities; verify both against your organization’s requirements.

These application patterns already appear in production. Crant combines social-media analytics and an LLM chatbot in its Dash application; its customer story reports 28 hours of weekly analysis time saved across the broader implementation. Somerset NHS Foundation Trust uses Dash Enterprise for applications including inpatient forecasting and anomaly detection, serving a wider analytical user base of more than 12,000 staff.

Start with the supplied sample, verify one chart and one conversational answer against the same calculation, and then replace the data under a documented reporting contract. Publish on Plotly Cloud or Dash Enterprise, enable Studio Embedded, and give your team interactive views and conversational exploration in the same application.

Frequently asked questions

bluesly
x
instagram
youtube
medium
fcaebook

Product

© 2026
Plotly. All rights reserved.
Cookie Preferences
aicpa/soc2