随着数据量的增长与业务可视化需求提升,传统静态图表已经无法满足需求。我们需要:
这正是Dash大显身手的舞台——由Plotly开发的Dash框架可快速构建Web数据仪表盘,无需HTML/CSS/JS基础。
Dash 应用结构主要包括:
我们将用Dash构建一个完整的数据仪表盘,功能包括:
模拟销售数据(字段:日期、产品、地区、销售额等),格式如下:
date,region,product,sales
2025-01-01,North,ProductA,1234
2025-01-01,South,ProductB,876
...
我们将通过Dash实时计算如下统计指标:
S = ∑ i = 1 n s a l e s i S = \sum_{i=1}^{n} sales_i S=i=1∑nsalesi
P r = ∑ i = 1 n r s a l e s i ∑ j = 1 n s a l e s j P_r = \frac{\sum_{i=1}^{n_r} sales_i}{\sum_{j=1}^{n} sales_j} Pr=∑j=1nsalesj∑i=1nrsalesi
D a v g = S N days D_{avg} = \frac{S}{N_{\text{days}}} Davg=NdaysS
import dash
from dash import html, dcc, Input, Output, State, dash_table
import pandas as pd
import plotly.express as px
import io
import base64
# 初始化应用
app = dash.Dash(__name__)
app.title = "销售数据仪表盘"
# 全局变量(存储上传的数据)
global_df = pd.DataFrame()
# 页面布局
app.layout = html.Div([
html.H2(" Dash 销售数据仪表盘", style={'textAlign': 'center'}),
# 文件上传
dcc.Upload(
id='upload-data',
children=html.Div(['拖拽或点击上传 CSV 文件']),
style={'width': '98%', 'height': '60px', 'lineHeight': '60px',
'borderWidth': '1px', 'borderStyle': 'dashed', 'borderRadius': '5px',
'textAlign': 'center', 'margin': '10px'},
multiple=False
),
html.Div(id='file-upload-message'),
# 筛选控件
html.Div([
dcc.DatePickerRange(id='date-range'),
dcc.Dropdown(id='product-dropdown', placeholder="选择产品", multi=True),
dcc.Dropdown(id='region-dropdown', placeholder="选择区域", multi=True)
], style={'display': 'flex', 'gap': '10px', 'margin': '20px'}),
# 指标卡展示
html.Div(id='stats-cards', style={'display': 'flex', 'gap': '20px', 'margin': '20px'}),
# 图表展示
dcc.Graph(id='line-chart'),
dcc.Graph(id='bar-chart'),
# 数据表格
dash_table.DataTable(id='data-table', page_size=10, style_table={'overflowX': 'auto'})
])
# 文件上传回调
@app.callback(
Output('file-upload-message', 'children'),
Output('date-range', 'min_date_allowed'),
Output('date-range', 'max_date_allowed'),
Output('date-range', 'start_date'),
Output('date-range', 'end_date'),
Output('product-dropdown', 'options'),
Output('region-dropdown', 'options'),
Input('upload-data', 'contents'),
State('upload-data', 'filename')
)
def update_data(contents, filename):
global global_df
if contents:
content_type, content_string = contents.split(',')
decoded = base64.b64decode(content_string)
global_df = pd.read_csv(io.StringIO(decoded.decode('utf-8')))
global_df['date'] = pd.to_datetime(global_df['date'])
min_date = global_df['date'].min()
max_date = global_df['date'].max()
product_opts = [{'label': p, 'value': p} for p in global_df['product'].unique()]
region_opts = [{'label': r, 'value': r} for r in global_df['region'].unique()]
return f"成功加载文件:{filename}", min_date, max_date, min_date, max_date, product_opts, region_opts
return "", None, None, None, None, [], []
# 主回调:更新图表与统计卡
@app.callback(
Output('stats-cards', 'children'),
Output('line-chart', 'figure'),
Output('bar-chart', 'figure'),
Output('data-table', 'data'),
Input('date-range', 'start_date'),
Input('date-range', 'end_date'),
Input('product-dropdown', 'value'),
Input('region-dropdown', 'value')
)
def update_dashboard(start_date, end_date, products, regions):
if global_df.empty:
return [], {}, {}, []
df = global_df.copy()
df = df[(df['date'] >= start_date) & (df['date'] <= end_date)]
if products:
df = df[df['product'].isin(products)]
if regions:
df = df[df['region'].isin(regions)]
# 聚合指标
total_sales = df['sales'].sum()
avg_sales = df.groupby('date')['sales'].sum().mean()
max_product = df.groupby('product')['sales'].sum().idxmax()
# 构建指标卡
stats = [
html.Div([
html.H4(" 总销售额"),
html.H3(f"{total_sales:,.2f}")
], style={'padding': '10px', 'border': '1px solid gray', 'borderRadius': '8px'}),
html.Div([
html.H4(" 日均销售"),
html.H3(f"{avg_sales:,.2f}")
], style={'padding': '10px', 'border': '1px solid gray', 'borderRadius': '8px'}),
html.Div([
html.H4(" 热门产品"),
html.H3(max_product)
], style={'padding': '10px', 'border': '1px solid gray', 'borderRadius': '8px'})
]
# 绘图
fig_line = px.line(df, x='date', y='sales', color='product', title="销售折线图")
fig_bar = px.bar(df, x='region', y='sales', color='product', title="区域销售柱状图")
return stats, fig_line, fig_bar, df.to_dict('records')
# 启动服务
if __name__ == '__main__':
app.run_server(debug=True)
问题描述 | 检查状态 | 修复建议 |
---|---|---|
上传文件格式异常是否捕捉 | ✅ | 使用try-except包裹read_csv |
全局DataFrame未初始化是否报错 | ✅ | 加入空判断 global_df.empty |
图表空数据时是否显示错误 | ✅ | 返回空figure: {} |
日期筛选控件初始化是否报错 | ✅ | 默认使用min/max_date作为初始范围 |
指标卡与图表是否联动更新 | ✅ | 所有控件使用同一过滤DataFrame |
CSS样式是否影响组件排布 | ✅ | 使用flex并设置gap、padding合理布局 |
通过本文,我们从0构建了一个具备上传、筛选、图表、聚合统计功能的Dash仪表盘,具备如下特性: