Flask+ECharts+MySQL搭建可交互疫情数据可视化大屏:多模块联动与实时优化

发布时间:2026/10/22 3:54:01
Flask+ECharts+MySQL搭建可交互疫情数据可视化大屏:多模块联动与实时优化 当时我接到一个数据可视化需求把一堆散落在上报渠道里的传染病报告数据做成一张能交互的大屏。左一张Excel表右一张PPT看数据的人最常问的一句话是这个地方能点进去看吗。于是我直接用Python Flask ECharts MySQL这套组合从零搭了一份疫情数据可视化系统。今天这篇就把整个设计与实现过程拆开讲重点聊多模块联动的设计思路以及实时展示的优化手段。说明一下所有展示数据完全是脱敏的模拟数据不指向任何真实地区或真实事件。这个系统能做什么一张大屏上分成几个区域地图展示各区域累计确诊分布折线图展示每日新增趋势柱状图显示各区域的治愈与死亡对比顶部是几个汇总数字卡片。最核心的地方在于点击地图上的某个区域其他所有图表会跟着一起变——趋势图聚焦该区域排行榜也切换到该区域内部的数据。翻到指定日期所有模块同步更新。这套交互在业务汇报里非常实用同时也是最容易出bug的地方所以我把这部分单独拿出来讲清楚。适合谁来参考如果你是刚学Flask的后端新手想做一个拿得出手的完整项目或者你在做数据可视化大屏但一直没有好的联动方案甚至你是产品经理想理解技术实现难度和边界都能从这里面找到直接能用的思路。文末我会展示代码目录结构和一份模拟数据集的生成说明照着跑就能复现整个系统。1. 需求落地一张大屏背后的数据叙事1.1 原始数据的窘境先说我被Excel折磨的那段时间。原始数据每天从各个上报渠道汇总过来格式非常混乱。字段大致有地区名称、日期、新增确诊、累计确诊、新增治愈、累计治愈、新增死亡、累计死亡。问题在于有的地区用全称有的用简称有的干脆把日期填成了文本格式根本没有办法直接做成图表。如果只是跑一张静态图表我花一晚上整理一版EXCEL就够了但对方要的是能筛日期、能点地图、能看细分的交互式大屏这就必须认真设计一套数据链路。1.2 可视化方案的设计目标我在做设计的时候先定了四个目标展示整体概况、支持时间维度滑动、支持地理维度下钻、所有维度可以联动。也就是说用户选了一个日期地图上的数字变、汇总卡片变、趋势线变整个系统所有模块都围绕同一个筛选条件来响应。这件事说起来简单真正做起来会遇到一连串问题后端API怎么设计前端全局状态怎么管图表实例怎么更新轮询数据怎么合并。我在下文都会一一展开。1.3 为什么我不做截图式大屏另外要说一下现在市面上不少大屏是提前把数据跑好导出成图片或静态网页再贴上去展示。这类方案的问题很明显每次想看过去某个时间点都要重新生成而且完全不能交互地图不能点排行不能筛选。所以我坚持做一个有状态的系统所有图表都监听一份全局筛选条件条件一变所有图表按需向后端请求数据并重绘。这就是我后面要说的多模块联动的核心基础。2. 技术栈拆解Flask MySQL 管数据ECharts 管展示2.1 后端为什么选 Flask而不是 Django 或 FastAPI先聊后端选型。Django自带ORM、Admin后台、认证体系对一个大屏项目来说太重了。本项目后端需要做的事情其实很单纯接收前端请求查数据库返回JSON。Flask的轻量和灵活正好合适所有路由都能自己控制不需要对框架做大量定制。有人会问FastAPI也很香自带数据校验和自动接口文档但在我这个场景下接口数量不多、参数也不算复杂Flask完全够用而且团队里其他同事熟悉Flask接手成本低。2.2 ECharts 为什么占据前端主导位置前端我没有引入React或Vue直接用了原生HTML ECharts。原因很简单ECharts是一个纯图表库地图、折线图、柱状图、饼图写一段配置就能出图。项目需要的地图下钻、tooltip提示框、区域高亮都是ECharts内置能力这帮我省掉了自己实现地图交互的大量工作。尤其是基于GeoJSON的注册模式我在联动的时候可以用registerMap动态注册区域数据再通过dispatchAction做高亮交互体验非常顺滑。2.3 MySQL 的定位聚合分析离不开关系型用MySQL可能有人会觉得数据量大怎么办是不是应该上NoSQL我建议你冷静算一笔账一个地区一天一条记录就算有几百个区县一年累计也不过十来万条。MySQL在这种数据量级下做SUM、COUNT、GROUP BY聚合操作非常快而且SQL写完就能跑不需要额外引入Spark或Flink。我的选型结论可以总结成一句数据量在百万级别以内事务和聚合分析需求为主MySQL就是最稳妥的选择。3. 数据库设计一张维表、一张事实表、一张版本表3.1 三张表的分工逻辑这套设计我借鉴了轻量级维度建模的思路。核心表只有三张area_dim地区维表存地区编码、地区名称、上级编码既便于前端展示名称也方便做下钻。daily_report每日疫情事实表存每个地区每天的各项指标数据。data_version数据版本表记录当前数据源的最新日期和版本号实时展示优化时靠它来判断是否需要增量更新。3.2 建表SQL示例下面是我实际用到的建表语句去掉了敏感字段保留核心结构。CREATE TABLE area_dim ( id INT PRIMARY KEY AUTO_INCREMENT, area_code VARCHAR(32) NOT NULL UNIQUE COMMENT 地区编码, area_name VARCHAR(64) NOT NULL COMMENT 地区名称, parent_code VARCHAR(32) DEFAULT NULL COMMENT 上级地区编码, level INT DEFAULT 2 COMMENT 区域层级1省级 2区县级, center_lng DECIMAL(10,7) DEFAULT NULL, center_lat DECIMAL(10,7) DEFAULT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT地区维表; CREATE TABLE daily_report ( id BIGINT PRIMARY KEY AUTO_INCREMENT, area_code VARCHAR(32) NOT NULL, report_date DATE NOT NULL, confirmed_add INT DEFAULT 0, confirmed_total INT DEFAULT 0, cured_add INT DEFAULT 0, cured_total INT DEFAULT 0, death_add INT DEFAULT 0, death_total INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_area_date (area_code, report_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT每日疫情数据表; CREATE TABLE data_version ( id INT PRIMARY KEY AUTO_INCREMENT, data_date DATE NOT NULL COMMENT 当前数据日期, version_no VARCHAR(32) NOT NULL COMMENT 数据版本号, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT数据版本表;uk_area_date这个联合唯一索引非常关键它保证同一个地区同一天只能有一条记录。后面做增量更新时可以直接用INSERT ... ON DUPLICATE KEY UPDATE来处理重复数据既简单又不容易出错。3.3 模拟数据怎么生成并且能安全分享既然要公开演示就不能用真实数据。我写了一个Python脚本用随机数生成模拟数据把区域抽象成模拟市A区、模拟市B区日期范围设为2023年1月1日到2023年12月31日每天给每个区域生成一组递增的数据。脚本文件放在项目仓库的generate_mock_data.py里。用这种方式生成的数据保留了真实业务字段的所有特征又不涉及任何真实地区、真实事件分享出去没有任何风险。你在实际部署时把脚本替换成自己数据源的导入逻辑就行。3.4 数据库索引与查询优化在daily_report表上除了联合唯一索引还可以考虑加一个idx_report_date专门应对按日期范围过滤的场景。我的实际经验是加上索引之后百万级数据量的汇总查询从1.2秒降到了200毫秒左右。这个收益在项目初期甚至比接口缓存的提升更明显。你可以在确认数据量之后再决定要不要加但不要把索引建得太杂写操作会受影响。4. Flask接口层给前端的不该是数据表而是业务视图4.1 接口设计的三个原则设计后端接口时我给自己定了三个规则接口返回的字段一定是前端直接能用的绝不把原始表结构直接抛出去。尽量按需查询比如地图只需要地区编码、地区名称、数值就不要返回其他冗余字段。所有接口都要接受统一的日期参数方便前端做大屏全局刷新。4.2 核心API与其代码我实现了5个核心接口/api/overview查汇总卡片/api/geo查地图分布/api/trend查时间趋势/api/top查地区排行榜/api/detail查明细列表。下面以趋势接口为例展示Flask的实现方式。app.route(/api/trend) def api_trend(): area_code request.args.get(area_code, ) start_date request.args.get(start_date) end_date request.args.get(end_date) where_sql 11 params [] if area_code: where_sql AND d.area_code %s params.append(area_code) if start_date and end_date: where_sql AND d.report_date BETWEEN %s AND %s params.append(start_date) params.append(end_date) sql f SELECT d.report_date, SUM(d.confirmed_add) AS confirmed_add, SUM(d.confirmed_total) AS confirmed_total, SUM(d.cured_total) AS cured_total, SUM(d.death_total) AS death_total FROM daily_report d WHERE {where_sql} GROUP BY d.report_date ORDER BY d.report_date conn get_connection() try: with conn.cursor(pymysql.cursors.DictCursor) as cur: cur.execute(sql, params) rows cur.fetchall() return jsonify({code: 0, msg: ok, data: rows}) except Exception as e: return jsonify({code: 1, msg: str(e)}) finally: conn.close()注意这里用f-string拼接where条件有一个隐患如果未来加了太多复杂条件字符串很容易拼错。我的习惯是维护一个where_parts数组每个条件独立放进数组最后用 AND .join(where_parts)拼接这样既不会漏空格也不会出现顺序混乱。4.3 联调阶段最容易踩的三个坑第一是乱码问题。返回的中文地名在页面上显示成???原因是MySQL连接字符集没设成utf8mb4需要在连接串里显式指定charsetutf8mb4。第二是跨域问题。前端文件直接打开时请求Flask接口会被浏览器拦截我引入了flask_cors的CORS(app)解决但生产环境最好限定允许的域名不要直接*。第三是时间格式不一致。前端可能传进来的是2023-01-01 00:00:00后端以为这是date类型一比较就报错。统一处理方式是所有接口都只接收YYYY-MM-DD字符串内部转成date或者直接用字符串比较。5. 多模块联动一个全局状态驱动所有图表5.1 联动的本质是状态同步很多人实现联动时会在一个图表的回调里直接修改另一个图表的配置然后重新setOption。这种硬耦合的方式会让代码变得非常脆一旦图表顺序变了回调里的引用就要改半天。我采用的方式是引入一个前端全局状态对象filterState里面存currentDate、selectedAreaCode等筛选条件。图表发生交互时唯一要做的事就是更新这个状态然后通知所有图表订阅者去拉新数据并重绘。这样联动就变成了经典的观察者模式状态变更 - 广播通知 - 各图表自行更新。5.2 地图点击联动趋势图和排行榜地图模块通过ECharts的map类型实例加载。用户点击某个区域时ECharts触发click事件事件参数里有name属性。我在点击回调里根据name反查出对应的areaCode写入filterState.selectedAreaCode然后调用refreshAllCharts()。趋势图会去请求该区域的历史趋势排行榜会展示该区域内各区县排名。反过来当用户点击排行榜的某一行时我通过dispatchAction高亮地图上的对应区域这样地图和图表就形成了双向联动。这里有两点很关键一是必须在地图加载完成后再绑定事件否则事件不会被触发二是如果使用动态加载的GeoJSONclick事件里的name必须和GeoJSON里每个区域的name属性一致否则反查不到数据。5.3 用代码说话一个很轻量的事件总线前端没有引入复杂框架所以我写了一个小型的发布订阅模块。const filterState { currentDate: 2023-12-31, selectedAreaCode: null, }; const chartEventBus { listeners: {}, on(eventName, cb) { if (!this.listeners[eventName]) { this.listeners[eventName] []; } this.listeners[eventName].push(cb); }, emit(eventName) { (this.listeners[eventName] || []).forEach((cb) cb()); }, }; function updateFilter(patch) { Object.assign(filterState, patch); chartEventBus.emit(filterChange); } // 以趋势图为例 chartEventBus.on(filterChange, () { fetchTrend().then((data) trendChart.setOption(data)); });这样以后新增一个图表模块完全不用修改地图事件代码只需要注册一个filterChange监听器扩展性会好很多。5.4 多图表的性能隐患重绘风暴虽然联动逻辑清晰了但这里有一个潜在的性能问题每次筛选条件变化所有图表同时请求后端同时setOption。如果筛选操作非常频繁比如拖着日期滑块连续滑动页面会出现明显的卡顿和闪烁。这个问题的解法我放在第6章详细讲。这里先记住一个结论并不是每次交互都需要立刻触发全量刷新适当的节流和延迟合并往往能让体验更流畅。6. 实时展示优化的三个关键技术点6.1 轮询定时器简单但别滥用很多大屏能做到实时跳动本质上就是一个定时器每隔几秒请求一次数据。我一开始也是这么干的设置setInterval每5秒请求一次所有接口。但很快就发现在接口数量多的情况下一次轮询会同时发起5-6个请求数据库连接池容易被打满前端也会出现明显的闪烁。所以第一个优化建议是把轮询间隔放到一个独立模块里管理并且只在真正需要实时性的时候才开启。比如演示模式下用10秒轮询常规查看模式下用30秒轮询。这个参数我写在配置文件中现场演示前可以直接调整。6.2 增量更新不是每次都拉全量数据实时展示还有一个更高效的层级增量更新。具体做法是前端记录当前已经渲染的数据日期范围轮询的时候不请求全量历史只请求最近N天的增量数据然后用前端方法把新旧数据合并。比如大屏初始加载时请求2023年1月到12月的全量数据之后每次请求只查最近7天或30天。这个方案对网络带宽和后端压力都很友好。为了判断后端是否有新数据我用了之前设计的data_version表。前端每次轮询先调用一个极轻量的接口/api/version返回当前数据最新日期和版本号如果和前端记录的不同才去拉增量数据并更新图表。如果版本号没变图表压根不用动。async function pollLoop() { const version await fetch(/api/version).then(r r.json()); if (version.data_date ! filterState.currentDataDate) { const delta await fetchTrend(filterState.selectedAreaCode); mergeAndUpdate(delta); } } setInterval(pollLoop, 15000);6.3 前端渲染优化图表实例的复用与局部更新ECharts的setOption本身默认会做增量更新这一点一定要用起来。不要在每次更新时销毁图表实例再新建一个那样会导致白屏和性能浪费。正确做法是维护一个chartInstanceMap每个DOM容器只对应一个永久实例每次数据更新后调用setOption传入新数据即可。另外在高亮和动画这类交互上优先使用dispatchAction而不是重复setOption开销会更小。我在本地实测中一次联动更新从最初的350ms降到了120ms左右体验提升非常明显。const chartInstanceMap new Map(); function getChart(domId) { if (!chartInstanceMap.has(domId)) { chartInstanceMap.set(domId, echarts.init(document.getElementById(domId))); } return chartInstanceMap.get(domId); }6.4 给接口加上缓存后端性能的大步提升前端优化做完后端也可以做一部分事情。我使用了flask_caching给接口加了一层简单的内存缓存。地图分布接口每天变化不大所以我给/api/geo加了30秒的TTL缓存。这样即使前端频繁刷新后端也不会每次都跑到MySQL里执行聚合查询。实测在并发10个请求的情况下带缓存的接口响应时间稳定在20ms以内而聚合查询则需要80ms左右。需要注意的是带日期参数的接口不能随便缓存我把缓存key设计成接口名_日期_区域确保不同参数之间不互相污染。7. 踩坑记录项目从能跑到能演示的四个深坑7.1 地图区域名与数据库地区编码不一致地图点击时拿到的区域名是模拟市A区但数据库里存的是areaCode。最初我直接用区域名去查MySQL结果自然是查不到。解决方案是在area_dim表里增加一个geo_name字段专门保存地图里用的名称再在接口返回时同时给出areaCode和areaName。地图事件回调里根据params.name反查areaCode然后更新全局状态。如果图省事也可以在前端做一个name-to-code映射表但那样维护成本更高我最后还是放在数据库维表里更干净、更易维护。7.2 Flask开发模式下轮询接口偶尔报错我用Flask自带开发服务器时开了debugTrue结果发现轮询接口偶尔会报ConnectionResetError。排查了很久一度以为是MySQL连接池的问题。后来才意识到开发服务器在代码变更时会重新加载部分请求的线程会被打断这是开发环境自身的问题。生产环境我部署的是gunicorn加多worker这个报错再也没有出现过。如果你只是本地跑Demo可以不用管但别把开发模式的表现当成生产环境的性能基准。7.3 数据库连接池大小怎么调很多人会问连接池设多大合适。我一开始设了20本地开发没问题。但部署到低配服务器后如果同时跑前端构建和后端服务连接数很容易被打满。调优建议是先用10个起步再根据并发量逐渐调到30。你可以用SHOW VARIABLES LIKE max_connections查看MySQL上限不要盲目设一个很大的值否则数据库反而会被拖垮。7.4 日期钻取后地图数据不刷新大屏上的时间轴我希望做成可点击钻取。最初做的时候点击日期后只是简单修改了filterState.currentDate结果发现地图数据没刷新。后来排查发现有些图表回调在初始化时只执行了一次并没有注册到事件总线上。我在每个图表初始化完后强制调用了一次chartEventBus.emit(filterChange)来确保初始状态一致并且设置全局筛选后统一由事件总线驱动所有图表刷新而不是在个别回调里手动再拉一次数据。这样才真正解决了时间轴动地图不动的诡异问题。8. 代码仓库结构与附代码数据使用说明8.1 目录结构一览这是一个典型的前后端分离项目前端不需要Node构建直接用浏览器打开即可。目录结构很清晰epidemic-visualization/ ├── backend/ │ ├── app.py # Flask 主入口 │ ├── db_connector.py # MySQL 连接池封装 │ ├── config.py # 数据库配置 │ └── sql/ │ └── init.sql # 建表语句 ├── frontend/ │ ├── index.html # 大屏页面 │ ├── css/ │ │ └── style.css │ └── js/ │ ├── charts.js # 各图表封装 │ ├── eventBus.js # 事件总线 │ └── main.js # 初始化入口 ├── data/ │ └── generate_mock_data.py # 模拟数据生成脚本 └── README.md8.2 如何把项目跑起来第一步准备MySQL执行backend/sql/init.sql初始化表。第二步运行generate_mock_data.py生成模拟数据脚本会插入2023年一整年的随机数据。第三步修改config.py里的数据库账号、密码和host。第四步进入backend目录执行python app.py启动后端默认端口5000。第五步用浏览器打开frontend/index.html。如果前后端不在同一域名浏览器会拦截跨域请求。最省事的方法是通过Flask的静态文件托管来访问前端页面或者在本地安装flask-cors解决。我在开发时是直接在前端用相对路径请求后端接口配合CORS(app)省了很多配置麻烦。8.3 模拟数据集说明与后续扩展方向标题里提到的附代码数据指的就是我这份模拟数据集以及生成脚本。你可以用脚本随时生成任意日期范围的数据这在测试阶段非常方便。真正上线时把生成逻辑替换成你真实数据源的导入逻辑即可。如果想继续往下做我建议优先考虑三件事引入APScheduler定时任务每天自动从数据源拉取最新数据并更新数据库。加入用户权限控制不同角色只能看不同层级的数据。用nginx托管前端静态文件并反向代理Flask接口做成正式部署形态。如果你还想做秒级推送可以考虑在Flask后面接入异步任务和WebSocket通道但目前这个系统应对日常展示场景已经足够了。做这个项目我最大的体会是一个可视化系统的成败往往不取决于图表好不好看而取决于数据逻辑清不清晰、联动响不响应。当你把数据库字段设计干净、后端接口返回简洁、前端状态管理统一之后所谓实时大屏自然水到渠成。最后再分享一个小技巧如果你也要做类似大屏先把模拟数据和表结构准备好再去写代码。数据模型是地基地基稳了后面的联动和优化都是在盖楼。希望这篇记录能帮你少踩几个坑。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询