疫情数据可视化Java Web项目:从表设计到ECharts大屏完整实践

发布时间:2026/9/23 23:54:37
疫情数据可视化Java Web项目:从表设计到ECharts大屏完整实践
简介这是一份基于Java与ECharts的疫情数据可视化分析系统完整源码面向Java开发者、数据分析学习者及毕业设计选题人群可用于快速搭建从数据采集、存储到图表展示的整套流程。压缩包共106个文件约10MB主要包含28个Java源文件、13个JavaScript脚本、13个XML配置、10个CSS样式、10个Map文件、SQL脚本及项目说明文档等覆盖Spring Boot后端、MyBatis持久层、MySQL数据存储与前端ECharts图表交互等关键模块。项目支持疫情数据自动抓取与动态更新前端通过折线图、柱状图、饼图等方式直观呈现累计确诊、治愈、死亡等统计数据。已有1275人学习下载。借助源码可掌握爬虫编写、接口设计、持久化映射、可视化组件组合等实用技能同时配有项目目录结构与配置文件便于二次开发和毕业设计扩展。1. 打开这个压缩包之前先搞清楚它到底解决了什么问题把压缩包解压出来你会看到一整个 Java Web 工程src 目录下躺着几十个 Java 文件webapp 里还有几个 html 和 js第一次打开的人经常不知道从哪读起。这套系统的定位不是花架子 demo而是一条完整的数据链路——从 MySQL 里的疫情统计表到后端接口输出 JSON再到前端 ECharts 渲染成折线图、柱状图和中国地图。它把 Java 基础、MyBatis 封装、前端可视化三块内容串在了一起这正是它作为课程设计、毕业设计或者 Java 入门练手项目最常见的价值点。我建议你拿到源码先别急着点运行也别一头扎进 Controller 里看逻辑。先打开数据库脚本建表再把 Mapper 里的 SQL 读一遍理解每个图表对应哪条查询最后才看页面是怎么用 ECharts 把 JSON 变成图的。这套阅读顺序能帮你省下一大半调试时间也顺便躲开那种跑起来全是红字的尴尬开场。适合人群很明确想学 Java 后端接口怎么写的人想搞懂前后端数据怎么对接的人以及需要一套拿来改改就能答辩的疫情可视化项目的人。2. 先把数据模型立住疫情统计表怎么设计才够用2.1 为什么疫情统计用宽表而不是一堆关联表很多新手拿到课题第一反应是建省份表、城市表、每日数据表三张表再用外键关联觉得这才叫正规。但疫情数据分析系统的查询模式非常固定按日期聚合、按省份聚合、按城市下钻几乎没有多表 JOIN 的复杂业务。这种情况下单张宽表反而最好用——查询少一次 JOIN就少一份索引设计和性能调优的成本也少一个连表查出来数据重复的翻车点。我一般会把所有维度直接拍平到一张表里日期、省份、城市放在同一行新增确诊、累计确诊、治愈、死亡、疑似这些指标都做成独立字段。城市字段允许为空空值就表示这一行是省级汇总数据。这样一条 SQL 就能同时支撑趋势图按日期分组和分布图按省份分组不需要在多个表之间来回倒腾。CREATE TABLE epidemic_daily ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, stat_date DATE NOT NULL COMMENT 统计日期, province VARCHAR(32) NOT NULL COMMENT 省份名称, city VARCHAR(32) DEFAULT NULL COMMENT 城市名称空值表示省级汇总, confirm_add INT NOT NULL DEFAULT 0 COMMENT 新增确诊数, confirm_total INT NOT NULL DEFAULT 0 COMMENT 累计确诊数, cure_total INT NOT NULL DEFAULT 0 COMMENT 累计治愈数, dead_total INT NOT NULL DEFAULT 0 COMMENT 累计死亡数, suspect_total INT NOT NULL DEFAULT 0 COMMENT 现有疑似数, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, UNIQUE KEY uk_date_province_city (stat_date, province, city) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT疫情每日统计表;这段 DDL 有几个地方值得说。联合唯一键uk_date_province_city保证了同一日期、同一省份、同一城市不会重复插入这是做数据清洗时最重要的约束city字段允许 NULL因为省级汇总行的粒度本来就比市级数据粗NULL 比空字符串更语义化。confirm_add和confirm_total分开存是因为折线图通常要同时展示每日新增和累计两条曲线存成两个字段省去计算。引擎固定 InnoDB事务和行级锁在批量更新数据时更稳。2.2 MyBatis Mapper 里最常见的两个查询写法表建好之后对应的 MyBatis Mapper 是整个后端的核心因为所有图表的数据来源都在这里。趋势图要的是每天全国汇总的新增和累计值用一条分组查询搞定省份对比图要的是指定日期下各省份的累计值加一个排序和条数限制。两条 SQL 的过滤条件、分组字段、排序方向都不同分开写比硬塞进一个万能接口清晰得多。select idselectDailyTrend resultTypejava.util.Map SELECT stat_date, SUM(confirm_add) AS confirm_add, SUM(confirm_total) AS confirm_total FROM epidemic_daily WHERE stat_date BETWEEN #{startDate} AND #{endDate} GROUP BY stat_date ORDER BY stat_date /select select idselectProvinceRank resultTypejava.util.Map SELECT province, SUM(confirm_total) AS confirm_total FROM epidemic_daily WHERE stat_date #{statDate} AND city IS NULL GROUP BY province ORDER BY confirm_total DESC LIMIT #{limit} /select第一条查询里BETWEEN是闭区间startDate 和 endDate 都包含在内所以前端传参时要注意别多传一天。SUM(confirm_add)在省级汇总行上做求和得到的就是全国当天新增总量这个逻辑成立的前提是数据里同时有省市两级行——如果只用市级数据SUM 之前必须加WHERE city IS NOT NULL。第二条查询用city IS NULL过滤掉市级重复计数否则省级排名会把下属城市的累计值全部重复算进去排名直接翻车。两张查询都用resultTypejava.util.Map省去写 VO 类ECharts 本来就是按 key 取值的Map 足够。2.3 数据从哪来三种来源和各自的适用范围我一直觉得可视化系统的数据来源是最容易被忽略、却最影响项目完整度的环节。疫情数据不是凭空生成的你需要三种方案里选一种人工整理的真实公开数据、模拟数据生成器、爬虫抓取。人工数据适合答辩演示量小、可控、不依赖网络模拟数据适合开发调试能造出任意时间范围和数值趋势爬虫适合想做成完整产品的但涉及数据源稳定性和合规问题学习用途也要注意尺度。我推荐最稳妥的组合开发阶段用模拟数据生成器最终演示时灌入一版整理好的真实数据。public void generateMockData(int days, int provinceCount) { LocalDate start LocalDate.of(2024, 1, 1); Random random new Random(); for (int i 0; i days; i) { LocalDate date start.plusDays(i); for (int p 0; p provinceCount; p) { int base random.nextInt(500); epidemicDailyMapper.insert(new EpidemicDaily(date, 省份 (p 1), null, base, base * 10, base * 8, base / 20, base / 5)); } } }这段生成器每行给一个省份的省级汇总数据base作为新增确诊的基准数累计确诊、治愈、死亡都从它推算这样生成出来的趋势数据不会忽高忽低图表看起来更接近真实走势。注意这里只生成了省级数据所以前面 Mapper 里的省份排名查询可以直接用如果你想做城市下钻图需要再套一层循环把每个省份的多个城市也插进去。批量插入时用 MyBatis 的foreach标签几百条数据一次性提交别一条条 insert否则运行一次要等很久。3. 后端接口把数据整理成 ECharts 要的 JSON 形状3.1 接口设计按图表维度拆不按页面拆很多人第一步就错在接口设计上——按页面拆接口一个页面一个接口返回一大坨 JSON。结果折线图要改数据范围柱状图要改排序条数只能动接口。正确做法是按图表维度拆一个图表对应一个接口查询条件通过参数透传。常见做法是四个接口/api/trend做日期趋势折线图/api/provinceRank做省份 Top 柱状图/api/mapData做中国地图分布/api/summary做顶部汇总数字卡片。这套设计不管是大屏还是普通页面都能复用改样式不动接口。我给一张接口参数表照着写 controller 基本不会漏参数接口路径请求参数返回内容/api/trendstartDate, endDate每日新增与累计的日期序列/api/provinceRankstatDate, limit指定日期省份累计 TopN/api/mapDatastatDate各省份累计确诊键值数组/api/summarystatDate全国累计、当日新增、治愈、死亡统一返回结构也很关键。不管成功还是失败都包一层{code: 200, message: success, data: ...}前端只要判断 code 就能统一处理错误不用每个接口单独写 try-catch。这个结构看起来多此一举但当前端图表多起来、异常情况多了之后你就知道统一包装的好处了——至少不会出现接口报错时前端拿到一个 undefined 却找不到原因的黑匣子。3.2 日期序列补齐折线图 X 轴为什么总是缺几天GROUP BY stat_date查出来的结果天然缺数据——哪一天没有录入记录那一天的日期就不会出现在结果集里。直接拿这个结果喂给 ECharts折线图 X 轴就会从 1 号直接跳到 3 号中间缺一天。很多课程设计项目就栽在这一步看起来是图表数据不对其实是后端返回的日期序列本身不连续。解决思路很简单后端先按日期范围生成一个完整的日期列表再用查询结果去填充数据库里没有的那天补 0。public ListMapString, Object buildTrend(String startDate, String endDate) { ListMapString, Object result new ArrayList(); MapString, MapString, Object dataMap new HashMap(); mapper.selectDailyTrend(startDate, endDate).forEach(row - { dataMap.put(String.valueOf(row.get(stat_date)), row); }); LocalDate start LocalDate.parse(startDate); LocalDate end LocalDate.parse(endDate); while (!start.isAfter(end)) { String dateStr start.toString(); MapString, Object row dataMap.get(dateStr); MapString, Object item new LinkedHashMap(); item.put(date, dateStr); item.put(confirmAdd, row null ? 0 : row.get(confirm_add)); item.put(confirmTotal, row null ? 0 : row.get(confirm_total)); result.add(item); start start.plusDays(1); } return result; }这段代码的关键在LinkedHashMap——它保证插入顺序所以 result 里的日期一定是升序排列的。row null时补 0是折线图连续性的核心宁可补 0 也不让 X 轴少刻度。另外注意我用了start.toString()把 LocalDate 直接转成yyyy-MM-dd字符串MySQL 的 DATE 类型和字符串比较时需要格式完全一致这一点埋了不少坑后面避坑章会展开。补齐的逻辑放在 Java 层而不放在 SQL 层是因为 SQL 里做日期序列补齐要写递归 CTE 或者造数字表代码可读性差Java 层十几行就搞定。3.3 返回结构统一一个 Result 类省掉一半前端判断接口写多了你会发现每个方法都手动去 new HashMap 拼结构很容易乱而且拼出来的 key 大小写不统一前端取数据时还得反复翻浏览器 Network 面板看真实返回格式。我的习惯是写一个泛型 Result 类把成功、失败、异常三种情况全部封装好Controller 里两行代码就能返回一个结构稳定的 JSON。Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } public static T ResultT error(Integer code, String message) { ResultT r new Result(); r.code code; r.message message; return r; } }这个类的重点是把data做成了泛型趋势接口传ListMap省份排名接口传ListMap汇总接口传一个Map都可以用同一个类承载。前端判断逻辑也简单了if (res.code 200)就渲染图表否则弹错误提示。不要小看这十几行代码它避免了每个接口各自为政返回不同结构也让后面的异常统一处理变成了可能——全局异常处理器捕获到SQLException、NullPointerException时直接Result.error(500, e.getMessage())前端至少能看到结构化错误信息不用对着 Network 里的 500 页发愁。4. 前端拼大屏折线图、柱状图、中国地图的接入写法4.1 折线图两条曲线共用 X 轴的数据组织方式后端数据就绪之后前端的工作就是把 JSON 里的数组映射到 ECharts 的 option 上。这里最常见的错误是把两条曲线的 data 分别写死X 轴却只取了其中一条曲线的日期一旦两条曲线数据范围不一致图表就会错位。正确做法是画一个完整的日期数组作为xAxis.data两条曲线的 data 都从这个数组派生出来用map方法把每一项的指标值取出来。下面这段代码是一个可以直接跑通的最小实现。fetch(/api/trend?startDate2024-01-01endDate2024-01-31) .then(res res.json()) .then(res { if (res.code ! 200) throw new Error(res.message); const dates res.data.map(item item.date); const confirmAdd res.data.map(item item.confirmAdd); const confirmTotal res.data.map(item item.confirmTotal); myChart.setOption({ tooltip: { trigger: axis }, legend: { data: [当日新增, 累计确诊] }, grid: { left: 60, right: 40, top: 50, bottom: 40 }, xAxis: { type: category, data: dates }, yAxis: [ { type: value, name: 新增人数 }, { type: value, name: 累计人数 } ], series: [ { name: 当日新增, type: line, smooth: true, data: confirmAdd, areaStyle: { color: #ffd666 } }, { name: 累计确诊, type: line, smooth: true, yAxisIndex: 1, data: confirmTotal, areaStyle: { opacity: 0.15, color: #1890ff } } ] }); });这段代码里dates数组同时喂给xAxis.data和两条 series 的 data从根本上避免了 X 轴和曲线错位的问题。yAxisIndex: 1让新增和累计各用一条 Y 轴因为两个指标的数据量级差很大叠在同一个 Y 轴上累计曲线会变成一条直线。areaStyle给折线加了渐变区域填充视觉上更接近大屏风格——如果你想用echarts areaStyle 渐变色那种效果直接把color写成new echarts.graphic.LinearGradient(0, 0, 0, 1, [{offset: 0, color: #1890ff}, {offset: 1, color: #e6f7ff}])就行。注意图表容器必须要有确定的height否则init之后 setOption 什么都看不到。4.2 柱状图与饼图省份 Top 10 和占比分布省份排名柱状图的数据结构比折线图简单核心是把后端返回的[{province, confirmTotal}]数组拆成两个平行数组。这里有一个值得养成习惯的写法用Array.prototype.sort在后端已经排好序的情况下再兜底排一次防止接口改造导致顺序错乱。柱状图的柱子颜色不用默认的纯色给每根柱子设置一个渐变视觉上会专业很多这也是热词里echarts 柱状图设置渐变色的常规用法。fetch(/api/provinceRank?statDate2024-01-31limit10) .then(res res.json()) .then(res { const sorted [...res.data].sort((a, b) b.confirmTotal - a.confirmTotal); myBarChart.setOption({ xAxis: { type: category, data: sorted.map(i i.province) }, yAxis: { type: value, name: 累计确诊 }, series: [{ type: bar, data: sorted.map((item, index) ({ value: item.confirmTotal, itemStyle: { borderRadius: [4, 4, 0, 0], color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: #1677ff }, { offset: 1, color: #69b1ff } ]) } })) }] }); })data数组里每一项直接塞对象而不是只塞数值是为了给每根柱子单独配置样式这就是 ECharts 里 data 对象化写法的好处。饼图更简单直接把[{name: 省份, value: 累计}]数组塞给series.data就完事注意饼图的name字段必须唯一否则图例会合并。如果你想要echarts 词云图 示例那种视觉效果换个思路把type: bar改成type: wordCloud再引入对应的扩展脚本但疫情数据用词云语义不太对口还是柱状图更直观。4.3 中国地图地图 JSON 加载与注册的坑中国地图是这种疫情分析系统最有冲击力的一张图也是翻车率最高的一张图。ECharts 从 5.0 开始不再内置中国地图数据必须自己准备china.json并通过echarts.registerMap注册。很多源码包里直接放一个china.js引入就算了这种方式能跑但不是最优解——china.js是脚本文件体积大还影响页面加载速度。推荐把 JSON 文件放到静态目录用 fetch 异步加载。fetch(/maps/china.json) .then(res res.json()) .then(geoJson { echarts.registerMap(china, geoJson); fetch(/api/mapData?statDate2024-01-31) .then(res res.json()) .then(res { mapChart.setOption({ tooltip: { trigger: item, formatter: {b}: {c} 例 }, visualMap: { min: 0, max: 1000, left: left, top: bottom, text: [高, 低], inRange: { color: [#e8f4ff, #1677ff] } }, series: [{ type: map, map: china, roam: true, label: { show: false }, data: res.data }] }); }); });这段代码的执行顺序很讲究必须先拿到地图 JSON 并注册才能 setOption如果先 setOption 再用地图数据ECharts 会因为找不到china这一地图类型而报错。visualMap是地图配色的核心——它的min和max要根据数据的实际范围来设如果数据是几百到几万max: 1000会让大部分省份都是深色看不出梯度。roam: true开启缩放拖拽大屏演示时很有用。地图数据里的name字段必须和 china.json 里的省份名称完全一致比如内蒙古自治区不能简写成内蒙古否则那一块区域就永远不上色。这一步是玄学一样的问题实际上就是名称不匹配。5. 避坑与排查跑通这个源码项目一定会踩的 5 个坑5.1 时间字段查不出数据JDBC 时区与日期格式不一致现象接口返回的 data 是空数组但数据库里明明有数据用 Navicat 跑同样的 SQL 也能查出来。 原因两个问题叠加。第一JDBC 连接串里没有配置serverTimezoneMySQL 8.0 默认时区和 JVM 时区不一致DATE类型解析错位第二前端传的日期格式是yyyy/MM/dd或者带时分秒和后端LocalDate.parse期望的yyyy-MM-dd不匹配。 解决连接串里显式加上?serverTimezoneAsia/ShanghaiuseSSLfalse后端接收参数时统一用DateTimeFormat(pattern yyyy-MM-dd)注解不让前端猜格式。顺手把 Mapper 里的日期比较写成stat_date #{startDate} AND stat_date #{endDate}替代BETWEEN排查边界问题更直观。5.2 中国地图区域空白不显示现象页面加载正常图例和 visualMap 都在但中国地图区域白茫茫一片控制台也没有报错。 原因最常见的两个——地图 JSON 没注册成功就 setOption以及 series.data 里的省份名称和地图里的名称对不上。前者 addEventListener 看 fetch 是否返回 404后者逐个对比名称。 解决把注册和 setOption 串成 Promise 链确保registerMap执行完成之后再请求数据。名称问题上我给数据接口里加一层清洗映射把内蒙古转成内蒙古自治区广西转成广西壮族自治区。这类名称规范化在疫情数据源里极其常见好不容易爬下来的数据就栽在这一步上。5.3 大屏加载慢接口要好几秒现象刷新页面后图表依次空白过几秒才陆续渲染出来接口响应时间在 Network 里能看到 2000ms 以上。 原因大部分是 SQL 没走索引。GROUP BY stat_date在数据量大时全表扫描WHERE stat_date #{statDate}也没命中索引。如果代码里还有循环查库——比如先查省份列表再逐个查每个省份的详情——那接口慢是必然的。 解决给epidemic_daily加联合索引(stat_date, province, city)让日期过滤和省份分组都能走索引。循环查库的地方改成一次查出全部数据在 Java 内存里用 Map 分组。这个优化做完接口从几秒降到几十毫秒很正常属于性价比最高的一个优化动作。5.4 前端拿到 JSON 但图表就是渲染不出来现象Network 里能看到完整的 JSON 数据控制台也没有 JS 报错但图表区域空白。检查发现容器的高度是 0。 原因初始化 ECharts 时容器还在页面布局阶段宽高没计算出来echarts.init拿到一个高度为 0 的 DOM图表自然画不出来。还有一种情况是容器设置了百分比高度但父级没有明确高度。 解决给图表容器写死一个高度比如height: 400px不要在 CSS 里用height: 100%指望父级传下来。大屏项目如果用的是 flex 布局等window.onload之后再 init或者在requestAnimationFrame里延迟初始化。这是 ECharts 新手遇到最多的隐形坑看起来像玄学其实根因就这一条。5.5 隔夜后页面刷新报数据库连接异常现象项目刚启动时一切正常过一晚第二天刷新大屏后端报CommunicationsException或Connection is not available重启 Tomcat 又恢复正常。 原因MySQL 的wait_timeout默认 8 小时超过这个时间空闲连接会被服务端关闭而连接池里的连接不知道还在复用这些死连接。 解决在 Druid 或 HikariCP 连接池配置里加上连接检测参数。以 Druid 为例testWhileIdletrue、validationQuerySELECT 1、timeBetweenEvictionRunsMillis60000。HikariCP 是 Spring Boot 默认连接池配connectionTimeout30000、maximumPoolSize10再给 pool 加keepalive-time60000问题就能绕过去。这是 Java 后端项目部署后最常见的隔夜翻车案例属于部署期必修课。6. 从能跑到能用三个值得动手的扩展方向6.1 日期范围联动给图表加一个筛选控制条目前的接口已经支持startDate和endDate参数前端只差一个联动控件。常见做法是在页面顶部放两个input typedate监听change事件后重新请求趋势图和省份排名接口。这样一个简单的日期联动就能让系统从展示固定数据变成可按需分析使用体验完全不一样。注意每次重新请求后要调用myChart.setOption(option, true)第二个参数true表示完全覆盖旧配置否则上一次展开的 tooltip 或缩放状态会残留图表会出现诡异的叠加。6.2 导出图表图片大屏页面最实用的隐藏功能ECharts 自带getDataURL方法可以把画布内容导出成 base64 图片再配合一个a标签的download属性实现一键下载。这段逻辑不需要任何额外依赖十几行代码就能搞定挂在折线图右上角作为一个小按钮汇报演示时可以直接把图贴进文档里。如果你的项目还需要导出数据表格再引入 EasyExcel 做ListMap到 Excel 的转换注意别一次导出全量数据加个日期范围参数防内存溢出。6.3 自动刷新让大屏真正动起来大屏场景下数据要不断更新最常见做法是setInterval定时请求接口。这里有个衡量点每次调用setOption时不要传true让 ECharts 做增量更新这样图表只在数据变化的部分做动画过渡视觉上不会整个图表闪一下。窗口失焦停掉定时器、重新聚焦再恢复避免页面在后台挂着时白白消耗带宽。这个细节看起来小但做数据可视化大屏项目时用户的评价往往就体现在这些过渡处理上。我自己的习惯是把这些扩展点都独立封装成函数比如loadTrend()、loadMap()在页面初始化时先调用一次之后每个定时器只管调用对应函数。这样做的好处是代码结构清晰而且后期做日期联动、自动刷新时不会把逻辑改成一团乱麻。希望这几个方向能帮你的项目从运行起来走到真正能演示、能讲解尤其是答辩或者作品展示时这些细节往往是加分最多的部分希望帮到你。本文还有配套的精品资源点击获取