news 2026/10/22 3:54:01

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 接口设计的三个原则

设计后端接口时,我给自己定了三个规则:

  1. 接口返回的字段一定是前端直接能用的,绝不把原始表结构直接抛出去。
  2. 尽量按需查询,比如地图只需要地区编码、地区名称、数值,就不要返回其他冗余字段。
  3. 所有接口都要接受统一的日期参数,方便前端做大屏全局刷新。

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 = "1=1" 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,需要在连接串里显式指定charset='utf8mb4'。第二是跨域问题。前端文件直接打开时请求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高亮地图上的对应区域,这样地图和图表就形成了双向联动。

这里有两点很关键:一是必须在地图加载完成后再绑定事件,否则事件不会被触发;二是如果使用动态加载的GeoJSON,click事件里的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自带开发服务器时开了debug=True,结果发现轮询接口偶尔会报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.md

8.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通道,但目前这个系统应对日常展示场景已经足够了。

做这个项目,我最大的体会是:一个可视化系统的成败,往往不取决于图表好不好看,而取决于数据逻辑清不清晰、联动响不响应。当你把数据库字段设计干净、后端接口返回简洁、前端状态管理统一之后,所谓"实时大屏"自然水到渠成。最后再分享一个小技巧:如果你也要做类似大屏,先把模拟数据和表结构准备好,再去写代码。数据模型是地基,地基稳了,后面的联动和优化都是在盖楼。希望这篇记录能帮你少踩几个坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 3:53:57

Vibe Coding高效协作:给AI写一份专属工作手册

前阵子不是写了Vibe Coding学习笔记系列嘛,前面几篇讲的是怎么和AI结对编程、怎么把大需求拆成小块、怎么用自然语言把逻辑边界说清楚。到了第六篇,我想聊聊一个特别容易被忽略但极其影响产出质量的东西——给AI准备一份"工作手册"。说白了&am…

作者头像 李华
网站建设 2026/10/22 3:53:30

从Copilot到Vibe Coding:AI编程协作模式的进化与工程实践

1. 从“自动补全”到“替我写完整块”:Copilot 教会我的事大概是从 Copilot 这类工具刚火起来那阵子,我就开始在日常开发里重度依赖它了。坦白讲,最开始的体验确实惊艳:写一个函数的时候,注释还没敲完,代码…

作者头像 李华
网站建设 2026/10/22 3:53:03

SpringBoot智能停车计费系统:计费引擎与并发控制实战

智能停车计费系统这个题目,看起来是典型的SpringBoot课设标配,但真正动手做过就会明白:系统的难点根本不在增删改查,而在计费规则的灵活性和出入场流程的边界处理。我前阵子刚好完整做了一套这样的系统,从入场记录、灵…

作者头像 李华
网站建设 2026/10/22 3:53:00

提示工程评估框架:从凭感觉到数据驱动的prompt优化指南

提示工程这两年炒得火热,但一个很要命的问题是:大量顶着“提示工程师”头衔的人,日常做的最多的事,是改两个词、跑一下、看一眼、凭感觉说一句“比刚才好了”。如果你也处于这种状态,这篇文章请你一定看完。我自己的经…

作者头像 李华
网站建设 2026/10/22 3:52:46

千亿开源模型与国产大模型竞争白热化:选型部署避坑指南

今天AI圈的热搜里,一眼扫过去就是两类消息:一边是某海外巨头把千亿参数的开源模型甩了出来,另一边是国产模型在好几个榜单上跟头部闭源模型咬得越来越紧。哪怕你没点进详情,光看这个标题都能闻到一股熟悉的味道——开源和闭源、国…

作者头像 李华
网站建设 2026/10/22 3:50:56

OpenClaw本地AI助理:7分钟集成一个Skill保姆级指南(含踩坑实录)

最近我把 OpenClaw(社区里一直有人叫它 Clawdbot)重新捡起来折腾了一阵,起因很简单:想让它帮我把日常杂活自动化,但又不想事事都走云端。这个项目本质上是一套本地优先的 AI 个人助理框架,最大卖点是 Skill…

作者头像 李华