ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Flask + ECharts 构建数据可视化看板:从零到一实战指南

Flask + ECharts 构建数据可视化看板:从零到一实战指南 在实际的数据分析项目中单纯使用 Jupyter Notebook 进行探索性分析往往只是第一步。要让分析结果具备交互性、可分享性和业务价值构建一个集数据获取、处理、分析和可视化于一体的 Web 应用是更专业的做法。使用 Python 的 Flask 框架作为后端结合百度开源的 ECharts 库进行前端图表渲染可以快速搭建出功能强大且美观的数据可视化平台。这类项目不仅能够巩固你的全栈技能更能将抽象的数据转化为直观的洞察是简历中极具分量的实战项目。本文将以一个模拟的“霸王茶姬”门店销量数据分析为背景带你从零开始构建一个完整的 Flask ECharts 数据可视化项目。你将学习到如何设计数据模型、搭建 Flask 应用骨架、连接数据库、编写 API 接口以及在前端使用 ECharts 动态渲染多种图表。完成这个项目后你将掌握一个可复用的数据分析可视化项目模板并能将其应用于其他业务场景。1. 项目核心架构与技术栈解析在动手编码之前理解整个项目的技术选型和数据流转逻辑至关重要。这能帮助你在遇到问题时快速定位是前端、后端还是数据层的故障。1.1 为什么选择 Flask ECharts 组合Flask 是一个轻量级的 Python Web 框架以其灵活、简洁和易于扩展的特性著称。对于数据分析类项目我们通常不需要 Django 那样重量级、全功能的框架。Flask 允许我们以最小的开销快速构建 RESTful API专注于数据处理和业务逻辑的交付。ECharts 是一个由百度开源的数据可视化图表库使用 JavaScript 实现。它提供了丰富的图表类型折线图、柱状图、饼图、散点图、地图等并且配置项高度灵活交互性能优秀。其核心工作流程是前端通过 AJAX 从 Flask 后端获取格式化好的 JSON 数据然后将数据绑定到预先定义好的图表配置项上最后渲染出交互式图表。这个组合的优势在于前后端分离清晰Flask 负责“计算”和“提供数据”ECharts 负责“展示”和“交互”。这种架构也便于后期扩展例如替换 Vue/React 作为前端框架或者将 Flask 替换为 FastAPI。1.2 项目数据流与核心模块设计一个完整的数据分析可视化项目其数据流通常遵循“数据源 - 数据处理 - 数据存储 - 后端服务 - 前端展示”的路径。对于本练习项目我们将其简化为以下几个核心模块数据层 (Data Layer): 使用 MySQL 作为关系型数据库存储模拟生成的销量数据。我们将创建数据表并编写脚本生成模拟数据。后端服务层 (Backend Service Layer): 使用 Flask 框架构建。主要职责包括定义应用路由URL。连接数据库执行 SQL 查询。对查询结果进行聚合、转换等数据处理。将处理后的数据封装成 JSON 格式通过 API 接口提供给前端。提供静态文件HTML, CSS, JS服务。前端展示层 (Frontend Presentation Layer): 由 HTML、JavaScript 和 ECharts 构成。主要职责包括构建基础页面布局。使用 AJAX或 Fetch API调用后端提供的 API。使用获取到的 JSON 数据初始化并渲染 ECharts 图表。实现图表间的联动、数据筛选等交互功能。整个系统的调用关系是用户浏览器请求 Flask 渲染的首页 - 首页加载 JS 脚本 - JS 脚本请求 Flask 的数据 API - Flask 查询数据库并返回 JSON - JS 用 JSON 数据渲染 ECharts 图表。2. 开发环境准备与项目初始化一个稳定的开发环境是项目顺利推进的基础。请严格按照以下步骤配置避免因环境问题导致后续步骤失败。2.1 Python 环境与必备包安装首先确保你的系统已安装 Python建议版本 3.8 及以上。你可以通过命令行检查python --version # 或 python3 --version接下来为项目创建一个独立的虚拟环境。这能有效隔离项目依赖避免包版本冲突。# 在项目根目录下执行 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate激活后命令行提示符前通常会显示(venv)。然后安装本项目所需的 Python 包pip install flask pymysql pandasflask: Web 框架核心。pymysql: MySQL 数据库的 Python 连接器。pandas: 用于数据清洗、处理和聚合在复杂数据分析场景下非常有用。虽然简单项目可直接用 SQL 聚合但 Pandas 能提供更灵活的内存计算。2.2 数据库准备与模拟数据生成本项目使用 MySQL 作为数据存储。请确保你已安装并运行 MySQL 服务可以使用本地安装的 MySQL或云数据库服务。登录 MySQL 并创建数据库-- 通过命令行登录MySQL密码提示时输入你的root密码 mysql -u root -p -- 创建数据库指定字符集为utf8mb4以支持中文 CREATE DATABASE tea_sales CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 使用该数据库 USE tea_sales;创建数据表我们设计一张sales_records表来存储销量记录。CREATE TABLE sales_records ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, date DATE NOT NULL COMMENT 销售日期, store_name VARCHAR(50) NOT NULL COMMENT 门店名称, product_name VARCHAR(50) NOT NULL COMMENT 产品名称, sales_volume INT NOT NULL COMMENT 销量杯, revenue DECIMAL(10, 2) NOT NULL COMMENT 销售额元, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 记录创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT销量记录表; -- 为常用查询字段添加索引提升查询性能 CREATE INDEX idx_date ON sales_records (date); CREATE INDEX idx_store ON sales_records (store_name);生成模拟数据创建一个 Python 脚本generate_data.py来批量插入模拟数据。这比手动插入高效得多。# generate_data.py import pymysql from datetime import datetime, timedelta import random # 数据库连接配置 db_config { host: localhost, user: root, password: your_password, # 替换为你的MySQL密码 database: tea_sales, charset: utf8mb4 } # 模拟数据参数 stores [北京国贸店, 上海南京路店, 广州天河店, 深圳福田店, 杭州西湖店] products [伯牙绝弦, 春日桃桃, 桂馥兰香, 花田乌龙, 青青糯山] start_date datetime(2024, 1, 1) days 180 # 生成半年的数据 def generate_sales_data(): connection pymysql.connect(**db_config) cursor connection.cursor() sql INSERT INTO sales_records (date, store_name, product_name, sales_volume, revenue) VALUES (%s, %s, %s, %s, %s) data_to_insert [] for i in range(days): current_date start_date timedelta(daysi) for store in stores: for product in products: # 生成随机销量并模拟一些销售趋势如周末销量更高 base_volume random.randint(50, 150) if current_date.weekday() 5: # 周六、周日 base_volume int(base_volume * random.uniform(1.2, 1.8)) # 模拟不同产品的单价差异 price_map {伯牙绝弦: 18, 春日桃桃: 22, 桂馥兰香: 20, 花田乌龙: 16, 青青糯山: 19} unit_price price_map[product] revenue base_volume * unit_price data_to_insert.append((current_date.date(), store, product, base_volume, revenue)) # 分批提交避免单次插入数据量过大 if len(data_to_insert) 1000: cursor.executemany(sql, data_to_insert) connection.commit() data_to_insert.clear() print(f已批量插入数据当前日期: {current_date.date()}) # 插入剩余数据 if data_to_insert: cursor.executemany(sql, data_to_insert) connection.commit() cursor.close() connection.close() print(模拟数据生成完毕) if __name__ __main__: generate_sales_data()运行此脚本前请务必将db_config中的password替换为你自己的 MySQL root 密码。然后在激活的虚拟环境中运行python generate_data.py脚本运行成功后可以在 MySQL 中查询验证SELECT COUNT(*) FROM sales_records; SELECT * FROM sales_records LIMIT 5;3. 构建 Flask 后端应用后端是项目的引擎负责处理所有数据请求和业务逻辑。我们将按照 Flask 应用的典型结构来组织代码。3.1 项目目录结构规划一个清晰的项目结构有助于代码管理和维护。建议创建如下目录flask-echarts-tea-sales/ ├── app.py # Flask 应用主入口 ├── config.py # 配置文件数据库连接等 ├── generate_data.py # 模拟数据生成脚本之前已创建 ├── requirements.txt # 项目依赖列表 ├── static/ # 静态资源文件夹 │ ├── css/ │ │ └── style.css │ └── js/ │ └── charts.js # ECharts 初始化及数据获取逻辑 └── templates/ # Jinja2 模板文件夹 └── index.html # 主页面首先创建requirements.txt文件记录依赖Flask2.3.3 PyMySQL1.1.0 pandas2.1.0然后创建配置文件config.py将敏感信息和可变配置集中管理# config.py import os class Config: # 密钥用于会话安全等生产环境应从环境变量读取 SECRET_KEY os.environ.get(SECRET_KEY) or a-hard-to-guess-string-for-dev # 数据库配置 DB_HOST localhost DB_USER root DB_PASSWORD your_password # 务必替换 DB_NAME tea_sales DB_CHARSET utf8mb4 property def SQLALCHEMY_DATABASE_URI(self): # 如果使用Flask-SQLAlchemy可以用这个格式 # 本项目直接使用pymysql此属性仅供参考 return fmysqlpymysql://{self.DB_USER}:{self.DB_PASSWORD}{self.DB_HOST}/{self.DB_NAME}?charset{self.DB_CHARSET} config Config()3.2 编写 Flask 主应用与数据 API现在创建核心文件app.py。我们将在这里初始化 Flask 应用、定义路由和编写数据接口。# app.py from flask import Flask, render_template, jsonify import pymysql from config import config import json from datetime import datetime app Flask(__name__) app.config.from_object(config) def get_db_connection(): 创建并返回一个数据库连接 connection pymysql.connect( hostapp.config[DB_HOST], userapp.config[DB_USER], passwordapp.config[DB_PASSWORD], databaseapp.config[DB_NAME], charsetapp.config[DB_CHARSET], cursorclasspymysql.cursors.DictCursor # 返回字典格式的结果 ) return connection app.route(/) def index(): 渲染主页面 return render_template(index.html) app.route(/api/sales_summary) def get_sales_summary(): API: 获取销售概览数据近30天趋势门店排名产品占比 connection get_db_connection() try: with connection.cursor() as cursor: # 1. 近30天每日总销售额趋势 trend_sql SELECT date, SUM(revenue) as total_revenue FROM sales_records WHERE date DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY date ORDER BY date cursor.execute(trend_sql) trend_data cursor.fetchall() # 格式化日期和金额 trend_result { dates: [item[date].strftime(%m-%d) for item in trend_data], revenues: [float(item[total_revenue]) for item in trend_data] } # 2. 门店销售额排名本月 store_rank_sql SELECT store_name, SUM(revenue) as total_revenue FROM sales_records WHERE YEAR(date) YEAR(CURDATE()) AND MONTH(date) MONTH(CURDATE()) GROUP BY store_name ORDER BY total_revenue DESC LIMIT 10 cursor.execute(store_rank_sql) store_rank_data cursor.fetchall() store_rank_result { stores: [item[store_name] for item in store_rank_data], revenues: [float(item[total_revenue]) for item in store_rank_data] } # 3. 产品销量占比本月 product_pie_sql SELECT product_name, SUM(sales_volume) as total_volume FROM sales_records WHERE YEAR(date) YEAR(CURDATE()) AND MONTH(date) MONTH(CURDATE()) GROUP BY product_name cursor.execute(product_pie_sql) product_pie_data cursor.fetchall() product_pie_result [ {name: item[product_name], value: item[total_volume]} for item in product_pie_data ] return jsonify({ success: True, data: { trend: trend_result, store_rank: store_rank_result, product_pie: product_pie_result } }) except Exception as e: app.logger.error(fError in get_sales_summary: {e}) return jsonify({success: False, message: str(e)}), 500 finally: connection.close() app.route(/api/store_detail/store_name) def get_store_detail(store_name): API: 获取指定门店的详细销售数据 connection get_db_connection() try: with connection.cursor() as cursor: # 获取该门店各产品近7天的销量 sql SELECT product_name, date, SUM(sales_volume) as daily_volume FROM sales_records WHERE store_name %s AND date DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY product_name, date ORDER BY date, product_name cursor.execute(sql, (store_name,)) raw_data cursor.fetchall() # 将数据转换为前端ECharts需要的格式产品为系列日期为X轴 # 先获取所有唯一的产品和日期 products sorted(set([item[product_name] for item in raw_data])) dates sorted(set([item[date].strftime(%m-%d) for item in raw_data])) # 初始化数据结构 series_data {product: [0] * len(dates) for product in products} date_index {date: idx for idx, date in enumerate(dates)} # 填充数据 for item in raw_data: p item[product_name] d item[date].strftime(%m-%d) idx date_index[d] series_data[p][idx] item[daily_volume] # 构建ECharts需要的series数组 series [] for product in products: series.append({ name: product, type: line, data: series_data[product] }) result { dates: dates, series: series } return jsonify({success: True, data: result}) except Exception as e: app.logger.error(fError in get_store_detail for {store_name}: {e}) return jsonify({success: False, message: str(e)}), 500 finally: connection.close() if __name__ __main__: # debugTrue 仅用于开发生产环境必须关闭 app.run(debugTrue, host0.0.0.0, port5000)关键代码解释get_db_connection(): 封装了数据库连接逻辑使用DictCursor使查询结果以字典形式返回便于处理。app.route(/api/sales_summary): 定义了第一个数据 API。它在一个接口中返回了三种不同维度的聚合数据减少了前端请求次数。SQL 查询中使用了CURDATE()等日期函数来动态计算“最近30天”、“本月”。app.route(/api/store_detail/store_name): 这是一个动态路由store_name是路径参数。前端点击不同门店时可以传递不同的门店名来获取对应的详细数据。这里的数据处理逻辑稍复杂需要将行数据(日期产品销量)转换为 ECharts 多系列折线图需要的格式X轴日期列表 Series: [{name:产品1, data:[...]}, ...]。jsonify(): Flask 提供的函数将 Python 字典转换为 JSON 格式的 HTTP 响应并自动设置正确的Content-Type头。错误处理使用try...except...finally确保数据库连接被正确关闭。在捕获异常时记录日志并返回错误信息给前端这是生产级应用的基本要求。3.3 创建前端模板与静态资源首先创建主页面模板templates/index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title霸王茶姬销量数据分析看板/title !-- 引入 ECharts 核心库 -- script srchttps://cdn.jsdelivr.net/npm/echarts5.5.1/dist/echarts.min.js/script !-- 引入中国地图扩展如果需要 -- !-- script srchttps://cdn.jsdelivr.net/npm/echarts/map/js/china.js/script -- !-- 引入项目自定义样式 -- link relstylesheet href{{ url_for(static, filenamecss/style.css) }} /head body div classcontainer header classdashboard-header h1 霸王茶姬门店销售数据可视化看板/h1 p classsubtitle数据更新日期span idcurrent-date/span/p /header div classdashboard-grid !-- 第一行概览图表 -- div classchart-card full-width h2 近30天销售趋势/h2 div idtrend-chart classchart-container/div /div !-- 第二行两个并列图表 -- div classchart-card h2 本月门店销售额排名/h2 div idstore-rank-chart classchart-container/div /div div classchart-card h2 本月产品销量构成/h2 div idproduct-pie-chart classchart-container/div /div !-- 第三行门店详情图表 -- div classchart-card full-width h2 门店详情分析 small(点击上方柱状图门店查看)/small/h2 div idstore-detail-chart classchart-container p classplaceholder请点击上方“门店销售额排名”图表中的柱子查看该门店各产品近7天销售趋势。/p /div /div /div footer classdashboard-footer p技术栈Python Flask MySQL ECharts | 数据为模拟生成仅用于演示/p /footer /div !-- 引入自定义JavaScript -- script src{{ url_for(static, filenamejs/charts.js) }}/script script // 页面加载完成后初始化图表 document.addEventListener(DOMContentLoaded, function() { // 设置当前日期 const now new Date(); document.getElementById(current-date).textContent now.toLocaleDateString(zh-CN); // 初始化所有图表 initAllCharts(); }); /script /body /html然后创建基本的样式文件static/css/style.css/* style.css */ * { margin: 0; padding: 0; box-sizing: border-box; font-family: Segoe UI, Microsoft YaHei, sans-serif; } body { background-color: #f5f7fa; color: #333; line-height: 1.6; padding: 20px; } .container { max-width: 1400px; margin: 0 auto; } .dashboard-header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 2px solid #eaeaea; } .dashboard-header h1 { color: #2c3e50; margin-bottom: 10px; } .subtitle { color: #7f8c8d; font-size: 1.1em; } .dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); gap: 25px; margin-bottom: 30px; } .chart-card { background: white; border-radius: 12px; padding: 25px; box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08); transition: transform 0.3s ease, box-shadow 0.3s ease; } .chart-card:hover { transform: translateY(-5px); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12); } .chart-card.full-width { grid-column: 1 / -1; } .chart-card h2 { color: #34495e; margin-bottom: 20px; font-size: 1.4em; display: flex; align-items: center; } .chart-card h2 small { font-size: 0.7em; color: #95a5a6; margin-left: 10px; font-weight: normal; } .chart-container { width: 100%; height: 400px; /* 为图表容器设置固定高度 */ } .placeholder { text-align: center; color: #bdc3c7; font-style: italic; line-height: 400px; /* 垂直居中 */ margin: 0; } .dashboard-footer { text-align: center; margin-top: 40px; padding-top: 20px; border-top: 1px solid #eaeaea; color: #95a5a6; font-size: 0.9em; } /* 响应式调整 */ media (max-width: 1100px) { .dashboard-grid { grid-template-columns: 1fr; } .chart-card { min-width: auto; } }最后创建最核心的 JavaScript 文件static/js/charts.js在这里我们将调用后端 API 并渲染 ECharts 图表// charts.js // 全局图表实例 let trendChart, storeRankChart, productPieChart, storeDetailChart; // 当前选中的门店 let currentSelectedStore null; // 初始化所有图表 function initAllCharts() { // 初始化图表DOM容器 trendChart echarts.init(document.getElementById(trend-chart)); storeRankChart echarts.init(document.getElementById(store-rank-chart)); productPieChart echarts.init(document.getElementById(product-pie-chart)); storeDetailChart echarts.init(document.getElementById(store-detail-chart)); // 加载数据并渲染图表 loadSalesSummaryData(); // 为门店排名图表添加点击事件监听 bindStoreChartClickEvent(); } // 加载销售概览数据 function loadSalesSummaryData() { fetch(/api/sales_summary) .then(response response.json()) .then(result { if (result.success) { const data result.data; renderTrendChart(data.trend); renderStoreRankChart(data.store_rank); renderProductPieChart(data.product_pie); } else { console.error(Failed to load summary data:, result.message); alert(加载概览数据失败请检查控制台或后端日志。); } }) .catch(error { console.error(Network error:, error); alert(网络请求失败请确保后端服务已启动。); }); } // 1. 渲染销售趋势折线图 function renderTrendChart(trendData) { const option { tooltip: { trigger: axis, formatter: function(params) { let res div stylemargin-bottom:5px${params[0].axisValue}/div; params.forEach(p { res div${p.marker} ${p.seriesName}: b¥${p.value.toLocaleString()}/b/div; }); return res; } }, grid: { left: 3%, right: 4%, bottom: 10%, top: 15%, containLabel: true }, xAxis: { type: category, boundaryGap: false, data: trendData.dates, axisLabel: { rotate: 45 // 日期标签旋转避免重叠 } }, yAxis: { type: value, axisLabel: { formatter: ¥{value} } }, series: [{ name: 销售额, type: line, smooth: true, symbol: circle, symbolSize: 8, itemStyle: { color: #5470c6 }, lineStyle: { width: 4 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: rgba(84, 112, 198, 0.6) }, { offset: 1, color: rgba(84, 112, 198, 0.05) } ]) }, data: trendData.revenues }] }; trendChart.setOption(option); // 窗口大小变化时重绘图表 window.addEventListener(resize, function() { trendChart.resize(); }); } // 2. 渲染门店排名柱状图 function renderStoreRankChart(rankData) { const option { tooltip: { trigger: axis, axisPointer: { type: shadow }, formatter: {b}: ¥{c} }, grid: { left: 3%, right: 4%, bottom: 10%, top: 10%, containLabel: true }, xAxis: { type: value, axisLabel: { formatter: ¥{value} } }, yAxis: { type: category, data: rankData.stores, axisLabel: { fontSize: 12 }, inverse: true // 让排名第一的显示在最上面 }, series: [{ name: 销售额, type: bar, data: rankData.revenues, itemStyle: { color: function(params) { // 给前三名不同的颜色 const colorList [#c23531, #2f4554, #61a0a8, #91c7ae, #749f83]; return colorList[params.dataIndex] || #d48265; } }, label: { show: true, position: right, formatter: ¥{c} } }] }; storeRankChart.setOption(option); window.addEventListener(resize, function() { storeRankChart.resize(); }); } // 3. 渲染产品销量饼图 function renderProductPieChart(pieData) { const option { tooltip: { trigger: item, formatter: {a} br/{b}: {c}杯 ({d}%) }, legend: { orient: vertical, left: left, top: center }, series: [{ name: 产品销量, type: pie, radius: [40%, 70%], // 环形图 center: [60%, 50%], avoidLabelOverlap: false, itemStyle: { borderRadius: 10, borderColor: #fff, borderWidth: 2 }, label: { show: false, position: center }, emphasis: { label: { show: true, fontSize: 18, fontWeight: bold } }, labelLine: { show: false }, data: pieData }] }; productPieChart.setOption(option); window.addEventListener(resize, function() { productPieChart.resize(); }); } // 4. 渲染门店详情折线图 function renderStoreDetailChart(detailData) { if (!detailData || !detailData.series || detailData.series.length 0) { // 如果没有数据显示提示 storeDetailChart.clear(); storeDetailChart.setOption({ graphic: { type: text, left: center, top: middle, style: { text: 暂无数据或未选择门店, fontSize: 16, fill: #999 } } }); return; } const option { tooltip: { trigger: axis }, legend: { data: detailData.series.map(s s.name), top: 5% }, grid: { left: 3%, right: 4%, bottom: 10%, top: 15%, containLabel: true }, xAxis: { type: category, boundaryGap: false, data: detailData.dates }, yAxis: { type: value, name: 销量杯 }, series: detailData.series }; storeDetailChart.setOption(option); window.addEventListener(resize, function() { storeDetailChart.resize(); }); } // 绑定门店排名图表的点击事件 function bindStoreChartClickEvent() { storeRankChart.on(click, function(params) { if (params.componentType series params.seriesType bar) { const storeName params.name; currentSelectedStore storeName; loadStoreDetailData(storeName); } }); } // 加载指定门店的详情数据 function loadStoreDetailData(storeName) { fetch(/api/store_detail/${encodeURIComponent(storeName)}) .then(response response.json()) .then(result { if (result.success) { renderStoreDetailChart(result.data); // 更新详情图表标题提示 const titleElement document.querySelector(#store-detail-chart).parentNode.querySelector(h2); const smallText titleElement.querySelector(small); if (smallText) { smallText.textContent 当前查看: ${storeName}; } } else { console.error(Failed to load detail for ${storeName}:, result.message); alert(加载门店【${storeName}】详情失败。); } }) .catch(error { console.error(Network error:, error); }); }4. 运行验证与结果分析至此所有核心代码已编写完成。现在让我们启动应用验证整个流程是否畅通。4.1 启动 Flask 应用并访问确保你的虚拟环境已激活且所有依赖已安装 (pip install -r requirements.txt)。确保config.py中的数据库配置正确且 MySQL 服务正在运行。在项目根目录下运行 Flask 应用python app.py如果一切正常你将在终端看到类似输出* Serving Flask app app * Debug mode: on WARNING: This is a development server. Do not use it in a production deployment. * Running on all addresses (0.0.0.0) * Running on http://127.0.0.1:5000 * Running on http://192.168.x.x:5000 Press CTRLC to quit打开浏览器访问http://127.0.0.1:5000。你应该能看到一个完整的仪表盘页面。4.2 功能验证与交互测试按照以下步骤逐一测试确保每个功能点都正常工作页面加载页面应正常显示包含标题和三个图表容器。销售趋势图、门店排名柱状图、产品占比饼图应已渲染并显示数据。数据准确性检查图表数据是否合理。例如趋势图应有30个数据点门店排名应有5家门店与我们模拟数据一致饼图应包含5种产品。图表交互悬停提示将鼠标悬停在任意图表的图形元素上折线点、柱子、饼图扇形应出现 Tooltip 显示详细数据。门店详情联动点击“门店销售额排名”柱状图中的任意一个柱子代表一家门店。下方的“门店详情分析”区域应刷新显示该门店各产品近7天的销量趋势折线图。同时详情图表的标题应更新为“当前查看: [门店名]”。API 接口测试可以直接在浏览器地址栏测试后端 API访问http://127.0.0.1:5000/api/sales_summary应返回一个完整的 JSON 对象。访问http://127.0.0.1:5000/api/store_detail/北京国贸店应返回北京国贸店近7天各产品销量的 JSON 数据。4.3 预期结果与界面解读如果一切顺利你将看到一个功能完整、交互流畅的数据可视化仪表盘。销售趋势图展示了最近30天每日的总销售额变化。你可以观察到可能的周期性波动如周末高峰。门店排名图直观展示了本月各家门店的销售额对比。颜色区分了前三名。产品占比图以环形图形式展示了本月各产品的销量占比清晰显示了哪些是畅销品。门店详情图初始状态有提示文字。当你点击上方柱状图的门店后这里会动态变为该门店各产品近7天的销量趋势多折线图便于进行单品分析和对比。这个看板实现了从宏观概览趋势、排名、占比到微观下钻具体门店详情的完整数据分析路径是业务分析中非常典型的场景。5. 常见问题排查与优化建议在开发和学习过程中你可能会遇到一些问题。以下是一些常见问题的排查思路和解决方案。5.1 启动与运行阶段问题问题现象可能原因检查方式处理建议运行python app.py报ModuleNotFoundError1. 虚拟环境未激活。2. 依赖包未安装。1. 命令行前是否有(venv)。2. 运行pip list查看是否安装了 flask, pymysql。1. 激活虚拟环境。2. 在虚拟环境中运行pip install -r requirements.txt。访问http://127.0.0.1:5000无法连接1. Flask 应用未成功启动。2. 端口被占用。3. 防火墙或安全软件阻止。1. 检查终端是否有成功启动的输出。2. 尝试更换端口app.run(port5001)。3. 检查系统防火墙设置。1. 根据终端错误信息解决。2. 使用netstat -ano | findstr :5000(Win) 或lsof -i:5000(Mac/Linux) 查看端口占用并结束进程。3. 临时关闭防火墙或添加规则。页面能打开但图表区域空白或显示“加载中”1. 浏览器控制台有 JS 错误。2. 后端 API 请求失败。3. ECharts 库加载失败。1. 按 F12 打开开发者工具查看 Console 和 Network 标签页。2. 在 Network 中查看/api/sales_summary请求的状态码和响应内容。1. 修复 JS 语法错误。2. 检查后端 API 路由和逻辑确保返回正确的 JSON。3. 检查网络确保能访问 ECharts CDN。图表有数据但中文显示为方框1. 数据库连接字符集非utf8mb4。2. Flask 响应头未设置正确编码。3. HTML 页面字符集声明错误。1. 检查CREATE DATABASE和连接配置。2. 检查浏览器接收的响应头Content-Type。3. 检查 HTML 的meta charset。1. 确保数据库、连接、表都使用utf8mb4。2. Flask 默认使用 UTF-8通常没问题。3. 确保 HTML 文件保存为 UTF-8 编码且meta charsetUTF-8。5.2 数据与逻辑问题问题现象可能原因检查方式处理建议图表没有数据显示“暂无数据”1. 数据库中没有数据。2. SQL 查询条件错误如日期范围。3. API 接口内部报错。1. 登录 MySQL 查询sales_records表是否有数据。2. 在 Flask 后端打印 SQL 语句和查询结果。3. 查看 Flask 运行终端的错误输出。1. 运行generate_data.py生成数据。2. 调试 SQL可以先在 MySQL 客户端直接运行以验证。3. 在后端代码中添加print或使用日志记录关键变量。点击门店柱子详情图表不更新1. 点击事件未绑定成功。2. 前端路由拼接错误。3. 后端动态路由未正确处理门店名。1. 检查浏览器控制台是否有 JS 错误。2. 查看 Network 中点击后发起的请求 URL 是否正确。3. 检查后端get_store_detail函数是否收到正确的store_name参数。1. 确保bindStoreChartClickEvent在图表渲染后被调用。2. 使用encodeURIComponent处理门店名中的特殊字符。3. 在后端打印接收到的store_name参数。数据量变大后页面加载慢1. 前端一次性请求数据量过大。2. 后端 SQL 查询未优化。3. 缺少数据库索引。1. 使用浏览器开发者工具的 Network 面板查看请求响应大小和时间。2. 使用EXPLAIN分析 SQL 语句。3. 检查常用查询字段如date,store_name是否有索引。1. 后端进行分页或聚合只返回前端渲染所需的数据。2. 优化 SQL避免SELECT *使用合适的WHERE和GROUP BY。3. 为高频查询条件字段添加索引。5.3 项目优化与扩展方向当前项目是一个完整的教学原型但要用于更严肃的场景或投入生产环境还需要考虑以下优化点配置管理将数据库密码等敏感信息从代码中移除使用环境变量或专门的配置文件如.env文件并通过python-dotenv加载。绝对不要将密码硬编码在代码中并提交到版本控制系统。数据库连接池目前每次 API 请求都新建和关闭数据库连接在高并发下性能很差。应使用连接池技术如DBUtils或SQLAlchemy内置的连接池。错误处理与日志增强后端错误处理给前端返回更友好的错误信息同时将详细的错误日志记录到文件便于排查生产环境问题。前端体验优化加载状态在数据加载时图表区域显示一个加载动画。数据刷新添加一个按钮或设置定时器定期如每5分钟自动刷新数据。响应式布局进一步优化 CSS确保在手机和平板上有良好的显示效果。图表功能增强数据缩放在趋势图上添加 ECharts 的 dataZoom 组件允许用户查看特定时间段的细节。图例开关在门店详情折线图中允许用户点击图例来显示/隐藏某个产品的曲线。导出图片添加一个按钮允许用户将图表导出为 PNG 图片。后端架构优化使用蓝图如果 API 增多使用 Flask 的 Blueprint 来模块化路由。加入缓存对于不经常变化的聚合数据如昨日排名可以使用 Redis 或内存缓存减少数据库查询压力。异步任务如果数据生成或处理非常耗时可以考虑使用 Celery 等工具将其转为后台异步任务通过 WebSocket 或轮询通知前端任务完成。通过解决上述问题并实施优化这个项目将从学习Demo升级为一个健壮、可维护、接近生产级别的数据可视化应用这无疑会成为你简历中一个亮眼的实战项目。
返回列表