资讯动态

Java基于ECharts的疫情数据可视化系统:CSV清洗到图表联动

发布时间:2026/10/5 2:58:17 来源:尧图企业网站定制
简介一套基于Java技术栈的疫情数据可视化分析系统完整源码面向需要学习Spring Boot、MyBatis、MySQL与ECharts整合开发的中高级开发者也适合作为毕业设计或课程项目的参考实现。项目内置爬虫程序自动抓取COVID-19累计确诊、治愈、死亡等数据并通过ECharts折线图、柱状图、饼图等方式动态展示帮助理解疫情趋势和统计数据。压缩包共106个文件以28个Java源码、13个JavaScript、13个XML、10个CSS等为主涵盖后端业务逻辑、前端图表配置、数据库脚本及项目配置文件整体约10MB目录结构清晰便于按模块检索。已有1275人学习下载。通过源码可掌握从数据采集、存储到可视化展示的完整链路尤其能深入理解Spring Boot自动配置、MyBatis动态SQL与MySQL表设计以及ECharts图表交互和数据绑定的具体写法适合直接复用或二次扩展。1. Java基于ECharts的数据可视化疫情分析系统源码先想清楚它到底解决什么Java基于ECharts的数据可视化疫情分析系统源码这类项目在高校毕设和 Java 面试题里出现频率很高但很多人卡在同一个地方后端返回的数据结构跟图表的 series 对不上。真正的难点不是画图而是把 CSV 里的疫情原始数据清洗成折线图、柱状图、中国地图各自要的格式。这套系统的价值集中在四件事数据落地、聚合统计、接口输出、图表联动。适合两类人刚学完 Spring Boot 和 ECharts 基础、需要一个完整闭环练手的新手以及准备面试、想拿数据可视化项目当项目亮点的 Java 开发。读完你能自己搭出一个可复现的大屏并且知道哪几步最容易翻车。2. 选型与整体架构Java 后端 ECharts 前端数据链路怎么搭2.1 疫情数据从哪来静态 CSV 与第三方接口两条路疫情分析系统第一步不是写接口而是把数据拿到手。常见做法是两条路一是直接使用开源数据仓库维护的疫情 CSV 文件字段通常包含国家、省份、日期、累计确诊、累计治愈、累计死亡二是调用第三方汇总接口这种接口早期很多但后来接口地址频繁变动、字段命名不统一还容易出现跨域和限流拿来当课堂作业可以放到正式项目里非常不可靠。所以我看过的多数源码包采用的是第一条路把一份 CSV 放在 src/main/resources/data 目录下启动时一次加载进内存。CSV 的字段设计一般长这样字段名类型说明province_nameString省份名建议不带“省/市”后缀dateString日期格式 yyyy-MM-ddconfirmedint累计确诊数curedint累计治愈数deadint累计死亡数注意这里的 province_name 不要直接写“北京市”或“广西壮族自治区”因为 ECharts 中国地图里的标准名称是“北京”“广西”后面做地图展示时省去一次字符串替换。如果数据源里已经带了后缀那就需要写归一化映射这一点在第 3 章会单独讲。为什么直接用文件而不是数据库这类系统的单表数据量一般只有几万行全量加载到内存完全扛得住没必要为了“像正式项目”而硬上 MySQL。如果毕设题目明确要求用数据库那常见做法是 Spring Boot MyBatis MySQLSQL 初始化脚本放在项目的 sql 目录下但要注意MyBatis 在这里的价值是完成一次性的 insert 导入而不是承担复杂查询。真正的高频查询是内存聚合不是 SQL join。2.2 后端骨架Spring Boot 分层与统一返回结构后端骨架不需要复杂但分层要清楚。Controller 负责接收参数Service 负责聚合统计数据访问层退化成 DataRepository因为数据源是 CSV 而不是数据库表。三层分完之后前端永远只跟一种结构打交道——统一返回结果。下面这个 Result 类几乎在每个类似的源码包里都能看到public class ResultT { private int code; private String msg; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.code 200; r.msg ok; r.data data; return r; } public static T ResultT error(String msg) { ResultT r new Result(); r.code 500; r.msg msg; r.data null; return r; } // getter / setter 省略 }对应的实体类 CovidDaily 也直接对应 CSV 每一行public class CovidDaily { private LocalDate date; private String province; private int confirmed; private int cured; private int dead; // getter / setter 省略 }Code 字段的约定建议统一200 表示业务成功500 表示业务失败。前端拿到响应后先判断 code 再渲染图表而不是一看到 HTTP 200 就认为是成功。这样做的目的是把异常处理放到同一个拦截器里避免每个图表页面都写一遍错误分支。参数上msg 要写人可以看懂的话比如“start 不能晚于 end”方便排查问题。有些源码会把统计逻辑直接写在 Controller 里图表要换一个聚合维度就得重写接口这是最典型的反面写法。我在改这种项目时第一步就是把统计逻辑全部下沉到 ServiceController 里只留参数解析和数据透传。2.3 ECharts 与后端如何对接图表配置不该由前端写死很多人第一次接触这类项目时会有一个误解后端应该返回整个 ECharts option前端拿到后直接 setOption。这个做法看似省事实际把表现层硬编码进了后端。前端改一个图例位置、换一组颜色都要后端重新发版本。更常见的做法是后端返回“数据”前端自己组装 option。后端接口只输出图表真正需要的数组比如折线图返回时间序列地图返回姓名和数值对横向柱状图返回排名列表。前端负责把数组塞进 series.data。接口返回内容消费组件/api/trend全国累计确诊/治愈/死亡随时间变化的数据点折线图/api/map各省份现存确诊值列表中国地图/api/ranking省份确诊数 Top N 列表横向柱状图前端用什么框架都行原生 JS、Vue、React 都能接这套接口。Vue 项目里常见做法是在 mounted 里发请求拿到数据后调用 chart.setOption。这里唯一要注意的是图表实例必须等 DOM 挂载完成后再初始化否则 echarts.init 会拿到 null页面直接白屏。按这个约定做后端可以完全不知道前端用了什么框架这也是企业级数据可视化大屏的基本解耦方式。3. 把疫情数据清洗成图表能用的结构聚合统计与日期补全3.1 确诊、治愈、死亡的时间序列怎么组织原始 CSV 一般是长表格式每个省份每天一行。但折线图需要的是宽表格式横轴是日期每个指标各成一条线。所以第一步是先做聚合再把日期排好序。Java 里最合适的容器是 TreeMap它天然按 key 升序排列省去手动排序。public ListMapString, Object buildNationalTrend(ListCovidDaily all, LocalDate start, LocalDate end) { MapLocalDate, long[] dayMap new TreeMap(); for (CovidDaily d : all) { long[] values dayMap.computeIfAbsent(d.getDate(), k - new long[3]); values[0] d.getConfirmed(); values[1] d.getCured(); values[2] d.getDead(); } ListMapString, Object result new ArrayList(); for (LocalDate date start; !date.isAfter(end); date date.plusDays(1)) { long[] values dayMap.getOrDefault(date, new long[3]); MapString, Object item new LinkedHashMap(); item.put(date, date.toString()); item.put(confirmed, values[0]); item.put(cured, values[1]); item.put(dead, values[2]); result.add(item); } return result; }这段代码的逻辑有两层第一层用 computeIfAbsent 把同一日期的所有省份数据累加得到全国总量第二层从 start 遍历到 end逐天生成数据点。这里用 LinkedHashMap 是为了保证 JSON 序列化后的字段顺序是 date、confirmed、cured、dead前端取数时报错率会低很多。参数说明start 和 end 分别控制折线图的横轴范围来自前端请求参数。如果不做这个限制数据源从 2020 年 1 月到 2023 年的全部日期都会画出来x 轴会挤成一片。getOrDefault 的作用是防止 CSV 里某一天完全没有任何省份记录时出现空指针但也要注意如果是累计值缺日直接补 0 会让曲线突然掉到原点这是灾难级的视觉错误。更稳妥的做法是拿前一天的值填充后面避坑环节会展开。3.2 省级地图数据把“省份名”对齐到 ECharts 地图的坑中国地图是疫情可视化里最吸睛的部分也是坑最多的部分。坑不在图表配置在于数据里的省份名和地图 GeoJSON 里的名字对不上。比如数据源写“内蒙古自治区”ECharts 地图里叫“内蒙古”数据源写“澳门特别行政区”地图里叫“澳门”。如果对不上地图上的该省份会显示为空白并且控制台没有明显报错很多人盯着地图看半天不知道问题出在数据上。常见的做法是在后端做一次省份名归一化写成静态映射private static final MapString, String PROVINCE_ALIAS new HashMap(); static { PROVINCE_ALIAS.put(北京市, 北京); PROVINCE_ALIAS.put(天津市, 天津); PROVINCE_ALIAS.put(上海市, 上海); PROVINCE_ALIAS.put(重庆市, 重庆); PROVINCE_ALIAS.put(内蒙古自治区, 内蒙古); PROVINCE_ALIAS.put(广西壮族自治区, 广西); PROVINCE_ALIAS.put(西藏自治区, 西藏); PROVINCE_ALIAS.put(宁夏回族自治区, 宁夏); PROVINCE_ALIAS.put(新疆维吾尔自治区, 新疆); PROVINCE_ALIAS.put(香港特别行政区, 香港); PROVINCE_ALIAS.put(澳门特别行政区, 澳门); } public static String normalizeProvince(String raw) { return PROVINCE_ALIAS.getOrDefault(raw, raw); }这个映射表放后端而不是前端原因是后端统一处理一份前端所有图表都能受益。如果前端每个图表各写一套映射后面加一个“湖北”的别名都要改多个文件。另外要注意 ECharts 中国地图中有几个特殊区域北京、天津、上海、重庆四个直辖市用的是短名其余省份一般也是短名所以“XX 省”这种写法基本都要剥掉。地图注册时 registerMap 的 GeoJSON 里 name 字段决定了匹配规则数据里多一个空格都匹配不上。3.3 必调参数时间粒度、排序阈值与 null 处理数据清洗阶段有几个参数值得单独调出来讲。第一是时间粒度按日聚合还是按周聚合。按日聚合在疫情数据上很直观但波动大按周聚合更平滑适合看趋势。按周聚合不能直接用 Calendar 的默认周规则否则跨年时周数混乱。简单做法是判断日期是否为周一每周的聚合值落在该周周一的日期上。第二是排序阈值。排名图通常取 Top 10 或 Top 15但排序时相同的数值会被折叠。比如两个省份都是 1000 例排序后先后顺序每次刷新可能不同图表看起来就会“跳”。解决方式是在数值相同时按省份名的拼音顺序做二次排序保证稳定。第三是 null 处理。CSV 文件里经常有空单元格用 BufferedReader 读到后如果直接 Integer.parseInt会抛出 NumberFormatException。通常的做法是解析时把空字符串当成 0但这只适用于每日新增值对于累计值出现空缺时应当沿用前一天的累计数否则数值会从 10000 突然变成 0。更稳妥的写法是解析完成后跑一遍 fillHoles 方法按日期顺序把缺失的累计值补成前一条记录的值。这个细节很多人不做结果折线图中间莫名出现一个深坑看起来像是疫情数据回升其实是数据缺口造成的假象。4. 后端 API 设计给图表提供刚好的 JSON别让前端二次加工4.1 /api/trend、/api/map、/api/ranking 三类接口的返回结构后端接口设计的核心原则是一个图表对应一个接口接口返回的数据结构要能被 ECharts 直接消费。前端不应该做求和、过滤、排序这类事情因为 Java 后端做这些操作既快又容易测试。/api/trend返回折线图数据结构是数组每个元素包含日期和三个指标{ code: 200, msg: ok, data: [ { date: 2020-02-01, confirmed: 14380, cured: 328, dead: 304 }, { date: 2020-02-02, confirmed: 17205, cured: 475, dead: 361 } ] }/api/map返回中国地图数据结构是 name/value 数组{ code: 200, msg: ok, data: [ { name: 湖北, value: 36267 }, { name: 广东, value: 1122 } ] }/api/ranking返回横向柱状图数据只看 Top N{ code: 200, msg: ok, data: [ { name: 湖北, value: 36267 }, { name: 广东, value: 1122 } ] }三个接口的共同点data 永远是一个数组数组元素是对象而不是嵌套很深的结构。这样前端写起来就是一行 map不会出现在页面上堆一大堆 reduce 逻辑的情况。需要注意 map 接口的 data 里必须包含全部省份即使 value 是 0 也要输出否则 ECharts 地图上该省份鼠标悬停时没有 tooltip。4.2 Spring Boot Controller 代码与参数校验Controller 层只做三件事解析参数、调用 Service、包装 Result。参数校验必须在进入 Service 之前完成否则聚合逻辑跑到一半才发现日期范围非法浪费不必要的计算。RestController RequestMapping(/api) public class CovidController { private final CovidService covidService; public CovidController(CovidService covidService) { this.covidService covidService; } GetMapping(/trend) public ResultListMapString, Object trend( RequestParam(defaultValue 2020-01-01) String start, RequestParam(defaultValue 2020-12-31) String end) { LocalDate startDate LocalDate.parse(start); LocalDate endDate LocalDate.parse(end); if (startDate.isAfter(endDate)) { return Result.error(start 不能晚于 end); } return Result.ok(covidService.buildNationalTrend(startDate, endDate)); } GetMapping(/map) public ResultListProvinceValue map() { return Result.ok(covidService.buildProvinceMap()); } GetMapping(/ranking) public ResultListProvinceValue ranking( RequestParam(defaultValue 10) int top) { if (top 1 || top 50) { return Result.error(top 必须在 1-50 之间); } return Result.ok(covidService.buildProvinceRanking(top)); } }LocalDate.parse 默认解析 ISO 格式即 yyyy-MM-dd所以前端传“2020-02-01”这种字符串时不需要额外的日期格式化器这是 Spring Boot 默认就支持的。如果传进来的是时间戳那就要在全局配置里加 JsonFormat 注解但疫情大屏项目一般都用字符串日期简单直接。top 参数限制在 1 到 50 之间是为了防止有人传一个 1000 把前端横向柱状图的柱子画成一条线。参数校验失败时仍然返回 HTTP 200只是 code 变成 500原因是前端请求拦截器统一处理业务错误比处理 HTTP 状态码更省事。如果项目走严格 RESTful 风格也可以返回 400但给图表项目用会增加前端判断分支收益不大。4.3 缓存策略疫情数据全量加载一次接口只做聚合疫情数据属于典型的“低频更新、高频读取”每天的数据量有限但图表接口可能被反复请求。常见的做法是启动时把 CSV 全部加载到内存之后接口每次都基于内存数据聚合而不是重新读文件。这里给出一个用 PostConstruct 实现的加载器Component public class CovidDataHolder { private final ListCovidDaily dailyList new CopyOnWriteArrayList(); PostConstruct public void init() throws IOException { try (InputStream in getClass().getResourceAsStream(/data/covid_data.csv); BufferedReader reader new BufferedReader( new InputStreamReader(in, StandardCharsets.UTF_8))) { String line; boolean first true; while ((line reader.readLine()) ! null) { if (first) { first false; continue; } String[] parts line.split(,); CovidDaily d new CovidDaily(); d.setDate(LocalDate.parse(parts[0])); d.setProvince(normalizeProvince(parts[1])); d.setConfirmed(Integer.parseInt(parts[2].trim())); d.setCured(Integer.parseInt(parts[3].trim())); d.setDead(Integer.parseInt(parts[4].trim())); dailyList.add(d); } } } public ListCovidDaily getAll() { return dailyList; } }这段代码里最容易踩的坑有三个。第一是字符集CSV 必须用 UTF-8 读取Windows 下另存的 GBK 文件会让中文省份名变成乱码地图上所有省份都匹配不上。第二是表头while 循环里 first 变量用来跳过第一行如果没有表头就直接去掉这个判断。第三是空字符串Integer.parseInt() 会直接抛异常稳妥做法是先 trim 再判断长度长度为 0 时补 0。当并发量稍微上来一点每次请求都全量遍历几万行数据也能撑住但如果想更进一步可以用 Caffeine 对聚合结果做缓存key 是 start_end_topvalue 是聚合后的 List。这样同一个时间范围的请求第二次直接命中缓存响应时间从毫秒级降到微秒级。要注意的是 Caffeine 的 key 必须覆盖所有影响结果的参数否则会出现区间 A 的结果被返回给区间 B 的请求属于典型的缓存污染。5. ECharts 端开发排查大屏布局与五个常见问题5.1 折线图、柱状图、中国地图的 option 组织后端接口准备好后前端的工作就是把数据映射成 option。折线图最常见的是全国累计趋势前端代码大致如下fetch(/api/trend) .then(res res.json()) .then(body { if (body.code ! 200) return; const data body.data; const option { tooltip: { trigger: axis }, legend: { data: [累计确诊, 治愈, 死亡] }, xAxis: { type: category, data: data.map(d d.date) }, yAxis: { type: value, name: 人数 }, series: [ { name: 累计确诊, type: line, smooth: true, data: data.map(d d.confirmed), areaStyle: { opacity: 0.18 } }, { name: 治愈, type: line, smooth: true, data: data.map(d d.cured) }, { name: 死亡, type: line, smooth: true, data: data.map(d d.dead) } ] }; trendChart.setOption(option); });areaStyle 的 opacity 设成 0.18 而不是默认值 1目的是让面积填充色透出网格线视觉上更清爽。如果做成渐变色可以用 echarts.graphic.LinearGradient这是 ECharts 5 里柱状图设置渐变色的标准方式适用于柱状图想突出“由暗到亮”的场景。中国地图的 option 跟折线图完全是两套思路需要先用 registerMap 注册 GeoJSON再配置 visualMap 分段着色fetch(./china.json) .then(res res.json()) .then(geoJson { echarts.registerMap(china, geoJson); }); fetch(/api/map) .then(res res.json()) .then(body { const option { tooltip: { formatter: p p.name br/现存确诊 p.value }, visualMap: { min: 0, max: 6000, inRange: { color: [#e0f3f8, #fee08b, #d73027] } }, series: [{ type: map, map: china, data: body.data }] }; mapChart.setOption(option); });横向柱状图在疫情分析里常用来做省份排名也叫横向进度条效果配置核心是 yAxis 用 category 类型、xAxis 用 value 类型。这个图很适合做 Top 10 排名因为省份名称在左边纵向排列长度一目了然不会像普通柱状图那样标签重叠。5.2 地图注册与异步加载ECharts 4 与 5 的差异地图这一块最容易翻车的是版本差异。ECharts 4 时代可以直接通过引入 china.js 文件来使用中国地图很多老教程都这么写。但 ECharts 5 把地图 GeoJSON 从核心包里移除了必须自己准备一份 china.json 并调用 registerMap 注册。这段注册逻辑还必须保证在 setOption 之前完成否则 series 里写 map: china 时图表实例找不到地图数据页面显示空白。常见做法是把 fetch china.json 和 fetch 业务数据放在同一个 async 流程里用 Promise.all 并行请求等两者都返回后再初始化图表。串行请求也可以但会明显拖慢首屏。如果直接把 china.json 放在 static 目录下最简单的写法就是用 fetch 相对路径加载避免 webpack 打包时的路径问题。5.3 五个必踩的图表坑渐变、刻度、tooltip、空数据与刷新下面这五条是我在任何 ECharts 项目里都会提前检查一遍的问题。每条按现象、原因、解决描述。坑一折线图 x 轴日期挤成一团。现象是横轴刻度标签叠在一起完全看不清。原因是日期点太多ECharts 默认不会自动抽样几十个日期全部渲染当然会重叠。解决方式是给 xAxis.axisLabel 设置 interval: auto或者配合 dataZoom 做窗口缩放。dataZoom 的典型配置是 inside: true 和 start/end 百分比比如只看最近 30 天就把 start 设为 80end 设为 100。坑二柱状图上柱子的数值标签不显示。现象是柱子高度正常但柱子上方没有数字。原因不是图表坏了而是 series.label 没有开启。解决方式是写 label: { show: true, position: top }。这里要区分两个概念yAxis.axisLabel 控制坐标轴刻度的文字series.label 控制柱子上方的数值标签。很多人把两者混在一起配置结果调了半天 yAxis 没反应。坑三地图上 value 为 0 的省份没有 tooltip。现象是鼠标划过新疆、西藏这类低风险地区时弹不出提示框。原因是后端接口只返回了有数据的省份ECharts 找不到这个区域的 data 项自然不渲染 tooltip。解决方式是后端 map 接口返回全部省级行政区value 为 0 也占位。坑四第二次 setOption 后图表数据不更新。现象是第一次请求画出了曲线第二次请求换了时间段图表没反应或还残留旧曲线。原因是 setOption 默认是合并模式旧 series 数据不会被清除。解决方式是 setOption(option, true)第二个参数 notMerge 设为 true强制整体替换。坑五页面白屏控制台报 JSON 解析错误。现象是 fetch 能拿到响应但 res.json() 失败。原因是后端返回的不是纯 JSON可能是错误页 HTML也可能是接口路径不对返回了 404 页面。解决方式是先把响应状态打出来再检查 /api 接口的完整路径和端口。另外 CSV 里中文乱码也会引发类似问题重点排查后端读取文件的字符集。5.4 时间轴组件用 timeline 实现疫情的逐月演变疫情大屏通常不止一张静态图还希望用时间轴看 1 月到 6 月的动态变化。ECharts 的 timeline 组件非常适合这个需求它只需要一个 baseOption 加一个 options 数组const months [2020-01, 2020-02, 2020-03, 2020-04, 2020-05, 2020-06]; const option { timeline: { data: months, autoPlay: true, playInterval: 1500 }, baseOption: { xAxis: { type: category, data: provinceNames }, yAxis: { type: value }, series: [{ type: bar, data: [] }] }, options: months.map(month ({ series: [{ data: monthData[month] }] })) };baseOption 里放所有时间点都不变的配置比如 x 轴省份列表、y 轴名称、图例。options 数组里每个元素只写当前时间点有差异的部分比如柱子数据。playInterval 是自动播放间隔单位毫秒1500 到 2000 之间比较合适太快看不清省份名太慢让人觉得卡顿。这里有个常见问题timeline 的 data 数组长度必须和 options 数组长度一致否则最后一个时间点会没有数据。如果数据只有 5 个月而 timeline 写了 6 个点第 6 个点会显示空白或者是上一帧的残留。做动态地图时每个时间点都要生成一份完整的地图数据不要把两个月的数组混在一起。6. 让图表动起来的最小验证路径从解压到出图的四步6.1 先看目录再跑项目pom 和 static 是关键解压源码后不要急着运行先花两分钟确认三件事根目录有没有 pom.xml这决定它是 Maven 项目还是 Eclipse 普通工程src/main/resources 下有没有 csv、json、sql 这类数据文件src/main/resources/static 下有没有 index.html 或 dashboard.html。疫情分析前端页面通常放在 static 目录里由 Spring Boot 直接托管这样不存在跨域问题。没有 static 目录的话前端就得单独起一个服务还要在后端配置 CORS 过滤器。6.2 最小运行命令与验证点在项目根目录执行mvn spring-boot:run第一次运行会下载依赖耗时取决于网络。启动日志里看到 Tomcat started on port(s): 8080 之后打开浏览器访问 http://localhost:8080/index.html。如果页面能看到折线图、柱状图、中国地图同时出现说明从数据加载到接口输出全链路没问题。如果只有部分图表有数据优先打开浏览器开发者工具看 Network 面板里哪个 /api 请求返回了非 200 的 code。6.3 进阶技巧预警阈值与模拟刷新查数据方向想做得更出彩可以加一个预警阈值当日新增确诊超过某个值就把柱状图画成红色。后端返回数据时附带新增量字段前端在 series.data 里用对象形式给每个柱子单独上色series: [{ type: bar, data: ranking.map(v ({ name: v.name, value: v.value, itemStyle: { color: v.dailyIncrease 1000 ? #d73027 : #1890ff } })) }]刷新方面不一定上 WebSocket轮询就够用。前端 setInterval 每 30 秒重新请求一次接口后端在内存里拿到最新数据后直接返回图表整体替换。注意 setOption 时带上 notMerge 参数否则旧数据会和新数据叠加曲线越画越花。我最早做这个项目时把聚合逻辑写在 Controller 里图表每换一种粒度就要重写接口后来老老实实拆成 Service。第二个教训是 setOption 不带 notMerge前端页面看起来像“图表有记忆”刷新一次就多一条残影排查了很久才发现是合并模式的问题。这套方案作为毕设和面试项目完全够用真正投入生产前你还需要补上权限控制、SQL 防注入和监控告警。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑