简介:中国大学生计算机设计大赛数据可视化赛道国奖项目的完整源码包,面向备赛学生、课程设计开发者以及对前端可视化感兴趣的入门与进阶人群,可用于快速理解并复刻一套可运行的数据可视化作品。压缩包共69个文件,以53个JavaScript脚本、4个HTML页面、3个CSS样式文件为主,另含Python数据处理脚本、PyCharm工程配置和静态图片资源,整体仅683KB,目录按templates、static等模块划分,清晰易读。HTML负责页面结构,CSS负责视觉风格,JS结合ECharts完成图表渲染与交互,Python脚本承担数据读取与预处理,组成一个从数据到界面的完整链路。资源内含中国、世界、省份等多维度可视化页面,既能学习多页面数据看板的组织方式,也可借鉴获奖项目的配色、布局与交互细节,较快提升作品完整度和竞赛竞争力。目前已有1249人学习下载,值得数据可视化赛道备赛者细细拆解。
1. 国奖疫情可视化源码拆解:Flask + ECharts 的完整数据链路
很多学生作品在比赛答辩时能撑住"页面好看",但一被评委追问"数据是怎么更新的?是写死在 HTML 里还是接口拉的?"就语塞。这个拿到国三的疫情可视化项目,答案非常明确:页面是模板,数据走接口。压缩包里的05计算机设计大赛.py是 Flask 后端,templates/下四个 HTML 是前端页面,static/里放着 ECharts 和地图 GeoJSON。整套代码把"采集-存储-接口-渲染"串成了一条完整的链路。适合准备中国大学生计算机设计大赛数据可视化赛道、想从静态 HTML 跳转到前后端动态加载的选手。想改成大学生消费行为数据可视化,或者做成企业级数据可视化看板,这套源码结构也能直接套。
2. Flask 后端主程序:路由、模板渲染与数据接口的设计
2.1 为什么是这个技术栈
比赛中很多作品用的是"HTML + 一个 JSON 文件 + ECharts",这能出图,但撑不起"交互和更新"。这个项目选择 Flask 做后端,核心原因是轻。Django 的 ORM、Admin 和中间件在单机演示场景里全是负担,而 Flask 用一个入口文件就能描述完整的数据流。一个.py文件管路由、管模板渲染、管 API 返回,压缩包体积小,答辩时也好讲。前端可以只通过fetch拿数据,HTML 里不写死任何数值,这也是答辩时能讲出"动态加载"的关键依据。
2.2 解压后的目录结构与 Flask 约定
拿到 zip 解压后,你会看到这样的结构,我先标出和 Flask 关系最大的部分:
. ├── 05计算机设计大赛.py # Flask 主程序,整个后端 ├── templates/ │ ├── home.html # 首页:项目说明 + 导航 │ ├── China_Web.html # 中国地图可视化 │ ├── World_Web.html # 世界地图可视化 │ └── Province_Web.html # 省份下钻可视化 ├── static/ │ ├── js/ # echarts.js、自定义脚本 │ ├── css/ # 页面样式 │ └── images/ # 背景和封面图 └── .gitignoretemplates/和static/是 Flask 的两个约定目录:前者放render_template要渲染的 HTML,后者放浏览器直接访问的静态资源。你不需要配置任何东西,Flask 默认就会去这两个目录找文件。数据 JSON 如果没出现在目录树里,通常在static/data/下,或者在项目根目录,你解压后全局搜一下.json文件就能定位到。
2.3 主程序路由与数据接口实现
05计算机设计大赛.py的核心逻辑可以收敛成下面这段骨架代码,注释对应原项目里最基础的四个页面:
from flask import Flask, render_template, jsonify import json app = Flask(__name__) # 页面路由:渲染模板 @app.route('/') def home(): return render_template('home.html') @app.route('/china') def china(): return render_template('China_Web.html') @app.route('/world') def world(): return render_template('World_Web.html') @app.route('/province') def province(): return render_template('Province_Web.html') # 数据接口:前端从这份 JSON 里动态取数 @app.route('/api/covid/data') def covid_data(): with open('static/data/covid_data.json', 'r', encoding='utf-8') as f: data = json.load(f) return jsonify(data) if __name__ == '__main__': app.run(host='127.0.0.1', port=5050, debug=True)这里值得讲清楚三件事。第一,render_template('China_Web.html')会自动去templates/目录拼接路径,所以你写相对路径时不需要带templates/前缀。第二,jsonify(data)会把 Python 的列表和字典转成 JSON 响应,并且自动设置Content-Type: application/json,前端res.json()拿到的就是结构化数据。第三,debug=True能在开发时热重载代码,但比赛现场演示如果开启,一旦代码有改动服务会重启,可能打断演示节奏,我一般会在展示前把它改成False。
主程序里还有一处细节:app.run的host和port参数。调试时127.0.0.1:5050就够了,但如果想把屏幕投到另一台设备、或者让评委用手机访问看板,就要把host改成0.0.0.0。
| 参数 | 作用 | 建议值 |
|---|---|---|
host | 监听地址 | 本机调试用127.0.0.1,局域网演示用0.0.0.0 |
port | 服务端口 | 5000 或 5050,避开 80 和 8080 的常见占用 |
debug | 热重载与错误页 | 开发True,演示False |
2.4 数据量变大以后:从 JSON 切到 MySQL
当数据从几百条变成几万条,继续用json.load读文件会越来越慢,而且每次服务重启都要重新加载。常见做法是换成 MySQL,用 PyMySQL 做查询。如果原项目代码里带了数据库部分,写法大致是这样的:
import pymysql from pymysql.cursors import DictCursor def query_covid_data(): conn = pymysql.connect( host='localhost', user='root', password='123456', database='covid_db', charset='utf8mb4' ) with conn.cursor(DictCursor) as cursor: cursor.execute( "SELECT province, confirmed FROM covid_stats " "WHERE date = (SELECT MAX(date) FROM covid_stats)" ) rows = cursor.fetchall() conn.close() return rowsDictCursor让每行结果以字典形式返回,rows直接就能被jsonify序列化。把查询结果塞进接口后,前端代码一行都不用改,这就是接口层带来的解耦价值。你切换数据源时,只需要保证返回结构里的name和value字段名不变,ECharts 那边就能稳定渲染。
3. ECharts 数据可视化实现:三个模板如何共用一套数据接口
3.1 三张页面的定位差异
templates/里三个可视化页面不是三个独立项目,是同一套数据接口的三种展示角度。China_Web.html是全局视角,展示全国各省疫情分布;World_Web.html换成世界地图,展示各国数据;Province_Web.html是省域下钻,点击某个省看到市级数据。
三者的 ECharts 配置高度相似,最核心的差异在map参数。China_Web.html里map: 'china',World_Web.html里map: 'world',Province_Web.html里则是map: '北京'或map: 'huBei'这类注册名。地图名不是随便起的,它必须等于你用registerMap注册时给的名字,所以先看代码里地图是怎么注册的,再决定页面里写什么值。
3.2 数据接口调用与 setOption 参数
页面里的取数和渲染逻辑,我整理成一份可复用的写法:
async function renderMap(containerId, mapName) { const res = await fetch('/api/covid/data'); const data = await res.json(); const chart = echarts.init(document.getElementById(containerId)); chart.setOption({ tooltip: { trigger: 'item', formatter: (params) => `${params.name}:${params.value ?? 0} 例` }, visualMap: { min: 0, max: data.maxConfirmed || 100, text: ['高', '低'], inRange: { color: ['#e0f3f8', '#fee08b', '#d73027'] } }, series: [{ type: 'map', map: mapName, roam: true, data: data.list }] }); } renderMap('chart-main', 'china');这段代码让三个页面可以共用一个函数,差别只是传入的mapName。重点说几个参数:tooltip.formatter控制了鼠标悬停时的信息格式,params.value ?? 0防止数据缺失时显示undefined;visualMap的min和max直接决定颜色深浅的映射区间,如果最大值是 100 但你写成 1000,整个地图全部是浅色,看不出梯度;inRange.color是一个颜色数组,低值到高值从左到右渐变;series.roam开启缩放拖拽,这个在大屏演示时很实用。fetch 返回的data.list必须是[{ name: '湖北', value: 67800 }, ...]的结构,name要跟地图 GeoJSON 里的行政区域名完全一致。
3.3 地图注册顺序:这个坑比赛现场最容易踩
ECharts 5 以后,官方包不再内置中国地图和世界地图 GeoJSON,只引入echarts.min.js还不够,必须先把地图数据注册进去:
fetch('./static/js/map/china.json') .then(res => res.json()) .then(geoJson => { echarts.registerMap('china', geoJson); });registerMap的两个参数:第一个是你在series.map里引用的名字,第二个是 GeoJSON 对象。这个调用是异步的,一个常见报错是map china not exists,原因就是setOption跑在了registerMap前面。我一般会包一层async函数,先 fetch 地图,再 fetch 数据,最后setOption,保证时序。
提示:
registerMap的名字一旦定下来,页面map参数和注册名必须完全一致,大小写都不能错。比赛现场如果地图空白,优先检查这一步。
3.4 三个页面的配置差异与复用建议
把三个页面的差异放在一起对比,方便你改自己的项目:
| 页面 | map名称 | 数据维度 | 附加组件 |
|---|---|---|---|
China_Web.html | china | 各省累计确诊 | visualMap、tooltip |
World_Web.html | world | 各国累计确诊 | 按大洲过滤、dataZoom |
Province_Web.html | 省名注册的地图 | 各市/区县确诊 | 返回上一级按钮 |
如果你要复用,建议把renderMap抽到static/js/common.js,三个页面各自只保留初始化容器和地图名。静态资源走static/js路径,浏览器缓存命中率也会更高。这一层抽完,整个项目的页面代码量能砍掉一半,答辩时讲组件复用也更有说服力。
4. 从 zip 源码到可运行看板:本地复现与高频报错排查
4.1 搭建运行环境
拿到zip之后,第一步不是双击打开 HTML,而是先把后端跑起来。这个项目依赖 Python 3.8+(从压缩包里的cpython-38缓存能看出来),建议用虚拟环境隔离依赖,避免和你本机其他项目的包打架:
unzip XX可视化项目源码.zip cd 项目目录 python3 -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install flask pymysql python 05计算机设计大赛.py启动后终端会出现Running on http://127.0.0.1:5050,浏览器直接访问这个地址即可。Windows 下解压用右键菜单或Expand-Archive -Path XX.zip -DestinationPath .,后面命令除虚拟环境激活脚本路径不同外,其余一致。这里有几个容易忽略的点。第一,PyMySQL 只在代码里真的写了数据库连接时才需要装,如果原项目纯读 JSON,装 Flask 就够了。第二,__pycache__目录和05计算机设计大赛.cpython-38.pyc是 Python 运行时的字节码缓存,删除不影响任何功能,提交 zip 前建议清掉,体积更小。第三,.idea/是 PyCharm 的工作区配置,用 VS Code 或别的 IDE 不必保留。
注意:不要直接双击打开
templates/下的 HTML,页面里的fetch('/api/...')会因为没有后端服务而全部失败,必须通过 Flask 启动后访问http://127.0.0.1:5050。
4.2 高频报错定位表
复现和比赛阶段,我遇到频率最高的报错集中在这几类:
| 现象 | 直接原因 | 处理动作 |
|---|---|---|
TemplateNotFound: home.html | HTML 不在templates/目录下 | 检查文件路径,Flask 只认templates/ |
JSONDecodeError | 数据 JSON 编码不是 UTF-8 | 用open(path, encoding='utf-8')读文件 |
map china not exists | GeoJSON 没注册或注册顺序靠后 | 先registerMap再setOption |
端口被占用OSError: [Errno 48] | 5050 已被其他进程占 | 换端口或lsof -i:5050看占用进程 |
| 地图显示空白 | 数据name与 GeoJSON 行政名不一致 | 打印前后端 name 字段,逐项对齐 |
表格里第一行其实是目录约定问题,很多选手把 zip 解压后直接改了 HTML 文件名,或者把home.html移出了templates/,Flask 就会立刻报TemplateNotFound。解决方式也很简单:保持render_template里的名字和你实际文件一致,目录层级别动。第二行 JSON 编码问题在 Windows 上特别常见,记事本另存为时默认可能是 GBK,保存成 UTF-8 就能规避。Windows 上查看端口占用用netstat -ano | findstr :5050,拿到 PID 后在任务管理器里结束进程。
4.3 把项目数据换成你自己的数据
替换数据是最常见的改造,难点往往不是爬虫或导入,而是格式对齐。后端接口要返回的是:
[ { "name": "湖北", "value": 67800 }, { "name": "广东", "value": 1500 } ]其中name必须和地图 GeoJSON 里的name完全一致。比如中国地图的 GeoJSON 里通常是"湖北"而非"湖北省",如果 JSON 文件里写的是全称,地图上这个区域就会空着。遇到这种情况,我一般先写一段 Python 脚本做名称映射:
import csv import json name_map = {'湖北省': '湖北', '广东省': '广东'} # 按实际数据补全 result = [] with open('data.csv', 'r', encoding='utf-8') as f: for row in csv.DictReader(f): std_name = name_map.get(row['province'], row['province']) result.append({'name': std_name, 'value': int(row['confirmed'])}) with open('static/data/covid_data.json', 'w', encoding='utf-8') as f: json.dump({'list': result, 'maxConfirmed': max(x['value'] for x in result)}, f, ensure_ascii=False)这个脚本同时算出了maxConfirmed,前端visualMap.max就可以直接引用,不用担心颜色区间失真。运行完刷新页面,新数据就生效了,后端代码一行没动。这就是接口层设计带来的好处:数据源替换被限制在 JSON 文件和转换脚本之间,不会波及渲染层。
5. 进阶:把比赛作品改造成企业级数据可视化看板
5.1 从一次性渲染到定时增量刷新
比赛页面的一次setOption在真实业务里不够用,因为数据会持续变化。企业级数据可视化看板的常见做法是前端轮询,后端保持接口稳定不变,前端每隔一个固定周期拉取最新数据:
async function refreshChart() { const res = await fetch('/api/covid/data'); const data = await res.json(); myChart.setOption({ series: [{ data: data.list }] }); } setInterval(refreshChart, 60000);这里有一个多数教程不会讲清的细节:setOption默认是 merge 模式,你只传series.data,它会自动对比新旧数据并更新,图表不会闪,也不会打断用户正在进行的缩放。千万不要加true作为第二个参数,那是notMerge模式,会清空地图状态,导致缩放位置重置,体验很差。
5.2 接口聚合与首屏加载
另外可以把分散的接口合并成一个,减少请求次数。比如返回一个data对象,里面同时包含中国看板、世界看板的字段,前端一次请求拿回全部。接口合并之后,首屏加载时可以进一步减小白屏时间。验证刷新是否生效,最直接的办法是打开浏览器控制台的 Network 面板,筛选 XHR 请求,观察/api/covid/data这条请求是否每 60 秒出现一次。把setInterval(refreshChart, 60000)改成setInterval(refreshChart, 30000),刷新频率会立刻翻倍——你可以用这个实验直观感受轮询间隔对后端接口压力的影响。
本文还有配套的精品资源,点击获取